diff --git a/.buildkite/ftr_configs.yml b/.buildkite/ftr_configs.yml index c0205e8af28f1..be16e5f1ee408 100644 --- a/.buildkite/ftr_configs.yml +++ b/.buildkite/ftr_configs.yml @@ -7,6 +7,7 @@ disabled: - x-pack/test/functional/config.base.js - x-pack/test/detection_engine_api_integration/security_and_spaces/config.base.ts - x-pack/test/functional_enterprise_search/base_config.ts + - x-pack/test/localization/config.base.ts - test/server_integration/config.base.js # QA suites that are run out-of-band @@ -116,9 +117,13 @@ enabled: - test/server_integration/http/ssl/config.js - test/ui_capabilities/newsfeed_err/config.ts - x-pack/test/accessibility/config.ts + - x-pack/test/localization/config.ja_jp.ts + - x-pack/test/localization/config.fr_fr.ts + - x-pack/test/localization/config.zh_cn.ts - x-pack/test/alerting_api_integration/basic/config.ts - x-pack/test/alerting_api_integration/security_and_spaces/group1/config.ts - x-pack/test/alerting_api_integration/security_and_spaces/group2/config.ts + - x-pack/test/alerting_api_integration/security_and_spaces/group3/config.ts - x-pack/test/alerting_api_integration/security_and_spaces/group2/config_non_dedicated_task_runner.ts - x-pack/test/alerting_api_integration/spaces_only/config.ts - x-pack/test/api_integration_basic/config.ts @@ -133,6 +138,7 @@ enabled: - x-pack/test/cases_api_integration/security_and_spaces/config_trial.ts - x-pack/test/cases_api_integration/security_and_spaces/config_no_public_base_url.ts - x-pack/test/cases_api_integration/spaces_only/config.ts + - x-pack/test/cloud_security_posture_functional/config.ts - x-pack/test/detection_engine_api_integration/basic/config.ts - x-pack/test/detection_engine_api_integration/security_and_spaces/group1/config.ts - x-pack/test/detection_engine_api_integration/security_and_spaces/group2/config.ts @@ -155,7 +161,6 @@ enabled: - x-pack/test/functional_embedded/config.ts - x-pack/test/functional_enterprise_search/without_host_configured.config.ts - x-pack/test/functional_execution_context/config.ts - - x-pack/test/functional_synthetics/config.js - x-pack/test/functional_with_es_ssl/config.ts - x-pack/test/functional/apps/advanced_settings/config.ts - x-pack/test/functional/apps/aiops/config.ts diff --git a/.buildkite/pipeline-utils/ci-stats/pick_test_group_run_order.ts b/.buildkite/pipeline-utils/ci-stats/pick_test_group_run_order.ts index dfb384d7e3998..2468111d6933b 100644 --- a/.buildkite/pipeline-utils/ci-stats/pick_test_group_run_order.ts +++ b/.buildkite/pipeline-utils/ci-stats/pick_test_group_run_order.ts @@ -389,7 +389,7 @@ export async function pickTestGroupRunOrder() { label: 'Jest Tests', command: getRequiredEnv('JEST_UNIT_SCRIPT'), parallelism: unit.count, - timeout_in_minutes: 90, + timeout_in_minutes: 60, key: 'jest', agents: { queue: 'n2-4-spot', @@ -409,7 +409,7 @@ export async function pickTestGroupRunOrder() { label: 'Jest Integration Tests', command: getRequiredEnv('JEST_INTEGRATION_SCRIPT'), parallelism: integration.count, - timeout_in_minutes: 120, + timeout_in_minutes: 60, key: 'jest-integration', agents: { queue: 'n2-4-spot', @@ -446,7 +446,7 @@ export async function pickTestGroupRunOrder() { ({ title, key, queue = defaultQueue }): BuildkiteStep => ({ label: title, command: getRequiredEnv('FTR_CONFIGS_SCRIPT'), - timeout_in_minutes: 150, + timeout_in_minutes: 60, agents: { queue, }, diff --git a/.buildkite/scripts/pipelines/pull_request/pipeline.ts b/.buildkite/scripts/pipelines/pull_request/pipeline.ts index ca18f62c60866..dc33d470b8b50 100644 --- a/.buildkite/scripts/pipelines/pull_request/pipeline.ts +++ b/.buildkite/scripts/pipelines/pull_request/pipeline.ts @@ -56,12 +56,14 @@ const uploadPipeline = (pipelineContent: string | object) => { if ( (await doAnyChangesMatch([ + /^packages\/kbn-securitysolution-.*/, /^x-pack\/plugins\/lists/, /^x-pack\/plugins\/security_solution/, /^x-pack\/plugins\/timelines/, /^x-pack\/plugins\/triggers_actions_ui\/public\/application\/sections\/action_connector_form/, /^x-pack\/plugins\/triggers_actions_ui\/public\/application\/context\/actions_connectors_context\.tsx/, /^x-pack\/test\/security_solution_cypress/, + /^fleet_packages\.json/, // It contains reference to prebuilt detection rules, we want to run security solution tests if it changes ])) || GITHUB_PR_LABELS.includes('ci:all-cypress-suites') ) { diff --git a/.buildkite/scripts/steps/cloud/purge_deployments.ts b/.buildkite/scripts/steps/cloud/purge_deployments.ts index 3553404aeec54..a166f09b73d6a 100644 --- a/.buildkite/scripts/steps/cloud/purge_deployments.ts +++ b/.buildkite/scripts/steps/cloud/purge_deployments.ts @@ -25,16 +25,26 @@ for (const deployment of prDeployments) { const prNumber = deployment.name.match(/^kibana-pr-([0-9]+)$/)[1]; const prJson = execSync(`gh pr view '${prNumber}' --json state,labels,commits`).toString(); const pullRequest = JSON.parse(prJson); + const prOpen = pullRequest.state === 'OPEN'; const lastCommit = pullRequest.commits.slice(-1)[0]; const lastCommitTimestamp = new Date(lastCommit.committedDate).getTime() / 1000; - if (pullRequest.state !== 'OPEN') { + const persistDeployment = Boolean( + pullRequest.labels.filter((label: any) => label.name === 'ci:cloud-persist-deployment').length + ); + if (prOpen && persistDeployment) { + continue; + } + + if (!prOpen) { console.log(`Pull Request #${prNumber} is no longer open, will delete associated deployment`); deploymentsToPurge.push(deployment); } else if ( - !pullRequest.labels.filter((label: any) => - /^ci:(deploy-cloud|cloud-deploy|cloud-redeploy)$/.test(label.name) + !Boolean( + pullRequest.labels.filter((label: any) => + /^ci:(cloud-deploy|cloud-redeploy)$/.test(label.name) + ).length ) ) { console.log( diff --git a/.buildkite/scripts/steps/functional/performance_playwright.sh b/.buildkite/scripts/steps/functional/performance_playwright.sh index 5be8f62c7e01f..747dd74198102 100644 --- a/.buildkite/scripts/steps/functional/performance_playwright.sh +++ b/.buildkite/scripts/steps/functional/performance_playwright.sh @@ -7,117 +7,13 @@ source .buildkite/scripts/common/util.sh is_test_execution_step .buildkite/scripts/bootstrap.sh + # These tests are running on static workers so we have to make sure we delete previous build of Kibana rm -rf "$KIBANA_BUILD_LOCATION" .buildkite/scripts/download_build_artifacts.sh -function is_running { - kill -0 "$1" &>/dev/null -} - -# unset env vars defined in other parts of CI for automatic APM collection of -# Kibana. We manage APM config in our FTR config and performance service, and -# APM treats config in the ENV with a very high precedence. -unset ELASTIC_APM_ENVIRONMENT -unset ELASTIC_APM_TRANSACTION_SAMPLE_RATE -unset ELASTIC_APM_SERVER_URL -unset ELASTIC_APM_SECRET_TOKEN -unset ELASTIC_APM_ACTIVE -unset ELASTIC_APM_CONTEXT_PROPAGATION_ONLY -unset ELASTIC_APM_ACTIVE -unset ELASTIC_APM_SERVER_URL -unset ELASTIC_APM_SECRET_TOKEN -unset ELASTIC_APM_GLOBAL_LABELS - -# `kill $esPid` doesn't work, seems that kbn-es doesn't listen to signals correctly, this does work -trap 'killall node -q' EXIT - -export TEST_ES_URL=http://elastic:changeme@localhost:9200 -export TEST_ES_DISABLE_STARTUP=true - -echo "--- determining which journeys to run" - -journeys=$(buildkite-agent meta-data get "failed-journeys" --default '') -if [ "$journeys" != "" ]; then - echo "re-running failed journeys:${journeys}" -else - paths=() - for path in x-pack/performance/journeys/*; do - paths+=("$path") - done - journeys=$(printf "%s\n" "${paths[@]}") - echo "running discovered journeys:${journeys}" -fi - -# track failed journeys here which might get written to metadata -failedJourneys=() - -while read -r journey; do - if [ "$journey" == "" ]; then - continue; - fi - - echo "--- $journey - 🔎 Start es" - - node scripts/es snapshot& - export esPid=$! - - # Pings the es server every second for up to 2 minutes until it is green - curl \ - --fail \ - --silent \ - --retry 120 \ - --retry-delay 1 \ - --retry-connrefused \ - -XGET "${TEST_ES_URL}/_cluster/health?wait_for_nodes=>=1&wait_for_status=yellow" \ - > /dev/null - - echo "✅ ES is ready and will run in the background" - - phases=("WARMUP" "TEST") - status=0 - for phase in "${phases[@]}"; do - echo "--- $journey - $phase" - - export TEST_PERFORMANCE_PHASE="$phase" - - set +e - node scripts/functional_tests \ - --config "$journey" \ - --kibana-install-dir "$KIBANA_BUILD_LOCATION" \ - --debug \ - --bail - status=$? - set -e - - if [ $status -ne 0 ]; then - failedJourneys+=("$journey") - echo "^^^ +++" - echo "❌ FTR failed with status code: $status" - break - fi - done - - # remove trap, we're manually shutting down - trap - EXIT; - - echo "--- $journey - 🔎 Shutdown ES" - killall node - echo "waiting for $esPid to exit gracefully"; - - timeout=30 #seconds - dur=0 - while is_running $esPid; do - sleep 1; - ((dur=dur+1)) - if [ $dur -ge $timeout ]; then - echo "es still running after $dur seconds, killing ES and node forcefully"; - killall -SIGKILL java - killall -SIGKILL node - sleep 5; - fi - done -done <<< "$journeys" +echo "--- Running performance tests" +node scripts/run_performance.js --kibana-install-dir "$KIBANA_BUILD_LOCATION" echo "--- Upload journey step screenshots" JOURNEY_SCREENSHOTS_DIR="${KIBANA_DIR}/data/journey_screenshots" @@ -126,11 +22,3 @@ if [ -d "$JOURNEY_SCREENSHOTS_DIR" ]; then buildkite-agent artifact upload "**/*fullscreen*.png" cd "$KIBANA_DIR" fi - -echo "--- report/record failed journeys" -if [ "${failedJourneys[*]}" != "" ]; then - buildkite-agent meta-data set "failed-journeys" "$(printf "%s\n" "${failedJourneys[@]}")" - - echo "failed journeys: ${failedJourneys[*]}" - exit 1 -fi diff --git a/.buildkite/scripts/steps/scalability/benchmarking.sh b/.buildkite/scripts/steps/scalability/benchmarking.sh index 3fbf1896d1877..e47e0bc10a3c5 100755 --- a/.buildkite/scripts/steps/scalability/benchmarking.sh +++ b/.buildkite/scripts/steps/scalability/benchmarking.sh @@ -74,53 +74,9 @@ download_artifacts echo "--- Clone kibana-load-testing repo and compile project" checkout_and_compile_load_runner -echo "--- Run Scalability Tests with Elasticsearch started only once and Kibana restart before each journey" +echo "--- Run Scalability Tests" cd "$KIBANA_DIR" -node scripts/es snapshot& - -esPid=$! -# Set trap on EXIT to stop Elasticsearch process -trap "kill -9 $esPid" EXIT - -# unset env vars defined in other parts of CI for automatic APM collection of -# Kibana. We manage APM config in our FTR config and performance service, and -# APM treats config in the ENV with a very high precedence. -unset ELASTIC_APM_ENVIRONMENT -unset ELASTIC_APM_TRANSACTION_SAMPLE_RATE -unset ELASTIC_APM_SERVER_URL -unset ELASTIC_APM_SECRET_TOKEN -unset ELASTIC_APM_ACTIVE -unset ELASTIC_APM_CONTEXT_PROPAGATION_ONLY -unset ELASTIC_APM_GLOBAL_LABELS -unset ELASTIC_APM_MAX_QUEUE_SIZE -unset ELASTIC_APM_METRICS_INTERVAL -unset ELASTIC_APM_CAPTURE_SPAN_STACK_TRACES -unset ELASTIC_APM_BREAKDOWN_METRICS - - -export TEST_ES_DISABLE_STARTUP=true -ES_HOST="localhost:9200" -export TEST_ES_URL="http://elastic:changeme@${ES_HOST}" -# Overriding Gatling default configuration -export ES_URL="http://${ES_HOST}" - -# Pings the ES server every second for 2 mins until its status is green -curl --retry 120 \ - --retry-delay 1 \ - --retry-connrefused \ - -I -XGET "${TEST_ES_URL}/_cluster/health?wait_for_nodes=>=1&wait_for_status=yellow" - -export ELASTIC_APM_ACTIVE=true - -for journey in scalability_traces/server/*; do - export SCALABILITY_JOURNEY_PATH="$KIBANA_DIR/$journey" - echo "--- Run scalability file: $SCALABILITY_JOURNEY_PATH" - node scripts/functional_tests \ - --config x-pack/test/scalability/config.ts \ - --kibana-install-dir "$KIBANA_BUILD_LOCATION" \ - --logToFile \ - --debug -done +node scripts/run_scalability --kibana-install-dir "$KIBANA_BUILD_LOCATION" --journey-config-path "scalability_traces/server" echo "--- Upload test results" upload_test_results diff --git a/.buildkite/scripts/steps/storybooks/build_and_upload.ts b/.buildkite/scripts/steps/storybooks/build_and_upload.ts index cbd746f899513..e315c7fbcceba 100644 --- a/.buildkite/scripts/steps/storybooks/build_and_upload.ts +++ b/.buildkite/scripts/steps/storybooks/build_and_upload.ts @@ -31,7 +31,6 @@ const STORYBOOKS = [ 'expression_reveal_image', 'expression_shape', 'expression_tagcloud', - 'files', 'fleet', 'home', 'infra', diff --git a/.buildkite/scripts/steps/test/jest_parallel.sh b/.buildkite/scripts/steps/test/jest_parallel.sh index dfaa2db079dd8..8a7f94157d61b 100755 --- a/.buildkite/scripts/steps/test/jest_parallel.sh +++ b/.buildkite/scripts/steps/test/jest_parallel.sh @@ -26,6 +26,13 @@ echo "--- downloading jest test run order" download_artifact jest_run_order.json . configs=$(jq -r 'getpath([env.TEST_TYPE]) | .groups[env.JOB | tonumber].names | .[]' jest_run_order.json) +echo "+++ ⚠️ WARNING ⚠️" +echo " + console.log(), console.warn(), and console.error() output in jest tests causes a massive amount + of noise on CI without any percevable benefit, so they have been disabled. If you want to log + output in your test temporarily, you can modify 'packages/kbn-test/src/jest/setup/disable_console_logs.js' +" + while read -r config; do echo "--- $ node scripts/jest --config $config" diff --git a/.eslintignore b/.eslintignore index 66aca11d9dba8..2e7bee2f74f32 100644 --- a/.eslintignore +++ b/.eslintignore @@ -39,7 +39,7 @@ snapshots.js /packages/kbn-test/src/functional_test_runner/lib/config/__tests__/fixtures/ /packages/kbn-ui-framework/dist /packages/kbn-flot-charts/lib -/packages/kbn-monaco/src/painless/antlr +/packages/kbn-monaco/src/**/antlr # Bazel /bazel-* diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 253dd86879dda..77ad86f2313e7 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -12,6 +12,7 @@ /src/plugins/discover/ @elastic/kibana-data-discovery /src/plugins/saved_search/ @elastic/kibana-data-discovery /x-pack/plugins/discover_enhanced/ @elastic/kibana-data-discovery +/x-pack/test/functional/apps/discover/ @elastic/kibana-data-discovery /test/functional/apps/discover/ @elastic/kibana-data-discovery /test/functional/apps/context/ @elastic/kibana-data-discovery /test/api_integration/apis/unified_field_list/ @elastic/kibana-data-discovery @@ -22,6 +23,14 @@ /src/plugins/data_view_editor/ @elastic/kibana-data-discovery /src/plugins/data_view_field_editor/ @elastic/kibana-data-discovery /src/plugins/data_view_management/ @elastic/kibana-data-discovery +/src/plugins/data/ @elastic/kibana-visualizations @elastic/kibana-data-discovery +/src/plugins/field_formats/ @elastic/kibana-data-discovery +/x-pack/test/search_sessions_integration/ @elastic/kibana-data-discovery +/test/plugin_functional/test_suites/data_plugin @elastic/kibana-data-discovery +/examples/field_formats_example/ @elastic/kibana-data-discovery +/examples/partial_results_example/ @elastic/kibana-data-discovery +/examples/search_examples/ @elastic/kibana-data-discovery +/examples/demo_search/ @elastic/kibana-data-discovery # Vis Editors /x-pack/plugins/lens/ @elastic/kibana-visualizations @@ -55,29 +64,6 @@ /x-pack/plugins/graph/ @elastic/kibana-visualizations /x-pack/test/functional/apps/graph @elastic/kibana-visualizations -# Application Services -/examples/dashboard_embeddable_examples/ @elastic/kibana-app-services -/examples/demo_search/ @elastic/kibana-app-services -/examples/developer_examples/ @elastic/kibana-app-services -/examples/embeddable_examples/ @elastic/kibana-app-services -/examples/embeddable_explorer/ @elastic/kibana-app-services -/examples/field_formats_example/ @elastic/kibana-app-services -/examples/partial_results_example/ @elastic/kibana-app-services -/examples/search_examples/ @elastic/kibana-app-services -/src/plugins/data/ @elastic/kibana-visualizations @elastic/kibana-data-discovery -/src/plugins/embeddable/ @elastic/kibana-app-services -/src/plugins/field_formats/ @elastic/kibana-app-services -/src/plugins/inspector/ @elastic/kibana-app-services -/src/plugins/kibana_utils/ @elastic/kibana-app-services -/src/plugins/navigation/ @elastic/kibana-app-services -/src/plugins/inspector/ @elastic/kibana-app-services -/x-pack/plugins/embeddable_enhanced/ @elastic/kibana-app-services -/x-pack/plugins/runtime_fields @elastic/kibana-app-services -/src/plugins/dashboard/public/application/embeddable/viewport/print_media @elastic/kibana-app-services -/x-pack/test/search_sessions_integration/ @elastic/kibana-app-services -/test/plugin_functional/test_suites/panel_actions @elastic/kibana-app-services -/test/plugin_functional/test_suites/data_plugin @elastic/kibana-app-services - # Global Experience /src/plugins/bfetch/ @elastic/kibana-global-experience @@ -86,7 +72,7 @@ /src/plugins/share/ @elastic/kibana-global-experience /src/plugins/ui_actions/ @elastic/kibana-global-experience /src/plugins/ui_actions_enhanced/ @elastic/kibana-global-experience - +/src/plugins/navigation/ @elastic/kibana-global-experience /x-pack/plugins/notifications/ @elastic/kibana-global-experience ## Examples @@ -95,6 +81,7 @@ /examples/state_containers_examples/ @elastic/kibana-global-experience /examples/ui_action_examples/ @elastic/kibana-global-experience /examples/ui_actions_explorer/ @elastic/kibana-global-experience +/examples/developer_examples/ @elastic/kibana-global-experience /x-pack/examples/ui_actions_enhanced_examples/ @elastic/kibana-global-experience ### Overview Plugin and Packages @@ -119,7 +106,7 @@ /docs/setup/configuring-reporting.asciidoc @elastic/kibana-global-experience ### Global Experience Tagging -/src/plugins/saved_objects_tagging_oss @elastic/kibana-global-experience +/src/plugins/saved_objects_tagging_oss @elastic/kibana-global-experience /x-pack/plugins/saved_objects_tagging/ @elastic/kibana-global-experience /x-pack/test/saved_object_tagging/ @elastic/kibana-global-experience @@ -151,7 +138,7 @@ # Home/Overview/Landing Pages /x-pack/plugins/observability/public/pages/home @elastic/observability-design /x-pack/plugins/observability/public/pages/landing @elastic/observability-design -/x-pack/plugins/observability/public/pages/overview @elastic/observability-design +/x-pack/plugins/observability/public/pages/overview @elastic/observability-design @elastic/actionable-observability # Actionable Observability /x-pack/plugins/observability/common/rules @elastic/actionable-observability @@ -179,7 +166,7 @@ /x-pack/test/fleet_api_integration @elastic/fleet /x-pack/test/fleet_cypress @elastic/fleet /x-pack/test/fleet_functional @elastic/fleet -/src/dev/build/tasks/bundle_fleet_packages.ts +/src/dev/build/tasks/bundle_fleet_packages.ts @elastic/fleet @elastic/kibana-operations # APM /x-pack/plugins/apm/ @elastic/apm-ui @@ -234,7 +221,14 @@ /test/functional/services/dashboard/ @elastic/kibana-presentation /x-pack/plugins/canvas/ @elastic/kibana-presentation /x-pack/plugins/dashboard_enhanced/ @elastic/kibana-presentation -/x-pack/test/functional/apps/canvas/ @elastic/kibana-presentation +/x-pack/test/functional/apps/canvas/ @elastic/kibana-presentation +/examples/dashboard_embeddable_examples/ @elastic/kibana-presentation +/examples/embeddable_examples/ @elastic/kibana-presentation +/examples/embeddable_explorer/ @elastic/kibana-presentation +/src/plugins/embeddable/ @elastic/kibana-presentation +/src/plugins/inspector/ @elastic/kibana-presentation +/x-pack/plugins/embeddable_enhanced/ @elastic/kibana-presentation +/test/plugin_functional/test_suites/panel_actions @elastic/kibana-presentation #CC# /src/plugins/kibana_react/public/code_editor/ @elastic/kibana-presentation # Machine Learning @@ -416,7 +410,8 @@ /x-pack/plugins/cross_cluster_replication/ @elastic/platform-deployment-management /x-pack/plugins/index_lifecycle_management/ @elastic/platform-deployment-management /x-pack/plugins/grokdebugger/ @elastic/platform-deployment-management -/x-pack/plugins/index_management/ @elastic/platform-deployment-management +/x-pack/plugins/index_management/ @elastic/platform-deployment-management +/x-pack/plugins/runtime_fields @elastic/platform-deployment-management /x-pack/plugins/license_api_guard/ @elastic/platform-deployment-management /x-pack/plugins/license_management/ @elastic/platform-deployment-management /x-pack/plugins/painless_lab/ @elastic/platform-deployment-management @@ -647,11 +642,6 @@ x-pack/plugins/threat_intelligence @elastic/protections-experience x-pack/plugins/security_solution/public/threat_intelligence @elastic/protections-experience x-pack/test/threat_intelligence_cypress @elastic/protections-experience - -# Security Intelligence And Analytics -/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/content/prepackaged_rules @elastic/security-intelligence-analytics - - # Security Asset Management /x-pack/plugins/osquery @elastic/security-asset-management /x-pack/plugins/security_solution/common/detection_engine/rule_response_actions @elastic/security-asset-management @@ -662,6 +652,8 @@ x-pack/test/threat_intelligence_cypress @elastic/protections-experience /x-pack/plugins/cloud_security_posture/ @elastic/kibana-cloud-security-posture /x-pack/plugins/security_solution/public/cloud_security_posture @elastic/kibana-cloud-security-posture /x-pack/test/api_integration/apis/cloud_security_posture/ @elastic/kibana-cloud-security-posture +/x-pack/test/cloud_security_posture_functional/ @elastic/kibana-cloud-security-posture + # Security Solution onboarding tour /x-pack/plugins/security_solution/public/common/components/guided_onboarding @elastic/security-threat-hunting-explore @@ -956,7 +948,7 @@ packages/kbn-logging-mocks @elastic/kibana-core packages/kbn-managed-vscode-config @elastic/kibana-operations packages/kbn-managed-vscode-config-cli @elastic/kibana-operations packages/kbn-mapbox-gl @elastic/kibana-gis -packages/kbn-monaco @elastic/kibana-app-services +packages/kbn-monaco @elastic/kibana-global-experience packages/kbn-optimizer @elastic/kibana-operations packages/kbn-optimizer-webpack-helpers @elastic/kibana-operations packages/kbn-osquery-io-ts-types @elastic/security-asset-management @@ -969,6 +961,7 @@ packages/kbn-plugin-helpers @elastic/kibana-operations packages/kbn-react-field @elastic/kibana-app-services packages/kbn-repo-source-classifier @elastic/kibana-operations packages/kbn-repo-source-classifier-cli @elastic/kibana-operations +packages/kbn-rison @elastic/kibana-operations packages/kbn-rule-data-utils @elastic/security-detections-response @elastic/actionable-observability @elastic/response-ops packages/kbn-safer-lodash-set @elastic/kibana-security packages/kbn-securitysolution-autocomplete @elastic/security-solution-platform @@ -1026,9 +1019,13 @@ packages/shared-ux/button/exit_full_screen/types @elastic/kibana-global-experien packages/shared-ux/card/no_data/impl @elastic/kibana-global-experience packages/shared-ux/card/no_data/mocks @elastic/kibana-global-experience packages/shared-ux/card/no_data/types @elastic/kibana-global-experience +packages/shared-ux/file/context @elastic/kibana-global-experience +packages/shared-ux/file/file_picker/impl @elastic/kibana-global-experience +packages/shared-ux/file/file_upload/impl @elastic/kibana-global-experience packages/shared-ux/file/image/impl @elastic/kibana-global-experience packages/shared-ux/file/image/mocks @elastic/kibana-global-experience -packages/shared-ux/file/image/types @elastic/kibana-global-experience +packages/shared-ux/file/mocks @elastic/kibana-global-experience +packages/shared-ux/file/types @elastic/kibana-global-experience packages/shared-ux/file/util @elastic/kibana-global-experience packages/shared-ux/link/redirect_app/impl @elastic/kibana-global-experience packages/shared-ux/link/redirect_app/mocks @elastic/kibana-global-experience @@ -1055,6 +1052,7 @@ packages/shared-ux/page/solution_nav @elastic/kibana-global-experience packages/shared-ux/prompt/no_data_views/impl @elastic/kibana-global-experience packages/shared-ux/prompt/no_data_views/mocks @elastic/kibana-global-experience packages/shared-ux/prompt/no_data_views/types @elastic/kibana-global-experience +packages/shared-ux/prompt/not_found @elastic/kibana-global-experience packages/shared-ux/router/impl @elastic/kibana-global-experience packages/shared-ux/router/mocks @elastic/kibana-global-experience packages/shared-ux/router/types @elastic/kibana-global-experience diff --git a/NOTICE.txt b/NOTICE.txt index 2f2d5bf318de2..adbd050e0b7f6 100644 --- a/NOTICE.txt +++ b/NOTICE.txt @@ -161,70 +161,6 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Detection Rules -Copyright 2021 Elasticsearch B.V. - ---- -This product bundles rules based on https://github.com/BlueTeamLabs/sentinel-attack -which is available under a "MIT" license. The rules based on this license are: - -- "Potential Evasion via Filter Manager" (06dceabf-adca-48af-ac79-ffdf4c3b1e9a) -- "Process Discovery via Tasklist" (cc16f774-59f9-462d-8b98-d27ccd4519ec) -- "Potential Modification of Accessibility Binaries" (7405ddf1-6c8e-41ce-818f-48bea6bcaed8) -- "Potential Application Shimming via Sdbinst" (fd4a992d-6130-4802-9ff8-829b89ae801f) -- "Trusted Developer Application Usage" (9d110cb3-5f4b-4c9a-b9f5-53f0a1707ae1) - -MIT License - -Copyright (c) 2019 Edoardo Gerosa, Olaf Hartong - -Permission is hereby granted, free of charge, to any person obtaining a copy of -this software and associated documentation files (the "Software"), to deal in -the Software without restriction, including without limitation the rights to -use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies -of the Software, and to permit persons to whom the Software is furnished to do -so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - ---- -This product bundles rules based on https://github.com/FSecureLABS/leonidas -which is available under a "MIT" license. The rules based on this license are: - -- "AWS Access Secret in Secrets Manager" (a00681e3-9ed6-447c-ab2c-be648821c622) - -MIT License - -Copyright (c) 2020 F-Secure LABS - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - --- MIT License diff --git a/api_docs/actions.devdocs.json b/api_docs/actions.devdocs.json index daf7786bf9b0c..22b480c048a51 100644 --- a/api_docs/actions.devdocs.json +++ b/api_docs/actions.devdocs.json @@ -1967,7 +1967,7 @@ }, "<", "ActionTypeConfig", - ">[]>; getOAuthAccessToken: ({ type, options }: Readonly<{} & { options: Readonly<{} & { config: Readonly<{} & { clientId: string; jwtKeyId: string; userIdentifierValue: string; }>; tokenUrl: string; secrets: Readonly<{ privateKeyPassword?: string | undefined; } & { clientSecret: string; privateKey: string; }>; }> | Readonly<{} & { scope: string; config: Readonly<{} & { clientId: string; tenantId: string; }>; tokenUrl: string; secrets: Readonly<{} & { clientSecret: string; }>; }>; type: \"client\" | \"jwt\"; }>, configurationUtilities: ", + ">[]>; getOAuthAccessToken: ({ type, options }: Readonly<{} & { options: Readonly<{} & { config: Readonly<{} & { clientId: string; jwtKeyId: string; userIdentifierValue: string; }>; tokenUrl: string; secrets: Readonly<{ privateKeyPassword?: string | undefined; } & { clientSecret: string; privateKey: string; }>; }> | Readonly<{} & { scope: string; config: Readonly<{} & { clientId: string; tenantId: string; }>; tokenUrl: string; secrets: Readonly<{} & { clientSecret: string; }>; }>; type: \"jwt\" | \"client\"; }>, configurationUtilities: ", "ActionsConfigurationUtilities", ") => Promise<{ accessToken: string | null; }>; enqueueExecution: (options: ", "ExecuteOptions", diff --git a/api_docs/actions.mdx b/api_docs/actions.mdx index a0e582a7e3fca..bc3dbbe17a621 100644 --- a/api_docs/actions.mdx +++ b/api_docs/actions.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/actions title: "actions" image: https://source.unsplash.com/400x175/?github description: API docs for the actions plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'actions'] --- import actionsObj from './actions.devdocs.json'; diff --git a/api_docs/advanced_settings.mdx b/api_docs/advanced_settings.mdx index afdcfde2d47cb..0706f53b99d87 100644 --- a/api_docs/advanced_settings.mdx +++ b/api_docs/advanced_settings.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/advancedSettings title: "advancedSettings" image: https://source.unsplash.com/400x175/?github description: API docs for the advancedSettings plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'advancedSettings'] --- import advancedSettingsObj from './advanced_settings.devdocs.json'; diff --git a/api_docs/aiops.devdocs.json b/api_docs/aiops.devdocs.json index 6ee28ad0d4fa6..ce17371ced270 100644 --- a/api_docs/aiops.devdocs.json +++ b/api_docs/aiops.devdocs.json @@ -203,7 +203,7 @@ "label": "CHANGE_POINT_DETECTION_ENABLED", "description": [], "signature": [ - "false" + "true" ], "path": "x-pack/plugins/aiops/common/index.ts", "deprecated": false, diff --git a/api_docs/aiops.mdx b/api_docs/aiops.mdx index 66ca5900faf75..b6c99301917c3 100644 --- a/api_docs/aiops.mdx +++ b/api_docs/aiops.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/aiops title: "aiops" image: https://source.unsplash.com/400x175/?github description: API docs for the aiops plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'aiops'] --- import aiopsObj from './aiops.devdocs.json'; diff --git a/api_docs/alerting.devdocs.json b/api_docs/alerting.devdocs.json index 9ad42cdce240f..409d2c7cabf6a 100644 --- a/api_docs/alerting.devdocs.json +++ b/api_docs/alerting.devdocs.json @@ -1186,7 +1186,7 @@ }, "" ], - "path": "x-pack/plugins/alerting/server/rules_client/rules_client.ts", + "path": "x-pack/plugins/alerting/server/rules_client/methods/bulk_edit.ts", "deprecated": false, "trackAdoption": false, "children": [ @@ -1208,7 +1208,7 @@ }, " | undefined" ], - "path": "x-pack/plugins/alerting/server/rules_client/rules_client.ts", + "path": "x-pack/plugins/alerting/server/rules_client/methods/bulk_edit.ts", "deprecated": false, "trackAdoption": false }, @@ -1229,7 +1229,7 @@ }, "[]" ], - "path": "x-pack/plugins/alerting/server/rules_client/rules_client.ts", + "path": "x-pack/plugins/alerting/server/rules_client/methods/bulk_edit.ts", "deprecated": false, "trackAdoption": false }, @@ -1243,7 +1243,7 @@ "signature": [ "RuleParamsModifier | undefined" ], - "path": "x-pack/plugins/alerting/server/rules_client/rules_client.ts", + "path": "x-pack/plugins/alerting/server/rules_client/methods/bulk_edit.ts", "deprecated": false, "trackAdoption": false } @@ -1267,7 +1267,7 @@ }, "" ], - "path": "x-pack/plugins/alerting/server/rules_client/rules_client.ts", + "path": "x-pack/plugins/alerting/server/rules_client/methods/bulk_edit.ts", "deprecated": false, "trackAdoption": false, "children": [ @@ -1281,7 +1281,7 @@ "signature": [ "string[]" ], - "path": "x-pack/plugins/alerting/server/rules_client/rules_client.ts", + "path": "x-pack/plugins/alerting/server/rules_client/methods/bulk_edit.ts", "deprecated": false, "trackAdoption": false }, @@ -1302,7 +1302,7 @@ }, "[]" ], - "path": "x-pack/plugins/alerting/server/rules_client/rules_client.ts", + "path": "x-pack/plugins/alerting/server/rules_client/methods/bulk_edit.ts", "deprecated": false, "trackAdoption": false }, @@ -1316,7 +1316,7 @@ "signature": [ "RuleParamsModifier | undefined" ], - "path": "x-pack/plugins/alerting/server/rules_client/rules_client.ts", + "path": "x-pack/plugins/alerting/server/rules_client/methods/bulk_edit.ts", "deprecated": false, "trackAdoption": false } @@ -1330,7 +1330,7 @@ "tags": [], "label": "BulkOperationError", "description": [], - "path": "x-pack/plugins/alerting/server/rules_client/rules_client.ts", + "path": "x-pack/plugins/alerting/server/rules_client/types.ts", "deprecated": false, "trackAdoption": false, "children": [ @@ -1341,7 +1341,7 @@ "tags": [], "label": "message", "description": [], - "path": "x-pack/plugins/alerting/server/rules_client/rules_client.ts", + "path": "x-pack/plugins/alerting/server/rules_client/types.ts", "deprecated": false, "trackAdoption": false }, @@ -1355,7 +1355,7 @@ "signature": [ "number | undefined" ], - "path": "x-pack/plugins/alerting/server/rules_client/rules_client.ts", + "path": "x-pack/plugins/alerting/server/rules_client/types.ts", "deprecated": false, "trackAdoption": false }, @@ -1369,7 +1369,7 @@ "signature": [ "{ id: string; name: string; }" ], - "path": "x-pack/plugins/alerting/server/rules_client/rules_client.ts", + "path": "x-pack/plugins/alerting/server/rules_client/types.ts", "deprecated": false, "trackAdoption": false } @@ -1393,7 +1393,7 @@ }, "" ], - "path": "x-pack/plugins/alerting/server/rules_client/rules_client.ts", + "path": "x-pack/plugins/alerting/server/rules_client/methods/find.ts", "deprecated": false, "trackAdoption": false, "children": [ @@ -1404,7 +1404,7 @@ "tags": [], "label": "page", "description": [], - "path": "x-pack/plugins/alerting/server/rules_client/rules_client.ts", + "path": "x-pack/plugins/alerting/server/rules_client/methods/find.ts", "deprecated": false, "trackAdoption": false }, @@ -1415,7 +1415,7 @@ "tags": [], "label": "perPage", "description": [], - "path": "x-pack/plugins/alerting/server/rules_client/rules_client.ts", + "path": "x-pack/plugins/alerting/server/rules_client/methods/find.ts", "deprecated": false, "trackAdoption": false }, @@ -1426,7 +1426,7 @@ "tags": [], "label": "total", "description": [], - "path": "x-pack/plugins/alerting/server/rules_client/rules_client.ts", + "path": "x-pack/plugins/alerting/server/rules_client/methods/find.ts", "deprecated": false, "trackAdoption": false }, @@ -1447,7 +1447,7 @@ }, "[]" ], - "path": "x-pack/plugins/alerting/server/rules_client/rules_client.ts", + "path": "x-pack/plugins/alerting/server/rules_client/methods/find.ts", "deprecated": false, "trackAdoption": false } @@ -2753,7 +2753,9 @@ "label": "BulkEditOperation", "description": [], "signature": [ - "{ operation: \"delete\" | \"set\" | \"add\"; field: \"tags\"; value: string[]; } | { operation: \"set\" | \"add\"; field: \"actions\"; value: NormalizedAlertAction[]; } | { operation: \"set\"; field: \"schedule\"; value: ", + "{ operation: \"delete\" | \"set\" | \"add\"; field: \"tags\"; value: string[]; } | { operation: \"set\" | \"add\"; field: \"actions\"; value: ", + "NormalizedAlertAction", + "[]; } | { operation: \"set\"; field: \"schedule\"; value: ", { "pluginId": "alerting", "scope": "common", @@ -2771,7 +2773,7 @@ }, "; } | { operation: \"delete\"; field: \"snoozeSchedule\"; value?: string[] | undefined; } | { operation: \"set\"; field: \"apiKey\"; value?: undefined; }" ], - "path": "x-pack/plugins/alerting/server/rules_client/rules_client.ts", + "path": "x-pack/plugins/alerting/server/rules_client/methods/bulk_edit.ts", "deprecated": false, "trackAdoption": false, "initialIsOpen": false @@ -2801,7 +2803,7 @@ }, "" ], - "path": "x-pack/plugins/alerting/server/rules_client/rules_client.ts", + "path": "x-pack/plugins/alerting/server/rules_client/methods/bulk_edit.ts", "deprecated": false, "trackAdoption": false, "initialIsOpen": false @@ -2910,7 +2912,9 @@ "section": "def-common.RuleTypeParams", "text": "RuleTypeParams" }, - " = never>({ id, includeLegacyId, includeSnoozeData, excludeFromPublicApi, }: { id: string; includeLegacyId?: boolean | undefined; includeSnoozeData?: boolean | undefined; excludeFromPublicApi?: boolean | undefined; }) => Promise<", + " = never>(params: ", + "GetParams", + ") => Promise<", { "pluginId": "alerting", "scope": "common", @@ -2920,9 +2924,9 @@ }, " | ", "SanitizedRuleWithLegacyId", - ">; delete: ({ id }: { id: string; }) => Promise<{}>; aggregate: ({ options: { fields, filter, ...options }, }?: { options?: ", + ">; delete: (params: { id: string; }) => Promise<{}>; aggregate: (params?: { options?: ", "AggregateOptions", - " | undefined; }) => Promise<", + " | undefined; } | undefined) => Promise<", "AggregateResult", ">; create: ({ data, options, }: ", + " = never>(params: ", "CreateOptions", ") => Promise<", { @@ -2950,9 +2954,9 @@ "section": "def-common.RuleTypeParams", "text": "RuleTypeParams" }, - " = never>({ options: { fields, ...options }, excludeFromPublicApi, includeSnoozeData, }?: { options?: ", - "FindOptions", - " | undefined; excludeFromPublicApi?: boolean | undefined; includeSnoozeData?: boolean | undefined; }) => Promise<", + " = never>(params?: ", + "FindParams", + " | undefined) => Promise<", { "pluginId": "alerting", "scope": "server", @@ -2968,7 +2972,7 @@ "section": "def-common.RuleTypeParams", "text": "RuleTypeParams" }, - " = never>({ id, data, }: ", + " = never>(params: ", "UpdateOptions", ") => Promise<", { @@ -2986,7 +2990,9 @@ "section": "def-common.RuleTypeParams", "text": "RuleTypeParams" }, - " = never>({ id, includeLegacyId, includeSnoozeData, }: { id: string; includeLegacyId?: boolean | undefined; includeSnoozeData?: boolean | undefined; }) => Promise<", + " = never>(params: ", + "ResolveParams", + ") => Promise<", { "pluginId": "alerting", "scope": "common", @@ -2994,7 +3000,7 @@ "section": "def-common.ResolvedSanitizedRule", "text": "ResolvedSanitizedRule" }, - ">; enable: ({ id }: { id: string; }) => Promise; disable: ({ id }: { id: string; }) => Promise; clone: >; enable: (options: { id: string; }) => Promise; disable: (options: { id: string; }) => Promise; clone: (id: string, { newId }: { newId?: string | undefined; }) => Promise<", + " = never>(args_0: string, args_1: { newId?: string | undefined; }) => Promise<", { "pluginId": "alerting", "scope": "common", @@ -3010,7 +3016,9 @@ "section": "def-common.SanitizedRule", "text": "SanitizedRule" }, - ">; muteAll: ({ id }: { id: string; }) => Promise; getAlertState: ({ id }: { id: string; }) => Promise; getAlertSummary: ({ id, dateStart, numberOfExecutions, }: ", + ">; muteAll: (options: { id: string; }) => Promise; getAlertState: (params: ", + "GetAlertStateParams", + ") => Promise; getAlertSummary: (params: ", "GetAlertSummaryParams", ") => Promise<", { @@ -3020,7 +3028,7 @@ "section": "def-common.AlertSummary", "text": "AlertSummary" }, - ">; getExecutionLogForRule: ({ id, dateStart, dateEnd, filter, page, perPage, sort, }: ", + ">; getExecutionLogForRule: (params: ", "GetExecutionLogByIdParams", ") => Promise<", { @@ -3030,7 +3038,7 @@ "section": "def-common.IExecutionLogResult", "text": "IExecutionLogResult" }, - ">; getGlobalExecutionLogWithAuth: ({ dateStart, dateEnd, filter, page, perPage, sort, namespaces, }: ", + ">; getGlobalExecutionLogWithAuth: (params: ", "GetGlobalExecutionLogParams", ") => Promise<", { @@ -3040,7 +3048,11 @@ "section": "def-common.IExecutionLogResult", "text": "IExecutionLogResult" }, - ">; getActionErrorLog: ({ id, dateStart, dateEnd, filter, page, perPage, sort, }: ", + ">; getRuleExecutionKPI: (params: ", + "GetRuleExecutionKPIParams", + ") => Promise<{ success: number; unknown: number; failure: number; warning: number; activeAlerts: number; newAlerts: number; recoveredAlerts: number; erroredActions: number; triggeredActions: number; }>; getGlobalExecutionKpiWithAuth: (params: ", + "GetGlobalExecutionKPIParams", + ") => Promise<{ success: number; unknown: number; failure: number; warning: number; activeAlerts: number; newAlerts: number; recoveredAlerts: number; erroredActions: number; triggeredActions: number; }>; getActionErrorLog: (params: ", "GetActionErrorLogByIdParams", ") => Promise<", { @@ -3050,7 +3062,7 @@ "section": "def-common.IExecutionErrorsResult", "text": "IExecutionErrorsResult" }, - ">; getActionErrorLogWithAuth: ({ id, dateStart, dateEnd, filter, page, perPage, sort, namespace, }: ", + ">; getActionErrorLogWithAuth: (params: ", "GetActionErrorLogByIdParams", ") => Promise<", { @@ -3060,11 +3072,7 @@ "section": "def-common.IExecutionErrorsResult", "text": "IExecutionErrorsResult" }, - ">; getGlobalExecutionKpiWithAuth: ({ dateStart, dateEnd, filter, namespaces, }: ", - "GetGlobalExecutionKPIParams", - ") => Promise<{ success: number; unknown: number; failure: number; warning: number; activeAlerts: number; newAlerts: number; recoveredAlerts: number; erroredActions: number; triggeredActions: number; }>; getRuleExecutionKPI: ({ id, dateStart, dateEnd, filter }: ", - "GetRuleExecutionKPIParams", - ") => Promise<{ success: number; unknown: number; failure: number; warning: number; activeAlerts: number; newAlerts: number; recoveredAlerts: number; erroredActions: number; triggeredActions: number; }>; bulkDeleteRules: (options: ", + ">; bulkDeleteRules: (options: ", "BulkOptions", ") => Promise<{ errors: ", { @@ -3146,27 +3154,15 @@ }, " | ", "RuleWithLegacyId", - ")[]; total: number; }>; updateApiKey: ({ id }: { id: string; }) => Promise; snooze: ({ id, snoozeSchedule, }: { id: string; snoozeSchedule: ", - { - "pluginId": "alerting", - "scope": "common", - "docId": "kibAlertingPluginApi", - "section": "def-common.RuleSnoozeSchedule", - "text": "RuleSnoozeSchedule" - }, - "; }) => Promise; unsnooze: ({ id, scheduleIds, }: { id: string; scheduleIds?: string[] | undefined; }) => Promise; calculateIsSnoozedUntil: (rule: { muteAll: boolean; snoozeSchedule?: ", - { - "pluginId": "alerting", - "scope": "common", - "docId": "kibAlertingPluginApi", - "section": "def-common.RuleSnooze", - "text": "RuleSnooze" - }, - " | undefined; }) => string | null; clearExpiredSnoozes: ({ id }: { id: string; }) => Promise; unmuteAll: ({ id }: { id: string; }) => Promise; muteInstance: ({ alertId, alertInstanceId }: ", + ")[]; total: number; }>; updateApiKey: (options: { id: string; }) => Promise; snooze: (options: ", + "SnoozeParams", + ") => Promise; unsnooze: (options: ", + "UnsnoozeParams", + ") => Promise; clearExpiredSnoozes: (options: { id: string; }) => Promise; unmuteAll: (options: { id: string; }) => Promise; muteInstance: (options: ", "MuteOptions", - ") => Promise; unmuteInstance: ({ alertId, alertInstanceId }: ", + ") => Promise; unmuteInstance: (options: ", "MuteOptions", - ") => Promise; runSoon: ({ id }: { id: string; }) => Promise; listAlertTypes: () => Promise Promise; runSoon: (options: { id: string; }) => Promise; listAlertTypes: () => Promise>; getSpaceId: () => string | undefined; }" ], @@ -5465,7 +5461,7 @@ "label": "status", "description": [], "signature": [ - "\"error\" | \"warning\" | \"unknown\" | \"pending\" | \"ok\" | \"active\"" + "\"error\" | \"warning\" | \"unknown\" | \"pending\" | \"active\" | \"ok\"" ], "path": "x-pack/plugins/alerting/common/rule.ts", "deprecated": false, @@ -6522,7 +6518,7 @@ "label": "AlertInstanceMeta", "description": [], "signature": [ - "{ lastScheduledActions?: ({ subgroup?: string | undefined; } & { group: string; date: Date; }) | undefined; }" + "{ lastScheduledActions?: ({ subgroup?: string | undefined; } & { group: string; date: Date; }) | undefined; flappingHistory?: boolean[] | undefined; flapping?: boolean | undefined; }" ], "path": "x-pack/plugins/alerting/common/alert_instance.ts", "deprecated": false, @@ -6716,7 +6712,7 @@ "label": "RawAlertInstance", "description": [], "signature": [ - "{ state?: { [x: string]: unknown; } | undefined; meta?: { lastScheduledActions?: ({ subgroup?: string | undefined; } & { group: string; date: Date; }) | undefined; } | undefined; }" + "{ state?: { [x: string]: unknown; } | undefined; meta?: { lastScheduledActions?: ({ subgroup?: string | undefined; } & { group: string; date: Date; }) | undefined; flappingHistory?: boolean[] | undefined; flapping?: boolean | undefined; } | undefined; }" ], "path": "x-pack/plugins/alerting/common/alert_instance.ts", "deprecated": false, @@ -6857,7 +6853,7 @@ "label": "RuleExecutionStatuses", "description": [], "signature": [ - "\"error\" | \"warning\" | \"unknown\" | \"pending\" | \"ok\" | \"active\"" + "\"error\" | \"warning\" | \"unknown\" | \"pending\" | \"active\" | \"ok\"" ], "path": "x-pack/plugins/alerting/common/rule.ts", "deprecated": false, @@ -6976,7 +6972,7 @@ "label": "RuleTaskState", "description": [], "signature": [ - "{ alertTypeState?: { [x: string]: unknown; } | undefined; alertInstances?: { [x: string]: { state?: { [x: string]: unknown; } | undefined; meta?: { lastScheduledActions?: ({ subgroup?: string | undefined; } & { group: string; date: Date; }) | undefined; } | undefined; }; } | undefined; previousStartedAt?: Date | null | undefined; }" + "{ alertTypeState?: { [x: string]: unknown; } | undefined; alertInstances?: { [x: string]: { state?: { [x: string]: unknown; } | undefined; meta?: { lastScheduledActions?: ({ subgroup?: string | undefined; } & { group: string; date: Date; }) | undefined; flappingHistory?: boolean[] | undefined; flapping?: boolean | undefined; } | undefined; }; } | undefined; alertRecoveredInstances?: { [x: string]: { state?: { [x: string]: unknown; } | undefined; meta?: { lastScheduledActions?: ({ subgroup?: string | undefined; } & { group: string; date: Date; }) | undefined; flappingHistory?: boolean[] | undefined; flapping?: boolean | undefined; } | undefined; }; } | undefined; previousStartedAt?: Date | null | undefined; }" ], "path": "x-pack/plugins/alerting/common/rule_task_instance.ts", "deprecated": false, @@ -7304,7 +7300,13 @@ "StringC", "; date: ", "Type", - "; }>]>; }>; }>" + "; }>]>; flappingHistory: ", + "ArrayC", + "<", + "BooleanC", + ">; flapping: ", + "BooleanC", + "; }>; }>" ], "path": "x-pack/plugins/alerting/common/alert_instance.ts", "deprecated": false, @@ -7436,7 +7438,45 @@ "StringC", "; date: ", "Type", - "; }>]>; }>; }>>; previousStartedAt: ", + "; }>]>; flappingHistory: ", + "ArrayC", + "<", + "BooleanC", + ">; flapping: ", + "BooleanC", + "; }>; }>>; alertRecoveredInstances: ", + "RecordC", + "<", + "StringC", + ", ", + "PartialC", + "<{ state: ", + "RecordC", + "<", + "StringC", + ", ", + "UnknownC", + ">; meta: ", + "PartialC", + "<{ lastScheduledActions: ", + "IntersectionC", + "<[", + "PartialC", + "<{ subgroup: ", + "StringC", + "; }>, ", + "TypeC", + "<{ group: ", + "StringC", + "; date: ", + "Type", + "; }>]>; flappingHistory: ", + "ArrayC", + "<", + "BooleanC", + ">; flapping: ", + "BooleanC", + "; }>; }>>; previousStartedAt: ", "UnionC", "<[", "NullC", diff --git a/api_docs/alerting.mdx b/api_docs/alerting.mdx index c4c220c72868c..75403b2908078 100644 --- a/api_docs/alerting.mdx +++ b/api_docs/alerting.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/alerting title: "alerting" image: https://source.unsplash.com/400x175/?github description: API docs for the alerting plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'alerting'] --- import alertingObj from './alerting.devdocs.json'; @@ -21,7 +21,7 @@ Contact [Response Ops](https://github.com/orgs/elastic/teams/response-ops) for q | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 417 | 0 | 408 | 28 | +| 417 | 0 | 408 | 34 | ## Client diff --git a/api_docs/apm.devdocs.json b/api_docs/apm.devdocs.json index daf3593733b1b..ea9c347652eef 100644 --- a/api_docs/apm.devdocs.json +++ b/api_docs/apm.devdocs.json @@ -202,7 +202,7 @@ "APMPluginSetupDependencies", ") => { config$: ", "Observable", - "; autoCreateApmDataView: boolean; serviceMapEnabled: boolean; serviceMapFingerprintBucketSize: number; serviceMapTraceIdBucketSize: number; serviceMapFingerprintGlobalBucketSize: number; serviceMapTraceIdGlobalBucketSize: number; serviceMapMaxTracesPerRequest: number; ui: Readonly<{} & { enabled: boolean; transactionGroupBucketSize: number; maxTraceItems: number; }>; searchAggregatedTransactions: ", + "; autoCreateApmDataView: boolean; serviceMapEnabled: boolean; serviceMapFingerprintBucketSize: number; serviceMapFingerprintGlobalBucketSize: number; serviceMapTraceIdBucketSize: number; serviceMapTraceIdGlobalBucketSize: number; serviceMapMaxTracesPerRequest: number; ui: Readonly<{} & { enabled: boolean; transactionGroupBucketSize: number; maxTraceItems: number; }>; searchAggregatedTransactions: ", "SearchAggregatedTransactionSetting", "; telemetryCollectionEnabled: boolean; metricsInterval: number; agent: Readonly<{} & { migrations: Readonly<{} & { enabled: boolean; }>; }>; forceSyntheticSource: boolean; }>>; getApmIndices: () => Promise>; createApmEventClient: ({ request, context, debug, }: { debug?: boolean | undefined; request: ", { @@ -431,7 +431,7 @@ "label": "config", "description": [], "signature": [ - "{ readonly indices: Readonly<{} & { metric: string; error: string; span: string; transaction: string; sourcemap: string; onboarding: string; }>; readonly autoCreateApmDataView: boolean; readonly serviceMapEnabled: boolean; readonly serviceMapFingerprintBucketSize: number; readonly serviceMapTraceIdBucketSize: number; readonly serviceMapFingerprintGlobalBucketSize: number; readonly serviceMapTraceIdGlobalBucketSize: number; readonly serviceMapMaxTracesPerRequest: number; readonly ui: Readonly<{} & { enabled: boolean; transactionGroupBucketSize: number; maxTraceItems: number; }>; readonly searchAggregatedTransactions: ", + "{ readonly indices: Readonly<{} & { metric: string; error: string; span: string; transaction: string; sourcemap: string; onboarding: string; }>; readonly autoCreateApmDataView: boolean; readonly serviceMapEnabled: boolean; readonly serviceMapFingerprintBucketSize: number; readonly serviceMapFingerprintGlobalBucketSize: number; readonly serviceMapTraceIdBucketSize: number; readonly serviceMapTraceIdGlobalBucketSize: number; readonly serviceMapMaxTracesPerRequest: number; readonly ui: Readonly<{} & { enabled: boolean; transactionGroupBucketSize: number; maxTraceItems: number; }>; readonly searchAggregatedTransactions: ", "SearchAggregatedTransactionSetting", "; readonly telemetryCollectionEnabled: boolean; readonly metricsInterval: number; readonly agent: Readonly<{} & { migrations: Readonly<{} & { enabled: boolean; }>; }>; readonly forceSyntheticSource: boolean; }" ], @@ -823,7 +823,7 @@ "label": "APMConfig", "description": [], "signature": [ - "{ readonly indices: Readonly<{} & { metric: string; error: string; span: string; transaction: string; sourcemap: string; onboarding: string; }>; readonly autoCreateApmDataView: boolean; readonly serviceMapEnabled: boolean; readonly serviceMapFingerprintBucketSize: number; readonly serviceMapTraceIdBucketSize: number; readonly serviceMapFingerprintGlobalBucketSize: number; readonly serviceMapTraceIdGlobalBucketSize: number; readonly serviceMapMaxTracesPerRequest: number; readonly ui: Readonly<{} & { enabled: boolean; transactionGroupBucketSize: number; maxTraceItems: number; }>; readonly searchAggregatedTransactions: ", + "{ readonly indices: Readonly<{} & { metric: string; error: string; span: string; transaction: string; sourcemap: string; onboarding: string; }>; readonly autoCreateApmDataView: boolean; readonly serviceMapEnabled: boolean; readonly serviceMapFingerprintBucketSize: number; readonly serviceMapFingerprintGlobalBucketSize: number; readonly serviceMapTraceIdBucketSize: number; readonly serviceMapTraceIdGlobalBucketSize: number; readonly serviceMapMaxTracesPerRequest: number; readonly ui: Readonly<{} & { enabled: boolean; transactionGroupBucketSize: number; maxTraceItems: number; }>; readonly searchAggregatedTransactions: ", "SearchAggregatedTransactionSetting", "; readonly telemetryCollectionEnabled: boolean; readonly metricsInterval: number; readonly agent: Readonly<{} & { migrations: Readonly<{} & { enabled: boolean; }>; }>; readonly forceSyntheticSource: boolean; }" ], @@ -3123,7 +3123,15 @@ "section": "def-common.ServerRoute", "text": "ServerRoute" }, - "<\"GET /api/apm/sourcemaps\", undefined, ", + "<\"GET /api/apm/sourcemaps\", ", + "PartialC", + "<{ query: ", + "PartialC", + "<{ page: ", + "Type", + "; perPage: ", + "Type", + "; }>; }>, ", { "pluginId": "apm", "scope": "server", @@ -3133,7 +3141,7 @@ }, ", { artifacts: ", "ArtifactSourceMap", - "[]; } | undefined, ", + "[]; total: number; } | undefined, ", "APMRouteCreateOptions", ">; \"DELETE /internal/apm/settings/custom_links/{id}\": ", { @@ -7256,7 +7264,7 @@ "description": [], "signature": [ "Observable", - "; autoCreateApmDataView: boolean; serviceMapEnabled: boolean; serviceMapFingerprintBucketSize: number; serviceMapTraceIdBucketSize: number; serviceMapFingerprintGlobalBucketSize: number; serviceMapTraceIdGlobalBucketSize: number; serviceMapMaxTracesPerRequest: number; ui: Readonly<{} & { enabled: boolean; transactionGroupBucketSize: number; maxTraceItems: number; }>; searchAggregatedTransactions: ", + "; autoCreateApmDataView: boolean; serviceMapEnabled: boolean; serviceMapFingerprintBucketSize: number; serviceMapFingerprintGlobalBucketSize: number; serviceMapTraceIdBucketSize: number; serviceMapTraceIdGlobalBucketSize: number; serviceMapMaxTracesPerRequest: number; ui: Readonly<{} & { enabled: boolean; transactionGroupBucketSize: number; maxTraceItems: number; }>; searchAggregatedTransactions: ", "SearchAggregatedTransactionSetting", "; telemetryCollectionEnabled: boolean; metricsInterval: number; agent: Readonly<{} & { migrations: Readonly<{} & { enabled: boolean; }>; }>; forceSyntheticSource: boolean; }>>" ], diff --git a/api_docs/apm.mdx b/api_docs/apm.mdx index 95e0a82d5ca38..949e32fd84c23 100644 --- a/api_docs/apm.mdx +++ b/api_docs/apm.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/apm title: "apm" image: https://source.unsplash.com/400x175/?github description: API docs for the apm plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'apm'] --- import apmObj from './apm.devdocs.json'; diff --git a/api_docs/banners.mdx b/api_docs/banners.mdx index 979a66403b5a8..fe1ac58de0444 100644 --- a/api_docs/banners.mdx +++ b/api_docs/banners.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/banners title: "banners" image: https://source.unsplash.com/400x175/?github description: API docs for the banners plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'banners'] --- import bannersObj from './banners.devdocs.json'; diff --git a/api_docs/bfetch.mdx b/api_docs/bfetch.mdx index b63de7a8aeed9..98dc5d8514fa8 100644 --- a/api_docs/bfetch.mdx +++ b/api_docs/bfetch.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/bfetch title: "bfetch" image: https://source.unsplash.com/400x175/?github description: API docs for the bfetch plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'bfetch'] --- import bfetchObj from './bfetch.devdocs.json'; diff --git a/api_docs/canvas.mdx b/api_docs/canvas.mdx index 9fdf487a247db..999ded241bf67 100644 --- a/api_docs/canvas.mdx +++ b/api_docs/canvas.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/canvas title: "canvas" image: https://source.unsplash.com/400x175/?github description: API docs for the canvas plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'canvas'] --- import canvasObj from './canvas.devdocs.json'; diff --git a/api_docs/cases.mdx b/api_docs/cases.mdx index 5dbe868748781..db0b75c9603a4 100644 --- a/api_docs/cases.mdx +++ b/api_docs/cases.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/cases title: "cases" image: https://source.unsplash.com/400x175/?github description: API docs for the cases plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'cases'] --- import casesObj from './cases.devdocs.json'; diff --git a/api_docs/charts.mdx b/api_docs/charts.mdx index 553316cf6a200..510d1f3d254f3 100644 --- a/api_docs/charts.mdx +++ b/api_docs/charts.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/charts title: "charts" image: https://source.unsplash.com/400x175/?github description: API docs for the charts plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'charts'] --- import chartsObj from './charts.devdocs.json'; diff --git a/api_docs/cloud.mdx b/api_docs/cloud.mdx index 4210bc2998d2d..cb595c90e5eb1 100644 --- a/api_docs/cloud.mdx +++ b/api_docs/cloud.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/cloud title: "cloud" image: https://source.unsplash.com/400x175/?github description: API docs for the cloud plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'cloud'] --- import cloudObj from './cloud.devdocs.json'; diff --git a/api_docs/cloud_chat.mdx b/api_docs/cloud_chat.mdx index 34acd8f44b99d..79d332e2a590d 100644 --- a/api_docs/cloud_chat.mdx +++ b/api_docs/cloud_chat.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/cloudChat title: "cloudChat" image: https://source.unsplash.com/400x175/?github description: API docs for the cloudChat plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'cloudChat'] --- import cloudChatObj from './cloud_chat.devdocs.json'; diff --git a/api_docs/cloud_experiments.mdx b/api_docs/cloud_experiments.mdx index fa04e5d423791..30fc6c6e82238 100644 --- a/api_docs/cloud_experiments.mdx +++ b/api_docs/cloud_experiments.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/cloudExperiments title: "cloudExperiments" image: https://source.unsplash.com/400x175/?github description: API docs for the cloudExperiments plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'cloudExperiments'] --- import cloudExperimentsObj from './cloud_experiments.devdocs.json'; diff --git a/api_docs/cloud_security_posture.mdx b/api_docs/cloud_security_posture.mdx index 8debf0d9c2daf..f78bcae01de2c 100644 --- a/api_docs/cloud_security_posture.mdx +++ b/api_docs/cloud_security_posture.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/cloudSecurityPosture title: "cloudSecurityPosture" image: https://source.unsplash.com/400x175/?github description: API docs for the cloudSecurityPosture plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'cloudSecurityPosture'] --- import cloudSecurityPostureObj from './cloud_security_posture.devdocs.json'; diff --git a/api_docs/console.mdx b/api_docs/console.mdx index 078028388f90c..799b617e99153 100644 --- a/api_docs/console.mdx +++ b/api_docs/console.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/console title: "console" image: https://source.unsplash.com/400x175/?github description: API docs for the console plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'console'] --- import consoleObj from './console.devdocs.json'; diff --git a/api_docs/controls.devdocs.json b/api_docs/controls.devdocs.json index 80a4e40f20e0d..4ad1f3165c5dc 100644 --- a/api_docs/controls.devdocs.json +++ b/api_docs/controls.devdocs.json @@ -288,7 +288,39 @@ "label": "addDataControlFromField", "description": [], "signature": [ - "({ uuid, dataViewId, fieldName, title, }: { uuid?: string | undefined; dataViewId: string; fieldName: string; title?: string | undefined; }) => Promise<", + "(controlProps: ", + { + "pluginId": "controls", + "scope": "public", + "docId": "kibControlsPluginApi", + "section": "def-public.AddDataControlProps", + "text": "AddDataControlProps" + }, + ") => Promise<", + { + "pluginId": "embeddable", + "scope": "public", + "docId": "kibEmbeddablePluginApi", + "section": "def-public.IEmbeddable", + "text": "IEmbeddable" + }, + "<", + { + "pluginId": "embeddable", + "scope": "common", + "docId": "kibEmbeddablePluginApi", + "section": "def-common.EmbeddableInput", + "text": "EmbeddableInput" + }, + ", ", + { + "pluginId": "embeddable", + "scope": "public", + "docId": "kibEmbeddablePluginApi", + "section": "def-public.EmbeddableOutput", + "text": "EmbeddableOutput" + }, + ", any> | ", { "pluginId": "embeddable", "scope": "public", @@ -296,7 +328,53 @@ "section": "def-public.ErrorEmbeddable", "text": "ErrorEmbeddable" }, - " | ", + ">" + ], + "path": "src/plugins/controls/public/control_group/embeddable/control_group_container.tsx", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "controls", + "id": "def-public.ControlGroupContainer.addDataControlFromField.$1", + "type": "Object", + "tags": [], + "label": "controlProps", + "description": [], + "signature": [ + { + "pluginId": "controls", + "scope": "public", + "docId": "kibControlsPluginApi", + "section": "def-public.AddDataControlProps", + "text": "AddDataControlProps" + } + ], + "path": "src/plugins/controls/public/control_group/embeddable/control_group_container.tsx", + "deprecated": false, + "trackAdoption": false, + "isRequired": true + } + ], + "returnComment": [] + }, + { + "parentPluginId": "controls", + "id": "def-public.ControlGroupContainer.addOptionsListControl", + "type": "Function", + "tags": [], + "label": "addOptionsListControl", + "description": [], + "signature": [ + "(controlProps: ", + { + "pluginId": "controls", + "scope": "public", + "docId": "kibControlsPluginApi", + "section": "def-public.AddOptionsListControlProps", + "text": "AddOptionsListControlProps" + }, + ") => Promise<", { "pluginId": "embeddable", "scope": "public", @@ -304,57 +382,87 @@ "section": "def-public.IEmbeddable", "text": "IEmbeddable" }, - "<{ viewMode: ", + "<", { "pluginId": "embeddable", "scope": "common", "docId": "kibEmbeddablePluginApi", - "section": "def-common.ViewMode", - "text": "ViewMode" + "section": "def-common.EmbeddableInput", + "text": "EmbeddableInput" }, - "; title: string; id: string; lastReloadRequestTime: number; hidePanelTitles: boolean; enhancements: ", + ", ", { - "pluginId": "@kbn/utility-types", - "scope": "server", - "docId": "kibKbnUtilityTypesPluginApi", - "section": "def-server.SerializableRecord", - "text": "SerializableRecord" + "pluginId": "embeddable", + "scope": "public", + "docId": "kibEmbeddablePluginApi", + "section": "def-public.EmbeddableOutput", + "text": "EmbeddableOutput" }, - "; disabledActions: string[]; disableTriggers: boolean; searchSessionId: string; syncColors: boolean; syncCursor: boolean; syncTooltips: boolean; executionContext: ", + ", any> | ", { - "pluginId": "@kbn/core-execution-context-common", - "scope": "common", - "docId": "kibKbnCoreExecutionContextCommonPluginApi", - "section": "def-common.KibanaExecutionContext", - "text": "KibanaExecutionContext" + "pluginId": "embeddable", + "scope": "public", + "docId": "kibEmbeddablePluginApi", + "section": "def-public.ErrorEmbeddable", + "text": "ErrorEmbeddable" }, - "; query: ", + ">" + ], + "path": "src/plugins/controls/public/control_group/embeddable/control_group_container.tsx", + "deprecated": false, + "trackAdoption": false, + "children": [ { - "pluginId": "@kbn/es-query", - "scope": "common", - "docId": "kibKbnEsQueryPluginApi", - "section": "def-common.Query", - "text": "Query" - }, - "; filters: ", + "parentPluginId": "controls", + "id": "def-public.ControlGroupContainer.addOptionsListControl.$1", + "type": "CompoundType", + "tags": [], + "label": "controlProps", + "description": [], + "signature": [ + { + "pluginId": "controls", + "scope": "public", + "docId": "kibControlsPluginApi", + "section": "def-public.AddOptionsListControlProps", + "text": "AddOptionsListControlProps" + } + ], + "path": "src/plugins/controls/public/control_group/embeddable/control_group_container.tsx", + "deprecated": false, + "trackAdoption": false, + "isRequired": true + } + ], + "returnComment": [] + }, + { + "parentPluginId": "controls", + "id": "def-public.ControlGroupContainer.addRangeSliderControl", + "type": "Function", + "tags": [], + "label": "addRangeSliderControl", + "description": [], + "signature": [ + "(controlProps: ", + "AddRangeSliderControlProps", + ") => Promise<", { - "pluginId": "@kbn/es-query", - "scope": "common", - "docId": "kibKbnEsQueryPluginApi", - "section": "def-common.Filter", - "text": "Filter" + "pluginId": "embeddable", + "scope": "public", + "docId": "kibEmbeddablePluginApi", + "section": "def-public.IEmbeddable", + "text": "IEmbeddable" }, - "[]; timeRange: ", + "<", { - "pluginId": "@kbn/es-query", + "pluginId": "embeddable", "scope": "common", - "docId": "kibKbnEsQueryPluginApi", - "section": "def-common.TimeRange", - "text": "TimeRange" + "docId": "kibEmbeddablePluginApi", + "section": "def-common.EmbeddableInput", + "text": "EmbeddableInput" }, - "; timeslice: [number, number]; controlStyle: \"twoLine\" | \"oneLine\"; ignoreParentSettings: ", - "ParentIgnoreSettings", - "; fieldName: string; parentFieldName: string; childFieldName: string; dataViewId: string; }, ", + ", ", { "pluginId": "embeddable", "scope": "public", @@ -362,7 +470,15 @@ "section": "def-public.EmbeddableOutput", "text": "EmbeddableOutput" }, - ", any>>" + ", any> | ", + { + "pluginId": "embeddable", + "scope": "public", + "docId": "kibEmbeddablePluginApi", + "section": "def-public.ErrorEmbeddable", + "text": "ErrorEmbeddable" + }, + ">" ], "path": "src/plugins/controls/public/control_group/embeddable/control_group_container.tsx", "deprecated": false, @@ -370,70 +486,70 @@ "children": [ { "parentPluginId": "controls", - "id": "def-public.ControlGroupContainer.addDataControlFromField.$1", - "type": "Object", + "id": "def-public.ControlGroupContainer.addRangeSliderControl.$1", + "type": "CompoundType", "tags": [], - "label": "{\n uuid,\n dataViewId,\n fieldName,\n title,\n }", + "label": "controlProps", "description": [], + "signature": [ + "AddRangeSliderControlProps" + ], "path": "src/plugins/controls/public/control_group/embeddable/control_group_container.tsx", "deprecated": false, "trackAdoption": false, - "children": [ - { - "parentPluginId": "controls", - "id": "def-public.ControlGroupContainer.addDataControlFromField.$1.uuid", - "type": "string", - "tags": [], - "label": "uuid", - "description": [], - "signature": [ - "string | undefined" - ], - "path": "src/plugins/controls/public/control_group/embeddable/control_group_container.tsx", - "deprecated": false, - "trackAdoption": false - }, - { - "parentPluginId": "controls", - "id": "def-public.ControlGroupContainer.addDataControlFromField.$1.dataViewId", - "type": "string", - "tags": [], - "label": "dataViewId", - "description": [], - "path": "src/plugins/controls/public/control_group/embeddable/control_group_container.tsx", - "deprecated": false, - "trackAdoption": false - }, - { - "parentPluginId": "controls", - "id": "def-public.ControlGroupContainer.addDataControlFromField.$1.fieldName", - "type": "string", - "tags": [], - "label": "fieldName", - "description": [], - "path": "src/plugins/controls/public/control_group/embeddable/control_group_container.tsx", - "deprecated": false, - "trackAdoption": false - }, - { - "parentPluginId": "controls", - "id": "def-public.ControlGroupContainer.addDataControlFromField.$1.title", - "type": "string", - "tags": [], - "label": "title", - "description": [], - "signature": [ - "string | undefined" - ], - "path": "src/plugins/controls/public/control_group/embeddable/control_group_container.tsx", - "deprecated": false, - "trackAdoption": false - } - ] + "isRequired": true } ], "returnComment": [] }, + { + "parentPluginId": "controls", + "id": "def-public.ControlGroupContainer.addTimeSliderControl", + "type": "Function", + "tags": [], + "label": "addTimeSliderControl", + "description": [], + "signature": [ + "() => Promise<", + { + "pluginId": "embeddable", + "scope": "public", + "docId": "kibEmbeddablePluginApi", + "section": "def-public.IEmbeddable", + "text": "IEmbeddable" + }, + "<", + { + "pluginId": "embeddable", + "scope": "common", + "docId": "kibEmbeddablePluginApi", + "section": "def-common.EmbeddableInput", + "text": "EmbeddableInput" + }, + ", ", + { + "pluginId": "embeddable", + "scope": "public", + "docId": "kibEmbeddablePluginApi", + "section": "def-public.EmbeddableOutput", + "text": "EmbeddableOutput" + }, + ", any> | ", + { + "pluginId": "embeddable", + "scope": "public", + "docId": "kibEmbeddablePluginApi", + "section": "def-public.ErrorEmbeddable", + "text": "ErrorEmbeddable" + }, + ">" + ], + "path": "src/plugins/controls/public/control_group/embeddable/control_group_container.tsx", + "deprecated": false, + "trackAdoption": false, + "children": [], + "returnComment": [] + }, { "parentPluginId": "controls", "id": "def-public.ControlGroupContainer.getCreateControlButton", @@ -3224,7 +3340,7 @@ "section": "def-public.ControlGroupRendererProps", "text": "ControlGroupRendererProps" }, - "> & { readonly _result: ({ onEmbeddableLoad, getCreationOptions, }: ", + "> & { readonly _result: ({ onLoadComplete, getInitialInput, }: ", { "pluginId": "controls", "scope": "public", @@ -3371,89 +3487,188 @@ "interfaces": [ { "parentPluginId": "controls", - "id": "def-public.CalloutProps", + "id": "def-public.AddDataControlProps", "type": "Interface", "tags": [], - "label": "CalloutProps", + "label": "AddDataControlProps", "description": [], - "path": "src/plugins/controls/public/controls_callout/controls_callout.tsx", + "path": "src/plugins/controls/public/control_group/control_group_input_builder.ts", "deprecated": false, "trackAdoption": false, "children": [ { "parentPluginId": "controls", - "id": "def-public.CalloutProps.getCreateControlButton", - "type": "Function", + "id": "def-public.AddDataControlProps.controlId", + "type": "string", "tags": [], - "label": "getCreateControlButton", + "label": "controlId", "description": [], "signature": [ - "(() => JSX.Element) | undefined" + "string | undefined" ], - "path": "src/plugins/controls/public/controls_callout/controls_callout.tsx", + "path": "src/plugins/controls/public/control_group/control_group_input_builder.ts", "deprecated": false, - "trackAdoption": false, - "children": [], - "returnComment": [] - } - ], - "initialIsOpen": false - }, - { - "parentPluginId": "controls", - "id": "def-public.CommonControlOutput", - "type": "Interface", - "tags": [], - "label": "CommonControlOutput", - "description": [], - "path": "src/plugins/controls/public/types.ts", - "deprecated": false, - "trackAdoption": false, - "children": [ + "trackAdoption": false + }, { "parentPluginId": "controls", - "id": "def-public.CommonControlOutput.filters", - "type": "Array", + "id": "def-public.AddDataControlProps.dataViewId", + "type": "string", "tags": [], - "label": "filters", + "label": "dataViewId", "description": [], - "signature": [ - { - "pluginId": "@kbn/es-query", - "scope": "common", - "docId": "kibKbnEsQueryPluginApi", - "section": "def-common.Filter", - "text": "Filter" - }, - "[] | undefined" - ], - "path": "src/plugins/controls/public/types.ts", + "path": "src/plugins/controls/public/control_group/control_group_input_builder.ts", "deprecated": false, "trackAdoption": false }, { "parentPluginId": "controls", - "id": "def-public.CommonControlOutput.dataViewId", + "id": "def-public.AddDataControlProps.fieldName", "type": "string", "tags": [], - "label": "dataViewId", + "label": "fieldName", "description": [], - "signature": [ - "string | undefined" - ], - "path": "src/plugins/controls/public/types.ts", + "path": "src/plugins/controls/public/control_group/control_group_input_builder.ts", "deprecated": false, "trackAdoption": false }, { "parentPluginId": "controls", - "id": "def-public.CommonControlOutput.timeslice", - "type": "Object", + "id": "def-public.AddDataControlProps.grow", + "type": "CompoundType", "tags": [], - "label": "timeslice", + "label": "grow", "description": [], "signature": [ - "[number, number] | undefined" + "boolean | undefined" + ], + "path": "src/plugins/controls/public/control_group/control_group_input_builder.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "controls", + "id": "def-public.AddDataControlProps.title", + "type": "string", + "tags": [], + "label": "title", + "description": [], + "signature": [ + "string | undefined" + ], + "path": "src/plugins/controls/public/control_group/control_group_input_builder.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "controls", + "id": "def-public.AddDataControlProps.width", + "type": "CompoundType", + "tags": [], + "label": "width", + "description": [], + "signature": [ + { + "pluginId": "controls", + "scope": "common", + "docId": "kibControlsPluginApi", + "section": "def-common.ControlWidth", + "text": "ControlWidth" + }, + " | undefined" + ], + "path": "src/plugins/controls/public/control_group/control_group_input_builder.ts", + "deprecated": false, + "trackAdoption": false + } + ], + "initialIsOpen": false + }, + { + "parentPluginId": "controls", + "id": "def-public.CalloutProps", + "type": "Interface", + "tags": [], + "label": "CalloutProps", + "description": [], + "path": "src/plugins/controls/public/controls_callout/controls_callout.tsx", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "controls", + "id": "def-public.CalloutProps.getCreateControlButton", + "type": "Function", + "tags": [], + "label": "getCreateControlButton", + "description": [], + "signature": [ + "(() => JSX.Element) | undefined" + ], + "path": "src/plugins/controls/public/controls_callout/controls_callout.tsx", + "deprecated": false, + "trackAdoption": false, + "children": [], + "returnComment": [] + } + ], + "initialIsOpen": false + }, + { + "parentPluginId": "controls", + "id": "def-public.CommonControlOutput", + "type": "Interface", + "tags": [], + "label": "CommonControlOutput", + "description": [], + "path": "src/plugins/controls/public/types.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "controls", + "id": "def-public.CommonControlOutput.filters", + "type": "Array", + "tags": [], + "label": "filters", + "description": [], + "signature": [ + { + "pluginId": "@kbn/es-query", + "scope": "common", + "docId": "kibKbnEsQueryPluginApi", + "section": "def-common.Filter", + "text": "Filter" + }, + "[] | undefined" + ], + "path": "src/plugins/controls/public/types.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "controls", + "id": "def-public.CommonControlOutput.dataViewId", + "type": "string", + "tags": [], + "label": "dataViewId", + "description": [], + "signature": [ + "string | undefined" + ], + "path": "src/plugins/controls/public/types.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "controls", + "id": "def-public.CommonControlOutput.timeslice", + "type": "Object", + "tags": [], + "label": "timeslice", + "description": [], + "signature": [ + "[number, number] | undefined" ], "path": "src/plugins/controls/public/types.ts", "deprecated": false, @@ -3672,13 +3887,13 @@ "children": [ { "parentPluginId": "controls", - "id": "def-public.ControlGroupRendererProps.onEmbeddableLoad", + "id": "def-public.ControlGroupRendererProps.onLoadComplete", "type": "Function", "tags": [], - "label": "onEmbeddableLoad", + "label": "onLoadComplete", "description": [], "signature": [ - "(controlGroupContainer: ", + "((controlGroup: ", { "pluginId": "controls", "scope": "public", @@ -3686,7 +3901,7 @@ "section": "def-public.ControlGroupContainer", "text": "ControlGroupContainer" }, - ") => void" + ") => void) | undefined" ], "path": "src/plugins/controls/public/control_group/control_group_renderer.tsx", "deprecated": false, @@ -3694,10 +3909,10 @@ "children": [ { "parentPluginId": "controls", - "id": "def-public.ControlGroupRendererProps.onEmbeddableLoad.$1", + "id": "def-public.ControlGroupRendererProps.onLoadComplete.$1", "type": "Object", "tags": [], - "label": "controlGroupContainer", + "label": "controlGroup", "description": [], "signature": [ { @@ -3718,13 +3933,13 @@ }, { "parentPluginId": "controls", - "id": "def-public.ControlGroupRendererProps.getCreationOptions", + "id": "def-public.ControlGroupRendererProps.getInitialInput", "type": "Function", "tags": [], - "label": "getCreationOptions", + "label": "getInitialInput", "description": [], "signature": [ - "(builder: { addDataControlFromField: (initialInput: Partial<", + "(initialInput: Partial<", { "pluginId": "controls", "scope": "common", @@ -3732,17 +3947,57 @@ "section": "def-common.ControlGroupInput", "text": "ControlGroupInput" }, - ">, newPanelInput: { title?: string | undefined; panelId?: string | undefined; fieldName: string; dataViewId: string; } & Partial<", + ">, builder: { addDataControlFromField: (initialInput: Partial<", { "pluginId": "controls", "scope": "common", "docId": "kibControlsPluginApi", - "section": "def-common.ControlPanelState", - "text": "ControlPanelState" + "section": "def-common.ControlGroupInput", + "text": "ControlGroupInput" }, - "<", - "ControlInput", - ">>) => Promise; }) => Promise, controlProps: ", + { + "pluginId": "controls", + "scope": "public", + "docId": "kibControlsPluginApi", + "section": "def-public.AddDataControlProps", + "text": "AddDataControlProps" + }, + ") => Promise; addOptionsListControl: (initialInput: Partial<", + { + "pluginId": "controls", + "scope": "common", + "docId": "kibControlsPluginApi", + "section": "def-common.ControlGroupInput", + "text": "ControlGroupInput" + }, + ">, controlProps: ", + { + "pluginId": "controls", + "scope": "public", + "docId": "kibControlsPluginApi", + "section": "def-public.AddOptionsListControlProps", + "text": "AddOptionsListControlProps" + }, + ") => void; addRangeSliderControl: (initialInput: Partial<", + { + "pluginId": "controls", + "scope": "common", + "docId": "kibControlsPluginApi", + "section": "def-common.ControlGroupInput", + "text": "ControlGroupInput" + }, + ">, controlProps: ", + "AddRangeSliderControlProps", + ") => void; addTimeSliderControl: (initialInput: Partial<", + { + "pluginId": "controls", + "scope": "common", + "docId": "kibControlsPluginApi", + "section": "def-common.ControlGroupInput", + "text": "ControlGroupInput" + }, + ">) => void; }) => Promise" + ], + "path": "src/plugins/controls/public/control_group/control_group_renderer.tsx", + "deprecated": false, + "trackAdoption": false, + "isRequired": true + }, + { + "parentPluginId": "controls", + "id": "def-public.ControlGroupRendererProps.getInitialInput.$2", "type": "Object", "tags": [], "label": "builder", @@ -3772,17 +4050,49 @@ "section": "def-common.ControlGroupInput", "text": "ControlGroupInput" }, - ">, newPanelInput: { title?: string | undefined; panelId?: string | undefined; fieldName: string; dataViewId: string; } & Partial<", + ">, controlProps: ", + { + "pluginId": "controls", + "scope": "public", + "docId": "kibControlsPluginApi", + "section": "def-public.AddDataControlProps", + "text": "AddDataControlProps" + }, + ") => Promise; addOptionsListControl: (initialInput: Partial<", { "pluginId": "controls", "scope": "common", "docId": "kibControlsPluginApi", - "section": "def-common.ControlPanelState", - "text": "ControlPanelState" + "section": "def-common.ControlGroupInput", + "text": "ControlGroupInput" }, - "<", - "ControlInput", - ">>) => Promise; }" + ">, controlProps: ", + { + "pluginId": "controls", + "scope": "public", + "docId": "kibControlsPluginApi", + "section": "def-public.AddOptionsListControlProps", + "text": "AddOptionsListControlProps" + }, + ") => void; addRangeSliderControl: (initialInput: Partial<", + { + "pluginId": "controls", + "scope": "common", + "docId": "kibControlsPluginApi", + "section": "def-common.ControlGroupInput", + "text": "ControlGroupInput" + }, + ">, controlProps: ", + "AddRangeSliderControlProps", + ") => void; addTimeSliderControl: (initialInput: Partial<", + { + "pluginId": "controls", + "scope": "common", + "docId": "kibControlsPluginApi", + "section": "def-common.ControlGroupInput", + "text": "ControlGroupInput" + }, + ">) => void; }" ], "path": "src/plugins/controls/public/control_group/control_group_renderer.tsx", "deprecated": false, @@ -4244,6 +4554,36 @@ ], "enums": [], "misc": [ + { + "parentPluginId": "controls", + "id": "def-public.AddOptionsListControlProps", + "type": "Type", + "tags": [], + "label": "AddOptionsListControlProps", + "description": [], + "signature": [ + { + "pluginId": "controls", + "scope": "public", + "docId": "kibControlsPluginApi", + "section": "def-public.AddDataControlProps", + "text": "AddDataControlProps" + }, + " & Partial<", + { + "pluginId": "controls", + "scope": "common", + "docId": "kibControlsPluginApi", + "section": "def-common.OptionsListEmbeddableInput", + "text": "OptionsListEmbeddableInput" + }, + ">" + ], + "path": "src/plugins/controls/public/control_group/control_group_input_builder.ts", + "deprecated": false, + "trackAdoption": false, + "initialIsOpen": false + }, { "parentPluginId": "controls", "id": "def-public.CONTROL_GROUP_TYPE", @@ -4472,29 +4812,79 @@ }, { "parentPluginId": "controls", - "id": "def-public.OPTIONS_LIST_CONTROL", - "type": "string", - "tags": [], - "label": "OPTIONS_LIST_CONTROL", - "description": [], - "signature": [ - "\"optionsListControl\"" - ], - "path": "src/plugins/controls/common/options_list/types.ts", - "deprecated": false, - "trackAdoption": false, - "initialIsOpen": false - }, - { - "parentPluginId": "controls", - "id": "def-public.RANGE_SLIDER_CONTROL", - "type": "string", + "id": "def-public.DataControlInput", + "type": "Type", "tags": [], - "label": "RANGE_SLIDER_CONTROL", + "label": "DataControlInput", "description": [], "signature": [ - "\"rangeSliderControl\"" - ], + { + "pluginId": "embeddable", + "scope": "common", + "docId": "kibEmbeddablePluginApi", + "section": "def-common.EmbeddableInput", + "text": "EmbeddableInput" + }, + " & { query?: ", + { + "pluginId": "@kbn/es-query", + "scope": "common", + "docId": "kibKbnEsQueryPluginApi", + "section": "def-common.Query", + "text": "Query" + }, + " | undefined; filters?: ", + { + "pluginId": "@kbn/es-query", + "scope": "common", + "docId": "kibKbnEsQueryPluginApi", + "section": "def-common.Filter", + "text": "Filter" + }, + "[] | undefined; timeRange?: ", + { + "pluginId": "@kbn/es-query", + "scope": "common", + "docId": "kibKbnEsQueryPluginApi", + "section": "def-common.TimeRange", + "text": "TimeRange" + }, + " | undefined; timeslice?: [number, number] | undefined; controlStyle?: ", + "ControlStyle", + " | undefined; ignoreParentSettings?: ", + "ParentIgnoreSettings", + " | undefined; } & { fieldName: string; parentFieldName?: string | undefined; childFieldName?: string | undefined; dataViewId: string; }" + ], + "path": "src/plugins/controls/common/types.ts", + "deprecated": false, + "trackAdoption": false, + "initialIsOpen": false + }, + { + "parentPluginId": "controls", + "id": "def-public.OPTIONS_LIST_CONTROL", + "type": "string", + "tags": [], + "label": "OPTIONS_LIST_CONTROL", + "description": [], + "signature": [ + "\"optionsListControl\"" + ], + "path": "src/plugins/controls/common/options_list/types.ts", + "deprecated": false, + "trackAdoption": false, + "initialIsOpen": false + }, + { + "parentPluginId": "controls", + "id": "def-public.RANGE_SLIDER_CONTROL", + "type": "string", + "tags": [], + "label": "RANGE_SLIDER_CONTROL", + "description": [], + "signature": [ + "\"rangeSliderControl\"" + ], "path": "src/plugins/controls/common/range_slider/types.ts", "deprecated": false, "trackAdoption": false, @@ -4516,7 +4906,289 @@ "initialIsOpen": false } ], - "objects": [] + "objects": [ + { + "parentPluginId": "controls", + "id": "def-public.controlGroupInputBuilder", + "type": "Object", + "tags": [], + "label": "controlGroupInputBuilder", + "description": [], + "path": "src/plugins/controls/public/control_group/control_group_input_builder.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "controls", + "id": "def-public.controlGroupInputBuilder.addDataControlFromField", + "type": "Function", + "tags": [], + "label": "addDataControlFromField", + "description": [], + "signature": [ + "(initialInput: Partial<", + { + "pluginId": "controls", + "scope": "common", + "docId": "kibControlsPluginApi", + "section": "def-common.ControlGroupInput", + "text": "ControlGroupInput" + }, + ">, controlProps: ", + { + "pluginId": "controls", + "scope": "public", + "docId": "kibControlsPluginApi", + "section": "def-public.AddDataControlProps", + "text": "AddDataControlProps" + }, + ") => Promise" + ], + "path": "src/plugins/controls/public/control_group/control_group_input_builder.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "controls", + "id": "def-public.controlGroupInputBuilder.addDataControlFromField.$1", + "type": "Object", + "tags": [], + "label": "initialInput", + "description": [], + "signature": [ + "Partial<", + { + "pluginId": "controls", + "scope": "common", + "docId": "kibControlsPluginApi", + "section": "def-common.ControlGroupInput", + "text": "ControlGroupInput" + }, + ">" + ], + "path": "src/plugins/controls/public/control_group/control_group_input_builder.ts", + "deprecated": false, + "trackAdoption": false, + "isRequired": true + }, + { + "parentPluginId": "controls", + "id": "def-public.controlGroupInputBuilder.addDataControlFromField.$2", + "type": "Object", + "tags": [], + "label": "controlProps", + "description": [], + "signature": [ + { + "pluginId": "controls", + "scope": "public", + "docId": "kibControlsPluginApi", + "section": "def-public.AddDataControlProps", + "text": "AddDataControlProps" + } + ], + "path": "src/plugins/controls/public/control_group/control_group_input_builder.ts", + "deprecated": false, + "trackAdoption": false, + "isRequired": true + } + ], + "returnComment": [] + }, + { + "parentPluginId": "controls", + "id": "def-public.controlGroupInputBuilder.addOptionsListControl", + "type": "Function", + "tags": [], + "label": "addOptionsListControl", + "description": [], + "signature": [ + "(initialInput: Partial<", + { + "pluginId": "controls", + "scope": "common", + "docId": "kibControlsPluginApi", + "section": "def-common.ControlGroupInput", + "text": "ControlGroupInput" + }, + ">, controlProps: ", + { + "pluginId": "controls", + "scope": "public", + "docId": "kibControlsPluginApi", + "section": "def-public.AddOptionsListControlProps", + "text": "AddOptionsListControlProps" + }, + ") => void" + ], + "path": "src/plugins/controls/public/control_group/control_group_input_builder.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "controls", + "id": "def-public.controlGroupInputBuilder.addOptionsListControl.$1", + "type": "Object", + "tags": [], + "label": "initialInput", + "description": [], + "signature": [ + "Partial<", + { + "pluginId": "controls", + "scope": "common", + "docId": "kibControlsPluginApi", + "section": "def-common.ControlGroupInput", + "text": "ControlGroupInput" + }, + ">" + ], + "path": "src/plugins/controls/public/control_group/control_group_input_builder.ts", + "deprecated": false, + "trackAdoption": false, + "isRequired": true + }, + { + "parentPluginId": "controls", + "id": "def-public.controlGroupInputBuilder.addOptionsListControl.$2", + "type": "CompoundType", + "tags": [], + "label": "controlProps", + "description": [], + "signature": [ + { + "pluginId": "controls", + "scope": "public", + "docId": "kibControlsPluginApi", + "section": "def-public.AddOptionsListControlProps", + "text": "AddOptionsListControlProps" + } + ], + "path": "src/plugins/controls/public/control_group/control_group_input_builder.ts", + "deprecated": false, + "trackAdoption": false, + "isRequired": true + } + ], + "returnComment": [] + }, + { + "parentPluginId": "controls", + "id": "def-public.controlGroupInputBuilder.addRangeSliderControl", + "type": "Function", + "tags": [], + "label": "addRangeSliderControl", + "description": [], + "signature": [ + "(initialInput: Partial<", + { + "pluginId": "controls", + "scope": "common", + "docId": "kibControlsPluginApi", + "section": "def-common.ControlGroupInput", + "text": "ControlGroupInput" + }, + ">, controlProps: ", + "AddRangeSliderControlProps", + ") => void" + ], + "path": "src/plugins/controls/public/control_group/control_group_input_builder.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "controls", + "id": "def-public.controlGroupInputBuilder.addRangeSliderControl.$1", + "type": "Object", + "tags": [], + "label": "initialInput", + "description": [], + "signature": [ + "Partial<", + { + "pluginId": "controls", + "scope": "common", + "docId": "kibControlsPluginApi", + "section": "def-common.ControlGroupInput", + "text": "ControlGroupInput" + }, + ">" + ], + "path": "src/plugins/controls/public/control_group/control_group_input_builder.ts", + "deprecated": false, + "trackAdoption": false, + "isRequired": true + }, + { + "parentPluginId": "controls", + "id": "def-public.controlGroupInputBuilder.addRangeSliderControl.$2", + "type": "CompoundType", + "tags": [], + "label": "controlProps", + "description": [], + "signature": [ + "AddRangeSliderControlProps" + ], + "path": "src/plugins/controls/public/control_group/control_group_input_builder.ts", + "deprecated": false, + "trackAdoption": false, + "isRequired": true + } + ], + "returnComment": [] + }, + { + "parentPluginId": "controls", + "id": "def-public.controlGroupInputBuilder.addTimeSliderControl", + "type": "Function", + "tags": [], + "label": "addTimeSliderControl", + "description": [], + "signature": [ + "(initialInput: Partial<", + { + "pluginId": "controls", + "scope": "common", + "docId": "kibControlsPluginApi", + "section": "def-common.ControlGroupInput", + "text": "ControlGroupInput" + }, + ">) => void" + ], + "path": "src/plugins/controls/public/control_group/control_group_input_builder.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "controls", + "id": "def-public.controlGroupInputBuilder.addTimeSliderControl.$1", + "type": "Object", + "tags": [], + "label": "initialInput", + "description": [], + "signature": [ + "Partial<", + { + "pluginId": "controls", + "scope": "common", + "docId": "kibControlsPluginApi", + "section": "def-common.ControlGroupInput", + "text": "ControlGroupInput" + }, + ">" + ], + "path": "src/plugins/controls/public/control_group/control_group_input_builder.ts", + "deprecated": false, + "trackAdoption": false, + "isRequired": true + } + ], + "returnComment": [] + } + ], + "initialIsOpen": false + } + ] }, "server": { "classes": [], diff --git a/api_docs/controls.mdx b/api_docs/controls.mdx index 6fb0a92d0cad0..722b77d934f4f 100644 --- a/api_docs/controls.mdx +++ b/api_docs/controls.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/controls title: "controls" image: https://source.unsplash.com/400x175/?github description: API docs for the controls plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'controls'] --- import controlsObj from './controls.devdocs.json'; @@ -21,10 +21,13 @@ Contact [Kibana Presentation](https://github.com/orgs/elastic/teams/kibana-prese | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 245 | 0 | 236 | 9 | +| 268 | 0 | 259 | 10 | ## Client +### Objects + + ### Functions diff --git a/api_docs/core.devdocs.json b/api_docs/core.devdocs.json index 6f0a2860610a5..c10a629fb75c7 100644 --- a/api_docs/core.devdocs.json +++ b/api_docs/core.devdocs.json @@ -13186,11 +13186,11 @@ }, { "plugin": "alerting", - "path": "x-pack/plugins/alerting/server/types.ts" + "path": "x-pack/plugins/alerting/server/rules_client/common/inject_references.ts" }, { "plugin": "alerting", - "path": "x-pack/plugins/alerting/server/types.ts" + "path": "x-pack/plugins/alerting/server/rules_client/common/inject_references.ts" }, { "plugin": "alerting", @@ -13218,11 +13218,11 @@ }, { "plugin": "alerting", - "path": "x-pack/plugins/alerting/server/rules_client/rules_client.ts" + "path": "x-pack/plugins/alerting/server/types.ts" }, { "plugin": "alerting", - "path": "x-pack/plugins/alerting/server/rules_client/rules_client.ts" + "path": "x-pack/plugins/alerting/server/types.ts" }, { "plugin": "canvas", @@ -17081,6 +17081,29 @@ "path": "src/plugins/discover/server/ui_settings.ts" } ] + }, + { + "parentPluginId": "core", + "id": "def-public.UiSettingsParams.scope", + "type": "CompoundType", + "tags": [], + "label": "scope", + "description": [ + "\nScope of the setting. `Global` denotes a setting globally available across namespaces. `Namespace` denotes a setting\nscoped to a namespace. The default value is 'namespace'" + ], + "signature": [ + { + "pluginId": "@kbn/core-ui-settings-common", + "scope": "common", + "docId": "kibKbnCoreUiSettingsCommonPluginApi", + "section": "def-common.UiSettingsScope", + "text": "UiSettingsScope" + }, + " | undefined" + ], + "path": "packages/core/ui-settings/core-ui-settings-common/src/ui_settings.ts", + "deprecated": false, + "trackAdoption": false } ], "initialIsOpen": false @@ -17724,7 +17747,7 @@ "section": "def-common.AppStatus", "text": "AppStatus" }, - " | undefined; searchable?: boolean | undefined; deepLinks?: ", + " | undefined; tooltip?: string | undefined; searchable?: boolean | undefined; deepLinks?: ", { "pluginId": "@kbn/core-application-browser", "scope": "common", @@ -17740,7 +17763,7 @@ "section": "def-common.AppNavLinkStatus", "text": "AppNavLinkStatus" }, - " | undefined; defaultPath?: string | undefined; tooltip?: string | undefined; }" + " | undefined; defaultPath?: string | undefined; }" ], "path": "packages/core/application/core-application-browser/src/application.ts", "deprecated": false, @@ -18247,7 +18270,15 @@ "section": "def-common.DeprecationSettings", "text": "DeprecationSettings" }, - " | undefined; order?: number | undefined; metric?: { type: string; name: string; } | undefined; }" + " | undefined; order?: number | undefined; metric?: { type: string; name: string; } | undefined; scope?: ", + { + "pluginId": "@kbn/core-ui-settings-common", + "scope": "common", + "docId": "kibKbnCoreUiSettingsCommonPluginApi", + "section": "def-common.UiSettingsScope", + "text": "UiSettingsScope" + }, + " | undefined; }" ], "path": "packages/core/ui-settings/core-ui-settings-common/src/ui_settings.ts", "deprecated": false, @@ -49019,11 +49050,11 @@ }, { "plugin": "alerting", - "path": "x-pack/plugins/alerting/server/types.ts" + "path": "x-pack/plugins/alerting/server/rules_client/common/inject_references.ts" }, { "plugin": "alerting", - "path": "x-pack/plugins/alerting/server/types.ts" + "path": "x-pack/plugins/alerting/server/rules_client/common/inject_references.ts" }, { "plugin": "alerting", @@ -49051,11 +49082,11 @@ }, { "plugin": "alerting", - "path": "x-pack/plugins/alerting/server/rules_client/rules_client.ts" + "path": "x-pack/plugins/alerting/server/types.ts" }, { "plugin": "alerting", - "path": "x-pack/plugins/alerting/server/rules_client/rules_client.ts" + "path": "x-pack/plugins/alerting/server/types.ts" }, { "plugin": "canvas", @@ -59390,6 +59421,29 @@ "path": "src/plugins/discover/server/ui_settings.ts" } ] + }, + { + "parentPluginId": "core", + "id": "def-server.UiSettingsParams.scope", + "type": "CompoundType", + "tags": [], + "label": "scope", + "description": [ + "\nScope of the setting. `Global` denotes a setting globally available across namespaces. `Namespace` denotes a setting\nscoped to a namespace. The default value is 'namespace'" + ], + "signature": [ + { + "pluginId": "@kbn/core-ui-settings-common", + "scope": "common", + "docId": "kibKbnCoreUiSettingsCommonPluginApi", + "section": "def-common.UiSettingsScope", + "text": "UiSettingsScope" + }, + " | undefined" + ], + "path": "packages/core/ui-settings/core-ui-settings-common/src/ui_settings.ts", + "deprecated": false, + "trackAdoption": false } ], "initialIsOpen": false @@ -64088,7 +64142,15 @@ "section": "def-common.DeprecationSettings", "text": "DeprecationSettings" }, - " | undefined; order?: number | undefined; metric?: { type: string; name: string; } | undefined; }" + " | undefined; order?: number | undefined; metric?: { type: string; name: string; } | undefined; scope?: ", + { + "pluginId": "@kbn/core-ui-settings-common", + "scope": "common", + "docId": "kibKbnCoreUiSettingsCommonPluginApi", + "section": "def-common.UiSettingsScope", + "text": "UiSettingsScope" + }, + " | undefined; }" ], "path": "packages/core/ui-settings/core-ui-settings-common/src/ui_settings.ts", "deprecated": false, diff --git a/api_docs/core.mdx b/api_docs/core.mdx index 7d04c82f22522..ff5dc9a59fdb8 100644 --- a/api_docs/core.mdx +++ b/api_docs/core.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/core title: "core" image: https://source.unsplash.com/400x175/?github description: API docs for the core plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'core'] --- import coreObj from './core.devdocs.json'; @@ -21,7 +21,7 @@ Contact [Kibana Core](https://github.com/orgs/elastic/teams/kibana-core) for que | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 2796 | 17 | 1007 | 0 | +| 2798 | 17 | 1007 | 0 | ## Client diff --git a/api_docs/custom_integrations.mdx b/api_docs/custom_integrations.mdx index 1d40b6a834755..576f634d02c4f 100644 --- a/api_docs/custom_integrations.mdx +++ b/api_docs/custom_integrations.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/customIntegrations title: "customIntegrations" image: https://source.unsplash.com/400x175/?github description: API docs for the customIntegrations plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'customIntegrations'] --- import customIntegrationsObj from './custom_integrations.devdocs.json'; diff --git a/api_docs/dashboard.mdx b/api_docs/dashboard.mdx index 1ee93c4d72b46..0ed694db26020 100644 --- a/api_docs/dashboard.mdx +++ b/api_docs/dashboard.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/dashboard title: "dashboard" image: https://source.unsplash.com/400x175/?github description: API docs for the dashboard plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'dashboard'] --- import dashboardObj from './dashboard.devdocs.json'; diff --git a/api_docs/dashboard_enhanced.mdx b/api_docs/dashboard_enhanced.mdx index d81753599a3b2..37580b9406278 100644 --- a/api_docs/dashboard_enhanced.mdx +++ b/api_docs/dashboard_enhanced.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/dashboardEnhanced title: "dashboardEnhanced" image: https://source.unsplash.com/400x175/?github description: API docs for the dashboardEnhanced plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'dashboardEnhanced'] --- import dashboardEnhancedObj from './dashboard_enhanced.devdocs.json'; diff --git a/api_docs/data.devdocs.json b/api_docs/data.devdocs.json index cd54583193b4b..f64011e1dfda5 100644 --- a/api_docs/data.devdocs.json +++ b/api_docs/data.devdocs.json @@ -7788,6 +7788,20 @@ "path": "src/plugins/data_views/common/types.ts", "deprecated": false, "trackAdoption": false + }, + { + "parentPluginId": "data", + "id": "def-public.GetFieldsOptions.includeUnmapped", + "type": "CompoundType", + "tags": [], + "label": "includeUnmapped", + "description": [], + "signature": [ + "boolean | undefined" + ], + "path": "src/plugins/data_views/common/types.ts", + "deprecated": false, + "trackAdoption": false } ], "initialIsOpen": false @@ -12345,10 +12359,6 @@ "plugin": "graph", "path": "x-pack/plugins/graph/public/plugin.ts" }, - { - "plugin": "securitySolution", - "path": "x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts" - }, { "plugin": "stackAlerts", "path": "x-pack/plugins/stack_alerts/public/rule_types/geo_containment/query_builder/expressions/boundary_index_expression.tsx" @@ -12927,7 +12937,7 @@ }, { "plugin": "securitySolution", - "path": "x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/utils.ts" + "path": "x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts" }, { "plugin": "securitySolution", @@ -12935,7 +12945,7 @@ }, { "plugin": "securitySolution", - "path": "x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts" + "path": "x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/utils.ts" }, { "plugin": "securitySolution", @@ -13097,10 +13107,6 @@ "plugin": "dataViews", "path": "src/plugins/data_views/common/data_views/data_view.ts" }, - { - "plugin": "dataViewEditor", - "path": "src/plugins/data_view_editor/public/components/data_view_editor_flyout_content.tsx" - }, { "plugin": "unifiedSearch", "path": "src/plugins/unified_search/public/autocomplete/providers/value_suggestion_provider.ts" @@ -13133,6 +13139,14 @@ "plugin": "savedObjectsManagement", "path": "src/plugins/saved_objects_management/public/management_section/objects_table/components/flyout.tsx" }, + { + "plugin": "controls", + "path": "src/plugins/controls/public/services/options_list/options_list_service.ts" + }, + { + "plugin": "controls", + "path": "src/plugins/controls/public/services/options_list/options_list_service.ts" + }, { "plugin": "unifiedFieldList", "path": "src/plugins/unified_field_list/public/services/field_stats/load_field_stats.ts" @@ -13157,14 +13171,6 @@ "plugin": "unifiedFieldList", "path": "src/plugins/unified_field_list/public/hooks/use_existing_fields.ts" }, - { - "plugin": "controls", - "path": "src/plugins/controls/public/services/options_list/options_list_service.ts" - }, - { - "plugin": "controls", - "path": "src/plugins/controls/public/services/options_list/options_list_service.ts" - }, { "plugin": "lens", "path": "x-pack/plugins/lens/public/data_views_service/loader.ts" @@ -13377,6 +13383,10 @@ "plugin": "ml", "path": "x-pack/plugins/ml/public/application/jobs/new_job/recognize/page.tsx" }, + { + "plugin": "ml", + "path": "x-pack/plugins/ml/public/application/jobs/new_job/recognize/page.tsx" + }, { "plugin": "ml", "path": "x-pack/plugins/ml/public/application/jobs/new_job/job_from_lens/quick_create_job.ts" @@ -13489,6 +13499,10 @@ "plugin": "securitySolution", "path": "x-pack/plugins/security_solution/server/lib/sourcerer/routes/index.ts" }, + { + "plugin": "timelines", + "path": "x-pack/plugins/timelines/server/search_strategy/index_fields/index.ts" + }, { "plugin": "stackAlerts", "path": "x-pack/plugins/stack_alerts/public/rule_types/geo_containment/query_builder/expressions/boundary_index_expression.tsx" @@ -13533,10 +13547,6 @@ "plugin": "synthetics", "path": "x-pack/plugins/synthetics/public/legacy_uptime/components/overview/alerts/monitor_expressions/filters_expression_select.tsx" }, - { - "plugin": "timelines", - "path": "x-pack/plugins/timelines/server/search_strategy/index_fields/index.ts" - }, { "plugin": "transform", "path": "x-pack/plugins/transform/public/app/sections/create_transform/components/step_details/step_details_form.tsx" @@ -17012,7 +17022,7 @@ "\n Get a list of field objects for an index pattern that may contain wildcards\n" ], "signature": [ - "(options: { pattern: string | string[]; metaFields?: string[] | undefined; fieldCapsOptions?: { allow_no_indices: boolean; } | undefined; type?: string | undefined; rollupIndex?: string | undefined; filter?: ", + "(options: { pattern: string | string[]; metaFields?: string[] | undefined; fieldCapsOptions?: { allow_no_indices: boolean; includeUnmapped?: boolean | undefined; } | undefined; type?: string | undefined; rollupIndex?: string | undefined; filter?: ", "QueryDslQueryContainer", " | undefined; }) => Promise<{ fields: ", { @@ -17075,7 +17085,7 @@ "label": "fieldCapsOptions", "description": [], "signature": [ - "{ allow_no_indices: boolean; } | undefined" + "{ allow_no_indices: boolean; includeUnmapped?: boolean | undefined; } | undefined" ], "path": "src/plugins/data_views/server/fetcher/index_patterns_fetcher.ts", "deprecated": false, @@ -20629,7 +20639,7 @@ }, { "plugin": "securitySolution", - "path": "x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/utils.ts" + "path": "x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts" }, { "plugin": "securitySolution", @@ -20637,7 +20647,7 @@ }, { "plugin": "securitySolution", - "path": "x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts" + "path": "x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/utils.ts" }, { "plugin": "securitySolution", @@ -20799,10 +20809,6 @@ "plugin": "dataViews", "path": "src/plugins/data_views/common/data_views/data_view.ts" }, - { - "plugin": "dataViewEditor", - "path": "src/plugins/data_view_editor/public/components/data_view_editor_flyout_content.tsx" - }, { "plugin": "unifiedSearch", "path": "src/plugins/unified_search/public/autocomplete/providers/value_suggestion_provider.ts" @@ -20835,6 +20841,14 @@ "plugin": "savedObjectsManagement", "path": "src/plugins/saved_objects_management/public/management_section/objects_table/components/flyout.tsx" }, + { + "plugin": "controls", + "path": "src/plugins/controls/public/services/options_list/options_list_service.ts" + }, + { + "plugin": "controls", + "path": "src/plugins/controls/public/services/options_list/options_list_service.ts" + }, { "plugin": "unifiedFieldList", "path": "src/plugins/unified_field_list/public/services/field_stats/load_field_stats.ts" @@ -20859,14 +20873,6 @@ "plugin": "unifiedFieldList", "path": "src/plugins/unified_field_list/public/hooks/use_existing_fields.ts" }, - { - "plugin": "controls", - "path": "src/plugins/controls/public/services/options_list/options_list_service.ts" - }, - { - "plugin": "controls", - "path": "src/plugins/controls/public/services/options_list/options_list_service.ts" - }, { "plugin": "lens", "path": "x-pack/plugins/lens/public/data_views_service/loader.ts" @@ -21079,6 +21085,10 @@ "plugin": "ml", "path": "x-pack/plugins/ml/public/application/jobs/new_job/recognize/page.tsx" }, + { + "plugin": "ml", + "path": "x-pack/plugins/ml/public/application/jobs/new_job/recognize/page.tsx" + }, { "plugin": "ml", "path": "x-pack/plugins/ml/public/application/jobs/new_job/job_from_lens/quick_create_job.ts" @@ -21191,6 +21201,10 @@ "plugin": "securitySolution", "path": "x-pack/plugins/security_solution/server/lib/sourcerer/routes/index.ts" }, + { + "plugin": "timelines", + "path": "x-pack/plugins/timelines/server/search_strategy/index_fields/index.ts" + }, { "plugin": "stackAlerts", "path": "x-pack/plugins/stack_alerts/public/rule_types/geo_containment/query_builder/expressions/boundary_index_expression.tsx" @@ -21235,10 +21249,6 @@ "plugin": "synthetics", "path": "x-pack/plugins/synthetics/public/legacy_uptime/components/overview/alerts/monitor_expressions/filters_expression_select.tsx" }, - { - "plugin": "timelines", - "path": "x-pack/plugins/timelines/server/search_strategy/index_fields/index.ts" - }, { "plugin": "transform", "path": "x-pack/plugins/transform/public/app/sections/create_transform/components/step_details/step_details_form.tsx" @@ -26663,6 +26673,20 @@ "path": "src/plugins/data_views/common/types.ts", "deprecated": false, "trackAdoption": false + }, + { + "parentPluginId": "data", + "id": "def-common.GetFieldsOptions.includeUnmapped", + "type": "CompoundType", + "tags": [], + "label": "includeUnmapped", + "description": [], + "signature": [ + "boolean | undefined" + ], + "path": "src/plugins/data_views/common/types.ts", + "deprecated": false, + "trackAdoption": false } ], "initialIsOpen": false diff --git a/api_docs/data.mdx b/api_docs/data.mdx index f72b755f35c90..6cac2cae26de2 100644 --- a/api_docs/data.mdx +++ b/api_docs/data.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/data title: "data" image: https://source.unsplash.com/400x175/?github description: API docs for the data plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'data'] --- import dataObj from './data.devdocs.json'; @@ -21,7 +21,7 @@ Contact [Data Discovery](https://github.com/orgs/elastic/teams/kibana-data-disco | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 3265 | 119 | 2553 | 27 | +| 3269 | 119 | 2557 | 27 | ## Client diff --git a/api_docs/data_query.mdx b/api_docs/data_query.mdx index 37d2e22767948..9cefe6d57154f 100644 --- a/api_docs/data_query.mdx +++ b/api_docs/data_query.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/data-query title: "data.query" image: https://source.unsplash.com/400x175/?github description: API docs for the data.query plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'data.query'] --- import dataQueryObj from './data_query.devdocs.json'; @@ -21,7 +21,7 @@ Contact [Data Discovery](https://github.com/orgs/elastic/teams/kibana-data-disco | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 3265 | 119 | 2553 | 27 | +| 3269 | 119 | 2557 | 27 | ## Client diff --git a/api_docs/data_search.devdocs.json b/api_docs/data_search.devdocs.json index 0fedf85ba91d7..26a9e5917f5c0 100644 --- a/api_docs/data_search.devdocs.json +++ b/api_docs/data_search.devdocs.json @@ -15523,7 +15523,7 @@ "label": "handleRequest", "description": [], "signature": [ - "({ abortSignal, aggs, filters, indexPattern, inspectorAdapters, query, searchSessionId, searchSourceService, timeFields, timeRange, disableShardWarnings, getNow, executionContext, }: ", + "({ abortSignal, aggs, filters, indexPattern, inspectorAdapters, query, searchSessionId, searchSourceService, timeFields, timeRange, disableShardWarnings, getNow, executionContext, title, description, }: ", { "pluginId": "data", "scope": "common", @@ -15552,7 +15552,7 @@ "id": "def-common.handleRequest.$1", "type": "Object", "tags": [], - "label": "{\n abortSignal,\n aggs,\n filters,\n indexPattern,\n inspectorAdapters,\n query,\n searchSessionId,\n searchSourceService,\n timeFields,\n timeRange,\n disableShardWarnings,\n getNow,\n executionContext,\n}", + "label": "{\n abortSignal,\n aggs,\n filters,\n indexPattern,\n inspectorAdapters,\n query,\n searchSessionId,\n searchSourceService,\n timeFields,\n timeRange,\n disableShardWarnings,\n getNow,\n executionContext,\n title,\n description,\n}", "description": [], "signature": [ { @@ -29414,6 +29414,34 @@ "path": "src/plugins/data/common/search/expressions/esaggs/request_handler.ts", "deprecated": false, "trackAdoption": false + }, + { + "parentPluginId": "data", + "id": "def-common.RequestHandlerParams.title", + "type": "string", + "tags": [], + "label": "title", + "description": [], + "signature": [ + "string | undefined" + ], + "path": "src/plugins/data/common/search/expressions/esaggs/request_handler.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "data", + "id": "def-common.RequestHandlerParams.description", + "type": "string", + "tags": [], + "label": "description", + "description": [], + "signature": [ + "string | undefined" + ], + "path": "src/plugins/data/common/search/expressions/esaggs/request_handler.ts", + "deprecated": false, + "trackAdoption": false } ], "initialIsOpen": false diff --git a/api_docs/data_search.mdx b/api_docs/data_search.mdx index b0cfc765614a8..d46e3d8717cf5 100644 --- a/api_docs/data_search.mdx +++ b/api_docs/data_search.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/data-search title: "data.search" image: https://source.unsplash.com/400x175/?github description: API docs for the data.search plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'data.search'] --- import dataSearchObj from './data_search.devdocs.json'; @@ -21,7 +21,7 @@ Contact [Data Discovery](https://github.com/orgs/elastic/teams/kibana-data-disco | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 3265 | 119 | 2553 | 27 | +| 3269 | 119 | 2557 | 27 | ## Client diff --git a/api_docs/data_view_editor.mdx b/api_docs/data_view_editor.mdx index 0bf5478c84cea..f64a65e3d3041 100644 --- a/api_docs/data_view_editor.mdx +++ b/api_docs/data_view_editor.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/dataViewEditor title: "dataViewEditor" image: https://source.unsplash.com/400x175/?github description: API docs for the dataViewEditor plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'dataViewEditor'] --- import dataViewEditorObj from './data_view_editor.devdocs.json'; diff --git a/api_docs/data_view_field_editor.mdx b/api_docs/data_view_field_editor.mdx index 5fd612cc57ed5..1ab12b52e9343 100644 --- a/api_docs/data_view_field_editor.mdx +++ b/api_docs/data_view_field_editor.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/dataViewFieldEditor title: "dataViewFieldEditor" image: https://source.unsplash.com/400x175/?github description: API docs for the dataViewFieldEditor plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'dataViewFieldEditor'] --- import dataViewFieldEditorObj from './data_view_field_editor.devdocs.json'; diff --git a/api_docs/data_view_management.mdx b/api_docs/data_view_management.mdx index 6bb9325bd6b0b..40818c0712d28 100644 --- a/api_docs/data_view_management.mdx +++ b/api_docs/data_view_management.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/dataViewManagement title: "dataViewManagement" image: https://source.unsplash.com/400x175/?github description: API docs for the dataViewManagement plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'dataViewManagement'] --- import dataViewManagementObj from './data_view_management.devdocs.json'; diff --git a/api_docs/data_views.devdocs.json b/api_docs/data_views.devdocs.json index 928e45600eeec..608e1f45bd5b0 100644 --- a/api_docs/data_views.devdocs.json +++ b/api_docs/data_views.devdocs.json @@ -77,7 +77,7 @@ }, { "plugin": "securitySolution", - "path": "x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/utils.ts" + "path": "x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts" }, { "plugin": "securitySolution", @@ -85,7 +85,7 @@ }, { "plugin": "securitySolution", - "path": "x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts" + "path": "x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/utils.ts" }, { "plugin": "securitySolution", @@ -263,10 +263,6 @@ "plugin": "data", "path": "src/plugins/data/common/search/aggs/param_types/field.ts" }, - { - "plugin": "dataViewEditor", - "path": "src/plugins/data_view_editor/public/components/data_view_editor_flyout_content.tsx" - }, { "plugin": "unifiedSearch", "path": "src/plugins/unified_search/public/autocomplete/providers/value_suggestion_provider.ts" @@ -291,6 +287,14 @@ "plugin": "savedObjectsManagement", "path": "src/plugins/saved_objects_management/public/management_section/objects_table/components/flyout.tsx" }, + { + "plugin": "controls", + "path": "src/plugins/controls/public/services/options_list/options_list_service.ts" + }, + { + "plugin": "controls", + "path": "src/plugins/controls/public/services/options_list/options_list_service.ts" + }, { "plugin": "unifiedFieldList", "path": "src/plugins/unified_field_list/public/services/field_stats/load_field_stats.ts" @@ -315,14 +319,6 @@ "plugin": "unifiedFieldList", "path": "src/plugins/unified_field_list/public/hooks/use_existing_fields.ts" }, - { - "plugin": "controls", - "path": "src/plugins/controls/public/services/options_list/options_list_service.ts" - }, - { - "plugin": "controls", - "path": "src/plugins/controls/public/services/options_list/options_list_service.ts" - }, { "plugin": "lens", "path": "x-pack/plugins/lens/public/data_views_service/loader.ts" @@ -535,6 +531,10 @@ "plugin": "ml", "path": "x-pack/plugins/ml/public/application/jobs/new_job/recognize/page.tsx" }, + { + "plugin": "ml", + "path": "x-pack/plugins/ml/public/application/jobs/new_job/recognize/page.tsx" + }, { "plugin": "ml", "path": "x-pack/plugins/ml/public/application/jobs/new_job/job_from_lens/quick_create_job.ts" @@ -647,6 +647,10 @@ "plugin": "securitySolution", "path": "x-pack/plugins/security_solution/server/lib/sourcerer/routes/index.ts" }, + { + "plugin": "timelines", + "path": "x-pack/plugins/timelines/server/search_strategy/index_fields/index.ts" + }, { "plugin": "stackAlerts", "path": "x-pack/plugins/stack_alerts/public/rule_types/geo_containment/query_builder/expressions/boundary_index_expression.tsx" @@ -691,10 +695,6 @@ "plugin": "synthetics", "path": "x-pack/plugins/synthetics/public/legacy_uptime/components/overview/alerts/monitor_expressions/filters_expression_select.tsx" }, - { - "plugin": "timelines", - "path": "x-pack/plugins/timelines/server/search_strategy/index_fields/index.ts" - }, { "plugin": "transform", "path": "x-pack/plugins/transform/public/app/sections/create_transform/components/step_details/step_details_form.tsx" @@ -8378,7 +8378,7 @@ }, { "plugin": "securitySolution", - "path": "x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/utils.ts" + "path": "x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts" }, { "plugin": "securitySolution", @@ -8386,7 +8386,7 @@ }, { "plugin": "securitySolution", - "path": "x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts" + "path": "x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/utils.ts" }, { "plugin": "securitySolution", @@ -8564,10 +8564,6 @@ "plugin": "data", "path": "src/plugins/data/common/search/aggs/param_types/field.ts" }, - { - "plugin": "dataViewEditor", - "path": "src/plugins/data_view_editor/public/components/data_view_editor_flyout_content.tsx" - }, { "plugin": "unifiedSearch", "path": "src/plugins/unified_search/public/autocomplete/providers/value_suggestion_provider.ts" @@ -8592,6 +8588,14 @@ "plugin": "savedObjectsManagement", "path": "src/plugins/saved_objects_management/public/management_section/objects_table/components/flyout.tsx" }, + { + "plugin": "controls", + "path": "src/plugins/controls/public/services/options_list/options_list_service.ts" + }, + { + "plugin": "controls", + "path": "src/plugins/controls/public/services/options_list/options_list_service.ts" + }, { "plugin": "unifiedFieldList", "path": "src/plugins/unified_field_list/public/services/field_stats/load_field_stats.ts" @@ -8616,14 +8620,6 @@ "plugin": "unifiedFieldList", "path": "src/plugins/unified_field_list/public/hooks/use_existing_fields.ts" }, - { - "plugin": "controls", - "path": "src/plugins/controls/public/services/options_list/options_list_service.ts" - }, - { - "plugin": "controls", - "path": "src/plugins/controls/public/services/options_list/options_list_service.ts" - }, { "plugin": "lens", "path": "x-pack/plugins/lens/public/data_views_service/loader.ts" @@ -8836,6 +8832,10 @@ "plugin": "ml", "path": "x-pack/plugins/ml/public/application/jobs/new_job/recognize/page.tsx" }, + { + "plugin": "ml", + "path": "x-pack/plugins/ml/public/application/jobs/new_job/recognize/page.tsx" + }, { "plugin": "ml", "path": "x-pack/plugins/ml/public/application/jobs/new_job/job_from_lens/quick_create_job.ts" @@ -8948,6 +8948,10 @@ "plugin": "securitySolution", "path": "x-pack/plugins/security_solution/server/lib/sourcerer/routes/index.ts" }, + { + "plugin": "timelines", + "path": "x-pack/plugins/timelines/server/search_strategy/index_fields/index.ts" + }, { "plugin": "stackAlerts", "path": "x-pack/plugins/stack_alerts/public/rule_types/geo_containment/query_builder/expressions/boundary_index_expression.tsx" @@ -8992,10 +8996,6 @@ "plugin": "synthetics", "path": "x-pack/plugins/synthetics/public/legacy_uptime/components/overview/alerts/monitor_expressions/filters_expression_select.tsx" }, - { - "plugin": "timelines", - "path": "x-pack/plugins/timelines/server/search_strategy/index_fields/index.ts" - }, { "plugin": "transform", "path": "x-pack/plugins/transform/public/app/sections/create_transform/components/step_details/step_details_form.tsx" @@ -12785,7 +12785,7 @@ "\n Get a list of field objects for an index pattern that may contain wildcards\n" ], "signature": [ - "(options: { pattern: string | string[]; metaFields?: string[] | undefined; fieldCapsOptions?: { allow_no_indices: boolean; } | undefined; type?: string | undefined; rollupIndex?: string | undefined; filter?: ", + "(options: { pattern: string | string[]; metaFields?: string[] | undefined; fieldCapsOptions?: { allow_no_indices: boolean; includeUnmapped?: boolean | undefined; } | undefined; type?: string | undefined; rollupIndex?: string | undefined; filter?: ", "QueryDslQueryContainer", " | undefined; }) => Promise<{ fields: ", { @@ -12848,7 +12848,7 @@ "label": "fieldCapsOptions", "description": [], "signature": [ - "{ allow_no_indices: boolean; } | undefined" + "{ allow_no_indices: boolean; includeUnmapped?: boolean | undefined; } | undefined" ], "path": "src/plugins/data_views/server/fetcher/index_patterns_fetcher.ts", "deprecated": false, @@ -15760,7 +15760,7 @@ }, { "plugin": "securitySolution", - "path": "x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/utils.ts" + "path": "x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts" }, { "plugin": "securitySolution", @@ -15768,7 +15768,7 @@ }, { "plugin": "securitySolution", - "path": "x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts" + "path": "x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/utils.ts" }, { "plugin": "securitySolution", @@ -15946,10 +15946,6 @@ "plugin": "data", "path": "src/plugins/data/common/search/aggs/param_types/field.ts" }, - { - "plugin": "dataViewEditor", - "path": "src/plugins/data_view_editor/public/components/data_view_editor_flyout_content.tsx" - }, { "plugin": "unifiedSearch", "path": "src/plugins/unified_search/public/autocomplete/providers/value_suggestion_provider.ts" @@ -15974,6 +15970,14 @@ "plugin": "savedObjectsManagement", "path": "src/plugins/saved_objects_management/public/management_section/objects_table/components/flyout.tsx" }, + { + "plugin": "controls", + "path": "src/plugins/controls/public/services/options_list/options_list_service.ts" + }, + { + "plugin": "controls", + "path": "src/plugins/controls/public/services/options_list/options_list_service.ts" + }, { "plugin": "unifiedFieldList", "path": "src/plugins/unified_field_list/public/services/field_stats/load_field_stats.ts" @@ -15998,14 +16002,6 @@ "plugin": "unifiedFieldList", "path": "src/plugins/unified_field_list/public/hooks/use_existing_fields.ts" }, - { - "plugin": "controls", - "path": "src/plugins/controls/public/services/options_list/options_list_service.ts" - }, - { - "plugin": "controls", - "path": "src/plugins/controls/public/services/options_list/options_list_service.ts" - }, { "plugin": "lens", "path": "x-pack/plugins/lens/public/data_views_service/loader.ts" @@ -16218,6 +16214,10 @@ "plugin": "ml", "path": "x-pack/plugins/ml/public/application/jobs/new_job/recognize/page.tsx" }, + { + "plugin": "ml", + "path": "x-pack/plugins/ml/public/application/jobs/new_job/recognize/page.tsx" + }, { "plugin": "ml", "path": "x-pack/plugins/ml/public/application/jobs/new_job/job_from_lens/quick_create_job.ts" @@ -16330,6 +16330,10 @@ "plugin": "securitySolution", "path": "x-pack/plugins/security_solution/server/lib/sourcerer/routes/index.ts" }, + { + "plugin": "timelines", + "path": "x-pack/plugins/timelines/server/search_strategy/index_fields/index.ts" + }, { "plugin": "stackAlerts", "path": "x-pack/plugins/stack_alerts/public/rule_types/geo_containment/query_builder/expressions/boundary_index_expression.tsx" @@ -16374,10 +16378,6 @@ "plugin": "synthetics", "path": "x-pack/plugins/synthetics/public/legacy_uptime/components/overview/alerts/monitor_expressions/filters_expression_select.tsx" }, - { - "plugin": "timelines", - "path": "x-pack/plugins/timelines/server/search_strategy/index_fields/index.ts" - }, { "plugin": "transform", "path": "x-pack/plugins/transform/public/app/sections/create_transform/components/step_details/step_details_form.tsx" @@ -23434,6 +23434,20 @@ "path": "src/plugins/data_views/common/types.ts", "deprecated": false, "trackAdoption": false + }, + { + "parentPluginId": "dataViews", + "id": "def-common.GetFieldsOptions.includeUnmapped", + "type": "CompoundType", + "tags": [], + "label": "includeUnmapped", + "description": [], + "signature": [ + "boolean | undefined" + ], + "path": "src/plugins/data_views/common/types.ts", + "deprecated": false, + "trackAdoption": false } ], "initialIsOpen": false diff --git a/api_docs/data_views.mdx b/api_docs/data_views.mdx index 9f93c2c9f829c..bd75a26b3a55f 100644 --- a/api_docs/data_views.mdx +++ b/api_docs/data_views.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/dataViews title: "dataViews" image: https://source.unsplash.com/400x175/?github description: API docs for the dataViews plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'dataViews'] --- import dataViewsObj from './data_views.devdocs.json'; @@ -21,7 +21,7 @@ Contact [App Services](https://github.com/orgs/elastic/teams/kibana-app-services | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 1021 | 0 | 227 | 2 | +| 1022 | 0 | 228 | 2 | ## Client diff --git a/api_docs/data_visualizer.mdx b/api_docs/data_visualizer.mdx index 1a4561331bf2d..76681eb22fd84 100644 --- a/api_docs/data_visualizer.mdx +++ b/api_docs/data_visualizer.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/dataVisualizer title: "dataVisualizer" image: https://source.unsplash.com/400x175/?github description: API docs for the dataVisualizer plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'dataVisualizer'] --- import dataVisualizerObj from './data_visualizer.devdocs.json'; diff --git a/api_docs/deprecations_by_api.mdx b/api_docs/deprecations_by_api.mdx index 291edd4254e5d..9390a895cb4c7 100644 --- a/api_docs/deprecations_by_api.mdx +++ b/api_docs/deprecations_by_api.mdx @@ -7,7 +7,7 @@ id: kibDevDocsDeprecationsByApi slug: /kibana-dev-docs/api-meta/deprecated-api-list-by-api title: Deprecated API usage by API description: A list of deprecated APIs, which plugins are still referencing them, and when they need to be removed by. -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana'] --- @@ -28,15 +28,15 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana'] | | @kbn/core-saved-objects-common, savedObjects, embeddable, visualizations, dashboard, fleet, infra, canvas, graph, actions, alerting, enterpriseSearch, securitySolution, taskManager, savedSearch, ml, @kbn/core-saved-objects-server-internal | - | | | core, savedObjects, embeddable, visualizations, dashboard, fleet, infra, canvas, graph, actions, alerting, enterpriseSearch, securitySolution, taskManager, savedSearch, ml, @kbn/core-saved-objects-server-internal | - | | | discover, maps, monitoring | - | -| | @kbn/es-query, securitySolution, timelines, lists, threatIntelligence, dataViews, dataViewEditor, unifiedSearch, triggersActionsUi, savedObjectsManagement, unifiedFieldList, controls, lens, aiops, ml, infra, visTypeTimeseries, apm, observability, dataVisualizer, fleet, canvas, graph, stackAlerts, synthetics, transform, upgradeAssistant, ux, maps, dataViewManagement, inputControlVis, visDefaultEditor, presentationUtil, visTypeTimelion, visTypeVega, discover, data | - | +| | @kbn/es-query, securitySolution, timelines, lists, threatIntelligence, dataViews, unifiedSearch, triggersActionsUi, savedObjectsManagement, controls, unifiedFieldList, lens, aiops, ml, infra, visTypeTimeseries, apm, observability, dataVisualizer, fleet, canvas, graph, stackAlerts, synthetics, transform, upgradeAssistant, ux, maps, dataViewManagement, inputControlVis, visDefaultEditor, presentationUtil, visTypeTimelion, visTypeVega, discover, data | - | | | discover | - | -| | @kbn/es-query, securitySolution, timelines, lists, threatIntelligence, dataViews, dataViewEditor, unifiedSearch, triggersActionsUi, savedObjectsManagement, unifiedFieldList, controls, lens, aiops, ml, infra, visTypeTimeseries, apm, observability, dataVisualizer, fleet, canvas, graph, stackAlerts, synthetics, transform, upgradeAssistant, ux, maps, dataViewManagement, inputControlVis, visDefaultEditor, presentationUtil, visTypeTimelion, visTypeVega, discover, data | - | -| | @kbn/es-query, securitySolution, timelines, lists, threatIntelligence, data, dataViewEditor, unifiedSearch, triggersActionsUi, savedObjectsManagement, unifiedFieldList, controls, lens, aiops, ml, infra, visTypeTimeseries, apm, observability, dataVisualizer, fleet, canvas, graph, stackAlerts, synthetics, transform, upgradeAssistant, ux, maps, dataViewManagement, inputControlVis, visDefaultEditor, presentationUtil, visTypeTimelion, visTypeVega, discover | - | +| | @kbn/es-query, securitySolution, timelines, lists, threatIntelligence, dataViews, unifiedSearch, triggersActionsUi, savedObjectsManagement, controls, unifiedFieldList, lens, aiops, ml, infra, visTypeTimeseries, apm, observability, dataVisualizer, fleet, canvas, graph, stackAlerts, synthetics, transform, upgradeAssistant, ux, maps, dataViewManagement, inputControlVis, visDefaultEditor, presentationUtil, visTypeTimelion, visTypeVega, discover, data | - | +| | @kbn/es-query, securitySolution, timelines, lists, threatIntelligence, data, unifiedSearch, triggersActionsUi, savedObjectsManagement, controls, unifiedFieldList, lens, aiops, ml, infra, visTypeTimeseries, apm, observability, dataVisualizer, fleet, canvas, graph, stackAlerts, synthetics, transform, upgradeAssistant, ux, maps, dataViewManagement, inputControlVis, visDefaultEditor, presentationUtil, visTypeTimelion, visTypeVega, discover | - | | | data, discover, embeddable | - | | | advancedSettings, discover | - | | | advancedSettings, discover | - | | | advancedSettings, discover | - | -| | infra, graph, securitySolution, stackAlerts, inputControlVis, savedObjects | - | +| | infra, graph, stackAlerts, inputControlVis, securitySolution, savedObjects | - | | | securitySolution | - | | | encryptedSavedObjects, actions, data, ml, logstash, securitySolution, cloudChat | - | | | lists, securitySolution, @kbn/securitysolution-io-ts-list-types | - | @@ -88,7 +88,7 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana'] | | @kbn/core-plugins-server-internal | - | | | spaces, security, alerting | 8.8.0 | | | spaces, security, actions, alerting, ml, remoteClusters, graph, indexLifecycleManagement, mapsEms, painlessLab, rollup, searchprofiler, securitySolution, snapshotRestore, transform, upgradeAssistant | 8.8.0 | -| | embeddable, discover, presentationUtil, dashboard, graph | 8.8.0 | +| | embeddable, presentationUtil, dashboard, discover, graph | 8.8.0 | | | apm, security, securitySolution | 8.8.0 | | | apm, security, securitySolution | 8.8.0 | | | @kbn/core-application-browser-internal, @kbn/core-application-browser-mocks, visualizations, dashboard, lens, maps, ml, securitySolution, security | 8.8.0 | diff --git a/api_docs/deprecations_by_plugin.mdx b/api_docs/deprecations_by_plugin.mdx index 092f77fc7fd71..482d455281611 100644 --- a/api_docs/deprecations_by_plugin.mdx +++ b/api_docs/deprecations_by_plugin.mdx @@ -7,7 +7,7 @@ id: kibDevDocsDeprecationsByPlugin slug: /kibana-dev-docs/api-meta/deprecated-api-list-by-plugin title: Deprecated API usage by plugin description: A list of deprecated APIs, which plugins are still referencing them, and when they need to be removed by. -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana'] --- @@ -354,16 +354,6 @@ so TS and code-reference navigation might not highlight them. | -## dataViewEditor - -| Deprecated API | Reference location(s) | Remove By | -| ---------------|-----------|-----------| -| | [data_view_editor_flyout_content.tsx](https://github.com/elastic/kibana/tree/main/src/plugins/data_view_editor/public/components/data_view_editor_flyout_content.tsx#:~:text=title), [data_view_editor_flyout_content.tsx](https://github.com/elastic/kibana/tree/main/src/plugins/data_view_editor/public/components/data_view_editor_flyout_content.tsx#:~:text=title) | - | -| | [data_view_editor_flyout_content.tsx](https://github.com/elastic/kibana/tree/main/src/plugins/data_view_editor/public/components/data_view_editor_flyout_content.tsx#:~:text=title), [data_view_editor_flyout_content.tsx](https://github.com/elastic/kibana/tree/main/src/plugins/data_view_editor/public/components/data_view_editor_flyout_content.tsx#:~:text=title) | - | -| | [data_view_editor_flyout_content.tsx](https://github.com/elastic/kibana/tree/main/src/plugins/data_view_editor/public/components/data_view_editor_flyout_content.tsx#:~:text=title) | - | - - - ## dataViewManagement | Deprecated API | Reference location(s) | Remove By | @@ -671,9 +661,9 @@ so TS and code-reference navigation might not highlight them. | | Deprecated API | Reference location(s) | Remove By | | ---------------|-----------|-----------| -| | [index_patterns.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/ml/server/models/data_frame_analytics/index_patterns.ts#:~:text=title), [rollup.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/ml/server/models/job_service/new_job_caps/rollup.ts#:~:text=title), [alerting_service.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/ml/server/lib/alerts/alerting_service.ts#:~:text=title), [data_recognizer.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/ml/server/models/data_recognizer/data_recognizer.ts#:~:text=title), [index_utils.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/ml/public/application/util/index_utils.ts#:~:text=title), [index_utils.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/ml/public/application/util/index_utils.ts#:~:text=title), [index_utils.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/ml/public/application/util/index_utils.ts#:~:text=title), [new_job_capabilities_service_analytics.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/ml/public/application/services/new_job_capabilities/new_job_capabilities_service_analytics.ts#:~:text=title), [new_job_capabilities_service_analytics.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/ml/public/application/services/new_job_capabilities/new_job_capabilities_service_analytics.ts#:~:text=title), [configuration_step_details.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/configuration_step_details.tsx#:~:text=title)+ 76 more | - | -| | [index_patterns.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/ml/server/models/data_frame_analytics/index_patterns.ts#:~:text=title), [rollup.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/ml/server/models/job_service/new_job_caps/rollup.ts#:~:text=title), [alerting_service.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/ml/server/lib/alerts/alerting_service.ts#:~:text=title), [data_recognizer.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/ml/server/models/data_recognizer/data_recognizer.ts#:~:text=title), [index_utils.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/ml/public/application/util/index_utils.ts#:~:text=title), [index_utils.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/ml/public/application/util/index_utils.ts#:~:text=title), [index_utils.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/ml/public/application/util/index_utils.ts#:~:text=title), [new_job_capabilities_service_analytics.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/ml/public/application/services/new_job_capabilities/new_job_capabilities_service_analytics.ts#:~:text=title), [new_job_capabilities_service_analytics.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/ml/public/application/services/new_job_capabilities/new_job_capabilities_service_analytics.ts#:~:text=title), [configuration_step_details.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/configuration_step_details.tsx#:~:text=title)+ 76 more | - | -| | [index_patterns.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/ml/server/models/data_frame_analytics/index_patterns.ts#:~:text=title), [rollup.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/ml/server/models/job_service/new_job_caps/rollup.ts#:~:text=title), [alerting_service.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/ml/server/lib/alerts/alerting_service.ts#:~:text=title), [data_recognizer.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/ml/server/models/data_recognizer/data_recognizer.ts#:~:text=title), [index_utils.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/ml/public/application/util/index_utils.ts#:~:text=title), [index_utils.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/ml/public/application/util/index_utils.ts#:~:text=title), [index_utils.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/ml/public/application/util/index_utils.ts#:~:text=title), [new_job_capabilities_service_analytics.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/ml/public/application/services/new_job_capabilities/new_job_capabilities_service_analytics.ts#:~:text=title), [new_job_capabilities_service_analytics.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/ml/public/application/services/new_job_capabilities/new_job_capabilities_service_analytics.ts#:~:text=title), [configuration_step_details.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/configuration_step_details.tsx#:~:text=title)+ 33 more | - | +| | [index_patterns.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/ml/server/models/data_frame_analytics/index_patterns.ts#:~:text=title), [rollup.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/ml/server/models/job_service/new_job_caps/rollup.ts#:~:text=title), [alerting_service.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/ml/server/lib/alerts/alerting_service.ts#:~:text=title), [data_recognizer.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/ml/server/models/data_recognizer/data_recognizer.ts#:~:text=title), [index_utils.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/ml/public/application/util/index_utils.ts#:~:text=title), [index_utils.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/ml/public/application/util/index_utils.ts#:~:text=title), [index_utils.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/ml/public/application/util/index_utils.ts#:~:text=title), [new_job_capabilities_service_analytics.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/ml/public/application/services/new_job_capabilities/new_job_capabilities_service_analytics.ts#:~:text=title), [new_job_capabilities_service_analytics.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/ml/public/application/services/new_job_capabilities/new_job_capabilities_service_analytics.ts#:~:text=title), [configuration_step_details.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/configuration_step_details.tsx#:~:text=title)+ 78 more | - | +| | [index_patterns.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/ml/server/models/data_frame_analytics/index_patterns.ts#:~:text=title), [rollup.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/ml/server/models/job_service/new_job_caps/rollup.ts#:~:text=title), [alerting_service.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/ml/server/lib/alerts/alerting_service.ts#:~:text=title), [data_recognizer.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/ml/server/models/data_recognizer/data_recognizer.ts#:~:text=title), [index_utils.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/ml/public/application/util/index_utils.ts#:~:text=title), [index_utils.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/ml/public/application/util/index_utils.ts#:~:text=title), [index_utils.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/ml/public/application/util/index_utils.ts#:~:text=title), [new_job_capabilities_service_analytics.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/ml/public/application/services/new_job_capabilities/new_job_capabilities_service_analytics.ts#:~:text=title), [new_job_capabilities_service_analytics.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/ml/public/application/services/new_job_capabilities/new_job_capabilities_service_analytics.ts#:~:text=title), [configuration_step_details.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/configuration_step_details.tsx#:~:text=title)+ 78 more | - | +| | [index_patterns.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/ml/server/models/data_frame_analytics/index_patterns.ts#:~:text=title), [rollup.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/ml/server/models/job_service/new_job_caps/rollup.ts#:~:text=title), [alerting_service.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/ml/server/lib/alerts/alerting_service.ts#:~:text=title), [data_recognizer.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/ml/server/models/data_recognizer/data_recognizer.ts#:~:text=title), [index_utils.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/ml/public/application/util/index_utils.ts#:~:text=title), [index_utils.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/ml/public/application/util/index_utils.ts#:~:text=title), [index_utils.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/ml/public/application/util/index_utils.ts#:~:text=title), [new_job_capabilities_service_analytics.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/ml/public/application/services/new_job_capabilities/new_job_capabilities_service_analytics.ts#:~:text=title), [new_job_capabilities_service_analytics.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/ml/public/application/services/new_job_capabilities/new_job_capabilities_service_analytics.ts#:~:text=title), [configuration_step_details.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/configuration_step_details.tsx#:~:text=title)+ 34 more | - | | | [ml_page.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/ml/public/application/components/ml_page/ml_page.tsx#:~:text=KibanaPageTemplate), [ml_page.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/ml/public/application/components/ml_page/ml_page.tsx#:~:text=KibanaPageTemplate), [ml_page.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/ml/public/application/components/ml_page/ml_page.tsx#:~:text=KibanaPageTemplate) | - | | | [ml_page.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/ml/public/application/components/ml_page/ml_page.tsx#:~:text=RedirectAppLinks), [ml_page.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/ml/public/application/components/ml_page/ml_page.tsx#:~:text=RedirectAppLinks), [ml_page.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/ml/public/application/components/ml_page/ml_page.tsx#:~:text=RedirectAppLinks), [jobs_list_page.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/ml/public/application/management/jobs_list/components/jobs_list_page/jobs_list_page.tsx#:~:text=RedirectAppLinks), [jobs_list_page.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/ml/public/application/management/jobs_list/components/jobs_list_page/jobs_list_page.tsx#:~:text=RedirectAppLinks), [jobs_list_page.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/ml/public/application/management/jobs_list/components/jobs_list_page/jobs_list_page.tsx#:~:text=RedirectAppLinks) | - | | | [plugin.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/ml/public/plugin.ts#:~:text=license%24) | 8.8.0 | @@ -856,13 +846,13 @@ migrates to using the Kibana Privilege model: https://github.com/elastic/kibana/ | ---------------|-----------|-----------| | | [wrap_search_source_client.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/lib/detection_engine/rule_preview/api/preview_rules/wrap_search_source_client.ts#:~:text=create) | - | | | [wrap_search_source_client.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/lib/detection_engine/rule_preview/api/preview_rules/wrap_search_source_client.test.ts#:~:text=fetch), [wrap_search_source_client.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/lib/detection_engine/rule_preview/api/preview_rules/wrap_search_source_client.test.ts#:~:text=fetch), [wrap_search_source_client.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/lib/detection_engine/rule_preview/api/preview_rules/wrap_search_source_client.test.ts#:~:text=fetch), [wrap_search_source_client.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/lib/detection_engine/rule_preview/api/preview_rules/wrap_search_source_client.test.ts#:~:text=fetch) | - | -| | [middleware.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts#:~:text=indexPatterns), [dependencies_start_mock.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/common/mock/endpoint/dependencies_start_mock.ts#:~:text=indexPatterns) | - | -| | [index.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/common/containers/source/index.tsx#:~:text=title), [index.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/common/containers/source/index.tsx#:~:text=title), [utils.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/utils.ts#:~:text=title), [get_es_query_filter.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/detections/containers/detection_engine/exceptions/get_es_query_filter.ts#:~:text=title), [middleware.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts#:~:text=title), [get_query_filter.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/lib/detection_engine/signals/get_query_filter.ts#:~:text=title), [index_pattern.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/common/mock/index_pattern.ts#:~:text=title), [utils.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/flyout_components/alerts_actions/utils.test.ts#:~:text=title), [validators.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/detections/components/rules/eql_query_bar/validators.ts#:~:text=title), [index.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/index.tsx#:~:text=title)+ 18 more | - | +| | [dependencies_start_mock.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/common/mock/endpoint/dependencies_start_mock.ts#:~:text=indexPatterns) | - | +| | [index.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/common/containers/source/index.tsx#:~:text=title), [index.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/common/containers/source/index.tsx#:~:text=title), [middleware.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts#:~:text=title), [get_es_query_filter.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/detections/containers/detection_engine/exceptions/get_es_query_filter.ts#:~:text=title), [utils.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/utils.ts#:~:text=title), [get_query_filter.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/lib/detection_engine/signals/get_query_filter.ts#:~:text=title), [index_pattern.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/common/mock/index_pattern.ts#:~:text=title), [utils.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/flyout_components/alerts_actions/utils.test.ts#:~:text=title), [validators.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/detections/components/rules/eql_query_bar/validators.ts#:~:text=title), [index.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/index.tsx#:~:text=title)+ 18 more | - | | | [wrap_search_source_client.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/lib/detection_engine/rule_preview/api/preview_rules/wrap_search_source_client.ts#:~:text=create) | - | | | [wrap_search_source_client.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/lib/detection_engine/rule_preview/api/preview_rules/wrap_search_source_client.test.ts#:~:text=fetch), [wrap_search_source_client.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/lib/detection_engine/rule_preview/api/preview_rules/wrap_search_source_client.test.ts#:~:text=fetch), [wrap_search_source_client.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/lib/detection_engine/rule_preview/api/preview_rules/wrap_search_source_client.test.ts#:~:text=fetch), [wrap_search_source_client.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/lib/detection_engine/rule_preview/api/preview_rules/wrap_search_source_client.test.ts#:~:text=fetch) | - | | | [api.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/common/hooks/eql/api.ts#:~:text=options) | - | -| | [index.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/common/containers/source/index.tsx#:~:text=title), [index.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/common/containers/source/index.tsx#:~:text=title), [utils.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/utils.ts#:~:text=title), [get_es_query_filter.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/detections/containers/detection_engine/exceptions/get_es_query_filter.ts#:~:text=title), [middleware.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts#:~:text=title), [get_query_filter.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/lib/detection_engine/signals/get_query_filter.ts#:~:text=title), [index_pattern.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/common/mock/index_pattern.ts#:~:text=title), [utils.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/flyout_components/alerts_actions/utils.test.ts#:~:text=title), [validators.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/detections/components/rules/eql_query_bar/validators.ts#:~:text=title), [index.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/index.tsx#:~:text=title)+ 18 more | - | -| | [index.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/common/containers/source/index.tsx#:~:text=title), [index.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/common/containers/source/index.tsx#:~:text=title), [utils.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/utils.ts#:~:text=title), [get_es_query_filter.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/detections/containers/detection_engine/exceptions/get_es_query_filter.ts#:~:text=title), [middleware.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts#:~:text=title), [get_query_filter.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/lib/detection_engine/signals/get_query_filter.ts#:~:text=title), [index_pattern.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/common/mock/index_pattern.ts#:~:text=title), [utils.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/flyout_components/alerts_actions/utils.test.ts#:~:text=title), [validators.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/detections/components/rules/eql_query_bar/validators.ts#:~:text=title), [index.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/index.tsx#:~:text=title)+ 4 more | - | +| | [index.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/common/containers/source/index.tsx#:~:text=title), [index.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/common/containers/source/index.tsx#:~:text=title), [middleware.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts#:~:text=title), [get_es_query_filter.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/detections/containers/detection_engine/exceptions/get_es_query_filter.ts#:~:text=title), [utils.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/utils.ts#:~:text=title), [get_query_filter.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/lib/detection_engine/signals/get_query_filter.ts#:~:text=title), [index_pattern.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/common/mock/index_pattern.ts#:~:text=title), [utils.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/flyout_components/alerts_actions/utils.test.ts#:~:text=title), [validators.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/detections/components/rules/eql_query_bar/validators.ts#:~:text=title), [index.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/index.tsx#:~:text=title)+ 18 more | - | +| | [index.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/common/containers/source/index.tsx#:~:text=title), [index.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/common/containers/source/index.tsx#:~:text=title), [middleware.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts#:~:text=title), [get_es_query_filter.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/detections/containers/detection_engine/exceptions/get_es_query_filter.ts#:~:text=title), [utils.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/utils.ts#:~:text=title), [get_query_filter.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/lib/detection_engine/signals/get_query_filter.ts#:~:text=title), [index_pattern.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/common/mock/index_pattern.ts#:~:text=title), [utils.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/flyout_components/alerts_actions/utils.test.ts#:~:text=title), [validators.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/detections/components/rules/eql_query_bar/validators.ts#:~:text=title), [index.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/index.tsx#:~:text=title)+ 4 more | - | | | [policy_config.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/common/license/policy_config.test.ts#:~:text=mode), [policy_config.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/common/license/policy_config.test.ts#:~:text=mode), [policy_config.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/common/license/policy_config.test.ts#:~:text=mode), [fleet_integration.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/fleet_integration/fleet_integration.test.ts#:~:text=mode), [fleet_integration.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/fleet_integration/fleet_integration.test.ts#:~:text=mode), [create_default_policy.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/fleet_integration/handlers/create_default_policy.test.ts#:~:text=mode), [create_default_policy.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/fleet_integration/handlers/create_default_policy.test.ts#:~:text=mode), [license_watch.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/endpoint/lib/policy/license_watch.test.ts#:~:text=mode), [license_watch.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/endpoint/lib/policy/license_watch.test.ts#:~:text=mode), [license_watch.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/endpoint/lib/policy/license_watch.test.ts#:~:text=mode)+ 7 more | 8.8.0 | | | [policy_config.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/common/license/policy_config.test.ts#:~:text=mode), [policy_config.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/common/license/policy_config.test.ts#:~:text=mode), [policy_config.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/common/license/policy_config.test.ts#:~:text=mode), [fleet_integration.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/fleet_integration/fleet_integration.test.ts#:~:text=mode), [fleet_integration.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/fleet_integration/fleet_integration.test.ts#:~:text=mode), [create_default_policy.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/fleet_integration/handlers/create_default_policy.test.ts#:~:text=mode), [create_default_policy.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/fleet_integration/handlers/create_default_policy.test.ts#:~:text=mode), [license_watch.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/endpoint/lib/policy/license_watch.test.ts#:~:text=mode), [license_watch.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/endpoint/lib/policy/license_watch.test.ts#:~:text=mode), [license_watch.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/endpoint/lib/policy/license_watch.test.ts#:~:text=mode)+ 7 more | 8.8.0 | | | [query.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/query.ts#:~:text=license%24) | 8.8.0 | @@ -874,7 +864,7 @@ migrates to using the Kibana Privilege model: https://github.com/elastic/kibana/ | | [policy_hooks.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_hooks.ts#:~:text=ENDPOINT_TRUSTED_APPS_LIST_ID), [policy_hooks.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_hooks.ts#:~:text=ENDPOINT_TRUSTED_APPS_LIST_ID), [constants.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/management/pages/trusted_apps/constants.ts#:~:text=ENDPOINT_TRUSTED_APPS_LIST_ID), [constants.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/management/pages/trusted_apps/constants.ts#:~:text=ENDPOINT_TRUSTED_APPS_LIST_ID), [api_client.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/management/pages/trusted_apps/service/api_client.ts#:~:text=ENDPOINT_TRUSTED_APPS_LIST_ID), [api_client.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/management/pages/trusted_apps/service/api_client.ts#:~:text=ENDPOINT_TRUSTED_APPS_LIST_ID), [api_client.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/management/pages/trusted_apps/service/api_client.ts#:~:text=ENDPOINT_TRUSTED_APPS_LIST_ID), [lists.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.ts#:~:text=ENDPOINT_TRUSTED_APPS_LIST_ID), [manifest_manager.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.ts#:~:text=ENDPOINT_TRUSTED_APPS_LIST_ID), [manifest_manager.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.ts#:~:text=ENDPOINT_TRUSTED_APPS_LIST_ID)+ 34 more | - | | | [constants.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/management/pages/trusted_apps/constants.ts#:~:text=ENDPOINT_TRUSTED_APPS_LIST_NAME), [constants.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/management/pages/trusted_apps/constants.ts#:~:text=ENDPOINT_TRUSTED_APPS_LIST_NAME), [index.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/scripts/endpoint/trusted_apps/index.ts#:~:text=ENDPOINT_TRUSTED_APPS_LIST_NAME), [index.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/scripts/endpoint/trusted_apps/index.ts#:~:text=ENDPOINT_TRUSTED_APPS_LIST_NAME) | - | | | [constants.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/management/pages/trusted_apps/constants.ts#:~:text=ENDPOINT_TRUSTED_APPS_LIST_DESCRIPTION), [constants.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/management/pages/trusted_apps/constants.ts#:~:text=ENDPOINT_TRUSTED_APPS_LIST_DESCRIPTION), [index.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/scripts/endpoint/trusted_apps/index.ts#:~:text=ENDPOINT_TRUSTED_APPS_LIST_DESCRIPTION), [index.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/scripts/endpoint/trusted_apps/index.ts#:~:text=ENDPOINT_TRUSTED_APPS_LIST_DESCRIPTION) | - | -| | [constants.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/management/pages/event_filters/constants.ts#:~:text=ENDPOINT_EVENT_FILTERS_LIST_ID), [constants.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/management/pages/event_filters/constants.ts#:~:text=ENDPOINT_EVENT_FILTERS_LIST_ID), [constants.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/management/pages/event_filters/constants.ts#:~:text=ENDPOINT_EVENT_FILTERS_LIST_ID), [form.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/form.tsx#:~:text=ENDPOINT_EVENT_FILTERS_LIST_ID), [form.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/form.tsx#:~:text=ENDPOINT_EVENT_FILTERS_LIST_ID), [utils.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/management/pages/event_filters/view/utils.ts#:~:text=ENDPOINT_EVENT_FILTERS_LIST_ID), [utils.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/management/pages/event_filters/view/utils.ts#:~:text=ENDPOINT_EVENT_FILTERS_LIST_ID), [service_actions.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/management/pages/event_filters/service/service_actions.ts#:~:text=ENDPOINT_EVENT_FILTERS_LIST_ID), [service_actions.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/management/pages/event_filters/service/service_actions.ts#:~:text=ENDPOINT_EVENT_FILTERS_LIST_ID), [service_actions.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/management/pages/event_filters/service/service_actions.ts#:~:text=ENDPOINT_EVENT_FILTERS_LIST_ID)+ 32 more | - | +| | [policy_hooks.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_hooks.ts#:~:text=ENDPOINT_EVENT_FILTERS_LIST_ID), [policy_hooks.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_hooks.ts#:~:text=ENDPOINT_EVENT_FILTERS_LIST_ID), [constants.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/management/pages/event_filters/constants.ts#:~:text=ENDPOINT_EVENT_FILTERS_LIST_ID), [constants.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/management/pages/event_filters/constants.ts#:~:text=ENDPOINT_EVENT_FILTERS_LIST_ID), [constants.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/management/pages/event_filters/constants.ts#:~:text=ENDPOINT_EVENT_FILTERS_LIST_ID), [form.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/form.tsx#:~:text=ENDPOINT_EVENT_FILTERS_LIST_ID), [form.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/form.tsx#:~:text=ENDPOINT_EVENT_FILTERS_LIST_ID), [utils.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/management/pages/event_filters/view/utils.ts#:~:text=ENDPOINT_EVENT_FILTERS_LIST_ID), [utils.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/management/pages/event_filters/view/utils.ts#:~:text=ENDPOINT_EVENT_FILTERS_LIST_ID), [service_actions.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/management/pages/event_filters/service/service_actions.ts#:~:text=ENDPOINT_EVENT_FILTERS_LIST_ID)+ 32 more | - | | | [constants.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/management/pages/event_filters/constants.ts#:~:text=ENDPOINT_EVENT_FILTERS_LIST_NAME), [constants.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/management/pages/event_filters/constants.ts#:~:text=ENDPOINT_EVENT_FILTERS_LIST_NAME), [create_event_filters.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/fleet_integration/handlers/create_event_filters.ts#:~:text=ENDPOINT_EVENT_FILTERS_LIST_NAME), [create_event_filters.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/fleet_integration/handlers/create_event_filters.ts#:~:text=ENDPOINT_EVENT_FILTERS_LIST_NAME), [index.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/scripts/endpoint/event_filters/index.ts#:~:text=ENDPOINT_EVENT_FILTERS_LIST_NAME), [index.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/scripts/endpoint/event_filters/index.ts#:~:text=ENDPOINT_EVENT_FILTERS_LIST_NAME) | - | | | [constants.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/management/pages/event_filters/constants.ts#:~:text=ENDPOINT_EVENT_FILTERS_LIST_DESCRIPTION), [constants.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/management/pages/event_filters/constants.ts#:~:text=ENDPOINT_EVENT_FILTERS_LIST_DESCRIPTION), [create_event_filters.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/fleet_integration/handlers/create_event_filters.ts#:~:text=ENDPOINT_EVENT_FILTERS_LIST_DESCRIPTION), [create_event_filters.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/fleet_integration/handlers/create_event_filters.ts#:~:text=ENDPOINT_EVENT_FILTERS_LIST_DESCRIPTION), [index.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/scripts/endpoint/event_filters/index.ts#:~:text=ENDPOINT_EVENT_FILTERS_LIST_DESCRIPTION), [index.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/scripts/endpoint/event_filters/index.ts#:~:text=ENDPOINT_EVENT_FILTERS_LIST_DESCRIPTION) | - | | | [constants.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/constants.ts#:~:text=ENDPOINT_HOST_ISOLATION_EXCEPTIONS_LIST_ID), [constants.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/constants.ts#:~:text=ENDPOINT_HOST_ISOLATION_EXCEPTIONS_LIST_ID), [host_isolation_exceptions_api_client.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/host_isolation_exceptions_api_client.ts#:~:text=ENDPOINT_HOST_ISOLATION_EXCEPTIONS_LIST_ID), [host_isolation_exceptions_api_client.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/host_isolation_exceptions_api_client.ts#:~:text=ENDPOINT_HOST_ISOLATION_EXCEPTIONS_LIST_ID), [host_isolation_exceptions_api_client.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/host_isolation_exceptions_api_client.ts#:~:text=ENDPOINT_HOST_ISOLATION_EXCEPTIONS_LIST_ID), [lists.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.ts#:~:text=ENDPOINT_HOST_ISOLATION_EXCEPTIONS_LIST_ID), [manifest_manager.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.ts#:~:text=ENDPOINT_HOST_ISOLATION_EXCEPTIONS_LIST_ID), [manifest_manager.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.ts#:~:text=ENDPOINT_HOST_ISOLATION_EXCEPTIONS_LIST_ID), [host_isolation_exceptions_validator.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/lists_integration/endpoint/validators/host_isolation_exceptions_validator.ts#:~:text=ENDPOINT_HOST_ISOLATION_EXCEPTIONS_LIST_ID), [host_isolation_exceptions_validator.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/lists_integration/endpoint/validators/host_isolation_exceptions_validator.ts#:~:text=ENDPOINT_HOST_ISOLATION_EXCEPTIONS_LIST_ID)+ 16 more | - | diff --git a/api_docs/deprecations_by_team.mdx b/api_docs/deprecations_by_team.mdx index ede9e02a71db5..5d09767e68e8e 100644 --- a/api_docs/deprecations_by_team.mdx +++ b/api_docs/deprecations_by_team.mdx @@ -7,7 +7,7 @@ id: kibDevDocsDeprecationsDueByTeam slug: /kibana-dev-docs/api-meta/deprecations-due-by-team title: Deprecated APIs due to be removed, by team description: Lists the teams that are referencing deprecated APIs with a remove by date. -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana'] --- diff --git a/api_docs/dev_tools.mdx b/api_docs/dev_tools.mdx index 7d34635b28e23..e73ffbc64e29b 100644 --- a/api_docs/dev_tools.mdx +++ b/api_docs/dev_tools.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/devTools title: "devTools" image: https://source.unsplash.com/400x175/?github description: API docs for the devTools plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'devTools'] --- import devToolsObj from './dev_tools.devdocs.json'; diff --git a/api_docs/discover.devdocs.json b/api_docs/discover.devdocs.json index 32ec559942db0..c2dc34959d5b0 100644 --- a/api_docs/discover.devdocs.json +++ b/api_docs/discover.devdocs.json @@ -628,6 +628,22 @@ "path": "src/plugins/discover/public/locator.ts", "deprecated": false, "trackAdoption": false + }, + { + "parentPluginId": "discover", + "id": "def-public.DiscoverAppLocatorParams.breakdownField", + "type": "string", + "tags": [], + "label": "breakdownField", + "description": [ + "\nBreakdown field" + ], + "signature": [ + "string | undefined" + ], + "path": "src/plugins/discover/public/locator.ts", + "deprecated": false, + "trackAdoption": false } ], "initialIsOpen": false @@ -1295,6 +1311,20 @@ "path": "src/plugins/saved_search/public/services/saved_searches/types.ts", "deprecated": false, "trackAdoption": false + }, + { + "parentPluginId": "discover", + "id": "def-public.SavedSearch.breakdownField", + "type": "string", + "tags": [], + "label": "breakdownField", + "description": [], + "signature": [ + "string | undefined" + ], + "path": "src/plugins/saved_search/public/services/saved_searches/types.ts", + "deprecated": false, + "trackAdoption": false } ], "initialIsOpen": false diff --git a/api_docs/discover.mdx b/api_docs/discover.mdx index d3120d13b3655..49932fb02dec3 100644 --- a/api_docs/discover.mdx +++ b/api_docs/discover.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/discover title: "discover" image: https://source.unsplash.com/400x175/?github description: API docs for the discover plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'discover'] --- import discoverObj from './discover.devdocs.json'; @@ -21,7 +21,7 @@ Contact [Data Discovery](https://github.com/orgs/elastic/teams/kibana-data-disco | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 98 | 0 | 81 | 4 | +| 100 | 0 | 82 | 4 | ## Client diff --git a/api_docs/discover_enhanced.mdx b/api_docs/discover_enhanced.mdx index 7fedfa86e518c..0ee593a0f0f69 100644 --- a/api_docs/discover_enhanced.mdx +++ b/api_docs/discover_enhanced.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/discoverEnhanced title: "discoverEnhanced" image: https://source.unsplash.com/400x175/?github description: API docs for the discoverEnhanced plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'discoverEnhanced'] --- import discoverEnhancedObj from './discover_enhanced.devdocs.json'; diff --git a/api_docs/embeddable.devdocs.json b/api_docs/embeddable.devdocs.json index 359f01ac6cc60..f276b7806e715 100644 --- a/api_docs/embeddable.devdocs.json +++ b/api_docs/embeddable.devdocs.json @@ -1277,7 +1277,7 @@ "section": "def-public.IEmbeddable", "text": "IEmbeddable" }, - ">(type: string, explicitInput: Partial) => Promise>(type: string, explicitInput: Partial) => Promise<", { "pluginId": "embeddable", "scope": "public", @@ -1285,7 +1285,7 @@ "section": "def-public.ErrorEmbeddable", "text": "ErrorEmbeddable" }, - ">" + " | E>" ], "path": "src/plugins/embeddable/public/lib/containers/container.ts", "deprecated": false, @@ -1702,7 +1702,7 @@ "section": "def-public.EmbeddableOutput", "text": "EmbeddableOutput" }, - ", any>>(id: string) => Promise<", + ", any>>(id: string) => Promise" + ">" ], "path": "src/plugins/embeddable/public/lib/containers/container.ts", "deprecated": false, @@ -1941,6 +1941,124 @@ } ], "returnComment": [] + }, + { + "parentPluginId": "embeddable", + "id": "def-public.Container.createAndSaveEmbeddable", + "type": "Function", + "tags": [], + "label": "createAndSaveEmbeddable", + "description": [], + "signature": [ + " = ", + { + "pluginId": "embeddable", + "scope": "public", + "docId": "kibEmbeddablePluginApi", + "section": "def-public.IEmbeddable", + "text": "IEmbeddable" + }, + ">(type: string, panelState: ", + { + "pluginId": "embeddable", + "scope": "common", + "docId": "kibEmbeddablePluginApi", + "section": "def-common.PanelState", + "text": "PanelState" + }, + "<{ id: string; }>) => Promise" + ], + "path": "src/plugins/embeddable/public/lib/containers/container.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "embeddable", + "id": "def-public.Container.createAndSaveEmbeddable.$1", + "type": "string", + "tags": [], + "label": "type", + "description": [], + "signature": [ + "string" + ], + "path": "src/plugins/embeddable/public/lib/containers/container.ts", + "deprecated": false, + "trackAdoption": false, + "isRequired": true + }, + { + "parentPluginId": "embeddable", + "id": "def-public.Container.createAndSaveEmbeddable.$2", + "type": "Object", + "tags": [], + "label": "panelState", + "description": [], + "signature": [ + { + "pluginId": "embeddable", + "scope": "common", + "docId": "kibEmbeddablePluginApi", + "section": "def-common.PanelState", + "text": "PanelState" + }, + "<{ id: string; }>" + ], + "path": "src/plugins/embeddable/public/lib/containers/container.ts", + "deprecated": false, + "trackAdoption": false, + "isRequired": true + } + ], + "returnComment": [] } ], "initialIsOpen": false diff --git a/api_docs/embeddable.mdx b/api_docs/embeddable.mdx index 9cd54e68d3223..c372fff589e45 100644 --- a/api_docs/embeddable.mdx +++ b/api_docs/embeddable.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/embeddable title: "embeddable" image: https://source.unsplash.com/400x175/?github description: API docs for the embeddable plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'embeddable'] --- import embeddableObj from './embeddable.devdocs.json'; @@ -21,7 +21,7 @@ Contact [App Services](https://github.com/orgs/elastic/teams/kibana-app-services | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 510 | 6 | 410 | 4 | +| 513 | 6 | 413 | 4 | ## Client diff --git a/api_docs/embeddable_enhanced.mdx b/api_docs/embeddable_enhanced.mdx index e37e2e36eb67b..5de1e544c511a 100644 --- a/api_docs/embeddable_enhanced.mdx +++ b/api_docs/embeddable_enhanced.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/embeddableEnhanced title: "embeddableEnhanced" image: https://source.unsplash.com/400x175/?github description: API docs for the embeddableEnhanced plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'embeddableEnhanced'] --- import embeddableEnhancedObj from './embeddable_enhanced.devdocs.json'; diff --git a/api_docs/encrypted_saved_objects.mdx b/api_docs/encrypted_saved_objects.mdx index 19db76e9c9241..b9d571acf264c 100644 --- a/api_docs/encrypted_saved_objects.mdx +++ b/api_docs/encrypted_saved_objects.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/encryptedSavedObjects title: "encryptedSavedObjects" image: https://source.unsplash.com/400x175/?github description: API docs for the encryptedSavedObjects plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'encryptedSavedObjects'] --- import encryptedSavedObjectsObj from './encrypted_saved_objects.devdocs.json'; diff --git a/api_docs/enterprise_search.mdx b/api_docs/enterprise_search.mdx index 05f28b821bb5d..80da471d33b7e 100644 --- a/api_docs/enterprise_search.mdx +++ b/api_docs/enterprise_search.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/enterpriseSearch title: "enterpriseSearch" image: https://source.unsplash.com/400x175/?github description: API docs for the enterpriseSearch plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'enterpriseSearch'] --- import enterpriseSearchObj from './enterprise_search.devdocs.json'; diff --git a/api_docs/es_ui_shared.mdx b/api_docs/es_ui_shared.mdx index 9dfe6ebc2df02..21aee307763d2 100644 --- a/api_docs/es_ui_shared.mdx +++ b/api_docs/es_ui_shared.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/esUiShared title: "esUiShared" image: https://source.unsplash.com/400x175/?github description: API docs for the esUiShared plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'esUiShared'] --- import esUiSharedObj from './es_ui_shared.devdocs.json'; diff --git a/api_docs/event_annotation.mdx b/api_docs/event_annotation.mdx index 8a07bc181e7b8..1be5dd9f66fd5 100644 --- a/api_docs/event_annotation.mdx +++ b/api_docs/event_annotation.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/eventAnnotation title: "eventAnnotation" image: https://source.unsplash.com/400x175/?github description: API docs for the eventAnnotation plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'eventAnnotation'] --- import eventAnnotationObj from './event_annotation.devdocs.json'; diff --git a/api_docs/event_log.devdocs.json b/api_docs/event_log.devdocs.json index af0bd4e00e28c..a9d6c887f136e 100644 --- a/api_docs/event_log.devdocs.json +++ b/api_docs/event_log.devdocs.json @@ -1499,7 +1499,7 @@ "label": "data", "description": [], "signature": [ - "(Readonly<{ error?: Readonly<{ type?: string | undefined; id?: string | undefined; message?: string | undefined; code?: string | undefined; stack_trace?: string | undefined; } & {}> | undefined; tags?: string[] | undefined; log?: Readonly<{ logger?: string | undefined; level?: string | undefined; } & {}> | undefined; user?: Readonly<{ name?: string | undefined; } & {}> | undefined; message?: string | undefined; kibana?: Readonly<{ alert?: Readonly<{ rule?: Readonly<{ consumer?: string | undefined; execution?: Readonly<{ status?: string | undefined; metrics?: Readonly<{ number_of_triggered_actions?: string | number | undefined; number_of_generated_actions?: string | number | undefined; alert_counts?: Readonly<{ recovered?: string | number | undefined; active?: string | number | undefined; new?: string | number | undefined; } & {}> | undefined; number_of_searches?: string | number | undefined; total_indexing_duration_ms?: string | number | undefined; es_search_duration_ms?: string | number | undefined; total_search_duration_ms?: string | number | undefined; execution_gap_duration_s?: string | number | undefined; rule_type_run_duration_ms?: string | number | undefined; process_alerts_duration_ms?: string | number | undefined; trigger_actions_duration_ms?: string | number | undefined; process_rule_duration_ms?: string | number | undefined; claim_to_start_duration_ms?: string | number | undefined; prepare_rule_duration_ms?: string | number | undefined; total_run_duration_ms?: string | number | undefined; total_enrichment_duration_ms?: string | number | undefined; } & {}> | undefined; uuid?: string | undefined; status_order?: string | number | undefined; } & {}> | undefined; rule_type_id?: string | undefined; } & {}> | undefined; flapping?: boolean | undefined; } & {}> | undefined; version?: string | undefined; alerting?: Readonly<{ status?: string | undefined; outcome?: string | undefined; instance_id?: string | undefined; action_group_id?: string | undefined; action_subgroup?: string | undefined; } & {}> | undefined; server_uuid?: string | undefined; task?: Readonly<{ id?: string | undefined; schedule_delay?: string | number | undefined; scheduled?: string | undefined; } & {}> | undefined; saved_objects?: Readonly<{ type?: string | undefined; id?: string | undefined; namespace?: string | undefined; rel?: string | undefined; type_id?: string | undefined; } & {}>[] | undefined; space_ids?: string[] | undefined; } & {}> | undefined; ecs?: Readonly<{ version?: string | undefined; } & {}> | undefined; rule?: Readonly<{ name?: string | undefined; description?: string | undefined; category?: string | undefined; id?: string | undefined; uuid?: string | undefined; version?: string | undefined; license?: string | undefined; reference?: string | undefined; author?: string[] | undefined; ruleset?: string | undefined; } & {}> | undefined; event?: Readonly<{ start?: string | undefined; category?: string[] | undefined; type?: string[] | undefined; id?: string | undefined; reason?: string | undefined; created?: string | undefined; outcome?: string | undefined; end?: string | undefined; original?: string | undefined; duration?: string | number | undefined; kind?: string | undefined; hash?: string | undefined; code?: string | undefined; url?: string | undefined; action?: string | undefined; severity?: string | number | undefined; dataset?: string | undefined; ingested?: string | undefined; module?: string | undefined; provider?: string | undefined; reference?: string | undefined; risk_score?: number | undefined; risk_score_norm?: number | undefined; sequence?: string | number | undefined; timezone?: string | undefined; } & {}> | undefined; '@timestamp'?: string | undefined; } & {}> | undefined)[]" + "(Readonly<{ error?: Readonly<{ type?: string | undefined; id?: string | undefined; message?: string | undefined; code?: string | undefined; stack_trace?: string | undefined; } & {}> | undefined; tags?: string[] | undefined; log?: Readonly<{ logger?: string | undefined; level?: string | undefined; } & {}> | undefined; user?: Readonly<{ name?: string | undefined; } & {}> | undefined; message?: string | undefined; kibana?: Readonly<{ alert?: Readonly<{ rule?: Readonly<{ consumer?: string | undefined; execution?: Readonly<{ status?: string | undefined; metrics?: Readonly<{ number_of_triggered_actions?: string | number | undefined; number_of_generated_actions?: string | number | undefined; alert_counts?: Readonly<{ active?: string | number | undefined; recovered?: string | number | undefined; new?: string | number | undefined; } & {}> | undefined; number_of_searches?: string | number | undefined; total_indexing_duration_ms?: string | number | undefined; es_search_duration_ms?: string | number | undefined; total_search_duration_ms?: string | number | undefined; execution_gap_duration_s?: string | number | undefined; rule_type_run_duration_ms?: string | number | undefined; process_alerts_duration_ms?: string | number | undefined; trigger_actions_duration_ms?: string | number | undefined; process_rule_duration_ms?: string | number | undefined; claim_to_start_duration_ms?: string | number | undefined; prepare_rule_duration_ms?: string | number | undefined; total_run_duration_ms?: string | number | undefined; total_enrichment_duration_ms?: string | number | undefined; } & {}> | undefined; uuid?: string | undefined; status_order?: string | number | undefined; } & {}> | undefined; rule_type_id?: string | undefined; } & {}> | undefined; flapping?: boolean | undefined; } & {}> | undefined; version?: string | undefined; alerting?: Readonly<{ status?: string | undefined; outcome?: string | undefined; instance_id?: string | undefined; action_group_id?: string | undefined; action_subgroup?: string | undefined; } & {}> | undefined; server_uuid?: string | undefined; task?: Readonly<{ id?: string | undefined; schedule_delay?: string | number | undefined; scheduled?: string | undefined; } & {}> | undefined; saved_objects?: Readonly<{ type?: string | undefined; id?: string | undefined; namespace?: string | undefined; rel?: string | undefined; type_id?: string | undefined; } & {}>[] | undefined; space_ids?: string[] | undefined; } & {}> | undefined; ecs?: Readonly<{ version?: string | undefined; } & {}> | undefined; rule?: Readonly<{ name?: string | undefined; description?: string | undefined; category?: string | undefined; id?: string | undefined; version?: string | undefined; license?: string | undefined; reference?: string | undefined; author?: string[] | undefined; ruleset?: string | undefined; uuid?: string | undefined; } & {}> | undefined; event?: Readonly<{ start?: string | undefined; category?: string[] | undefined; type?: string[] | undefined; id?: string | undefined; reason?: string | undefined; created?: string | undefined; outcome?: string | undefined; end?: string | undefined; original?: string | undefined; duration?: string | number | undefined; kind?: string | undefined; hash?: string | undefined; url?: string | undefined; code?: string | undefined; action?: string | undefined; severity?: string | number | undefined; dataset?: string | undefined; ingested?: string | undefined; module?: string | undefined; provider?: string | undefined; reference?: string | undefined; risk_score?: number | undefined; risk_score_norm?: number | undefined; sequence?: string | number | undefined; timezone?: string | undefined; } & {}> | undefined; '@timestamp'?: string | undefined; } & {}> | undefined)[]" ], "path": "x-pack/plugins/event_log/server/es/cluster_client_adapter.ts", "deprecated": false, @@ -1519,7 +1519,7 @@ "label": "IEvent", "description": [], "signature": [ - "DeepPartial | undefined; tags?: string[] | undefined; log?: Readonly<{ logger?: string | undefined; level?: string | undefined; } & {}> | undefined; user?: Readonly<{ name?: string | undefined; } & {}> | undefined; message?: string | undefined; kibana?: Readonly<{ alert?: Readonly<{ rule?: Readonly<{ consumer?: string | undefined; execution?: Readonly<{ status?: string | undefined; metrics?: Readonly<{ number_of_triggered_actions?: string | number | undefined; number_of_generated_actions?: string | number | undefined; alert_counts?: Readonly<{ recovered?: string | number | undefined; active?: string | number | undefined; new?: string | number | undefined; } & {}> | undefined; number_of_searches?: string | number | undefined; total_indexing_duration_ms?: string | number | undefined; es_search_duration_ms?: string | number | undefined; total_search_duration_ms?: string | number | undefined; execution_gap_duration_s?: string | number | undefined; rule_type_run_duration_ms?: string | number | undefined; process_alerts_duration_ms?: string | number | undefined; trigger_actions_duration_ms?: string | number | undefined; process_rule_duration_ms?: string | number | undefined; claim_to_start_duration_ms?: string | number | undefined; prepare_rule_duration_ms?: string | number | undefined; total_run_duration_ms?: string | number | undefined; total_enrichment_duration_ms?: string | number | undefined; } & {}> | undefined; uuid?: string | undefined; status_order?: string | number | undefined; } & {}> | undefined; rule_type_id?: string | undefined; } & {}> | undefined; flapping?: boolean | undefined; } & {}> | undefined; version?: string | undefined; alerting?: Readonly<{ status?: string | undefined; outcome?: string | undefined; instance_id?: string | undefined; action_group_id?: string | undefined; action_subgroup?: string | undefined; } & {}> | undefined; server_uuid?: string | undefined; task?: Readonly<{ id?: string | undefined; schedule_delay?: string | number | undefined; scheduled?: string | undefined; } & {}> | undefined; saved_objects?: Readonly<{ type?: string | undefined; id?: string | undefined; namespace?: string | undefined; rel?: string | undefined; type_id?: string | undefined; } & {}>[] | undefined; space_ids?: string[] | undefined; } & {}> | undefined; ecs?: Readonly<{ version?: string | undefined; } & {}> | undefined; rule?: Readonly<{ name?: string | undefined; description?: string | undefined; category?: string | undefined; id?: string | undefined; uuid?: string | undefined; version?: string | undefined; license?: string | undefined; reference?: string | undefined; author?: string[] | undefined; ruleset?: string | undefined; } & {}> | undefined; event?: Readonly<{ start?: string | undefined; category?: string[] | undefined; type?: string[] | undefined; id?: string | undefined; reason?: string | undefined; created?: string | undefined; outcome?: string | undefined; end?: string | undefined; original?: string | undefined; duration?: string | number | undefined; kind?: string | undefined; hash?: string | undefined; code?: string | undefined; url?: string | undefined; action?: string | undefined; severity?: string | number | undefined; dataset?: string | undefined; ingested?: string | undefined; module?: string | undefined; provider?: string | undefined; reference?: string | undefined; risk_score?: number | undefined; risk_score_norm?: number | undefined; sequence?: string | number | undefined; timezone?: string | undefined; } & {}> | undefined; '@timestamp'?: string | undefined; } & {}>>> | undefined" + "DeepPartial | undefined; tags?: string[] | undefined; log?: Readonly<{ logger?: string | undefined; level?: string | undefined; } & {}> | undefined; user?: Readonly<{ name?: string | undefined; } & {}> | undefined; message?: string | undefined; kibana?: Readonly<{ alert?: Readonly<{ rule?: Readonly<{ consumer?: string | undefined; execution?: Readonly<{ status?: string | undefined; metrics?: Readonly<{ number_of_triggered_actions?: string | number | undefined; number_of_generated_actions?: string | number | undefined; alert_counts?: Readonly<{ active?: string | number | undefined; recovered?: string | number | undefined; new?: string | number | undefined; } & {}> | undefined; number_of_searches?: string | number | undefined; total_indexing_duration_ms?: string | number | undefined; es_search_duration_ms?: string | number | undefined; total_search_duration_ms?: string | number | undefined; execution_gap_duration_s?: string | number | undefined; rule_type_run_duration_ms?: string | number | undefined; process_alerts_duration_ms?: string | number | undefined; trigger_actions_duration_ms?: string | number | undefined; process_rule_duration_ms?: string | number | undefined; claim_to_start_duration_ms?: string | number | undefined; prepare_rule_duration_ms?: string | number | undefined; total_run_duration_ms?: string | number | undefined; total_enrichment_duration_ms?: string | number | undefined; } & {}> | undefined; uuid?: string | undefined; status_order?: string | number | undefined; } & {}> | undefined; rule_type_id?: string | undefined; } & {}> | undefined; flapping?: boolean | undefined; } & {}> | undefined; version?: string | undefined; alerting?: Readonly<{ status?: string | undefined; outcome?: string | undefined; instance_id?: string | undefined; action_group_id?: string | undefined; action_subgroup?: string | undefined; } & {}> | undefined; server_uuid?: string | undefined; task?: Readonly<{ id?: string | undefined; schedule_delay?: string | number | undefined; scheduled?: string | undefined; } & {}> | undefined; saved_objects?: Readonly<{ type?: string | undefined; id?: string | undefined; namespace?: string | undefined; rel?: string | undefined; type_id?: string | undefined; } & {}>[] | undefined; space_ids?: string[] | undefined; } & {}> | undefined; ecs?: Readonly<{ version?: string | undefined; } & {}> | undefined; rule?: Readonly<{ name?: string | undefined; description?: string | undefined; category?: string | undefined; id?: string | undefined; version?: string | undefined; license?: string | undefined; reference?: string | undefined; author?: string[] | undefined; ruleset?: string | undefined; uuid?: string | undefined; } & {}> | undefined; event?: Readonly<{ start?: string | undefined; category?: string[] | undefined; type?: string[] | undefined; id?: string | undefined; reason?: string | undefined; created?: string | undefined; outcome?: string | undefined; end?: string | undefined; original?: string | undefined; duration?: string | number | undefined; kind?: string | undefined; hash?: string | undefined; url?: string | undefined; code?: string | undefined; action?: string | undefined; severity?: string | number | undefined; dataset?: string | undefined; ingested?: string | undefined; module?: string | undefined; provider?: string | undefined; reference?: string | undefined; risk_score?: number | undefined; risk_score_norm?: number | undefined; sequence?: string | number | undefined; timezone?: string | undefined; } & {}> | undefined; '@timestamp'?: string | undefined; } & {}>>> | undefined" ], "path": "x-pack/plugins/event_log/generated/schemas.ts", "deprecated": false, @@ -1534,7 +1534,7 @@ "label": "IValidatedEvent", "description": [], "signature": [ - "Readonly<{ error?: Readonly<{ type?: string | undefined; id?: string | undefined; message?: string | undefined; code?: string | undefined; stack_trace?: string | undefined; } & {}> | undefined; tags?: string[] | undefined; log?: Readonly<{ logger?: string | undefined; level?: string | undefined; } & {}> | undefined; user?: Readonly<{ name?: string | undefined; } & {}> | undefined; message?: string | undefined; kibana?: Readonly<{ alert?: Readonly<{ rule?: Readonly<{ consumer?: string | undefined; execution?: Readonly<{ status?: string | undefined; metrics?: Readonly<{ number_of_triggered_actions?: string | number | undefined; number_of_generated_actions?: string | number | undefined; alert_counts?: Readonly<{ recovered?: string | number | undefined; active?: string | number | undefined; new?: string | number | undefined; } & {}> | undefined; number_of_searches?: string | number | undefined; total_indexing_duration_ms?: string | number | undefined; es_search_duration_ms?: string | number | undefined; total_search_duration_ms?: string | number | undefined; execution_gap_duration_s?: string | number | undefined; rule_type_run_duration_ms?: string | number | undefined; process_alerts_duration_ms?: string | number | undefined; trigger_actions_duration_ms?: string | number | undefined; process_rule_duration_ms?: string | number | undefined; claim_to_start_duration_ms?: string | number | undefined; prepare_rule_duration_ms?: string | number | undefined; total_run_duration_ms?: string | number | undefined; total_enrichment_duration_ms?: string | number | undefined; } & {}> | undefined; uuid?: string | undefined; status_order?: string | number | undefined; } & {}> | undefined; rule_type_id?: string | undefined; } & {}> | undefined; flapping?: boolean | undefined; } & {}> | undefined; version?: string | undefined; alerting?: Readonly<{ status?: string | undefined; outcome?: string | undefined; instance_id?: string | undefined; action_group_id?: string | undefined; action_subgroup?: string | undefined; } & {}> | undefined; server_uuid?: string | undefined; task?: Readonly<{ id?: string | undefined; schedule_delay?: string | number | undefined; scheduled?: string | undefined; } & {}> | undefined; saved_objects?: Readonly<{ type?: string | undefined; id?: string | undefined; namespace?: string | undefined; rel?: string | undefined; type_id?: string | undefined; } & {}>[] | undefined; space_ids?: string[] | undefined; } & {}> | undefined; ecs?: Readonly<{ version?: string | undefined; } & {}> | undefined; rule?: Readonly<{ name?: string | undefined; description?: string | undefined; category?: string | undefined; id?: string | undefined; uuid?: string | undefined; version?: string | undefined; license?: string | undefined; reference?: string | undefined; author?: string[] | undefined; ruleset?: string | undefined; } & {}> | undefined; event?: Readonly<{ start?: string | undefined; category?: string[] | undefined; type?: string[] | undefined; id?: string | undefined; reason?: string | undefined; created?: string | undefined; outcome?: string | undefined; end?: string | undefined; original?: string | undefined; duration?: string | number | undefined; kind?: string | undefined; hash?: string | undefined; code?: string | undefined; url?: string | undefined; action?: string | undefined; severity?: string | number | undefined; dataset?: string | undefined; ingested?: string | undefined; module?: string | undefined; provider?: string | undefined; reference?: string | undefined; risk_score?: number | undefined; risk_score_norm?: number | undefined; sequence?: string | number | undefined; timezone?: string | undefined; } & {}> | undefined; '@timestamp'?: string | undefined; } & {}> | undefined" + "Readonly<{ error?: Readonly<{ type?: string | undefined; id?: string | undefined; message?: string | undefined; code?: string | undefined; stack_trace?: string | undefined; } & {}> | undefined; tags?: string[] | undefined; log?: Readonly<{ logger?: string | undefined; level?: string | undefined; } & {}> | undefined; user?: Readonly<{ name?: string | undefined; } & {}> | undefined; message?: string | undefined; kibana?: Readonly<{ alert?: Readonly<{ rule?: Readonly<{ consumer?: string | undefined; execution?: Readonly<{ status?: string | undefined; metrics?: Readonly<{ number_of_triggered_actions?: string | number | undefined; number_of_generated_actions?: string | number | undefined; alert_counts?: Readonly<{ active?: string | number | undefined; recovered?: string | number | undefined; new?: string | number | undefined; } & {}> | undefined; number_of_searches?: string | number | undefined; total_indexing_duration_ms?: string | number | undefined; es_search_duration_ms?: string | number | undefined; total_search_duration_ms?: string | number | undefined; execution_gap_duration_s?: string | number | undefined; rule_type_run_duration_ms?: string | number | undefined; process_alerts_duration_ms?: string | number | undefined; trigger_actions_duration_ms?: string | number | undefined; process_rule_duration_ms?: string | number | undefined; claim_to_start_duration_ms?: string | number | undefined; prepare_rule_duration_ms?: string | number | undefined; total_run_duration_ms?: string | number | undefined; total_enrichment_duration_ms?: string | number | undefined; } & {}> | undefined; uuid?: string | undefined; status_order?: string | number | undefined; } & {}> | undefined; rule_type_id?: string | undefined; } & {}> | undefined; flapping?: boolean | undefined; } & {}> | undefined; version?: string | undefined; alerting?: Readonly<{ status?: string | undefined; outcome?: string | undefined; instance_id?: string | undefined; action_group_id?: string | undefined; action_subgroup?: string | undefined; } & {}> | undefined; server_uuid?: string | undefined; task?: Readonly<{ id?: string | undefined; schedule_delay?: string | number | undefined; scheduled?: string | undefined; } & {}> | undefined; saved_objects?: Readonly<{ type?: string | undefined; id?: string | undefined; namespace?: string | undefined; rel?: string | undefined; type_id?: string | undefined; } & {}>[] | undefined; space_ids?: string[] | undefined; } & {}> | undefined; ecs?: Readonly<{ version?: string | undefined; } & {}> | undefined; rule?: Readonly<{ name?: string | undefined; description?: string | undefined; category?: string | undefined; id?: string | undefined; version?: string | undefined; license?: string | undefined; reference?: string | undefined; author?: string[] | undefined; ruleset?: string | undefined; uuid?: string | undefined; } & {}> | undefined; event?: Readonly<{ start?: string | undefined; category?: string[] | undefined; type?: string[] | undefined; id?: string | undefined; reason?: string | undefined; created?: string | undefined; outcome?: string | undefined; end?: string | undefined; original?: string | undefined; duration?: string | number | undefined; kind?: string | undefined; hash?: string | undefined; url?: string | undefined; code?: string | undefined; action?: string | undefined; severity?: string | number | undefined; dataset?: string | undefined; ingested?: string | undefined; module?: string | undefined; provider?: string | undefined; reference?: string | undefined; risk_score?: number | undefined; risk_score_norm?: number | undefined; sequence?: string | number | undefined; timezone?: string | undefined; } & {}> | undefined; '@timestamp'?: string | undefined; } & {}> | undefined" ], "path": "x-pack/plugins/event_log/generated/schemas.ts", "deprecated": false, diff --git a/api_docs/event_log.mdx b/api_docs/event_log.mdx index b8d51a5ce3fba..ebc7137d5d2b7 100644 --- a/api_docs/event_log.mdx +++ b/api_docs/event_log.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/eventLog title: "eventLog" image: https://source.unsplash.com/400x175/?github description: API docs for the eventLog plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'eventLog'] --- import eventLogObj from './event_log.devdocs.json'; diff --git a/api_docs/expression_error.mdx b/api_docs/expression_error.mdx index 9078a1f726be7..11c81100860ec 100644 --- a/api_docs/expression_error.mdx +++ b/api_docs/expression_error.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/expressionError title: "expressionError" image: https://source.unsplash.com/400x175/?github description: API docs for the expressionError plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'expressionError'] --- import expressionErrorObj from './expression_error.devdocs.json'; diff --git a/api_docs/expression_gauge.mdx b/api_docs/expression_gauge.mdx index 15fc2987fcc1a..b210c0949ac25 100644 --- a/api_docs/expression_gauge.mdx +++ b/api_docs/expression_gauge.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/expressionGauge title: "expressionGauge" image: https://source.unsplash.com/400x175/?github description: API docs for the expressionGauge plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'expressionGauge'] --- import expressionGaugeObj from './expression_gauge.devdocs.json'; diff --git a/api_docs/expression_heatmap.mdx b/api_docs/expression_heatmap.mdx index 2f574dd28e239..be9a1342de6a7 100644 --- a/api_docs/expression_heatmap.mdx +++ b/api_docs/expression_heatmap.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/expressionHeatmap title: "expressionHeatmap" image: https://source.unsplash.com/400x175/?github description: API docs for the expressionHeatmap plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'expressionHeatmap'] --- import expressionHeatmapObj from './expression_heatmap.devdocs.json'; diff --git a/api_docs/expression_image.mdx b/api_docs/expression_image.mdx index 89ab837f3d0bb..760c9b9b85571 100644 --- a/api_docs/expression_image.mdx +++ b/api_docs/expression_image.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/expressionImage title: "expressionImage" image: https://source.unsplash.com/400x175/?github description: API docs for the expressionImage plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'expressionImage'] --- import expressionImageObj from './expression_image.devdocs.json'; diff --git a/api_docs/expression_legacy_metric_vis.mdx b/api_docs/expression_legacy_metric_vis.mdx index e1b2b9ae9fd2f..7320a0804c594 100644 --- a/api_docs/expression_legacy_metric_vis.mdx +++ b/api_docs/expression_legacy_metric_vis.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/expressionLegacyMetricVis title: "expressionLegacyMetricVis" image: https://source.unsplash.com/400x175/?github description: API docs for the expressionLegacyMetricVis plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'expressionLegacyMetricVis'] --- import expressionLegacyMetricVisObj from './expression_legacy_metric_vis.devdocs.json'; diff --git a/api_docs/expression_metric.mdx b/api_docs/expression_metric.mdx index 7398dbe10ce3f..47c59d750f269 100644 --- a/api_docs/expression_metric.mdx +++ b/api_docs/expression_metric.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/expressionMetric title: "expressionMetric" image: https://source.unsplash.com/400x175/?github description: API docs for the expressionMetric plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'expressionMetric'] --- import expressionMetricObj from './expression_metric.devdocs.json'; diff --git a/api_docs/expression_metric_vis.mdx b/api_docs/expression_metric_vis.mdx index b42f338758f1c..3e707b4cf8174 100644 --- a/api_docs/expression_metric_vis.mdx +++ b/api_docs/expression_metric_vis.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/expressionMetricVis title: "expressionMetricVis" image: https://source.unsplash.com/400x175/?github description: API docs for the expressionMetricVis plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'expressionMetricVis'] --- import expressionMetricVisObj from './expression_metric_vis.devdocs.json'; diff --git a/api_docs/expression_partition_vis.devdocs.json b/api_docs/expression_partition_vis.devdocs.json index cd8bf44d3cfa0..46b022d98fc9b 100644 --- a/api_docs/expression_partition_vis.devdocs.json +++ b/api_docs/expression_partition_vis.devdocs.json @@ -472,6 +472,20 @@ "deprecated": false, "trackAdoption": false }, + { + "parentPluginId": "expressionPartitionVis", + "id": "def-common.LabelsParams.colorOverrides", + "type": "Object", + "tags": [], + "label": "colorOverrides", + "description": [], + "signature": [ + "{ [x: string]: string; }" + ], + "path": "src/plugins/chart_expressions/expression_partition_vis/common/types/expression_renderers.ts", + "deprecated": false, + "trackAdoption": false + }, { "parentPluginId": "expressionPartitionVis", "id": "def-common.LabelsParams.truncate", @@ -542,7 +556,7 @@ "section": "def-common.MosaicVisConfig", "text": "MosaicVisConfig" }, - " extends Omit" + " extends Omit" ], "path": "src/plugins/chart_expressions/expression_partition_vis/common/types/expression_renderers.ts", "deprecated": false, @@ -1078,7 +1092,7 @@ "section": "def-common.ValueFormats", "text": "ValueFormats" }, - "; percentDecimals: number; truncate?: number | null | undefined; last_level?: boolean | undefined; }" + "; percentDecimals: number; colorOverrides: Record; truncate?: number | null | undefined; last_level?: boolean | undefined; }" ], "path": "src/plugins/chart_expressions/expression_partition_vis/common/types/expression_functions.ts", "deprecated": false, diff --git a/api_docs/expression_partition_vis.mdx b/api_docs/expression_partition_vis.mdx index 1a0b8f7af1ffe..93174faee444c 100644 --- a/api_docs/expression_partition_vis.mdx +++ b/api_docs/expression_partition_vis.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/expressionPartitionVis title: "expressionPartitionVis" image: https://source.unsplash.com/400x175/?github description: API docs for the expressionPartitionVis plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'expressionPartitionVis'] --- import expressionPartitionVisObj from './expression_partition_vis.devdocs.json'; @@ -21,7 +21,7 @@ Contact [Vis Editors](https://github.com/orgs/elastic/teams/kibana-visualization | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 71 | 0 | 71 | 2 | +| 72 | 0 | 72 | 2 | ## Client diff --git a/api_docs/expression_repeat_image.mdx b/api_docs/expression_repeat_image.mdx index 936093461ebca..acf4d6a16a952 100644 --- a/api_docs/expression_repeat_image.mdx +++ b/api_docs/expression_repeat_image.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/expressionRepeatImage title: "expressionRepeatImage" image: https://source.unsplash.com/400x175/?github description: API docs for the expressionRepeatImage plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'expressionRepeatImage'] --- import expressionRepeatImageObj from './expression_repeat_image.devdocs.json'; diff --git a/api_docs/expression_reveal_image.mdx b/api_docs/expression_reveal_image.mdx index 1745b34f7fe45..79c2c425f3042 100644 --- a/api_docs/expression_reveal_image.mdx +++ b/api_docs/expression_reveal_image.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/expressionRevealImage title: "expressionRevealImage" image: https://source.unsplash.com/400x175/?github description: API docs for the expressionRevealImage plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'expressionRevealImage'] --- import expressionRevealImageObj from './expression_reveal_image.devdocs.json'; diff --git a/api_docs/expression_shape.mdx b/api_docs/expression_shape.mdx index 6502d94049f64..eae8ecaf4c44f 100644 --- a/api_docs/expression_shape.mdx +++ b/api_docs/expression_shape.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/expressionShape title: "expressionShape" image: https://source.unsplash.com/400x175/?github description: API docs for the expressionShape plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'expressionShape'] --- import expressionShapeObj from './expression_shape.devdocs.json'; diff --git a/api_docs/expression_tagcloud.mdx b/api_docs/expression_tagcloud.mdx index ffabc2f3a2804..d57c2333490e7 100644 --- a/api_docs/expression_tagcloud.mdx +++ b/api_docs/expression_tagcloud.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/expressionTagcloud title: "expressionTagcloud" image: https://source.unsplash.com/400x175/?github description: API docs for the expressionTagcloud plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'expressionTagcloud'] --- import expressionTagcloudObj from './expression_tagcloud.devdocs.json'; diff --git a/api_docs/expression_x_y.mdx b/api_docs/expression_x_y.mdx index caceb53a01efd..c65c8a4667f05 100644 --- a/api_docs/expression_x_y.mdx +++ b/api_docs/expression_x_y.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/expressionXY title: "expressionXY" image: https://source.unsplash.com/400x175/?github description: API docs for the expressionXY plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'expressionXY'] --- import expressionXYObj from './expression_x_y.devdocs.json'; diff --git a/api_docs/expressions.mdx b/api_docs/expressions.mdx index 052710b2f1a50..d6c2ef371a449 100644 --- a/api_docs/expressions.mdx +++ b/api_docs/expressions.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/expressions title: "expressions" image: https://source.unsplash.com/400x175/?github description: API docs for the expressions plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'expressions'] --- import expressionsObj from './expressions.devdocs.json'; diff --git a/api_docs/features.mdx b/api_docs/features.mdx index 7b6126621d894..9fae063de9331 100644 --- a/api_docs/features.mdx +++ b/api_docs/features.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/features title: "features" image: https://source.unsplash.com/400x175/?github description: API docs for the features plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'features'] --- import featuresObj from './features.devdocs.json'; diff --git a/api_docs/field_formats.mdx b/api_docs/field_formats.mdx index 2baf34bf4ce1b..c15174434f417 100644 --- a/api_docs/field_formats.mdx +++ b/api_docs/field_formats.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/fieldFormats title: "fieldFormats" image: https://source.unsplash.com/400x175/?github description: API docs for the fieldFormats plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'fieldFormats'] --- import fieldFormatsObj from './field_formats.devdocs.json'; diff --git a/api_docs/file_upload.mdx b/api_docs/file_upload.mdx index 29cad094b4f70..699fd05437bd5 100644 --- a/api_docs/file_upload.mdx +++ b/api_docs/file_upload.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/fileUpload title: "fileUpload" image: https://source.unsplash.com/400x175/?github description: API docs for the fileUpload plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'fileUpload'] --- import fileUploadObj from './file_upload.devdocs.json'; diff --git a/api_docs/files.devdocs.json b/api_docs/files.devdocs.json index 761cb3e7578f2..664a95623f5a9 100644 --- a/api_docs/files.devdocs.json +++ b/api_docs/files.devdocs.json @@ -2,655 +2,109 @@ "id": "files", "client": { "classes": [], - "functions": [ - { - "parentPluginId": "files", - "id": "def-public.FilePicker", - "type": "Function", - "tags": [], - "label": "FilePicker", - "description": [], - "signature": [ - "(props: ", - { - "pluginId": "files", - "scope": "public", - "docId": "kibFilesPluginApi", - "section": "def-public.Props", - "text": "Props" - }, - ") => JSX.Element" - ], - "path": "src/plugins/files/public/components/file_picker/index.tsx", - "deprecated": false, - "trackAdoption": false, - "children": [ - { - "parentPluginId": "files", - "id": "def-public.FilePicker.$1", - "type": "Object", - "tags": [], - "label": "props", - "description": [], - "signature": [ - { - "pluginId": "files", - "scope": "public", - "docId": "kibFilesPluginApi", - "section": "def-public.Props", - "text": "Props" - }, - "" - ], - "path": "src/plugins/files/public/components/file_picker/index.tsx", - "deprecated": false, - "trackAdoption": false, - "isRequired": true - } - ], - "returnComment": [], - "initialIsOpen": false - }, - { - "parentPluginId": "files", - "id": "def-public.FilesContext", - "type": "Function", - "tags": [], - "label": "FilesContext", - "description": [], - "signature": [ - "({ client, children }: React.PropsWithChildren) => JSX.Element" - ], - "path": "src/plugins/files/public/components/context.tsx", - "deprecated": false, - "trackAdoption": false, - "children": [ - { - "parentPluginId": "files", - "id": "def-public.FilesContext.$1", - "type": "CompoundType", - "tags": [], - "label": "{ client, children }", - "description": [], - "signature": [ - "React.PropsWithChildren" - ], - "path": "src/plugins/files/public/components/context.tsx", - "deprecated": false, - "trackAdoption": false, - "isRequired": true - } - ], - "returnComment": [], - "initialIsOpen": false - }, - { - "parentPluginId": "files", - "id": "def-public.UploadFile", - "type": "Function", - "tags": [], - "label": "UploadFile", - "description": [], - "signature": [ - "(props: ", - { - "pluginId": "files", - "scope": "public", - "docId": "kibFilesPluginApi", - "section": "def-public.UploadFileProps", - "text": "UploadFileProps" - }, - ") => JSX.Element" - ], - "path": "src/plugins/files/public/components/upload_file/index.tsx", - "deprecated": false, - "trackAdoption": false, - "children": [ - { - "parentPluginId": "files", - "id": "def-public.UploadFile.$1", - "type": "CompoundType", - "tags": [], - "label": "props", - "description": [], - "signature": [ - { - "pluginId": "files", - "scope": "public", - "docId": "kibFilesPluginApi", - "section": "def-public.UploadFileProps", - "text": "UploadFileProps" - } - ], - "path": "src/plugins/files/public/components/upload_file/index.tsx", - "deprecated": false, - "trackAdoption": false, - "isRequired": true - } - ], - "returnComment": [], - "initialIsOpen": false - } - ], - "interfaces": [ - { - "parentPluginId": "files", - "id": "def-public.FilesClient", - "type": "Interface", - "tags": [], - "label": "FilesClient", - "description": [ - "\nA client that can be used to manage a specific {@link FileKind}." - ], - "signature": [ - { - "pluginId": "files", - "scope": "public", - "docId": "kibFilesPluginApi", - "section": "def-public.FilesClient", - "text": "FilesClient" - }, - " extends GlobalEndpoints" - ], - "path": "src/plugins/files/public/types.ts", - "deprecated": false, - "trackAdoption": false, - "children": [ - { - "parentPluginId": "files", - "id": "def-public.FilesClient.create", - "type": "Function", - "tags": [], - "label": "create", - "description": [ - "\nCreate a new file object with the provided metadata.\n" - ], - "signature": [ - "(args: Readonly<{ meta?: Readonly<{} & {}> | undefined; alt?: string | undefined; mimeType?: string | undefined; } & { name: string; }> & { abortSignal?: AbortSignal | undefined; } & { kind: string; }) => Promise<{ file: ", - { - "pluginId": "files", - "scope": "common", - "docId": "kibFilesPluginApi", - "section": "def-common.FileJSON", - "text": "FileJSON" - }, - "; }>" - ], - "path": "src/plugins/files/public/types.ts", - "deprecated": false, - "trackAdoption": false, - "returnComment": [], - "children": [ - { - "parentPluginId": "files", - "id": "def-public.FilesClient.create.$1", - "type": "CompoundType", - "tags": [], - "label": "args", - "description": [ - "- create file args" - ], - "signature": [ - "E[\"inputs\"][\"body\"] & E[\"inputs\"][\"params\"] & E[\"inputs\"][\"query\"] & { abortSignal?: AbortSignal | undefined; } & { kind: string; } & ExtraArgs" - ], - "path": "src/plugins/files/public/types.ts", - "deprecated": false, - "trackAdoption": false - } - ] - }, - { - "parentPluginId": "files", - "id": "def-public.FilesClient.delete", - "type": "Function", - "tags": [], - "label": "delete", - "description": [ - "\nDelete a file object and all associated share and content objects.\n" - ], - "signature": [ - "(args: Readonly<{} & { id: string; }> & { abortSignal?: AbortSignal | undefined; } & { kind: string; }) => Promise<{ ok: true; }>" - ], - "path": "src/plugins/files/public/types.ts", - "deprecated": false, - "trackAdoption": false, - "returnComment": [], - "children": [ - { - "parentPluginId": "files", - "id": "def-public.FilesClient.delete.$1", - "type": "CompoundType", - "tags": [], - "label": "args", - "description": [ - "- delete file args" - ], - "signature": [ - "E[\"inputs\"][\"body\"] & E[\"inputs\"][\"params\"] & E[\"inputs\"][\"query\"] & { abortSignal?: AbortSignal | undefined; } & { kind: string; } & ExtraArgs" - ], - "path": "src/plugins/files/public/types.ts", - "deprecated": false, - "trackAdoption": false - } - ] - }, - { - "parentPluginId": "files", - "id": "def-public.FilesClient.getById", - "type": "Function", - "tags": [], - "label": "getById", - "description": [ - "\nGet a file object by ID.\n" - ], - "signature": [ - "(args: Readonly<{} & { id: string; }> & { abortSignal?: AbortSignal | undefined; } & { kind: string; }) => Promise<{ file: ", - { - "pluginId": "files", - "scope": "common", - "docId": "kibFilesPluginApi", - "section": "def-common.FileJSON", - "text": "FileJSON" - }, - "; }>" - ], - "path": "src/plugins/files/public/types.ts", - "deprecated": false, - "trackAdoption": false, - "returnComment": [], - "children": [ - { - "parentPluginId": "files", - "id": "def-public.FilesClient.getById.$1", - "type": "CompoundType", - "tags": [], - "label": "args", - "description": [ - "- get file by ID args" - ], - "signature": [ - "E[\"inputs\"][\"body\"] & E[\"inputs\"][\"params\"] & E[\"inputs\"][\"query\"] & { abortSignal?: AbortSignal | undefined; } & { kind: string; } & ExtraArgs" - ], - "path": "src/plugins/files/public/types.ts", - "deprecated": false, - "trackAdoption": false - } - ] - }, - { - "parentPluginId": "files", - "id": "def-public.FilesClient.list", - "type": "Function", - "tags": [], - "label": "list", - "description": [ - "\nList all file objects, of a given {@link FileKind}.\n" - ], - "signature": [ - "(args: Readonly<{ name?: string | string[] | undefined; status?: string | string[] | undefined; meta?: Readonly<{} & {}> | undefined; extension?: string | string[] | undefined; } & {}> & Readonly<{ page?: number | undefined; perPage?: number | undefined; } & {}> & { abortSignal?: AbortSignal | undefined; } & { kind: string; }) => Promise<{ files: ", - { - "pluginId": "files", - "scope": "common", - "docId": "kibFilesPluginApi", - "section": "def-common.FileJSON", - "text": "FileJSON" - }, - "[]; total: number; }>" - ], - "path": "src/plugins/files/public/types.ts", - "deprecated": false, - "trackAdoption": false, - "returnComment": [], - "children": [ - { - "parentPluginId": "files", - "id": "def-public.FilesClient.list.$1", - "type": "CompoundType", - "tags": [], - "label": "args", - "description": [ - "- list files args" - ], - "signature": [ - "E[\"inputs\"][\"body\"] & E[\"inputs\"][\"params\"] & E[\"inputs\"][\"query\"] & { abortSignal?: AbortSignal | undefined; } & { kind: string; } & ExtraArgs" - ], - "path": "src/plugins/files/public/types.ts", - "deprecated": false, - "trackAdoption": false - } - ] - }, - { - "parentPluginId": "files", - "id": "def-public.FilesClient.update", - "type": "Function", - "tags": [], - "label": "update", - "description": [ - "\nUpdate a set of of metadata values of the file object.\n" - ], - "signature": [ - "(args: Readonly<{ name?: string | undefined; meta?: Readonly<{} & {}> | undefined; alt?: string | undefined; } & {}> & Readonly<{} & { id: string; }> & { abortSignal?: AbortSignal | undefined; } & { kind: string; }) => Promise<{ file: ", - { - "pluginId": "files", - "scope": "common", - "docId": "kibFilesPluginApi", - "section": "def-common.FileJSON", - "text": "FileJSON" - }, - "; }>" - ], - "path": "src/plugins/files/public/types.ts", - "deprecated": false, - "trackAdoption": false, - "returnComment": [], - "children": [ - { - "parentPluginId": "files", - "id": "def-public.FilesClient.update.$1", - "type": "CompoundType", - "tags": [], - "label": "args", - "description": [ - "- update file args" - ], - "signature": [ - "E[\"inputs\"][\"body\"] & E[\"inputs\"][\"params\"] & E[\"inputs\"][\"query\"] & { abortSignal?: AbortSignal | undefined; } & { kind: string; } & ExtraArgs" - ], - "path": "src/plugins/files/public/types.ts", - "deprecated": false, - "trackAdoption": false - } - ] - }, - { - "parentPluginId": "files", - "id": "def-public.FilesClient.upload", - "type": "Function", - "tags": [], - "label": "upload", - "description": [ - "\nStream the contents of the file to Kibana server for storage.\n" - ], - "signature": [ - "(args: Readonly<{} & { id: string; }> & Readonly<{ selfDestructOnAbort?: boolean | undefined; } & {}> & { body: unknown; kind: string; abortSignal?: AbortSignal | undefined; contentType?: string | undefined; }) => Promise<{ ok: true; size: number; }>" - ], - "path": "src/plugins/files/public/types.ts", - "deprecated": false, - "trackAdoption": false, - "children": [ - { - "parentPluginId": "files", - "id": "def-public.FilesClient.upload.$1", - "type": "CompoundType", - "tags": [], - "label": "args", - "description": [ - "- upload file args" - ], - "signature": [ - "Readonly<{} & { id: string; }> & Readonly<{ selfDestructOnAbort?: boolean | undefined; } & {}> & { body: unknown; kind: string; abortSignal?: AbortSignal | undefined; contentType?: string | undefined; }" - ], - "path": "src/plugins/files/public/types.ts", - "deprecated": false, - "trackAdoption": false, - "isRequired": true - } - ], - "returnComment": [] - }, - { - "parentPluginId": "files", - "id": "def-public.FilesClient.download", - "type": "Function", - "tags": [], - "label": "download", - "description": [ - "\nStream a download of the file object's content.\n" - ], - "signature": [ - "(args: Readonly<{ fileName?: string | undefined; } & { id: string; }> & { abortSignal?: AbortSignal | undefined; } & { kind: string; }) => Promise" - ], - "path": "src/plugins/files/public/types.ts", - "deprecated": false, - "trackAdoption": false, - "returnComment": [], - "children": [ - { - "parentPluginId": "files", - "id": "def-public.FilesClient.download.$1", - "type": "CompoundType", - "tags": [], - "label": "args", - "description": [ - "- download file args" - ], - "signature": [ - "E[\"inputs\"][\"body\"] & E[\"inputs\"][\"params\"] & E[\"inputs\"][\"query\"] & { abortSignal?: AbortSignal | undefined; } & { kind: string; } & ExtraArgs" - ], - "path": "src/plugins/files/public/types.ts", - "deprecated": false, - "trackAdoption": false - } - ] - }, - { - "parentPluginId": "files", - "id": "def-public.FilesClient.getDownloadHref", - "type": "Function", - "tags": [], - "label": "getDownloadHref", - "description": [ - "\nGet a string for downloading a file that can be passed to a button element's\nhref for download.\n" - ], - "signature": [ - "(args: Pick<", - { - "pluginId": "files", - "scope": "common", - "docId": "kibFilesPluginApi", - "section": "def-common.FileJSON", - "text": "FileJSON" - }, - ", \"id\" | \"fileKind\">) => string" - ], - "path": "src/plugins/files/public/types.ts", - "deprecated": false, - "trackAdoption": false, - "children": [ - { - "parentPluginId": "files", - "id": "def-public.FilesClient.getDownloadHref.$1", - "type": "Object", - "tags": [], - "label": "args", - "description": [ - "- get download URL args" - ], - "signature": [ - "Pick<", - { - "pluginId": "files", - "scope": "common", - "docId": "kibFilesPluginApi", - "section": "def-common.FileJSON", - "text": "FileJSON" - }, - ", \"id\" | \"fileKind\">" - ], - "path": "src/plugins/files/public/types.ts", - "deprecated": false, - "trackAdoption": false, - "isRequired": true - } - ], - "returnComment": [] - }, + "functions": [], + "interfaces": [ + { + "parentPluginId": "files", + "id": "def-public.FilesClient", + "type": "Interface", + "tags": [], + "label": "FilesClient", + "description": [ + "\nA client that can be used to manage a specific {@link FileKind}." + ], + "signature": [ + "FilesClient", + " extends ", + "BaseFilesClient", + "" + ], + "path": "src/plugins/files/common/files_client.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ { "parentPluginId": "files", - "id": "def-public.FilesClient.share", + "id": "def-public.FilesClient.getMetrics", "type": "Function", - "tags": [ - "note" - ], - "label": "share", + "tags": [], + "label": "getMetrics", "description": [ - "\nShare a file by creating a new file share instance.\n" + "\nGet metrics of file system, like storage usage.\n" ], "signature": [ - "(args: Readonly<{ name?: string | undefined; validUntil?: number | undefined; } & {}> & Readonly<{} & { fileId: string; }> & { abortSignal?: AbortSignal | undefined; } & { kind: string; }) => Promise<", + "() => Promise<", { "pluginId": "files", "scope": "common", "docId": "kibFilesPluginApi", - "section": "def-common.FileShareJSONWithToken", - "text": "FileShareJSONWithToken" + "section": "def-common.FilesMetrics", + "text": "FilesMetrics" }, ">" ], - "path": "src/plugins/files/public/types.ts", - "deprecated": false, - "trackAdoption": false, - "returnComment": [], - "children": [ - { - "parentPluginId": "files", - "id": "def-public.FilesClient.share.$1", - "type": "CompoundType", - "tags": [], - "label": "args", - "description": [ - "- File share arguments" - ], - "signature": [ - "E[\"inputs\"][\"body\"] & E[\"inputs\"][\"params\"] & E[\"inputs\"][\"query\"] & { abortSignal?: AbortSignal | undefined; } & { kind: string; } & ExtraArgs" - ], - "path": "src/plugins/files/public/types.ts", - "deprecated": false, - "trackAdoption": false - } - ] - }, - { - "parentPluginId": "files", - "id": "def-public.FilesClient.unshare", - "type": "Function", - "tags": [], - "label": "unshare", - "description": [ - "\nDelete a file share instance.\n" - ], - "signature": [ - "(args: Readonly<{} & { id: string; }> & { abortSignal?: AbortSignal | undefined; } & { kind: string; }) => Promise<{ ok: true; }>" - ], - "path": "src/plugins/files/public/types.ts", + "path": "src/plugins/files/common/files_client.ts", "deprecated": false, "trackAdoption": false, - "returnComment": [], - "children": [ - { - "parentPluginId": "files", - "id": "def-public.FilesClient.unshare.$1", - "type": "CompoundType", - "tags": [], - "label": "args", - "description": [ - "- File unshare arguments" - ], - "signature": [ - "E[\"inputs\"][\"body\"] & E[\"inputs\"][\"params\"] & E[\"inputs\"][\"query\"] & { abortSignal?: AbortSignal | undefined; } & { kind: string; } & ExtraArgs" - ], - "path": "src/plugins/files/public/types.ts", - "deprecated": false, - "trackAdoption": false - } - ] + "children": [], + "returnComment": [] }, { "parentPluginId": "files", - "id": "def-public.FilesClient.getShare", + "id": "def-public.FilesClient.publicDownload", "type": "Function", "tags": [], - "label": "getShare", + "label": "publicDownload", "description": [ - "\nGet a file share instance.\n" + "\nDownload a file, bypassing regular security by way of a\nsecret share token.\n" ], "signature": [ - "(args: Readonly<{} & { id: string; }> & { abortSignal?: AbortSignal | undefined; } & { kind: string; }) => Promise<{ share: ", - { - "pluginId": "files", - "scope": "common", - "docId": "kibFilesPluginApi", - "section": "def-common.FileShareJSON", - "text": "FileShareJSON" - }, - "; }>" + "(args: { token: string; fileName?: string | undefined; }) => any" ], - "path": "src/plugins/files/public/types.ts", + "path": "src/plugins/files/common/files_client.ts", "deprecated": false, "trackAdoption": false, - "returnComment": [], "children": [ { "parentPluginId": "files", - "id": "def-public.FilesClient.getShare.$1", - "type": "CompoundType", + "id": "def-public.FilesClient.publicDownload.$1", + "type": "Object", "tags": [], "label": "args", - "description": [ - "- Get file share arguments" - ], - "signature": [ - "E[\"inputs\"][\"body\"] & E[\"inputs\"][\"params\"] & E[\"inputs\"][\"query\"] & { abortSignal?: AbortSignal | undefined; } & { kind: string; } & ExtraArgs" - ], - "path": "src/plugins/files/public/types.ts", + "description": [], + "path": "src/plugins/files/common/files_client.ts", "deprecated": false, - "trackAdoption": false + "trackAdoption": false, + "children": [ + { + "parentPluginId": "files", + "id": "def-public.FilesClient.publicDownload.$1.token", + "type": "string", + "tags": [], + "label": "token", + "description": [], + "path": "src/plugins/files/common/files_client.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "files", + "id": "def-public.FilesClient.publicDownload.$1.fileName", + "type": "string", + "tags": [], + "label": "fileName", + "description": [], + "signature": [ + "string | undefined" + ], + "path": "src/plugins/files/common/files_client.ts", + "deprecated": false, + "trackAdoption": false + } + ] } - ] - }, - { - "parentPluginId": "files", - "id": "def-public.FilesClient.listShares", - "type": "Function", - "tags": [], - "label": "listShares", - "description": [ - "\nList all file shares. Optionally scoping to a specific\nfile.\n" - ], - "signature": [ - "(args: Readonly<{ page?: number | undefined; perPage?: number | undefined; forFileId?: string | undefined; } & {}> & { abortSignal?: AbortSignal | undefined; } & { kind: string; }) => Promise<{ shares: ", - { - "pluginId": "files", - "scope": "common", - "docId": "kibFilesPluginApi", - "section": "def-common.FileShareJSON", - "text": "FileShareJSON" - }, - "[]; }>" ], - "path": "src/plugins/files/public/types.ts", - "deprecated": false, - "trackAdoption": false, - "returnComment": [], - "children": [ - { - "parentPluginId": "files", - "id": "def-public.FilesClient.listShares.$1", - "type": "CompoundType", - "tags": [], - "label": "args", - "description": [ - "- Get file share arguments" - ], - "signature": [ - "E[\"inputs\"][\"body\"] & E[\"inputs\"][\"params\"] & E[\"inputs\"][\"query\"] & { abortSignal?: AbortSignal | undefined; } & { kind: string; } & ExtraArgs" - ], - "path": "src/plugins/files/public/types.ts", - "deprecated": false, - "trackAdoption": false - } - ] + "returnComment": [] } ], "initialIsOpen": false @@ -664,7 +118,7 @@ "description": [ "\nA factory for creating a {@link ScopedFilesClient}" ], - "path": "src/plugins/files/public/types.ts", + "path": "src/plugins/files/common/files_client.ts", "deprecated": false, "trackAdoption": false, "children": [ @@ -679,16 +133,10 @@ ], "signature": [ "() => ", - { - "pluginId": "files", - "scope": "public", - "docId": "kibFilesPluginApi", - "section": "def-public.FilesClient", - "text": "FilesClient" - }, + "FilesClient", "" ], - "path": "src/plugins/files/public/types.ts", + "path": "src/plugins/files/common/files_client.ts", "deprecated": false, "trackAdoption": false, "children": [], @@ -705,16 +153,10 @@ ], "signature": [ "(fileKind: string) => ", - { - "pluginId": "files", - "scope": "public", - "docId": "kibFilesPluginApi", - "section": "def-public.ScopedFilesClient", - "text": "ScopedFilesClient" - }, + "ScopedFilesClient", "" ], - "path": "src/plugins/files/public/types.ts", + "path": "src/plugins/files/common/files_client.ts", "deprecated": false, "trackAdoption": false, "children": [ @@ -724,197 +166,19 @@ "type": "string", "tags": [], "label": "fileKind", - "description": [ - "- The {@link FileKind } to create a client for." - ], - "signature": [ - "string" - ], - "path": "src/plugins/files/public/types.ts", - "deprecated": false, - "trackAdoption": false, - "isRequired": true - } - ], - "returnComment": [] - } - ], - "initialIsOpen": false - }, - { - "parentPluginId": "files", - "id": "def-public.Props", - "type": "Interface", - "tags": [], - "label": "Props", - "description": [], - "signature": [ - { - "pluginId": "files", - "scope": "public", - "docId": "kibFilesPluginApi", - "section": "def-public.Props", - "text": "Props" - }, - "" - ], - "path": "src/plugins/files/public/components/file_picker/file_picker.tsx", - "deprecated": false, - "trackAdoption": false, - "children": [ - { - "parentPluginId": "files", - "id": "def-public.Props.kind", - "type": "Uncategorized", - "tags": [], - "label": "kind", - "description": [ - "\nThe file kind that was passed to the registry." - ], - "signature": [ - "Kind" - ], - "path": "src/plugins/files/public/components/file_picker/file_picker.tsx", - "deprecated": false, - "trackAdoption": false - }, - { - "parentPluginId": "files", - "id": "def-public.Props.onClose", - "type": "Function", - "tags": [], - "label": "onClose", - "description": [ - "\nWill be called when the modal is closed" - ], - "signature": [ - "() => void" - ], - "path": "src/plugins/files/public/components/file_picker/file_picker.tsx", - "deprecated": false, - "trackAdoption": false, - "children": [], - "returnComment": [] - }, - { - "parentPluginId": "files", - "id": "def-public.Props.onDone", - "type": "Function", - "tags": [], - "label": "onDone", - "description": [ - "\nWill be called after a user has a selected a set of files" - ], - "signature": [ - "(files: ", - { - "pluginId": "files", - "scope": "common", - "docId": "kibFilesPluginApi", - "section": "def-common.FileJSON", - "text": "FileJSON" - }, - "[]) => void" - ], - "path": "src/plugins/files/public/components/file_picker/file_picker.tsx", - "deprecated": false, - "trackAdoption": false, - "children": [ - { - "parentPluginId": "files", - "id": "def-public.Props.onDone.$1", - "type": "Array", - "tags": [], - "label": "files", - "description": [], - "signature": [ - { - "pluginId": "files", - "scope": "common", - "docId": "kibFilesPluginApi", - "section": "def-common.FileJSON", - "text": "FileJSON" - }, - "[]" - ], - "path": "src/plugins/files/public/components/file_picker/file_picker.tsx", - "deprecated": false, - "trackAdoption": false, - "isRequired": true - } - ], - "returnComment": [] - }, - { - "parentPluginId": "files", - "id": "def-public.Props.onUpload", - "type": "Function", - "tags": [], - "label": "onUpload", - "description": [ - "\nWhen a user has successfully uploaded some files this callback will be called" - ], - "signature": [ - "((done: ", - "DoneNotification", - "[]) => void) | undefined" - ], - "path": "src/plugins/files/public/components/file_picker/file_picker.tsx", - "deprecated": false, - "trackAdoption": false, - "children": [ - { - "parentPluginId": "files", - "id": "def-public.Props.onUpload.$1", - "type": "Array", - "tags": [], - "label": "done", - "description": [], + "description": [ + "- The {@link FileKind } to create a client for." + ], "signature": [ - "DoneNotification", - "[]" + "string" ], - "path": "src/plugins/files/public/components/file_picker/file_picker.tsx", + "path": "src/plugins/files/common/files_client.ts", "deprecated": false, "trackAdoption": false, "isRequired": true } ], "returnComment": [] - }, - { - "parentPluginId": "files", - "id": "def-public.Props.pageSize", - "type": "number", - "tags": [], - "label": "pageSize", - "description": [ - "\nThe number of results to show per page." - ], - "signature": [ - "number | undefined" - ], - "path": "src/plugins/files/public/components/file_picker/file_picker.tsx", - "deprecated": false, - "trackAdoption": false - }, - { - "parentPluginId": "files", - "id": "def-public.Props.multiple", - "type": "CompoundType", - "tags": [ - "default" - ], - "label": "multiple", - "description": [ - "\nWhether you can select one or more files\n" - ], - "signature": [ - "boolean | undefined" - ], - "path": "src/plugins/files/public/components/file_picker/file_picker.tsx", - "deprecated": false, - "trackAdoption": false } ], "initialIsOpen": false @@ -930,63 +194,7 @@ "label": "FilesClientResponses", "description": [], "signature": [ - "{ create: { file: ", - { - "pluginId": "files", - "scope": "common", - "docId": "kibFilesPluginApi", - "section": "def-common.FileJSON", - "text": "FileJSON" - }, - "; }; delete: { ok: true; }; getById: { file: ", - { - "pluginId": "files", - "scope": "common", - "docId": "kibFilesPluginApi", - "section": "def-common.FileJSON", - "text": "FileJSON" - }, - "; }; list: { files: ", - { - "pluginId": "files", - "scope": "common", - "docId": "kibFilesPluginApi", - "section": "def-common.FileJSON", - "text": "FileJSON" - }, - "[]; total: number; }; update: { file: ", - { - "pluginId": "files", - "scope": "common", - "docId": "kibFilesPluginApi", - "section": "def-common.FileJSON", - "text": "FileJSON" - }, - "; }; upload: { ok: true; size: number; }; download: any; getDownloadHref: string; share: ", - { - "pluginId": "files", - "scope": "common", - "docId": "kibFilesPluginApi", - "section": "def-common.FileShareJSON", - "text": "FileShareJSON" - }, - " & { token: string; }; unshare: { ok: true; }; getShare: { share: ", - { - "pluginId": "files", - "scope": "common", - "docId": "kibFilesPluginApi", - "section": "def-common.FileShareJSON", - "text": "FileShareJSON" - }, - "; }; listShares: { shares: ", - { - "pluginId": "files", - "scope": "common", - "docId": "kibFilesPluginApi", - "section": "def-common.FileShareJSON", - "text": "FileShareJSON" - }, - "[]; }; getMetrics: ", + "{ getMetrics: ", { "pluginId": "files", "scope": "common", @@ -995,16 +203,20 @@ "text": "FilesMetrics" }, "; publicDownload: any; find: { files: ", - { - "pluginId": "files", - "scope": "common", - "docId": "kibFilesPluginApi", - "section": "def-common.FileJSON", - "text": "FileJSON" - }, - "[]; total: number; }; bulkDelete: Result; }" + "FileJSON", + "[]; total: number; }; bulkDelete: { succeeded: string[]; failed?: [id: string, reason: string][] | undefined; }; create: { file: ", + "FileJSON", + "; }; delete: { ok: true; }; getById: { file: ", + "FileJSON", + "; }; list: { files: ", + "FileJSON", + "[]; total: number; }; update: { file: ", + "FileJSON", + "; }; upload: { ok: true; size: number; }; download: any; getDownloadHref: string; share: any; unshare: { ok: true; }; getShare: { share: FileShareJSON; }; listShares: { shares: FileShareJSON[]; }; getFileKind: ", + "FileKind", + "; }" ], - "path": "src/plugins/files/public/types.ts", + "path": "src/plugins/files/common/files_client.ts", "deprecated": false, "trackAdoption": false, "initialIsOpen": false @@ -1019,71 +231,7 @@ "\nA files client that is scoped to a specific {@link FileKind}.\n\nMore convenient if you want to re-use the same client for the same file kind\nand not specify the kind every time." ], "signature": [ - "{ create: (arg: Omit | undefined; alt?: string | undefined; mimeType?: string | undefined; } & { name: string; }> & { abortSignal?: AbortSignal | undefined; } & { kind: string; }, \"kind\">) => Promise<{ file: ", - { - "pluginId": "files", - "scope": "common", - "docId": "kibFilesPluginApi", - "section": "def-common.FileJSON", - "text": "FileJSON" - }, - "; }>; delete: (arg: Omit & { abortSignal?: AbortSignal | undefined; } & { kind: string; }, \"kind\">) => Promise<{ ok: true; }>; getById: (arg: Omit & { abortSignal?: AbortSignal | undefined; } & { kind: string; }, \"kind\">) => Promise<{ file: ", - { - "pluginId": "files", - "scope": "common", - "docId": "kibFilesPluginApi", - "section": "def-common.FileJSON", - "text": "FileJSON" - }, - "; }>; list: (arg?: Omit | undefined; extension?: string | string[] | undefined; } & {}> & Readonly<{ page?: number | undefined; perPage?: number | undefined; } & {}> & { abortSignal?: AbortSignal | undefined; } & { kind: string; }, \"kind\"> | undefined) => Promise<{ files: ", - { - "pluginId": "files", - "scope": "common", - "docId": "kibFilesPluginApi", - "section": "def-common.FileJSON", - "text": "FileJSON" - }, - "[]; total: number; }>; update: (arg: Omit | undefined; alt?: string | undefined; } & {}> & Readonly<{} & { id: string; }> & { abortSignal?: AbortSignal | undefined; } & { kind: string; }, \"kind\">) => Promise<{ file: ", - { - "pluginId": "files", - "scope": "common", - "docId": "kibFilesPluginApi", - "section": "def-common.FileJSON", - "text": "FileJSON" - }, - "; }>; upload: (arg: Omit & Readonly<{ selfDestructOnAbort?: boolean | undefined; } & {}> & { body: unknown; kind: string; abortSignal?: AbortSignal | undefined; contentType?: string | undefined; }, \"kind\">) => Promise<{ ok: true; size: number; }>; download: (arg: Omit & { abortSignal?: AbortSignal | undefined; } & { kind: string; }, \"kind\">) => Promise; getDownloadHref: (arg: Omit, \"id\" | \"fileKind\">, \"kind\">) => string; share: (arg: Omit & Readonly<{} & { fileId: string; }> & { abortSignal?: AbortSignal | undefined; } & { kind: string; }, \"kind\">) => Promise<", - { - "pluginId": "files", - "scope": "common", - "docId": "kibFilesPluginApi", - "section": "def-common.FileShareJSONWithToken", - "text": "FileShareJSONWithToken" - }, - ">; unshare: (arg: Omit & { abortSignal?: AbortSignal | undefined; } & { kind: string; }, \"kind\">) => Promise<{ ok: true; }>; getShare: (arg: Omit & { abortSignal?: AbortSignal | undefined; } & { kind: string; }, \"kind\">) => Promise<{ share: ", - { - "pluginId": "files", - "scope": "common", - "docId": "kibFilesPluginApi", - "section": "def-common.FileShareJSON", - "text": "FileShareJSON" - }, - "; }>; listShares: (arg: Omit & { abortSignal?: AbortSignal | undefined; } & { kind: string; }, \"kind\">) => Promise<{ shares: ", - { - "pluginId": "files", - "scope": "common", - "docId": "kibFilesPluginApi", - "section": "def-common.FileShareJSON", - "text": "FileShareJSON" - }, - "[]; }>; getMetrics: (arg: Omit) => Promise<", + "{ getMetrics: (arg: Omit) => Promise<", { "pluginId": "files", "scope": "common", @@ -1091,39 +239,55 @@ "section": "def-common.FilesMetrics", "text": "FilesMetrics" }, - ">; publicDownload: (arg: Omit & Readonly<{} & { token: string; }> & { abortSignal?: AbortSignal | undefined; }, \"kind\">) => Promise; find: (arg: Omit | undefined; extension?: string | string[] | undefined; kind?: string | string[] | undefined; } & {}> & Readonly<{ page?: number | undefined; perPage?: number | undefined; } & {}> & { abortSignal?: AbortSignal | undefined; }, \"kind\">) => Promise<{ files: ", - { - "pluginId": "files", - "scope": "common", - "docId": "kibFilesPluginApi", - "section": "def-common.FileJSON", - "text": "FileJSON" - }, - "[]; total: number; }>; bulkDelete: (arg: Omit & { abortSignal?: AbortSignal | undefined; }, \"kind\">) => Promise; }" - ], - "path": "src/plugins/files/public/types.ts", - "deprecated": false, - "trackAdoption": false, - "initialIsOpen": false - }, - { - "parentPluginId": "files", - "id": "def-public.UploadFileProps", - "type": "Type", - "tags": [], - "label": "UploadFileProps", - "description": [], - "signature": [ - { - "pluginId": "files", - "scope": "public", - "docId": "kibFilesPluginApi", - "section": "def-public.Props", - "text": "Props" - }, - " & { lazyLoadFallback?: React.ReactNode; }" + ">; publicDownload: (arg: Omit<{ token: string; fileName?: string | undefined; }, \"kind\">) => any; find: (arg: Omit<{ kind?: string | string[] | undefined; status?: string | string[] | undefined; extension?: string | string[] | undefined; name?: string | string[] | undefined; meta?: M | undefined; } & ", + "Pagination", + " & ", + "Abortable", + ", \"kind\">) => Promise<{ files: ", + "FileJSON", + "[]; total: number; }>; bulkDelete: (arg: Omit<{ ids: string[]; } & ", + "Abortable", + ", \"kind\">) => Promise<{ succeeded: string[]; failed?: [id: string, reason: string][] | undefined; }>; create: (arg: Omit<{ name: string; meta?: M | undefined; alt?: string | undefined; mimeType?: string | undefined; kind: string; } & ", + "Abortable", + ", \"kind\">) => Promise<{ file: ", + "FileJSON", + "; }>; delete: (arg: Omit<{ id: string; kind: string; } & ", + "Abortable", + ", \"kind\">) => Promise<{ ok: true; }>; getById: (arg: Omit<{ id: string; kind: string; } & ", + "Abortable", + ", \"kind\">) => Promise<{ file: ", + "FileJSON", + "; }>; list: (arg?: Omit<{ kind: string; status?: string | string[] | undefined; extension?: string | string[] | undefined; name?: string | string[] | undefined; meta?: M | undefined; } & ", + "Pagination", + " & ", + "Abortable", + ", \"kind\"> | undefined) => Promise<{ files: ", + "FileJSON", + "[]; total: number; }>; update: (arg: Omit<{ id: string; kind: string; name?: string | undefined; meta?: M | undefined; alt?: string | undefined; } & ", + "Abortable", + ", \"kind\">) => Promise<{ file: ", + "FileJSON", + "; }>; upload: (arg: Omit<{ id: string; body: unknown; kind: string; abortSignal?: AbortSignal | undefined; contentType?: string | undefined; selfDestructOnAbort?: boolean | undefined; } & ", + "Abortable", + ", \"kind\">) => Promise<{ ok: true; size: number; }>; download: (arg: Omit<{ fileName?: string | undefined; id: string; kind: string; } & ", + "Abortable", + ", \"kind\">) => Promise; getDownloadHref: (arg: Omit, \"id\" | \"fileKind\">, \"kind\">) => string; share: (arg: Omit<{ name?: string | undefined; validUntil?: number | undefined; fileId: string; kind: string; } & ", + "Abortable", + ", \"kind\">) => Promise; unshare: (arg: Omit<{ id: string; kind: string; } & ", + "Abortable", + ", \"kind\">) => Promise<{ ok: true; }>; getShare: (arg: Omit<{ id: string; kind: string; } & ", + "Abortable", + ", \"kind\">) => Promise<{ share: FileShareJSON; }>; listShares: (arg: Omit<{ forFileId?: string | undefined; kind: string; } & ", + "Pagination", + " & ", + "Abortable", + ", \"kind\">) => Promise<{ shares: FileShareJSON[]; }>; getFileKind: (arg: Omit) => ", + "FileKind", + "; }" ], - "path": "src/plugins/files/public/components/upload_file/index.tsx", + "path": "src/plugins/files/common/files_client.ts", "deprecated": false, "trackAdoption": false, "initialIsOpen": false @@ -1427,13 +591,7 @@ "\nA factory for creating an {@link FilesClient} instance. This requires a\nregistered {@link FileKind}.\n" ], "signature": [ - { - "pluginId": "files", - "scope": "public", - "docId": "kibFilesPluginApi", - "section": "def-public.FilesClientFactory", - "text": "FilesClientFactory" - } + "FilesClientFactory" ], "path": "src/plugins/files/public/plugin.ts", "deprecated": false, @@ -1456,13 +614,7 @@ ], "signature": [ "(fileKind: ", - { - "pluginId": "files", - "scope": "common", - "docId": "kibFilesPluginApi", - "section": "def-common.FileKind", - "text": "FileKind" - }, + "FileKind", ") => void" ], "path": "src/plugins/files/public/plugin.ts", @@ -1479,13 +631,7 @@ "- the file kind to register" ], "signature": [ - { - "pluginId": "files", - "scope": "common", - "docId": "kibFilesPluginApi", - "section": "def-common.FileKind", - "text": "FileKind" - } + "FileKind" ], "path": "src/plugins/files/public/plugin.ts", "deprecated": false, @@ -1508,13 +654,7 @@ "description": [], "signature": [ "{ filesClientFactory: ", - { - "pluginId": "files", - "scope": "public", - "docId": "kibFilesPluginApi", - "section": "def-public.FilesClientFactory", - "text": "FilesClientFactory" - }, + "FilesClientFactory", "; }" ], "path": "src/plugins/files/public/plugin.ts", @@ -3649,21 +2789,9 @@ ], "signature": [ "Required> & ", - { - "pluginId": "files", - "scope": "common", - "docId": "kibFilesPluginApi", - "section": "def-common.BaseFileMetadata", - "text": "BaseFileMetadata" - }, + "BaseFileMetadata", " & { FileKind: string; Meta?: M | undefined; }" ], "path": "src/plugins/files/server/file_client/file_metadata_client/file_metadata_client.ts", @@ -4382,13 +3510,7 @@ "text": "FindFileArgs" }, ") => Promise<{ files: ", - { - "pluginId": "files", - "scope": "common", - "docId": "kibFilesPluginApi", - "section": "def-common.FileJSON", - "text": "FileJSON" - }, + "FileJSON", "[]; total: number; }>" ], "path": "src/plugins/files/server/file_service/file_service.ts", @@ -5246,13 +4368,7 @@ ], "signature": [ "{ name?: string | undefined; created?: string | undefined; Status?: \"AWAITING_UPLOAD\" | \"UPLOADING\" | \"READY\" | \"UPLOAD_ERROR\" | \"DELETED\" | undefined; Updated?: string | undefined; mime_type?: string | undefined; size?: number | undefined; hash?: { [hashName: string]: string | undefined; md5?: string | undefined; sha1?: string | undefined; sha256?: string | undefined; sha384?: string | undefined; sha512?: string | undefined; ssdeep?: string | undefined; tlsh?: string | undefined; } | undefined; user?: { name?: string | undefined; id?: string | undefined; } | undefined; extension?: string | undefined; Alt?: string | undefined; ChunkSize?: number | undefined; Compression?: ", - { - "pluginId": "files", - "scope": "common", - "docId": "kibFilesPluginApi", - "section": "def-common.FileCompression", - "text": "FileCompression" - }, + "FileCompression", " | undefined; FileKind?: string | undefined; Meta?: M | undefined; }" ], "path": "src/plugins/files/server/file_client/file_metadata_client/file_metadata_client.ts", @@ -5417,13 +4533,7 @@ ], "signature": [ "(fileKind: ", - { - "pluginId": "files", - "scope": "common", - "docId": "kibFilesPluginApi", - "section": "def-common.FileKind", - "text": "FileKind" - }, + "FileKind", ") => void" ], "path": "src/plugins/files/server/types.ts", @@ -5441,13 +4551,7 @@ "- the file kind to register" ], "signature": [ - { - "pluginId": "files", - "scope": "common", - "docId": "kibFilesPluginApi", - "section": "def-common.FileKind", - "text": "FileKind" - } + "FileKind" ], "path": "src/plugins/files/server/types.ts", "deprecated": false, @@ -5586,13 +4690,7 @@ "\nFile metadata in camelCase form." ], "signature": [ - { - "pluginId": "files", - "scope": "common", - "docId": "kibFilesPluginApi", - "section": "def-common.FileJSON", - "text": "FileJSON" - }, + "FileJSON", "" ], "path": "src/plugins/files/common/types.ts", @@ -5913,13 +5011,7 @@ ], "signature": [ "() => ", - { - "pluginId": "files", - "scope": "common", - "docId": "kibFilesPluginApi", - "section": "def-common.FileJSON", - "text": "FileJSON" - }, + "FileJSON", "" ], "path": "src/plugins/files/common/types.ts", @@ -5941,16 +5033,10 @@ "\nAttributes of a file that represent a serialised version of the file." ], "signature": [ - { - "pluginId": "files", - "scope": "common", - "docId": "kibFilesPluginApi", - "section": "def-common.FileJSON", - "text": "FileJSON" - }, + "FileJSON", "" ], - "path": "src/plugins/files/common/types.ts", + "path": "packages/shared-ux/file/types/index.d.ts", "deprecated": false, "trackAdoption": false, "children": [ @@ -5963,7 +5049,7 @@ "description": [ "\nUnique file ID." ], - "path": "src/plugins/files/common/types.ts", + "path": "packages/shared-ux/file/types/index.d.ts", "deprecated": false, "trackAdoption": false }, @@ -5976,7 +5062,7 @@ "description": [ "\nISO string of when this file was created" ], - "path": "src/plugins/files/common/types.ts", + "path": "packages/shared-ux/file/types/index.d.ts", "deprecated": false, "trackAdoption": false }, @@ -5989,7 +5075,7 @@ "description": [ "\nISO string of when the file was updated" ], - "path": "src/plugins/files/common/types.ts", + "path": "packages/shared-ux/file/types/index.d.ts", "deprecated": false, "trackAdoption": false }, @@ -6004,7 +5090,7 @@ "description": [ "\nFile name.\n" ], - "path": "src/plugins/files/common/types.ts", + "path": "packages/shared-ux/file/types/index.d.ts", "deprecated": false, "trackAdoption": false }, @@ -6020,7 +5106,7 @@ "signature": [ "string | undefined" ], - "path": "src/plugins/files/common/types.ts", + "path": "packages/shared-ux/file/types/index.d.ts", "deprecated": false, "trackAdoption": false }, @@ -6036,7 +5122,7 @@ "signature": [ "number | undefined" ], - "path": "src/plugins/files/common/types.ts", + "path": "packages/shared-ux/file/types/index.d.ts", "deprecated": false, "trackAdoption": false }, @@ -6054,7 +5140,7 @@ "signature": [ "string | undefined" ], - "path": "src/plugins/files/common/types.ts", + "path": "packages/shared-ux/file/types/index.d.ts", "deprecated": false, "trackAdoption": false }, @@ -6070,7 +5156,7 @@ "signature": [ "Meta | undefined" ], - "path": "src/plugins/files/common/types.ts", + "path": "packages/shared-ux/file/types/index.d.ts", "deprecated": false, "trackAdoption": false }, @@ -6086,7 +5172,7 @@ "signature": [ "string | undefined" ], - "path": "src/plugins/files/common/types.ts", + "path": "packages/shared-ux/file/types/index.d.ts", "deprecated": false, "trackAdoption": false }, @@ -6101,7 +5187,7 @@ "description": [ "\nA unique kind that governs various aspects of the file. A consumer of the\nfiles service must register a file kind and link their files to a specific\nkind.\n" ], - "path": "src/plugins/files/common/types.ts", + "path": "packages/shared-ux/file/types/index.d.ts", "deprecated": false, "trackAdoption": false }, @@ -6117,7 +5203,7 @@ "signature": [ "\"AWAITING_UPLOAD\" | \"UPLOADING\" | \"READY\" | \"UPLOAD_ERROR\" | \"DELETED\"" ], - "path": "src/plugins/files/common/types.ts", + "path": "packages/shared-ux/file/types/index.d.ts", "deprecated": false, "trackAdoption": false }, @@ -6133,7 +5219,7 @@ "signature": [ "{ name?: string | undefined; id?: string | undefined; } | undefined" ], - "path": "src/plugins/files/common/types.ts", + "path": "packages/shared-ux/file/types/index.d.ts", "deprecated": false, "trackAdoption": false } @@ -6144,14 +5230,10 @@ "parentPluginId": "files", "id": "def-common.FileKind", "type": "Interface", - "tags": [ - "note" - ], + "tags": [], "label": "FileKind", - "description": [ - "\nA descriptor of meta values associated with a set or \"kind\" of files.\n" - ], - "path": "src/plugins/files/common/types.ts", + "description": [], + "path": "packages/shared-ux/file/types/index.d.ts", "deprecated": false, "trackAdoption": false, "children": [ @@ -6164,7 +5246,7 @@ "description": [ "\nUnique file kind ID" ], - "path": "src/plugins/files/common/types.ts", + "path": "packages/shared-ux/file/types/index.d.ts", "deprecated": false, "trackAdoption": false }, @@ -6182,7 +5264,7 @@ "signature": [ "number | undefined" ], - "path": "src/plugins/files/common/types.ts", + "path": "packages/shared-ux/file/types/index.d.ts", "deprecated": false, "trackAdoption": false }, @@ -6200,30 +5282,23 @@ "signature": [ "string[] | undefined" ], - "path": "src/plugins/files/common/types.ts", + "path": "packages/shared-ux/file/types/index.d.ts", "deprecated": false, "trackAdoption": false }, { "parentPluginId": "files", "id": "def-common.FileKind.blobStoreSettings", - "type": "Object", + "type": "Any", "tags": [], "label": "blobStoreSettings", "description": [ "\nBlob store specific settings that enable configuration of storage\ndetails." ], "signature": [ - { - "pluginId": "files", - "scope": "common", - "docId": "kibFilesPluginApi", - "section": "def-common.BlobStorageSettings", - "text": "BlobStorageSettings" - }, - " | undefined" + "any" ], - "path": "src/plugins/files/common/types.ts", + "path": "packages/shared-ux/file/types/index.d.ts", "deprecated": false, "trackAdoption": false }, @@ -6239,9 +5314,9 @@ "\nSpecify which HTTP routes to create for the file kind.\n\nYou can always create your own HTTP routes for working with files but\nthis interface allows you to expose basic CRUD operations, upload, download\nand sharing of files over a RESTful-like interface.\n" ], "signature": [ - "{ create?: HttpEndpointDefinition | undefined; update?: HttpEndpointDefinition | undefined; delete?: HttpEndpointDefinition | undefined; getById?: HttpEndpointDefinition | undefined; list?: HttpEndpointDefinition | undefined; download?: HttpEndpointDefinition | undefined; share?: HttpEndpointDefinition | undefined; }" + "{ create?: any; update?: any; delete?: any; getById?: any; list?: any; download?: any; share?: any; }" ], - "path": "src/plugins/files/common/types.ts", + "path": "packages/shared-ux/file/types/index.d.ts", "deprecated": false, "trackAdoption": false } @@ -6537,24 +5612,12 @@ ], "signature": [ "{ name?: string | undefined; mime_type?: string | undefined; created?: string | undefined; size?: number | undefined; hash?: { [hashName: string]: string | undefined; md5?: string | undefined; sha1?: string | undefined; sha256?: string | undefined; sha384?: string | undefined; sha512?: string | undefined; ssdeep?: string | undefined; tlsh?: string | undefined; } | undefined; user?: { name?: string | undefined; id?: string | undefined; } | undefined; extension?: string | undefined; Alt?: string | undefined; Updated?: string | undefined; Status?: ", - { - "pluginId": "files", - "scope": "common", - "docId": "kibFilesPluginApi", - "section": "def-common.FileStatus", - "text": "FileStatus" - }, + "FileStatus", " | undefined; ChunkSize?: number | undefined; Compression?: ", - { - "pluginId": "files", - "scope": "common", - "docId": "kibFilesPluginApi", - "section": "def-common.FileCompression", - "text": "FileCompression" - }, + "FileCompression", " | undefined; }" ], - "path": "src/plugins/files/common/types.ts", + "path": "packages/shared-ux/file/types/index.d.ts", "deprecated": false, "trackAdoption": false, "initialIsOpen": false @@ -6605,7 +5668,7 @@ "signature": [ "\"none\" | \"br\" | \"gzip\" | \"deflate\"" ], - "path": "src/plugins/files/common/types.ts", + "path": "packages/shared-ux/file/types/index.d.ts", "deprecated": false, "trackAdoption": false, "initialIsOpen": false @@ -6621,24 +5684,12 @@ ], "signature": [ "Required> & ", - { - "pluginId": "files", - "scope": "common", - "docId": "kibFilesPluginApi", - "section": "def-common.BaseFileMetadata", - "text": "BaseFileMetadata" - }, + "BaseFileMetadata", " & { FileKind: string; Meta?: Meta | undefined; }" ], - "path": "src/plugins/files/common/types.ts", + "path": "packages/shared-ux/file/types/index.d.ts", "deprecated": false, "trackAdoption": false, "initialIsOpen": false @@ -6661,13 +5712,7 @@ "text": "SavedObject" }, "<", - { - "pluginId": "files", - "scope": "common", - "docId": "kibFilesPluginApi", - "section": "def-common.FileMetadata", - "text": "FileMetadata" - }, + "FileMetadata", ">" ], "path": "src/plugins/files/common/types.ts", @@ -6724,13 +5769,11 @@ "type": "Type", "tags": [], "label": "FileStatus", - "description": [ - "\nStatus of a file.\n\nAWAITING_UPLOAD - A file object has been created but does not have any contents.\nUPLOADING - File contents are being uploaded.\nREADY - File contents have been uploaded and are ready for to be downloaded.\nUPLOAD_ERROR - An attempt was made to upload file contents but failed.\nDELETED - The file contents have been or are being deleted." - ], + "description": [], "signature": [ "\"AWAITING_UPLOAD\" | \"UPLOADING\" | \"READY\" | \"UPLOAD_ERROR\" | \"DELETED\"" ], - "path": "src/plugins/files/common/types.ts", + "path": "packages/shared-ux/file/types/index.d.ts", "deprecated": false, "trackAdoption": false, "initialIsOpen": false diff --git a/api_docs/files.mdx b/api_docs/files.mdx index 02efc1b7d18e0..4887e67e7540b 100644 --- a/api_docs/files.mdx +++ b/api_docs/files.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/files title: "files" image: https://source.unsplash.com/400x175/?github description: API docs for the files plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'files'] --- import filesObj from './files.devdocs.json'; @@ -21,7 +21,7 @@ Contact [@elastic/kibana-app-services](https://github.com/orgs/elastic/teams/tea | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 287 | 0 | 50 | 3 | +| 252 | 1 | 45 | 5 | ## Client @@ -34,9 +34,6 @@ Contact [@elastic/kibana-app-services](https://github.com/orgs/elastic/teams/tea ### Objects -### Functions - - ### Interfaces diff --git a/api_docs/files_management.mdx b/api_docs/files_management.mdx index 652f05c27ad93..8eec341226c8a 100644 --- a/api_docs/files_management.mdx +++ b/api_docs/files_management.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/filesManagement title: "filesManagement" image: https://source.unsplash.com/400x175/?github description: API docs for the filesManagement plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'filesManagement'] --- import filesManagementObj from './files_management.devdocs.json'; diff --git a/api_docs/fleet.devdocs.json b/api_docs/fleet.devdocs.json index 2a3246e0b2b07..ce10a6d3a887a 100644 --- a/api_docs/fleet.devdocs.json +++ b/api_docs/fleet.devdocs.json @@ -9058,7 +9058,7 @@ "label": "status", "description": [], "signature": [ - "\"active\" | \"inactive\"" + "\"inactive\" | \"active\"" ], "path": "x-pack/plugins/fleet/common/types/models/agent_policy.ts", "deprecated": false, @@ -10647,7 +10647,7 @@ "label": "agent", "description": [], "signature": [ - "{ monitoring: { namespace?: string | undefined; use_output?: string | undefined; enabled: boolean; metrics: boolean; logs: boolean; }; download: { source_uri: string; }; } | undefined" + "{ monitoring: { namespace?: string | undefined; use_output?: string | undefined; enabled: boolean; metrics: boolean; logs: boolean; }; download: { sourceURI: string; }; } | undefined" ], "path": "x-pack/plugins/fleet/common/types/models/agent_policy.ts", "deprecated": false, @@ -15149,6 +15149,21 @@ "trackAdoption": false, "initialIsOpen": false }, + { + "parentPluginId": "fleet", + "id": "def-common.FLEET_CLOUD_SECURITY_POSTURE_KSPM_POLICY_TEMPLATE", + "type": "string", + "tags": [], + "label": "FLEET_CLOUD_SECURITY_POSTURE_KSPM_POLICY_TEMPLATE", + "description": [], + "signature": [ + "\"kspm\"" + ], + "path": "x-pack/plugins/fleet/common/constants/epm.ts", + "deprecated": false, + "trackAdoption": false, + "initialIsOpen": false + }, { "parentPluginId": "fleet", "id": "def-common.FLEET_CLOUD_SECURITY_POSTURE_PACKAGE", @@ -17629,6 +17644,22 @@ "trackAdoption": false, "children": [], "returnComment": [] + }, + { + "parentPluginId": "fleet", + "id": "def-common.appRoutesService.postHealthCheckPath", + "type": "Function", + "tags": [], + "label": "postHealthCheckPath", + "description": [], + "signature": [ + "() => string" + ], + "path": "x-pack/plugins/fleet/common/services/routes.ts", + "deprecated": false, + "trackAdoption": false, + "children": [], + "returnComment": [] } ], "initialIsOpen": false diff --git a/api_docs/fleet.mdx b/api_docs/fleet.mdx index f22cd5ee92303..dbe1f861338e6 100644 --- a/api_docs/fleet.mdx +++ b/api_docs/fleet.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/fleet title: "fleet" image: https://source.unsplash.com/400x175/?github description: API docs for the fleet plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'fleet'] --- import fleetObj from './fleet.devdocs.json'; @@ -21,7 +21,7 @@ Contact [Fleet](https://github.com/orgs/elastic/teams/fleet) for questions regar | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 1025 | 3 | 920 | 19 | +| 1027 | 3 | 922 | 19 | ## Client diff --git a/api_docs/global_search.mdx b/api_docs/global_search.mdx index eaf3411f1ee30..dd559086a535d 100644 --- a/api_docs/global_search.mdx +++ b/api_docs/global_search.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/globalSearch title: "globalSearch" image: https://source.unsplash.com/400x175/?github description: API docs for the globalSearch plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'globalSearch'] --- import globalSearchObj from './global_search.devdocs.json'; diff --git a/api_docs/guided_onboarding.devdocs.json b/api_docs/guided_onboarding.devdocs.json index 922e9e6561fee..526cf2cbe6f8e 100644 --- a/api_docs/guided_onboarding.devdocs.json +++ b/api_docs/guided_onboarding.devdocs.json @@ -87,7 +87,13 @@ "() => ", "Observable", "<", - "PluginState", + { + "pluginId": "guidedOnboarding", + "scope": "common", + "docId": "kibGuidedOnboardingPluginApi", + "section": "def-common.PluginState", + "text": "PluginState" + }, " | undefined>" ], "path": "src/plugins/guided_onboarding/public/types.ts", @@ -129,7 +135,13 @@ "description": [], "signature": [ "(state: { status?: ", - "PluginStatus", + { + "pluginId": "guidedOnboarding", + "scope": "common", + "docId": "kibGuidedOnboardingPluginApi", + "section": "def-common.PluginStatus", + "text": "PluginStatus" + }, " | undefined; guide?: ", { "pluginId": "@kbn/guided-onboarding", @@ -139,7 +151,13 @@ "text": "GuideState" }, " | undefined; }, panelState: boolean) => Promise<{ pluginState: ", - "PluginState", + { + "pluginId": "guidedOnboarding", + "scope": "common", + "docId": "kibGuidedOnboardingPluginApi", + "section": "def-common.PluginState", + "text": "PluginState" + }, "; } | undefined>" ], "path": "src/plugins/guided_onboarding/public/types.ts", @@ -165,7 +183,13 @@ "label": "status", "description": [], "signature": [ - "PluginStatus", + { + "pluginId": "guidedOnboarding", + "scope": "common", + "docId": "kibGuidedOnboardingPluginApi", + "section": "def-common.PluginStatus", + "text": "PluginStatus" + }, " | undefined" ], "path": "src/plugins/guided_onboarding/public/types.ts", @@ -238,7 +262,13 @@ "text": "GuideState" }, " | undefined) => Promise<{ pluginState: ", - "PluginState", + { + "pluginId": "guidedOnboarding", + "scope": "common", + "docId": "kibGuidedOnboardingPluginApi", + "section": "def-common.PluginState", + "text": "PluginState" + }, "; } | undefined>" ], "path": "src/plugins/guided_onboarding/public/types.ts", @@ -308,7 +338,13 @@ "text": "GuideState" }, ") => Promise<{ pluginState: ", - "PluginState", + { + "pluginId": "guidedOnboarding", + "scope": "common", + "docId": "kibGuidedOnboardingPluginApi", + "section": "def-common.PluginState", + "text": "PluginState" + }, "; } | undefined>" ], "path": "src/plugins/guided_onboarding/public/types.ts", @@ -356,7 +392,13 @@ "text": "GuideId" }, ") => Promise<{ pluginState: ", - "PluginState", + { + "pluginId": "guidedOnboarding", + "scope": "common", + "docId": "kibGuidedOnboardingPluginApi", + "section": "def-common.PluginState", + "text": "PluginState" + }, "; } | undefined>" ], "path": "src/plugins/guided_onboarding/public/types.ts", @@ -489,7 +531,13 @@ "text": "GuideStepIds" }, ") => Promise<{ pluginState: ", - "PluginState", + { + "pluginId": "guidedOnboarding", + "scope": "common", + "docId": "kibGuidedOnboardingPluginApi", + "section": "def-common.PluginState", + "text": "PluginState" + }, "; } | undefined>" ], "path": "src/plugins/guided_onboarding/public/types.ts", @@ -566,7 +614,13 @@ "text": "GuideStepIds" }, ") => Promise<{ pluginState: ", - "PluginState", + { + "pluginId": "guidedOnboarding", + "scope": "common", + "docId": "kibGuidedOnboardingPluginApi", + "section": "def-common.PluginState", + "text": "PluginState" + }, "; } | undefined>" ], "path": "src/plugins/guided_onboarding/public/types.ts", @@ -661,7 +715,13 @@ "description": [], "signature": [ "(integration?: string | undefined) => Promise<{ pluginState: ", - "PluginState", + { + "pluginId": "guidedOnboarding", + "scope": "common", + "docId": "kibGuidedOnboardingPluginApi", + "section": "def-common.PluginState", + "text": "PluginState" + }, "; } | undefined>" ], "path": "src/plugins/guided_onboarding/public/types.ts", @@ -695,7 +755,13 @@ "description": [], "signature": [ "() => Promise<{ pluginState: ", - "PluginState", + { + "pluginId": "guidedOnboarding", + "scope": "common", + "docId": "kibGuidedOnboardingPluginApi", + "section": "def-common.PluginState", + "text": "PluginState" + }, "; } | undefined>" ], "path": "src/plugins/guided_onboarding/public/types.ts", @@ -718,85 +784,68 @@ "path": "src/plugins/guided_onboarding/public/types.ts", "deprecated": false, "trackAdoption": false - } - ], - "initialIsOpen": false - } - ], - "enums": [], - "misc": [], - "objects": [ - { - "parentPluginId": "guidedOnboarding", - "id": "def-public.guidesConfig", - "type": "Object", - "tags": [], - "label": "guidesConfig", - "description": [], - "path": "src/plugins/guided_onboarding/public/constants/guides_config/index.ts", - "deprecated": false, - "trackAdoption": false, - "children": [ - { - "parentPluginId": "guidedOnboarding", - "id": "def-public.guidesConfig.security", - "type": "Object", - "tags": [], - "label": "security", - "description": [], - "signature": [ - "GuideConfig" - ], - "path": "src/plugins/guided_onboarding/public/constants/guides_config/index.ts", - "deprecated": false, - "trackAdoption": false }, { "parentPluginId": "guidedOnboarding", - "id": "def-public.guidesConfig.observability", - "type": "Object", - "tags": [], - "label": "observability", - "description": [], - "signature": [ - "GuideConfig" - ], - "path": "src/plugins/guided_onboarding/public/constants/guides_config/index.ts", - "deprecated": false, - "trackAdoption": false - }, - { - "parentPluginId": "guidedOnboarding", - "id": "def-public.guidesConfig.search", - "type": "Object", + "id": "def-public.GuidedOnboardingApi.getGuideConfig", + "type": "Function", "tags": [], - "label": "search", + "label": "getGuideConfig", "description": [], "signature": [ - "GuideConfig" + "(guideId: ", + { + "pluginId": "@kbn/guided-onboarding", + "scope": "common", + "docId": "kibKbnGuidedOnboardingPluginApi", + "section": "def-common.GuideId", + "text": "GuideId" + }, + ") => Promise<", + { + "pluginId": "guidedOnboarding", + "scope": "common", + "docId": "kibGuidedOnboardingPluginApi", + "section": "def-common.GuideConfig", + "text": "GuideConfig" + }, + " | undefined>" ], - "path": "src/plugins/guided_onboarding/public/constants/guides_config/index.ts", + "path": "src/plugins/guided_onboarding/public/types.ts", "deprecated": false, - "trackAdoption": false - }, - { - "parentPluginId": "guidedOnboarding", - "id": "def-public.guidesConfig.testGuide", - "type": "Object", - "tags": [], - "label": "testGuide", - "description": [], - "signature": [ - "GuideConfig" + "trackAdoption": false, + "children": [ + { + "parentPluginId": "guidedOnboarding", + "id": "def-public.GuidedOnboardingApi.getGuideConfig.$1", + "type": "CompoundType", + "tags": [], + "label": "guideId", + "description": [], + "signature": [ + { + "pluginId": "@kbn/guided-onboarding", + "scope": "common", + "docId": "kibKbnGuidedOnboardingPluginApi", + "section": "def-common.GuideId", + "text": "GuideId" + } + ], + "path": "src/plugins/guided_onboarding/public/types.ts", + "deprecated": false, + "trackAdoption": false, + "isRequired": true + } ], - "path": "src/plugins/guided_onboarding/public/constants/guides_config/index.ts", - "deprecated": false, - "trackAdoption": false + "returnComment": [] } ], "initialIsOpen": false } ], + "enums": [], + "misc": [], + "objects": [], "setup": { "parentPluginId": "guidedOnboarding", "id": "def-public.GuidedOnboardingPluginSetup", @@ -887,9 +936,538 @@ "common": { "classes": [], "functions": [], - "interfaces": [], + "interfaces": [ + { + "parentPluginId": "guidedOnboarding", + "id": "def-common.GuideConfig", + "type": "Interface", + "tags": [], + "label": "GuideConfig", + "description": [], + "path": "src/plugins/guided_onboarding/common/types.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "guidedOnboarding", + "id": "def-common.GuideConfig.title", + "type": "string", + "tags": [], + "label": "title", + "description": [], + "path": "src/plugins/guided_onboarding/common/types.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "guidedOnboarding", + "id": "def-common.GuideConfig.description", + "type": "string", + "tags": [], + "label": "description", + "description": [], + "path": "src/plugins/guided_onboarding/common/types.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "guidedOnboarding", + "id": "def-common.GuideConfig.guideName", + "type": "string", + "tags": [], + "label": "guideName", + "description": [], + "path": "src/plugins/guided_onboarding/common/types.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "guidedOnboarding", + "id": "def-common.GuideConfig.docs", + "type": "Object", + "tags": [], + "label": "docs", + "description": [], + "signature": [ + "{ text: string; url: string; } | undefined" + ], + "path": "src/plugins/guided_onboarding/common/types.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "guidedOnboarding", + "id": "def-common.GuideConfig.completedGuideRedirectLocation", + "type": "Object", + "tags": [], + "label": "completedGuideRedirectLocation", + "description": [], + "signature": [ + "{ appID: string; path: string; } | undefined" + ], + "path": "src/plugins/guided_onboarding/common/types.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "guidedOnboarding", + "id": "def-common.GuideConfig.steps", + "type": "Array", + "tags": [], + "label": "steps", + "description": [], + "signature": [ + { + "pluginId": "guidedOnboarding", + "scope": "common", + "docId": "kibGuidedOnboardingPluginApi", + "section": "def-common.StepConfig", + "text": "StepConfig" + }, + "[]" + ], + "path": "src/plugins/guided_onboarding/common/types.ts", + "deprecated": false, + "trackAdoption": false + } + ], + "initialIsOpen": false + }, + { + "parentPluginId": "guidedOnboarding", + "id": "def-common.PluginState", + "type": "Interface", + "tags": [], + "label": "PluginState", + "description": [], + "path": "src/plugins/guided_onboarding/common/types.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "guidedOnboarding", + "id": "def-common.PluginState.status", + "type": "CompoundType", + "tags": [], + "label": "status", + "description": [], + "signature": [ + "\"error\" | \"complete\" | \"not_started\" | \"in_progress\" | \"quit\" | \"skipped\"" + ], + "path": "src/plugins/guided_onboarding/common/types.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "guidedOnboarding", + "id": "def-common.PluginState.isActivePeriod", + "type": "boolean", + "tags": [], + "label": "isActivePeriod", + "description": [], + "path": "src/plugins/guided_onboarding/common/types.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "guidedOnboarding", + "id": "def-common.PluginState.activeGuide", + "type": "Object", + "tags": [], + "label": "activeGuide", + "description": [], + "signature": [ + { + "pluginId": "@kbn/guided-onboarding", + "scope": "common", + "docId": "kibKbnGuidedOnboardingPluginApi", + "section": "def-common.GuideState", + "text": "GuideState" + }, + " | undefined" + ], + "path": "src/plugins/guided_onboarding/common/types.ts", + "deprecated": false, + "trackAdoption": false + } + ], + "initialIsOpen": false + }, + { + "parentPluginId": "guidedOnboarding", + "id": "def-common.StepConfig", + "type": "Interface", + "tags": [], + "label": "StepConfig", + "description": [], + "path": "src/plugins/guided_onboarding/common/types.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "guidedOnboarding", + "id": "def-common.StepConfig.id", + "type": "CompoundType", + "tags": [], + "label": "id", + "description": [], + "signature": [ + "\"add_data\" | \"view_dashboard\" | \"tour_observability\" | \"rules\" | \"alertsCases\" | \"search_experience\" | \"step1\" | \"step2\" | \"step3\"" + ], + "path": "src/plugins/guided_onboarding/common/types.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "guidedOnboarding", + "id": "def-common.StepConfig.title", + "type": "string", + "tags": [], + "label": "title", + "description": [], + "path": "src/plugins/guided_onboarding/common/types.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "guidedOnboarding", + "id": "def-common.StepConfig.description", + "type": "string", + "tags": [], + "label": "description", + "description": [], + "signature": [ + "string | undefined" + ], + "path": "src/plugins/guided_onboarding/common/types.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "guidedOnboarding", + "id": "def-common.StepConfig.descriptionList", + "type": "Array", + "tags": [], + "label": "descriptionList", + "description": [], + "signature": [ + "React.ReactNode[] | undefined" + ], + "path": "src/plugins/guided_onboarding/common/types.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "guidedOnboarding", + "id": "def-common.StepConfig.location", + "type": "Object", + "tags": [], + "label": "location", + "description": [], + "signature": [ + "{ appID: string; path: string; } | undefined" + ], + "path": "src/plugins/guided_onboarding/common/types.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "guidedOnboarding", + "id": "def-common.StepConfig.status", + "type": "CompoundType", + "tags": [], + "label": "status", + "description": [], + "signature": [ + { + "pluginId": "@kbn/guided-onboarding", + "scope": "common", + "docId": "kibKbnGuidedOnboardingPluginApi", + "section": "def-common.StepStatus", + "text": "StepStatus" + }, + " | undefined" + ], + "path": "src/plugins/guided_onboarding/common/types.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "guidedOnboarding", + "id": "def-common.StepConfig.integration", + "type": "string", + "tags": [], + "label": "integration", + "description": [], + "signature": [ + "string | undefined" + ], + "path": "src/plugins/guided_onboarding/common/types.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "guidedOnboarding", + "id": "def-common.StepConfig.manualCompletion", + "type": "Object", + "tags": [], + "label": "manualCompletion", + "description": [], + "signature": [ + "{ title: string; description: string; readyToCompleteOnNavigation?: boolean | undefined; } | undefined" + ], + "path": "src/plugins/guided_onboarding/common/types.ts", + "deprecated": false, + "trackAdoption": false + } + ], + "initialIsOpen": false + } + ], "enums": [], - "misc": [], - "objects": [] + "misc": [ + { + "parentPluginId": "guidedOnboarding", + "id": "def-common.API_BASE_PATH", + "type": "string", + "tags": [], + "label": "API_BASE_PATH", + "description": [], + "signature": [ + "\"/api/guided_onboarding\"" + ], + "path": "src/plugins/guided_onboarding/common/constants.ts", + "deprecated": false, + "trackAdoption": false, + "initialIsOpen": false + }, + { + "parentPluginId": "guidedOnboarding", + "id": "def-common.GuidesConfig", + "type": "Type", + "tags": [], + "label": "GuidesConfig", + "description": [], + "signature": [ + "{ search: ", + { + "pluginId": "guidedOnboarding", + "scope": "common", + "docId": "kibGuidedOnboardingPluginApi", + "section": "def-common.GuideConfig", + "text": "GuideConfig" + }, + "; security: ", + { + "pluginId": "guidedOnboarding", + "scope": "common", + "docId": "kibGuidedOnboardingPluginApi", + "section": "def-common.GuideConfig", + "text": "GuideConfig" + }, + "; observability: ", + { + "pluginId": "guidedOnboarding", + "scope": "common", + "docId": "kibGuidedOnboardingPluginApi", + "section": "def-common.GuideConfig", + "text": "GuideConfig" + }, + "; testGuide: ", + { + "pluginId": "guidedOnboarding", + "scope": "common", + "docId": "kibGuidedOnboardingPluginApi", + "section": "def-common.GuideConfig", + "text": "GuideConfig" + }, + "; }" + ], + "path": "src/plugins/guided_onboarding/common/types.ts", + "deprecated": false, + "trackAdoption": false, + "initialIsOpen": false + }, + { + "parentPluginId": "guidedOnboarding", + "id": "def-common.PLUGIN_ID", + "type": "string", + "tags": [], + "label": "PLUGIN_ID", + "description": [], + "signature": [ + "\"guidedOnboarding\"" + ], + "path": "src/plugins/guided_onboarding/common/constants.ts", + "deprecated": false, + "trackAdoption": false, + "initialIsOpen": false + }, + { + "parentPluginId": "guidedOnboarding", + "id": "def-common.PLUGIN_NAME", + "type": "string", + "tags": [], + "label": "PLUGIN_NAME", + "description": [], + "signature": [ + "\"guidedOnboarding\"" + ], + "path": "src/plugins/guided_onboarding/common/constants.ts", + "deprecated": false, + "trackAdoption": false, + "initialIsOpen": false + }, + { + "parentPluginId": "guidedOnboarding", + "id": "def-common.PluginStatus", + "type": "Type", + "tags": [], + "label": "PluginStatus", + "description": [ + "\nGuided onboarding overall status:\n not_started: no guides have been started yet\n in_progress: a guide is currently active\n complete: at least one guide has been completed\n quit: the user quit a guide before completion\n skipped: the user skipped on the landing page\n error: unable to retrieve the plugin state from saved objects" + ], + "signature": [ + "\"error\" | \"complete\" | \"not_started\" | \"in_progress\" | \"quit\" | \"skipped\"" + ], + "path": "src/plugins/guided_onboarding/common/types.ts", + "deprecated": false, + "trackAdoption": false, + "initialIsOpen": false + } + ], + "objects": [ + { + "parentPluginId": "guidedOnboarding", + "id": "def-common.testGuideConfig", + "type": "Object", + "tags": [], + "label": "testGuideConfig", + "description": [], + "path": "src/plugins/guided_onboarding/common/test_guide_config.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "guidedOnboarding", + "id": "def-common.testGuideConfig.title", + "type": "string", + "tags": [], + "label": "title", + "description": [], + "path": "src/plugins/guided_onboarding/common/test_guide_config.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "guidedOnboarding", + "id": "def-common.testGuideConfig.description", + "type": "string", + "tags": [], + "label": "description", + "description": [], + "path": "src/plugins/guided_onboarding/common/test_guide_config.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "guidedOnboarding", + "id": "def-common.testGuideConfig.guideName", + "type": "string", + "tags": [], + "label": "guideName", + "description": [], + "path": "src/plugins/guided_onboarding/common/test_guide_config.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "guidedOnboarding", + "id": "def-common.testGuideConfig.completedGuideRedirectLocation", + "type": "Object", + "tags": [], + "label": "completedGuideRedirectLocation", + "description": [], + "path": "src/plugins/guided_onboarding/common/test_guide_config.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "guidedOnboarding", + "id": "def-common.testGuideConfig.completedGuideRedirectLocation.appID", + "type": "string", + "tags": [], + "label": "appID", + "description": [], + "path": "src/plugins/guided_onboarding/common/test_guide_config.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "guidedOnboarding", + "id": "def-common.testGuideConfig.completedGuideRedirectLocation.path", + "type": "string", + "tags": [], + "label": "path", + "description": [], + "path": "src/plugins/guided_onboarding/common/test_guide_config.ts", + "deprecated": false, + "trackAdoption": false + } + ] + }, + { + "parentPluginId": "guidedOnboarding", + "id": "def-common.testGuideConfig.docs", + "type": "Object", + "tags": [], + "label": "docs", + "description": [], + "path": "src/plugins/guided_onboarding/common/test_guide_config.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "guidedOnboarding", + "id": "def-common.testGuideConfig.docs.text", + "type": "string", + "tags": [], + "label": "text", + "description": [], + "path": "src/plugins/guided_onboarding/common/test_guide_config.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "guidedOnboarding", + "id": "def-common.testGuideConfig.docs.url", + "type": "string", + "tags": [], + "label": "url", + "description": [], + "path": "src/plugins/guided_onboarding/common/test_guide_config.ts", + "deprecated": false, + "trackAdoption": false + } + ] + }, + { + "parentPluginId": "guidedOnboarding", + "id": "def-common.testGuideConfig.steps", + "type": "Array", + "tags": [], + "label": "steps", + "description": [], + "signature": [ + "({ id: \"step1\"; title: string; descriptionList: string[]; location: { appID: string; path: string; }; integration: string; } | { id: \"step2\"; title: string; descriptionList: string[]; location: { appID: string; path: string; }; manualCompletion: { title: string; description: string; readyToCompleteOnNavigation: true; }; } | { id: \"step3\"; title: string; description: string; manualCompletion: { title: string; description: string; }; location: { appID: string; path: string; }; })[]" + ], + "path": "src/plugins/guided_onboarding/common/test_guide_config.ts", + "deprecated": false, + "trackAdoption": false + } + ], + "initialIsOpen": false + } + ] } } \ No newline at end of file diff --git a/api_docs/guided_onboarding.mdx b/api_docs/guided_onboarding.mdx index ae4008a46de42..8a9752301a8fc 100644 --- a/api_docs/guided_onboarding.mdx +++ b/api_docs/guided_onboarding.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/guidedOnboarding title: "guidedOnboarding" image: https://source.unsplash.com/400x175/?github description: API docs for the guidedOnboarding plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'guidedOnboarding'] --- import guidedOnboardingObj from './guided_onboarding.devdocs.json'; @@ -21,7 +21,7 @@ Contact [Journey Onboarding](https://github.com/orgs/elastic/teams/platform-onbo | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 43 | 0 | 43 | 3 | +| 76 | 0 | 75 | 0 | ## Client @@ -31,9 +31,6 @@ Contact [Journey Onboarding](https://github.com/orgs/elastic/teams/platform-onbo ### Start -### Objects - - ### Interfaces @@ -45,3 +42,14 @@ Contact [Journey Onboarding](https://github.com/orgs/elastic/teams/platform-onbo ### Start +## Common + +### Objects + + +### Interfaces + + +### Consts, variables and types + + diff --git a/api_docs/home.mdx b/api_docs/home.mdx index ced84d299c583..ca9719338caa4 100644 --- a/api_docs/home.mdx +++ b/api_docs/home.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/home title: "home" image: https://source.unsplash.com/400x175/?github description: API docs for the home plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'home'] --- import homeObj from './home.devdocs.json'; diff --git a/api_docs/index_lifecycle_management.mdx b/api_docs/index_lifecycle_management.mdx index c4697a8e1793c..8a895f5280516 100644 --- a/api_docs/index_lifecycle_management.mdx +++ b/api_docs/index_lifecycle_management.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/indexLifecycleManagement title: "indexLifecycleManagement" image: https://source.unsplash.com/400x175/?github description: API docs for the indexLifecycleManagement plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'indexLifecycleManagement'] --- import indexLifecycleManagementObj from './index_lifecycle_management.devdocs.json'; diff --git a/api_docs/index_management.mdx b/api_docs/index_management.mdx index ae7938a217777..4221e526363e6 100644 --- a/api_docs/index_management.mdx +++ b/api_docs/index_management.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/indexManagement title: "indexManagement" image: https://source.unsplash.com/400x175/?github description: API docs for the indexManagement plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'indexManagement'] --- import indexManagementObj from './index_management.devdocs.json'; diff --git a/api_docs/infra.mdx b/api_docs/infra.mdx index f812fcdb71f3b..0ec5591904b00 100644 --- a/api_docs/infra.mdx +++ b/api_docs/infra.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/infra title: "infra" image: https://source.unsplash.com/400x175/?github description: API docs for the infra plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'infra'] --- import infraObj from './infra.devdocs.json'; diff --git a/api_docs/inspector.mdx b/api_docs/inspector.mdx index d52346a673b90..41b58ab447a78 100644 --- a/api_docs/inspector.mdx +++ b/api_docs/inspector.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/inspector title: "inspector" image: https://source.unsplash.com/400x175/?github description: API docs for the inspector plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'inspector'] --- import inspectorObj from './inspector.devdocs.json'; diff --git a/api_docs/interactive_setup.mdx b/api_docs/interactive_setup.mdx index 9b36886fb6dd4..33ae0cc2eecd5 100644 --- a/api_docs/interactive_setup.mdx +++ b/api_docs/interactive_setup.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/interactiveSetup title: "interactiveSetup" image: https://source.unsplash.com/400x175/?github description: API docs for the interactiveSetup plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'interactiveSetup'] --- import interactiveSetupObj from './interactive_setup.devdocs.json'; diff --git a/api_docs/kbn_ace.mdx b/api_docs/kbn_ace.mdx index 1fe33674c07d9..edf1b998f8243 100644 --- a/api_docs/kbn_ace.mdx +++ b/api_docs/kbn_ace.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ace title: "@kbn/ace" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ace plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ace'] --- import kbnAceObj from './kbn_ace.devdocs.json'; diff --git a/api_docs/kbn_aiops_components.mdx b/api_docs/kbn_aiops_components.mdx index f54e38463f07c..6fda38b56d21c 100644 --- a/api_docs/kbn_aiops_components.mdx +++ b/api_docs/kbn_aiops_components.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-aiops-components title: "@kbn/aiops-components" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/aiops-components plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/aiops-components'] --- import kbnAiopsComponentsObj from './kbn_aiops_components.devdocs.json'; diff --git a/api_docs/kbn_aiops_utils.mdx b/api_docs/kbn_aiops_utils.mdx index d66dad80cfe89..8311239b37a5f 100644 --- a/api_docs/kbn_aiops_utils.mdx +++ b/api_docs/kbn_aiops_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-aiops-utils title: "@kbn/aiops-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/aiops-utils plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/aiops-utils'] --- import kbnAiopsUtilsObj from './kbn_aiops_utils.devdocs.json'; diff --git a/api_docs/kbn_alerts.mdx b/api_docs/kbn_alerts.mdx index 56c33642f9c34..bfbb951a66639 100644 --- a/api_docs/kbn_alerts.mdx +++ b/api_docs/kbn_alerts.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-alerts title: "@kbn/alerts" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/alerts plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/alerts'] --- import kbnAlertsObj from './kbn_alerts.devdocs.json'; diff --git a/api_docs/kbn_analytics.mdx b/api_docs/kbn_analytics.mdx index 1513ed1be103e..8553ddf4bfce8 100644 --- a/api_docs/kbn_analytics.mdx +++ b/api_docs/kbn_analytics.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-analytics title: "@kbn/analytics" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/analytics plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/analytics'] --- import kbnAnalyticsObj from './kbn_analytics.devdocs.json'; diff --git a/api_docs/kbn_analytics_client.mdx b/api_docs/kbn_analytics_client.mdx index 9fd4e50be3704..0663c1d388be9 100644 --- a/api_docs/kbn_analytics_client.mdx +++ b/api_docs/kbn_analytics_client.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-analytics-client title: "@kbn/analytics-client" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/analytics-client plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/analytics-client'] --- import kbnAnalyticsClientObj from './kbn_analytics_client.devdocs.json'; diff --git a/api_docs/kbn_analytics_shippers_elastic_v3_browser.mdx b/api_docs/kbn_analytics_shippers_elastic_v3_browser.mdx index 8603668b02783..1b41ac6e1a750 100644 --- a/api_docs/kbn_analytics_shippers_elastic_v3_browser.mdx +++ b/api_docs/kbn_analytics_shippers_elastic_v3_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-analytics-shippers-elastic-v3-browser title: "@kbn/analytics-shippers-elastic-v3-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/analytics-shippers-elastic-v3-browser plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/analytics-shippers-elastic-v3-browser'] --- import kbnAnalyticsShippersElasticV3BrowserObj from './kbn_analytics_shippers_elastic_v3_browser.devdocs.json'; diff --git a/api_docs/kbn_analytics_shippers_elastic_v3_common.mdx b/api_docs/kbn_analytics_shippers_elastic_v3_common.mdx index 33eb911ab48b6..198d1f5ec90b1 100644 --- a/api_docs/kbn_analytics_shippers_elastic_v3_common.mdx +++ b/api_docs/kbn_analytics_shippers_elastic_v3_common.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-analytics-shippers-elastic-v3-common title: "@kbn/analytics-shippers-elastic-v3-common" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/analytics-shippers-elastic-v3-common plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/analytics-shippers-elastic-v3-common'] --- import kbnAnalyticsShippersElasticV3CommonObj from './kbn_analytics_shippers_elastic_v3_common.devdocs.json'; diff --git a/api_docs/kbn_analytics_shippers_elastic_v3_server.mdx b/api_docs/kbn_analytics_shippers_elastic_v3_server.mdx index dfef95cff8602..5c80309b4d982 100644 --- a/api_docs/kbn_analytics_shippers_elastic_v3_server.mdx +++ b/api_docs/kbn_analytics_shippers_elastic_v3_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-analytics-shippers-elastic-v3-server title: "@kbn/analytics-shippers-elastic-v3-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/analytics-shippers-elastic-v3-server plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/analytics-shippers-elastic-v3-server'] --- import kbnAnalyticsShippersElasticV3ServerObj from './kbn_analytics_shippers_elastic_v3_server.devdocs.json'; diff --git a/api_docs/kbn_analytics_shippers_fullstory.mdx b/api_docs/kbn_analytics_shippers_fullstory.mdx index feb1f33e65c1a..851028dee57a3 100644 --- a/api_docs/kbn_analytics_shippers_fullstory.mdx +++ b/api_docs/kbn_analytics_shippers_fullstory.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-analytics-shippers-fullstory title: "@kbn/analytics-shippers-fullstory" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/analytics-shippers-fullstory plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/analytics-shippers-fullstory'] --- import kbnAnalyticsShippersFullstoryObj from './kbn_analytics_shippers_fullstory.devdocs.json'; diff --git a/api_docs/kbn_analytics_shippers_gainsight.mdx b/api_docs/kbn_analytics_shippers_gainsight.mdx index 499e1625c3af6..bb96f23eb58b0 100644 --- a/api_docs/kbn_analytics_shippers_gainsight.mdx +++ b/api_docs/kbn_analytics_shippers_gainsight.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-analytics-shippers-gainsight title: "@kbn/analytics-shippers-gainsight" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/analytics-shippers-gainsight plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/analytics-shippers-gainsight'] --- import kbnAnalyticsShippersGainsightObj from './kbn_analytics_shippers_gainsight.devdocs.json'; diff --git a/api_docs/kbn_apm_config_loader.mdx b/api_docs/kbn_apm_config_loader.mdx index 6a75cb5459150..8fad38e55100f 100644 --- a/api_docs/kbn_apm_config_loader.mdx +++ b/api_docs/kbn_apm_config_loader.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-apm-config-loader title: "@kbn/apm-config-loader" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/apm-config-loader plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/apm-config-loader'] --- import kbnApmConfigLoaderObj from './kbn_apm_config_loader.devdocs.json'; diff --git a/api_docs/kbn_apm_synthtrace.mdx b/api_docs/kbn_apm_synthtrace.mdx index 0165cb835caaa..fdaf7e17a7247 100644 --- a/api_docs/kbn_apm_synthtrace.mdx +++ b/api_docs/kbn_apm_synthtrace.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-apm-synthtrace title: "@kbn/apm-synthtrace" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/apm-synthtrace plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/apm-synthtrace'] --- import kbnApmSynthtraceObj from './kbn_apm_synthtrace.devdocs.json'; diff --git a/api_docs/kbn_apm_utils.mdx b/api_docs/kbn_apm_utils.mdx index b79818eb45692..bb0a8c30ba7b6 100644 --- a/api_docs/kbn_apm_utils.mdx +++ b/api_docs/kbn_apm_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-apm-utils title: "@kbn/apm-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/apm-utils plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/apm-utils'] --- import kbnApmUtilsObj from './kbn_apm_utils.devdocs.json'; diff --git a/api_docs/kbn_axe_config.mdx b/api_docs/kbn_axe_config.mdx index f7b1c0bfc7ee9..19a5f1ad98ea9 100644 --- a/api_docs/kbn_axe_config.mdx +++ b/api_docs/kbn_axe_config.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-axe-config title: "@kbn/axe-config" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/axe-config plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/axe-config'] --- import kbnAxeConfigObj from './kbn_axe_config.devdocs.json'; diff --git a/api_docs/kbn_cases_components.mdx b/api_docs/kbn_cases_components.mdx index 2144d9337d59e..68ebca7534261 100644 --- a/api_docs/kbn_cases_components.mdx +++ b/api_docs/kbn_cases_components.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-cases-components title: "@kbn/cases-components" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/cases-components plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/cases-components'] --- import kbnCasesComponentsObj from './kbn_cases_components.devdocs.json'; diff --git a/api_docs/kbn_chart_icons.mdx b/api_docs/kbn_chart_icons.mdx index 34b4392fd7ab8..12b71b74d943c 100644 --- a/api_docs/kbn_chart_icons.mdx +++ b/api_docs/kbn_chart_icons.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-chart-icons title: "@kbn/chart-icons" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/chart-icons plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/chart-icons'] --- import kbnChartIconsObj from './kbn_chart_icons.devdocs.json'; diff --git a/api_docs/kbn_ci_stats_core.mdx b/api_docs/kbn_ci_stats_core.mdx index 592831bdfbdf7..1253b6e7356c6 100644 --- a/api_docs/kbn_ci_stats_core.mdx +++ b/api_docs/kbn_ci_stats_core.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ci-stats-core title: "@kbn/ci-stats-core" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ci-stats-core plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ci-stats-core'] --- import kbnCiStatsCoreObj from './kbn_ci_stats_core.devdocs.json'; diff --git a/api_docs/kbn_ci_stats_performance_metrics.mdx b/api_docs/kbn_ci_stats_performance_metrics.mdx index c45641f710e21..37810fd10cd7b 100644 --- a/api_docs/kbn_ci_stats_performance_metrics.mdx +++ b/api_docs/kbn_ci_stats_performance_metrics.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ci-stats-performance-metrics title: "@kbn/ci-stats-performance-metrics" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ci-stats-performance-metrics plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ci-stats-performance-metrics'] --- import kbnCiStatsPerformanceMetricsObj from './kbn_ci_stats_performance_metrics.devdocs.json'; diff --git a/api_docs/kbn_ci_stats_reporter.mdx b/api_docs/kbn_ci_stats_reporter.mdx index f6af9bfe95f85..c7a0357ba39e3 100644 --- a/api_docs/kbn_ci_stats_reporter.mdx +++ b/api_docs/kbn_ci_stats_reporter.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ci-stats-reporter title: "@kbn/ci-stats-reporter" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ci-stats-reporter plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ci-stats-reporter'] --- import kbnCiStatsReporterObj from './kbn_ci_stats_reporter.devdocs.json'; diff --git a/api_docs/kbn_cli_dev_mode.mdx b/api_docs/kbn_cli_dev_mode.mdx index 7981d8d7d047c..53bd40fe1a3a1 100644 --- a/api_docs/kbn_cli_dev_mode.mdx +++ b/api_docs/kbn_cli_dev_mode.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-cli-dev-mode title: "@kbn/cli-dev-mode" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/cli-dev-mode plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/cli-dev-mode'] --- import kbnCliDevModeObj from './kbn_cli_dev_mode.devdocs.json'; diff --git a/api_docs/kbn_coloring.mdx b/api_docs/kbn_coloring.mdx index 8c36c45007838..16dd69cf9317e 100644 --- a/api_docs/kbn_coloring.mdx +++ b/api_docs/kbn_coloring.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-coloring title: "@kbn/coloring" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/coloring plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/coloring'] --- import kbnColoringObj from './kbn_coloring.devdocs.json'; diff --git a/api_docs/kbn_config.mdx b/api_docs/kbn_config.mdx index 981028b68e44f..708ceb9b6c05a 100644 --- a/api_docs/kbn_config.mdx +++ b/api_docs/kbn_config.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-config title: "@kbn/config" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/config plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/config'] --- import kbnConfigObj from './kbn_config.devdocs.json'; diff --git a/api_docs/kbn_config_mocks.mdx b/api_docs/kbn_config_mocks.mdx index 907d388e410e7..3ed7da29772af 100644 --- a/api_docs/kbn_config_mocks.mdx +++ b/api_docs/kbn_config_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-config-mocks title: "@kbn/config-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/config-mocks plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/config-mocks'] --- import kbnConfigMocksObj from './kbn_config_mocks.devdocs.json'; diff --git a/api_docs/kbn_config_schema.devdocs.json b/api_docs/kbn_config_schema.devdocs.json index 6f14501525b6d..fba38fe0d44cc 100644 --- a/api_docs/kbn_config_schema.devdocs.json +++ b/api_docs/kbn_config_schema.devdocs.json @@ -463,6 +463,9 @@ "label": "rightOperand", "description": [], "signature": [ + "A | ", + "Reference", + " | ", { "pluginId": "@kbn/config-schema", "scope": "server", @@ -470,9 +473,7 @@ "section": "def-server.Type", "text": "Type" }, - " | A | ", - "Reference", - "" + "" ], "path": "packages/kbn-config-schema/src/types/conditional_type.ts", "deprecated": false, @@ -1465,7 +1466,9 @@ "ConditionalTypeValue", ", B, C>(leftOperand: ", "Reference", - ", rightOperand: ", + ", rightOperand: A | ", + "Reference", + " | ", { "pluginId": "@kbn/config-schema", "scope": "server", @@ -1473,9 +1476,7 @@ "section": "def-server.Type", "text": "Type" }, - " | A | ", - "Reference", - ", equalType: ", + ", equalType: ", { "pluginId": "@kbn/config-schema", "scope": "server", @@ -2520,7 +2521,9 @@ "ConditionalTypeValue", ", B, C>(leftOperand: ", "Reference", - ", rightOperand: ", + ", rightOperand: A | ", + "Reference", + " | ", { "pluginId": "@kbn/config-schema", "scope": "server", @@ -2528,9 +2531,7 @@ "section": "def-server.Type", "text": "Type" }, - " | A | ", - "Reference", - ", equalType: ", + ", equalType: ", { "pluginId": "@kbn/config-schema", "scope": "server", @@ -2586,6 +2587,9 @@ "label": "rightOperand", "description": [], "signature": [ + "A | ", + "Reference", + " | ", { "pluginId": "@kbn/config-schema", "scope": "server", @@ -2593,9 +2597,7 @@ "section": "def-server.Type", "text": "Type" }, - " | A | ", - "Reference", - "" + "" ], "path": "packages/kbn-config-schema/index.ts", "deprecated": false, diff --git a/api_docs/kbn_config_schema.mdx b/api_docs/kbn_config_schema.mdx index 7e38e76e8faf9..fd7a3358c5970 100644 --- a/api_docs/kbn_config_schema.mdx +++ b/api_docs/kbn_config_schema.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-config-schema title: "@kbn/config-schema" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/config-schema plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/config-schema'] --- import kbnConfigSchemaObj from './kbn_config_schema.devdocs.json'; diff --git a/api_docs/kbn_content_management_inspector.mdx b/api_docs/kbn_content_management_inspector.mdx index 7d390268b2f13..c48ec78d3145d 100644 --- a/api_docs/kbn_content_management_inspector.mdx +++ b/api_docs/kbn_content_management_inspector.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-content-management-inspector title: "@kbn/content-management-inspector" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/content-management-inspector plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/content-management-inspector'] --- import kbnContentManagementInspectorObj from './kbn_content_management_inspector.devdocs.json'; diff --git a/api_docs/kbn_content_management_table_list.mdx b/api_docs/kbn_content_management_table_list.mdx index c2d801b266565..1df20fece5df8 100644 --- a/api_docs/kbn_content_management_table_list.mdx +++ b/api_docs/kbn_content_management_table_list.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-content-management-table-list title: "@kbn/content-management-table-list" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/content-management-table-list plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/content-management-table-list'] --- import kbnContentManagementTableListObj from './kbn_content_management_table_list.devdocs.json'; diff --git a/api_docs/kbn_core_analytics_browser.mdx b/api_docs/kbn_core_analytics_browser.mdx index cbbf43264a26b..e138442ddda8a 100644 --- a/api_docs/kbn_core_analytics_browser.mdx +++ b/api_docs/kbn_core_analytics_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-analytics-browser title: "@kbn/core-analytics-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-analytics-browser plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-analytics-browser'] --- import kbnCoreAnalyticsBrowserObj from './kbn_core_analytics_browser.devdocs.json'; diff --git a/api_docs/kbn_core_analytics_browser_internal.mdx b/api_docs/kbn_core_analytics_browser_internal.mdx index 86efececb8812..7b3f475c815bd 100644 --- a/api_docs/kbn_core_analytics_browser_internal.mdx +++ b/api_docs/kbn_core_analytics_browser_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-analytics-browser-internal title: "@kbn/core-analytics-browser-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-analytics-browser-internal plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-analytics-browser-internal'] --- import kbnCoreAnalyticsBrowserInternalObj from './kbn_core_analytics_browser_internal.devdocs.json'; diff --git a/api_docs/kbn_core_analytics_browser_mocks.mdx b/api_docs/kbn_core_analytics_browser_mocks.mdx index e00e0e3d32549..6bdc7c9ae3833 100644 --- a/api_docs/kbn_core_analytics_browser_mocks.mdx +++ b/api_docs/kbn_core_analytics_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-analytics-browser-mocks title: "@kbn/core-analytics-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-analytics-browser-mocks plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-analytics-browser-mocks'] --- import kbnCoreAnalyticsBrowserMocksObj from './kbn_core_analytics_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_analytics_server.mdx b/api_docs/kbn_core_analytics_server.mdx index 0c7a347617c86..bd76c00f3956b 100644 --- a/api_docs/kbn_core_analytics_server.mdx +++ b/api_docs/kbn_core_analytics_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-analytics-server title: "@kbn/core-analytics-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-analytics-server plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-analytics-server'] --- import kbnCoreAnalyticsServerObj from './kbn_core_analytics_server.devdocs.json'; diff --git a/api_docs/kbn_core_analytics_server_internal.mdx b/api_docs/kbn_core_analytics_server_internal.mdx index 5c17e966ac723..83c5fe8f88193 100644 --- a/api_docs/kbn_core_analytics_server_internal.mdx +++ b/api_docs/kbn_core_analytics_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-analytics-server-internal title: "@kbn/core-analytics-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-analytics-server-internal plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-analytics-server-internal'] --- import kbnCoreAnalyticsServerInternalObj from './kbn_core_analytics_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_analytics_server_mocks.mdx b/api_docs/kbn_core_analytics_server_mocks.mdx index 20fb07c92dc99..c623338681c61 100644 --- a/api_docs/kbn_core_analytics_server_mocks.mdx +++ b/api_docs/kbn_core_analytics_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-analytics-server-mocks title: "@kbn/core-analytics-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-analytics-server-mocks plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-analytics-server-mocks'] --- import kbnCoreAnalyticsServerMocksObj from './kbn_core_analytics_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_application_browser.devdocs.json b/api_docs/kbn_core_application_browser.devdocs.json index a71db82bf7b24..3617d74cf3736 100644 --- a/api_docs/kbn_core_application_browser.devdocs.json +++ b/api_docs/kbn_core_application_browser.devdocs.json @@ -2184,7 +2184,7 @@ "section": "def-common.AppStatus", "text": "AppStatus" }, - " | undefined; searchable?: boolean | undefined; deepLinks?: ", + " | undefined; tooltip?: string | undefined; searchable?: boolean | undefined; deepLinks?: ", { "pluginId": "@kbn/core-application-browser", "scope": "common", @@ -2200,7 +2200,7 @@ "section": "def-common.AppNavLinkStatus", "text": "AppNavLinkStatus" }, - " | undefined; defaultPath?: string | undefined; tooltip?: string | undefined; }" + " | undefined; defaultPath?: string | undefined; }" ], "path": "packages/core/application/core-application-browser/src/application.ts", "deprecated": false, diff --git a/api_docs/kbn_core_application_browser.mdx b/api_docs/kbn_core_application_browser.mdx index dcb897e44765d..bae8feca61517 100644 --- a/api_docs/kbn_core_application_browser.mdx +++ b/api_docs/kbn_core_application_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-application-browser title: "@kbn/core-application-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-application-browser plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-application-browser'] --- import kbnCoreApplicationBrowserObj from './kbn_core_application_browser.devdocs.json'; diff --git a/api_docs/kbn_core_application_browser_internal.mdx b/api_docs/kbn_core_application_browser_internal.mdx index 8b31ce4d81403..e0db4e3081f87 100644 --- a/api_docs/kbn_core_application_browser_internal.mdx +++ b/api_docs/kbn_core_application_browser_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-application-browser-internal title: "@kbn/core-application-browser-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-application-browser-internal plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-application-browser-internal'] --- import kbnCoreApplicationBrowserInternalObj from './kbn_core_application_browser_internal.devdocs.json'; diff --git a/api_docs/kbn_core_application_browser_mocks.mdx b/api_docs/kbn_core_application_browser_mocks.mdx index 9c008160aacd3..fbb7b0b5d2b5e 100644 --- a/api_docs/kbn_core_application_browser_mocks.mdx +++ b/api_docs/kbn_core_application_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-application-browser-mocks title: "@kbn/core-application-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-application-browser-mocks plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-application-browser-mocks'] --- import kbnCoreApplicationBrowserMocksObj from './kbn_core_application_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_application_common.mdx b/api_docs/kbn_core_application_common.mdx index b81c0a7c9a5da..6bcfc76965570 100644 --- a/api_docs/kbn_core_application_common.mdx +++ b/api_docs/kbn_core_application_common.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-application-common title: "@kbn/core-application-common" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-application-common plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-application-common'] --- import kbnCoreApplicationCommonObj from './kbn_core_application_common.devdocs.json'; diff --git a/api_docs/kbn_core_apps_browser_internal.mdx b/api_docs/kbn_core_apps_browser_internal.mdx index 59ce3cf11d292..a403ceec9757f 100644 --- a/api_docs/kbn_core_apps_browser_internal.mdx +++ b/api_docs/kbn_core_apps_browser_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-apps-browser-internal title: "@kbn/core-apps-browser-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-apps-browser-internal plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-apps-browser-internal'] --- import kbnCoreAppsBrowserInternalObj from './kbn_core_apps_browser_internal.devdocs.json'; diff --git a/api_docs/kbn_core_apps_browser_mocks.mdx b/api_docs/kbn_core_apps_browser_mocks.mdx index 3433f452ed1fe..c4674b146ceb0 100644 --- a/api_docs/kbn_core_apps_browser_mocks.mdx +++ b/api_docs/kbn_core_apps_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-apps-browser-mocks title: "@kbn/core-apps-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-apps-browser-mocks plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-apps-browser-mocks'] --- import kbnCoreAppsBrowserMocksObj from './kbn_core_apps_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_apps_server_internal.mdx b/api_docs/kbn_core_apps_server_internal.mdx index 73587da109ae8..1d37c28ba0538 100644 --- a/api_docs/kbn_core_apps_server_internal.mdx +++ b/api_docs/kbn_core_apps_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-apps-server-internal title: "@kbn/core-apps-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-apps-server-internal plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-apps-server-internal'] --- import kbnCoreAppsServerInternalObj from './kbn_core_apps_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_base_browser_mocks.mdx b/api_docs/kbn_core_base_browser_mocks.mdx index d0327305635b2..18e091e19fa70 100644 --- a/api_docs/kbn_core_base_browser_mocks.mdx +++ b/api_docs/kbn_core_base_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-base-browser-mocks title: "@kbn/core-base-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-base-browser-mocks plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-base-browser-mocks'] --- import kbnCoreBaseBrowserMocksObj from './kbn_core_base_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_base_common.mdx b/api_docs/kbn_core_base_common.mdx index 1a66a19ccd359..eee27b24d3a87 100644 --- a/api_docs/kbn_core_base_common.mdx +++ b/api_docs/kbn_core_base_common.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-base-common title: "@kbn/core-base-common" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-base-common plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-base-common'] --- import kbnCoreBaseCommonObj from './kbn_core_base_common.devdocs.json'; diff --git a/api_docs/kbn_core_base_server_internal.mdx b/api_docs/kbn_core_base_server_internal.mdx index 005dbe6a6545b..501e2a8132fb6 100644 --- a/api_docs/kbn_core_base_server_internal.mdx +++ b/api_docs/kbn_core_base_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-base-server-internal title: "@kbn/core-base-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-base-server-internal plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-base-server-internal'] --- import kbnCoreBaseServerInternalObj from './kbn_core_base_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_base_server_mocks.mdx b/api_docs/kbn_core_base_server_mocks.mdx index 213da2d32ab8f..9d62b2053cd48 100644 --- a/api_docs/kbn_core_base_server_mocks.mdx +++ b/api_docs/kbn_core_base_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-base-server-mocks title: "@kbn/core-base-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-base-server-mocks plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-base-server-mocks'] --- import kbnCoreBaseServerMocksObj from './kbn_core_base_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_capabilities_browser_mocks.mdx b/api_docs/kbn_core_capabilities_browser_mocks.mdx index 5c71f5360c8ea..bb4763c496985 100644 --- a/api_docs/kbn_core_capabilities_browser_mocks.mdx +++ b/api_docs/kbn_core_capabilities_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-capabilities-browser-mocks title: "@kbn/core-capabilities-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-capabilities-browser-mocks plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-capabilities-browser-mocks'] --- import kbnCoreCapabilitiesBrowserMocksObj from './kbn_core_capabilities_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_capabilities_common.mdx b/api_docs/kbn_core_capabilities_common.mdx index e52c960f4c83d..0b179701b2bed 100644 --- a/api_docs/kbn_core_capabilities_common.mdx +++ b/api_docs/kbn_core_capabilities_common.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-capabilities-common title: "@kbn/core-capabilities-common" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-capabilities-common plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-capabilities-common'] --- import kbnCoreCapabilitiesCommonObj from './kbn_core_capabilities_common.devdocs.json'; diff --git a/api_docs/kbn_core_capabilities_server.mdx b/api_docs/kbn_core_capabilities_server.mdx index 593c30c7d435c..a23dc6b94595c 100644 --- a/api_docs/kbn_core_capabilities_server.mdx +++ b/api_docs/kbn_core_capabilities_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-capabilities-server title: "@kbn/core-capabilities-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-capabilities-server plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-capabilities-server'] --- import kbnCoreCapabilitiesServerObj from './kbn_core_capabilities_server.devdocs.json'; diff --git a/api_docs/kbn_core_capabilities_server_mocks.mdx b/api_docs/kbn_core_capabilities_server_mocks.mdx index ea4a968f46b6e..8494e6833af33 100644 --- a/api_docs/kbn_core_capabilities_server_mocks.mdx +++ b/api_docs/kbn_core_capabilities_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-capabilities-server-mocks title: "@kbn/core-capabilities-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-capabilities-server-mocks plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-capabilities-server-mocks'] --- import kbnCoreCapabilitiesServerMocksObj from './kbn_core_capabilities_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_chrome_browser.mdx b/api_docs/kbn_core_chrome_browser.mdx index 1c10eeb36ba1c..1197ec3f49581 100644 --- a/api_docs/kbn_core_chrome_browser.mdx +++ b/api_docs/kbn_core_chrome_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-chrome-browser title: "@kbn/core-chrome-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-chrome-browser plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-chrome-browser'] --- import kbnCoreChromeBrowserObj from './kbn_core_chrome_browser.devdocs.json'; diff --git a/api_docs/kbn_core_chrome_browser_mocks.mdx b/api_docs/kbn_core_chrome_browser_mocks.mdx index 264692bed44fc..ead8f251e06f5 100644 --- a/api_docs/kbn_core_chrome_browser_mocks.mdx +++ b/api_docs/kbn_core_chrome_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-chrome-browser-mocks title: "@kbn/core-chrome-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-chrome-browser-mocks plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-chrome-browser-mocks'] --- import kbnCoreChromeBrowserMocksObj from './kbn_core_chrome_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_config_server_internal.mdx b/api_docs/kbn_core_config_server_internal.mdx index f813d02d0551d..4fbd29fc8eb96 100644 --- a/api_docs/kbn_core_config_server_internal.mdx +++ b/api_docs/kbn_core_config_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-config-server-internal title: "@kbn/core-config-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-config-server-internal plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-config-server-internal'] --- import kbnCoreConfigServerInternalObj from './kbn_core_config_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_deprecations_browser.mdx b/api_docs/kbn_core_deprecations_browser.mdx index e73cebcba742c..eb0162b866ab6 100644 --- a/api_docs/kbn_core_deprecations_browser.mdx +++ b/api_docs/kbn_core_deprecations_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-deprecations-browser title: "@kbn/core-deprecations-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-deprecations-browser plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-deprecations-browser'] --- import kbnCoreDeprecationsBrowserObj from './kbn_core_deprecations_browser.devdocs.json'; diff --git a/api_docs/kbn_core_deprecations_browser_internal.mdx b/api_docs/kbn_core_deprecations_browser_internal.mdx index bdf6860daa3a4..e405cf5256531 100644 --- a/api_docs/kbn_core_deprecations_browser_internal.mdx +++ b/api_docs/kbn_core_deprecations_browser_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-deprecations-browser-internal title: "@kbn/core-deprecations-browser-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-deprecations-browser-internal plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-deprecations-browser-internal'] --- import kbnCoreDeprecationsBrowserInternalObj from './kbn_core_deprecations_browser_internal.devdocs.json'; diff --git a/api_docs/kbn_core_deprecations_browser_mocks.mdx b/api_docs/kbn_core_deprecations_browser_mocks.mdx index b6a1170314309..d24971ac00b4b 100644 --- a/api_docs/kbn_core_deprecations_browser_mocks.mdx +++ b/api_docs/kbn_core_deprecations_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-deprecations-browser-mocks title: "@kbn/core-deprecations-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-deprecations-browser-mocks plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-deprecations-browser-mocks'] --- import kbnCoreDeprecationsBrowserMocksObj from './kbn_core_deprecations_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_deprecations_common.mdx b/api_docs/kbn_core_deprecations_common.mdx index 03bfb72f3344e..8b67195a256b4 100644 --- a/api_docs/kbn_core_deprecations_common.mdx +++ b/api_docs/kbn_core_deprecations_common.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-deprecations-common title: "@kbn/core-deprecations-common" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-deprecations-common plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-deprecations-common'] --- import kbnCoreDeprecationsCommonObj from './kbn_core_deprecations_common.devdocs.json'; diff --git a/api_docs/kbn_core_deprecations_server.mdx b/api_docs/kbn_core_deprecations_server.mdx index 2aa81ebc13ea9..de5f355dbc3eb 100644 --- a/api_docs/kbn_core_deprecations_server.mdx +++ b/api_docs/kbn_core_deprecations_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-deprecations-server title: "@kbn/core-deprecations-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-deprecations-server plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-deprecations-server'] --- import kbnCoreDeprecationsServerObj from './kbn_core_deprecations_server.devdocs.json'; diff --git a/api_docs/kbn_core_deprecations_server_internal.mdx b/api_docs/kbn_core_deprecations_server_internal.mdx index eef2e6638d3b9..15ad044e228d2 100644 --- a/api_docs/kbn_core_deprecations_server_internal.mdx +++ b/api_docs/kbn_core_deprecations_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-deprecations-server-internal title: "@kbn/core-deprecations-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-deprecations-server-internal plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-deprecations-server-internal'] --- import kbnCoreDeprecationsServerInternalObj from './kbn_core_deprecations_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_deprecations_server_mocks.mdx b/api_docs/kbn_core_deprecations_server_mocks.mdx index 7a712c0ded8fe..97a0f29614524 100644 --- a/api_docs/kbn_core_deprecations_server_mocks.mdx +++ b/api_docs/kbn_core_deprecations_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-deprecations-server-mocks title: "@kbn/core-deprecations-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-deprecations-server-mocks plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-deprecations-server-mocks'] --- import kbnCoreDeprecationsServerMocksObj from './kbn_core_deprecations_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_doc_links_browser.mdx b/api_docs/kbn_core_doc_links_browser.mdx index b31305351c1eb..efa4e081fea9b 100644 --- a/api_docs/kbn_core_doc_links_browser.mdx +++ b/api_docs/kbn_core_doc_links_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-doc-links-browser title: "@kbn/core-doc-links-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-doc-links-browser plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-doc-links-browser'] --- import kbnCoreDocLinksBrowserObj from './kbn_core_doc_links_browser.devdocs.json'; diff --git a/api_docs/kbn_core_doc_links_browser_mocks.mdx b/api_docs/kbn_core_doc_links_browser_mocks.mdx index 6aa33778bd2dd..401afc56835c9 100644 --- a/api_docs/kbn_core_doc_links_browser_mocks.mdx +++ b/api_docs/kbn_core_doc_links_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-doc-links-browser-mocks title: "@kbn/core-doc-links-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-doc-links-browser-mocks plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-doc-links-browser-mocks'] --- import kbnCoreDocLinksBrowserMocksObj from './kbn_core_doc_links_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_doc_links_server.mdx b/api_docs/kbn_core_doc_links_server.mdx index bd34997e184dc..0685587258be5 100644 --- a/api_docs/kbn_core_doc_links_server.mdx +++ b/api_docs/kbn_core_doc_links_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-doc-links-server title: "@kbn/core-doc-links-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-doc-links-server plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-doc-links-server'] --- import kbnCoreDocLinksServerObj from './kbn_core_doc_links_server.devdocs.json'; diff --git a/api_docs/kbn_core_doc_links_server_mocks.mdx b/api_docs/kbn_core_doc_links_server_mocks.mdx index 5f6a42dc32c8f..e3d0c922b753e 100644 --- a/api_docs/kbn_core_doc_links_server_mocks.mdx +++ b/api_docs/kbn_core_doc_links_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-doc-links-server-mocks title: "@kbn/core-doc-links-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-doc-links-server-mocks plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-doc-links-server-mocks'] --- import kbnCoreDocLinksServerMocksObj from './kbn_core_doc_links_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_elasticsearch_client_server_internal.mdx b/api_docs/kbn_core_elasticsearch_client_server_internal.mdx index 399f73605d1d9..02504301f8043 100644 --- a/api_docs/kbn_core_elasticsearch_client_server_internal.mdx +++ b/api_docs/kbn_core_elasticsearch_client_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-elasticsearch-client-server-internal title: "@kbn/core-elasticsearch-client-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-elasticsearch-client-server-internal plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-elasticsearch-client-server-internal'] --- import kbnCoreElasticsearchClientServerInternalObj from './kbn_core_elasticsearch_client_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_elasticsearch_client_server_mocks.mdx b/api_docs/kbn_core_elasticsearch_client_server_mocks.mdx index 6d59687448f58..97a0c2572ec7e 100644 --- a/api_docs/kbn_core_elasticsearch_client_server_mocks.mdx +++ b/api_docs/kbn_core_elasticsearch_client_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-elasticsearch-client-server-mocks title: "@kbn/core-elasticsearch-client-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-elasticsearch-client-server-mocks plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-elasticsearch-client-server-mocks'] --- import kbnCoreElasticsearchClientServerMocksObj from './kbn_core_elasticsearch_client_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_elasticsearch_server.mdx b/api_docs/kbn_core_elasticsearch_server.mdx index 82b7b611c837f..97c87063435e5 100644 --- a/api_docs/kbn_core_elasticsearch_server.mdx +++ b/api_docs/kbn_core_elasticsearch_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-elasticsearch-server title: "@kbn/core-elasticsearch-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-elasticsearch-server plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-elasticsearch-server'] --- import kbnCoreElasticsearchServerObj from './kbn_core_elasticsearch_server.devdocs.json'; diff --git a/api_docs/kbn_core_elasticsearch_server_internal.devdocs.json b/api_docs/kbn_core_elasticsearch_server_internal.devdocs.json index e7449c52e06ce..b6dba147ae5f1 100644 --- a/api_docs/kbn_core_elasticsearch_server_internal.devdocs.json +++ b/api_docs/kbn_core_elasticsearch_server_internal.devdocs.json @@ -2941,7 +2941,7 @@ "label": "ElasticsearchConfigType", "description": [], "signature": [ - "{ readonly username?: string | undefined; readonly password?: string | undefined; readonly serviceAccountToken?: string | undefined; readonly requestTimeout: moment.Duration; readonly compression: boolean; readonly ssl: Readonly<{ key?: string | undefined; certificateAuthorities?: string | string[] | undefined; certificate?: string | undefined; keyPassphrase?: string | undefined; } & { verificationMode: \"none\" | \"full\" | \"certificate\"; keystore: Readonly<{ path?: string | undefined; password?: string | undefined; } & {}>; truststore: Readonly<{ path?: string | undefined; password?: string | undefined; } & {}>; alwaysPresentCertificate: boolean; }>; readonly healthCheck: Readonly<{} & { delay: moment.Duration; }>; readonly customHeaders: Record; readonly hosts: string | string[]; readonly sniffOnStart: boolean; readonly sniffInterval: false | moment.Duration; readonly sniffOnConnectionFault: boolean; readonly maxSockets: number; readonly maxIdleSockets: number; readonly idleSocketTimeout: moment.Duration; readonly requestHeadersWhitelist: string | string[]; readonly shardTimeout: moment.Duration; readonly pingTimeout: moment.Duration; readonly logQueries: boolean; readonly apiVersion: string; readonly ignoreVersionMismatch: boolean; readonly skipStartupConnectionCheck: boolean; }" + "{ readonly username?: string | undefined; readonly password?: string | undefined; readonly serviceAccountToken?: string | undefined; readonly requestTimeout: moment.Duration; readonly compression: boolean; readonly ssl: Readonly<{ key?: string | undefined; certificate?: string | undefined; certificateAuthorities?: string | string[] | undefined; keyPassphrase?: string | undefined; } & { verificationMode: \"none\" | \"full\" | \"certificate\"; keystore: Readonly<{ path?: string | undefined; password?: string | undefined; } & {}>; truststore: Readonly<{ path?: string | undefined; password?: string | undefined; } & {}>; alwaysPresentCertificate: boolean; }>; readonly sniffOnStart: boolean; readonly sniffInterval: false | moment.Duration; readonly sniffOnConnectionFault: boolean; readonly hosts: string | string[]; readonly maxSockets: number; readonly maxIdleSockets: number; readonly idleSocketTimeout: moment.Duration; readonly requestHeadersWhitelist: string | string[]; readonly customHeaders: Record; readonly shardTimeout: moment.Duration; readonly pingTimeout: moment.Duration; readonly logQueries: boolean; readonly apiVersion: string; readonly healthCheck: Readonly<{} & { delay: moment.Duration; }>; readonly ignoreVersionMismatch: boolean; readonly skipStartupConnectionCheck: boolean; }" ], "path": "packages/core/elasticsearch/core-elasticsearch-server-internal/src/elasticsearch_config.ts", "deprecated": false, diff --git a/api_docs/kbn_core_elasticsearch_server_internal.mdx b/api_docs/kbn_core_elasticsearch_server_internal.mdx index 4332c800b8528..db70417f5abe6 100644 --- a/api_docs/kbn_core_elasticsearch_server_internal.mdx +++ b/api_docs/kbn_core_elasticsearch_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-elasticsearch-server-internal title: "@kbn/core-elasticsearch-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-elasticsearch-server-internal plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-elasticsearch-server-internal'] --- import kbnCoreElasticsearchServerInternalObj from './kbn_core_elasticsearch_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_elasticsearch_server_mocks.mdx b/api_docs/kbn_core_elasticsearch_server_mocks.mdx index a5e1d3f53e2e2..18a21a41197d1 100644 --- a/api_docs/kbn_core_elasticsearch_server_mocks.mdx +++ b/api_docs/kbn_core_elasticsearch_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-elasticsearch-server-mocks title: "@kbn/core-elasticsearch-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-elasticsearch-server-mocks plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-elasticsearch-server-mocks'] --- import kbnCoreElasticsearchServerMocksObj from './kbn_core_elasticsearch_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_environment_server_internal.mdx b/api_docs/kbn_core_environment_server_internal.mdx index c84aaf023f8ed..87f9413cc1b79 100644 --- a/api_docs/kbn_core_environment_server_internal.mdx +++ b/api_docs/kbn_core_environment_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-environment-server-internal title: "@kbn/core-environment-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-environment-server-internal plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-environment-server-internal'] --- import kbnCoreEnvironmentServerInternalObj from './kbn_core_environment_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_environment_server_mocks.mdx b/api_docs/kbn_core_environment_server_mocks.mdx index 9f96e167544e7..f59073f470892 100644 --- a/api_docs/kbn_core_environment_server_mocks.mdx +++ b/api_docs/kbn_core_environment_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-environment-server-mocks title: "@kbn/core-environment-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-environment-server-mocks plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-environment-server-mocks'] --- import kbnCoreEnvironmentServerMocksObj from './kbn_core_environment_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_execution_context_browser.mdx b/api_docs/kbn_core_execution_context_browser.mdx index 6f8b3fe1416b8..dcb694db9c788 100644 --- a/api_docs/kbn_core_execution_context_browser.mdx +++ b/api_docs/kbn_core_execution_context_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-execution-context-browser title: "@kbn/core-execution-context-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-execution-context-browser plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-execution-context-browser'] --- import kbnCoreExecutionContextBrowserObj from './kbn_core_execution_context_browser.devdocs.json'; diff --git a/api_docs/kbn_core_execution_context_browser_internal.mdx b/api_docs/kbn_core_execution_context_browser_internal.mdx index 8c5682cb18598..66a42cf170fb8 100644 --- a/api_docs/kbn_core_execution_context_browser_internal.mdx +++ b/api_docs/kbn_core_execution_context_browser_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-execution-context-browser-internal title: "@kbn/core-execution-context-browser-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-execution-context-browser-internal plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-execution-context-browser-internal'] --- import kbnCoreExecutionContextBrowserInternalObj from './kbn_core_execution_context_browser_internal.devdocs.json'; diff --git a/api_docs/kbn_core_execution_context_browser_mocks.mdx b/api_docs/kbn_core_execution_context_browser_mocks.mdx index 6ddcaf614c996..155c49ebe0c0e 100644 --- a/api_docs/kbn_core_execution_context_browser_mocks.mdx +++ b/api_docs/kbn_core_execution_context_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-execution-context-browser-mocks title: "@kbn/core-execution-context-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-execution-context-browser-mocks plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-execution-context-browser-mocks'] --- import kbnCoreExecutionContextBrowserMocksObj from './kbn_core_execution_context_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_execution_context_common.mdx b/api_docs/kbn_core_execution_context_common.mdx index 876e9708df9e2..26daf043a5389 100644 --- a/api_docs/kbn_core_execution_context_common.mdx +++ b/api_docs/kbn_core_execution_context_common.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-execution-context-common title: "@kbn/core-execution-context-common" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-execution-context-common plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-execution-context-common'] --- import kbnCoreExecutionContextCommonObj from './kbn_core_execution_context_common.devdocs.json'; diff --git a/api_docs/kbn_core_execution_context_server.mdx b/api_docs/kbn_core_execution_context_server.mdx index daf579c9abdc3..56764e150126b 100644 --- a/api_docs/kbn_core_execution_context_server.mdx +++ b/api_docs/kbn_core_execution_context_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-execution-context-server title: "@kbn/core-execution-context-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-execution-context-server plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-execution-context-server'] --- import kbnCoreExecutionContextServerObj from './kbn_core_execution_context_server.devdocs.json'; diff --git a/api_docs/kbn_core_execution_context_server_internal.mdx b/api_docs/kbn_core_execution_context_server_internal.mdx index fd25efeb0fe7c..e3afcaa5d1120 100644 --- a/api_docs/kbn_core_execution_context_server_internal.mdx +++ b/api_docs/kbn_core_execution_context_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-execution-context-server-internal title: "@kbn/core-execution-context-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-execution-context-server-internal plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-execution-context-server-internal'] --- import kbnCoreExecutionContextServerInternalObj from './kbn_core_execution_context_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_execution_context_server_mocks.mdx b/api_docs/kbn_core_execution_context_server_mocks.mdx index 87b89e42dd8ca..522a26b552a92 100644 --- a/api_docs/kbn_core_execution_context_server_mocks.mdx +++ b/api_docs/kbn_core_execution_context_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-execution-context-server-mocks title: "@kbn/core-execution-context-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-execution-context-server-mocks plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-execution-context-server-mocks'] --- import kbnCoreExecutionContextServerMocksObj from './kbn_core_execution_context_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_fatal_errors_browser.mdx b/api_docs/kbn_core_fatal_errors_browser.mdx index b173da1cd77e8..637063fc00cbf 100644 --- a/api_docs/kbn_core_fatal_errors_browser.mdx +++ b/api_docs/kbn_core_fatal_errors_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-fatal-errors-browser title: "@kbn/core-fatal-errors-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-fatal-errors-browser plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-fatal-errors-browser'] --- import kbnCoreFatalErrorsBrowserObj from './kbn_core_fatal_errors_browser.devdocs.json'; diff --git a/api_docs/kbn_core_fatal_errors_browser_mocks.mdx b/api_docs/kbn_core_fatal_errors_browser_mocks.mdx index 3a1c0788391c1..4ad8cff0e44d0 100644 --- a/api_docs/kbn_core_fatal_errors_browser_mocks.mdx +++ b/api_docs/kbn_core_fatal_errors_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-fatal-errors-browser-mocks title: "@kbn/core-fatal-errors-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-fatal-errors-browser-mocks plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-fatal-errors-browser-mocks'] --- import kbnCoreFatalErrorsBrowserMocksObj from './kbn_core_fatal_errors_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_http_browser.mdx b/api_docs/kbn_core_http_browser.mdx index ac5111a476df5..719a3bcfb2fea 100644 --- a/api_docs/kbn_core_http_browser.mdx +++ b/api_docs/kbn_core_http_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-http-browser title: "@kbn/core-http-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-http-browser plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-http-browser'] --- import kbnCoreHttpBrowserObj from './kbn_core_http_browser.devdocs.json'; diff --git a/api_docs/kbn_core_http_browser_internal.mdx b/api_docs/kbn_core_http_browser_internal.mdx index ec4dff62bb052..63dda315f6e63 100644 --- a/api_docs/kbn_core_http_browser_internal.mdx +++ b/api_docs/kbn_core_http_browser_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-http-browser-internal title: "@kbn/core-http-browser-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-http-browser-internal plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-http-browser-internal'] --- import kbnCoreHttpBrowserInternalObj from './kbn_core_http_browser_internal.devdocs.json'; diff --git a/api_docs/kbn_core_http_browser_mocks.mdx b/api_docs/kbn_core_http_browser_mocks.mdx index ddbcc84495951..dbeee26176bc5 100644 --- a/api_docs/kbn_core_http_browser_mocks.mdx +++ b/api_docs/kbn_core_http_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-http-browser-mocks title: "@kbn/core-http-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-http-browser-mocks plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-http-browser-mocks'] --- import kbnCoreHttpBrowserMocksObj from './kbn_core_http_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_http_common.mdx b/api_docs/kbn_core_http_common.mdx index 2bfa9fe37e27b..2b3318c8f682e 100644 --- a/api_docs/kbn_core_http_common.mdx +++ b/api_docs/kbn_core_http_common.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-http-common title: "@kbn/core-http-common" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-http-common plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-http-common'] --- import kbnCoreHttpCommonObj from './kbn_core_http_common.devdocs.json'; diff --git a/api_docs/kbn_core_http_context_server_mocks.mdx b/api_docs/kbn_core_http_context_server_mocks.mdx index b0fcf76482f8e..0b461097533d8 100644 --- a/api_docs/kbn_core_http_context_server_mocks.mdx +++ b/api_docs/kbn_core_http_context_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-http-context-server-mocks title: "@kbn/core-http-context-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-http-context-server-mocks plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-http-context-server-mocks'] --- import kbnCoreHttpContextServerMocksObj from './kbn_core_http_context_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_http_request_handler_context_server.mdx b/api_docs/kbn_core_http_request_handler_context_server.mdx index c5b083ae4b003..d5c4b5931d8d1 100644 --- a/api_docs/kbn_core_http_request_handler_context_server.mdx +++ b/api_docs/kbn_core_http_request_handler_context_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-http-request-handler-context-server title: "@kbn/core-http-request-handler-context-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-http-request-handler-context-server plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-http-request-handler-context-server'] --- import kbnCoreHttpRequestHandlerContextServerObj from './kbn_core_http_request_handler_context_server.devdocs.json'; diff --git a/api_docs/kbn_core_http_resources_server.mdx b/api_docs/kbn_core_http_resources_server.mdx index 599cd4648dcfb..51f1d143e50bb 100644 --- a/api_docs/kbn_core_http_resources_server.mdx +++ b/api_docs/kbn_core_http_resources_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-http-resources-server title: "@kbn/core-http-resources-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-http-resources-server plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-http-resources-server'] --- import kbnCoreHttpResourcesServerObj from './kbn_core_http_resources_server.devdocs.json'; diff --git a/api_docs/kbn_core_http_resources_server_internal.mdx b/api_docs/kbn_core_http_resources_server_internal.mdx index f24f8e350358a..d332395077dfe 100644 --- a/api_docs/kbn_core_http_resources_server_internal.mdx +++ b/api_docs/kbn_core_http_resources_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-http-resources-server-internal title: "@kbn/core-http-resources-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-http-resources-server-internal plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-http-resources-server-internal'] --- import kbnCoreHttpResourcesServerInternalObj from './kbn_core_http_resources_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_http_resources_server_mocks.mdx b/api_docs/kbn_core_http_resources_server_mocks.mdx index 251703c1b1e58..f649b1a5084dc 100644 --- a/api_docs/kbn_core_http_resources_server_mocks.mdx +++ b/api_docs/kbn_core_http_resources_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-http-resources-server-mocks title: "@kbn/core-http-resources-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-http-resources-server-mocks plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-http-resources-server-mocks'] --- import kbnCoreHttpResourcesServerMocksObj from './kbn_core_http_resources_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_http_router_server_internal.mdx b/api_docs/kbn_core_http_router_server_internal.mdx index 324f4fb9f5e7d..3590e069264ea 100644 --- a/api_docs/kbn_core_http_router_server_internal.mdx +++ b/api_docs/kbn_core_http_router_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-http-router-server-internal title: "@kbn/core-http-router-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-http-router-server-internal plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-http-router-server-internal'] --- import kbnCoreHttpRouterServerInternalObj from './kbn_core_http_router_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_http_router_server_mocks.mdx b/api_docs/kbn_core_http_router_server_mocks.mdx index c954d9aefa875..99681132acd3f 100644 --- a/api_docs/kbn_core_http_router_server_mocks.mdx +++ b/api_docs/kbn_core_http_router_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-http-router-server-mocks title: "@kbn/core-http-router-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-http-router-server-mocks plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-http-router-server-mocks'] --- import kbnCoreHttpRouterServerMocksObj from './kbn_core_http_router_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_http_server.mdx b/api_docs/kbn_core_http_server.mdx index 235b80be2b3a1..d7d58d0622bde 100644 --- a/api_docs/kbn_core_http_server.mdx +++ b/api_docs/kbn_core_http_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-http-server title: "@kbn/core-http-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-http-server plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-http-server'] --- import kbnCoreHttpServerObj from './kbn_core_http_server.devdocs.json'; diff --git a/api_docs/kbn_core_http_server_internal.devdocs.json b/api_docs/kbn_core_http_server_internal.devdocs.json index d4c9428b8a5da..a7be449344f0c 100644 --- a/api_docs/kbn_core_http_server_internal.devdocs.json +++ b/api_docs/kbn_core_http_server_internal.devdocs.json @@ -843,7 +843,7 @@ "label": "HttpConfigType", "description": [], "signature": [ - "{ readonly uuid?: string | undefined; readonly basePath?: string | undefined; readonly publicBaseUrl?: string | undefined; readonly name: string; readonly host: string; readonly compression: Readonly<{ referrerWhitelist?: string[] | undefined; } & { enabled: boolean; brotli: Readonly<{} & { enabled: boolean; quality: number; }>; }>; readonly ssl: Readonly<{ key?: string | undefined; certificateAuthorities?: string | string[] | undefined; certificate?: string | undefined; keyPassphrase?: string | undefined; redirectHttpFromPort?: number | undefined; } & { enabled: boolean; keystore: Readonly<{ path?: string | undefined; password?: string | undefined; } & {}>; truststore: Readonly<{ path?: string | undefined; password?: string | undefined; } & {}>; cipherSuites: string[]; supportedProtocols: string[]; clientAuthentication: \"optional\" | \"none\" | \"required\"; }>; readonly port: number; readonly cors: Readonly<{} & { enabled: boolean; allowCredentials: boolean; allowOrigin: string[] | \"*\"[]; }>; readonly autoListen: boolean; readonly shutdownTimeout: moment.Duration; readonly securityResponseHeaders: Readonly<{} & { referrerPolicy: \"origin\" | \"no-referrer\" | \"no-referrer-when-downgrade\" | \"origin-when-cross-origin\" | \"same-origin\" | \"strict-origin\" | \"strict-origin-when-cross-origin\" | \"unsafe-url\" | null; disableEmbedding: boolean; strictTransportSecurity: string | null; xContentTypeOptions: \"nosniff\" | null; permissionsPolicy: string | null; }>; readonly customResponseHeaders: Record; readonly maxPayload: ", + "{ readonly basePath?: string | undefined; readonly uuid?: string | undefined; readonly publicBaseUrl?: string | undefined; readonly name: string; readonly host: string; readonly compression: Readonly<{ referrerWhitelist?: string[] | undefined; } & { enabled: boolean; brotli: Readonly<{} & { enabled: boolean; quality: number; }>; }>; readonly ssl: Readonly<{ key?: string | undefined; certificate?: string | undefined; certificateAuthorities?: string | string[] | undefined; keyPassphrase?: string | undefined; redirectHttpFromPort?: number | undefined; } & { enabled: boolean; keystore: Readonly<{ path?: string | undefined; password?: string | undefined; } & {}>; truststore: Readonly<{ path?: string | undefined; password?: string | undefined; } & {}>; cipherSuites: string[]; supportedProtocols: string[]; clientAuthentication: \"optional\" | \"none\" | \"required\"; }>; readonly port: number; readonly cors: Readonly<{} & { enabled: boolean; allowCredentials: boolean; allowOrigin: string[] | \"*\"[]; }>; readonly autoListen: boolean; readonly shutdownTimeout: moment.Duration; readonly securityResponseHeaders: Readonly<{} & { referrerPolicy: \"origin\" | \"no-referrer\" | \"no-referrer-when-downgrade\" | \"origin-when-cross-origin\" | \"same-origin\" | \"strict-origin\" | \"strict-origin-when-cross-origin\" | \"unsafe-url\" | null; disableEmbedding: boolean; strictTransportSecurity: string | null; xContentTypeOptions: \"nosniff\" | null; permissionsPolicy: string | null; }>; readonly customResponseHeaders: Record; readonly maxPayload: ", { "pluginId": "@kbn/config-schema", "scope": "server", diff --git a/api_docs/kbn_core_http_server_internal.mdx b/api_docs/kbn_core_http_server_internal.mdx index a777baa4c6831..415997e2e5d97 100644 --- a/api_docs/kbn_core_http_server_internal.mdx +++ b/api_docs/kbn_core_http_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-http-server-internal title: "@kbn/core-http-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-http-server-internal plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-http-server-internal'] --- import kbnCoreHttpServerInternalObj from './kbn_core_http_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_http_server_mocks.mdx b/api_docs/kbn_core_http_server_mocks.mdx index 3672d70203e67..a6cdf0088c229 100644 --- a/api_docs/kbn_core_http_server_mocks.mdx +++ b/api_docs/kbn_core_http_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-http-server-mocks title: "@kbn/core-http-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-http-server-mocks plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-http-server-mocks'] --- import kbnCoreHttpServerMocksObj from './kbn_core_http_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_i18n_browser.mdx b/api_docs/kbn_core_i18n_browser.mdx index 40fb17fac0417..ca63095520e9e 100644 --- a/api_docs/kbn_core_i18n_browser.mdx +++ b/api_docs/kbn_core_i18n_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-i18n-browser title: "@kbn/core-i18n-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-i18n-browser plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-i18n-browser'] --- import kbnCoreI18nBrowserObj from './kbn_core_i18n_browser.devdocs.json'; diff --git a/api_docs/kbn_core_i18n_browser_mocks.mdx b/api_docs/kbn_core_i18n_browser_mocks.mdx index 3bb5c77d25033..98d67894aa374 100644 --- a/api_docs/kbn_core_i18n_browser_mocks.mdx +++ b/api_docs/kbn_core_i18n_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-i18n-browser-mocks title: "@kbn/core-i18n-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-i18n-browser-mocks plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-i18n-browser-mocks'] --- import kbnCoreI18nBrowserMocksObj from './kbn_core_i18n_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_i18n_server.mdx b/api_docs/kbn_core_i18n_server.mdx index 240fe516b6a34..5c358e1bf3ee5 100644 --- a/api_docs/kbn_core_i18n_server.mdx +++ b/api_docs/kbn_core_i18n_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-i18n-server title: "@kbn/core-i18n-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-i18n-server plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-i18n-server'] --- import kbnCoreI18nServerObj from './kbn_core_i18n_server.devdocs.json'; diff --git a/api_docs/kbn_core_i18n_server_internal.mdx b/api_docs/kbn_core_i18n_server_internal.mdx index bf77cdf77ba50..a56485818b13e 100644 --- a/api_docs/kbn_core_i18n_server_internal.mdx +++ b/api_docs/kbn_core_i18n_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-i18n-server-internal title: "@kbn/core-i18n-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-i18n-server-internal plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-i18n-server-internal'] --- import kbnCoreI18nServerInternalObj from './kbn_core_i18n_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_i18n_server_mocks.mdx b/api_docs/kbn_core_i18n_server_mocks.mdx index bec5d320b09e3..5db1c4d7c80ce 100644 --- a/api_docs/kbn_core_i18n_server_mocks.mdx +++ b/api_docs/kbn_core_i18n_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-i18n-server-mocks title: "@kbn/core-i18n-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-i18n-server-mocks plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-i18n-server-mocks'] --- import kbnCoreI18nServerMocksObj from './kbn_core_i18n_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_injected_metadata_browser.mdx b/api_docs/kbn_core_injected_metadata_browser.mdx index 43651d51864bb..589e3f65df79f 100644 --- a/api_docs/kbn_core_injected_metadata_browser.mdx +++ b/api_docs/kbn_core_injected_metadata_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-injected-metadata-browser title: "@kbn/core-injected-metadata-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-injected-metadata-browser plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-injected-metadata-browser'] --- import kbnCoreInjectedMetadataBrowserObj from './kbn_core_injected_metadata_browser.devdocs.json'; diff --git a/api_docs/kbn_core_injected_metadata_browser_mocks.mdx b/api_docs/kbn_core_injected_metadata_browser_mocks.mdx index cfb8064d52e35..30a3e3268391c 100644 --- a/api_docs/kbn_core_injected_metadata_browser_mocks.mdx +++ b/api_docs/kbn_core_injected_metadata_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-injected-metadata-browser-mocks title: "@kbn/core-injected-metadata-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-injected-metadata-browser-mocks plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-injected-metadata-browser-mocks'] --- import kbnCoreInjectedMetadataBrowserMocksObj from './kbn_core_injected_metadata_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_integrations_browser_internal.mdx b/api_docs/kbn_core_integrations_browser_internal.mdx index 658320256e564..1470dceb97475 100644 --- a/api_docs/kbn_core_integrations_browser_internal.mdx +++ b/api_docs/kbn_core_integrations_browser_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-integrations-browser-internal title: "@kbn/core-integrations-browser-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-integrations-browser-internal plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-integrations-browser-internal'] --- import kbnCoreIntegrationsBrowserInternalObj from './kbn_core_integrations_browser_internal.devdocs.json'; diff --git a/api_docs/kbn_core_integrations_browser_mocks.mdx b/api_docs/kbn_core_integrations_browser_mocks.mdx index e7e7e70589955..b1007cdd878b1 100644 --- a/api_docs/kbn_core_integrations_browser_mocks.mdx +++ b/api_docs/kbn_core_integrations_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-integrations-browser-mocks title: "@kbn/core-integrations-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-integrations-browser-mocks plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-integrations-browser-mocks'] --- import kbnCoreIntegrationsBrowserMocksObj from './kbn_core_integrations_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_lifecycle_browser.mdx b/api_docs/kbn_core_lifecycle_browser.mdx index 01628e673aa98..d1e8a310cbd48 100644 --- a/api_docs/kbn_core_lifecycle_browser.mdx +++ b/api_docs/kbn_core_lifecycle_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-lifecycle-browser title: "@kbn/core-lifecycle-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-lifecycle-browser plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-lifecycle-browser'] --- import kbnCoreLifecycleBrowserObj from './kbn_core_lifecycle_browser.devdocs.json'; diff --git a/api_docs/kbn_core_lifecycle_browser_mocks.mdx b/api_docs/kbn_core_lifecycle_browser_mocks.mdx index 8ce5676b84941..50624f64bbbdb 100644 --- a/api_docs/kbn_core_lifecycle_browser_mocks.mdx +++ b/api_docs/kbn_core_lifecycle_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-lifecycle-browser-mocks title: "@kbn/core-lifecycle-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-lifecycle-browser-mocks plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-lifecycle-browser-mocks'] --- import kbnCoreLifecycleBrowserMocksObj from './kbn_core_lifecycle_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_lifecycle_server.mdx b/api_docs/kbn_core_lifecycle_server.mdx index 2070bb0d8439b..85b789abdec82 100644 --- a/api_docs/kbn_core_lifecycle_server.mdx +++ b/api_docs/kbn_core_lifecycle_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-lifecycle-server title: "@kbn/core-lifecycle-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-lifecycle-server plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-lifecycle-server'] --- import kbnCoreLifecycleServerObj from './kbn_core_lifecycle_server.devdocs.json'; diff --git a/api_docs/kbn_core_lifecycle_server_mocks.mdx b/api_docs/kbn_core_lifecycle_server_mocks.mdx index 87802fafb715e..1eeab2afa1890 100644 --- a/api_docs/kbn_core_lifecycle_server_mocks.mdx +++ b/api_docs/kbn_core_lifecycle_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-lifecycle-server-mocks title: "@kbn/core-lifecycle-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-lifecycle-server-mocks plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-lifecycle-server-mocks'] --- import kbnCoreLifecycleServerMocksObj from './kbn_core_lifecycle_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_logging_browser_mocks.mdx b/api_docs/kbn_core_logging_browser_mocks.mdx index d8599ad27e298..b9913f64c82de 100644 --- a/api_docs/kbn_core_logging_browser_mocks.mdx +++ b/api_docs/kbn_core_logging_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-logging-browser-mocks title: "@kbn/core-logging-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-logging-browser-mocks plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-logging-browser-mocks'] --- import kbnCoreLoggingBrowserMocksObj from './kbn_core_logging_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_logging_common_internal.mdx b/api_docs/kbn_core_logging_common_internal.mdx index 200ad9bb2b129..21952c6855467 100644 --- a/api_docs/kbn_core_logging_common_internal.mdx +++ b/api_docs/kbn_core_logging_common_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-logging-common-internal title: "@kbn/core-logging-common-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-logging-common-internal plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-logging-common-internal'] --- import kbnCoreLoggingCommonInternalObj from './kbn_core_logging_common_internal.devdocs.json'; diff --git a/api_docs/kbn_core_logging_server.mdx b/api_docs/kbn_core_logging_server.mdx index f269ea5b28642..265cd19274dd6 100644 --- a/api_docs/kbn_core_logging_server.mdx +++ b/api_docs/kbn_core_logging_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-logging-server title: "@kbn/core-logging-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-logging-server plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-logging-server'] --- import kbnCoreLoggingServerObj from './kbn_core_logging_server.devdocs.json'; diff --git a/api_docs/kbn_core_logging_server_internal.mdx b/api_docs/kbn_core_logging_server_internal.mdx index e5f1f8b340965..8685a560e55e1 100644 --- a/api_docs/kbn_core_logging_server_internal.mdx +++ b/api_docs/kbn_core_logging_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-logging-server-internal title: "@kbn/core-logging-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-logging-server-internal plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-logging-server-internal'] --- import kbnCoreLoggingServerInternalObj from './kbn_core_logging_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_logging_server_mocks.mdx b/api_docs/kbn_core_logging_server_mocks.mdx index c866cbeca6c43..cf39bf305fcc8 100644 --- a/api_docs/kbn_core_logging_server_mocks.mdx +++ b/api_docs/kbn_core_logging_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-logging-server-mocks title: "@kbn/core-logging-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-logging-server-mocks plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-logging-server-mocks'] --- import kbnCoreLoggingServerMocksObj from './kbn_core_logging_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_metrics_collectors_server_internal.mdx b/api_docs/kbn_core_metrics_collectors_server_internal.mdx index fef74f7a31c16..0633547cd0e3d 100644 --- a/api_docs/kbn_core_metrics_collectors_server_internal.mdx +++ b/api_docs/kbn_core_metrics_collectors_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-metrics-collectors-server-internal title: "@kbn/core-metrics-collectors-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-metrics-collectors-server-internal plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-metrics-collectors-server-internal'] --- import kbnCoreMetricsCollectorsServerInternalObj from './kbn_core_metrics_collectors_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_metrics_collectors_server_mocks.mdx b/api_docs/kbn_core_metrics_collectors_server_mocks.mdx index e154a035f5e49..9e25af476a8ed 100644 --- a/api_docs/kbn_core_metrics_collectors_server_mocks.mdx +++ b/api_docs/kbn_core_metrics_collectors_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-metrics-collectors-server-mocks title: "@kbn/core-metrics-collectors-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-metrics-collectors-server-mocks plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-metrics-collectors-server-mocks'] --- import kbnCoreMetricsCollectorsServerMocksObj from './kbn_core_metrics_collectors_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_metrics_server.mdx b/api_docs/kbn_core_metrics_server.mdx index 687ce60731be7..82cadc3e70ca5 100644 --- a/api_docs/kbn_core_metrics_server.mdx +++ b/api_docs/kbn_core_metrics_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-metrics-server title: "@kbn/core-metrics-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-metrics-server plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-metrics-server'] --- import kbnCoreMetricsServerObj from './kbn_core_metrics_server.devdocs.json'; diff --git a/api_docs/kbn_core_metrics_server_internal.mdx b/api_docs/kbn_core_metrics_server_internal.mdx index 2d6f75ae3af16..5c8f5d210fe3a 100644 --- a/api_docs/kbn_core_metrics_server_internal.mdx +++ b/api_docs/kbn_core_metrics_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-metrics-server-internal title: "@kbn/core-metrics-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-metrics-server-internal plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-metrics-server-internal'] --- import kbnCoreMetricsServerInternalObj from './kbn_core_metrics_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_metrics_server_mocks.mdx b/api_docs/kbn_core_metrics_server_mocks.mdx index 411a6c3194580..a92671fc66e6f 100644 --- a/api_docs/kbn_core_metrics_server_mocks.mdx +++ b/api_docs/kbn_core_metrics_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-metrics-server-mocks title: "@kbn/core-metrics-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-metrics-server-mocks plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-metrics-server-mocks'] --- import kbnCoreMetricsServerMocksObj from './kbn_core_metrics_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_mount_utils_browser.mdx b/api_docs/kbn_core_mount_utils_browser.mdx index 0cad214dc53b0..524a6ba0799ba 100644 --- a/api_docs/kbn_core_mount_utils_browser.mdx +++ b/api_docs/kbn_core_mount_utils_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-mount-utils-browser title: "@kbn/core-mount-utils-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-mount-utils-browser plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-mount-utils-browser'] --- import kbnCoreMountUtilsBrowserObj from './kbn_core_mount_utils_browser.devdocs.json'; diff --git a/api_docs/kbn_core_node_server.mdx b/api_docs/kbn_core_node_server.mdx index 134bcd77f9255..a534f7faac9c6 100644 --- a/api_docs/kbn_core_node_server.mdx +++ b/api_docs/kbn_core_node_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-node-server title: "@kbn/core-node-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-node-server plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-node-server'] --- import kbnCoreNodeServerObj from './kbn_core_node_server.devdocs.json'; diff --git a/api_docs/kbn_core_node_server_internal.mdx b/api_docs/kbn_core_node_server_internal.mdx index 57d26f4d58d23..dd6205e5f2370 100644 --- a/api_docs/kbn_core_node_server_internal.mdx +++ b/api_docs/kbn_core_node_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-node-server-internal title: "@kbn/core-node-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-node-server-internal plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-node-server-internal'] --- import kbnCoreNodeServerInternalObj from './kbn_core_node_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_node_server_mocks.mdx b/api_docs/kbn_core_node_server_mocks.mdx index 69e9f6f473de7..38e27111a9a2c 100644 --- a/api_docs/kbn_core_node_server_mocks.mdx +++ b/api_docs/kbn_core_node_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-node-server-mocks title: "@kbn/core-node-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-node-server-mocks plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-node-server-mocks'] --- import kbnCoreNodeServerMocksObj from './kbn_core_node_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_notifications_browser.mdx b/api_docs/kbn_core_notifications_browser.mdx index be9c165e72852..c1de6633c662c 100644 --- a/api_docs/kbn_core_notifications_browser.mdx +++ b/api_docs/kbn_core_notifications_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-notifications-browser title: "@kbn/core-notifications-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-notifications-browser plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-notifications-browser'] --- import kbnCoreNotificationsBrowserObj from './kbn_core_notifications_browser.devdocs.json'; diff --git a/api_docs/kbn_core_notifications_browser_internal.mdx b/api_docs/kbn_core_notifications_browser_internal.mdx index 43576511308a1..5ac8c462cbdf0 100644 --- a/api_docs/kbn_core_notifications_browser_internal.mdx +++ b/api_docs/kbn_core_notifications_browser_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-notifications-browser-internal title: "@kbn/core-notifications-browser-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-notifications-browser-internal plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-notifications-browser-internal'] --- import kbnCoreNotificationsBrowserInternalObj from './kbn_core_notifications_browser_internal.devdocs.json'; diff --git a/api_docs/kbn_core_notifications_browser_mocks.mdx b/api_docs/kbn_core_notifications_browser_mocks.mdx index 1426ba3f72119..a400d724ea5df 100644 --- a/api_docs/kbn_core_notifications_browser_mocks.mdx +++ b/api_docs/kbn_core_notifications_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-notifications-browser-mocks title: "@kbn/core-notifications-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-notifications-browser-mocks plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-notifications-browser-mocks'] --- import kbnCoreNotificationsBrowserMocksObj from './kbn_core_notifications_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_overlays_browser.mdx b/api_docs/kbn_core_overlays_browser.mdx index ad0340a226dba..ffd99cc42ad2c 100644 --- a/api_docs/kbn_core_overlays_browser.mdx +++ b/api_docs/kbn_core_overlays_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-overlays-browser title: "@kbn/core-overlays-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-overlays-browser plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-overlays-browser'] --- import kbnCoreOverlaysBrowserObj from './kbn_core_overlays_browser.devdocs.json'; diff --git a/api_docs/kbn_core_overlays_browser_internal.mdx b/api_docs/kbn_core_overlays_browser_internal.mdx index 881cc17dfc8d3..0e293392196ef 100644 --- a/api_docs/kbn_core_overlays_browser_internal.mdx +++ b/api_docs/kbn_core_overlays_browser_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-overlays-browser-internal title: "@kbn/core-overlays-browser-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-overlays-browser-internal plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-overlays-browser-internal'] --- import kbnCoreOverlaysBrowserInternalObj from './kbn_core_overlays_browser_internal.devdocs.json'; diff --git a/api_docs/kbn_core_overlays_browser_mocks.mdx b/api_docs/kbn_core_overlays_browser_mocks.mdx index 65d614274d23c..1cbc299038109 100644 --- a/api_docs/kbn_core_overlays_browser_mocks.mdx +++ b/api_docs/kbn_core_overlays_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-overlays-browser-mocks title: "@kbn/core-overlays-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-overlays-browser-mocks plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-overlays-browser-mocks'] --- import kbnCoreOverlaysBrowserMocksObj from './kbn_core_overlays_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_plugins_browser.mdx b/api_docs/kbn_core_plugins_browser.mdx index 486590747b307..5fc96cf686686 100644 --- a/api_docs/kbn_core_plugins_browser.mdx +++ b/api_docs/kbn_core_plugins_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-plugins-browser title: "@kbn/core-plugins-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-plugins-browser plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-plugins-browser'] --- import kbnCorePluginsBrowserObj from './kbn_core_plugins_browser.devdocs.json'; diff --git a/api_docs/kbn_core_plugins_browser_mocks.mdx b/api_docs/kbn_core_plugins_browser_mocks.mdx index 80c4336501dde..594d990bd860f 100644 --- a/api_docs/kbn_core_plugins_browser_mocks.mdx +++ b/api_docs/kbn_core_plugins_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-plugins-browser-mocks title: "@kbn/core-plugins-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-plugins-browser-mocks plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-plugins-browser-mocks'] --- import kbnCorePluginsBrowserMocksObj from './kbn_core_plugins_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_plugins_server.mdx b/api_docs/kbn_core_plugins_server.mdx index 1a58798b58f5e..752d43b5d06ac 100644 --- a/api_docs/kbn_core_plugins_server.mdx +++ b/api_docs/kbn_core_plugins_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-plugins-server title: "@kbn/core-plugins-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-plugins-server plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-plugins-server'] --- import kbnCorePluginsServerObj from './kbn_core_plugins_server.devdocs.json'; diff --git a/api_docs/kbn_core_plugins_server_mocks.mdx b/api_docs/kbn_core_plugins_server_mocks.mdx index 85dd22915e97b..924a88eaa189a 100644 --- a/api_docs/kbn_core_plugins_server_mocks.mdx +++ b/api_docs/kbn_core_plugins_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-plugins-server-mocks title: "@kbn/core-plugins-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-plugins-server-mocks plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-plugins-server-mocks'] --- import kbnCorePluginsServerMocksObj from './kbn_core_plugins_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_preboot_server.mdx b/api_docs/kbn_core_preboot_server.mdx index 4c9a82e81c1da..af54da408b8b8 100644 --- a/api_docs/kbn_core_preboot_server.mdx +++ b/api_docs/kbn_core_preboot_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-preboot-server title: "@kbn/core-preboot-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-preboot-server plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-preboot-server'] --- import kbnCorePrebootServerObj from './kbn_core_preboot_server.devdocs.json'; diff --git a/api_docs/kbn_core_preboot_server_mocks.mdx b/api_docs/kbn_core_preboot_server_mocks.mdx index e88c2f2068168..7161dd6089bc5 100644 --- a/api_docs/kbn_core_preboot_server_mocks.mdx +++ b/api_docs/kbn_core_preboot_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-preboot-server-mocks title: "@kbn/core-preboot-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-preboot-server-mocks plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-preboot-server-mocks'] --- import kbnCorePrebootServerMocksObj from './kbn_core_preboot_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_rendering_browser_mocks.mdx b/api_docs/kbn_core_rendering_browser_mocks.mdx index e02ef4903ea7d..98b3ada83b6f7 100644 --- a/api_docs/kbn_core_rendering_browser_mocks.mdx +++ b/api_docs/kbn_core_rendering_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-rendering-browser-mocks title: "@kbn/core-rendering-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-rendering-browser-mocks plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-rendering-browser-mocks'] --- import kbnCoreRenderingBrowserMocksObj from './kbn_core_rendering_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_rendering_server_internal.mdx b/api_docs/kbn_core_rendering_server_internal.mdx index 29bc8e333e375..8d99f5656f813 100644 --- a/api_docs/kbn_core_rendering_server_internal.mdx +++ b/api_docs/kbn_core_rendering_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-rendering-server-internal title: "@kbn/core-rendering-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-rendering-server-internal plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-rendering-server-internal'] --- import kbnCoreRenderingServerInternalObj from './kbn_core_rendering_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_rendering_server_mocks.mdx b/api_docs/kbn_core_rendering_server_mocks.mdx index 83b62504c2327..b1961c3ecd812 100644 --- a/api_docs/kbn_core_rendering_server_mocks.mdx +++ b/api_docs/kbn_core_rendering_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-rendering-server-mocks title: "@kbn/core-rendering-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-rendering-server-mocks plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-rendering-server-mocks'] --- import kbnCoreRenderingServerMocksObj from './kbn_core_rendering_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_root_server_internal.mdx b/api_docs/kbn_core_root_server_internal.mdx index eb8b31f3f11ed..52cd2903f08d8 100644 --- a/api_docs/kbn_core_root_server_internal.mdx +++ b/api_docs/kbn_core_root_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-root-server-internal title: "@kbn/core-root-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-root-server-internal plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-root-server-internal'] --- import kbnCoreRootServerInternalObj from './kbn_core_root_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_saved_objects_api_browser.mdx b/api_docs/kbn_core_saved_objects_api_browser.mdx index 8561a99f05387..a4b699f0e1925 100644 --- a/api_docs/kbn_core_saved_objects_api_browser.mdx +++ b/api_docs/kbn_core_saved_objects_api_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-saved-objects-api-browser title: "@kbn/core-saved-objects-api-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-saved-objects-api-browser plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-saved-objects-api-browser'] --- import kbnCoreSavedObjectsApiBrowserObj from './kbn_core_saved_objects_api_browser.devdocs.json'; diff --git a/api_docs/kbn_core_saved_objects_api_server.mdx b/api_docs/kbn_core_saved_objects_api_server.mdx index 501779f6c2ea3..2b7aa07349f6f 100644 --- a/api_docs/kbn_core_saved_objects_api_server.mdx +++ b/api_docs/kbn_core_saved_objects_api_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-saved-objects-api-server title: "@kbn/core-saved-objects-api-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-saved-objects-api-server plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-saved-objects-api-server'] --- import kbnCoreSavedObjectsApiServerObj from './kbn_core_saved_objects_api_server.devdocs.json'; diff --git a/api_docs/kbn_core_saved_objects_api_server_internal.mdx b/api_docs/kbn_core_saved_objects_api_server_internal.mdx index 34cba812441fe..c55a87a284e78 100644 --- a/api_docs/kbn_core_saved_objects_api_server_internal.mdx +++ b/api_docs/kbn_core_saved_objects_api_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-saved-objects-api-server-internal title: "@kbn/core-saved-objects-api-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-saved-objects-api-server-internal plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-saved-objects-api-server-internal'] --- import kbnCoreSavedObjectsApiServerInternalObj from './kbn_core_saved_objects_api_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_saved_objects_api_server_mocks.mdx b/api_docs/kbn_core_saved_objects_api_server_mocks.mdx index 409080180c525..450fdf0f3b9b3 100644 --- a/api_docs/kbn_core_saved_objects_api_server_mocks.mdx +++ b/api_docs/kbn_core_saved_objects_api_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-saved-objects-api-server-mocks title: "@kbn/core-saved-objects-api-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-saved-objects-api-server-mocks plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-saved-objects-api-server-mocks'] --- import kbnCoreSavedObjectsApiServerMocksObj from './kbn_core_saved_objects_api_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_saved_objects_base_server_internal.mdx b/api_docs/kbn_core_saved_objects_base_server_internal.mdx index bda13dad438cc..67582471ad545 100644 --- a/api_docs/kbn_core_saved_objects_base_server_internal.mdx +++ b/api_docs/kbn_core_saved_objects_base_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-saved-objects-base-server-internal title: "@kbn/core-saved-objects-base-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-saved-objects-base-server-internal plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-saved-objects-base-server-internal'] --- import kbnCoreSavedObjectsBaseServerInternalObj from './kbn_core_saved_objects_base_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_saved_objects_base_server_mocks.mdx b/api_docs/kbn_core_saved_objects_base_server_mocks.mdx index 2416dd47e943f..2b0b79781c939 100644 --- a/api_docs/kbn_core_saved_objects_base_server_mocks.mdx +++ b/api_docs/kbn_core_saved_objects_base_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-saved-objects-base-server-mocks title: "@kbn/core-saved-objects-base-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-saved-objects-base-server-mocks plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-saved-objects-base-server-mocks'] --- import kbnCoreSavedObjectsBaseServerMocksObj from './kbn_core_saved_objects_base_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_saved_objects_browser.mdx b/api_docs/kbn_core_saved_objects_browser.mdx index f30d0adb1a9d9..57541aa7881aa 100644 --- a/api_docs/kbn_core_saved_objects_browser.mdx +++ b/api_docs/kbn_core_saved_objects_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-saved-objects-browser title: "@kbn/core-saved-objects-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-saved-objects-browser plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-saved-objects-browser'] --- import kbnCoreSavedObjectsBrowserObj from './kbn_core_saved_objects_browser.devdocs.json'; diff --git a/api_docs/kbn_core_saved_objects_browser_internal.mdx b/api_docs/kbn_core_saved_objects_browser_internal.mdx index 4feba730c4f3c..dcba6934248c6 100644 --- a/api_docs/kbn_core_saved_objects_browser_internal.mdx +++ b/api_docs/kbn_core_saved_objects_browser_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-saved-objects-browser-internal title: "@kbn/core-saved-objects-browser-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-saved-objects-browser-internal plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-saved-objects-browser-internal'] --- import kbnCoreSavedObjectsBrowserInternalObj from './kbn_core_saved_objects_browser_internal.devdocs.json'; diff --git a/api_docs/kbn_core_saved_objects_browser_mocks.mdx b/api_docs/kbn_core_saved_objects_browser_mocks.mdx index 34cc60c606e68..6617d3cb0917b 100644 --- a/api_docs/kbn_core_saved_objects_browser_mocks.mdx +++ b/api_docs/kbn_core_saved_objects_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-saved-objects-browser-mocks title: "@kbn/core-saved-objects-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-saved-objects-browser-mocks plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-saved-objects-browser-mocks'] --- import kbnCoreSavedObjectsBrowserMocksObj from './kbn_core_saved_objects_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_saved_objects_common.devdocs.json b/api_docs/kbn_core_saved_objects_common.devdocs.json index b05750c4e35ab..7152e7fdd895a 100644 --- a/api_docs/kbn_core_saved_objects_common.devdocs.json +++ b/api_docs/kbn_core_saved_objects_common.devdocs.json @@ -668,11 +668,11 @@ }, { "plugin": "alerting", - "path": "x-pack/plugins/alerting/server/types.ts" + "path": "x-pack/plugins/alerting/server/rules_client/common/inject_references.ts" }, { "plugin": "alerting", - "path": "x-pack/plugins/alerting/server/types.ts" + "path": "x-pack/plugins/alerting/server/rules_client/common/inject_references.ts" }, { "plugin": "alerting", @@ -700,11 +700,11 @@ }, { "plugin": "alerting", - "path": "x-pack/plugins/alerting/server/rules_client/rules_client.ts" + "path": "x-pack/plugins/alerting/server/types.ts" }, { "plugin": "alerting", - "path": "x-pack/plugins/alerting/server/rules_client/rules_client.ts" + "path": "x-pack/plugins/alerting/server/types.ts" }, { "plugin": "canvas", diff --git a/api_docs/kbn_core_saved_objects_common.mdx b/api_docs/kbn_core_saved_objects_common.mdx index 9f0e8425b1c7e..cf290fbc0847d 100644 --- a/api_docs/kbn_core_saved_objects_common.mdx +++ b/api_docs/kbn_core_saved_objects_common.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-saved-objects-common title: "@kbn/core-saved-objects-common" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-saved-objects-common plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-saved-objects-common'] --- import kbnCoreSavedObjectsCommonObj from './kbn_core_saved_objects_common.devdocs.json'; diff --git a/api_docs/kbn_core_saved_objects_import_export_server_internal.mdx b/api_docs/kbn_core_saved_objects_import_export_server_internal.mdx index 3f7f0725ff7aa..046fc83286c77 100644 --- a/api_docs/kbn_core_saved_objects_import_export_server_internal.mdx +++ b/api_docs/kbn_core_saved_objects_import_export_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-saved-objects-import-export-server-internal title: "@kbn/core-saved-objects-import-export-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-saved-objects-import-export-server-internal plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-saved-objects-import-export-server-internal'] --- import kbnCoreSavedObjectsImportExportServerInternalObj from './kbn_core_saved_objects_import_export_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_saved_objects_import_export_server_mocks.mdx b/api_docs/kbn_core_saved_objects_import_export_server_mocks.mdx index fe726a91ddd92..bbdd4ae995b98 100644 --- a/api_docs/kbn_core_saved_objects_import_export_server_mocks.mdx +++ b/api_docs/kbn_core_saved_objects_import_export_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-saved-objects-import-export-server-mocks title: "@kbn/core-saved-objects-import-export-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-saved-objects-import-export-server-mocks plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-saved-objects-import-export-server-mocks'] --- import kbnCoreSavedObjectsImportExportServerMocksObj from './kbn_core_saved_objects_import_export_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_saved_objects_migration_server_internal.devdocs.json b/api_docs/kbn_core_saved_objects_migration_server_internal.devdocs.json index 3ebff3e152f4c..8db7b0244e6a6 100644 --- a/api_docs/kbn_core_saved_objects_migration_server_internal.devdocs.json +++ b/api_docs/kbn_core_saved_objects_migration_server_internal.devdocs.json @@ -1675,6 +1675,47 @@ "returnComment": [], "initialIsOpen": false }, + { + "parentPluginId": "@kbn/core-saved-objects-migration-server-internal", + "id": "def-server.updateTargetMappingsMeta", + "type": "Function", + "tags": [], + "label": "updateTargetMappingsMeta", + "description": [ + "\nUpdates an index's mappings _meta information" + ], + "signature": [ + "({ client, index, meta, }: ", + "UpdateTargetMappingsMetaParams", + ") => ", + "TaskEither", + "<", + "RetryableEsClientError", + ", \"update_mappings_meta_succeeded\">" + ], + "path": "packages/core/saved-objects/core-saved-objects-migration-server-internal/src/actions/update_target_mappings_meta.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/core-saved-objects-migration-server-internal", + "id": "def-server.updateTargetMappingsMeta.$1", + "type": "Object", + "tags": [], + "label": "{\n client,\n index,\n meta,\n }", + "description": [], + "signature": [ + "UpdateTargetMappingsMetaParams" + ], + "path": "packages/core/saved-objects/core-saved-objects-migration-server-internal/src/actions/update_target_mappings_meta.ts", + "deprecated": false, + "trackAdoption": false, + "isRequired": true + } + ], + "returnComment": [], + "initialIsOpen": false + }, { "parentPluginId": "@kbn/core-saved-objects-migration-server-internal", "id": "def-server.waitForIndexStatus", diff --git a/api_docs/kbn_core_saved_objects_migration_server_internal.mdx b/api_docs/kbn_core_saved_objects_migration_server_internal.mdx index 800633c5281c8..1ce539d23e5cb 100644 --- a/api_docs/kbn_core_saved_objects_migration_server_internal.mdx +++ b/api_docs/kbn_core_saved_objects_migration_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-saved-objects-migration-server-internal title: "@kbn/core-saved-objects-migration-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-saved-objects-migration-server-internal plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-saved-objects-migration-server-internal'] --- import kbnCoreSavedObjectsMigrationServerInternalObj from './kbn_core_saved_objects_migration_server_internal.devdocs.json'; @@ -21,7 +21,7 @@ Contact Kibana Core for questions regarding this plugin. | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 110 | 0 | 78 | 44 | +| 112 | 0 | 79 | 45 | ## Server diff --git a/api_docs/kbn_core_saved_objects_migration_server_mocks.mdx b/api_docs/kbn_core_saved_objects_migration_server_mocks.mdx index f103d9caa1cbf..b58a03abf12f8 100644 --- a/api_docs/kbn_core_saved_objects_migration_server_mocks.mdx +++ b/api_docs/kbn_core_saved_objects_migration_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-saved-objects-migration-server-mocks title: "@kbn/core-saved-objects-migration-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-saved-objects-migration-server-mocks plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-saved-objects-migration-server-mocks'] --- import kbnCoreSavedObjectsMigrationServerMocksObj from './kbn_core_saved_objects_migration_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_saved_objects_server.mdx b/api_docs/kbn_core_saved_objects_server.mdx index 83a0f6a5e1d48..a2d91cb312168 100644 --- a/api_docs/kbn_core_saved_objects_server.mdx +++ b/api_docs/kbn_core_saved_objects_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-saved-objects-server title: "@kbn/core-saved-objects-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-saved-objects-server plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-saved-objects-server'] --- import kbnCoreSavedObjectsServerObj from './kbn_core_saved_objects_server.devdocs.json'; diff --git a/api_docs/kbn_core_saved_objects_server_internal.mdx b/api_docs/kbn_core_saved_objects_server_internal.mdx index 201a65f339c41..b8b1440cb9e51 100644 --- a/api_docs/kbn_core_saved_objects_server_internal.mdx +++ b/api_docs/kbn_core_saved_objects_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-saved-objects-server-internal title: "@kbn/core-saved-objects-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-saved-objects-server-internal plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-saved-objects-server-internal'] --- import kbnCoreSavedObjectsServerInternalObj from './kbn_core_saved_objects_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_saved_objects_server_mocks.mdx b/api_docs/kbn_core_saved_objects_server_mocks.mdx index 7d63588ad0e5b..6beb73f5ea5f2 100644 --- a/api_docs/kbn_core_saved_objects_server_mocks.mdx +++ b/api_docs/kbn_core_saved_objects_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-saved-objects-server-mocks title: "@kbn/core-saved-objects-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-saved-objects-server-mocks plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-saved-objects-server-mocks'] --- import kbnCoreSavedObjectsServerMocksObj from './kbn_core_saved_objects_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_saved_objects_utils_server.mdx b/api_docs/kbn_core_saved_objects_utils_server.mdx index a7bd747a5bb0f..0d0a6ed6b5641 100644 --- a/api_docs/kbn_core_saved_objects_utils_server.mdx +++ b/api_docs/kbn_core_saved_objects_utils_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-saved-objects-utils-server title: "@kbn/core-saved-objects-utils-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-saved-objects-utils-server plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-saved-objects-utils-server'] --- import kbnCoreSavedObjectsUtilsServerObj from './kbn_core_saved_objects_utils_server.devdocs.json'; diff --git a/api_docs/kbn_core_status_common.mdx b/api_docs/kbn_core_status_common.mdx index 09a1b41ff0ee2..172df432d6b25 100644 --- a/api_docs/kbn_core_status_common.mdx +++ b/api_docs/kbn_core_status_common.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-status-common title: "@kbn/core-status-common" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-status-common plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-status-common'] --- import kbnCoreStatusCommonObj from './kbn_core_status_common.devdocs.json'; diff --git a/api_docs/kbn_core_status_common_internal.mdx b/api_docs/kbn_core_status_common_internal.mdx index 32786578cc83f..4c6131b8516c1 100644 --- a/api_docs/kbn_core_status_common_internal.mdx +++ b/api_docs/kbn_core_status_common_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-status-common-internal title: "@kbn/core-status-common-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-status-common-internal plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-status-common-internal'] --- import kbnCoreStatusCommonInternalObj from './kbn_core_status_common_internal.devdocs.json'; diff --git a/api_docs/kbn_core_status_server.mdx b/api_docs/kbn_core_status_server.mdx index d6c8321fd3c3d..92a3c9e57cc03 100644 --- a/api_docs/kbn_core_status_server.mdx +++ b/api_docs/kbn_core_status_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-status-server title: "@kbn/core-status-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-status-server plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-status-server'] --- import kbnCoreStatusServerObj from './kbn_core_status_server.devdocs.json'; diff --git a/api_docs/kbn_core_status_server_internal.mdx b/api_docs/kbn_core_status_server_internal.mdx index deb469f6b732f..5eaec4116d746 100644 --- a/api_docs/kbn_core_status_server_internal.mdx +++ b/api_docs/kbn_core_status_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-status-server-internal title: "@kbn/core-status-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-status-server-internal plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-status-server-internal'] --- import kbnCoreStatusServerInternalObj from './kbn_core_status_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_status_server_mocks.mdx b/api_docs/kbn_core_status_server_mocks.mdx index 8441cf7a34e86..a6345b834e246 100644 --- a/api_docs/kbn_core_status_server_mocks.mdx +++ b/api_docs/kbn_core_status_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-status-server-mocks title: "@kbn/core-status-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-status-server-mocks plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-status-server-mocks'] --- import kbnCoreStatusServerMocksObj from './kbn_core_status_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_test_helpers_deprecations_getters.mdx b/api_docs/kbn_core_test_helpers_deprecations_getters.mdx index b4ca0a1fafed6..9ef1ba5516ed2 100644 --- a/api_docs/kbn_core_test_helpers_deprecations_getters.mdx +++ b/api_docs/kbn_core_test_helpers_deprecations_getters.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-test-helpers-deprecations-getters title: "@kbn/core-test-helpers-deprecations-getters" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-test-helpers-deprecations-getters plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-test-helpers-deprecations-getters'] --- import kbnCoreTestHelpersDeprecationsGettersObj from './kbn_core_test_helpers_deprecations_getters.devdocs.json'; diff --git a/api_docs/kbn_core_test_helpers_http_setup_browser.mdx b/api_docs/kbn_core_test_helpers_http_setup_browser.mdx index 8acf04307d6ed..da490735f5d19 100644 --- a/api_docs/kbn_core_test_helpers_http_setup_browser.mdx +++ b/api_docs/kbn_core_test_helpers_http_setup_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-test-helpers-http-setup-browser title: "@kbn/core-test-helpers-http-setup-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-test-helpers-http-setup-browser plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-test-helpers-http-setup-browser'] --- import kbnCoreTestHelpersHttpSetupBrowserObj from './kbn_core_test_helpers_http_setup_browser.devdocs.json'; diff --git a/api_docs/kbn_core_test_helpers_kbn_server.mdx b/api_docs/kbn_core_test_helpers_kbn_server.mdx index 5623e07b55598..0213334b956a8 100644 --- a/api_docs/kbn_core_test_helpers_kbn_server.mdx +++ b/api_docs/kbn_core_test_helpers_kbn_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-test-helpers-kbn-server title: "@kbn/core-test-helpers-kbn-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-test-helpers-kbn-server plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-test-helpers-kbn-server'] --- import kbnCoreTestHelpersKbnServerObj from './kbn_core_test_helpers_kbn_server.devdocs.json'; diff --git a/api_docs/kbn_core_test_helpers_so_type_serializer.mdx b/api_docs/kbn_core_test_helpers_so_type_serializer.mdx index b112408a82cb0..281fa22114d0d 100644 --- a/api_docs/kbn_core_test_helpers_so_type_serializer.mdx +++ b/api_docs/kbn_core_test_helpers_so_type_serializer.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-test-helpers-so-type-serializer title: "@kbn/core-test-helpers-so-type-serializer" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-test-helpers-so-type-serializer plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-test-helpers-so-type-serializer'] --- import kbnCoreTestHelpersSoTypeSerializerObj from './kbn_core_test_helpers_so_type_serializer.devdocs.json'; diff --git a/api_docs/kbn_core_test_helpers_test_utils.mdx b/api_docs/kbn_core_test_helpers_test_utils.mdx index 20ec2e672e876..6b8f435287bc4 100644 --- a/api_docs/kbn_core_test_helpers_test_utils.mdx +++ b/api_docs/kbn_core_test_helpers_test_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-test-helpers-test-utils title: "@kbn/core-test-helpers-test-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-test-helpers-test-utils plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-test-helpers-test-utils'] --- import kbnCoreTestHelpersTestUtilsObj from './kbn_core_test_helpers_test_utils.devdocs.json'; diff --git a/api_docs/kbn_core_theme_browser.mdx b/api_docs/kbn_core_theme_browser.mdx index 6e91a4f7e12e9..6c2d9b8fe5398 100644 --- a/api_docs/kbn_core_theme_browser.mdx +++ b/api_docs/kbn_core_theme_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-theme-browser title: "@kbn/core-theme-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-theme-browser plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-theme-browser'] --- import kbnCoreThemeBrowserObj from './kbn_core_theme_browser.devdocs.json'; diff --git a/api_docs/kbn_core_theme_browser_internal.mdx b/api_docs/kbn_core_theme_browser_internal.mdx index 7d33b7e0fcc90..a5b579a6409a2 100644 --- a/api_docs/kbn_core_theme_browser_internal.mdx +++ b/api_docs/kbn_core_theme_browser_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-theme-browser-internal title: "@kbn/core-theme-browser-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-theme-browser-internal plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-theme-browser-internal'] --- import kbnCoreThemeBrowserInternalObj from './kbn_core_theme_browser_internal.devdocs.json'; diff --git a/api_docs/kbn_core_theme_browser_mocks.mdx b/api_docs/kbn_core_theme_browser_mocks.mdx index 795d47756beba..c358d75875604 100644 --- a/api_docs/kbn_core_theme_browser_mocks.mdx +++ b/api_docs/kbn_core_theme_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-theme-browser-mocks title: "@kbn/core-theme-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-theme-browser-mocks plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-theme-browser-mocks'] --- import kbnCoreThemeBrowserMocksObj from './kbn_core_theme_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_ui_settings_browser.mdx b/api_docs/kbn_core_ui_settings_browser.mdx index 80d4a96114207..7d2b54fb1e786 100644 --- a/api_docs/kbn_core_ui_settings_browser.mdx +++ b/api_docs/kbn_core_ui_settings_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-ui-settings-browser title: "@kbn/core-ui-settings-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-ui-settings-browser plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-ui-settings-browser'] --- import kbnCoreUiSettingsBrowserObj from './kbn_core_ui_settings_browser.devdocs.json'; diff --git a/api_docs/kbn_core_ui_settings_browser_internal.mdx b/api_docs/kbn_core_ui_settings_browser_internal.mdx index 32d19e4d4a45a..d353ce845f2dc 100644 --- a/api_docs/kbn_core_ui_settings_browser_internal.mdx +++ b/api_docs/kbn_core_ui_settings_browser_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-ui-settings-browser-internal title: "@kbn/core-ui-settings-browser-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-ui-settings-browser-internal plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-ui-settings-browser-internal'] --- import kbnCoreUiSettingsBrowserInternalObj from './kbn_core_ui_settings_browser_internal.devdocs.json'; diff --git a/api_docs/kbn_core_ui_settings_browser_mocks.mdx b/api_docs/kbn_core_ui_settings_browser_mocks.mdx index f14d590632951..2188bab8a59b2 100644 --- a/api_docs/kbn_core_ui_settings_browser_mocks.mdx +++ b/api_docs/kbn_core_ui_settings_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-ui-settings-browser-mocks title: "@kbn/core-ui-settings-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-ui-settings-browser-mocks plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-ui-settings-browser-mocks'] --- import kbnCoreUiSettingsBrowserMocksObj from './kbn_core_ui_settings_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_ui_settings_common.devdocs.json b/api_docs/kbn_core_ui_settings_common.devdocs.json index 9a3b58162141f..603bbdb0bb853 100644 --- a/api_docs/kbn_core_ui_settings_common.devdocs.json +++ b/api_docs/kbn_core_ui_settings_common.devdocs.json @@ -347,6 +347,29 @@ "path": "src/plugins/discover/server/ui_settings.ts" } ] + }, + { + "parentPluginId": "@kbn/core-ui-settings-common", + "id": "def-common.UiSettingsParams.scope", + "type": "CompoundType", + "tags": [], + "label": "scope", + "description": [ + "\nScope of the setting. `Global` denotes a setting globally available across namespaces. `Namespace` denotes a setting\nscoped to a namespace. The default value is 'namespace'" + ], + "signature": [ + { + "pluginId": "@kbn/core-ui-settings-common", + "scope": "common", + "docId": "kibKbnCoreUiSettingsCommonPluginApi", + "section": "def-common.UiSettingsScope", + "text": "UiSettingsScope" + }, + " | undefined" + ], + "path": "packages/core/ui-settings/core-ui-settings-common/src/ui_settings.ts", + "deprecated": false, + "trackAdoption": false } ], "initialIsOpen": false @@ -434,7 +457,32 @@ "section": "def-common.DeprecationSettings", "text": "DeprecationSettings" }, - " | undefined; order?: number | undefined; metric?: { type: string; name: string; } | undefined; }" + " | undefined; order?: number | undefined; metric?: { type: string; name: string; } | undefined; scope?: ", + { + "pluginId": "@kbn/core-ui-settings-common", + "scope": "common", + "docId": "kibKbnCoreUiSettingsCommonPluginApi", + "section": "def-common.UiSettingsScope", + "text": "UiSettingsScope" + }, + " | undefined; }" + ], + "path": "packages/core/ui-settings/core-ui-settings-common/src/ui_settings.ts", + "deprecated": false, + "trackAdoption": false, + "initialIsOpen": false + }, + { + "parentPluginId": "@kbn/core-ui-settings-common", + "id": "def-common.UiSettingsScope", + "type": "Type", + "tags": [], + "label": "UiSettingsScope", + "description": [ + "\nDenotes the scope of the setting" + ], + "signature": [ + "\"namespace\" | \"global\"" ], "path": "packages/core/ui-settings/core-ui-settings-common/src/ui_settings.ts", "deprecated": false, diff --git a/api_docs/kbn_core_ui_settings_common.mdx b/api_docs/kbn_core_ui_settings_common.mdx index bc120e6713c5f..f2719144ca127 100644 --- a/api_docs/kbn_core_ui_settings_common.mdx +++ b/api_docs/kbn_core_ui_settings_common.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-ui-settings-common title: "@kbn/core-ui-settings-common" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-ui-settings-common plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-ui-settings-common'] --- import kbnCoreUiSettingsCommonObj from './kbn_core_ui_settings_common.devdocs.json'; @@ -21,7 +21,7 @@ Contact Kibana Core for questions regarding this plugin. | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 23 | 0 | 3 | 0 | +| 25 | 0 | 3 | 0 | ## Common diff --git a/api_docs/kbn_core_ui_settings_server.mdx b/api_docs/kbn_core_ui_settings_server.mdx index 40b74ff79c708..0a3e003b4cbf9 100644 --- a/api_docs/kbn_core_ui_settings_server.mdx +++ b/api_docs/kbn_core_ui_settings_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-ui-settings-server title: "@kbn/core-ui-settings-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-ui-settings-server plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-ui-settings-server'] --- import kbnCoreUiSettingsServerObj from './kbn_core_ui_settings_server.devdocs.json'; diff --git a/api_docs/kbn_core_ui_settings_server_internal.devdocs.json b/api_docs/kbn_core_ui_settings_server_internal.devdocs.json index 81c6e14698d39..001529fd6484e 100644 --- a/api_docs/kbn_core_ui_settings_server_internal.devdocs.json +++ b/api_docs/kbn_core_ui_settings_server_internal.devdocs.json @@ -26,9 +26,9 @@ "text": "UiSettingsClient" }, " extends ", - "BaseUiSettingsClient" + "UiSettingsClientCommon" ], - "path": "packages/core/ui-settings/core-ui-settings-server-internal/src/ui_settings_client.ts", + "path": "packages/core/ui-settings/core-ui-settings-server-internal/src/clients/ui_settings_client.ts", "deprecated": false, "trackAdoption": false, "children": [ @@ -42,7 +42,7 @@ "signature": [ "any" ], - "path": "packages/core/ui-settings/core-ui-settings-server-internal/src/ui_settings_client.ts", + "path": "packages/core/ui-settings/core-ui-settings-server-internal/src/clients/ui_settings_client.ts", "deprecated": false, "trackAdoption": false, "children": [ @@ -54,63 +54,68 @@ "label": "options", "description": [], "signature": [ - { - "pluginId": "@kbn/core-ui-settings-server-internal", - "scope": "server", - "docId": "kibKbnCoreUiSettingsServerInternalPluginApi", - "section": "def-server.UiSettingsServiceOptions", - "text": "UiSettingsServiceOptions" - } + "UiSettingsServiceOptions" ], - "path": "packages/core/ui-settings/core-ui-settings-server-internal/src/ui_settings_client.ts", + "path": "packages/core/ui-settings/core-ui-settings-server-internal/src/clients/ui_settings_client.ts", "deprecated": false, "trackAdoption": false, "isRequired": true } ], "returnComment": [] - }, + } + ], + "initialIsOpen": false + }, + { + "parentPluginId": "@kbn/core-ui-settings-server-internal", + "id": "def-server.UiSettingsGlobalClient", + "type": "Class", + "tags": [], + "label": "UiSettingsGlobalClient", + "description": [ + "\nGlobal UiSettingsClient" + ], + "signature": [ { - "parentPluginId": "@kbn/core-ui-settings-server-internal", - "id": "def-server.UiSettingsClient.getUserProvided", - "type": "Function", - "tags": [], - "label": "getUserProvided", - "description": [], - "signature": [ - "() => Promise>" - ], - "path": "packages/core/ui-settings/core-ui-settings-server-internal/src/ui_settings_client.ts", - "deprecated": false, - "trackAdoption": false, - "children": [], - "returnComment": [] + "pluginId": "@kbn/core-ui-settings-server-internal", + "scope": "server", + "docId": "kibKbnCoreUiSettingsServerInternalPluginApi", + "section": "def-server.UiSettingsGlobalClient", + "text": "UiSettingsGlobalClient" }, + " extends ", + "UiSettingsClientCommon" + ], + "path": "packages/core/ui-settings/core-ui-settings-server-internal/src/clients/ui_settings_global_client.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ { "parentPluginId": "@kbn/core-ui-settings-server-internal", - "id": "def-server.UiSettingsClient.setMany", + "id": "def-server.UiSettingsGlobalClient.Unnamed", "type": "Function", "tags": [], - "label": "setMany", + "label": "Constructor", "description": [], "signature": [ - "(changes: Record) => Promise" + "any" ], - "path": "packages/core/ui-settings/core-ui-settings-server-internal/src/ui_settings_client.ts", + "path": "packages/core/ui-settings/core-ui-settings-server-internal/src/clients/ui_settings_global_client.ts", "deprecated": false, "trackAdoption": false, "children": [ { "parentPluginId": "@kbn/core-ui-settings-server-internal", - "id": "def-server.UiSettingsClient.setMany.$1", + "id": "def-server.UiSettingsGlobalClient.Unnamed.$1", "type": "Object", "tags": [], - "label": "changes", + "label": "options", "description": [], "signature": [ - "Record" + "UiSettingsServiceOptions" ], - "path": "packages/core/ui-settings/core-ui-settings-server-internal/src/ui_settings_client.ts", + "path": "packages/core/ui-settings/core-ui-settings-server-internal/src/clients/ui_settings_global_client.ts", "deprecated": false, "trackAdoption": false, "isRequired": true @@ -120,44 +125,29 @@ }, { "parentPluginId": "@kbn/core-ui-settings-server-internal", - "id": "def-server.UiSettingsClient.set", + "id": "def-server.UiSettingsGlobalClient.setMany", "type": "Function", "tags": [], - "label": "set", + "label": "setMany", "description": [], "signature": [ - "(key: string, value: any) => Promise" + "(changes: Record) => Promise" ], - "path": "packages/core/ui-settings/core-ui-settings-server-internal/src/ui_settings_client.ts", + "path": "packages/core/ui-settings/core-ui-settings-server-internal/src/clients/ui_settings_global_client.ts", "deprecated": false, "trackAdoption": false, "children": [ { "parentPluginId": "@kbn/core-ui-settings-server-internal", - "id": "def-server.UiSettingsClient.set.$1", - "type": "string", - "tags": [], - "label": "key", - "description": [], - "signature": [ - "string" - ], - "path": "packages/core/ui-settings/core-ui-settings-server-internal/src/ui_settings_client.ts", - "deprecated": false, - "trackAdoption": false, - "isRequired": true - }, - { - "parentPluginId": "@kbn/core-ui-settings-server-internal", - "id": "def-server.UiSettingsClient.set.$2", - "type": "Any", + "id": "def-server.UiSettingsGlobalClient.setMany.$1", + "type": "Object", "tags": [], - "label": "value", + "label": "changes", "description": [], "signature": [ - "any" + "Record" ], - "path": "packages/core/ui-settings/core-ui-settings-server-internal/src/ui_settings_client.ts", + "path": "packages/core/ui-settings/core-ui-settings-server-internal/src/clients/ui_settings_global_client.ts", "deprecated": false, "trackAdoption": false, "isRequired": true @@ -167,21 +157,21 @@ }, { "parentPluginId": "@kbn/core-ui-settings-server-internal", - "id": "def-server.UiSettingsClient.remove", + "id": "def-server.UiSettingsGlobalClient.set", "type": "Function", "tags": [], - "label": "remove", + "label": "set", "description": [], "signature": [ - "(key: string) => Promise" + "(key: string, value: any) => Promise" ], - "path": "packages/core/ui-settings/core-ui-settings-server-internal/src/ui_settings_client.ts", + "path": "packages/core/ui-settings/core-ui-settings-server-internal/src/clients/ui_settings_global_client.ts", "deprecated": false, "trackAdoption": false, "children": [ { "parentPluginId": "@kbn/core-ui-settings-server-internal", - "id": "def-server.UiSettingsClient.remove.$1", + "id": "def-server.UiSettingsGlobalClient.set.$1", "type": "string", "tags": [], "label": "key", @@ -189,39 +179,22 @@ "signature": [ "string" ], - "path": "packages/core/ui-settings/core-ui-settings-server-internal/src/ui_settings_client.ts", + "path": "packages/core/ui-settings/core-ui-settings-server-internal/src/clients/ui_settings_global_client.ts", "deprecated": false, "trackAdoption": false, "isRequired": true - } - ], - "returnComment": [] - }, - { - "parentPluginId": "@kbn/core-ui-settings-server-internal", - "id": "def-server.UiSettingsClient.removeMany", - "type": "Function", - "tags": [], - "label": "removeMany", - "description": [], - "signature": [ - "(keys: string[]) => Promise" - ], - "path": "packages/core/ui-settings/core-ui-settings-server-internal/src/ui_settings_client.ts", - "deprecated": false, - "trackAdoption": false, - "children": [ + }, { "parentPluginId": "@kbn/core-ui-settings-server-internal", - "id": "def-server.UiSettingsClient.removeMany.$1", - "type": "Array", + "id": "def-server.UiSettingsGlobalClient.set.$2", + "type": "Any", "tags": [], - "label": "keys", + "label": "value", "description": [], "signature": [ - "string[]" + "any" ], - "path": "packages/core/ui-settings/core-ui-settings-server-internal/src/ui_settings_client.ts", + "path": "packages/core/ui-settings/core-ui-settings-server-internal/src/clients/ui_settings_global_client.ts", "deprecated": false, "trackAdoption": false, "isRequired": true @@ -268,131 +241,7 @@ "initialIsOpen": false } ], - "interfaces": [ - { - "parentPluginId": "@kbn/core-ui-settings-server-internal", - "id": "def-server.UiSettingsServiceOptions", - "type": "Interface", - "tags": [], - "label": "UiSettingsServiceOptions", - "description": [], - "path": "packages/core/ui-settings/core-ui-settings-server-internal/src/ui_settings_client.ts", - "deprecated": false, - "trackAdoption": false, - "children": [ - { - "parentPluginId": "@kbn/core-ui-settings-server-internal", - "id": "def-server.UiSettingsServiceOptions.type", - "type": "string", - "tags": [], - "label": "type", - "description": [], - "path": "packages/core/ui-settings/core-ui-settings-server-internal/src/ui_settings_client.ts", - "deprecated": false, - "trackAdoption": false - }, - { - "parentPluginId": "@kbn/core-ui-settings-server-internal", - "id": "def-server.UiSettingsServiceOptions.id", - "type": "string", - "tags": [], - "label": "id", - "description": [], - "path": "packages/core/ui-settings/core-ui-settings-server-internal/src/ui_settings_client.ts", - "deprecated": false, - "trackAdoption": false - }, - { - "parentPluginId": "@kbn/core-ui-settings-server-internal", - "id": "def-server.UiSettingsServiceOptions.buildNum", - "type": "number", - "tags": [], - "label": "buildNum", - "description": [], - "path": "packages/core/ui-settings/core-ui-settings-server-internal/src/ui_settings_client.ts", - "deprecated": false, - "trackAdoption": false - }, - { - "parentPluginId": "@kbn/core-ui-settings-server-internal", - "id": "def-server.UiSettingsServiceOptions.savedObjectsClient", - "type": "Object", - "tags": [], - "label": "savedObjectsClient", - "description": [], - "signature": [ - { - "pluginId": "@kbn/core-saved-objects-api-server", - "scope": "server", - "docId": "kibKbnCoreSavedObjectsApiServerPluginApi", - "section": "def-server.SavedObjectsClientContract", - "text": "SavedObjectsClientContract" - } - ], - "path": "packages/core/ui-settings/core-ui-settings-server-internal/src/ui_settings_client.ts", - "deprecated": false, - "trackAdoption": false - }, - { - "parentPluginId": "@kbn/core-ui-settings-server-internal", - "id": "def-server.UiSettingsServiceOptions.overrides", - "type": "Object", - "tags": [], - "label": "overrides", - "description": [], - "signature": [ - "Record | undefined" - ], - "path": "packages/core/ui-settings/core-ui-settings-server-internal/src/ui_settings_client.ts", - "deprecated": false, - "trackAdoption": false - }, - { - "parentPluginId": "@kbn/core-ui-settings-server-internal", - "id": "def-server.UiSettingsServiceOptions.defaults", - "type": "Object", - "tags": [], - "label": "defaults", - "description": [], - "signature": [ - "Record> | undefined" - ], - "path": "packages/core/ui-settings/core-ui-settings-server-internal/src/ui_settings_client.ts", - "deprecated": false, - "trackAdoption": false - }, - { - "parentPluginId": "@kbn/core-ui-settings-server-internal", - "id": "def-server.UiSettingsServiceOptions.log", - "type": "Object", - "tags": [], - "label": "log", - "description": [], - "signature": [ - { - "pluginId": "@kbn/logging", - "scope": "server", - "docId": "kibKbnLoggingPluginApi", - "section": "def-server.Logger", - "text": "Logger" - } - ], - "path": "packages/core/ui-settings/core-ui-settings-server-internal/src/ui_settings_client.ts", - "deprecated": false, - "trackAdoption": false - } - ], - "initialIsOpen": false - } - ], + "interfaces": [], "enums": [], "misc": [], "objects": [ diff --git a/api_docs/kbn_core_ui_settings_server_internal.mdx b/api_docs/kbn_core_ui_settings_server_internal.mdx index 028112f8f6a8d..9fdf4a8219f12 100644 --- a/api_docs/kbn_core_ui_settings_server_internal.mdx +++ b/api_docs/kbn_core_ui_settings_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-ui-settings-server-internal title: "@kbn/core-ui-settings-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-ui-settings-server-internal plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-ui-settings-server-internal'] --- import kbnCoreUiSettingsServerInternalObj from './kbn_core_ui_settings_server_internal.devdocs.json'; @@ -21,7 +21,7 @@ Contact Kibana Core for questions regarding this plugin. | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 28 | 1 | 28 | 2 | +| 18 | 1 | 17 | 3 | ## Server @@ -34,6 +34,3 @@ Contact Kibana Core for questions regarding this plugin. ### Classes -### Interfaces - - diff --git a/api_docs/kbn_core_ui_settings_server_mocks.mdx b/api_docs/kbn_core_ui_settings_server_mocks.mdx index f1accbfe59173..449e9c0f401c6 100644 --- a/api_docs/kbn_core_ui_settings_server_mocks.mdx +++ b/api_docs/kbn_core_ui_settings_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-ui-settings-server-mocks title: "@kbn/core-ui-settings-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-ui-settings-server-mocks plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-ui-settings-server-mocks'] --- import kbnCoreUiSettingsServerMocksObj from './kbn_core_ui_settings_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_usage_data_server.mdx b/api_docs/kbn_core_usage_data_server.mdx index f3b97362da5ce..a21338ea0b94b 100644 --- a/api_docs/kbn_core_usage_data_server.mdx +++ b/api_docs/kbn_core_usage_data_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-usage-data-server title: "@kbn/core-usage-data-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-usage-data-server plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-usage-data-server'] --- import kbnCoreUsageDataServerObj from './kbn_core_usage_data_server.devdocs.json'; diff --git a/api_docs/kbn_core_usage_data_server_internal.mdx b/api_docs/kbn_core_usage_data_server_internal.mdx index 592a7d750d157..f9a3628309597 100644 --- a/api_docs/kbn_core_usage_data_server_internal.mdx +++ b/api_docs/kbn_core_usage_data_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-usage-data-server-internal title: "@kbn/core-usage-data-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-usage-data-server-internal plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-usage-data-server-internal'] --- import kbnCoreUsageDataServerInternalObj from './kbn_core_usage_data_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_usage_data_server_mocks.mdx b/api_docs/kbn_core_usage_data_server_mocks.mdx index 90aa9b0baaa56..a6d403d73d330 100644 --- a/api_docs/kbn_core_usage_data_server_mocks.mdx +++ b/api_docs/kbn_core_usage_data_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-usage-data-server-mocks title: "@kbn/core-usage-data-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-usage-data-server-mocks plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-usage-data-server-mocks'] --- import kbnCoreUsageDataServerMocksObj from './kbn_core_usage_data_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_crypto.mdx b/api_docs/kbn_crypto.mdx index 40fc59a9626cc..f94ccd080d6e3 100644 --- a/api_docs/kbn_crypto.mdx +++ b/api_docs/kbn_crypto.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-crypto title: "@kbn/crypto" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/crypto plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/crypto'] --- import kbnCryptoObj from './kbn_crypto.devdocs.json'; diff --git a/api_docs/kbn_crypto_browser.mdx b/api_docs/kbn_crypto_browser.mdx index 5502bf1134c89..fce8645543f33 100644 --- a/api_docs/kbn_crypto_browser.mdx +++ b/api_docs/kbn_crypto_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-crypto-browser title: "@kbn/crypto-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/crypto-browser plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/crypto-browser'] --- import kbnCryptoBrowserObj from './kbn_crypto_browser.devdocs.json'; diff --git a/api_docs/kbn_datemath.mdx b/api_docs/kbn_datemath.mdx index 12cc62c58f589..36a60224ac046 100644 --- a/api_docs/kbn_datemath.mdx +++ b/api_docs/kbn_datemath.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-datemath title: "@kbn/datemath" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/datemath plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/datemath'] --- import kbnDatemathObj from './kbn_datemath.devdocs.json'; diff --git a/api_docs/kbn_dev_cli_errors.mdx b/api_docs/kbn_dev_cli_errors.mdx index 7d4e4c5f99619..35fa845810eb5 100644 --- a/api_docs/kbn_dev_cli_errors.mdx +++ b/api_docs/kbn_dev_cli_errors.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-dev-cli-errors title: "@kbn/dev-cli-errors" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/dev-cli-errors plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/dev-cli-errors'] --- import kbnDevCliErrorsObj from './kbn_dev_cli_errors.devdocs.json'; diff --git a/api_docs/kbn_dev_cli_runner.mdx b/api_docs/kbn_dev_cli_runner.mdx index b69d7e7c8ab86..82e447bd4fedb 100644 --- a/api_docs/kbn_dev_cli_runner.mdx +++ b/api_docs/kbn_dev_cli_runner.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-dev-cli-runner title: "@kbn/dev-cli-runner" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/dev-cli-runner plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/dev-cli-runner'] --- import kbnDevCliRunnerObj from './kbn_dev_cli_runner.devdocs.json'; diff --git a/api_docs/kbn_dev_proc_runner.mdx b/api_docs/kbn_dev_proc_runner.mdx index d98fa5b8a7ea9..5f04661513e32 100644 --- a/api_docs/kbn_dev_proc_runner.mdx +++ b/api_docs/kbn_dev_proc_runner.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-dev-proc-runner title: "@kbn/dev-proc-runner" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/dev-proc-runner plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/dev-proc-runner'] --- import kbnDevProcRunnerObj from './kbn_dev_proc_runner.devdocs.json'; diff --git a/api_docs/kbn_dev_utils.mdx b/api_docs/kbn_dev_utils.mdx index 1fa38989362e1..4d19ab132d457 100644 --- a/api_docs/kbn_dev_utils.mdx +++ b/api_docs/kbn_dev_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-dev-utils title: "@kbn/dev-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/dev-utils plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/dev-utils'] --- import kbnDevUtilsObj from './kbn_dev_utils.devdocs.json'; diff --git a/api_docs/kbn_doc_links.devdocs.json b/api_docs/kbn_doc_links.devdocs.json index ea3e6bd91ea30..1dd51047468a0 100644 --- a/api_docs/kbn_doc_links.devdocs.json +++ b/api_docs/kbn_doc_links.devdocs.json @@ -300,7 +300,7 @@ "label": "enterpriseSearch", "description": [], "signature": [ - "{ readonly apiKeys: string; readonly bulkApi: string; readonly configuration: string; readonly connectors: string; readonly connectorsMongoDB: string; readonly connectorsMySQL: string; readonly connectorsWorkplaceSearch: string; readonly crawlerManaging: string; readonly crawlerOverview: string; readonly deployTrainedModels: string; readonly documentLevelSecurity: string; readonly ingestPipelines: string; readonly languageAnalyzers: string; readonly languageClients: string; readonly licenseManagement: string; readonly mailService: string; readonly start: string; readonly syncRules: string; readonly troubleshootSetup: string; readonly usersAccess: string; }" + "{ readonly apiKeys: string; readonly bulkApi: string; readonly configuration: string; readonly connectors: string; readonly connectorsMongoDB: string; readonly connectorsMySQL: string; readonly connectorsWorkplaceSearch: string; readonly crawlerManaging: string; readonly crawlerOverview: string; readonly deployTrainedModels: string; readonly documentLevelSecurity: string; readonly ingestPipelines: string; readonly languageAnalyzers: string; readonly languageClients: string; readonly licenseManagement: string; readonly machineLearningStart: string; readonly mailService: string; readonly start: string; readonly syncRules: string; readonly troubleshootSetup: string; readonly usersAccess: string; }" ], "path": "packages/kbn-doc-links/src/types.ts", "deprecated": false, diff --git a/api_docs/kbn_doc_links.mdx b/api_docs/kbn_doc_links.mdx index b3fa514ea3c9e..1c1b4012855dd 100644 --- a/api_docs/kbn_doc_links.mdx +++ b/api_docs/kbn_doc_links.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-doc-links title: "@kbn/doc-links" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/doc-links plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/doc-links'] --- import kbnDocLinksObj from './kbn_doc_links.devdocs.json'; diff --git a/api_docs/kbn_docs_utils.mdx b/api_docs/kbn_docs_utils.mdx index 213bf4fae76bb..ba35ce87392ed 100644 --- a/api_docs/kbn_docs_utils.mdx +++ b/api_docs/kbn_docs_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-docs-utils title: "@kbn/docs-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/docs-utils plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/docs-utils'] --- import kbnDocsUtilsObj from './kbn_docs_utils.devdocs.json'; diff --git a/api_docs/kbn_ebt_tools.mdx b/api_docs/kbn_ebt_tools.mdx index fb56bb9097720..b6365638ea64b 100644 --- a/api_docs/kbn_ebt_tools.mdx +++ b/api_docs/kbn_ebt_tools.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ebt-tools title: "@kbn/ebt-tools" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ebt-tools plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ebt-tools'] --- import kbnEbtToolsObj from './kbn_ebt_tools.devdocs.json'; diff --git a/api_docs/kbn_es.mdx b/api_docs/kbn_es.mdx index f3797f5d7a396..c5d35491c1d47 100644 --- a/api_docs/kbn_es.mdx +++ b/api_docs/kbn_es.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-es title: "@kbn/es" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/es plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/es'] --- import kbnEsObj from './kbn_es.devdocs.json'; diff --git a/api_docs/kbn_es_archiver.mdx b/api_docs/kbn_es_archiver.mdx index 215df0607cbd4..bb62c790c9dc3 100644 --- a/api_docs/kbn_es_archiver.mdx +++ b/api_docs/kbn_es_archiver.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-es-archiver title: "@kbn/es-archiver" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/es-archiver plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/es-archiver'] --- import kbnEsArchiverObj from './kbn_es_archiver.devdocs.json'; diff --git a/api_docs/kbn_es_errors.mdx b/api_docs/kbn_es_errors.mdx index 8720764373856..dc2b497807641 100644 --- a/api_docs/kbn_es_errors.mdx +++ b/api_docs/kbn_es_errors.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-es-errors title: "@kbn/es-errors" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/es-errors plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/es-errors'] --- import kbnEsErrorsObj from './kbn_es_errors.devdocs.json'; diff --git a/api_docs/kbn_es_query.mdx b/api_docs/kbn_es_query.mdx index e46c5be987516..e82a9d1ace2c5 100644 --- a/api_docs/kbn_es_query.mdx +++ b/api_docs/kbn_es_query.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-es-query title: "@kbn/es-query" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/es-query plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/es-query'] --- import kbnEsQueryObj from './kbn_es_query.devdocs.json'; diff --git a/api_docs/kbn_es_types.mdx b/api_docs/kbn_es_types.mdx index 77f752454205c..f3b30e4636862 100644 --- a/api_docs/kbn_es_types.mdx +++ b/api_docs/kbn_es_types.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-es-types title: "@kbn/es-types" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/es-types plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/es-types'] --- import kbnEsTypesObj from './kbn_es_types.devdocs.json'; diff --git a/api_docs/kbn_eslint_plugin_imports.mdx b/api_docs/kbn_eslint_plugin_imports.mdx index a17d9ec1200ef..c59217429a209 100644 --- a/api_docs/kbn_eslint_plugin_imports.mdx +++ b/api_docs/kbn_eslint_plugin_imports.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-eslint-plugin-imports title: "@kbn/eslint-plugin-imports" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/eslint-plugin-imports plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/eslint-plugin-imports'] --- import kbnEslintPluginImportsObj from './kbn_eslint_plugin_imports.devdocs.json'; diff --git a/api_docs/kbn_field_types.mdx b/api_docs/kbn_field_types.mdx index 7141052e1de15..32d8ab220e015 100644 --- a/api_docs/kbn_field_types.mdx +++ b/api_docs/kbn_field_types.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-field-types title: "@kbn/field-types" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/field-types plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/field-types'] --- import kbnFieldTypesObj from './kbn_field_types.devdocs.json'; diff --git a/api_docs/kbn_find_used_node_modules.mdx b/api_docs/kbn_find_used_node_modules.mdx index 189da556b863b..8ea0ecaa65676 100644 --- a/api_docs/kbn_find_used_node_modules.mdx +++ b/api_docs/kbn_find_used_node_modules.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-find-used-node-modules title: "@kbn/find-used-node-modules" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/find-used-node-modules plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/find-used-node-modules'] --- import kbnFindUsedNodeModulesObj from './kbn_find_used_node_modules.devdocs.json'; diff --git a/api_docs/kbn_ftr_common_functional_services.mdx b/api_docs/kbn_ftr_common_functional_services.mdx index 378bcebf0faee..c0d757e2929a8 100644 --- a/api_docs/kbn_ftr_common_functional_services.mdx +++ b/api_docs/kbn_ftr_common_functional_services.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ftr-common-functional-services title: "@kbn/ftr-common-functional-services" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ftr-common-functional-services plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ftr-common-functional-services'] --- import kbnFtrCommonFunctionalServicesObj from './kbn_ftr_common_functional_services.devdocs.json'; diff --git a/api_docs/kbn_generate.mdx b/api_docs/kbn_generate.mdx index 277cba2088587..227026103aa5d 100644 --- a/api_docs/kbn_generate.mdx +++ b/api_docs/kbn_generate.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-generate title: "@kbn/generate" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/generate plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/generate'] --- import kbnGenerateObj from './kbn_generate.devdocs.json'; diff --git a/api_docs/kbn_get_repo_files.mdx b/api_docs/kbn_get_repo_files.mdx index 99e842c4da4bd..6c46cb85de663 100644 --- a/api_docs/kbn_get_repo_files.mdx +++ b/api_docs/kbn_get_repo_files.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-get-repo-files title: "@kbn/get-repo-files" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/get-repo-files plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/get-repo-files'] --- import kbnGetRepoFilesObj from './kbn_get_repo_files.devdocs.json'; diff --git a/api_docs/kbn_guided_onboarding.devdocs.json b/api_docs/kbn_guided_onboarding.devdocs.json index 13782e7daa9e6..39521593c905b 100644 --- a/api_docs/kbn_guided_onboarding.devdocs.json +++ b/api_docs/kbn_guided_onboarding.devdocs.json @@ -310,7 +310,7 @@ "label": "status", "description": [], "signature": [ - "\"complete\" | \"in_progress\" | \"active\" | \"inactive\" | \"ready_to_complete\"" + "\"complete\" | \"in_progress\" | \"inactive\" | \"active\" | \"ready_to_complete\"" ], "path": "packages/kbn-guided-onboarding/src/types.ts", "deprecated": false, @@ -379,7 +379,7 @@ "\nAllowed states for each step in a guide:\n inactive: Step has not started\n active: Step is ready to start (i.e., the guide has been started)\n in_progress: Step has been started and is in progress\n ready_to_complete: Step can be manually completed\n complete: Step has been completed" ], "signature": [ - "\"complete\" | \"in_progress\" | \"active\" | \"inactive\" | \"ready_to_complete\"" + "\"complete\" | \"in_progress\" | \"inactive\" | \"active\" | \"ready_to_complete\"" ], "path": "packages/kbn-guided-onboarding/src/types.ts", "deprecated": false, diff --git a/api_docs/kbn_guided_onboarding.mdx b/api_docs/kbn_guided_onboarding.mdx index 5659eded5fd5f..bf1243ea0cd52 100644 --- a/api_docs/kbn_guided_onboarding.mdx +++ b/api_docs/kbn_guided_onboarding.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-guided-onboarding title: "@kbn/guided-onboarding" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/guided-onboarding plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/guided-onboarding'] --- import kbnGuidedOnboardingObj from './kbn_guided_onboarding.devdocs.json'; diff --git a/api_docs/kbn_handlebars.devdocs.json b/api_docs/kbn_handlebars.devdocs.json index 440a27d24ef64..e1233a052e2cb 100644 --- a/api_docs/kbn_handlebars.devdocs.json +++ b/api_docs/kbn_handlebars.devdocs.json @@ -49,7 +49,71 @@ "initialIsOpen": false } ], - "interfaces": [], + "interfaces": [ + { + "parentPluginId": "@kbn/handlebars", + "id": "def-common.DecoratorsHash", + "type": "Interface", + "tags": [], + "label": "DecoratorsHash", + "description": [], + "path": "packages/kbn-handlebars/index.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/handlebars", + "id": "def-common.DecoratorsHash.Unnamed", + "type": "IndexSignature", + "tags": [], + "label": "[name: string]: DecoratorFunction", + "description": [], + "signature": [ + "[name: string]: ", + { + "pluginId": "@kbn/handlebars", + "scope": "common", + "docId": "kibKbnHandlebarsPluginApi", + "section": "def-common.DecoratorFunction", + "text": "DecoratorFunction" + } + ], + "path": "packages/kbn-handlebars/index.ts", + "deprecated": false, + "trackAdoption": false + } + ], + "initialIsOpen": false + }, + { + "parentPluginId": "@kbn/handlebars", + "id": "def-common.HelpersHash", + "type": "Interface", + "tags": [], + "label": "HelpersHash", + "description": [], + "path": "packages/kbn-handlebars/index.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/handlebars", + "id": "def-common.HelpersHash.Unnamed", + "type": "IndexSignature", + "tags": [], + "label": "[name: string]: HelperDelegate", + "description": [], + "signature": [ + "[name: string]: Handlebars.HelperDelegate" + ], + "path": "packages/kbn-handlebars/index.ts", + "deprecated": false, + "trackAdoption": false + } + ], + "initialIsOpen": false + } + ], "enums": [], "misc": [ { @@ -69,6 +133,113 @@ "trackAdoption": false, "initialIsOpen": false }, + { + "parentPluginId": "@kbn/handlebars", + "id": "def-common.DecoratorFunction", + "type": "Type", + "tags": [], + "label": "DecoratorFunction", + "description": [ + "\nAccording to the [decorator docs]{@link https://github.com/handlebars-lang/handlebars.js/blob/4.x/docs/decorators-api.md},\na decorator will be called with a different set of arugments than what's actually happening in the upstream code.\nSo here I assume that the docs are wrong and that the upstream code is correct. In reality, `context` is the last 4\ndocumented arguments rolled into one object." + ], + "signature": [ + "(prog: Handlebars.TemplateDelegate, props: Record, container: Container, options: any) => any" + ], + "path": "packages/kbn-handlebars/index.ts", + "deprecated": false, + "trackAdoption": false, + "returnComment": [], + "children": [ + { + "parentPluginId": "@kbn/handlebars", + "id": "def-common.DecoratorFunction.$1", + "type": "Function", + "tags": [], + "label": "prog", + "description": [], + "signature": [ + "Handlebars.TemplateDelegate" + ], + "path": "packages/kbn-handlebars/index.ts", + "deprecated": false, + "trackAdoption": false, + "returnComment": [], + "children": [ + { + "parentPluginId": "@kbn/handlebars", + "id": "def-common.DecoratorFunction.$1.$1", + "type": "Uncategorized", + "tags": [], + "label": "context", + "description": [], + "signature": [ + "T" + ], + "path": "node_modules/handlebars/types/index.d.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/handlebars", + "id": "def-common.DecoratorFunction.$1.$2", + "type": "Object", + "tags": [], + "label": "options", + "description": [], + "signature": [ + "Handlebars.RuntimeOptions | undefined" + ], + "path": "node_modules/handlebars/types/index.d.ts", + "deprecated": false, + "trackAdoption": false + } + ] + }, + { + "parentPluginId": "@kbn/handlebars", + "id": "def-common.DecoratorFunction.$2", + "type": "Object", + "tags": [], + "label": "props", + "description": [], + "signature": [ + "{ [x: string]: any; }" + ], + "path": "packages/kbn-handlebars/index.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/handlebars", + "id": "def-common.DecoratorFunction.$3", + "type": "Object", + "tags": [], + "label": "container", + "description": [], + "signature": [ + "Container" + ], + "path": "packages/kbn-handlebars/index.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/handlebars", + "id": "def-common.DecoratorFunction.$4", + "type": "Any", + "tags": [], + "label": "options", + "description": [], + "signature": [ + "any" + ], + "path": "packages/kbn-handlebars/index.ts", + "deprecated": false, + "trackAdoption": false + } + ], + "initialIsOpen": false + }, { "parentPluginId": "@kbn/handlebars", "id": "def-common.ExtendedCompileOptions", @@ -96,7 +267,7 @@ "\nSupported Handlebars runtime options\n\nThis is a subset of all the runtime options supported by the upstream\nHandlebars module." ], "signature": [ - "{ data?: any; helpers?: { [name: string]: Function; } | undefined; blockParams?: any[] | undefined; }" + "{ data?: any; helpers?: { [name: string]: Function; } | undefined; blockParams?: any[] | undefined; decorators?: { [name: string]: Function; } | undefined; }" ], "path": "packages/kbn-handlebars/index.ts", "deprecated": false, diff --git a/api_docs/kbn_handlebars.mdx b/api_docs/kbn_handlebars.mdx index d84a28798b3fd..3b45c346f990b 100644 --- a/api_docs/kbn_handlebars.mdx +++ b/api_docs/kbn_handlebars.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-handlebars title: "@kbn/handlebars" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/handlebars plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/handlebars'] --- import kbnHandlebarsObj from './kbn_handlebars.devdocs.json'; @@ -21,7 +21,7 @@ Contact [Owner missing] for questions regarding this plugin. | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 6 | 0 | 0 | 0 | +| 17 | 1 | 8 | 0 | ## Common @@ -31,6 +31,9 @@ Contact [Owner missing] for questions regarding this plugin. ### Functions +### Interfaces + + ### Consts, variables and types diff --git a/api_docs/kbn_hapi_mocks.mdx b/api_docs/kbn_hapi_mocks.mdx index 718c5dbd8070b..398c30372675e 100644 --- a/api_docs/kbn_hapi_mocks.mdx +++ b/api_docs/kbn_hapi_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-hapi-mocks title: "@kbn/hapi-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/hapi-mocks plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/hapi-mocks'] --- import kbnHapiMocksObj from './kbn_hapi_mocks.devdocs.json'; diff --git a/api_docs/kbn_health_gateway_server.mdx b/api_docs/kbn_health_gateway_server.mdx index 51c135d2e2436..024699e541abc 100644 --- a/api_docs/kbn_health_gateway_server.mdx +++ b/api_docs/kbn_health_gateway_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-health-gateway-server title: "@kbn/health-gateway-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/health-gateway-server plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/health-gateway-server'] --- import kbnHealthGatewayServerObj from './kbn_health_gateway_server.devdocs.json'; diff --git a/api_docs/kbn_home_sample_data_card.mdx b/api_docs/kbn_home_sample_data_card.mdx index 37006e293d67b..2dce5130032c1 100644 --- a/api_docs/kbn_home_sample_data_card.mdx +++ b/api_docs/kbn_home_sample_data_card.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-home-sample-data-card title: "@kbn/home-sample-data-card" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/home-sample-data-card plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/home-sample-data-card'] --- import kbnHomeSampleDataCardObj from './kbn_home_sample_data_card.devdocs.json'; diff --git a/api_docs/kbn_home_sample_data_tab.mdx b/api_docs/kbn_home_sample_data_tab.mdx index ff89ad7b02d6b..46fbb683c6fd6 100644 --- a/api_docs/kbn_home_sample_data_tab.mdx +++ b/api_docs/kbn_home_sample_data_tab.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-home-sample-data-tab title: "@kbn/home-sample-data-tab" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/home-sample-data-tab plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/home-sample-data-tab'] --- import kbnHomeSampleDataTabObj from './kbn_home_sample_data_tab.devdocs.json'; diff --git a/api_docs/kbn_i18n.mdx b/api_docs/kbn_i18n.mdx index a784c4af0b4f4..61f983ef24b9e 100644 --- a/api_docs/kbn_i18n.mdx +++ b/api_docs/kbn_i18n.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-i18n title: "@kbn/i18n" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/i18n plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/i18n'] --- import kbnI18nObj from './kbn_i18n.devdocs.json'; diff --git a/api_docs/kbn_i18n_react.mdx b/api_docs/kbn_i18n_react.mdx index 1affb709617c4..54c6c66b68d04 100644 --- a/api_docs/kbn_i18n_react.mdx +++ b/api_docs/kbn_i18n_react.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-i18n-react title: "@kbn/i18n-react" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/i18n-react plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/i18n-react'] --- import kbnI18nReactObj from './kbn_i18n_react.devdocs.json'; diff --git a/api_docs/kbn_import_resolver.mdx b/api_docs/kbn_import_resolver.mdx index e56a2a54dfd45..ca22474813c34 100644 --- a/api_docs/kbn_import_resolver.mdx +++ b/api_docs/kbn_import_resolver.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-import-resolver title: "@kbn/import-resolver" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/import-resolver plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/import-resolver'] --- import kbnImportResolverObj from './kbn_import_resolver.devdocs.json'; diff --git a/api_docs/kbn_interpreter.mdx b/api_docs/kbn_interpreter.mdx index 2bb5255a5642e..a4d5b8857f922 100644 --- a/api_docs/kbn_interpreter.mdx +++ b/api_docs/kbn_interpreter.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-interpreter title: "@kbn/interpreter" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/interpreter plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/interpreter'] --- import kbnInterpreterObj from './kbn_interpreter.devdocs.json'; diff --git a/api_docs/kbn_io_ts_utils.devdocs.json b/api_docs/kbn_io_ts_utils.devdocs.json index 4f27d88f64b54..d6bef46b1ce92 100644 --- a/api_docs/kbn_io_ts_utils.devdocs.json +++ b/api_docs/kbn_io_ts_utils.devdocs.json @@ -325,8 +325,48 @@ } ], "enums": [], - "misc": [], + "misc": [ + { + "parentPluginId": "@kbn/io-ts-utils", + "id": "def-common.IndexPatternType", + "type": "Type", + "tags": [], + "label": "IndexPatternType", + "description": [], + "signature": [ + "string & ", + "Brand", + "<", + "IndexPatternBrand", + ">" + ], + "path": "packages/kbn-io-ts-utils/src/index_pattern_rt/index.ts", + "deprecated": false, + "trackAdoption": false, + "initialIsOpen": false + } + ], "objects": [ + { + "parentPluginId": "@kbn/io-ts-utils", + "id": "def-common.indexPatternRt", + "type": "Object", + "tags": [], + "label": "indexPatternRt", + "description": [], + "signature": [ + "BrandC", + "<", + "StringC", + ", ", + "IndexPatternBrand", + ">" + ], + "path": "packages/kbn-io-ts-utils/src/index_pattern_rt/index.ts", + "deprecated": false, + "trackAdoption": false, + "initialIsOpen": false + }, { "parentPluginId": "@kbn/io-ts-utils", "id": "def-common.isoToEpochRt", diff --git a/api_docs/kbn_io_ts_utils.mdx b/api_docs/kbn_io_ts_utils.mdx index 508dc2f256e08..2f4e3da459e79 100644 --- a/api_docs/kbn_io_ts_utils.mdx +++ b/api_docs/kbn_io_ts_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-io-ts-utils title: "@kbn/io-ts-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/io-ts-utils plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/io-ts-utils'] --- import kbnIoTsUtilsObj from './kbn_io_ts_utils.devdocs.json'; @@ -21,7 +21,7 @@ Contact [Owner missing] for questions regarding this plugin. | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 20 | 0 | 20 | 2 | +| 22 | 0 | 22 | 3 | ## Common @@ -34,3 +34,6 @@ Contact [Owner missing] for questions regarding this plugin. ### Interfaces +### Consts, variables and types + + diff --git a/api_docs/kbn_jest_serializers.mdx b/api_docs/kbn_jest_serializers.mdx index c005efdf7bb71..d902f388c6655 100644 --- a/api_docs/kbn_jest_serializers.mdx +++ b/api_docs/kbn_jest_serializers.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-jest-serializers title: "@kbn/jest-serializers" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/jest-serializers plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/jest-serializers'] --- import kbnJestSerializersObj from './kbn_jest_serializers.devdocs.json'; diff --git a/api_docs/kbn_journeys.mdx b/api_docs/kbn_journeys.mdx index b9b24c233fbf0..d8df916ebee13 100644 --- a/api_docs/kbn_journeys.mdx +++ b/api_docs/kbn_journeys.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-journeys title: "@kbn/journeys" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/journeys plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/journeys'] --- import kbnJourneysObj from './kbn_journeys.devdocs.json'; diff --git a/api_docs/kbn_kibana_manifest_schema.mdx b/api_docs/kbn_kibana_manifest_schema.mdx index 0213ce467d3ed..10aaf2d2201d2 100644 --- a/api_docs/kbn_kibana_manifest_schema.mdx +++ b/api_docs/kbn_kibana_manifest_schema.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-kibana-manifest-schema title: "@kbn/kibana-manifest-schema" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/kibana-manifest-schema plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/kibana-manifest-schema'] --- import kbnKibanaManifestSchemaObj from './kbn_kibana_manifest_schema.devdocs.json'; diff --git a/api_docs/kbn_language_documentation_popover.mdx b/api_docs/kbn_language_documentation_popover.mdx index c4178d5f3475e..512a589a6763d 100644 --- a/api_docs/kbn_language_documentation_popover.mdx +++ b/api_docs/kbn_language_documentation_popover.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-language-documentation-popover title: "@kbn/language-documentation-popover" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/language-documentation-popover plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/language-documentation-popover'] --- import kbnLanguageDocumentationPopoverObj from './kbn_language_documentation_popover.devdocs.json'; diff --git a/api_docs/kbn_logging.mdx b/api_docs/kbn_logging.mdx index 79b5754cc9aaf..d0798b73d5301 100644 --- a/api_docs/kbn_logging.mdx +++ b/api_docs/kbn_logging.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-logging title: "@kbn/logging" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/logging plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/logging'] --- import kbnLoggingObj from './kbn_logging.devdocs.json'; diff --git a/api_docs/kbn_logging_mocks.mdx b/api_docs/kbn_logging_mocks.mdx index 122155bf8eef8..f19e9a77aabf5 100644 --- a/api_docs/kbn_logging_mocks.mdx +++ b/api_docs/kbn_logging_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-logging-mocks title: "@kbn/logging-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/logging-mocks plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/logging-mocks'] --- import kbnLoggingMocksObj from './kbn_logging_mocks.devdocs.json'; diff --git a/api_docs/kbn_managed_vscode_config.mdx b/api_docs/kbn_managed_vscode_config.mdx index 9d97827bda1f1..1f682cd736348 100644 --- a/api_docs/kbn_managed_vscode_config.mdx +++ b/api_docs/kbn_managed_vscode_config.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-managed-vscode-config title: "@kbn/managed-vscode-config" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/managed-vscode-config plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/managed-vscode-config'] --- import kbnManagedVscodeConfigObj from './kbn_managed_vscode_config.devdocs.json'; diff --git a/api_docs/kbn_mapbox_gl.mdx b/api_docs/kbn_mapbox_gl.mdx index 89cdd0ddc6a07..634d5bfe7035d 100644 --- a/api_docs/kbn_mapbox_gl.mdx +++ b/api_docs/kbn_mapbox_gl.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-mapbox-gl title: "@kbn/mapbox-gl" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/mapbox-gl plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/mapbox-gl'] --- import kbnMapboxGlObj from './kbn_mapbox_gl.devdocs.json'; diff --git a/api_docs/kbn_ml_agg_utils.mdx b/api_docs/kbn_ml_agg_utils.mdx index d6a7ca29fe0d0..ae2e9b8272d94 100644 --- a/api_docs/kbn_ml_agg_utils.mdx +++ b/api_docs/kbn_ml_agg_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ml-agg-utils title: "@kbn/ml-agg-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ml-agg-utils plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ml-agg-utils'] --- import kbnMlAggUtilsObj from './kbn_ml_agg_utils.devdocs.json'; diff --git a/api_docs/kbn_ml_is_populated_object.mdx b/api_docs/kbn_ml_is_populated_object.mdx index f1f36f4a444f0..04c331017c417 100644 --- a/api_docs/kbn_ml_is_populated_object.mdx +++ b/api_docs/kbn_ml_is_populated_object.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ml-is-populated-object title: "@kbn/ml-is-populated-object" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ml-is-populated-object plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ml-is-populated-object'] --- import kbnMlIsPopulatedObjectObj from './kbn_ml_is_populated_object.devdocs.json'; diff --git a/api_docs/kbn_ml_string_hash.mdx b/api_docs/kbn_ml_string_hash.mdx index bed95c9340aef..6f753874df262 100644 --- a/api_docs/kbn_ml_string_hash.mdx +++ b/api_docs/kbn_ml_string_hash.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ml-string-hash title: "@kbn/ml-string-hash" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ml-string-hash plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ml-string-hash'] --- import kbnMlStringHashObj from './kbn_ml_string_hash.devdocs.json'; diff --git a/api_docs/kbn_monaco.mdx b/api_docs/kbn_monaco.mdx index 46a47956eda22..2f37f0945eee0 100644 --- a/api_docs/kbn_monaco.mdx +++ b/api_docs/kbn_monaco.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-monaco title: "@kbn/monaco" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/monaco plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/monaco'] --- import kbnMonacoObj from './kbn_monaco.devdocs.json'; diff --git a/api_docs/kbn_optimizer.mdx b/api_docs/kbn_optimizer.mdx index cf8190e40b8b1..94a18337f9157 100644 --- a/api_docs/kbn_optimizer.mdx +++ b/api_docs/kbn_optimizer.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-optimizer title: "@kbn/optimizer" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/optimizer plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/optimizer'] --- import kbnOptimizerObj from './kbn_optimizer.devdocs.json'; diff --git a/api_docs/kbn_optimizer_webpack_helpers.mdx b/api_docs/kbn_optimizer_webpack_helpers.mdx index 58bc72a785285..c0941af42b9c6 100644 --- a/api_docs/kbn_optimizer_webpack_helpers.mdx +++ b/api_docs/kbn_optimizer_webpack_helpers.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-optimizer-webpack-helpers title: "@kbn/optimizer-webpack-helpers" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/optimizer-webpack-helpers plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/optimizer-webpack-helpers'] --- import kbnOptimizerWebpackHelpersObj from './kbn_optimizer_webpack_helpers.devdocs.json'; diff --git a/api_docs/kbn_osquery_io_ts_types.mdx b/api_docs/kbn_osquery_io_ts_types.mdx index 7d047c77a6b51..2a83fc4541a97 100644 --- a/api_docs/kbn_osquery_io_ts_types.mdx +++ b/api_docs/kbn_osquery_io_ts_types.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-osquery-io-ts-types title: "@kbn/osquery-io-ts-types" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/osquery-io-ts-types plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/osquery-io-ts-types'] --- import kbnOsqueryIoTsTypesObj from './kbn_osquery_io_ts_types.devdocs.json'; diff --git a/api_docs/kbn_peggy.mdx b/api_docs/kbn_peggy.mdx index a349e9c6f2a6a..6040fd1034b17 100644 --- a/api_docs/kbn_peggy.mdx +++ b/api_docs/kbn_peggy.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-peggy title: "@kbn/peggy" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/peggy plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/peggy'] --- import kbnPeggyObj from './kbn_peggy.devdocs.json'; diff --git a/api_docs/kbn_performance_testing_dataset_extractor.mdx b/api_docs/kbn_performance_testing_dataset_extractor.mdx index 8c0eb29c33f73..666a9a47ebbb3 100644 --- a/api_docs/kbn_performance_testing_dataset_extractor.mdx +++ b/api_docs/kbn_performance_testing_dataset_extractor.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-performance-testing-dataset-extractor title: "@kbn/performance-testing-dataset-extractor" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/performance-testing-dataset-extractor plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/performance-testing-dataset-extractor'] --- import kbnPerformanceTestingDatasetExtractorObj from './kbn_performance_testing_dataset_extractor.devdocs.json'; diff --git a/api_docs/kbn_plugin_generator.mdx b/api_docs/kbn_plugin_generator.mdx index bf387d6304fbc..66170a55204c8 100644 --- a/api_docs/kbn_plugin_generator.mdx +++ b/api_docs/kbn_plugin_generator.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-plugin-generator title: "@kbn/plugin-generator" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/plugin-generator plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/plugin-generator'] --- import kbnPluginGeneratorObj from './kbn_plugin_generator.devdocs.json'; diff --git a/api_docs/kbn_plugin_helpers.mdx b/api_docs/kbn_plugin_helpers.mdx index 2a9e5e68dae26..0188289fe07e9 100644 --- a/api_docs/kbn_plugin_helpers.mdx +++ b/api_docs/kbn_plugin_helpers.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-plugin-helpers title: "@kbn/plugin-helpers" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/plugin-helpers plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/plugin-helpers'] --- import kbnPluginHelpersObj from './kbn_plugin_helpers.devdocs.json'; diff --git a/api_docs/kbn_react_field.mdx b/api_docs/kbn_react_field.mdx index d9fb0cb37fc9d..59a2f4792906e 100644 --- a/api_docs/kbn_react_field.mdx +++ b/api_docs/kbn_react_field.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-react-field title: "@kbn/react-field" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/react-field plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/react-field'] --- import kbnReactFieldObj from './kbn_react_field.devdocs.json'; diff --git a/api_docs/kbn_repo_source_classifier.mdx b/api_docs/kbn_repo_source_classifier.mdx index 124d1e6acfe7c..39c4185deddb5 100644 --- a/api_docs/kbn_repo_source_classifier.mdx +++ b/api_docs/kbn_repo_source_classifier.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-repo-source-classifier title: "@kbn/repo-source-classifier" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/repo-source-classifier plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/repo-source-classifier'] --- import kbnRepoSourceClassifierObj from './kbn_repo_source_classifier.devdocs.json'; diff --git a/api_docs/kbn_rison.devdocs.json b/api_docs/kbn_rison.devdocs.json new file mode 100644 index 0000000000000..ccfc5d62d9b7d --- /dev/null +++ b/api_docs/kbn_rison.devdocs.json @@ -0,0 +1,248 @@ +{ + "id": "@kbn/rison", + "client": { + "classes": [], + "functions": [], + "interfaces": [], + "enums": [], + "misc": [], + "objects": [] + }, + "server": { + "classes": [], + "functions": [ + { + "parentPluginId": "@kbn/rison", + "id": "def-server.decode", + "type": "Function", + "tags": [], + "label": "decode", + "description": [ + "\nparse a rison string into a javascript structure." + ], + "signature": [ + "(rison: string) => ", + { + "pluginId": "@kbn/rison", + "scope": "server", + "docId": "kibKbnRisonPluginApi", + "section": "def-server.RisonValue", + "text": "RisonValue" + } + ], + "path": "packages/kbn-rison/kbn_rison.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/rison", + "id": "def-server.decode.$1", + "type": "string", + "tags": [], + "label": "rison", + "description": [], + "signature": [ + "string" + ], + "path": "packages/kbn-rison/kbn_rison.ts", + "deprecated": false, + "trackAdoption": false, + "isRequired": true + } + ], + "returnComment": [], + "initialIsOpen": false + }, + { + "parentPluginId": "@kbn/rison", + "id": "def-server.decodeArray", + "type": "Function", + "tags": [], + "label": "decodeArray", + "description": [ + "\nparse an a-rison string into a javascript structure.\n\nthis simply adds array markup around the string before parsing." + ], + "signature": [ + "(rison: string) => ", + { + "pluginId": "@kbn/rison", + "scope": "server", + "docId": "kibKbnRisonPluginApi", + "section": "def-server.RisonValue", + "text": "RisonValue" + }, + "[]" + ], + "path": "packages/kbn-rison/kbn_rison.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/rison", + "id": "def-server.decodeArray.$1", + "type": "string", + "tags": [], + "label": "rison", + "description": [], + "signature": [ + "string" + ], + "path": "packages/kbn-rison/kbn_rison.ts", + "deprecated": false, + "trackAdoption": false, + "isRequired": true + } + ], + "returnComment": [], + "initialIsOpen": false + }, + { + "parentPluginId": "@kbn/rison", + "id": "def-server.encode", + "type": "Function", + "tags": [], + "label": "encode", + "description": [ + "\nrison-encode a javascript structure" + ], + "signature": [ + "(obj: any) => string" + ], + "path": "packages/kbn-rison/kbn_rison.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/rison", + "id": "def-server.encode.$1", + "type": "Any", + "tags": [], + "label": "obj", + "description": [], + "signature": [ + "any" + ], + "path": "packages/kbn-rison/kbn_rison.ts", + "deprecated": false, + "trackAdoption": false, + "isRequired": true + } + ], + "returnComment": [], + "initialIsOpen": false + }, + { + "parentPluginId": "@kbn/rison", + "id": "def-server.encodeArray", + "type": "Function", + "tags": [], + "label": "encodeArray", + "description": [ + "\nrison-encode a javascript array without surrounding parens" + ], + "signature": [ + "(array: any[]) => any" + ], + "path": "packages/kbn-rison/kbn_rison.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/rison", + "id": "def-server.encodeArray.$1", + "type": "Array", + "tags": [], + "label": "array", + "description": [], + "signature": [ + "any[]" + ], + "path": "packages/kbn-rison/kbn_rison.ts", + "deprecated": false, + "trackAdoption": false, + "isRequired": true + } + ], + "returnComment": [], + "initialIsOpen": false + }, + { + "parentPluginId": "@kbn/rison", + "id": "def-server.encodeUnknown", + "type": "Function", + "tags": [], + "label": "encodeUnknown", + "description": [], + "signature": [ + "(obj: any) => string | undefined" + ], + "path": "packages/kbn-rison/kbn_rison.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/rison", + "id": "def-server.encodeUnknown.$1", + "type": "Any", + "tags": [], + "label": "obj", + "description": [], + "signature": [ + "any" + ], + "path": "packages/kbn-rison/kbn_rison.ts", + "deprecated": false, + "trackAdoption": false, + "isRequired": true + } + ], + "returnComment": [], + "initialIsOpen": false + } + ], + "interfaces": [], + "enums": [], + "misc": [ + { + "parentPluginId": "@kbn/rison", + "id": "def-server.RisonValue", + "type": "Type", + "tags": [], + "label": "RisonValue", + "description": [], + "signature": [ + "string | number | boolean | ", + { + "pluginId": "@kbn/rison", + "scope": "server", + "docId": "kibKbnRisonPluginApi", + "section": "def-server.RisonValue", + "text": "RisonValue" + }, + "[] | { [key: string]: ", + { + "pluginId": "@kbn/rison", + "scope": "server", + "docId": "kibKbnRisonPluginApi", + "section": "def-server.RisonValue", + "text": "RisonValue" + }, + "; } | null" + ], + "path": "packages/kbn-rison/kbn_rison.ts", + "deprecated": false, + "trackAdoption": false, + "initialIsOpen": false + } + ], + "objects": [] + }, + "common": { + "classes": [], + "functions": [], + "interfaces": [], + "enums": [], + "misc": [], + "objects": [] + } +} \ No newline at end of file diff --git a/api_docs/kbn_rison.mdx b/api_docs/kbn_rison.mdx new file mode 100644 index 0000000000000..e7209d59cfc41 --- /dev/null +++ b/api_docs/kbn_rison.mdx @@ -0,0 +1,33 @@ +--- +#### +#### This document is auto-generated and is meant to be viewed inside our experimental, new docs system. +#### Reach out in #docs-engineering for more info. +#### +id: kibKbnRisonPluginApi +slug: /kibana-dev-docs/api/kbn-rison +title: "@kbn/rison" +image: https://source.unsplash.com/400x175/?github +description: API docs for the @kbn/rison plugin +date: 2022-12-07 +tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/rison'] +--- +import kbnRisonObj from './kbn_rison.devdocs.json'; + + + +Contact [Owner missing] for questions regarding this plugin. + +**Code health stats** + +| Public API count | Any count | Items lacking comments | Missing exports | +|-------------------|-----------|------------------------|-----------------| +| 11 | 2 | 7 | 0 | + +## Server + +### Functions + + +### Consts, variables and types + + diff --git a/api_docs/kbn_rule_data_utils.devdocs.json b/api_docs/kbn_rule_data_utils.devdocs.json index f2a733e93e056..74b279fa11a15 100644 --- a/api_docs/kbn_rule_data_utils.devdocs.json +++ b/api_docs/kbn_rule_data_utils.devdocs.json @@ -1251,7 +1251,7 @@ "label": "AlertStatus", "description": [], "signature": [ - "\"recovered\" | \"active\"" + "\"active\" | \"recovered\"" ], "path": "packages/kbn-rule-data-utils/src/alerts_as_data_status.ts", "deprecated": false, @@ -1401,7 +1401,7 @@ "label": "TechnicalRuleDataFieldName", "description": [], "signature": [ - "\"tags\" | \"kibana\" | \"@timestamp\" | \"kibana.alert.rule.rule_type_id\" | \"kibana.alert.rule.consumer\" | \"event.action\" | \"kibana.alert.rule.execution.uuid\" | \"kibana.alert\" | \"kibana.alert.rule\" | \"kibana.alert.rule.parameters\" | \"kibana.alert.rule.producer\" | \"kibana.space_ids\" | \"kibana.alert.uuid\" | \"kibana.alert.instance.id\" | \"kibana.alert.start\" | \"kibana.alert.time_range\" | \"kibana.alert.end\" | \"kibana.alert.duration.us\" | \"kibana.alert.severity\" | \"kibana.alert.status\" | \"kibana.alert.flapping\" | \"kibana.version\" | \"ecs.version\" | \"kibana.alert.risk_score\" | \"kibana.alert.workflow_status\" | \"kibana.alert.workflow_user\" | \"kibana.alert.workflow_reason\" | \"kibana.alert.system_status\" | \"kibana.alert.action_group\" | \"kibana.alert.reason\" | \"kibana.alert.rule.author\" | \"kibana.alert.rule.category\" | \"kibana.alert.rule.uuid\" | \"kibana.alert.rule.created_at\" | \"kibana.alert.rule.created_by\" | \"kibana.alert.rule.description\" | \"kibana.alert.rule.enabled\" | \"kibana.alert.rule.from\" | \"kibana.alert.rule.interval\" | \"kibana.alert.rule.license\" | \"kibana.alert.rule.name\" | \"kibana.alert.rule.note\" | \"kibana.alert.rule.references\" | \"kibana.alert.rule.rule_id\" | \"kibana.alert.rule.rule_name_override\" | \"kibana.alert.rule.tags\" | \"kibana.alert.rule.to\" | \"kibana.alert.rule.type\" | \"kibana.alert.rule.updated_at\" | \"kibana.alert.rule.updated_by\" | \"kibana.alert.rule.version\" | \"kibana.alert.suppression.terms\" | \"kibana.alert.suppression.terms.field\" | \"kibana.alert.suppression.terms.value\" | \"kibana.alert.suppression.start\" | \"kibana.alert.suppression.end\" | \"kibana.alert.suppression.docs_count\" | \"event.kind\" | \"event.module\" | \"kibana.alert.evaluation.threshold\" | \"kibana.alert.evaluation.value\" | \"kibana.alert.building_block_type\" | \"kibana.alert.rule.exceptions_list\" | \"kibana.alert.rule.namespace\" | \"kibana.alert.rule.threat.framework\" | \"kibana.alert.rule.threat.tactic.id\" | \"kibana.alert.rule.threat.tactic.name\" | \"kibana.alert.rule.threat.tactic.reference\" | \"kibana.alert.rule.threat.technique.id\" | \"kibana.alert.rule.threat.technique.name\" | \"kibana.alert.rule.threat.technique.reference\" | \"kibana.alert.rule.threat.technique.subtechnique.id\" | \"kibana.alert.rule.threat.technique.subtechnique.name\" | \"kibana.alert.rule.threat.technique.subtechnique.reference\"" + "\"tags\" | \"kibana\" | \"@timestamp\" | \"event.action\" | \"kibana.alert.rule.execution.uuid\" | \"kibana.alert.rule.rule_type_id\" | \"kibana.alert.rule.consumer\" | \"kibana.alert\" | \"kibana.alert.rule\" | \"kibana.alert.rule.parameters\" | \"kibana.alert.rule.producer\" | \"kibana.space_ids\" | \"kibana.alert.uuid\" | \"kibana.alert.instance.id\" | \"kibana.alert.start\" | \"kibana.alert.time_range\" | \"kibana.alert.end\" | \"kibana.alert.duration.us\" | \"kibana.alert.severity\" | \"kibana.alert.status\" | \"kibana.alert.flapping\" | \"kibana.version\" | \"ecs.version\" | \"kibana.alert.risk_score\" | \"kibana.alert.workflow_status\" | \"kibana.alert.workflow_user\" | \"kibana.alert.workflow_reason\" | \"kibana.alert.system_status\" | \"kibana.alert.action_group\" | \"kibana.alert.reason\" | \"kibana.alert.rule.author\" | \"kibana.alert.rule.category\" | \"kibana.alert.rule.uuid\" | \"kibana.alert.rule.created_at\" | \"kibana.alert.rule.created_by\" | \"kibana.alert.rule.description\" | \"kibana.alert.rule.enabled\" | \"kibana.alert.rule.from\" | \"kibana.alert.rule.interval\" | \"kibana.alert.rule.license\" | \"kibana.alert.rule.name\" | \"kibana.alert.rule.note\" | \"kibana.alert.rule.references\" | \"kibana.alert.rule.rule_id\" | \"kibana.alert.rule.rule_name_override\" | \"kibana.alert.rule.tags\" | \"kibana.alert.rule.to\" | \"kibana.alert.rule.type\" | \"kibana.alert.rule.updated_at\" | \"kibana.alert.rule.updated_by\" | \"kibana.alert.rule.version\" | \"kibana.alert.suppression.terms\" | \"kibana.alert.suppression.terms.field\" | \"kibana.alert.suppression.terms.value\" | \"kibana.alert.suppression.start\" | \"kibana.alert.suppression.end\" | \"kibana.alert.suppression.docs_count\" | \"event.kind\" | \"event.module\" | \"kibana.alert.evaluation.threshold\" | \"kibana.alert.evaluation.value\" | \"kibana.alert.building_block_type\" | \"kibana.alert.rule.exceptions_list\" | \"kibana.alert.rule.namespace\" | \"kibana.alert.rule.threat.framework\" | \"kibana.alert.rule.threat.tactic.id\" | \"kibana.alert.rule.threat.tactic.name\" | \"kibana.alert.rule.threat.tactic.reference\" | \"kibana.alert.rule.threat.technique.id\" | \"kibana.alert.rule.threat.technique.name\" | \"kibana.alert.rule.threat.technique.reference\" | \"kibana.alert.rule.threat.technique.subtechnique.id\" | \"kibana.alert.rule.threat.technique.subtechnique.name\" | \"kibana.alert.rule.threat.technique.subtechnique.reference\"" ], "path": "packages/kbn-rule-data-utils/src/technical_field_names.ts", "deprecated": false, diff --git a/api_docs/kbn_rule_data_utils.mdx b/api_docs/kbn_rule_data_utils.mdx index 80309dac257d7..9de4ceb5fd580 100644 --- a/api_docs/kbn_rule_data_utils.mdx +++ b/api_docs/kbn_rule_data_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-rule-data-utils title: "@kbn/rule-data-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/rule-data-utils plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/rule-data-utils'] --- import kbnRuleDataUtilsObj from './kbn_rule_data_utils.devdocs.json'; diff --git a/api_docs/kbn_securitysolution_autocomplete.mdx b/api_docs/kbn_securitysolution_autocomplete.mdx index 656f6c888050f..b3b9aed355d1e 100644 --- a/api_docs/kbn_securitysolution_autocomplete.mdx +++ b/api_docs/kbn_securitysolution_autocomplete.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-securitysolution-autocomplete title: "@kbn/securitysolution-autocomplete" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/securitysolution-autocomplete plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/securitysolution-autocomplete'] --- import kbnSecuritysolutionAutocompleteObj from './kbn_securitysolution_autocomplete.devdocs.json'; diff --git a/api_docs/kbn_securitysolution_es_utils.mdx b/api_docs/kbn_securitysolution_es_utils.mdx index 3bc75c36b79b6..993e01ec777db 100644 --- a/api_docs/kbn_securitysolution_es_utils.mdx +++ b/api_docs/kbn_securitysolution_es_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-securitysolution-es-utils title: "@kbn/securitysolution-es-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/securitysolution-es-utils plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/securitysolution-es-utils'] --- import kbnSecuritysolutionEsUtilsObj from './kbn_securitysolution_es_utils.devdocs.json'; diff --git a/api_docs/kbn_securitysolution_exception_list_components.devdocs.json b/api_docs/kbn_securitysolution_exception_list_components.devdocs.json index 7c5410f630730..ca9ebec0b58fd 100644 --- a/api_docs/kbn_securitysolution_exception_list_components.devdocs.json +++ b/api_docs/kbn_securitysolution_exception_list_components.devdocs.json @@ -309,6 +309,39 @@ ], "initialIsOpen": false }, + { + "parentPluginId": "@kbn/securitysolution-exception-list-components", + "id": "def-common.generateLinkedRulesMenuItems", + "type": "Function", + "tags": [], + "label": "generateLinkedRulesMenuItems", + "description": [], + "signature": [ + "({ dataTestSubj, linkedRules, securityLinkAnchorComponent, leftIcon, }: MenuItemLinkedRulesProps) => React.ReactElement>[] | null" + ], + "path": "packages/kbn-securitysolution-exception-list-components/src/generate_linked_rules_menu_item/index.tsx", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/securitysolution-exception-list-components", + "id": "def-common.generateLinkedRulesMenuItems.$1", + "type": "Object", + "tags": [], + "label": "{\n dataTestSubj,\n linkedRules,\n securityLinkAnchorComponent,\n leftIcon = '',\n}", + "description": [], + "signature": [ + "MenuItemLinkedRulesProps" + ], + "path": "packages/kbn-securitysolution-exception-list-components/src/generate_linked_rules_menu_item/index.tsx", + "deprecated": false, + "trackAdoption": false, + "isRequired": true + } + ], + "returnComment": [], + "initialIsOpen": false + }, { "parentPluginId": "@kbn/securitysolution-exception-list-components", "id": "def-common.HeaderMenu", diff --git a/api_docs/kbn_securitysolution_exception_list_components.mdx b/api_docs/kbn_securitysolution_exception_list_components.mdx index 2e8da6a2f6abb..75895fad2ba94 100644 --- a/api_docs/kbn_securitysolution_exception_list_components.mdx +++ b/api_docs/kbn_securitysolution_exception_list_components.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-securitysolution-exception-list-components title: "@kbn/securitysolution-exception-list-components" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/securitysolution-exception-list-components plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/securitysolution-exception-list-components'] --- import kbnSecuritysolutionExceptionListComponentsObj from './kbn_securitysolution_exception_list_components.devdocs.json'; @@ -21,7 +21,7 @@ Contact [Owner missing] for questions regarding this plugin. | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 102 | 0 | 91 | 1 | +| 104 | 0 | 93 | 1 | ## Common diff --git a/api_docs/kbn_securitysolution_hook_utils.mdx b/api_docs/kbn_securitysolution_hook_utils.mdx index 65a7bdfaedc2e..a60946c6aa4ee 100644 --- a/api_docs/kbn_securitysolution_hook_utils.mdx +++ b/api_docs/kbn_securitysolution_hook_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-securitysolution-hook-utils title: "@kbn/securitysolution-hook-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/securitysolution-hook-utils plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/securitysolution-hook-utils'] --- import kbnSecuritysolutionHookUtilsObj from './kbn_securitysolution_hook_utils.devdocs.json'; diff --git a/api_docs/kbn_securitysolution_io_ts_alerting_types.mdx b/api_docs/kbn_securitysolution_io_ts_alerting_types.mdx index 40c5ea429e16c..80223b52da256 100644 --- a/api_docs/kbn_securitysolution_io_ts_alerting_types.mdx +++ b/api_docs/kbn_securitysolution_io_ts_alerting_types.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-securitysolution-io-ts-alerting-types title: "@kbn/securitysolution-io-ts-alerting-types" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/securitysolution-io-ts-alerting-types plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/securitysolution-io-ts-alerting-types'] --- import kbnSecuritysolutionIoTsAlertingTypesObj from './kbn_securitysolution_io_ts_alerting_types.devdocs.json'; diff --git a/api_docs/kbn_securitysolution_io_ts_list_types.mdx b/api_docs/kbn_securitysolution_io_ts_list_types.mdx index d343f44c4baa7..4c1da22178851 100644 --- a/api_docs/kbn_securitysolution_io_ts_list_types.mdx +++ b/api_docs/kbn_securitysolution_io_ts_list_types.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-securitysolution-io-ts-list-types title: "@kbn/securitysolution-io-ts-list-types" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/securitysolution-io-ts-list-types plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/securitysolution-io-ts-list-types'] --- import kbnSecuritysolutionIoTsListTypesObj from './kbn_securitysolution_io_ts_list_types.devdocs.json'; diff --git a/api_docs/kbn_securitysolution_io_ts_types.mdx b/api_docs/kbn_securitysolution_io_ts_types.mdx index f842577dce9ae..df62854665aa9 100644 --- a/api_docs/kbn_securitysolution_io_ts_types.mdx +++ b/api_docs/kbn_securitysolution_io_ts_types.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-securitysolution-io-ts-types title: "@kbn/securitysolution-io-ts-types" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/securitysolution-io-ts-types plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/securitysolution-io-ts-types'] --- import kbnSecuritysolutionIoTsTypesObj from './kbn_securitysolution_io_ts_types.devdocs.json'; diff --git a/api_docs/kbn_securitysolution_io_ts_utils.mdx b/api_docs/kbn_securitysolution_io_ts_utils.mdx index d798cc88be05a..876ec8b2a2956 100644 --- a/api_docs/kbn_securitysolution_io_ts_utils.mdx +++ b/api_docs/kbn_securitysolution_io_ts_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-securitysolution-io-ts-utils title: "@kbn/securitysolution-io-ts-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/securitysolution-io-ts-utils plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/securitysolution-io-ts-utils'] --- import kbnSecuritysolutionIoTsUtilsObj from './kbn_securitysolution_io_ts_utils.devdocs.json'; diff --git a/api_docs/kbn_securitysolution_list_api.mdx b/api_docs/kbn_securitysolution_list_api.mdx index 83579bd04d17f..100ed0051283a 100644 --- a/api_docs/kbn_securitysolution_list_api.mdx +++ b/api_docs/kbn_securitysolution_list_api.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-securitysolution-list-api title: "@kbn/securitysolution-list-api" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/securitysolution-list-api plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/securitysolution-list-api'] --- import kbnSecuritysolutionListApiObj from './kbn_securitysolution_list_api.devdocs.json'; diff --git a/api_docs/kbn_securitysolution_list_constants.devdocs.json b/api_docs/kbn_securitysolution_list_constants.devdocs.json index 4b06a6e96b7c8..33636a6103033 100644 --- a/api_docs/kbn_securitysolution_list_constants.devdocs.json +++ b/api_docs/kbn_securitysolution_list_constants.devdocs.json @@ -251,6 +251,14 @@ "deprecated": true, "trackAdoption": false, "references": [ + { + "plugin": "securitySolution", + "path": "x-pack/plugins/security_solution/public/management/pages/policy/view/policy_hooks.ts" + }, + { + "plugin": "securitySolution", + "path": "x-pack/plugins/security_solution/public/management/pages/policy/view/policy_hooks.ts" + }, { "plugin": "securitySolution", "path": "x-pack/plugins/security_solution/public/management/pages/event_filters/constants.ts" @@ -303,14 +311,6 @@ "plugin": "securitySolution", "path": "x-pack/plugins/security_solution/public/management/pages/event_filters/service/api_client.ts" }, - { - "plugin": "securitySolution", - "path": "x-pack/plugins/security_solution/public/management/pages/policy/view/policy_hooks.ts" - }, - { - "plugin": "securitySolution", - "path": "x-pack/plugins/security_solution/public/management/pages/policy/view/policy_hooks.ts" - }, { "plugin": "securitySolution", "path": "x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.ts" diff --git a/api_docs/kbn_securitysolution_list_constants.mdx b/api_docs/kbn_securitysolution_list_constants.mdx index e8e1e1c9851e5..c7d728fcb2ef5 100644 --- a/api_docs/kbn_securitysolution_list_constants.mdx +++ b/api_docs/kbn_securitysolution_list_constants.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-securitysolution-list-constants title: "@kbn/securitysolution-list-constants" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/securitysolution-list-constants plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/securitysolution-list-constants'] --- import kbnSecuritysolutionListConstantsObj from './kbn_securitysolution_list_constants.devdocs.json'; diff --git a/api_docs/kbn_securitysolution_list_hooks.mdx b/api_docs/kbn_securitysolution_list_hooks.mdx index 3f3d588aa95e3..7ea1b13288615 100644 --- a/api_docs/kbn_securitysolution_list_hooks.mdx +++ b/api_docs/kbn_securitysolution_list_hooks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-securitysolution-list-hooks title: "@kbn/securitysolution-list-hooks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/securitysolution-list-hooks plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/securitysolution-list-hooks'] --- import kbnSecuritysolutionListHooksObj from './kbn_securitysolution_list_hooks.devdocs.json'; diff --git a/api_docs/kbn_securitysolution_list_utils.mdx b/api_docs/kbn_securitysolution_list_utils.mdx index 5ff7825ea5867..4b240593f4cab 100644 --- a/api_docs/kbn_securitysolution_list_utils.mdx +++ b/api_docs/kbn_securitysolution_list_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-securitysolution-list-utils title: "@kbn/securitysolution-list-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/securitysolution-list-utils plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/securitysolution-list-utils'] --- import kbnSecuritysolutionListUtilsObj from './kbn_securitysolution_list_utils.devdocs.json'; diff --git a/api_docs/kbn_securitysolution_rules.mdx b/api_docs/kbn_securitysolution_rules.mdx index fd567ea22cc29..9315d257b7227 100644 --- a/api_docs/kbn_securitysolution_rules.mdx +++ b/api_docs/kbn_securitysolution_rules.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-securitysolution-rules title: "@kbn/securitysolution-rules" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/securitysolution-rules plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/securitysolution-rules'] --- import kbnSecuritysolutionRulesObj from './kbn_securitysolution_rules.devdocs.json'; diff --git a/api_docs/kbn_securitysolution_t_grid.mdx b/api_docs/kbn_securitysolution_t_grid.mdx index 4881e0b5ef135..a47a100287019 100644 --- a/api_docs/kbn_securitysolution_t_grid.mdx +++ b/api_docs/kbn_securitysolution_t_grid.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-securitysolution-t-grid title: "@kbn/securitysolution-t-grid" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/securitysolution-t-grid plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/securitysolution-t-grid'] --- import kbnSecuritysolutionTGridObj from './kbn_securitysolution_t_grid.devdocs.json'; diff --git a/api_docs/kbn_securitysolution_utils.mdx b/api_docs/kbn_securitysolution_utils.mdx index 9df75164d6474..6fa33d5b3816c 100644 --- a/api_docs/kbn_securitysolution_utils.mdx +++ b/api_docs/kbn_securitysolution_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-securitysolution-utils title: "@kbn/securitysolution-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/securitysolution-utils plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/securitysolution-utils'] --- import kbnSecuritysolutionUtilsObj from './kbn_securitysolution_utils.devdocs.json'; diff --git a/api_docs/kbn_server_http_tools.mdx b/api_docs/kbn_server_http_tools.mdx index f18845bab26d9..9a9c8df585f2c 100644 --- a/api_docs/kbn_server_http_tools.mdx +++ b/api_docs/kbn_server_http_tools.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-server-http-tools title: "@kbn/server-http-tools" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/server-http-tools plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/server-http-tools'] --- import kbnServerHttpToolsObj from './kbn_server_http_tools.devdocs.json'; diff --git a/api_docs/kbn_server_route_repository.mdx b/api_docs/kbn_server_route_repository.mdx index 5c70c79af3dde..086e72c0c6a6d 100644 --- a/api_docs/kbn_server_route_repository.mdx +++ b/api_docs/kbn_server_route_repository.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-server-route-repository title: "@kbn/server-route-repository" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/server-route-repository plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/server-route-repository'] --- import kbnServerRouteRepositoryObj from './kbn_server_route_repository.devdocs.json'; diff --git a/api_docs/kbn_shared_svg.mdx b/api_docs/kbn_shared_svg.mdx index fbf2347325a0f..0f3b65d3edc04 100644 --- a/api_docs/kbn_shared_svg.mdx +++ b/api_docs/kbn_shared_svg.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-svg title: "@kbn/shared-svg" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-svg plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-svg'] --- import kbnSharedSvgObj from './kbn_shared_svg.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_avatar_solution.mdx b/api_docs/kbn_shared_ux_avatar_solution.mdx index 12954a6bfa182..d961080164606 100644 --- a/api_docs/kbn_shared_ux_avatar_solution.mdx +++ b/api_docs/kbn_shared_ux_avatar_solution.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-avatar-solution title: "@kbn/shared-ux-avatar-solution" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-avatar-solution plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-avatar-solution'] --- import kbnSharedUxAvatarSolutionObj from './kbn_shared_ux_avatar_solution.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_avatar_user_profile_components.mdx b/api_docs/kbn_shared_ux_avatar_user_profile_components.mdx index aa78f8b9aaf6e..f1e3416ad130d 100644 --- a/api_docs/kbn_shared_ux_avatar_user_profile_components.mdx +++ b/api_docs/kbn_shared_ux_avatar_user_profile_components.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-avatar-user-profile-components title: "@kbn/shared-ux-avatar-user-profile-components" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-avatar-user-profile-components plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-avatar-user-profile-components'] --- import kbnSharedUxAvatarUserProfileComponentsObj from './kbn_shared_ux_avatar_user_profile_components.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_button_exit_full_screen.mdx b/api_docs/kbn_shared_ux_button_exit_full_screen.mdx index ec1047188c4d0..64743e058271f 100644 --- a/api_docs/kbn_shared_ux_button_exit_full_screen.mdx +++ b/api_docs/kbn_shared_ux_button_exit_full_screen.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-button-exit-full-screen title: "@kbn/shared-ux-button-exit-full-screen" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-button-exit-full-screen plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-button-exit-full-screen'] --- import kbnSharedUxButtonExitFullScreenObj from './kbn_shared_ux_button_exit_full_screen.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_button_exit_full_screen_mocks.mdx b/api_docs/kbn_shared_ux_button_exit_full_screen_mocks.mdx index a872fddc888a8..6b1e441ae35d8 100644 --- a/api_docs/kbn_shared_ux_button_exit_full_screen_mocks.mdx +++ b/api_docs/kbn_shared_ux_button_exit_full_screen_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-button-exit-full-screen-mocks title: "@kbn/shared-ux-button-exit-full-screen-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-button-exit-full-screen-mocks plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-button-exit-full-screen-mocks'] --- import kbnSharedUxButtonExitFullScreenMocksObj from './kbn_shared_ux_button_exit_full_screen_mocks.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_button_toolbar.mdx b/api_docs/kbn_shared_ux_button_toolbar.mdx index dc3f7a6ca48ba..975aa866db568 100644 --- a/api_docs/kbn_shared_ux_button_toolbar.mdx +++ b/api_docs/kbn_shared_ux_button_toolbar.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-button-toolbar title: "@kbn/shared-ux-button-toolbar" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-button-toolbar plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-button-toolbar'] --- import kbnSharedUxButtonToolbarObj from './kbn_shared_ux_button_toolbar.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_card_no_data.devdocs.json b/api_docs/kbn_shared_ux_card_no_data.devdocs.json index 9128118a280ee..3f9b08b643f7a 100644 --- a/api_docs/kbn_shared_ux_card_no_data.devdocs.json +++ b/api_docs/kbn_shared_ux_card_no_data.devdocs.json @@ -186,7 +186,7 @@ "signature": [ "{ children?: React.ReactNode; description?: React.ReactNode; category?: string | undefined; onError?: React.ReactEventHandler | undefined; hidden?: boolean | undefined; icon?: React.ReactElement<", "EuiIconProps", - ", string | React.JSXElementConstructor> | null | undefined; id?: string | undefined; image?: string | React.ReactElement> | undefined; className?: string | undefined; title?: boolean | React.ReactChild | React.ReactFragment | React.ReactPortal | undefined; onChange?: React.FormEventHandler | undefined; onKeyDown?: React.KeyboardEventHandler | undefined; onClick?: React.MouseEventHandler | undefined; security?: string | undefined; defaultValue?: string | number | readonly string[] | undefined; lang?: string | undefined; defaultChecked?: boolean | undefined; suppressContentEditableWarning?: boolean | undefined; suppressHydrationWarning?: boolean | undefined; accessKey?: string | undefined; contentEditable?: \"inherit\" | Booleanish | undefined; contextMenu?: string | undefined; dir?: string | undefined; draggable?: Booleanish | undefined; placeholder?: string | undefined; slot?: string | undefined; spellCheck?: Booleanish | undefined; style?: React.CSSProperties | undefined; tabIndex?: number | undefined; translate?: \"no\" | \"yes\" | undefined; radioGroup?: string | undefined; role?: React.AriaRole | undefined; about?: string | undefined; datatype?: string | undefined; inlist?: any; prefix?: string | undefined; property?: string | undefined; resource?: string | undefined; typeof?: string | undefined; vocab?: string | undefined; autoCapitalize?: string | undefined; autoCorrect?: string | undefined; autoSave?: string | undefined; itemProp?: string | undefined; itemScope?: boolean | undefined; itemType?: string | undefined; itemID?: string | undefined; itemRef?: string | undefined; results?: number | undefined; unselectable?: \"on\" | \"off\" | undefined; inputMode?: \"none\" | \"email\" | \"search\" | \"text\" | \"tel\" | \"url\" | \"numeric\" | \"decimal\" | undefined; is?: string | undefined; 'aria-activedescendant'?: string | undefined; 'aria-atomic'?: Booleanish | undefined; 'aria-autocomplete'?: \"none\" | \"list\" | \"inline\" | \"both\" | undefined; 'aria-busy'?: Booleanish | undefined; 'aria-checked'?: boolean | \"mixed\" | \"false\" | \"true\" | undefined; 'aria-colcount'?: number | undefined; 'aria-colindex'?: number | undefined; 'aria-colspan'?: number | undefined; 'aria-controls'?: string | undefined; 'aria-current'?: boolean | \"date\" | \"location\" | \"time\" | \"page\" | \"false\" | \"true\" | \"step\" | undefined; 'aria-describedby'?: string | undefined; 'aria-details'?: string | undefined; 'aria-disabled'?: Booleanish | undefined; 'aria-dropeffect'?: \"none\" | \"copy\" | \"link\" | \"execute\" | \"move\" | \"popup\" | undefined; 'aria-errormessage'?: string | undefined; 'aria-expanded'?: Booleanish | undefined; 'aria-flowto'?: string | undefined; 'aria-grabbed'?: Booleanish | undefined; 'aria-haspopup'?: boolean | \"grid\" | \"menu\" | \"false\" | \"true\" | \"dialog\" | \"listbox\" | \"tree\" | undefined; 'aria-hidden'?: Booleanish | undefined; 'aria-invalid'?: boolean | \"false\" | \"true\" | \"grammar\" | \"spelling\" | undefined; 'aria-keyshortcuts'?: string | undefined; 'aria-label'?: string | undefined; 'aria-labelledby'?: string | undefined; 'aria-level'?: number | undefined; 'aria-live'?: \"off\" | \"assertive\" | \"polite\" | undefined; 'aria-modal'?: Booleanish | undefined; 'aria-multiline'?: Booleanish | undefined; 'aria-multiselectable'?: Booleanish | undefined; 'aria-orientation'?: \"horizontal\" | \"vertical\" | undefined; 'aria-owns'?: string | undefined; 'aria-placeholder'?: string | undefined; 'aria-posinset'?: number | undefined; 'aria-pressed'?: boolean | \"mixed\" | \"false\" | \"true\" | undefined; 'aria-readonly'?: Booleanish | undefined; 'aria-relevant'?: \"all\" | \"text\" | \"additions\" | \"additions removals\" | \"additions text\" | \"removals\" | \"removals additions\" | \"removals text\" | \"text additions\" | \"text removals\" | undefined; 'aria-required'?: Booleanish | undefined; 'aria-roledescription'?: string | undefined; 'aria-rowcount'?: number | undefined; 'aria-rowindex'?: number | undefined; 'aria-rowspan'?: number | undefined; 'aria-selected'?: Booleanish | undefined; 'aria-setsize'?: number | undefined; 'aria-sort'?: \"none\" | \"other\" | \"ascending\" | \"descending\" | undefined; 'aria-valuemax'?: number | undefined; 'aria-valuemin'?: number | undefined; 'aria-valuenow'?: number | undefined; 'aria-valuetext'?: string | undefined; dangerouslySetInnerHTML?: { __html: string; } | undefined; onCopy?: React.ClipboardEventHandler | undefined; onCopyCapture?: React.ClipboardEventHandler | undefined; onCut?: React.ClipboardEventHandler | undefined; onCutCapture?: React.ClipboardEventHandler | undefined; onPaste?: React.ClipboardEventHandler | undefined; onPasteCapture?: React.ClipboardEventHandler | undefined; onCompositionEnd?: React.CompositionEventHandler | undefined; onCompositionEndCapture?: React.CompositionEventHandler | undefined; onCompositionStart?: React.CompositionEventHandler | undefined; onCompositionStartCapture?: React.CompositionEventHandler | undefined; onCompositionUpdate?: React.CompositionEventHandler | undefined; onCompositionUpdateCapture?: React.CompositionEventHandler | undefined; onFocus?: React.FocusEventHandler | undefined; onFocusCapture?: React.FocusEventHandler | undefined; onBlur?: React.FocusEventHandler | undefined; onBlurCapture?: React.FocusEventHandler | undefined; onChangeCapture?: React.FormEventHandler | undefined; onBeforeInput?: React.FormEventHandler | undefined; onBeforeInputCapture?: React.FormEventHandler | undefined; onInput?: React.FormEventHandler | undefined; onInputCapture?: React.FormEventHandler | undefined; onReset?: React.FormEventHandler | undefined; onResetCapture?: React.FormEventHandler | undefined; onSubmit?: React.FormEventHandler | undefined; onSubmitCapture?: React.FormEventHandler | undefined; onInvalid?: React.FormEventHandler | undefined; onInvalidCapture?: React.FormEventHandler | undefined; onLoad?: React.ReactEventHandler | undefined; onLoadCapture?: React.ReactEventHandler | undefined; onErrorCapture?: React.ReactEventHandler | undefined; onKeyDownCapture?: React.KeyboardEventHandler | undefined; onKeyPress?: React.KeyboardEventHandler | undefined; onKeyPressCapture?: React.KeyboardEventHandler | undefined; onKeyUp?: React.KeyboardEventHandler | undefined; onKeyUpCapture?: React.KeyboardEventHandler | undefined; onAbort?: React.ReactEventHandler | undefined; onAbortCapture?: React.ReactEventHandler | undefined; onCanPlay?: React.ReactEventHandler | undefined; onCanPlayCapture?: React.ReactEventHandler | undefined; onCanPlayThrough?: React.ReactEventHandler | undefined; onCanPlayThroughCapture?: React.ReactEventHandler | undefined; onDurationChange?: React.ReactEventHandler | undefined; onDurationChangeCapture?: React.ReactEventHandler | undefined; onEmptied?: React.ReactEventHandler | undefined; onEmptiedCapture?: React.ReactEventHandler | undefined; onEncrypted?: React.ReactEventHandler | undefined; onEncryptedCapture?: React.ReactEventHandler | undefined; onEnded?: React.ReactEventHandler | undefined; onEndedCapture?: React.ReactEventHandler | undefined; onLoadedData?: React.ReactEventHandler | undefined; onLoadedDataCapture?: React.ReactEventHandler | undefined; onLoadedMetadata?: React.ReactEventHandler | undefined; onLoadedMetadataCapture?: React.ReactEventHandler | undefined; onLoadStart?: React.ReactEventHandler | undefined; onLoadStartCapture?: React.ReactEventHandler | undefined; onPause?: React.ReactEventHandler | undefined; onPauseCapture?: React.ReactEventHandler | undefined; onPlay?: React.ReactEventHandler | undefined; onPlayCapture?: React.ReactEventHandler | undefined; onPlaying?: React.ReactEventHandler | undefined; onPlayingCapture?: React.ReactEventHandler | undefined; onProgress?: React.ReactEventHandler | undefined; onProgressCapture?: React.ReactEventHandler | undefined; onRateChange?: React.ReactEventHandler | undefined; onRateChangeCapture?: React.ReactEventHandler | undefined; onSeeked?: React.ReactEventHandler | undefined; onSeekedCapture?: React.ReactEventHandler | undefined; onSeeking?: React.ReactEventHandler | undefined; onSeekingCapture?: React.ReactEventHandler | undefined; onStalled?: React.ReactEventHandler | undefined; onStalledCapture?: React.ReactEventHandler | undefined; onSuspend?: React.ReactEventHandler | undefined; onSuspendCapture?: React.ReactEventHandler | undefined; onTimeUpdate?: React.ReactEventHandler | undefined; onTimeUpdateCapture?: React.ReactEventHandler | undefined; onVolumeChange?: React.ReactEventHandler | undefined; onVolumeChangeCapture?: React.ReactEventHandler | undefined; onWaiting?: React.ReactEventHandler | undefined; onWaitingCapture?: React.ReactEventHandler | undefined; onAuxClick?: React.MouseEventHandler | undefined; onAuxClickCapture?: React.MouseEventHandler | undefined; onClickCapture?: React.MouseEventHandler | undefined; onContextMenu?: React.MouseEventHandler | undefined; onContextMenuCapture?: React.MouseEventHandler | undefined; onDoubleClick?: React.MouseEventHandler | undefined; onDoubleClickCapture?: React.MouseEventHandler | undefined; onDrag?: React.DragEventHandler | undefined; onDragCapture?: React.DragEventHandler | undefined; onDragEnd?: React.DragEventHandler | undefined; onDragEndCapture?: React.DragEventHandler | undefined; onDragEnter?: React.DragEventHandler | undefined; onDragEnterCapture?: React.DragEventHandler | undefined; onDragExit?: React.DragEventHandler | undefined; onDragExitCapture?: React.DragEventHandler | undefined; onDragLeave?: React.DragEventHandler | undefined; onDragLeaveCapture?: React.DragEventHandler | undefined; onDragOver?: React.DragEventHandler | undefined; onDragOverCapture?: React.DragEventHandler | undefined; onDragStart?: React.DragEventHandler | undefined; onDragStartCapture?: React.DragEventHandler | undefined; onDrop?: React.DragEventHandler | undefined; onDropCapture?: React.DragEventHandler | undefined; onMouseDown?: React.MouseEventHandler | undefined; onMouseDownCapture?: React.MouseEventHandler | undefined; onMouseEnter?: React.MouseEventHandler | undefined; onMouseLeave?: React.MouseEventHandler | undefined; onMouseMove?: React.MouseEventHandler | undefined; onMouseMoveCapture?: React.MouseEventHandler | undefined; onMouseOut?: React.MouseEventHandler | undefined; onMouseOutCapture?: React.MouseEventHandler | undefined; onMouseOver?: React.MouseEventHandler | undefined; onMouseOverCapture?: React.MouseEventHandler | undefined; onMouseUp?: React.MouseEventHandler | undefined; onMouseUpCapture?: React.MouseEventHandler | undefined; onSelect?: React.ReactEventHandler | undefined; onSelectCapture?: React.ReactEventHandler | undefined; onTouchCancel?: React.TouchEventHandler | undefined; onTouchCancelCapture?: React.TouchEventHandler | undefined; onTouchEnd?: React.TouchEventHandler | undefined; onTouchEndCapture?: React.TouchEventHandler | undefined; onTouchMove?: React.TouchEventHandler | undefined; onTouchMoveCapture?: React.TouchEventHandler | undefined; onTouchStart?: React.TouchEventHandler | undefined; onTouchStartCapture?: React.TouchEventHandler | undefined; onPointerDown?: React.PointerEventHandler | undefined; onPointerDownCapture?: React.PointerEventHandler | undefined; onPointerMove?: React.PointerEventHandler | undefined; onPointerMoveCapture?: React.PointerEventHandler | undefined; onPointerUp?: React.PointerEventHandler | undefined; onPointerUpCapture?: React.PointerEventHandler | undefined; onPointerCancel?: React.PointerEventHandler | undefined; onPointerCancelCapture?: React.PointerEventHandler | undefined; onPointerEnter?: React.PointerEventHandler | undefined; onPointerEnterCapture?: React.PointerEventHandler | undefined; onPointerLeave?: React.PointerEventHandler | undefined; onPointerLeaveCapture?: React.PointerEventHandler | undefined; onPointerOver?: React.PointerEventHandler | undefined; onPointerOverCapture?: React.PointerEventHandler | undefined; onPointerOut?: React.PointerEventHandler | undefined; onPointerOutCapture?: React.PointerEventHandler | undefined; onGotPointerCapture?: React.PointerEventHandler | undefined; onGotPointerCaptureCapture?: React.PointerEventHandler | undefined; onLostPointerCapture?: React.PointerEventHandler | undefined; onLostPointerCaptureCapture?: React.PointerEventHandler | undefined; onScroll?: React.UIEventHandler | undefined; onScrollCapture?: React.UIEventHandler | undefined; onWheel?: React.WheelEventHandler | undefined; onWheelCapture?: React.WheelEventHandler | undefined; onAnimationStart?: React.AnimationEventHandler | undefined; onAnimationStartCapture?: React.AnimationEventHandler | undefined; onAnimationEnd?: React.AnimationEventHandler | undefined; onAnimationEndCapture?: React.AnimationEventHandler | undefined; onAnimationIteration?: React.AnimationEventHandler | undefined; onAnimationIterationCapture?: React.AnimationEventHandler | undefined; onTransitionEnd?: React.TransitionEventHandler | undefined; onTransitionEndCapture?: React.TransitionEventHandler | undefined; 'data-test-subj'?: string | undefined; css?: ", + ", string | React.JSXElementConstructor> | null | undefined; id?: string | undefined; image?: string | React.ReactElement> | undefined; className?: string | undefined; title?: boolean | React.ReactChild | React.ReactFragment | React.ReactPortal | undefined; onChange?: React.FormEventHandler | undefined; onKeyDown?: React.KeyboardEventHandler | undefined; onClick?: React.MouseEventHandler | undefined; security?: string | undefined; defaultValue?: string | number | readonly string[] | undefined; lang?: string | undefined; defaultChecked?: boolean | undefined; suppressContentEditableWarning?: boolean | undefined; suppressHydrationWarning?: boolean | undefined; accessKey?: string | undefined; contentEditable?: \"inherit\" | Booleanish | undefined; contextMenu?: string | undefined; dir?: string | undefined; draggable?: Booleanish | undefined; placeholder?: string | undefined; slot?: string | undefined; spellCheck?: Booleanish | undefined; style?: React.CSSProperties | undefined; tabIndex?: number | undefined; translate?: \"no\" | \"yes\" | undefined; radioGroup?: string | undefined; role?: React.AriaRole | undefined; about?: string | undefined; datatype?: string | undefined; inlist?: any; prefix?: string | undefined; property?: string | undefined; resource?: string | undefined; typeof?: string | undefined; vocab?: string | undefined; autoCapitalize?: string | undefined; autoCorrect?: string | undefined; autoSave?: string | undefined; itemProp?: string | undefined; itemScope?: boolean | undefined; itemType?: string | undefined; itemID?: string | undefined; itemRef?: string | undefined; results?: number | undefined; unselectable?: \"on\" | \"off\" | undefined; inputMode?: \"none\" | \"email\" | \"search\" | \"text\" | \"url\" | \"tel\" | \"numeric\" | \"decimal\" | undefined; is?: string | undefined; 'aria-activedescendant'?: string | undefined; 'aria-atomic'?: Booleanish | undefined; 'aria-autocomplete'?: \"none\" | \"list\" | \"inline\" | \"both\" | undefined; 'aria-busy'?: Booleanish | undefined; 'aria-checked'?: boolean | \"mixed\" | \"false\" | \"true\" | undefined; 'aria-colcount'?: number | undefined; 'aria-colindex'?: number | undefined; 'aria-colspan'?: number | undefined; 'aria-controls'?: string | undefined; 'aria-current'?: boolean | \"date\" | \"location\" | \"time\" | \"page\" | \"false\" | \"true\" | \"step\" | undefined; 'aria-describedby'?: string | undefined; 'aria-details'?: string | undefined; 'aria-disabled'?: Booleanish | undefined; 'aria-dropeffect'?: \"none\" | \"copy\" | \"link\" | \"execute\" | \"move\" | \"popup\" | undefined; 'aria-errormessage'?: string | undefined; 'aria-expanded'?: Booleanish | undefined; 'aria-flowto'?: string | undefined; 'aria-grabbed'?: Booleanish | undefined; 'aria-haspopup'?: boolean | \"grid\" | \"menu\" | \"false\" | \"true\" | \"dialog\" | \"listbox\" | \"tree\" | undefined; 'aria-hidden'?: Booleanish | undefined; 'aria-invalid'?: boolean | \"false\" | \"true\" | \"grammar\" | \"spelling\" | undefined; 'aria-keyshortcuts'?: string | undefined; 'aria-label'?: string | undefined; 'aria-labelledby'?: string | undefined; 'aria-level'?: number | undefined; 'aria-live'?: \"off\" | \"assertive\" | \"polite\" | undefined; 'aria-modal'?: Booleanish | undefined; 'aria-multiline'?: Booleanish | undefined; 'aria-multiselectable'?: Booleanish | undefined; 'aria-orientation'?: \"horizontal\" | \"vertical\" | undefined; 'aria-owns'?: string | undefined; 'aria-placeholder'?: string | undefined; 'aria-posinset'?: number | undefined; 'aria-pressed'?: boolean | \"mixed\" | \"false\" | \"true\" | undefined; 'aria-readonly'?: Booleanish | undefined; 'aria-relevant'?: \"all\" | \"text\" | \"additions\" | \"additions removals\" | \"additions text\" | \"removals\" | \"removals additions\" | \"removals text\" | \"text additions\" | \"text removals\" | undefined; 'aria-required'?: Booleanish | undefined; 'aria-roledescription'?: string | undefined; 'aria-rowcount'?: number | undefined; 'aria-rowindex'?: number | undefined; 'aria-rowspan'?: number | undefined; 'aria-selected'?: Booleanish | undefined; 'aria-setsize'?: number | undefined; 'aria-sort'?: \"none\" | \"other\" | \"ascending\" | \"descending\" | undefined; 'aria-valuemax'?: number | undefined; 'aria-valuemin'?: number | undefined; 'aria-valuenow'?: number | undefined; 'aria-valuetext'?: string | undefined; dangerouslySetInnerHTML?: { __html: string; } | undefined; onCopy?: React.ClipboardEventHandler | undefined; onCopyCapture?: React.ClipboardEventHandler | undefined; onCut?: React.ClipboardEventHandler | undefined; onCutCapture?: React.ClipboardEventHandler | undefined; onPaste?: React.ClipboardEventHandler | undefined; onPasteCapture?: React.ClipboardEventHandler | undefined; onCompositionEnd?: React.CompositionEventHandler | undefined; onCompositionEndCapture?: React.CompositionEventHandler | undefined; onCompositionStart?: React.CompositionEventHandler | undefined; onCompositionStartCapture?: React.CompositionEventHandler | undefined; onCompositionUpdate?: React.CompositionEventHandler | undefined; onCompositionUpdateCapture?: React.CompositionEventHandler | undefined; onFocus?: React.FocusEventHandler | undefined; onFocusCapture?: React.FocusEventHandler | undefined; onBlur?: React.FocusEventHandler | undefined; onBlurCapture?: React.FocusEventHandler | undefined; onChangeCapture?: React.FormEventHandler | undefined; onBeforeInput?: React.FormEventHandler | undefined; onBeforeInputCapture?: React.FormEventHandler | undefined; onInput?: React.FormEventHandler | undefined; onInputCapture?: React.FormEventHandler | undefined; onReset?: React.FormEventHandler | undefined; onResetCapture?: React.FormEventHandler | undefined; onSubmit?: React.FormEventHandler | undefined; onSubmitCapture?: React.FormEventHandler | undefined; onInvalid?: React.FormEventHandler | undefined; onInvalidCapture?: React.FormEventHandler | undefined; onLoad?: React.ReactEventHandler | undefined; onLoadCapture?: React.ReactEventHandler | undefined; onErrorCapture?: React.ReactEventHandler | undefined; onKeyDownCapture?: React.KeyboardEventHandler | undefined; onKeyPress?: React.KeyboardEventHandler | undefined; onKeyPressCapture?: React.KeyboardEventHandler | undefined; onKeyUp?: React.KeyboardEventHandler | undefined; onKeyUpCapture?: React.KeyboardEventHandler | undefined; onAbort?: React.ReactEventHandler | undefined; onAbortCapture?: React.ReactEventHandler | undefined; onCanPlay?: React.ReactEventHandler | undefined; onCanPlayCapture?: React.ReactEventHandler | undefined; onCanPlayThrough?: React.ReactEventHandler | undefined; onCanPlayThroughCapture?: React.ReactEventHandler | undefined; onDurationChange?: React.ReactEventHandler | undefined; onDurationChangeCapture?: React.ReactEventHandler | undefined; onEmptied?: React.ReactEventHandler | undefined; onEmptiedCapture?: React.ReactEventHandler | undefined; onEncrypted?: React.ReactEventHandler | undefined; onEncryptedCapture?: React.ReactEventHandler | undefined; onEnded?: React.ReactEventHandler | undefined; onEndedCapture?: React.ReactEventHandler | undefined; onLoadedData?: React.ReactEventHandler | undefined; onLoadedDataCapture?: React.ReactEventHandler | undefined; onLoadedMetadata?: React.ReactEventHandler | undefined; onLoadedMetadataCapture?: React.ReactEventHandler | undefined; onLoadStart?: React.ReactEventHandler | undefined; onLoadStartCapture?: React.ReactEventHandler | undefined; onPause?: React.ReactEventHandler | undefined; onPauseCapture?: React.ReactEventHandler | undefined; onPlay?: React.ReactEventHandler | undefined; onPlayCapture?: React.ReactEventHandler | undefined; onPlaying?: React.ReactEventHandler | undefined; onPlayingCapture?: React.ReactEventHandler | undefined; onProgress?: React.ReactEventHandler | undefined; onProgressCapture?: React.ReactEventHandler | undefined; onRateChange?: React.ReactEventHandler | undefined; onRateChangeCapture?: React.ReactEventHandler | undefined; onSeeked?: React.ReactEventHandler | undefined; onSeekedCapture?: React.ReactEventHandler | undefined; onSeeking?: React.ReactEventHandler | undefined; onSeekingCapture?: React.ReactEventHandler | undefined; onStalled?: React.ReactEventHandler | undefined; onStalledCapture?: React.ReactEventHandler | undefined; onSuspend?: React.ReactEventHandler | undefined; onSuspendCapture?: React.ReactEventHandler | undefined; onTimeUpdate?: React.ReactEventHandler | undefined; onTimeUpdateCapture?: React.ReactEventHandler | undefined; onVolumeChange?: React.ReactEventHandler | undefined; onVolumeChangeCapture?: React.ReactEventHandler | undefined; onWaiting?: React.ReactEventHandler | undefined; onWaitingCapture?: React.ReactEventHandler | undefined; onAuxClick?: React.MouseEventHandler | undefined; onAuxClickCapture?: React.MouseEventHandler | undefined; onClickCapture?: React.MouseEventHandler | undefined; onContextMenu?: React.MouseEventHandler | undefined; onContextMenuCapture?: React.MouseEventHandler | undefined; onDoubleClick?: React.MouseEventHandler | undefined; onDoubleClickCapture?: React.MouseEventHandler | undefined; onDrag?: React.DragEventHandler | undefined; onDragCapture?: React.DragEventHandler | undefined; onDragEnd?: React.DragEventHandler | undefined; onDragEndCapture?: React.DragEventHandler | undefined; onDragEnter?: React.DragEventHandler | undefined; onDragEnterCapture?: React.DragEventHandler | undefined; onDragExit?: React.DragEventHandler | undefined; onDragExitCapture?: React.DragEventHandler | undefined; onDragLeave?: React.DragEventHandler | undefined; onDragLeaveCapture?: React.DragEventHandler | undefined; onDragOver?: React.DragEventHandler | undefined; onDragOverCapture?: React.DragEventHandler | undefined; onDragStart?: React.DragEventHandler | undefined; onDragStartCapture?: React.DragEventHandler | undefined; onDrop?: React.DragEventHandler | undefined; onDropCapture?: React.DragEventHandler | undefined; onMouseDown?: React.MouseEventHandler | undefined; onMouseDownCapture?: React.MouseEventHandler | undefined; onMouseEnter?: React.MouseEventHandler | undefined; onMouseLeave?: React.MouseEventHandler | undefined; onMouseMove?: React.MouseEventHandler | undefined; onMouseMoveCapture?: React.MouseEventHandler | undefined; onMouseOut?: React.MouseEventHandler | undefined; onMouseOutCapture?: React.MouseEventHandler | undefined; onMouseOver?: React.MouseEventHandler | undefined; onMouseOverCapture?: React.MouseEventHandler | undefined; onMouseUp?: React.MouseEventHandler | undefined; onMouseUpCapture?: React.MouseEventHandler | undefined; onSelect?: React.ReactEventHandler | undefined; onSelectCapture?: React.ReactEventHandler | undefined; onTouchCancel?: React.TouchEventHandler | undefined; onTouchCancelCapture?: React.TouchEventHandler | undefined; onTouchEnd?: React.TouchEventHandler | undefined; onTouchEndCapture?: React.TouchEventHandler | undefined; onTouchMove?: React.TouchEventHandler | undefined; onTouchMoveCapture?: React.TouchEventHandler | undefined; onTouchStart?: React.TouchEventHandler | undefined; onTouchStartCapture?: React.TouchEventHandler | undefined; onPointerDown?: React.PointerEventHandler | undefined; onPointerDownCapture?: React.PointerEventHandler | undefined; onPointerMove?: React.PointerEventHandler | undefined; onPointerMoveCapture?: React.PointerEventHandler | undefined; onPointerUp?: React.PointerEventHandler | undefined; onPointerUpCapture?: React.PointerEventHandler | undefined; onPointerCancel?: React.PointerEventHandler | undefined; onPointerCancelCapture?: React.PointerEventHandler | undefined; onPointerEnter?: React.PointerEventHandler | undefined; onPointerEnterCapture?: React.PointerEventHandler | undefined; onPointerLeave?: React.PointerEventHandler | undefined; onPointerLeaveCapture?: React.PointerEventHandler | undefined; onPointerOver?: React.PointerEventHandler | undefined; onPointerOverCapture?: React.PointerEventHandler | undefined; onPointerOut?: React.PointerEventHandler | undefined; onPointerOutCapture?: React.PointerEventHandler | undefined; onGotPointerCapture?: React.PointerEventHandler | undefined; onGotPointerCaptureCapture?: React.PointerEventHandler | undefined; onLostPointerCapture?: React.PointerEventHandler | undefined; onLostPointerCaptureCapture?: React.PointerEventHandler | undefined; onScroll?: React.UIEventHandler | undefined; onScrollCapture?: React.UIEventHandler | undefined; onWheel?: React.WheelEventHandler | undefined; onWheelCapture?: React.WheelEventHandler | undefined; onAnimationStart?: React.AnimationEventHandler | undefined; onAnimationStartCapture?: React.AnimationEventHandler | undefined; onAnimationEnd?: React.AnimationEventHandler | undefined; onAnimationEndCapture?: React.AnimationEventHandler | undefined; onAnimationIteration?: React.AnimationEventHandler | undefined; onAnimationIterationCapture?: React.AnimationEventHandler | undefined; onTransitionEnd?: React.TransitionEventHandler | undefined; onTransitionEndCapture?: React.TransitionEventHandler | undefined; 'data-test-subj'?: string | undefined; css?: ", "Interpolation", "<", "Theme", @@ -204,7 +204,7 @@ "ToolTipPositions", " | undefined; anchorProps?: (", "CommonProps", - " & React.HTMLAttributes) | undefined; title?: string | undefined; color?: \"subdued\" | \"accent\" | \"hollow\" | undefined; size?: \"m\" | \"s\" | undefined; } & ", + " & React.HTMLAttributes) | undefined; title?: string | undefined; color?: \"subdued\" | \"accent\" | \"hollow\" | undefined; size?: \"m\" | \"s\" | undefined; alignment?: \"middle\" | \"baseline\" | undefined; } & ", "DisambiguateSet", " & LabelAsString) | (", "CommonProps", @@ -220,7 +220,7 @@ "ToolTipPositions", " | undefined; anchorProps?: (", "CommonProps", - " & React.HTMLAttributes) | undefined; title?: string | undefined; color?: \"subdued\" | \"accent\" | \"hollow\" | undefined; size?: \"m\" | \"s\" | undefined; } & ", + " & React.HTMLAttributes) | undefined; title?: string | undefined; color?: \"subdued\" | \"accent\" | \"hollow\" | undefined; size?: \"m\" | \"s\" | undefined; alignment?: \"middle\" | \"baseline\" | undefined; } & ", "DisambiguateSet", " & ", "DisambiguateSet", @@ -240,7 +240,7 @@ "ToolTipPositions", " | undefined; anchorProps?: (", "CommonProps", - " & React.HTMLAttributes) | undefined; title?: string | undefined; color?: \"subdued\" | \"accent\" | \"hollow\" | undefined; size?: \"m\" | \"s\" | undefined; } & ", + " & React.HTMLAttributes) | undefined; title?: string | undefined; color?: \"subdued\" | \"accent\" | \"hollow\" | undefined; size?: \"m\" | \"s\" | undefined; alignment?: \"middle\" | \"baseline\" | undefined; } & ", "DisambiguateSet", " & LabelAsString) | (", "CommonProps", @@ -258,7 +258,7 @@ "ToolTipPositions", " | undefined; anchorProps?: (", "CommonProps", - " & React.HTMLAttributes) | undefined; title?: string | undefined; color?: \"subdued\" | \"accent\" | \"hollow\" | undefined; size?: \"m\" | \"s\" | undefined; } & ", + " & React.HTMLAttributes) | undefined; title?: string | undefined; color?: \"subdued\" | \"accent\" | \"hollow\" | undefined; size?: \"m\" | \"s\" | undefined; alignment?: \"middle\" | \"baseline\" | undefined; } & ", "DisambiguateSet", " & ", "DisambiguateSet", @@ -278,7 +278,7 @@ "ToolTipPositions", " | undefined; anchorProps?: (", "CommonProps", - " & React.HTMLAttributes) | undefined; title?: string | undefined; color?: \"subdued\" | \"accent\" | \"hollow\" | undefined; size?: \"m\" | \"s\" | undefined; } & ", + " & React.HTMLAttributes) | undefined; title?: string | undefined; color?: \"subdued\" | \"accent\" | \"hollow\" | undefined; size?: \"m\" | \"s\" | undefined; alignment?: \"middle\" | \"baseline\" | undefined; } & ", "DisambiguateSet", " & ", "DisambiguateSet", @@ -298,7 +298,7 @@ "ToolTipPositions", " | undefined; anchorProps?: (", "CommonProps", - " & React.HTMLAttributes) | undefined; title?: string | undefined; color?: \"subdued\" | \"accent\" | \"hollow\" | undefined; size?: \"m\" | \"s\" | undefined; } & ", + " & React.HTMLAttributes) | undefined; title?: string | undefined; color?: \"subdued\" | \"accent\" | \"hollow\" | undefined; size?: \"m\" | \"s\" | undefined; alignment?: \"middle\" | \"baseline\" | undefined; } & ", "DisambiguateSet", " & LabelAsString) | (", "CommonProps", @@ -316,7 +316,7 @@ "ToolTipPositions", " | undefined; anchorProps?: (", "CommonProps", - " & React.HTMLAttributes) | undefined; title?: string | undefined; color?: \"subdued\" | \"accent\" | \"hollow\" | undefined; size?: \"m\" | \"s\" | undefined; } & ", + " & React.HTMLAttributes) | undefined; title?: string | undefined; color?: \"subdued\" | \"accent\" | \"hollow\" | undefined; size?: \"m\" | \"s\" | undefined; alignment?: \"middle\" | \"baseline\" | undefined; } & ", "DisambiguateSet", " & ", "DisambiguateSet", @@ -336,7 +336,7 @@ "ToolTipPositions", " | undefined; anchorProps?: (", "CommonProps", - " & React.HTMLAttributes) | undefined; title?: string | undefined; color?: \"subdued\" | \"accent\" | \"hollow\" | undefined; size?: \"m\" | \"s\" | undefined; } & ", + " & React.HTMLAttributes) | undefined; title?: string | undefined; color?: \"subdued\" | \"accent\" | \"hollow\" | undefined; size?: \"m\" | \"s\" | undefined; alignment?: \"middle\" | \"baseline\" | undefined; } & ", "DisambiguateSet", " & ", "DisambiguateSet", diff --git a/api_docs/kbn_shared_ux_card_no_data.mdx b/api_docs/kbn_shared_ux_card_no_data.mdx index a80a437df5e52..9379ae75ac9e5 100644 --- a/api_docs/kbn_shared_ux_card_no_data.mdx +++ b/api_docs/kbn_shared_ux_card_no_data.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-card-no-data title: "@kbn/shared-ux-card-no-data" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-card-no-data plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-card-no-data'] --- import kbnSharedUxCardNoDataObj from './kbn_shared_ux_card_no_data.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_card_no_data_mocks.mdx b/api_docs/kbn_shared_ux_card_no_data_mocks.mdx index f4e8911d252ab..655543a6fa8f5 100644 --- a/api_docs/kbn_shared_ux_card_no_data_mocks.mdx +++ b/api_docs/kbn_shared_ux_card_no_data_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-card-no-data-mocks title: "@kbn/shared-ux-card-no-data-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-card-no-data-mocks plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-card-no-data-mocks'] --- import kbnSharedUxCardNoDataMocksObj from './kbn_shared_ux_card_no_data_mocks.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_file_context.devdocs.json b/api_docs/kbn_shared_ux_file_context.devdocs.json new file mode 100644 index 0000000000000..02025b778807c --- /dev/null +++ b/api_docs/kbn_shared_ux_file_context.devdocs.json @@ -0,0 +1,117 @@ +{ + "id": "@kbn/shared-ux-file-context", + "client": { + "classes": [], + "functions": [], + "interfaces": [], + "enums": [], + "misc": [], + "objects": [] + }, + "server": { + "classes": [], + "functions": [], + "interfaces": [], + "enums": [], + "misc": [], + "objects": [] + }, + "common": { + "classes": [], + "functions": [ + { + "parentPluginId": "@kbn/shared-ux-file-context", + "id": "def-common.FilesContext", + "type": "Function", + "tags": [], + "label": "FilesContext", + "description": [], + "signature": [ + "({ client, children }: React.PropsWithChildren) => JSX.Element" + ], + "path": "packages/shared-ux/file/context/src/index.tsx", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/shared-ux-file-context", + "id": "def-common.FilesContext.$1", + "type": "CompoundType", + "tags": [], + "label": "{ client, children }", + "description": [], + "signature": [ + "React.PropsWithChildren" + ], + "path": "packages/shared-ux/file/context/src/index.tsx", + "deprecated": false, + "trackAdoption": false, + "isRequired": true + } + ], + "returnComment": [], + "initialIsOpen": false + }, + { + "parentPluginId": "@kbn/shared-ux-file-context", + "id": "def-common.useFilesContext", + "type": "Function", + "tags": [], + "label": "useFilesContext", + "description": [], + "signature": [ + "() => ", + { + "pluginId": "@kbn/shared-ux-file-context", + "scope": "common", + "docId": "kibKbnSharedUxFileContextPluginApi", + "section": "def-common.FilesContextValue", + "text": "FilesContextValue" + } + ], + "path": "packages/shared-ux/file/context/src/index.tsx", + "deprecated": false, + "trackAdoption": false, + "children": [], + "returnComment": [], + "initialIsOpen": false + } + ], + "interfaces": [ + { + "parentPluginId": "@kbn/shared-ux-file-context", + "id": "def-common.FilesContextValue", + "type": "Interface", + "tags": [], + "label": "FilesContextValue", + "description": [], + "path": "packages/shared-ux/file/context/src/index.tsx", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/shared-ux-file-context", + "id": "def-common.FilesContextValue.client", + "type": "Object", + "tags": [], + "label": "client", + "description": [ + "\nA files client that will be used process uploads." + ], + "signature": [ + "BaseFilesClient", + "" + ], + "path": "packages/shared-ux/file/context/src/index.tsx", + "deprecated": false, + "trackAdoption": false + } + ], + "initialIsOpen": false + } + ], + "enums": [], + "misc": [], + "objects": [] + } +} \ No newline at end of file diff --git a/api_docs/kbn_shared_ux_file_context.mdx b/api_docs/kbn_shared_ux_file_context.mdx new file mode 100644 index 0000000000000..d60cdfeadfde7 --- /dev/null +++ b/api_docs/kbn_shared_ux_file_context.mdx @@ -0,0 +1,33 @@ +--- +#### +#### This document is auto-generated and is meant to be viewed inside our experimental, new docs system. +#### Reach out in #docs-engineering for more info. +#### +id: kibKbnSharedUxFileContextPluginApi +slug: /kibana-dev-docs/api/kbn-shared-ux-file-context +title: "@kbn/shared-ux-file-context" +image: https://source.unsplash.com/400x175/?github +description: API docs for the @kbn/shared-ux-file-context plugin +date: 2022-12-07 +tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-file-context'] +--- +import kbnSharedUxFileContextObj from './kbn_shared_ux_file_context.devdocs.json'; + + + +Contact [Owner missing] for questions regarding this plugin. + +**Code health stats** + +| Public API count | Any count | Items lacking comments | Missing exports | +|-------------------|-----------|------------------------|-----------------| +| 5 | 0 | 4 | 0 | + +## Common + +### Functions + + +### Interfaces + + diff --git a/api_docs/kbn_shared_ux_file_image.devdocs.json b/api_docs/kbn_shared_ux_file_image.devdocs.json index b1d29de390143..5d3350f6cf444 100644 --- a/api_docs/kbn_shared_ux_file_image.devdocs.json +++ b/api_docs/kbn_shared_ux_file_image.devdocs.json @@ -82,7 +82,9 @@ "label": "Props", "description": [], "signature": [ - "{ meta?: any; } & ", + "{ meta?: ", + "FileImageMetadata", + " | undefined; } & ", "EuiImageProps" ], "path": "packages/shared-ux/file/image/impl/src/image.tsx", diff --git a/api_docs/kbn_shared_ux_file_image.mdx b/api_docs/kbn_shared_ux_file_image.mdx index a14989362503f..9a249e5a86216 100644 --- a/api_docs/kbn_shared_ux_file_image.mdx +++ b/api_docs/kbn_shared_ux_file_image.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-file-image title: "@kbn/shared-ux-file-image" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-file-image plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-file-image'] --- import kbnSharedUxFileImageObj from './kbn_shared_ux_file_image.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_file_image_mocks.mdx b/api_docs/kbn_shared_ux_file_image_mocks.mdx index 5e3d338417828..7c9ba99759244 100644 --- a/api_docs/kbn_shared_ux_file_image_mocks.mdx +++ b/api_docs/kbn_shared_ux_file_image_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-file-image-mocks title: "@kbn/shared-ux-file-image-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-file-image-mocks plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-file-image-mocks'] --- import kbnSharedUxFileImageMocksObj from './kbn_shared_ux_file_image_mocks.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_file_mocks.devdocs.json b/api_docs/kbn_shared_ux_file_mocks.devdocs.json new file mode 100644 index 0000000000000..67aaba2cc7ed1 --- /dev/null +++ b/api_docs/kbn_shared_ux_file_mocks.devdocs.json @@ -0,0 +1,55 @@ +{ + "id": "@kbn/shared-ux-file-mocks", + "client": { + "classes": [], + "functions": [], + "interfaces": [], + "enums": [], + "misc": [], + "objects": [] + }, + "server": { + "classes": [], + "functions": [], + "interfaces": [], + "enums": [], + "misc": [], + "objects": [] + }, + "common": { + "classes": [], + "functions": [ + { + "parentPluginId": "@kbn/shared-ux-file-mocks", + "id": "def-common.createMockFilesClient", + "type": "Function", + "tags": [], + "label": "createMockFilesClient", + "description": [], + "signature": [ + "() => ", + { + "pluginId": "@kbn/utility-types-jest", + "scope": "server", + "docId": "kibKbnUtilityTypesJestPluginApi", + "section": "def-server.DeeplyMockedKeys", + "text": "DeeplyMockedKeys" + }, + "<", + "BaseFilesClient", + ">" + ], + "path": "packages/shared-ux/file/mocks/index.ts", + "deprecated": false, + "trackAdoption": false, + "children": [], + "returnComment": [], + "initialIsOpen": false + } + ], + "interfaces": [], + "enums": [], + "misc": [], + "objects": [] + } +} \ No newline at end of file diff --git a/api_docs/kbn_shared_ux_file_mocks.mdx b/api_docs/kbn_shared_ux_file_mocks.mdx new file mode 100644 index 0000000000000..9dee7a63f2789 --- /dev/null +++ b/api_docs/kbn_shared_ux_file_mocks.mdx @@ -0,0 +1,30 @@ +--- +#### +#### This document is auto-generated and is meant to be viewed inside our experimental, new docs system. +#### Reach out in #docs-engineering for more info. +#### +id: kibKbnSharedUxFileMocksPluginApi +slug: /kibana-dev-docs/api/kbn-shared-ux-file-mocks +title: "@kbn/shared-ux-file-mocks" +image: https://source.unsplash.com/400x175/?github +description: API docs for the @kbn/shared-ux-file-mocks plugin +date: 2022-12-07 +tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-file-mocks'] +--- +import kbnSharedUxFileMocksObj from './kbn_shared_ux_file_mocks.devdocs.json'; + + + +Contact [Owner missing] for questions regarding this plugin. + +**Code health stats** + +| Public API count | Any count | Items lacking comments | Missing exports | +|-------------------|-----------|------------------------|-----------------| +| 1 | 0 | 1 | 0 | + +## Common + +### Functions + + diff --git a/api_docs/kbn_shared_ux_file_util.devdocs.json b/api_docs/kbn_shared_ux_file_util.devdocs.json index c14e553be0947..c471a74aeec8e 100644 --- a/api_docs/kbn_shared_ux_file_util.devdocs.json +++ b/api_docs/kbn_shared_ux_file_util.devdocs.json @@ -143,7 +143,9 @@ "\nExtract image metadata, assumes that file or blob as an image!" ], "signature": [ - "(file: Blob | File) => Promise" + "(file: Blob | File) => Promise<", + "FileImageMetadata", + " | undefined>" ], "path": "packages/shared-ux/file/util/src/image_metadata.ts", "deprecated": false, @@ -212,6 +214,42 @@ ], "returnComment": [], "initialIsOpen": false + }, + { + "parentPluginId": "@kbn/shared-ux-file-util", + "id": "def-common.useBehaviorSubject", + "type": "Function", + "tags": [], + "label": "useBehaviorSubject", + "description": [], + "signature": [ + "(o$: ", + "BehaviorSubject", + ") => T" + ], + "path": "packages/shared-ux/file/util/src/use_behavior_subject.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/shared-ux-file-util", + "id": "def-common.useBehaviorSubject.$1", + "type": "Object", + "tags": [], + "label": "o$", + "description": [], + "signature": [ + "BehaviorSubject", + "" + ], + "path": "packages/shared-ux/file/util/src/use_behavior_subject.ts", + "deprecated": false, + "trackAdoption": false, + "isRequired": true + } + ], + "returnComment": [], + "initialIsOpen": false } ], "interfaces": [], @@ -225,7 +263,9 @@ "label": "ImageMetadataFactory", "description": [], "signature": [ - "(file: Blob | File) => Promise" + "(file: Blob | File) => Promise<", + "FileImageMetadata", + " | undefined>" ], "path": "packages/shared-ux/file/util/src/image_metadata.ts", "deprecated": false, diff --git a/api_docs/kbn_shared_ux_file_util.mdx b/api_docs/kbn_shared_ux_file_util.mdx index 686c5d9a8a46a..959ddcacd783e 100644 --- a/api_docs/kbn_shared_ux_file_util.mdx +++ b/api_docs/kbn_shared_ux_file_util.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-file-util title: "@kbn/shared-ux-file-util" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-file-util plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-file-util'] --- import kbnSharedUxFileUtilObj from './kbn_shared_ux_file_util.devdocs.json'; @@ -21,7 +21,7 @@ Contact [Owner missing] for questions regarding this plugin. | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 15 | 0 | 13 | 0 | +| 17 | 0 | 15 | 0 | ## Common diff --git a/api_docs/kbn_shared_ux_link_redirect_app.mdx b/api_docs/kbn_shared_ux_link_redirect_app.mdx index 7d8a11001661b..4d403e0f3d7e4 100644 --- a/api_docs/kbn_shared_ux_link_redirect_app.mdx +++ b/api_docs/kbn_shared_ux_link_redirect_app.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-link-redirect-app title: "@kbn/shared-ux-link-redirect-app" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-link-redirect-app plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-link-redirect-app'] --- import kbnSharedUxLinkRedirectAppObj from './kbn_shared_ux_link_redirect_app.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_link_redirect_app_mocks.mdx b/api_docs/kbn_shared_ux_link_redirect_app_mocks.mdx index 61c6e17238230..86edf0db66f4e 100644 --- a/api_docs/kbn_shared_ux_link_redirect_app_mocks.mdx +++ b/api_docs/kbn_shared_ux_link_redirect_app_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-link-redirect-app-mocks title: "@kbn/shared-ux-link-redirect-app-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-link-redirect-app-mocks plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-link-redirect-app-mocks'] --- import kbnSharedUxLinkRedirectAppMocksObj from './kbn_shared_ux_link_redirect_app_mocks.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_markdown.mdx b/api_docs/kbn_shared_ux_markdown.mdx index 90b6c0d6b59f9..470d000fb88c7 100644 --- a/api_docs/kbn_shared_ux_markdown.mdx +++ b/api_docs/kbn_shared_ux_markdown.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-markdown title: "@kbn/shared-ux-markdown" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-markdown plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-markdown'] --- import kbnSharedUxMarkdownObj from './kbn_shared_ux_markdown.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_markdown_mocks.devdocs.json b/api_docs/kbn_shared_ux_markdown_mocks.devdocs.json index 0f5ffb13f1421..addc3f7f83a31 100644 --- a/api_docs/kbn_shared_ux_markdown_mocks.devdocs.json +++ b/api_docs/kbn_shared_ux_markdown_mocks.devdocs.json @@ -433,7 +433,7 @@ "label": "getServices", "description": [], "signature": [ - "() => { children?: React.ReactNode; value?: string | undefined; onError?: React.ReactEventHandler | undefined; hidden?: boolean | undefined; color?: string | undefined; id?: string | undefined; className?: string | undefined; title?: string | undefined; onChange?: ((value: string) => void) | undefined; onKeyDown?: React.KeyboardEventHandler | undefined; onClick?: React.MouseEventHandler | undefined; security?: string | undefined; defaultValue?: string | number | readonly string[] | undefined; lang?: string | undefined; defaultChecked?: boolean | undefined; suppressContentEditableWarning?: boolean | undefined; suppressHydrationWarning?: boolean | undefined; accessKey?: string | undefined; contentEditable?: \"inherit\" | Booleanish | undefined; contextMenu?: string | undefined; dir?: string | undefined; draggable?: Booleanish | undefined; placeholder?: string | undefined; slot?: string | undefined; spellCheck?: Booleanish | undefined; style?: React.CSSProperties | undefined; tabIndex?: number | undefined; translate?: \"no\" | \"yes\" | undefined; radioGroup?: string | undefined; role?: React.AriaRole | undefined; about?: string | undefined; datatype?: string | undefined; inlist?: any; prefix?: string | undefined; property?: string | undefined; resource?: string | undefined; typeof?: string | undefined; vocab?: string | undefined; autoCapitalize?: string | undefined; autoCorrect?: string | undefined; autoSave?: string | undefined; itemProp?: string | undefined; itemScope?: boolean | undefined; itemType?: string | undefined; itemID?: string | undefined; itemRef?: string | undefined; results?: number | undefined; unselectable?: \"on\" | \"off\" | undefined; inputMode?: \"none\" | \"email\" | \"search\" | \"text\" | \"tel\" | \"url\" | \"numeric\" | \"decimal\" | undefined; is?: string | undefined; 'aria-activedescendant'?: string | undefined; 'aria-atomic'?: Booleanish | undefined; 'aria-autocomplete'?: \"none\" | \"list\" | \"inline\" | \"both\" | undefined; 'aria-busy'?: Booleanish | undefined; 'aria-checked'?: boolean | \"mixed\" | \"false\" | \"true\" | undefined; 'aria-colcount'?: number | undefined; 'aria-colindex'?: number | undefined; 'aria-colspan'?: number | undefined; 'aria-controls'?: string | undefined; 'aria-current'?: boolean | \"date\" | \"location\" | \"time\" | \"page\" | \"false\" | \"true\" | \"step\" | undefined; 'aria-describedby'?: string | undefined; 'aria-details'?: string | undefined; 'aria-disabled'?: Booleanish | undefined; 'aria-dropeffect'?: \"none\" | \"copy\" | \"link\" | \"execute\" | \"move\" | \"popup\" | undefined; 'aria-errormessage'?: string | undefined; 'aria-expanded'?: Booleanish | undefined; 'aria-flowto'?: string | undefined; 'aria-grabbed'?: Booleanish | undefined; 'aria-haspopup'?: boolean | \"grid\" | \"menu\" | \"false\" | \"true\" | \"dialog\" | \"listbox\" | \"tree\" | undefined; 'aria-hidden'?: Booleanish | undefined; 'aria-invalid'?: boolean | \"false\" | \"true\" | \"grammar\" | \"spelling\" | undefined; 'aria-keyshortcuts'?: string | undefined; 'aria-label'?: string | undefined; 'aria-labelledby'?: string | undefined; 'aria-level'?: number | undefined; 'aria-live'?: \"off\" | \"assertive\" | \"polite\" | undefined; 'aria-modal'?: Booleanish | undefined; 'aria-multiline'?: Booleanish | undefined; 'aria-multiselectable'?: Booleanish | undefined; 'aria-orientation'?: \"horizontal\" | \"vertical\" | undefined; 'aria-owns'?: string | undefined; 'aria-placeholder'?: string | undefined; 'aria-posinset'?: number | undefined; 'aria-pressed'?: boolean | \"mixed\" | \"false\" | \"true\" | undefined; 'aria-readonly'?: Booleanish | undefined; 'aria-relevant'?: \"all\" | \"text\" | \"additions\" | \"additions removals\" | \"additions text\" | \"removals\" | \"removals additions\" | \"removals text\" | \"text additions\" | \"text removals\" | undefined; 'aria-required'?: Booleanish | undefined; 'aria-roledescription'?: string | undefined; 'aria-rowcount'?: number | undefined; 'aria-rowindex'?: number | undefined; 'aria-rowspan'?: number | undefined; 'aria-selected'?: Booleanish | undefined; 'aria-setsize'?: number | undefined; 'aria-sort'?: \"none\" | \"other\" | \"ascending\" | \"descending\" | undefined; 'aria-valuemax'?: number | undefined; 'aria-valuemin'?: number | undefined; 'aria-valuenow'?: number | undefined; 'aria-valuetext'?: string | undefined; dangerouslySetInnerHTML?: { __html: string; } | undefined; onCopy?: React.ClipboardEventHandler | undefined; onCopyCapture?: React.ClipboardEventHandler | undefined; onCut?: React.ClipboardEventHandler | undefined; onCutCapture?: React.ClipboardEventHandler | undefined; onPaste?: React.ClipboardEventHandler | undefined; onPasteCapture?: React.ClipboardEventHandler | undefined; onCompositionEnd?: React.CompositionEventHandler | undefined; onCompositionEndCapture?: React.CompositionEventHandler | undefined; onCompositionStart?: React.CompositionEventHandler | undefined; onCompositionStartCapture?: React.CompositionEventHandler | undefined; onCompositionUpdate?: React.CompositionEventHandler | undefined; onCompositionUpdateCapture?: React.CompositionEventHandler | undefined; onFocus?: React.FocusEventHandler | undefined; onFocusCapture?: React.FocusEventHandler | undefined; onBlur?: React.FocusEventHandler | undefined; onBlurCapture?: React.FocusEventHandler | undefined; onChangeCapture?: React.FormEventHandler | undefined; onBeforeInput?: React.FormEventHandler | undefined; onBeforeInputCapture?: React.FormEventHandler | undefined; onInput?: React.FormEventHandler | undefined; onInputCapture?: React.FormEventHandler | undefined; onReset?: React.FormEventHandler | undefined; onResetCapture?: React.FormEventHandler | undefined; onSubmit?: React.FormEventHandler | undefined; onSubmitCapture?: React.FormEventHandler | undefined; onInvalid?: React.FormEventHandler | undefined; onInvalidCapture?: React.FormEventHandler | undefined; onLoad?: React.ReactEventHandler | undefined; onLoadCapture?: React.ReactEventHandler | undefined; onErrorCapture?: React.ReactEventHandler | undefined; onKeyDownCapture?: React.KeyboardEventHandler | undefined; onKeyPress?: React.KeyboardEventHandler | undefined; onKeyPressCapture?: React.KeyboardEventHandler | undefined; onKeyUp?: React.KeyboardEventHandler | undefined; onKeyUpCapture?: React.KeyboardEventHandler | undefined; onAbort?: React.ReactEventHandler | undefined; onAbortCapture?: React.ReactEventHandler | undefined; onCanPlay?: React.ReactEventHandler | undefined; onCanPlayCapture?: React.ReactEventHandler | undefined; onCanPlayThrough?: React.ReactEventHandler | undefined; onCanPlayThroughCapture?: React.ReactEventHandler | undefined; onDurationChange?: React.ReactEventHandler | undefined; onDurationChangeCapture?: React.ReactEventHandler | undefined; onEmptied?: React.ReactEventHandler | undefined; onEmptiedCapture?: React.ReactEventHandler | undefined; onEncrypted?: React.ReactEventHandler | undefined; onEncryptedCapture?: React.ReactEventHandler | undefined; onEnded?: React.ReactEventHandler | undefined; onEndedCapture?: React.ReactEventHandler | undefined; onLoadedData?: React.ReactEventHandler | undefined; onLoadedDataCapture?: React.ReactEventHandler | undefined; onLoadedMetadata?: React.ReactEventHandler | undefined; onLoadedMetadataCapture?: React.ReactEventHandler | undefined; onLoadStart?: React.ReactEventHandler | undefined; onLoadStartCapture?: React.ReactEventHandler | undefined; onPause?: React.ReactEventHandler | undefined; onPauseCapture?: React.ReactEventHandler | undefined; onPlay?: React.ReactEventHandler | undefined; onPlayCapture?: React.ReactEventHandler | undefined; onPlaying?: React.ReactEventHandler | undefined; onPlayingCapture?: React.ReactEventHandler | undefined; onProgress?: React.ReactEventHandler | undefined; onProgressCapture?: React.ReactEventHandler | undefined; onRateChange?: React.ReactEventHandler | undefined; onRateChangeCapture?: React.ReactEventHandler | undefined; onSeeked?: React.ReactEventHandler | undefined; onSeekedCapture?: React.ReactEventHandler | undefined; onSeeking?: React.ReactEventHandler | undefined; onSeekingCapture?: React.ReactEventHandler | undefined; onStalled?: React.ReactEventHandler | undefined; onStalledCapture?: React.ReactEventHandler | undefined; onSuspend?: React.ReactEventHandler | undefined; onSuspendCapture?: React.ReactEventHandler | undefined; onTimeUpdate?: React.ReactEventHandler | undefined; onTimeUpdateCapture?: React.ReactEventHandler | undefined; onVolumeChange?: React.ReactEventHandler | undefined; onVolumeChangeCapture?: React.ReactEventHandler | undefined; onWaiting?: React.ReactEventHandler | undefined; onWaitingCapture?: React.ReactEventHandler | undefined; onAuxClick?: React.MouseEventHandler | undefined; onAuxClickCapture?: React.MouseEventHandler | undefined; onClickCapture?: React.MouseEventHandler | undefined; onContextMenu?: React.MouseEventHandler | undefined; onContextMenuCapture?: React.MouseEventHandler | undefined; onDoubleClick?: React.MouseEventHandler | undefined; onDoubleClickCapture?: React.MouseEventHandler | undefined; onDrag?: React.DragEventHandler | undefined; onDragCapture?: React.DragEventHandler | undefined; onDragEnd?: React.DragEventHandler | undefined; onDragEndCapture?: React.DragEventHandler | undefined; onDragEnter?: React.DragEventHandler | undefined; onDragEnterCapture?: React.DragEventHandler | undefined; onDragExit?: React.DragEventHandler | undefined; onDragExitCapture?: React.DragEventHandler | undefined; onDragLeave?: React.DragEventHandler | undefined; onDragLeaveCapture?: React.DragEventHandler | undefined; onDragOver?: React.DragEventHandler | undefined; onDragOverCapture?: React.DragEventHandler | undefined; onDragStart?: React.DragEventHandler | undefined; onDragStartCapture?: React.DragEventHandler | undefined; onDrop?: React.DragEventHandler | undefined; onDropCapture?: React.DragEventHandler | undefined; onMouseDown?: React.MouseEventHandler | undefined; onMouseDownCapture?: React.MouseEventHandler | undefined; onMouseEnter?: React.MouseEventHandler | undefined; onMouseLeave?: React.MouseEventHandler | undefined; onMouseMove?: React.MouseEventHandler | undefined; onMouseMoveCapture?: React.MouseEventHandler | undefined; onMouseOut?: React.MouseEventHandler | undefined; onMouseOutCapture?: React.MouseEventHandler | undefined; onMouseOver?: React.MouseEventHandler | undefined; onMouseOverCapture?: React.MouseEventHandler | undefined; onMouseUp?: React.MouseEventHandler | undefined; onMouseUpCapture?: React.MouseEventHandler | undefined; onSelect?: React.ReactEventHandler | undefined; onSelectCapture?: React.ReactEventHandler | undefined; onTouchCancel?: React.TouchEventHandler | undefined; onTouchCancelCapture?: React.TouchEventHandler | undefined; onTouchEnd?: React.TouchEventHandler | undefined; onTouchEndCapture?: React.TouchEventHandler | undefined; onTouchMove?: React.TouchEventHandler | undefined; onTouchMoveCapture?: React.TouchEventHandler | undefined; onTouchStart?: React.TouchEventHandler | undefined; onTouchStartCapture?: React.TouchEventHandler | undefined; onPointerDown?: React.PointerEventHandler | undefined; onPointerDownCapture?: React.PointerEventHandler | undefined; onPointerMove?: React.PointerEventHandler | undefined; onPointerMoveCapture?: React.PointerEventHandler | undefined; onPointerUp?: React.PointerEventHandler | undefined; onPointerUpCapture?: React.PointerEventHandler | undefined; onPointerCancel?: React.PointerEventHandler | undefined; onPointerCancelCapture?: React.PointerEventHandler | undefined; onPointerEnter?: React.PointerEventHandler | undefined; onPointerEnterCapture?: React.PointerEventHandler | undefined; onPointerLeave?: React.PointerEventHandler | undefined; onPointerLeaveCapture?: React.PointerEventHandler | undefined; onPointerOver?: React.PointerEventHandler | undefined; onPointerOverCapture?: React.PointerEventHandler | undefined; onPointerOut?: React.PointerEventHandler | undefined; onPointerOutCapture?: React.PointerEventHandler | undefined; onGotPointerCapture?: React.PointerEventHandler | undefined; onGotPointerCaptureCapture?: React.PointerEventHandler | undefined; onLostPointerCapture?: React.PointerEventHandler | undefined; onLostPointerCaptureCapture?: React.PointerEventHandler | undefined; onScroll?: React.UIEventHandler | undefined; onScrollCapture?: React.UIEventHandler | undefined; onWheel?: React.WheelEventHandler | undefined; onWheelCapture?: React.WheelEventHandler | undefined; onAnimationStart?: React.AnimationEventHandler | undefined; onAnimationStartCapture?: React.AnimationEventHandler | undefined; onAnimationEnd?: React.AnimationEventHandler | undefined; onAnimationEndCapture?: React.AnimationEventHandler | undefined; onAnimationIteration?: React.AnimationEventHandler | undefined; onAnimationIterationCapture?: React.AnimationEventHandler | undefined; onTransitionEnd?: React.TransitionEventHandler | undefined; onTransitionEndCapture?: React.TransitionEventHandler | undefined; 'data-test-subj'?: string | undefined; css?: ", + "() => { children?: React.ReactNode; value?: string | undefined; onError?: React.ReactEventHandler | undefined; hidden?: boolean | undefined; color?: string | undefined; id?: string | undefined; className?: string | undefined; title?: string | undefined; onChange?: ((value: string) => void) | undefined; onKeyDown?: React.KeyboardEventHandler | undefined; onClick?: React.MouseEventHandler | undefined; security?: string | undefined; defaultValue?: string | number | readonly string[] | undefined; lang?: string | undefined; defaultChecked?: boolean | undefined; suppressContentEditableWarning?: boolean | undefined; suppressHydrationWarning?: boolean | undefined; accessKey?: string | undefined; contentEditable?: \"inherit\" | Booleanish | undefined; contextMenu?: string | undefined; dir?: string | undefined; draggable?: Booleanish | undefined; placeholder?: string | undefined; slot?: string | undefined; spellCheck?: Booleanish | undefined; style?: React.CSSProperties | undefined; tabIndex?: number | undefined; translate?: \"no\" | \"yes\" | undefined; radioGroup?: string | undefined; role?: React.AriaRole | undefined; about?: string | undefined; datatype?: string | undefined; inlist?: any; prefix?: string | undefined; property?: string | undefined; resource?: string | undefined; typeof?: string | undefined; vocab?: string | undefined; autoCapitalize?: string | undefined; autoCorrect?: string | undefined; autoSave?: string | undefined; itemProp?: string | undefined; itemScope?: boolean | undefined; itemType?: string | undefined; itemID?: string | undefined; itemRef?: string | undefined; results?: number | undefined; unselectable?: \"on\" | \"off\" | undefined; inputMode?: \"none\" | \"email\" | \"search\" | \"text\" | \"url\" | \"tel\" | \"numeric\" | \"decimal\" | undefined; is?: string | undefined; 'aria-activedescendant'?: string | undefined; 'aria-atomic'?: Booleanish | undefined; 'aria-autocomplete'?: \"none\" | \"list\" | \"inline\" | \"both\" | undefined; 'aria-busy'?: Booleanish | undefined; 'aria-checked'?: boolean | \"mixed\" | \"false\" | \"true\" | undefined; 'aria-colcount'?: number | undefined; 'aria-colindex'?: number | undefined; 'aria-colspan'?: number | undefined; 'aria-controls'?: string | undefined; 'aria-current'?: boolean | \"date\" | \"location\" | \"time\" | \"page\" | \"false\" | \"true\" | \"step\" | undefined; 'aria-describedby'?: string | undefined; 'aria-details'?: string | undefined; 'aria-disabled'?: Booleanish | undefined; 'aria-dropeffect'?: \"none\" | \"copy\" | \"link\" | \"execute\" | \"move\" | \"popup\" | undefined; 'aria-errormessage'?: string | undefined; 'aria-expanded'?: Booleanish | undefined; 'aria-flowto'?: string | undefined; 'aria-grabbed'?: Booleanish | undefined; 'aria-haspopup'?: boolean | \"grid\" | \"menu\" | \"false\" | \"true\" | \"dialog\" | \"listbox\" | \"tree\" | undefined; 'aria-hidden'?: Booleanish | undefined; 'aria-invalid'?: boolean | \"false\" | \"true\" | \"grammar\" | \"spelling\" | undefined; 'aria-keyshortcuts'?: string | undefined; 'aria-label'?: string | undefined; 'aria-labelledby'?: string | undefined; 'aria-level'?: number | undefined; 'aria-live'?: \"off\" | \"assertive\" | \"polite\" | undefined; 'aria-modal'?: Booleanish | undefined; 'aria-multiline'?: Booleanish | undefined; 'aria-multiselectable'?: Booleanish | undefined; 'aria-orientation'?: \"horizontal\" | \"vertical\" | undefined; 'aria-owns'?: string | undefined; 'aria-placeholder'?: string | undefined; 'aria-posinset'?: number | undefined; 'aria-pressed'?: boolean | \"mixed\" | \"false\" | \"true\" | undefined; 'aria-readonly'?: Booleanish | undefined; 'aria-relevant'?: \"all\" | \"text\" | \"additions\" | \"additions removals\" | \"additions text\" | \"removals\" | \"removals additions\" | \"removals text\" | \"text additions\" | \"text removals\" | undefined; 'aria-required'?: Booleanish | undefined; 'aria-roledescription'?: string | undefined; 'aria-rowcount'?: number | undefined; 'aria-rowindex'?: number | undefined; 'aria-rowspan'?: number | undefined; 'aria-selected'?: Booleanish | undefined; 'aria-setsize'?: number | undefined; 'aria-sort'?: \"none\" | \"other\" | \"ascending\" | \"descending\" | undefined; 'aria-valuemax'?: number | undefined; 'aria-valuemin'?: number | undefined; 'aria-valuenow'?: number | undefined; 'aria-valuetext'?: string | undefined; dangerouslySetInnerHTML?: { __html: string; } | undefined; onCopy?: React.ClipboardEventHandler | undefined; onCopyCapture?: React.ClipboardEventHandler | undefined; onCut?: React.ClipboardEventHandler | undefined; onCutCapture?: React.ClipboardEventHandler | undefined; onPaste?: React.ClipboardEventHandler | undefined; onPasteCapture?: React.ClipboardEventHandler | undefined; onCompositionEnd?: React.CompositionEventHandler | undefined; onCompositionEndCapture?: React.CompositionEventHandler | undefined; onCompositionStart?: React.CompositionEventHandler | undefined; onCompositionStartCapture?: React.CompositionEventHandler | undefined; onCompositionUpdate?: React.CompositionEventHandler | undefined; onCompositionUpdateCapture?: React.CompositionEventHandler | undefined; onFocus?: React.FocusEventHandler | undefined; onFocusCapture?: React.FocusEventHandler | undefined; onBlur?: React.FocusEventHandler | undefined; onBlurCapture?: React.FocusEventHandler | undefined; onChangeCapture?: React.FormEventHandler | undefined; onBeforeInput?: React.FormEventHandler | undefined; onBeforeInputCapture?: React.FormEventHandler | undefined; onInput?: React.FormEventHandler | undefined; onInputCapture?: React.FormEventHandler | undefined; onReset?: React.FormEventHandler | undefined; onResetCapture?: React.FormEventHandler | undefined; onSubmit?: React.FormEventHandler | undefined; onSubmitCapture?: React.FormEventHandler | undefined; onInvalid?: React.FormEventHandler | undefined; onInvalidCapture?: React.FormEventHandler | undefined; onLoad?: React.ReactEventHandler | undefined; onLoadCapture?: React.ReactEventHandler | undefined; onErrorCapture?: React.ReactEventHandler | undefined; onKeyDownCapture?: React.KeyboardEventHandler | undefined; onKeyPress?: React.KeyboardEventHandler | undefined; onKeyPressCapture?: React.KeyboardEventHandler | undefined; onKeyUp?: React.KeyboardEventHandler | undefined; onKeyUpCapture?: React.KeyboardEventHandler | undefined; onAbort?: React.ReactEventHandler | undefined; onAbortCapture?: React.ReactEventHandler | undefined; onCanPlay?: React.ReactEventHandler | undefined; onCanPlayCapture?: React.ReactEventHandler | undefined; onCanPlayThrough?: React.ReactEventHandler | undefined; onCanPlayThroughCapture?: React.ReactEventHandler | undefined; onDurationChange?: React.ReactEventHandler | undefined; onDurationChangeCapture?: React.ReactEventHandler | undefined; onEmptied?: React.ReactEventHandler | undefined; onEmptiedCapture?: React.ReactEventHandler | undefined; onEncrypted?: React.ReactEventHandler | undefined; onEncryptedCapture?: React.ReactEventHandler | undefined; onEnded?: React.ReactEventHandler | undefined; onEndedCapture?: React.ReactEventHandler | undefined; onLoadedData?: React.ReactEventHandler | undefined; onLoadedDataCapture?: React.ReactEventHandler | undefined; onLoadedMetadata?: React.ReactEventHandler | undefined; onLoadedMetadataCapture?: React.ReactEventHandler | undefined; onLoadStart?: React.ReactEventHandler | undefined; onLoadStartCapture?: React.ReactEventHandler | undefined; onPause?: React.ReactEventHandler | undefined; onPauseCapture?: React.ReactEventHandler | undefined; onPlay?: React.ReactEventHandler | undefined; onPlayCapture?: React.ReactEventHandler | undefined; onPlaying?: React.ReactEventHandler | undefined; onPlayingCapture?: React.ReactEventHandler | undefined; onProgress?: React.ReactEventHandler | undefined; onProgressCapture?: React.ReactEventHandler | undefined; onRateChange?: React.ReactEventHandler | undefined; onRateChangeCapture?: React.ReactEventHandler | undefined; onSeeked?: React.ReactEventHandler | undefined; onSeekedCapture?: React.ReactEventHandler | undefined; onSeeking?: React.ReactEventHandler | undefined; onSeekingCapture?: React.ReactEventHandler | undefined; onStalled?: React.ReactEventHandler | undefined; onStalledCapture?: React.ReactEventHandler | undefined; onSuspend?: React.ReactEventHandler | undefined; onSuspendCapture?: React.ReactEventHandler | undefined; onTimeUpdate?: React.ReactEventHandler | undefined; onTimeUpdateCapture?: React.ReactEventHandler | undefined; onVolumeChange?: React.ReactEventHandler | undefined; onVolumeChangeCapture?: React.ReactEventHandler | undefined; onWaiting?: React.ReactEventHandler | undefined; onWaitingCapture?: React.ReactEventHandler | undefined; onAuxClick?: React.MouseEventHandler | undefined; onAuxClickCapture?: React.MouseEventHandler | undefined; onClickCapture?: React.MouseEventHandler | undefined; onContextMenu?: React.MouseEventHandler | undefined; onContextMenuCapture?: React.MouseEventHandler | undefined; onDoubleClick?: React.MouseEventHandler | undefined; onDoubleClickCapture?: React.MouseEventHandler | undefined; onDrag?: React.DragEventHandler | undefined; onDragCapture?: React.DragEventHandler | undefined; onDragEnd?: React.DragEventHandler | undefined; onDragEndCapture?: React.DragEventHandler | undefined; onDragEnter?: React.DragEventHandler | undefined; onDragEnterCapture?: React.DragEventHandler | undefined; onDragExit?: React.DragEventHandler | undefined; onDragExitCapture?: React.DragEventHandler | undefined; onDragLeave?: React.DragEventHandler | undefined; onDragLeaveCapture?: React.DragEventHandler | undefined; onDragOver?: React.DragEventHandler | undefined; onDragOverCapture?: React.DragEventHandler | undefined; onDragStart?: React.DragEventHandler | undefined; onDragStartCapture?: React.DragEventHandler | undefined; onDrop?: React.DragEventHandler | undefined; onDropCapture?: React.DragEventHandler | undefined; onMouseDown?: React.MouseEventHandler | undefined; onMouseDownCapture?: React.MouseEventHandler | undefined; onMouseEnter?: React.MouseEventHandler | undefined; onMouseLeave?: React.MouseEventHandler | undefined; onMouseMove?: React.MouseEventHandler | undefined; onMouseMoveCapture?: React.MouseEventHandler | undefined; onMouseOut?: React.MouseEventHandler | undefined; onMouseOutCapture?: React.MouseEventHandler | undefined; onMouseOver?: React.MouseEventHandler | undefined; onMouseOverCapture?: React.MouseEventHandler | undefined; onMouseUp?: React.MouseEventHandler | undefined; onMouseUpCapture?: React.MouseEventHandler | undefined; onSelect?: React.ReactEventHandler | undefined; onSelectCapture?: React.ReactEventHandler | undefined; onTouchCancel?: React.TouchEventHandler | undefined; onTouchCancelCapture?: React.TouchEventHandler | undefined; onTouchEnd?: React.TouchEventHandler | undefined; onTouchEndCapture?: React.TouchEventHandler | undefined; onTouchMove?: React.TouchEventHandler | undefined; onTouchMoveCapture?: React.TouchEventHandler | undefined; onTouchStart?: React.TouchEventHandler | undefined; onTouchStartCapture?: React.TouchEventHandler | undefined; onPointerDown?: React.PointerEventHandler | undefined; onPointerDownCapture?: React.PointerEventHandler | undefined; onPointerMove?: React.PointerEventHandler | undefined; onPointerMoveCapture?: React.PointerEventHandler | undefined; onPointerUp?: React.PointerEventHandler | undefined; onPointerUpCapture?: React.PointerEventHandler | undefined; onPointerCancel?: React.PointerEventHandler | undefined; onPointerCancelCapture?: React.PointerEventHandler | undefined; onPointerEnter?: React.PointerEventHandler | undefined; onPointerEnterCapture?: React.PointerEventHandler | undefined; onPointerLeave?: React.PointerEventHandler | undefined; onPointerLeaveCapture?: React.PointerEventHandler | undefined; onPointerOver?: React.PointerEventHandler | undefined; onPointerOverCapture?: React.PointerEventHandler | undefined; onPointerOut?: React.PointerEventHandler | undefined; onPointerOutCapture?: React.PointerEventHandler | undefined; onGotPointerCapture?: React.PointerEventHandler | undefined; onGotPointerCaptureCapture?: React.PointerEventHandler | undefined; onLostPointerCapture?: React.PointerEventHandler | undefined; onLostPointerCaptureCapture?: React.PointerEventHandler | undefined; onScroll?: React.UIEventHandler | undefined; onScrollCapture?: React.UIEventHandler | undefined; onWheel?: React.WheelEventHandler | undefined; onWheelCapture?: React.WheelEventHandler | undefined; onAnimationStart?: React.AnimationEventHandler | undefined; onAnimationStartCapture?: React.AnimationEventHandler | undefined; onAnimationEnd?: React.AnimationEventHandler | undefined; onAnimationEndCapture?: React.AnimationEventHandler | undefined; onAnimationIteration?: React.AnimationEventHandler | undefined; onAnimationIterationCapture?: React.AnimationEventHandler | undefined; onTransitionEnd?: React.TransitionEventHandler | undefined; onTransitionEndCapture?: React.TransitionEventHandler | undefined; 'data-test-subj'?: string | undefined; css?: ", "Interpolation", "<", "Theme", diff --git a/api_docs/kbn_shared_ux_markdown_mocks.mdx b/api_docs/kbn_shared_ux_markdown_mocks.mdx index 2cbdfb1c2a91f..dc6ea8ac3b521 100644 --- a/api_docs/kbn_shared_ux_markdown_mocks.mdx +++ b/api_docs/kbn_shared_ux_markdown_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-markdown-mocks title: "@kbn/shared-ux-markdown-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-markdown-mocks plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-markdown-mocks'] --- import kbnSharedUxMarkdownMocksObj from './kbn_shared_ux_markdown_mocks.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_page_analytics_no_data.mdx b/api_docs/kbn_shared_ux_page_analytics_no_data.mdx index 3b2fa15566bd8..aea49e73ab154 100644 --- a/api_docs/kbn_shared_ux_page_analytics_no_data.mdx +++ b/api_docs/kbn_shared_ux_page_analytics_no_data.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-page-analytics-no-data title: "@kbn/shared-ux-page-analytics-no-data" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-page-analytics-no-data plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-page-analytics-no-data'] --- import kbnSharedUxPageAnalyticsNoDataObj from './kbn_shared_ux_page_analytics_no_data.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_page_analytics_no_data_mocks.mdx b/api_docs/kbn_shared_ux_page_analytics_no_data_mocks.mdx index 9192d879c2a74..19faa3fab5a0d 100644 --- a/api_docs/kbn_shared_ux_page_analytics_no_data_mocks.mdx +++ b/api_docs/kbn_shared_ux_page_analytics_no_data_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-page-analytics-no-data-mocks title: "@kbn/shared-ux-page-analytics-no-data-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-page-analytics-no-data-mocks plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-page-analytics-no-data-mocks'] --- import kbnSharedUxPageAnalyticsNoDataMocksObj from './kbn_shared_ux_page_analytics_no_data_mocks.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_page_kibana_no_data.mdx b/api_docs/kbn_shared_ux_page_kibana_no_data.mdx index 30098b0ba482f..9d04da757025e 100644 --- a/api_docs/kbn_shared_ux_page_kibana_no_data.mdx +++ b/api_docs/kbn_shared_ux_page_kibana_no_data.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-page-kibana-no-data title: "@kbn/shared-ux-page-kibana-no-data" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-page-kibana-no-data plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-page-kibana-no-data'] --- import kbnSharedUxPageKibanaNoDataObj from './kbn_shared_ux_page_kibana_no_data.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_page_kibana_no_data_mocks.mdx b/api_docs/kbn_shared_ux_page_kibana_no_data_mocks.mdx index 00313a13e4c05..3f3f1e4998fe8 100644 --- a/api_docs/kbn_shared_ux_page_kibana_no_data_mocks.mdx +++ b/api_docs/kbn_shared_ux_page_kibana_no_data_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-page-kibana-no-data-mocks title: "@kbn/shared-ux-page-kibana-no-data-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-page-kibana-no-data-mocks plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-page-kibana-no-data-mocks'] --- import kbnSharedUxPageKibanaNoDataMocksObj from './kbn_shared_ux_page_kibana_no_data_mocks.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_page_kibana_template.devdocs.json b/api_docs/kbn_shared_ux_page_kibana_template.devdocs.json index 42d13b89e4e8d..f3487bed459d8 100644 --- a/api_docs/kbn_shared_ux_page_kibana_template.devdocs.json +++ b/api_docs/kbn_shared_ux_page_kibana_template.devdocs.json @@ -181,7 +181,7 @@ "_EuiPageOuterProps", " & Omit<", "_EuiPageInnerProps", - "<\"main\">, \"border\"> & ", + "<\"main\">, \"border\" | \"component\"> & ", "_EuiPageRestrictWidth", " & ", "_EuiPageBottomBorder", @@ -189,7 +189,9 @@ "Property", ".MinHeight | undefined; offset?: number | undefined; mainProps?: (", "CommonProps", - " & React.HTMLAttributes) | undefined; } & { isEmptyState?: boolean | undefined; solutionNav?: ", + " & React.HTMLAttributes) | undefined; component?: ", + "ComponentTypes", + " | undefined; } & { isEmptyState?: boolean | undefined; solutionNav?: ", { "pluginId": "@kbn/shared-ux-page-solution-nav", "scope": "common", diff --git a/api_docs/kbn_shared_ux_page_kibana_template.mdx b/api_docs/kbn_shared_ux_page_kibana_template.mdx index b6345459460db..e6c6e87cdb6dc 100644 --- a/api_docs/kbn_shared_ux_page_kibana_template.mdx +++ b/api_docs/kbn_shared_ux_page_kibana_template.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-page-kibana-template title: "@kbn/shared-ux-page-kibana-template" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-page-kibana-template plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-page-kibana-template'] --- import kbnSharedUxPageKibanaTemplateObj from './kbn_shared_ux_page_kibana_template.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_page_kibana_template_mocks.mdx b/api_docs/kbn_shared_ux_page_kibana_template_mocks.mdx index b8c6adc2f7856..7c0405c89904d 100644 --- a/api_docs/kbn_shared_ux_page_kibana_template_mocks.mdx +++ b/api_docs/kbn_shared_ux_page_kibana_template_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-page-kibana-template-mocks title: "@kbn/shared-ux-page-kibana-template-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-page-kibana-template-mocks plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-page-kibana-template-mocks'] --- import kbnSharedUxPageKibanaTemplateMocksObj from './kbn_shared_ux_page_kibana_template_mocks.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_page_no_data.mdx b/api_docs/kbn_shared_ux_page_no_data.mdx index 51f1930575d21..05befb33f8739 100644 --- a/api_docs/kbn_shared_ux_page_no_data.mdx +++ b/api_docs/kbn_shared_ux_page_no_data.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-page-no-data title: "@kbn/shared-ux-page-no-data" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-page-no-data plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-page-no-data'] --- import kbnSharedUxPageNoDataObj from './kbn_shared_ux_page_no_data.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_page_no_data_config.devdocs.json b/api_docs/kbn_shared_ux_page_no_data_config.devdocs.json index 07e83e6991e88..73e12b09b9d23 100644 --- a/api_docs/kbn_shared_ux_page_no_data_config.devdocs.json +++ b/api_docs/kbn_shared_ux_page_no_data_config.devdocs.json @@ -144,7 +144,7 @@ "_EuiPageOuterProps", " & Omit<", "_EuiPageInnerProps", - "<\"main\">, \"border\"> & ", + "<\"main\">, \"border\" | \"component\"> & ", "_EuiPageRestrictWidth", " & ", "_EuiPageBottomBorder", @@ -152,7 +152,9 @@ "Property", ".MinHeight | undefined; offset?: number | undefined; mainProps?: (", "CommonProps", - " & React.HTMLAttributes) | undefined; } & { noDataConfig?: ", + " & React.HTMLAttributes) | undefined; component?: ", + "ComponentTypes", + " | undefined; } & { noDataConfig?: ", "NoDataPageProps", " | undefined; pageSideBar?: React.ReactNode; pageSideBarProps?: ", "EuiPageSidebarProps", @@ -222,7 +224,7 @@ "_EuiPageOuterProps", " & Omit<", "_EuiPageInnerProps", - "<\"main\">, \"border\"> & ", + "<\"main\">, \"border\" | \"component\"> & ", "_EuiPageRestrictWidth", " & ", "_EuiPageBottomBorder", @@ -230,7 +232,9 @@ "Property", ".MinHeight | undefined; offset?: number | undefined; mainProps?: (", "CommonProps", - " & React.HTMLAttributes) | undefined; } & { noDataConfig?: ", + " & React.HTMLAttributes) | undefined; component?: ", + "ComponentTypes", + " | undefined; } & { noDataConfig?: ", "NoDataPageProps", " | undefined; pageSideBar?: React.ReactNode; pageSideBarProps?: ", "EuiPageSidebarProps", diff --git a/api_docs/kbn_shared_ux_page_no_data_config.mdx b/api_docs/kbn_shared_ux_page_no_data_config.mdx index b65e9b64cdc45..1780b12ce0164 100644 --- a/api_docs/kbn_shared_ux_page_no_data_config.mdx +++ b/api_docs/kbn_shared_ux_page_no_data_config.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-page-no-data-config title: "@kbn/shared-ux-page-no-data-config" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-page-no-data-config plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-page-no-data-config'] --- import kbnSharedUxPageNoDataConfigObj from './kbn_shared_ux_page_no_data_config.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_page_no_data_config_mocks.mdx b/api_docs/kbn_shared_ux_page_no_data_config_mocks.mdx index 603ed5b012756..f0605f8beb159 100644 --- a/api_docs/kbn_shared_ux_page_no_data_config_mocks.mdx +++ b/api_docs/kbn_shared_ux_page_no_data_config_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-page-no-data-config-mocks title: "@kbn/shared-ux-page-no-data-config-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-page-no-data-config-mocks plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-page-no-data-config-mocks'] --- import kbnSharedUxPageNoDataConfigMocksObj from './kbn_shared_ux_page_no_data_config_mocks.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_page_no_data_mocks.mdx b/api_docs/kbn_shared_ux_page_no_data_mocks.mdx index f7f1517a866b6..a4642227d6521 100644 --- a/api_docs/kbn_shared_ux_page_no_data_mocks.mdx +++ b/api_docs/kbn_shared_ux_page_no_data_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-page-no-data-mocks title: "@kbn/shared-ux-page-no-data-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-page-no-data-mocks plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-page-no-data-mocks'] --- import kbnSharedUxPageNoDataMocksObj from './kbn_shared_ux_page_no_data_mocks.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_page_solution_nav.mdx b/api_docs/kbn_shared_ux_page_solution_nav.mdx index ff927e72c995c..cf2a439600f03 100644 --- a/api_docs/kbn_shared_ux_page_solution_nav.mdx +++ b/api_docs/kbn_shared_ux_page_solution_nav.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-page-solution-nav title: "@kbn/shared-ux-page-solution-nav" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-page-solution-nav plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-page-solution-nav'] --- import kbnSharedUxPageSolutionNavObj from './kbn_shared_ux_page_solution_nav.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_prompt_no_data_views.mdx b/api_docs/kbn_shared_ux_prompt_no_data_views.mdx index 1c4afb5bfcbc8..1099afe206d37 100644 --- a/api_docs/kbn_shared_ux_prompt_no_data_views.mdx +++ b/api_docs/kbn_shared_ux_prompt_no_data_views.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-prompt-no-data-views title: "@kbn/shared-ux-prompt-no-data-views" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-prompt-no-data-views plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-prompt-no-data-views'] --- import kbnSharedUxPromptNoDataViewsObj from './kbn_shared_ux_prompt_no_data_views.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_prompt_no_data_views_mocks.mdx b/api_docs/kbn_shared_ux_prompt_no_data_views_mocks.mdx index a7ba53b92c559..24d049e7ccb1c 100644 --- a/api_docs/kbn_shared_ux_prompt_no_data_views_mocks.mdx +++ b/api_docs/kbn_shared_ux_prompt_no_data_views_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-prompt-no-data-views-mocks title: "@kbn/shared-ux-prompt-no-data-views-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-prompt-no-data-views-mocks plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-prompt-no-data-views-mocks'] --- import kbnSharedUxPromptNoDataViewsMocksObj from './kbn_shared_ux_prompt_no_data_views_mocks.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_prompt_not_found.devdocs.json b/api_docs/kbn_shared_ux_prompt_not_found.devdocs.json new file mode 100644 index 0000000000000..99a5e86068dfb --- /dev/null +++ b/api_docs/kbn_shared_ux_prompt_not_found.devdocs.json @@ -0,0 +1,63 @@ +{ + "id": "@kbn/shared-ux-prompt-not-found", + "client": { + "classes": [], + "functions": [], + "interfaces": [], + "enums": [], + "misc": [], + "objects": [] + }, + "server": { + "classes": [], + "functions": [], + "interfaces": [], + "enums": [], + "misc": [], + "objects": [] + }, + "common": { + "classes": [], + "functions": [ + { + "parentPluginId": "@kbn/shared-ux-prompt-not-found", + "id": "def-common.NotFoundPrompt", + "type": "Function", + "tags": [], + "label": "NotFoundPrompt", + "description": [ + "\nPredefined `EuiEmptyPrompt` for 404 pages." + ], + "signature": [ + "({ actions }: NotFoundProps) => JSX.Element" + ], + "path": "packages/shared-ux/prompt/not_found/src/not_found_prompt.tsx", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/shared-ux-prompt-not-found", + "id": "def-common.NotFoundPrompt.$1", + "type": "Object", + "tags": [], + "label": "{ actions }", + "description": [], + "signature": [ + "NotFoundProps" + ], + "path": "packages/shared-ux/prompt/not_found/src/not_found_prompt.tsx", + "deprecated": false, + "trackAdoption": false, + "isRequired": true + } + ], + "returnComment": [], + "initialIsOpen": false + } + ], + "interfaces": [], + "enums": [], + "misc": [], + "objects": [] + } +} \ No newline at end of file diff --git a/api_docs/kbn_shared_ux_prompt_not_found.mdx b/api_docs/kbn_shared_ux_prompt_not_found.mdx new file mode 100644 index 0000000000000..ba8de80aa601b --- /dev/null +++ b/api_docs/kbn_shared_ux_prompt_not_found.mdx @@ -0,0 +1,30 @@ +--- +#### +#### This document is auto-generated and is meant to be viewed inside our experimental, new docs system. +#### Reach out in #docs-engineering for more info. +#### +id: kibKbnSharedUxPromptNotFoundPluginApi +slug: /kibana-dev-docs/api/kbn-shared-ux-prompt-not-found +title: "@kbn/shared-ux-prompt-not-found" +image: https://source.unsplash.com/400x175/?github +description: API docs for the @kbn/shared-ux-prompt-not-found plugin +date: 2022-12-07 +tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-prompt-not-found'] +--- +import kbnSharedUxPromptNotFoundObj from './kbn_shared_ux_prompt_not_found.devdocs.json'; + + + +Contact [Owner missing] for questions regarding this plugin. + +**Code health stats** + +| Public API count | Any count | Items lacking comments | Missing exports | +|-------------------|-----------|------------------------|-----------------| +| 2 | 0 | 1 | 0 | + +## Common + +### Functions + + diff --git a/api_docs/kbn_shared_ux_router.mdx b/api_docs/kbn_shared_ux_router.mdx index 323f5e5df95c5..50427f3ec333b 100644 --- a/api_docs/kbn_shared_ux_router.mdx +++ b/api_docs/kbn_shared_ux_router.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-router title: "@kbn/shared-ux-router" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-router plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-router'] --- import kbnSharedUxRouterObj from './kbn_shared_ux_router.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_router_mocks.mdx b/api_docs/kbn_shared_ux_router_mocks.mdx index 7344d2ba598e1..9da4bceb48b4b 100644 --- a/api_docs/kbn_shared_ux_router_mocks.mdx +++ b/api_docs/kbn_shared_ux_router_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-router-mocks title: "@kbn/shared-ux-router-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-router-mocks plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-router-mocks'] --- import kbnSharedUxRouterMocksObj from './kbn_shared_ux_router_mocks.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_storybook_config.mdx b/api_docs/kbn_shared_ux_storybook_config.mdx index 80c9ff29aabcd..b237179b16d74 100644 --- a/api_docs/kbn_shared_ux_storybook_config.mdx +++ b/api_docs/kbn_shared_ux_storybook_config.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-storybook-config title: "@kbn/shared-ux-storybook-config" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-storybook-config plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-storybook-config'] --- import kbnSharedUxStorybookConfigObj from './kbn_shared_ux_storybook_config.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_storybook_mock.mdx b/api_docs/kbn_shared_ux_storybook_mock.mdx index fdcdd051ae1c1..fb7c50370645f 100644 --- a/api_docs/kbn_shared_ux_storybook_mock.mdx +++ b/api_docs/kbn_shared_ux_storybook_mock.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-storybook-mock title: "@kbn/shared-ux-storybook-mock" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-storybook-mock plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-storybook-mock'] --- import kbnSharedUxStorybookMockObj from './kbn_shared_ux_storybook_mock.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_utility.mdx b/api_docs/kbn_shared_ux_utility.mdx index 4559966275099..981957afdbc08 100644 --- a/api_docs/kbn_shared_ux_utility.mdx +++ b/api_docs/kbn_shared_ux_utility.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-utility title: "@kbn/shared-ux-utility" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-utility plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-utility'] --- import kbnSharedUxUtilityObj from './kbn_shared_ux_utility.devdocs.json'; diff --git a/api_docs/kbn_some_dev_log.mdx b/api_docs/kbn_some_dev_log.mdx index d8a80e1dac0fd..fecfd1a7c50fe 100644 --- a/api_docs/kbn_some_dev_log.mdx +++ b/api_docs/kbn_some_dev_log.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-some-dev-log title: "@kbn/some-dev-log" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/some-dev-log plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/some-dev-log'] --- import kbnSomeDevLogObj from './kbn_some_dev_log.devdocs.json'; diff --git a/api_docs/kbn_sort_package_json.mdx b/api_docs/kbn_sort_package_json.mdx index ecf7757e68e9f..da3283def477c 100644 --- a/api_docs/kbn_sort_package_json.mdx +++ b/api_docs/kbn_sort_package_json.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-sort-package-json title: "@kbn/sort-package-json" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/sort-package-json plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/sort-package-json'] --- import kbnSortPackageJsonObj from './kbn_sort_package_json.devdocs.json'; diff --git a/api_docs/kbn_std.mdx b/api_docs/kbn_std.mdx index c5c01bb74aa96..5c8a0a4666187 100644 --- a/api_docs/kbn_std.mdx +++ b/api_docs/kbn_std.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-std title: "@kbn/std" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/std plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/std'] --- import kbnStdObj from './kbn_std.devdocs.json'; diff --git a/api_docs/kbn_stdio_dev_helpers.mdx b/api_docs/kbn_stdio_dev_helpers.mdx index 42c757a0d4a73..3d3700fe664ab 100644 --- a/api_docs/kbn_stdio_dev_helpers.mdx +++ b/api_docs/kbn_stdio_dev_helpers.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-stdio-dev-helpers title: "@kbn/stdio-dev-helpers" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/stdio-dev-helpers plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/stdio-dev-helpers'] --- import kbnStdioDevHelpersObj from './kbn_stdio_dev_helpers.devdocs.json'; diff --git a/api_docs/kbn_storybook.mdx b/api_docs/kbn_storybook.mdx index 21b8c9a273d5e..edc5a93e04ca5 100644 --- a/api_docs/kbn_storybook.mdx +++ b/api_docs/kbn_storybook.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-storybook title: "@kbn/storybook" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/storybook plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/storybook'] --- import kbnStorybookObj from './kbn_storybook.devdocs.json'; diff --git a/api_docs/kbn_telemetry_tools.mdx b/api_docs/kbn_telemetry_tools.mdx index 724726b19bba0..470a39f4e7ec8 100644 --- a/api_docs/kbn_telemetry_tools.mdx +++ b/api_docs/kbn_telemetry_tools.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-telemetry-tools title: "@kbn/telemetry-tools" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/telemetry-tools plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/telemetry-tools'] --- import kbnTelemetryToolsObj from './kbn_telemetry_tools.devdocs.json'; diff --git a/api_docs/kbn_test.mdx b/api_docs/kbn_test.mdx index 813651550a314..408d56d66292d 100644 --- a/api_docs/kbn_test.mdx +++ b/api_docs/kbn_test.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-test title: "@kbn/test" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/test plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/test'] --- import kbnTestObj from './kbn_test.devdocs.json'; diff --git a/api_docs/kbn_test_jest_helpers.mdx b/api_docs/kbn_test_jest_helpers.mdx index c12cd8871eace..7a6aacff50a9b 100644 --- a/api_docs/kbn_test_jest_helpers.mdx +++ b/api_docs/kbn_test_jest_helpers.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-test-jest-helpers title: "@kbn/test-jest-helpers" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/test-jest-helpers plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/test-jest-helpers'] --- import kbnTestJestHelpersObj from './kbn_test_jest_helpers.devdocs.json'; diff --git a/api_docs/kbn_test_subj_selector.mdx b/api_docs/kbn_test_subj_selector.mdx index 89ebe364f677e..947775ba20792 100644 --- a/api_docs/kbn_test_subj_selector.mdx +++ b/api_docs/kbn_test_subj_selector.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-test-subj-selector title: "@kbn/test-subj-selector" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/test-subj-selector plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/test-subj-selector'] --- import kbnTestSubjSelectorObj from './kbn_test_subj_selector.devdocs.json'; diff --git a/api_docs/kbn_tooling_log.mdx b/api_docs/kbn_tooling_log.mdx index 1b8c57b5d2f5f..a71f657104775 100644 --- a/api_docs/kbn_tooling_log.mdx +++ b/api_docs/kbn_tooling_log.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-tooling-log title: "@kbn/tooling-log" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/tooling-log plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/tooling-log'] --- import kbnToolingLogObj from './kbn_tooling_log.devdocs.json'; diff --git a/api_docs/kbn_type_summarizer.mdx b/api_docs/kbn_type_summarizer.mdx index b47d0d65c8116..632f176d939f1 100644 --- a/api_docs/kbn_type_summarizer.mdx +++ b/api_docs/kbn_type_summarizer.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-type-summarizer title: "@kbn/type-summarizer" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/type-summarizer plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/type-summarizer'] --- import kbnTypeSummarizerObj from './kbn_type_summarizer.devdocs.json'; diff --git a/api_docs/kbn_type_summarizer_core.mdx b/api_docs/kbn_type_summarizer_core.mdx index 26088618affb0..0a5bbca05708d 100644 --- a/api_docs/kbn_type_summarizer_core.mdx +++ b/api_docs/kbn_type_summarizer_core.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-type-summarizer-core title: "@kbn/type-summarizer-core" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/type-summarizer-core plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/type-summarizer-core'] --- import kbnTypeSummarizerCoreObj from './kbn_type_summarizer_core.devdocs.json'; diff --git a/api_docs/kbn_typed_react_router_config.mdx b/api_docs/kbn_typed_react_router_config.mdx index 4f6e3f7b72bd5..bae459deb6286 100644 --- a/api_docs/kbn_typed_react_router_config.mdx +++ b/api_docs/kbn_typed_react_router_config.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-typed-react-router-config title: "@kbn/typed-react-router-config" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/typed-react-router-config plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/typed-react-router-config'] --- import kbnTypedReactRouterConfigObj from './kbn_typed_react_router_config.devdocs.json'; diff --git a/api_docs/kbn_ui_shared_deps_src.devdocs.json b/api_docs/kbn_ui_shared_deps_src.devdocs.json index 95594baac9f58..d2f3014cbaeae 100644 --- a/api_docs/kbn_ui_shared_deps_src.devdocs.json +++ b/api_docs/kbn_ui_shared_deps_src.devdocs.json @@ -474,10 +474,10 @@ }, { "parentPluginId": "@kbn/ui-shared-deps-src", - "id": "def-server.externals.risonnode", + "id": "def-server.externals.kbnrison", "type": "string", "tags": [], - "label": "'rison-node'", + "label": "'@kbn/rison'", "description": [], "path": "packages/kbn-ui-shared-deps-src/src/definitions.js", "deprecated": false, diff --git a/api_docs/kbn_ui_shared_deps_src.mdx b/api_docs/kbn_ui_shared_deps_src.mdx index d357240b42fe8..123876b087342 100644 --- a/api_docs/kbn_ui_shared_deps_src.mdx +++ b/api_docs/kbn_ui_shared_deps_src.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ui-shared-deps-src title: "@kbn/ui-shared-deps-src" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ui-shared-deps-src plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ui-shared-deps-src'] --- import kbnUiSharedDepsSrcObj from './kbn_ui_shared_deps_src.devdocs.json'; diff --git a/api_docs/kbn_ui_theme.mdx b/api_docs/kbn_ui_theme.mdx index 95d935f8f1058..445b5c19378bd 100644 --- a/api_docs/kbn_ui_theme.mdx +++ b/api_docs/kbn_ui_theme.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ui-theme title: "@kbn/ui-theme" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ui-theme plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ui-theme'] --- import kbnUiThemeObj from './kbn_ui_theme.devdocs.json'; diff --git a/api_docs/kbn_user_profile_components.mdx b/api_docs/kbn_user_profile_components.mdx index d1c42cdc5f8ac..cf2e76826a8e8 100644 --- a/api_docs/kbn_user_profile_components.mdx +++ b/api_docs/kbn_user_profile_components.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-user-profile-components title: "@kbn/user-profile-components" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/user-profile-components plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/user-profile-components'] --- import kbnUserProfileComponentsObj from './kbn_user_profile_components.devdocs.json'; diff --git a/api_docs/kbn_utility_types.mdx b/api_docs/kbn_utility_types.mdx index 3615d52883bd0..b956387709362 100644 --- a/api_docs/kbn_utility_types.mdx +++ b/api_docs/kbn_utility_types.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-utility-types title: "@kbn/utility-types" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/utility-types plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/utility-types'] --- import kbnUtilityTypesObj from './kbn_utility_types.devdocs.json'; diff --git a/api_docs/kbn_utility_types_jest.mdx b/api_docs/kbn_utility_types_jest.mdx index 5eb4a9397b3c7..996d9dc5fa885 100644 --- a/api_docs/kbn_utility_types_jest.mdx +++ b/api_docs/kbn_utility_types_jest.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-utility-types-jest title: "@kbn/utility-types-jest" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/utility-types-jest plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/utility-types-jest'] --- import kbnUtilityTypesJestObj from './kbn_utility_types_jest.devdocs.json'; diff --git a/api_docs/kbn_utils.mdx b/api_docs/kbn_utils.mdx index a3a7edb54637a..0657a7a63b296 100644 --- a/api_docs/kbn_utils.mdx +++ b/api_docs/kbn_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-utils title: "@kbn/utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/utils plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/utils'] --- import kbnUtilsObj from './kbn_utils.devdocs.json'; diff --git a/api_docs/kbn_yarn_lock_validator.mdx b/api_docs/kbn_yarn_lock_validator.mdx index ffd341fcf8cba..3104a7300bc52 100644 --- a/api_docs/kbn_yarn_lock_validator.mdx +++ b/api_docs/kbn_yarn_lock_validator.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-yarn-lock-validator title: "@kbn/yarn-lock-validator" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/yarn-lock-validator plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/yarn-lock-validator'] --- import kbnYarnLockValidatorObj from './kbn_yarn_lock_validator.devdocs.json'; diff --git a/api_docs/kibana_overview.mdx b/api_docs/kibana_overview.mdx index e126552b6961a..c691771ac98cd 100644 --- a/api_docs/kibana_overview.mdx +++ b/api_docs/kibana_overview.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kibanaOverview title: "kibanaOverview" image: https://source.unsplash.com/400x175/?github description: API docs for the kibanaOverview plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'kibanaOverview'] --- import kibanaOverviewObj from './kibana_overview.devdocs.json'; diff --git a/api_docs/kibana_react.mdx b/api_docs/kibana_react.mdx index ce0e6af2d3c9f..dc33aca98f8d3 100644 --- a/api_docs/kibana_react.mdx +++ b/api_docs/kibana_react.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kibanaReact title: "kibanaReact" image: https://source.unsplash.com/400x175/?github description: API docs for the kibanaReact plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'kibanaReact'] --- import kibanaReactObj from './kibana_react.devdocs.json'; diff --git a/api_docs/kibana_utils.mdx b/api_docs/kibana_utils.mdx index 337a5e28498df..f35b57725e87a 100644 --- a/api_docs/kibana_utils.mdx +++ b/api_docs/kibana_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kibanaUtils title: "kibanaUtils" image: https://source.unsplash.com/400x175/?github description: API docs for the kibanaUtils plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'kibanaUtils'] --- import kibanaUtilsObj from './kibana_utils.devdocs.json'; diff --git a/api_docs/kubernetes_security.mdx b/api_docs/kubernetes_security.mdx index ba5d709dac827..b1c4b379be90a 100644 --- a/api_docs/kubernetes_security.mdx +++ b/api_docs/kubernetes_security.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kubernetesSecurity title: "kubernetesSecurity" image: https://source.unsplash.com/400x175/?github description: API docs for the kubernetesSecurity plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'kubernetesSecurity'] --- import kubernetesSecurityObj from './kubernetes_security.devdocs.json'; diff --git a/api_docs/lens.devdocs.json b/api_docs/lens.devdocs.json index 95bd3b3bc9e6c..4762dc2ec8807 100644 --- a/api_docs/lens.devdocs.json +++ b/api_docs/lens.devdocs.json @@ -742,22 +742,6 @@ "trackAdoption": false, "children": [], "returnComment": [] - }, - { - "parentPluginId": "lens", - "id": "def-public.Embeddable.getChartInfo", - "type": "Function", - "tags": [], - "label": "getChartInfo", - "description": [], - "signature": [ - "() => Readonly" - ], - "path": "x-pack/plugins/lens/public/embeddable/embeddable.tsx", - "deprecated": false, - "trackAdoption": false, - "children": [], - "returnComment": [] } ], "initialIsOpen": false @@ -1136,6 +1120,81 @@ ], "initialIsOpen": false }, + { + "parentPluginId": "lens", + "id": "def-public.ChartInfo", + "type": "Interface", + "tags": [], + "label": "ChartInfo", + "description": [], + "path": "x-pack/plugins/lens/public/chart_info_api.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "lens", + "id": "def-public.ChartInfo.layers", + "type": "Array", + "tags": [], + "label": "layers", + "description": [], + "signature": [ + "ChartLayerDescriptor", + "[]" + ], + "path": "x-pack/plugins/lens/public/chart_info_api.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "lens", + "id": "def-public.ChartInfo.visualizationType", + "type": "string", + "tags": [], + "label": "visualizationType", + "description": [], + "path": "x-pack/plugins/lens/public/chart_info_api.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "lens", + "id": "def-public.ChartInfo.filters", + "type": "Array", + "tags": [], + "label": "filters", + "description": [], + "signature": [ + { + "pluginId": "@kbn/es-query", + "scope": "common", + "docId": "kibKbnEsQueryPluginApi", + "section": "def-common.Filter", + "text": "Filter" + }, + "[]" + ], + "path": "x-pack/plugins/lens/public/chart_info_api.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "lens", + "id": "def-public.ChartInfo.query", + "type": "Object", + "tags": [], + "label": "query", + "description": [], + "signature": [ + "{ query: string | { [key: string]: any; }; language: string; }" + ], + "path": "x-pack/plugins/lens/public/chart_info_api.ts", + "deprecated": false, + "trackAdoption": false + } + ], + "initialIsOpen": false + }, { "parentPluginId": "lens", "id": "def-public.DataLayerArgs", @@ -3258,6 +3317,8 @@ "section": "def-public.FormulaPublicApi", "text": "FormulaPublicApi" }, + "; chartInfo: ", + "ChartInfoApi", "; }>" ], "path": "x-pack/plugins/lens/public/plugin.ts", @@ -3826,6 +3887,20 @@ "deprecated": false, "trackAdoption": false }, + { + "parentPluginId": "lens", + "id": "def-public.SharedPieLayerState.colorsByDimension", + "type": "Object", + "tags": [], + "label": "colorsByDimension", + "description": [], + "signature": [ + "Record | undefined" + ], + "path": "x-pack/plugins/lens/common/types.ts", + "deprecated": false, + "trackAdoption": false + }, { "parentPluginId": "lens", "id": "def-public.SharedPieLayerState.collapseFns", @@ -6036,6 +6111,43 @@ ], "returnComment": [] }, + { + "parentPluginId": "lens", + "id": "def-public.Visualization.hasLayerSettings", + "type": "Function", + "tags": [], + "label": "hasLayerSettings", + "description": [ + "\nAllows the visualization to announce whether or not it has any settings to show" + ], + "signature": [ + "((props: ", + "VisualizationConfigProps", + ") => boolean) | undefined" + ], + "path": "x-pack/plugins/lens/public/types.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "lens", + "id": "def-public.Visualization.hasLayerSettings.$1", + "type": "Object", + "tags": [], + "label": "props", + "description": [], + "signature": [ + "VisualizationConfigProps", + "" + ], + "path": "x-pack/plugins/lens/public/types.ts", + "deprecated": false, + "trackAdoption": false, + "isRequired": true + } + ], + "returnComment": [] + }, { "parentPluginId": "lens", "id": "def-public.Visualization.renderLayerSettings", @@ -12073,6 +12185,20 @@ "deprecated": false, "trackAdoption": false }, + { + "parentPluginId": "lens", + "id": "def-common.SharedPieLayerState.colorsByDimension", + "type": "Object", + "tags": [], + "label": "colorsByDimension", + "description": [], + "signature": [ + "Record | undefined" + ], + "path": "x-pack/plugins/lens/common/types.ts", + "deprecated": false, + "trackAdoption": false + }, { "parentPluginId": "lens", "id": "def-common.SharedPieLayerState.collapseFns", diff --git a/api_docs/lens.mdx b/api_docs/lens.mdx index 6ece0525e0f9d..1a4f6cd59e2ab 100644 --- a/api_docs/lens.mdx +++ b/api_docs/lens.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/lens title: "lens" image: https://source.unsplash.com/400x175/?github description: API docs for the lens plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'lens'] --- import lensObj from './lens.devdocs.json'; @@ -21,7 +21,7 @@ Contact [Vis Editors](https://github.com/orgs/elastic/teams/kibana-visualization | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 685 | 0 | 590 | 48 | +| 693 | 0 | 597 | 50 | ## Client diff --git a/api_docs/license_api_guard.mdx b/api_docs/license_api_guard.mdx index 88cefe4ba1364..ef120704814a7 100644 --- a/api_docs/license_api_guard.mdx +++ b/api_docs/license_api_guard.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/licenseApiGuard title: "licenseApiGuard" image: https://source.unsplash.com/400x175/?github description: API docs for the licenseApiGuard plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'licenseApiGuard'] --- import licenseApiGuardObj from './license_api_guard.devdocs.json'; diff --git a/api_docs/license_management.mdx b/api_docs/license_management.mdx index 6cae61b02c0c7..dc36f02726766 100644 --- a/api_docs/license_management.mdx +++ b/api_docs/license_management.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/licenseManagement title: "licenseManagement" image: https://source.unsplash.com/400x175/?github description: API docs for the licenseManagement plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'licenseManagement'] --- import licenseManagementObj from './license_management.devdocs.json'; diff --git a/api_docs/licensing.mdx b/api_docs/licensing.mdx index 478b6b08e883a..fa403e96ae21f 100644 --- a/api_docs/licensing.mdx +++ b/api_docs/licensing.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/licensing title: "licensing" image: https://source.unsplash.com/400x175/?github description: API docs for the licensing plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'licensing'] --- import licensingObj from './licensing.devdocs.json'; diff --git a/api_docs/lists.mdx b/api_docs/lists.mdx index ee5c3233fff01..c9753a644e4cf 100644 --- a/api_docs/lists.mdx +++ b/api_docs/lists.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/lists title: "lists" image: https://source.unsplash.com/400x175/?github description: API docs for the lists plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'lists'] --- import listsObj from './lists.devdocs.json'; diff --git a/api_docs/management.mdx b/api_docs/management.mdx index 28fe70db7c055..8cf643446054b 100644 --- a/api_docs/management.mdx +++ b/api_docs/management.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/management title: "management" image: https://source.unsplash.com/400x175/?github description: API docs for the management plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'management'] --- import managementObj from './management.devdocs.json'; diff --git a/api_docs/maps.devdocs.json b/api_docs/maps.devdocs.json index 11393b4a0aebd..8c3435ae4277a 100644 --- a/api_docs/maps.devdocs.json +++ b/api_docs/maps.devdocs.json @@ -4383,6 +4383,18 @@ "trackAdoption": false, "initialIsOpen": false }, + { + "parentPluginId": "maps", + "id": "def-common.LABEL_POSITIONS", + "type": "Enum", + "tags": [], + "label": "LABEL_POSITIONS", + "description": [], + "path": "x-pack/plugins/maps/common/constants.ts", + "deprecated": false, + "trackAdoption": false, + "initialIsOpen": false + }, { "parentPluginId": "maps", "id": "def-common.LAYER_TYPE", diff --git a/api_docs/maps.mdx b/api_docs/maps.mdx index 897e41548ac96..7d99f166a7c2c 100644 --- a/api_docs/maps.mdx +++ b/api_docs/maps.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/maps title: "maps" image: https://source.unsplash.com/400x175/?github description: API docs for the maps plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'maps'] --- import mapsObj from './maps.devdocs.json'; @@ -21,7 +21,7 @@ Contact [GIS](https://github.com/orgs/elastic/teams/kibana-gis) for questions re | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 266 | 0 | 265 | 26 | +| 267 | 0 | 266 | 26 | ## Client diff --git a/api_docs/maps_ems.mdx b/api_docs/maps_ems.mdx index 581f5c68ebddf..a42028d92ed20 100644 --- a/api_docs/maps_ems.mdx +++ b/api_docs/maps_ems.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/mapsEms title: "mapsEms" image: https://source.unsplash.com/400x175/?github description: API docs for the mapsEms plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'mapsEms'] --- import mapsEmsObj from './maps_ems.devdocs.json'; diff --git a/api_docs/ml.mdx b/api_docs/ml.mdx index e4745d7c11d28..0f71e564a0067 100644 --- a/api_docs/ml.mdx +++ b/api_docs/ml.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/ml title: "ml" image: https://source.unsplash.com/400x175/?github description: API docs for the ml plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'ml'] --- import mlObj from './ml.devdocs.json'; diff --git a/api_docs/monitoring.mdx b/api_docs/monitoring.mdx index 227919739cb65..3bcd2b7f0698a 100644 --- a/api_docs/monitoring.mdx +++ b/api_docs/monitoring.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/monitoring title: "monitoring" image: https://source.unsplash.com/400x175/?github description: API docs for the monitoring plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'monitoring'] --- import monitoringObj from './monitoring.devdocs.json'; diff --git a/api_docs/monitoring_collection.mdx b/api_docs/monitoring_collection.mdx index 3eab903946ef6..901a09433217f 100644 --- a/api_docs/monitoring_collection.mdx +++ b/api_docs/monitoring_collection.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/monitoringCollection title: "monitoringCollection" image: https://source.unsplash.com/400x175/?github description: API docs for the monitoringCollection plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'monitoringCollection'] --- import monitoringCollectionObj from './monitoring_collection.devdocs.json'; diff --git a/api_docs/navigation.devdocs.json b/api_docs/navigation.devdocs.json index f47ee2496eaea..7e70a907f5608 100644 --- a/api_docs/navigation.devdocs.json +++ b/api_docs/navigation.devdocs.json @@ -440,7 +440,7 @@ "ToolTipPositions", " | undefined; anchorProps?: (", "CommonProps", - " & React.HTMLAttributes) | undefined; title?: string | undefined; color?: \"subdued\" | \"accent\" | \"hollow\" | undefined; size?: \"m\" | \"s\" | undefined; } & ", + " & React.HTMLAttributes) | undefined; title?: string | undefined; color?: \"subdued\" | \"accent\" | \"hollow\" | undefined; size?: \"m\" | \"s\" | undefined; alignment?: \"middle\" | \"baseline\" | undefined; } & ", "DisambiguateSet", " & LabelAsString) | (", "CommonProps", @@ -456,7 +456,7 @@ "ToolTipPositions", " | undefined; anchorProps?: (", "CommonProps", - " & React.HTMLAttributes) | undefined; title?: string | undefined; color?: \"subdued\" | \"accent\" | \"hollow\" | undefined; size?: \"m\" | \"s\" | undefined; } & ", + " & React.HTMLAttributes) | undefined; title?: string | undefined; color?: \"subdued\" | \"accent\" | \"hollow\" | undefined; size?: \"m\" | \"s\" | undefined; alignment?: \"middle\" | \"baseline\" | undefined; } & ", "DisambiguateSet", " & ", "DisambiguateSet", @@ -476,7 +476,7 @@ "ToolTipPositions", " | undefined; anchorProps?: (", "CommonProps", - " & React.HTMLAttributes) | undefined; title?: string | undefined; color?: \"subdued\" | \"accent\" | \"hollow\" | undefined; size?: \"m\" | \"s\" | undefined; } & ", + " & React.HTMLAttributes) | undefined; title?: string | undefined; color?: \"subdued\" | \"accent\" | \"hollow\" | undefined; size?: \"m\" | \"s\" | undefined; alignment?: \"middle\" | \"baseline\" | undefined; } & ", "DisambiguateSet", " & LabelAsString) | (", "CommonProps", @@ -494,7 +494,7 @@ "ToolTipPositions", " | undefined; anchorProps?: (", "CommonProps", - " & React.HTMLAttributes) | undefined; title?: string | undefined; color?: \"subdued\" | \"accent\" | \"hollow\" | undefined; size?: \"m\" | \"s\" | undefined; } & ", + " & React.HTMLAttributes) | undefined; title?: string | undefined; color?: \"subdued\" | \"accent\" | \"hollow\" | undefined; size?: \"m\" | \"s\" | undefined; alignment?: \"middle\" | \"baseline\" | undefined; } & ", "DisambiguateSet", " & ", "DisambiguateSet", @@ -514,7 +514,7 @@ "ToolTipPositions", " | undefined; anchorProps?: (", "CommonProps", - " & React.HTMLAttributes) | undefined; title?: string | undefined; color?: \"subdued\" | \"accent\" | \"hollow\" | undefined; size?: \"m\" | \"s\" | undefined; } & ", + " & React.HTMLAttributes) | undefined; title?: string | undefined; color?: \"subdued\" | \"accent\" | \"hollow\" | undefined; size?: \"m\" | \"s\" | undefined; alignment?: \"middle\" | \"baseline\" | undefined; } & ", "DisambiguateSet", " & ", "DisambiguateSet", @@ -534,7 +534,7 @@ "ToolTipPositions", " | undefined; anchorProps?: (", "CommonProps", - " & React.HTMLAttributes) | undefined; title?: string | undefined; color?: \"subdued\" | \"accent\" | \"hollow\" | undefined; size?: \"m\" | \"s\" | undefined; } & ", + " & React.HTMLAttributes) | undefined; title?: string | undefined; color?: \"subdued\" | \"accent\" | \"hollow\" | undefined; size?: \"m\" | \"s\" | undefined; alignment?: \"middle\" | \"baseline\" | undefined; } & ", "DisambiguateSet", " & LabelAsString) | (", "CommonProps", @@ -552,7 +552,7 @@ "ToolTipPositions", " | undefined; anchorProps?: (", "CommonProps", - " & React.HTMLAttributes) | undefined; title?: string | undefined; color?: \"subdued\" | \"accent\" | \"hollow\" | undefined; size?: \"m\" | \"s\" | undefined; } & ", + " & React.HTMLAttributes) | undefined; title?: string | undefined; color?: \"subdued\" | \"accent\" | \"hollow\" | undefined; size?: \"m\" | \"s\" | undefined; alignment?: \"middle\" | \"baseline\" | undefined; } & ", "DisambiguateSet", " & ", "DisambiguateSet", @@ -572,7 +572,7 @@ "ToolTipPositions", " | undefined; anchorProps?: (", "CommonProps", - " & React.HTMLAttributes) | undefined; title?: string | undefined; color?: \"subdued\" | \"accent\" | \"hollow\" | undefined; size?: \"m\" | \"s\" | undefined; } & ", + " & React.HTMLAttributes) | undefined; title?: string | undefined; color?: \"subdued\" | \"accent\" | \"hollow\" | undefined; size?: \"m\" | \"s\" | undefined; alignment?: \"middle\" | \"baseline\" | undefined; } & ", "DisambiguateSet", " & ", "DisambiguateSet", diff --git a/api_docs/navigation.mdx b/api_docs/navigation.mdx index 3f696fbafcf40..afb1e359b16b5 100644 --- a/api_docs/navigation.mdx +++ b/api_docs/navigation.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/navigation title: "navigation" image: https://source.unsplash.com/400x175/?github description: API docs for the navigation plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'navigation'] --- import navigationObj from './navigation.devdocs.json'; diff --git a/api_docs/newsfeed.mdx b/api_docs/newsfeed.mdx index 7d941ab50e04f..a210ece084704 100644 --- a/api_docs/newsfeed.mdx +++ b/api_docs/newsfeed.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/newsfeed title: "newsfeed" image: https://source.unsplash.com/400x175/?github description: API docs for the newsfeed plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'newsfeed'] --- import newsfeedObj from './newsfeed.devdocs.json'; diff --git a/api_docs/notifications.mdx b/api_docs/notifications.mdx index fc512e0da6e78..5810c95b70253 100644 --- a/api_docs/notifications.mdx +++ b/api_docs/notifications.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/notifications title: "notifications" image: https://source.unsplash.com/400x175/?github description: API docs for the notifications plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'notifications'] --- import notificationsObj from './notifications.devdocs.json'; diff --git a/api_docs/observability.devdocs.json b/api_docs/observability.devdocs.json index 7ebcbda528954..3561806b83ff6 100644 --- a/api_docs/observability.devdocs.json +++ b/api_docs/observability.devdocs.json @@ -720,7 +720,7 @@ "signature": [ "React.ExoticComponent[] | undefined; isInApp?: boolean | undefined; observabilityRuleTypeRegistry: { register: (type: ", + "<{}> | undefined; alerts?: Record[] | undefined; isInApp?: boolean | undefined; observabilityRuleTypeRegistry: { register: (type: ", { "pluginId": "observability", "scope": "public", @@ -4495,6 +4495,138 @@ ], "initialIsOpen": false }, + { + "parentPluginId": "observability", + "id": "def-public.SLO", + "type": "Interface", + "tags": [], + "label": "SLO", + "description": [], + "path": "x-pack/plugins/observability/public/typings/slo/index.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "observability", + "id": "def-public.SLO.id", + "type": "string", + "tags": [], + "label": "id", + "description": [], + "path": "x-pack/plugins/observability/public/typings/slo/index.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "observability", + "id": "def-public.SLO.name", + "type": "string", + "tags": [], + "label": "name", + "description": [], + "path": "x-pack/plugins/observability/public/typings/slo/index.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "observability", + "id": "def-public.SLO.objective", + "type": "Object", + "tags": [], + "label": "objective", + "description": [], + "signature": [ + "{ target: number; }" + ], + "path": "x-pack/plugins/observability/public/typings/slo/index.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "observability", + "id": "def-public.SLO.summary", + "type": "Object", + "tags": [], + "label": "summary", + "description": [], + "signature": [ + "{ sliValue: number; errorBudget: { remaining: number; }; }" + ], + "path": "x-pack/plugins/observability/public/typings/slo/index.ts", + "deprecated": false, + "trackAdoption": false + } + ], + "initialIsOpen": false + }, + { + "parentPluginId": "observability", + "id": "def-public.SLOList", + "type": "Interface", + "tags": [], + "label": "SLOList", + "description": [], + "path": "x-pack/plugins/observability/public/typings/slo/index.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "observability", + "id": "def-public.SLOList.results", + "type": "Array", + "tags": [], + "label": "results", + "description": [], + "signature": [ + { + "pluginId": "observability", + "scope": "public", + "docId": "kibObservabilityPluginApi", + "section": "def-public.SLO", + "text": "SLO" + }, + "[]" + ], + "path": "x-pack/plugins/observability/public/typings/slo/index.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "observability", + "id": "def-public.SLOList.page", + "type": "number", + "tags": [], + "label": "page", + "description": [], + "path": "x-pack/plugins/observability/public/typings/slo/index.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "observability", + "id": "def-public.SLOList.perPage", + "type": "number", + "tags": [], + "label": "perPage", + "description": [], + "path": "x-pack/plugins/observability/public/typings/slo/index.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "observability", + "id": "def-public.SLOList.total", + "type": "number", + "tags": [], + "label": "total", + "description": [], + "path": "x-pack/plugins/observability/public/typings/slo/index.ts", + "deprecated": false, + "trackAdoption": false + } + ], + "initialIsOpen": false + }, { "parentPluginId": "observability", "id": "def-public.Stat", @@ -5607,6 +5739,31 @@ "trackAdoption": false, "initialIsOpen": false }, + { + "parentPluginId": "observability", + "id": "def-public.Subset", + "type": "Type", + "tags": [], + "label": "Subset", + "description": [ + "\nAllow partial of nested object" + ], + "signature": [ + "{ [attr in keyof K]?: (K[attr] extends object ? ", + { + "pluginId": "observability", + "scope": "public", + "docId": "kibObservabilityPluginApi", + "section": "def-public.Subset", + "text": "Subset" + }, + " : K[attr]) | undefined; }" + ], + "path": "x-pack/plugins/observability/public/typings/utils.ts", + "deprecated": false, + "trackAdoption": false, + "initialIsOpen": false + }, { "parentPluginId": "observability", "id": "def-public.TERMS_COLUMN", @@ -7393,7 +7550,7 @@ "label": "kqlQuery", "description": [], "signature": [ - "(kql: string) => ", + "(kql: string | undefined) => ", "QueryDslQueryContainer", "[]" ], @@ -7409,12 +7566,12 @@ "label": "kql", "description": [], "signature": [ - "string" + "string | undefined" ], "path": "x-pack/plugins/observability/server/utils/queries.ts", "deprecated": false, "trackAdoption": false, - "isRequired": true + "isRequired": false } ], "returnComment": [], @@ -7935,7 +8092,7 @@ "section": "def-server.ObservabilityRouteHandlerResources", "text": "ObservabilityRouteHandlerResources" }, - ", { page: number; per_page: number; total: number; results: { id: string; name: string; description: string; indicator: { type: \"sli.apm.transaction_duration\"; params: { environment: string; service: string; transaction_type: string; transaction_name: string; 'threshold.us': number; }; } | { type: \"sli.apm.transaction_error_rate\"; params: { environment: string; service: string; transaction_type: string; transaction_name: string; } & { good_status_codes?: (\"2xx\" | \"3xx\" | \"4xx\" | \"5xx\")[] | undefined; }; } | { type: \"sli.kql.custom\"; params: { index: string; query_filter: string; numerator: string; denominator: string; }; }; time_window: { duration: string; is_rolling: boolean; } | { duration: string; calendar: { start_time: string; }; }; budgeting_method: string; objective: { target: number; } & { timeslice_target?: number | undefined; timeslice_window?: string | undefined; }; revision: number; created_at: string; updated_at: string; }[]; }, ", + ", { page: number; per_page: number; total: number; results: { id: string; name: string; description: string; indicator: { type: \"sli.apm.transaction_duration\"; params: { environment: string; service: string; transaction_type: string; transaction_name: string; 'threshold.us': number; }; } | { type: \"sli.apm.transaction_error_rate\"; params: { environment: string; service: string; transaction_type: string; transaction_name: string; } & { good_status_codes?: (\"2xx\" | \"3xx\" | \"4xx\" | \"5xx\")[] | undefined; }; } | { type: \"sli.kql.custom\"; params: { index: string; filter: string; good: string; total: string; }; }; time_window: { duration: string; is_rolling: boolean; } | { duration: string; calendar: { start_time: string; }; }; budgeting_method: string; objective: { target: number; } & { timeslice_target?: number | undefined; timeslice_window?: string | undefined; }; summary: { sli_value: number; error_budget: { initial: number; consumed: number; remaining: number; is_estimated: boolean; }; }; settings: { timestamp_field: string; sync_delay: string; frequency: string; }; revision: number; created_at: string; updated_at: string; }[]; }, ", { "pluginId": "observability", "scope": "server", @@ -7995,7 +8152,7 @@ "section": "def-server.ObservabilityRouteHandlerResources", "text": "ObservabilityRouteHandlerResources" }, - ", { id: string; name: string; description: string; indicator: { type: \"sli.apm.transaction_duration\"; params: { environment: string; service: string; transaction_type: string; transaction_name: string; 'threshold.us': number; }; } | { type: \"sli.apm.transaction_error_rate\"; params: { environment: string; service: string; transaction_type: string; transaction_name: string; } & { good_status_codes?: (\"2xx\" | \"3xx\" | \"4xx\" | \"5xx\")[] | undefined; }; } | { type: \"sli.kql.custom\"; params: { index: string; query_filter: string; numerator: string; denominator: string; }; }; time_window: { duration: string; is_rolling: boolean; } | { duration: string; calendar: { start_time: string; }; }; budgeting_method: string; objective: { target: number; } & { timeslice_target?: number | undefined; timeslice_window?: string | undefined; }; summary: { sli_value: number; error_budget: { initial: number; consumed: number; remaining: number; is_estimated: boolean; }; }; revision: number; created_at: string; updated_at: string; }, ", + ", { id: string; name: string; description: string; indicator: { type: \"sli.apm.transaction_duration\"; params: { environment: string; service: string; transaction_type: string; transaction_name: string; 'threshold.us': number; }; } | { type: \"sli.apm.transaction_error_rate\"; params: { environment: string; service: string; transaction_type: string; transaction_name: string; } & { good_status_codes?: (\"2xx\" | \"3xx\" | \"4xx\" | \"5xx\")[] | undefined; }; } | { type: \"sli.kql.custom\"; params: { index: string; filter: string; good: string; total: string; }; }; time_window: { duration: string; is_rolling: boolean; } | { duration: string; calendar: { start_time: string; }; }; budgeting_method: string; objective: { target: number; } & { timeslice_target?: number | undefined; timeslice_window?: string | undefined; }; settings: { timestamp_field: string; sync_delay: string; frequency: string; }; summary: { sli_value: number; error_budget: { initial: number; consumed: number; remaining: number; is_estimated: boolean; }; }; revision: number; created_at: string; updated_at: string; }, ", { "pluginId": "observability", "scope": "server", @@ -8111,11 +8268,11 @@ "TypeC", "<{ index: ", "StringC", - "; query_filter: ", + "; filter: ", "StringC", - "; numerator: ", + "; good: ", "StringC", - "; denominator: ", + "; total: ", "StringC", "; }>; }>]>; time_window: ", "UnionC", @@ -8157,7 +8314,19 @@ "Type", "<", "Duration", - ", string, unknown>; }>]>; }>; }>, ", + ", string, unknown>; }>]>; settings: ", + "TypeC", + "<{ timestamp_field: ", + "StringC", + "; sync_delay: ", + "Type", + "<", + "Duration", + ", string, unknown>; frequency: ", + "Type", + "<", + "Duration", + ", string, unknown>; }>; }>; }>, ", { "pluginId": "observability", "scope": "server", @@ -8165,7 +8334,7 @@ "section": "def-server.ObservabilityRouteHandlerResources", "text": "ObservabilityRouteHandlerResources" }, - ", { id: string; name: string; description: string; indicator: { type: \"sli.apm.transaction_duration\"; params: { environment: string; service: string; transaction_type: string; transaction_name: string; 'threshold.us': number; }; } | { type: \"sli.apm.transaction_error_rate\"; params: { environment: string; service: string; transaction_type: string; transaction_name: string; } & { good_status_codes?: (\"2xx\" | \"3xx\" | \"4xx\" | \"5xx\")[] | undefined; }; } | { type: \"sli.kql.custom\"; params: { index: string; query_filter: string; numerator: string; denominator: string; }; }; time_window: { duration: string; is_rolling: boolean; } | { duration: string; calendar: { start_time: string; }; }; budgeting_method: string; objective: { target: number; } & { timeslice_target?: number | undefined; timeslice_window?: string | undefined; }; created_at: string; updated_at: string; }, ", + ", { id: string; name: string; description: string; indicator: { type: \"sli.apm.transaction_duration\"; params: { environment: string; service: string; transaction_type: string; transaction_name: string; 'threshold.us': number; }; } | { type: \"sli.apm.transaction_error_rate\"; params: { environment: string; service: string; transaction_type: string; transaction_name: string; } & { good_status_codes?: (\"2xx\" | \"3xx\" | \"4xx\" | \"5xx\")[] | undefined; }; } | { type: \"sli.kql.custom\"; params: { index: string; filter: string; good: string; total: string; }; }; time_window: { duration: string; is_rolling: boolean; } | { duration: string; calendar: { start_time: string; }; }; budgeting_method: string; objective: { target: number; } & { timeslice_target?: number | undefined; timeslice_window?: string | undefined; }; settings: { timestamp_field: string; sync_delay: string; frequency: string; }; created_at: string; updated_at: string; }, ", { "pluginId": "observability", "scope": "server", @@ -8184,6 +8353,8 @@ "<\"POST /api/observability/slos\", ", "TypeC", "<{ body: ", + "IntersectionC", + "<[", "TypeC", "<{ name: ", "StringC", @@ -8277,11 +8448,11 @@ "TypeC", "<{ index: ", "StringC", - "; query_filter: ", + "; filter: ", "StringC", - "; numerator: ", + "; good: ", "StringC", - "; denominator: ", + "; total: ", "StringC", "; }>; }>]>; time_window: ", "UnionC", @@ -8323,7 +8494,21 @@ "Type", "<", "Duration", - ", string, unknown>; }>]>; }>; }>, ", + ", string, unknown>; }>]>; }>, ", + "PartialC", + "<{ settings: ", + "PartialC", + "<{ timestamp_field: ", + "StringC", + "; sync_delay: ", + "Type", + "<", + "Duration", + ", string, unknown>; frequency: ", + "Type", + "<", + "Duration", + ", string, unknown>; }>; }>]>; }>, ", { "pluginId": "observability", "scope": "server", @@ -8439,7 +8624,7 @@ "section": "def-server.ObservabilityRouteHandlerResources", "text": "ObservabilityRouteHandlerResources" }, - ", { page: number; per_page: number; total: number; results: { id: string; name: string; description: string; indicator: { type: \"sli.apm.transaction_duration\"; params: { environment: string; service: string; transaction_type: string; transaction_name: string; 'threshold.us': number; }; } | { type: \"sli.apm.transaction_error_rate\"; params: { environment: string; service: string; transaction_type: string; transaction_name: string; } & { good_status_codes?: (\"2xx\" | \"3xx\" | \"4xx\" | \"5xx\")[] | undefined; }; } | { type: \"sli.kql.custom\"; params: { index: string; query_filter: string; numerator: string; denominator: string; }; }; time_window: { duration: string; is_rolling: boolean; } | { duration: string; calendar: { start_time: string; }; }; budgeting_method: string; objective: { target: number; } & { timeslice_target?: number | undefined; timeslice_window?: string | undefined; }; revision: number; created_at: string; updated_at: string; }[]; }, ", + ", { page: number; per_page: number; total: number; results: { id: string; name: string; description: string; indicator: { type: \"sli.apm.transaction_duration\"; params: { environment: string; service: string; transaction_type: string; transaction_name: string; 'threshold.us': number; }; } | { type: \"sli.apm.transaction_error_rate\"; params: { environment: string; service: string; transaction_type: string; transaction_name: string; } & { good_status_codes?: (\"2xx\" | \"3xx\" | \"4xx\" | \"5xx\")[] | undefined; }; } | { type: \"sli.kql.custom\"; params: { index: string; filter: string; good: string; total: string; }; }; time_window: { duration: string; is_rolling: boolean; } | { duration: string; calendar: { start_time: string; }; }; budgeting_method: string; objective: { target: number; } & { timeslice_target?: number | undefined; timeslice_window?: string | undefined; }; summary: { sli_value: number; error_budget: { initial: number; consumed: number; remaining: number; is_estimated: boolean; }; }; settings: { timestamp_field: string; sync_delay: string; frequency: string; }; revision: number; created_at: string; updated_at: string; }[]; }, ", { "pluginId": "observability", "scope": "server", @@ -8499,7 +8684,7 @@ "section": "def-server.ObservabilityRouteHandlerResources", "text": "ObservabilityRouteHandlerResources" }, - ", { id: string; name: string; description: string; indicator: { type: \"sli.apm.transaction_duration\"; params: { environment: string; service: string; transaction_type: string; transaction_name: string; 'threshold.us': number; }; } | { type: \"sli.apm.transaction_error_rate\"; params: { environment: string; service: string; transaction_type: string; transaction_name: string; } & { good_status_codes?: (\"2xx\" | \"3xx\" | \"4xx\" | \"5xx\")[] | undefined; }; } | { type: \"sli.kql.custom\"; params: { index: string; query_filter: string; numerator: string; denominator: string; }; }; time_window: { duration: string; is_rolling: boolean; } | { duration: string; calendar: { start_time: string; }; }; budgeting_method: string; objective: { target: number; } & { timeslice_target?: number | undefined; timeslice_window?: string | undefined; }; summary: { sli_value: number; error_budget: { initial: number; consumed: number; remaining: number; is_estimated: boolean; }; }; revision: number; created_at: string; updated_at: string; }, ", + ", { id: string; name: string; description: string; indicator: { type: \"sli.apm.transaction_duration\"; params: { environment: string; service: string; transaction_type: string; transaction_name: string; 'threshold.us': number; }; } | { type: \"sli.apm.transaction_error_rate\"; params: { environment: string; service: string; transaction_type: string; transaction_name: string; } & { good_status_codes?: (\"2xx\" | \"3xx\" | \"4xx\" | \"5xx\")[] | undefined; }; } | { type: \"sli.kql.custom\"; params: { index: string; filter: string; good: string; total: string; }; }; time_window: { duration: string; is_rolling: boolean; } | { duration: string; calendar: { start_time: string; }; }; budgeting_method: string; objective: { target: number; } & { timeslice_target?: number | undefined; timeslice_window?: string | undefined; }; settings: { timestamp_field: string; sync_delay: string; frequency: string; }; summary: { sli_value: number; error_budget: { initial: number; consumed: number; remaining: number; is_estimated: boolean; }; }; revision: number; created_at: string; updated_at: string; }, ", { "pluginId": "observability", "scope": "server", @@ -8615,11 +8800,11 @@ "TypeC", "<{ index: ", "StringC", - "; query_filter: ", + "; filter: ", "StringC", - "; numerator: ", + "; good: ", "StringC", - "; denominator: ", + "; total: ", "StringC", "; }>; }>]>; time_window: ", "UnionC", @@ -8661,7 +8846,19 @@ "Type", "<", "Duration", - ", string, unknown>; }>]>; }>; }>, ", + ", string, unknown>; }>]>; settings: ", + "TypeC", + "<{ timestamp_field: ", + "StringC", + "; sync_delay: ", + "Type", + "<", + "Duration", + ", string, unknown>; frequency: ", + "Type", + "<", + "Duration", + ", string, unknown>; }>; }>; }>, ", { "pluginId": "observability", "scope": "server", @@ -8669,7 +8866,7 @@ "section": "def-server.ObservabilityRouteHandlerResources", "text": "ObservabilityRouteHandlerResources" }, - ", { id: string; name: string; description: string; indicator: { type: \"sli.apm.transaction_duration\"; params: { environment: string; service: string; transaction_type: string; transaction_name: string; 'threshold.us': number; }; } | { type: \"sli.apm.transaction_error_rate\"; params: { environment: string; service: string; transaction_type: string; transaction_name: string; } & { good_status_codes?: (\"2xx\" | \"3xx\" | \"4xx\" | \"5xx\")[] | undefined; }; } | { type: \"sli.kql.custom\"; params: { index: string; query_filter: string; numerator: string; denominator: string; }; }; time_window: { duration: string; is_rolling: boolean; } | { duration: string; calendar: { start_time: string; }; }; budgeting_method: string; objective: { target: number; } & { timeslice_target?: number | undefined; timeslice_window?: string | undefined; }; created_at: string; updated_at: string; }, ", + ", { id: string; name: string; description: string; indicator: { type: \"sli.apm.transaction_duration\"; params: { environment: string; service: string; transaction_type: string; transaction_name: string; 'threshold.us': number; }; } | { type: \"sli.apm.transaction_error_rate\"; params: { environment: string; service: string; transaction_type: string; transaction_name: string; } & { good_status_codes?: (\"2xx\" | \"3xx\" | \"4xx\" | \"5xx\")[] | undefined; }; } | { type: \"sli.kql.custom\"; params: { index: string; filter: string; good: string; total: string; }; }; time_window: { duration: string; is_rolling: boolean; } | { duration: string; calendar: { start_time: string; }; }; budgeting_method: string; objective: { target: number; } & { timeslice_target?: number | undefined; timeslice_window?: string | undefined; }; settings: { timestamp_field: string; sync_delay: string; frequency: string; }; created_at: string; updated_at: string; }, ", { "pluginId": "observability", "scope": "server", @@ -8688,6 +8885,8 @@ "<\"POST /api/observability/slos\", ", "TypeC", "<{ body: ", + "IntersectionC", + "<[", "TypeC", "<{ name: ", "StringC", @@ -8781,11 +8980,11 @@ "TypeC", "<{ index: ", "StringC", - "; query_filter: ", + "; filter: ", "StringC", - "; numerator: ", + "; good: ", "StringC", - "; denominator: ", + "; total: ", "StringC", "; }>; }>]>; time_window: ", "UnionC", @@ -8827,7 +9026,21 @@ "Type", "<", "Duration", - ", string, unknown>; }>]>; }>; }>, ", + ", string, unknown>; }>]>; }>, ", + "PartialC", + "<{ settings: ", + "PartialC", + "<{ timestamp_field: ", + "StringC", + "; sync_delay: ", + "Type", + "<", + "Duration", + ", string, unknown>; frequency: ", + "Type", + "<", + "Duration", + ", string, unknown>; }>; }>]>; }>, ", { "pluginId": "observability", "scope": "server", @@ -9642,124 +9855,6 @@ } ] }, - { - "parentPluginId": "observability", - "id": "def-server.uiSettings.enableServiceMetrics", - "type": "Object", - "tags": [], - "label": "[enableServiceMetrics]", - "description": [], - "path": "x-pack/plugins/observability/server/ui_settings.ts", - "deprecated": false, - "trackAdoption": false, - "children": [ - { - "parentPluginId": "observability", - "id": "def-server.uiSettings.enableServiceMetrics.category", - "type": "Array", - "tags": [], - "label": "category", - "description": [], - "signature": [ - "string[]" - ], - "path": "x-pack/plugins/observability/server/ui_settings.ts", - "deprecated": false, - "trackAdoption": false - }, - { - "parentPluginId": "observability", - "id": "def-server.uiSettings.enableServiceMetrics.name", - "type": "Any", - "tags": [], - "label": "name", - "description": [], - "signature": [ - "any" - ], - "path": "x-pack/plugins/observability/server/ui_settings.ts", - "deprecated": false, - "trackAdoption": false - }, - { - "parentPluginId": "observability", - "id": "def-server.uiSettings.enableServiceMetrics.value", - "type": "boolean", - "tags": [], - "label": "value", - "description": [], - "signature": [ - "false" - ], - "path": "x-pack/plugins/observability/server/ui_settings.ts", - "deprecated": false, - "trackAdoption": false - }, - { - "parentPluginId": "observability", - "id": "def-server.uiSettings.enableServiceMetrics.description", - "type": "Any", - "tags": [], - "label": "description", - "description": [], - "signature": [ - "any" - ], - "path": "x-pack/plugins/observability/server/ui_settings.ts", - "deprecated": false, - "trackAdoption": false - }, - { - "parentPluginId": "observability", - "id": "def-server.uiSettings.enableServiceMetrics.schema", - "type": "Object", - "tags": [], - "label": "schema", - "description": [], - "signature": [ - { - "pluginId": "@kbn/config-schema", - "scope": "server", - "docId": "kibKbnConfigSchemaPluginApi", - "section": "def-server.Type", - "text": "Type" - }, - "" - ], - "path": "x-pack/plugins/observability/server/ui_settings.ts", - "deprecated": false, - "trackAdoption": false - }, - { - "parentPluginId": "observability", - "id": "def-server.uiSettings.enableServiceMetrics.requiresPageReload", - "type": "boolean", - "tags": [], - "label": "requiresPageReload", - "description": [], - "signature": [ - "true" - ], - "path": "x-pack/plugins/observability/server/ui_settings.ts", - "deprecated": false, - "trackAdoption": false - }, - { - "parentPluginId": "observability", - "id": "def-server.uiSettings.enableServiceMetrics.showInLabs", - "type": "boolean", - "tags": [], - "label": "showInLabs", - "description": [], - "signature": [ - "true" - ], - "path": "x-pack/plugins/observability/server/ui_settings.ts", - "deprecated": false, - "trackAdoption": false - } - ] - }, { "parentPluginId": "observability", "id": "def-server.uiSettings.apmServiceInventoryOptimizedSorting", @@ -10193,7 +10288,7 @@ "label": "value", "description": [], "signature": [ - "false" + "true" ], "path": "x-pack/plugins/observability/server/ui_settings.ts", "deprecated": false, @@ -10235,7 +10330,7 @@ "label": "showInLabs", "description": [], "signature": [ - "true" + "false" ], "path": "x-pack/plugins/observability/server/ui_settings.ts", "deprecated": false, @@ -11609,21 +11704,6 @@ "trackAdoption": false, "initialIsOpen": false }, - { - "parentPluginId": "observability", - "id": "def-common.enableServiceMetrics", - "type": "string", - "tags": [], - "label": "enableServiceMetrics", - "description": [], - "signature": [ - "\"observability:apmEnableServiceMetrics\"" - ], - "path": "x-pack/plugins/observability/common/ui_settings_keys.ts", - "deprecated": false, - "trackAdoption": false, - "initialIsOpen": false - }, { "parentPluginId": "observability", "id": "def-common.maxSuggestions", diff --git a/api_docs/observability.mdx b/api_docs/observability.mdx index 5faa0b62e6824..28201f51550e3 100644 --- a/api_docs/observability.mdx +++ b/api_docs/observability.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/observability title: "observability" image: https://source.unsplash.com/400x175/?github description: API docs for the observability plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'observability'] --- import observabilityObj from './observability.devdocs.json'; @@ -21,7 +21,7 @@ Contact [Observability UI](https://github.com/orgs/elastic/teams/observability-u | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 569 | 42 | 566 | 31 | +| 571 | 40 | 567 | 31 | ## Client diff --git a/api_docs/osquery.devdocs.json b/api_docs/osquery.devdocs.json index 2b441fc69b891..9ec19fc8e2d46 100644 --- a/api_docs/osquery.devdocs.json +++ b/api_docs/osquery.devdocs.json @@ -166,7 +166,9 @@ "label": "OsqueryResponseActionTypeForm", "description": [], "signature": [ - "(props: LazyOsqueryActionParamsFormProps) => JSX.Element" + "(props: ", + "OsqueryResponseActionsParamsFormProps", + ") => JSX.Element" ], "path": "x-pack/plugins/osquery/public/types.ts", "deprecated": false, @@ -181,7 +183,7 @@ "label": "props", "description": [], "signature": [ - "LazyOsqueryActionParamsFormProps" + "OsqueryResponseActionsParamsFormProps" ], "path": "x-pack/plugins/osquery/public/shared_components/lazy_osquery_action_params_form.tsx", "deprecated": false, diff --git a/api_docs/osquery.mdx b/api_docs/osquery.mdx index da1d1bb2e0f43..c873b152f21ab 100644 --- a/api_docs/osquery.mdx +++ b/api_docs/osquery.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/osquery title: "osquery" image: https://source.unsplash.com/400x175/?github description: API docs for the osquery plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'osquery'] --- import osqueryObj from './osquery.devdocs.json'; @@ -21,7 +21,7 @@ Contact [Security asset management](https://github.com/orgs/elastic/teams/securi | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 21 | 0 | 21 | 4 | +| 21 | 0 | 21 | 5 | ## Client diff --git a/api_docs/plugin_directory.mdx b/api_docs/plugin_directory.mdx index 7428d17ee8f98..15de6b78ad710 100644 --- a/api_docs/plugin_directory.mdx +++ b/api_docs/plugin_directory.mdx @@ -7,7 +7,7 @@ id: kibDevDocsPluginDirectory slug: /kibana-dev-docs/api-meta/plugin-api-directory title: Directory description: Directory of public APIs available through plugins or packages. -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana'] --- @@ -15,13 +15,13 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana'] | Count | Plugins or Packages with a
public API | Number of teams | |--------------|----------|------------------------| -| 520 | 435 | 40 | +| 527 | 439 | 40 | ### Public API health stats | API Count | Any Count | Missing comments | Missing exports | |--------------|----------|-----------------|--------| -| 33768 | 518 | 23483 | 1134 | +| 33658 | 520 | 23440 | 1145 | ## Plugin Directory @@ -30,7 +30,7 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana'] | | [Response Ops](https://github.com/orgs/elastic/teams/response-ops) | - | 226 | 8 | 221 | 24 | | | [Kibana Core](https://github.com/orgs/elastic/teams/kibana-core) | - | 36 | 1 | 32 | 2 | | | [Machine Learning UI](https://github.com/orgs/elastic/teams/ml-ui) | AIOps plugin maintained by ML team. | 12 | 0 | 1 | 2 | -| | [Response Ops](https://github.com/orgs/elastic/teams/response-ops) | - | 417 | 0 | 408 | 28 | +| | [Response Ops](https://github.com/orgs/elastic/teams/response-ops) | - | 417 | 0 | 408 | 34 | | | [APM UI](https://github.com/orgs/elastic/teams/apm-ui) | The user interface for Elastic APM | 41 | 0 | 41 | 57 | | | [Kibana Core](https://github.com/orgs/elastic/teams/kibana-core) | - | 9 | 0 | 9 | 0 | | | [App Services](https://github.com/orgs/elastic/teams/kibana-app-services) | Considering using bfetch capabilities when fetching large amounts of data. This services supports batching HTTP requests and streaming responses back. | 81 | 1 | 72 | 2 | @@ -45,23 +45,23 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana'] | cloudLinks | [Kibana Core](https://github.com/orgs/elastic/teams/@kibana-core) | Adds the links to the Elastic Cloud console | 0 | 0 | 0 | 0 | | | [Cloud Security Posture](https://github.com/orgs/elastic/teams/cloud-posture-security) | The cloud security posture plugin | 17 | 0 | 2 | 2 | | | [Stack Management](https://github.com/orgs/elastic/teams/kibana-stack-management) | - | 13 | 0 | 13 | 1 | -| | [Kibana Presentation](https://github.com/orgs/elastic/teams/kibana-presentation) | The Controls Plugin contains embeddable components intended to create a simple query interface for end users, and a powerful editing suite that allows dashboard authors to build controls | 245 | 0 | 236 | 9 | -| | [Kibana Core](https://github.com/orgs/elastic/teams/kibana-core) | - | 2796 | 17 | 1007 | 0 | +| | [Kibana Presentation](https://github.com/orgs/elastic/teams/kibana-presentation) | The Controls Plugin contains embeddable components intended to create a simple query interface for end users, and a powerful editing suite that allows dashboard authors to build controls | 268 | 0 | 259 | 10 | +| | [Kibana Core](https://github.com/orgs/elastic/teams/kibana-core) | - | 2798 | 17 | 1007 | 0 | | crossClusterReplication | [Stack Management](https://github.com/orgs/elastic/teams/kibana-stack-management) | - | 0 | 0 | 0 | 0 | | customBranding | [global-experience](https://github.com/orgs/elastic/teams/kibana-global-experience) | Enables customization of Kibana | 0 | 0 | 0 | 0 | | | [Fleet](https://github.com/orgs/elastic/teams/fleet) | Add custom data integrations so they can be displayed in the Fleet integrations app | 107 | 0 | 88 | 1 | | | [Kibana Presentation](https://github.com/orgs/elastic/teams/kibana-presentation) | Adds the Dashboard app to Kibana | 121 | 0 | 114 | 3 | | | [App Services](https://github.com/orgs/elastic/teams/kibana-app-services) | - | 52 | 0 | 51 | 0 | -| | [Data Discovery](https://github.com/orgs/elastic/teams/kibana-data-discovery) | Data services are useful for searching and querying data from Elasticsearch. Helpful utilities include: a re-usable react query bar, KQL autocomplete, async search, Data Views (Index Patterns) and field formatters. | 3265 | 119 | 2553 | 27 | +| | [Data Discovery](https://github.com/orgs/elastic/teams/kibana-data-discovery) | Data services are useful for searching and querying data from Elasticsearch. Helpful utilities include: a re-usable react query bar, KQL autocomplete, async search, Data Views (Index Patterns) and field formatters. | 3269 | 119 | 2557 | 27 | | | [App Services](https://github.com/orgs/elastic/teams/kibana-app-services) | This plugin provides the ability to create data views via a modal flyout inside Kibana apps | 16 | 0 | 7 | 0 | | | [App Services](https://github.com/orgs/elastic/teams/kibana-app-services) | Reusable data view field editor across Kibana | 60 | 0 | 30 | 0 | | | [App Services](https://github.com/orgs/elastic/teams/kibana-app-services) | Data view management app | 2 | 0 | 2 | 0 | -| | [App Services](https://github.com/orgs/elastic/teams/kibana-app-services) | Data services are useful for searching and querying data from Elasticsearch. Helpful utilities include: a re-usable react query bar, KQL autocomplete, async search, Data Views (Index Patterns) and field formatters. | 1021 | 0 | 227 | 2 | +| | [App Services](https://github.com/orgs/elastic/teams/kibana-app-services) | Data services are useful for searching and querying data from Elasticsearch. Helpful utilities include: a re-usable react query bar, KQL autocomplete, async search, Data Views (Index Patterns) and field formatters. | 1022 | 0 | 228 | 2 | | | [Machine Learning UI](https://github.com/orgs/elastic/teams/ml-ui) | The Data Visualizer tools help you understand your data, by analyzing the metrics and fields in a log file or an existing Elasticsearch index. | 28 | 3 | 24 | 1 | | | [Stack Management](https://github.com/orgs/elastic/teams/kibana-stack-management) | - | 10 | 0 | 8 | 2 | -| | [Data Discovery](https://github.com/orgs/elastic/teams/kibana-data-discovery) | This plugin contains the Discover application and the saved search embeddable. | 98 | 0 | 81 | 4 | +| | [Data Discovery](https://github.com/orgs/elastic/teams/kibana-data-discovery) | This plugin contains the Discover application and the saved search embeddable. | 100 | 0 | 82 | 4 | | | [Data Discovery](https://github.com/orgs/elastic/teams/kibana-data-discovery) | - | 37 | 0 | 35 | 2 | -| | [App Services](https://github.com/orgs/elastic/teams/kibana-app-services) | Adds embeddables service to Kibana | 510 | 6 | 410 | 4 | +| | [App Services](https://github.com/orgs/elastic/teams/kibana-app-services) | Adds embeddables service to Kibana | 513 | 6 | 413 | 4 | | | [App Services](https://github.com/orgs/elastic/teams/kibana-app-services) | Extends embeddable plugin with more functionality | 14 | 0 | 14 | 0 | | | [Platform Security](https://github.com/orgs/elastic/teams/kibana-security) | This plugin provides encryption and decryption utilities for saved objects containing sensitive information. | 51 | 0 | 44 | 0 | | | [Enterprise Search](https://github.com/orgs/elastic/teams/enterprise-search-frontend) | Adds dashboards for discovering and managing Enterprise Search products. | 9 | 0 | 9 | 0 | @@ -75,7 +75,7 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana'] | | [Vis Editors](https://github.com/orgs/elastic/teams/kibana-visualizations) | Adds a `metric` renderer and function to the expression plugin. The renderer will display the `legacy metric` chart. | 51 | 0 | 51 | 2 | | | [Kibana Presentation](https://github.com/orgs/elastic/teams/kibana-presentation) | Adds 'metric' function and renderer to expressions | 32 | 0 | 27 | 0 | | | [Vis Editors](https://github.com/orgs/elastic/teams/kibana-visualizations) | Adds a `metric` renderer and function to the expression plugin. The renderer will display the `metric` chart. | 63 | 0 | 63 | 2 | -| | [Vis Editors](https://github.com/orgs/elastic/teams/kibana-visualizations) | Expression Partition Visualization plugin adds a `partitionVis` renderer and `pieVis`, `mosaicVis`, `treemapVis`, `waffleVis` functions to the expression plugin. The renderer will display the `pie`, `waffle`, `treemap` and `mosaic` charts. | 71 | 0 | 71 | 2 | +| | [Vis Editors](https://github.com/orgs/elastic/teams/kibana-visualizations) | Expression Partition Visualization plugin adds a `partitionVis` renderer and `pieVis`, `mosaicVis`, `treemapVis`, `waffleVis` functions to the expression plugin. The renderer will display the `pie`, `waffle`, `treemap` and `mosaic` charts. | 72 | 0 | 72 | 2 | | | [Kibana Presentation](https://github.com/orgs/elastic/teams/kibana-presentation) | Adds 'repeatImage' function and renderer to expressions | 32 | 0 | 32 | 0 | | | [Kibana Presentation](https://github.com/orgs/elastic/teams/kibana-presentation) | Adds 'revealImage' function and renderer to expressions | 14 | 0 | 14 | 3 | | | [Kibana Presentation](https://github.com/orgs/elastic/teams/kibana-presentation) | Adds 'shape' function and renderer to expressions | 148 | 0 | 146 | 0 | @@ -85,15 +85,15 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana'] | | [Kibana Core](https://github.com/orgs/elastic/teams/kibana-core) | - | 227 | 0 | 96 | 2 | | | [App Services](https://github.com/orgs/elastic/teams/kibana-app-services) | Index pattern fields and ambiguous values formatters | 288 | 26 | 249 | 3 | | | [Machine Learning UI](https://github.com/orgs/elastic/teams/ml-ui) | The file upload plugin contains components and services for uploading a file, analyzing its data, and then importing the data into an Elasticsearch index. Supported file types include CSV, TSV, newline-delimited JSON and GeoJSON. | 62 | 0 | 62 | 2 | -| | [@elastic/kibana-app-services](https://github.com/orgs/elastic/teams/team:AppServicesUx) | File upload, download, sharing, and serving over HTTP implementation in Kibana. | 287 | 0 | 50 | 3 | +| | [@elastic/kibana-app-services](https://github.com/orgs/elastic/teams/team:AppServicesUx) | File upload, download, sharing, and serving over HTTP implementation in Kibana. | 252 | 1 | 45 | 5 | | | [@elastic/kibana-global-experience](https://github.com/orgs/elastic/teams/@elastic/kibana-global-experience) | Simple UI for managing files in Kibana | 2 | 1 | 2 | 0 | -| | [Fleet](https://github.com/orgs/elastic/teams/fleet) | - | 1025 | 3 | 920 | 19 | +| | [Fleet](https://github.com/orgs/elastic/teams/fleet) | - | 1027 | 3 | 922 | 19 | | | [Kibana Core](https://github.com/orgs/elastic/teams/kibana-core) | - | 68 | 0 | 14 | 5 | | globalSearchBar | [Kibana Core](https://github.com/orgs/elastic/teams/kibana-core) | - | 0 | 0 | 0 | 0 | | globalSearchProviders | [Kibana Core](https://github.com/orgs/elastic/teams/kibana-core) | - | 0 | 0 | 0 | 0 | | graph | [Data Discovery](https://github.com/orgs/elastic/teams/kibana-data-discovery) | - | 0 | 0 | 0 | 0 | | grokdebugger | [Stack Management](https://github.com/orgs/elastic/teams/kibana-stack-management) | - | 0 | 0 | 0 | 0 | -| | [Journey Onboarding](https://github.com/orgs/elastic/teams/platform-onboarding) | Guided onboarding framework | 43 | 0 | 43 | 3 | +| | [Journey Onboarding](https://github.com/orgs/elastic/teams/platform-onboarding) | Guided onboarding framework | 76 | 0 | 75 | 0 | | | [Kibana Core](https://github.com/orgs/elastic/teams/kibana-core) | - | 143 | 0 | 104 | 0 | | | [Stack Management](https://github.com/orgs/elastic/teams/kibana-stack-management) | - | 4 | 0 | 4 | 0 | | | [Stack Management](https://github.com/orgs/elastic/teams/kibana-stack-management) | - | 177 | 0 | 172 | 3 | @@ -107,14 +107,14 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana'] | kibanaUsageCollection | [Kibana Telemetry](https://github.com/orgs/elastic/teams/kibana-telemetry) | - | 0 | 0 | 0 | 0 | | | [App Services](https://github.com/orgs/elastic/teams/kibana-app-services) | - | 624 | 3 | 424 | 8 | | | [Security Team](https://github.com/orgs/elastic/teams/security-team) | - | 3 | 0 | 3 | 1 | -| | [Vis Editors](https://github.com/orgs/elastic/teams/kibana-visualizations) | Visualization editor allowing to quickly and easily configure compelling visualizations to use on dashboards and canvas workpads. Exposes components to embed visualizations and link into the Lens editor from within other apps in Kibana. | 685 | 0 | 590 | 48 | +| | [Vis Editors](https://github.com/orgs/elastic/teams/kibana-visualizations) | Visualization editor allowing to quickly and easily configure compelling visualizations to use on dashboards and canvas workpads. Exposes components to embed visualizations and link into the Lens editor from within other apps in Kibana. | 693 | 0 | 597 | 50 | | | [Stack Management](https://github.com/orgs/elastic/teams/kibana-stack-management) | - | 8 | 0 | 8 | 0 | | | [Stack Management](https://github.com/orgs/elastic/teams/kibana-stack-management) | - | 3 | 0 | 3 | 0 | | | [Kibana Core](https://github.com/orgs/elastic/teams/kibana-core) | - | 117 | 0 | 42 | 10 | | | [Security detections response](https://github.com/orgs/elastic/teams/security-detections-response) | - | 206 | 0 | 93 | 51 | | logstash | [Logstash](https://github.com/orgs/elastic/teams/logstash) | - | 0 | 0 | 0 | 0 | | | [Vis Editors](https://github.com/orgs/elastic/teams/kibana-visualizations) | - | 41 | 0 | 41 | 6 | -| | [GIS](https://github.com/orgs/elastic/teams/kibana-gis) | - | 266 | 0 | 265 | 26 | +| | [GIS](https://github.com/orgs/elastic/teams/kibana-gis) | - | 267 | 0 | 266 | 26 | | | [GIS](https://github.com/orgs/elastic/teams/kibana-gis) | - | 67 | 0 | 67 | 0 | | | [Machine Learning UI](https://github.com/orgs/elastic/teams/ml-ui) | This plugin provides access to the machine learning features provided by Elastic. | 254 | 9 | 78 | 40 | | | [Stack Monitoring](https://github.com/orgs/elastic/teams/stack-monitoring-ui) | - | 15 | 3 | 13 | 1 | @@ -122,8 +122,8 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana'] | | [App Services](https://github.com/orgs/elastic/teams/kibana-app-services) | - | 34 | 0 | 34 | 2 | | | [Kibana Core](https://github.com/orgs/elastic/teams/kibana-core) | - | 17 | 0 | 17 | 0 | | | [App Services](https://github.com/orgs/elastic/teams/kibana-app-services) | - | 2 | 0 | 2 | 1 | -| | [Observability UI](https://github.com/orgs/elastic/teams/observability-ui) | - | 569 | 42 | 566 | 31 | -| | [Security asset management](https://github.com/orgs/elastic/teams/security-asset-management) | - | 21 | 0 | 21 | 4 | +| | [Observability UI](https://github.com/orgs/elastic/teams/observability-ui) | - | 571 | 40 | 567 | 31 | +| | [Security asset management](https://github.com/orgs/elastic/teams/security-asset-management) | - | 21 | 0 | 21 | 5 | | painlessLab | [Stack Management](https://github.com/orgs/elastic/teams/kibana-stack-management) | - | 0 | 0 | 0 | 0 | | | [Kibana Presentation](https://github.com/orgs/elastic/teams/kibana-presentation) | The Presentation Utility Plugin is a set of common, shared components and toolkits for solutions within the Presentation space, (e.g. Dashboards, Canvas). | 227 | 7 | 171 | 12 | | | [profiling](https://github.com/orgs/elastic/teams/profiling-ui) | - | 14 | 2 | 14 | 0 | @@ -137,12 +137,12 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana'] | | [Kibana Core](https://github.com/orgs/elastic/teams/kibana-core) | - | 130 | 0 | 117 | 0 | | | [Kibana Core](https://github.com/orgs/elastic/teams/kibana-core) | - | 79 | 0 | 73 | 3 | | | [Kibana Core](https://github.com/orgs/elastic/teams/kibana-core) | - | 98 | 0 | 50 | 1 | -| | [Data Discovery](https://github.com/orgs/elastic/teams/kibana-data-discovery) | This plugin contains the definition and helper methods around saved searches, used by discover and visualizations. | 44 | 0 | 44 | 1 | +| | [Data Discovery](https://github.com/orgs/elastic/teams/kibana-data-discovery) | This plugin contains the definition and helper methods around saved searches, used by discover and visualizations. | 45 | 0 | 45 | 1 | | | [App Services](https://github.com/orgs/elastic/teams/kibana-app-services) | - | 32 | 0 | 13 | 0 | | | [Kibana Reporting Services](https://github.com/orgs/elastic/teams/kibana-reporting-services) | Kibana Screenshotting Plugin | 27 | 0 | 8 | 4 | | searchprofiler | [Stack Management](https://github.com/orgs/elastic/teams/kibana-stack-management) | - | 0 | 0 | 0 | 0 | | | [Platform Security](https://github.com/orgs/elastic/teams/kibana-security) | This plugin provides authentication and authorization features, and exposes functionality to understand the capabilities of the currently authenticated user. | 250 | 0 | 90 | 1 | -| | [Security solution](https://github.com/orgs/elastic/teams/security-solution) | - | 112 | 0 | 75 | 27 | +| | [Security solution](https://github.com/orgs/elastic/teams/security-solution) | - | 112 | 0 | 75 | 28 | | | [Security Team](https://github.com/orgs/elastic/teams/security-team) | - | 7 | 0 | 7 | 1 | | | [App Services](https://github.com/orgs/elastic/teams/kibana-app-services) | Adds URL Service and sharing capabilities to Kibana | 115 | 0 | 56 | 10 | | | [Stack Management](https://github.com/orgs/elastic/teams/kibana-stack-management) | - | 22 | 1 | 22 | 1 | @@ -155,15 +155,15 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana'] | | [Kibana Telemetry](https://github.com/orgs/elastic/teams/kibana-telemetry) | - | 31 | 0 | 26 | 6 | | | [Kibana Telemetry](https://github.com/orgs/elastic/teams/kibana-telemetry) | - | 1 | 0 | 1 | 0 | | | [Kibana Telemetry](https://github.com/orgs/elastic/teams/kibana-telemetry) | - | 11 | 0 | 10 | 0 | -| | [Protections Experience Team](https://github.com/orgs/elastic/teams/protections-experience) | Elastic threat intelligence helps you see if you are open to or have been subject to current or historical known threats | 26 | 0 | 8 | 3 | -| | [Security solution](https://github.com/orgs/elastic/teams/security-solution) | - | 462 | 1 | 350 | 33 | +| | [Protections Experience Team](https://github.com/orgs/elastic/teams/protections-experience) | Elastic threat intelligence helps you see if you are open to or have been subject to current or historical known threats | 34 | 0 | 14 | 3 | +| | [Security solution](https://github.com/orgs/elastic/teams/security-solution) | - | 257 | 1 | 214 | 21 | | | [Machine Learning UI](https://github.com/orgs/elastic/teams/ml-ui) | This plugin provides access to the transforms features provided by Elastic. Transforms enable you to convert existing Elasticsearch indices into summarized indices, which provide opportunities for new insights and analytics. | 4 | 0 | 4 | 1 | | translations | [Kibana Localization](https://github.com/orgs/elastic/teams/kibana-localization) | - | 0 | 0 | 0 | 0 | -| | [Response Ops](https://github.com/orgs/elastic/teams/response-ops) | - | 531 | 11 | 502 | 51 | +| | [Response Ops](https://github.com/orgs/elastic/teams/response-ops) | - | 532 | 11 | 503 | 51 | | | [App Services](https://github.com/orgs/elastic/teams/kibana-app-services) | Adds UI Actions service to Kibana | 133 | 2 | 92 | 11 | | | [App Services](https://github.com/orgs/elastic/teams/kibana-app-services) | Extends UI Actions plugin with more functionality | 206 | 0 | 142 | 9 | -| | [Data Discovery](https://github.com/orgs/elastic/teams/kibana-data-discovery) | Contains functionality for the field list which can be integrated into apps | 203 | 0 | 192 | 7 | -| | [Data Discovery](https://github.com/orgs/elastic/teams/kibana-data-discovery) | The `unifiedHistogram` plugin provides UI components to create a layout including a resizable histogram and a main display. | 56 | 0 | 29 | 0 | +| | [Data Discovery](https://github.com/orgs/elastic/teams/kibana-data-discovery) | Contains functionality for the field list which can be integrated into apps | 215 | 0 | 203 | 7 | +| | [Data Discovery](https://github.com/orgs/elastic/teams/kibana-data-discovery) | The `unifiedHistogram` plugin provides UI components to create a layout including a resizable histogram and a main display. | 52 | 0 | 15 | 0 | | | [Visualizations](https://github.com/orgs/elastic/teams/kibana-visualizations) | Contains all the key functionality of Kibana's unified search experience.Contains all the key functionality of Kibana's unified search experience. | 134 | 2 | 106 | 18 | | upgradeAssistant | [Stack Management](https://github.com/orgs/elastic/teams/kibana-stack-management) | - | 0 | 0 | 0 | 0 | | urlDrilldown | [App Services](https://github.com/orgs/elastic/teams/kibana-app-services) | Adds drilldown implementations to Kibana | 0 | 0 | 0 | 0 | @@ -337,7 +337,7 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana'] | | Kibana Core | - | 83 | 0 | 39 | 0 | | | Kibana Core | - | 25 | 0 | 23 | 0 | | | Kibana Core | - | 4 | 0 | 4 | 0 | -| | Kibana Core | - | 110 | 0 | 78 | 44 | +| | Kibana Core | - | 112 | 0 | 79 | 45 | | | Kibana Core | - | 12 | 0 | 12 | 0 | | | Kibana Core | - | 297 | 0 | 90 | 0 | | | Kibana Core | - | 69 | 0 | 69 | 4 | @@ -359,9 +359,9 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana'] | | Kibana Core | - | 25 | 1 | 13 | 0 | | | Kibana Core | - | 25 | 1 | 25 | 0 | | | Kibana Core | - | 4 | 0 | 4 | 0 | -| | Kibana Core | - | 23 | 0 | 3 | 0 | +| | Kibana Core | - | 25 | 0 | 3 | 0 | | | Kibana Core | - | 27 | 1 | 13 | 0 | -| | Kibana Core | - | 28 | 1 | 28 | 2 | +| | Kibana Core | - | 18 | 1 | 17 | 3 | | | Kibana Core | - | 6 | 0 | 6 | 0 | | | Kibana Core | - | 153 | 0 | 142 | 0 | | | Kibana Core | - | 8 | 0 | 8 | 2 | @@ -388,7 +388,7 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana'] | | [Owner missing] | - | 1 | 0 | 0 | 0 | | | [Owner missing] | - | 3 | 0 | 0 | 0 | | | [Owner missing] | - | 23 | 0 | 21 | 1 | -| | [Owner missing] | - | 6 | 0 | 0 | 0 | +| | [Owner missing] | - | 17 | 1 | 8 | 0 | | | [Owner missing] | - | 3 | 0 | 3 | 0 | | | Kibana Core | - | 1 | 0 | 1 | 0 | | | [Owner missing] | - | 32 | 0 | 22 | 0 | @@ -397,7 +397,7 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana'] | | Kibana Core | - | 61 | 0 | 1 | 0 | | | [Owner missing] | - | 43 | 0 | 36 | 0 | | | Visualizations | - | 52 | 12 | 41 | 0 | -| | [Owner missing] | - | 20 | 0 | 20 | 2 | +| | [Owner missing] | - | 22 | 0 | 22 | 3 | | | [Owner missing] | - | 13 | 0 | 13 | 0 | | | [Owner missing] | - | 64 | 0 | 59 | 5 | | | [Owner missing] | - | 96 | 0 | 95 | 0 | @@ -419,10 +419,11 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana'] | | [Owner missing] | Just some helpers for kibana plugin devs. | 1 | 0 | 1 | 0 | | | [Owner missing] | - | 21 | 0 | 10 | 0 | | | [Owner missing] | - | 6 | 0 | 6 | 1 | +| | [Owner missing] | - | 11 | 2 | 7 | 0 | | | [Owner missing] | - | 96 | 0 | 93 | 0 | | | [Owner missing] | Security Solution auto complete | 56 | 1 | 41 | 1 | | | [Owner missing] | security solution elastic search utilities to use across plugins such lists, security_solution, cases, etc... | 67 | 0 | 61 | 1 | -| | [Owner missing] | - | 102 | 0 | 91 | 1 | +| | [Owner missing] | - | 104 | 0 | 93 | 1 | | | [Owner missing] | Security Solution utilities for React hooks | 15 | 0 | 7 | 0 | | | [Owner missing] | io ts utilities and types to be shared with plugins from the security solution project | 138 | 0 | 119 | 0 | | | [Owner missing] | io ts utilities and types to be shared with plugins from the security solution project | 511 | 1 | 498 | 0 | @@ -445,9 +446,11 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana'] | | [Owner missing] | - | 25 | 0 | 8 | 0 | | | [Owner missing] | - | 10 | 0 | 4 | 0 | | | [Owner missing] | - | 32 | 0 | 28 | 0 | +| | [Owner missing] | - | 5 | 0 | 4 | 0 | | | [Owner missing] | - | 3 | 0 | 2 | 0 | | | [Owner missing] | - | 2 | 0 | 2 | 0 | -| | [Owner missing] | - | 15 | 0 | 13 | 0 | +| | [Owner missing] | - | 1 | 0 | 1 | 0 | +| | [Owner missing] | - | 17 | 0 | 15 | 0 | | | [Owner missing] | - | 17 | 0 | 9 | 0 | | | [Owner missing] | - | 10 | 0 | 9 | 0 | | | [Owner missing] | - | 2 | 0 | 2 | 1 | @@ -465,6 +468,7 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana'] | | [Owner missing] | - | 5 | 0 | 3 | 0 | | | [Owner missing] | - | 25 | 0 | 10 | 0 | | | [Owner missing] | - | 17 | 0 | 16 | 0 | +| | [Owner missing] | - | 2 | 0 | 1 | 0 | | | [Owner missing] | - | 2 | 0 | 1 | 0 | | | [Owner missing] | - | 1 | 0 | 1 | 0 | | | [Owner missing] | - | 2 | 0 | 0 | 0 | diff --git a/api_docs/presentation_util.mdx b/api_docs/presentation_util.mdx index 55ce69b371c15..38a4022b241ad 100644 --- a/api_docs/presentation_util.mdx +++ b/api_docs/presentation_util.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/presentationUtil title: "presentationUtil" image: https://source.unsplash.com/400x175/?github description: API docs for the presentationUtil plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'presentationUtil'] --- import presentationUtilObj from './presentation_util.devdocs.json'; diff --git a/api_docs/profiling.mdx b/api_docs/profiling.mdx index db7a0b7cbfe39..b32fa5d54ef9e 100644 --- a/api_docs/profiling.mdx +++ b/api_docs/profiling.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/profiling title: "profiling" image: https://source.unsplash.com/400x175/?github description: API docs for the profiling plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'profiling'] --- import profilingObj from './profiling.devdocs.json'; diff --git a/api_docs/remote_clusters.mdx b/api_docs/remote_clusters.mdx index 97344f47e893e..459333b9e23e0 100644 --- a/api_docs/remote_clusters.mdx +++ b/api_docs/remote_clusters.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/remoteClusters title: "remoteClusters" image: https://source.unsplash.com/400x175/?github description: API docs for the remoteClusters plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'remoteClusters'] --- import remoteClustersObj from './remote_clusters.devdocs.json'; diff --git a/api_docs/reporting.mdx b/api_docs/reporting.mdx index ee34ab2658e18..a59c71d85d33d 100644 --- a/api_docs/reporting.mdx +++ b/api_docs/reporting.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/reporting title: "reporting" image: https://source.unsplash.com/400x175/?github description: API docs for the reporting plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'reporting'] --- import reportingObj from './reporting.devdocs.json'; diff --git a/api_docs/rollup.mdx b/api_docs/rollup.mdx index bf0fa8d7430eb..1148f83fb7f42 100644 --- a/api_docs/rollup.mdx +++ b/api_docs/rollup.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/rollup title: "rollup" image: https://source.unsplash.com/400x175/?github description: API docs for the rollup plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'rollup'] --- import rollupObj from './rollup.devdocs.json'; diff --git a/api_docs/rule_registry.devdocs.json b/api_docs/rule_registry.devdocs.json index ad2e887fe4838..94a78d49d29a0 100644 --- a/api_docs/rule_registry.devdocs.json +++ b/api_docs/rule_registry.devdocs.json @@ -4520,7 +4520,7 @@ "label": "ParsedTechnicalFields", "description": [], "signature": [ - "{ readonly '@timestamp': string; readonly \"kibana.alert.rule.rule_type_id\": string; readonly \"kibana.alert.rule.consumer\": string; readonly \"kibana.alert.rule.producer\": string; readonly \"kibana.space_ids\": string[]; readonly \"kibana.alert.uuid\": string; readonly \"kibana.alert.instance.id\": string; readonly \"kibana.alert.status\": string; readonly \"kibana.alert.rule.category\": string; readonly \"kibana.alert.rule.uuid\": string; readonly \"kibana.alert.rule.name\": string; readonly tags?: string[] | undefined; readonly 'event.action'?: string | undefined; readonly \"kibana.alert.rule.execution.uuid\"?: string | undefined; readonly \"kibana.alert.rule.parameters\"?: { [key: string]: unknown; } | undefined; readonly \"kibana.alert.start\"?: string | undefined; readonly \"kibana.alert.time_range\"?: unknown; readonly \"kibana.alert.end\"?: string | undefined; readonly \"kibana.alert.duration.us\"?: number | undefined; readonly \"kibana.alert.severity\"?: string | undefined; readonly \"kibana.alert.flapping\"?: number | boolean | undefined; readonly \"kibana.version\"?: string | undefined; readonly \"ecs.version\"?: string | undefined; readonly \"kibana.alert.risk_score\"?: number | undefined; readonly \"kibana.alert.workflow_status\"?: string | undefined; readonly \"kibana.alert.workflow_user\"?: string | undefined; readonly \"kibana.alert.workflow_reason\"?: string | undefined; readonly \"kibana.alert.system_status\"?: string | undefined; readonly \"kibana.alert.action_group\"?: string | undefined; readonly \"kibana.alert.reason\"?: string | undefined; readonly \"kibana.alert.rule.author\"?: string | undefined; readonly \"kibana.alert.rule.created_at\"?: string | undefined; readonly \"kibana.alert.rule.created_by\"?: string | undefined; readonly \"kibana.alert.rule.description\"?: string | undefined; readonly \"kibana.alert.rule.enabled\"?: string | undefined; readonly \"kibana.alert.rule.from\"?: string | undefined; readonly \"kibana.alert.rule.interval\"?: string | undefined; readonly \"kibana.alert.rule.license\"?: string | undefined; readonly \"kibana.alert.rule.note\"?: string | undefined; readonly \"kibana.alert.rule.references\"?: string[] | undefined; readonly \"kibana.alert.rule.rule_id\"?: string | undefined; readonly \"kibana.alert.rule.rule_name_override\"?: string | undefined; readonly \"kibana.alert.rule.tags\"?: string[] | undefined; readonly \"kibana.alert.rule.to\"?: string | undefined; readonly \"kibana.alert.rule.type\"?: string | undefined; readonly \"kibana.alert.rule.updated_at\"?: string | undefined; readonly \"kibana.alert.rule.updated_by\"?: string | undefined; readonly \"kibana.alert.rule.version\"?: string | undefined; readonly \"kibana.alert.suppression.terms.field\"?: string[] | undefined; readonly \"kibana.alert.suppression.terms.value\"?: string[] | undefined; readonly \"kibana.alert.suppression.start\"?: string | undefined; readonly \"kibana.alert.suppression.end\"?: string | undefined; readonly \"kibana.alert.suppression.docs_count\"?: number | undefined; readonly 'event.kind'?: string | undefined; }" + "{ readonly '@timestamp': string; readonly \"kibana.alert.rule.rule_type_id\": string; readonly \"kibana.alert.rule.consumer\": string; readonly \"kibana.alert.rule.producer\": string; readonly \"kibana.space_ids\": string[]; readonly \"kibana.alert.uuid\": string; readonly \"kibana.alert.instance.id\": string; readonly \"kibana.alert.status\": string; readonly \"kibana.alert.rule.category\": string; readonly \"kibana.alert.rule.uuid\": string; readonly \"kibana.alert.rule.name\": string; readonly tags?: string[] | undefined; readonly 'event.action'?: string | undefined; readonly \"kibana.alert.rule.execution.uuid\"?: string | undefined; readonly \"kibana.alert.rule.parameters\"?: { [key: string]: unknown; } | undefined; readonly \"kibana.alert.start\"?: string | undefined; readonly \"kibana.alert.time_range\"?: unknown; readonly \"kibana.alert.end\"?: string | undefined; readonly \"kibana.alert.duration.us\"?: number | undefined; readonly \"kibana.alert.severity\"?: string | undefined; readonly \"kibana.alert.flapping\"?: boolean | undefined; readonly \"kibana.version\"?: string | undefined; readonly \"ecs.version\"?: string | undefined; readonly \"kibana.alert.risk_score\"?: number | undefined; readonly \"kibana.alert.workflow_status\"?: string | undefined; readonly \"kibana.alert.workflow_user\"?: string | undefined; readonly \"kibana.alert.workflow_reason\"?: string | undefined; readonly \"kibana.alert.system_status\"?: string | undefined; readonly \"kibana.alert.action_group\"?: string | undefined; readonly \"kibana.alert.reason\"?: string | undefined; readonly \"kibana.alert.rule.author\"?: string | undefined; readonly \"kibana.alert.rule.created_at\"?: string | undefined; readonly \"kibana.alert.rule.created_by\"?: string | undefined; readonly \"kibana.alert.rule.description\"?: string | undefined; readonly \"kibana.alert.rule.enabled\"?: string | undefined; readonly \"kibana.alert.rule.from\"?: string | undefined; readonly \"kibana.alert.rule.interval\"?: string | undefined; readonly \"kibana.alert.rule.license\"?: string | undefined; readonly \"kibana.alert.rule.note\"?: string | undefined; readonly \"kibana.alert.rule.references\"?: string[] | undefined; readonly \"kibana.alert.rule.rule_id\"?: string | undefined; readonly \"kibana.alert.rule.rule_name_override\"?: string | undefined; readonly \"kibana.alert.rule.tags\"?: string[] | undefined; readonly \"kibana.alert.rule.to\"?: string | undefined; readonly \"kibana.alert.rule.type\"?: string | undefined; readonly \"kibana.alert.rule.updated_at\"?: string | undefined; readonly \"kibana.alert.rule.updated_by\"?: string | undefined; readonly \"kibana.alert.rule.version\"?: string | undefined; readonly \"kibana.alert.suppression.terms.field\"?: string[] | undefined; readonly \"kibana.alert.suppression.terms.value\"?: string[] | undefined; readonly \"kibana.alert.suppression.start\"?: string | undefined; readonly \"kibana.alert.suppression.end\"?: string | undefined; readonly \"kibana.alert.suppression.docs_count\"?: number | undefined; readonly 'event.kind'?: string | undefined; }" ], "path": "x-pack/plugins/rule_registry/common/parse_technical_fields.ts", "deprecated": false, diff --git a/api_docs/rule_registry.mdx b/api_docs/rule_registry.mdx index 5526d70e124da..419b45a9c1add 100644 --- a/api_docs/rule_registry.mdx +++ b/api_docs/rule_registry.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/ruleRegistry title: "ruleRegistry" image: https://source.unsplash.com/400x175/?github description: API docs for the ruleRegistry plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'ruleRegistry'] --- import ruleRegistryObj from './rule_registry.devdocs.json'; diff --git a/api_docs/runtime_fields.mdx b/api_docs/runtime_fields.mdx index 0d29875f6cc79..7f1a3b20364b4 100644 --- a/api_docs/runtime_fields.mdx +++ b/api_docs/runtime_fields.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/runtimeFields title: "runtimeFields" image: https://source.unsplash.com/400x175/?github description: API docs for the runtimeFields plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'runtimeFields'] --- import runtimeFieldsObj from './runtime_fields.devdocs.json'; diff --git a/api_docs/saved_objects.devdocs.json b/api_docs/saved_objects.devdocs.json index 46665808b0549..0762363169d52 100644 --- a/api_docs/saved_objects.devdocs.json +++ b/api_docs/saved_objects.devdocs.json @@ -631,14 +631,6 @@ "plugin": "embeddable", "path": "src/plugins/embeddable/public/lib/attribute_service/attribute_service.tsx" }, - { - "plugin": "discover", - "path": "src/plugins/discover/public/application/main/components/top_nav/on_save_search.tsx" - }, - { - "plugin": "discover", - "path": "src/plugins/discover/public/application/main/components/top_nav/on_save_search.tsx" - }, { "plugin": "presentationUtil", "path": "src/plugins/presentation_util/public/components/saved_object_save_modal_dashboard.tsx" @@ -655,6 +647,14 @@ "plugin": "dashboard", "path": "src/plugins/dashboard/public/application/top_nav/save_modal.tsx" }, + { + "plugin": "discover", + "path": "src/plugins/discover/public/application/main/components/top_nav/on_save_search.tsx" + }, + { + "plugin": "discover", + "path": "src/plugins/discover/public/application/main/components/top_nav/on_save_search.tsx" + }, { "plugin": "graph", "path": "x-pack/plugins/graph/public/components/save_modal.tsx" diff --git a/api_docs/saved_objects.mdx b/api_docs/saved_objects.mdx index 740786bed70ef..3b8e5630ff01d 100644 --- a/api_docs/saved_objects.mdx +++ b/api_docs/saved_objects.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/savedObjects title: "savedObjects" image: https://source.unsplash.com/400x175/?github description: API docs for the savedObjects plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'savedObjects'] --- import savedObjectsObj from './saved_objects.devdocs.json'; diff --git a/api_docs/saved_objects_finder.mdx b/api_docs/saved_objects_finder.mdx index a62532d3bd37e..354d5c72abdf7 100644 --- a/api_docs/saved_objects_finder.mdx +++ b/api_docs/saved_objects_finder.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/savedObjectsFinder title: "savedObjectsFinder" image: https://source.unsplash.com/400x175/?github description: API docs for the savedObjectsFinder plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'savedObjectsFinder'] --- import savedObjectsFinderObj from './saved_objects_finder.devdocs.json'; diff --git a/api_docs/saved_objects_management.devdocs.json b/api_docs/saved_objects_management.devdocs.json index 5712c5eb8d603..c51e3b00daf11 100644 --- a/api_docs/saved_objects_management.devdocs.json +++ b/api_docs/saved_objects_management.devdocs.json @@ -294,7 +294,7 @@ "label": "euiColumn", "description": [], "signature": [ - "{ children?: React.ReactNode; name: React.ReactNode; description?: string | undefined; onError?: React.ReactEventHandler | undefined; render?: ((value: any, record: ", + "{ children?: React.ReactNode; name: React.ReactNode; description?: string | undefined; scope?: string | undefined; onError?: React.ReactEventHandler | undefined; render?: ((value: any, record: ", { "pluginId": "savedObjectsManagement", "scope": "public", @@ -310,7 +310,7 @@ "section": "def-public.SavedObjectsManagementRecord", "text": "SavedObjectsManagementRecord" }, - "; headers?: string | undefined; defaultValue?: string | number | readonly string[] | undefined; lang?: string | undefined; defaultChecked?: boolean | undefined; suppressContentEditableWarning?: boolean | undefined; suppressHydrationWarning?: boolean | undefined; accessKey?: string | undefined; contentEditable?: \"inherit\" | Booleanish | undefined; contextMenu?: string | undefined; dir?: string | undefined; draggable?: Booleanish | undefined; placeholder?: string | undefined; slot?: string | undefined; spellCheck?: Booleanish | undefined; style?: React.CSSProperties | undefined; tabIndex?: number | undefined; translate?: \"no\" | \"yes\" | undefined; radioGroup?: string | undefined; role?: React.AriaRole | undefined; about?: string | undefined; datatype?: string | undefined; inlist?: any; prefix?: string | undefined; property?: string | undefined; resource?: string | undefined; typeof?: string | undefined; vocab?: string | undefined; autoCapitalize?: string | undefined; autoCorrect?: string | undefined; autoSave?: string | undefined; itemProp?: string | undefined; itemScope?: boolean | undefined; itemType?: string | undefined; itemID?: string | undefined; itemRef?: string | undefined; results?: number | undefined; unselectable?: \"on\" | \"off\" | undefined; inputMode?: \"none\" | \"email\" | \"search\" | \"text\" | \"tel\" | \"url\" | \"numeric\" | \"decimal\" | undefined; is?: string | undefined; 'aria-activedescendant'?: string | undefined; 'aria-atomic'?: Booleanish | undefined; 'aria-autocomplete'?: \"none\" | \"list\" | \"inline\" | \"both\" | undefined; 'aria-busy'?: Booleanish | undefined; 'aria-checked'?: boolean | \"mixed\" | \"false\" | \"true\" | undefined; 'aria-colcount'?: number | undefined; 'aria-colindex'?: number | undefined; 'aria-colspan'?: number | undefined; 'aria-controls'?: string | undefined; 'aria-current'?: boolean | \"date\" | \"location\" | \"time\" | \"page\" | \"false\" | \"true\" | \"step\" | undefined; 'aria-describedby'?: string | undefined; 'aria-details'?: string | undefined; 'aria-disabled'?: Booleanish | undefined; 'aria-dropeffect'?: \"none\" | \"copy\" | \"link\" | \"execute\" | \"move\" | \"popup\" | undefined; 'aria-errormessage'?: string | undefined; 'aria-expanded'?: Booleanish | undefined; 'aria-flowto'?: string | undefined; 'aria-grabbed'?: Booleanish | undefined; 'aria-haspopup'?: boolean | \"grid\" | \"menu\" | \"false\" | \"true\" | \"dialog\" | \"listbox\" | \"tree\" | undefined; 'aria-hidden'?: Booleanish | undefined; 'aria-invalid'?: boolean | \"false\" | \"true\" | \"grammar\" | \"spelling\" | undefined; 'aria-keyshortcuts'?: string | undefined; 'aria-label'?: string | undefined; 'aria-labelledby'?: string | undefined; 'aria-level'?: number | undefined; 'aria-live'?: \"off\" | \"assertive\" | \"polite\" | undefined; 'aria-modal'?: Booleanish | undefined; 'aria-multiline'?: Booleanish | undefined; 'aria-multiselectable'?: Booleanish | undefined; 'aria-orientation'?: \"horizontal\" | \"vertical\" | undefined; 'aria-owns'?: string | undefined; 'aria-placeholder'?: string | undefined; 'aria-posinset'?: number | undefined; 'aria-pressed'?: boolean | \"mixed\" | \"false\" | \"true\" | undefined; 'aria-readonly'?: Booleanish | undefined; 'aria-relevant'?: \"all\" | \"text\" | \"additions\" | \"additions removals\" | \"additions text\" | \"removals\" | \"removals additions\" | \"removals text\" | \"text additions\" | \"text removals\" | undefined; 'aria-required'?: Booleanish | undefined; 'aria-roledescription'?: string | undefined; 'aria-rowcount'?: number | undefined; 'aria-rowindex'?: number | undefined; 'aria-rowspan'?: number | undefined; 'aria-selected'?: Booleanish | undefined; 'aria-setsize'?: number | undefined; 'aria-sort'?: \"none\" | \"other\" | \"ascending\" | \"descending\" | undefined; 'aria-valuemax'?: number | undefined; 'aria-valuemin'?: number | undefined; 'aria-valuenow'?: number | undefined; 'aria-valuetext'?: string | undefined; dangerouslySetInnerHTML?: { __html: string; } | undefined; onCopy?: React.ClipboardEventHandler | undefined; onCopyCapture?: React.ClipboardEventHandler | undefined; onCut?: React.ClipboardEventHandler | undefined; onCutCapture?: React.ClipboardEventHandler | undefined; onPaste?: React.ClipboardEventHandler | undefined; onPasteCapture?: React.ClipboardEventHandler | undefined; onCompositionEnd?: React.CompositionEventHandler | undefined; onCompositionEndCapture?: React.CompositionEventHandler | undefined; onCompositionStart?: React.CompositionEventHandler | undefined; onCompositionStartCapture?: React.CompositionEventHandler | undefined; onCompositionUpdate?: React.CompositionEventHandler | undefined; onCompositionUpdateCapture?: React.CompositionEventHandler | undefined; onFocus?: React.FocusEventHandler | undefined; onFocusCapture?: React.FocusEventHandler | undefined; onBlur?: React.FocusEventHandler | undefined; onBlurCapture?: React.FocusEventHandler | undefined; onChangeCapture?: React.FormEventHandler | undefined; onBeforeInput?: React.FormEventHandler | undefined; onBeforeInputCapture?: React.FormEventHandler | undefined; onInput?: React.FormEventHandler | undefined; onInputCapture?: React.FormEventHandler | undefined; onReset?: React.FormEventHandler | undefined; onResetCapture?: React.FormEventHandler | undefined; onSubmit?: React.FormEventHandler | undefined; onSubmitCapture?: React.FormEventHandler | undefined; onInvalid?: React.FormEventHandler | undefined; onInvalidCapture?: React.FormEventHandler | undefined; onLoad?: React.ReactEventHandler | undefined; onLoadCapture?: React.ReactEventHandler | undefined; onErrorCapture?: React.ReactEventHandler | undefined; onKeyDownCapture?: React.KeyboardEventHandler | undefined; onKeyPress?: React.KeyboardEventHandler | undefined; onKeyPressCapture?: React.KeyboardEventHandler | undefined; onKeyUp?: React.KeyboardEventHandler | undefined; onKeyUpCapture?: React.KeyboardEventHandler | undefined; onAbort?: React.ReactEventHandler | undefined; onAbortCapture?: React.ReactEventHandler | undefined; onCanPlay?: React.ReactEventHandler | undefined; onCanPlayCapture?: React.ReactEventHandler | undefined; onCanPlayThrough?: React.ReactEventHandler | undefined; onCanPlayThroughCapture?: React.ReactEventHandler | undefined; onDurationChange?: React.ReactEventHandler | undefined; onDurationChangeCapture?: React.ReactEventHandler | undefined; onEmptied?: React.ReactEventHandler | undefined; onEmptiedCapture?: React.ReactEventHandler | undefined; onEncrypted?: React.ReactEventHandler | undefined; onEncryptedCapture?: React.ReactEventHandler | undefined; onEnded?: React.ReactEventHandler | undefined; onEndedCapture?: React.ReactEventHandler | undefined; onLoadedData?: React.ReactEventHandler | undefined; onLoadedDataCapture?: React.ReactEventHandler | undefined; onLoadedMetadata?: React.ReactEventHandler | undefined; onLoadedMetadataCapture?: React.ReactEventHandler | undefined; onLoadStart?: React.ReactEventHandler | undefined; onLoadStartCapture?: React.ReactEventHandler | undefined; onPause?: React.ReactEventHandler | undefined; onPauseCapture?: React.ReactEventHandler | undefined; onPlay?: React.ReactEventHandler | undefined; onPlayCapture?: React.ReactEventHandler | undefined; onPlaying?: React.ReactEventHandler | undefined; onPlayingCapture?: React.ReactEventHandler | undefined; onProgress?: React.ReactEventHandler | undefined; onProgressCapture?: React.ReactEventHandler | undefined; onRateChange?: React.ReactEventHandler | undefined; onRateChangeCapture?: React.ReactEventHandler | undefined; onSeeked?: React.ReactEventHandler | undefined; onSeekedCapture?: React.ReactEventHandler | undefined; onSeeking?: React.ReactEventHandler | undefined; onSeekingCapture?: React.ReactEventHandler | undefined; onStalled?: React.ReactEventHandler | undefined; onStalledCapture?: React.ReactEventHandler | undefined; onSuspend?: React.ReactEventHandler | undefined; onSuspendCapture?: React.ReactEventHandler | undefined; onTimeUpdate?: React.ReactEventHandler | undefined; onTimeUpdateCapture?: React.ReactEventHandler | undefined; onVolumeChange?: React.ReactEventHandler | undefined; onVolumeChangeCapture?: React.ReactEventHandler | undefined; onWaiting?: React.ReactEventHandler | undefined; onWaitingCapture?: React.ReactEventHandler | undefined; onAuxClick?: React.MouseEventHandler | undefined; onAuxClickCapture?: React.MouseEventHandler | undefined; onClickCapture?: React.MouseEventHandler | undefined; onContextMenu?: React.MouseEventHandler | undefined; onContextMenuCapture?: React.MouseEventHandler | undefined; onDoubleClick?: React.MouseEventHandler | undefined; onDoubleClickCapture?: React.MouseEventHandler | undefined; onDrag?: React.DragEventHandler | undefined; onDragCapture?: React.DragEventHandler | undefined; onDragEnd?: React.DragEventHandler | undefined; onDragEndCapture?: React.DragEventHandler | undefined; onDragEnter?: React.DragEventHandler | undefined; onDragEnterCapture?: React.DragEventHandler | undefined; onDragExit?: React.DragEventHandler | undefined; onDragExitCapture?: React.DragEventHandler | undefined; onDragLeave?: React.DragEventHandler | undefined; onDragLeaveCapture?: React.DragEventHandler | undefined; onDragOver?: React.DragEventHandler | undefined; onDragOverCapture?: React.DragEventHandler | undefined; onDragStart?: React.DragEventHandler | undefined; onDragStartCapture?: React.DragEventHandler | undefined; onDrop?: React.DragEventHandler | undefined; onDropCapture?: React.DragEventHandler | undefined; onMouseDown?: React.MouseEventHandler | undefined; onMouseDownCapture?: React.MouseEventHandler | undefined; onMouseEnter?: React.MouseEventHandler | undefined; onMouseLeave?: React.MouseEventHandler | undefined; onMouseMove?: React.MouseEventHandler | undefined; onMouseMoveCapture?: React.MouseEventHandler | undefined; onMouseOut?: React.MouseEventHandler | undefined; onMouseOutCapture?: React.MouseEventHandler | undefined; onMouseOver?: React.MouseEventHandler | undefined; onMouseOverCapture?: React.MouseEventHandler | undefined; onMouseUp?: React.MouseEventHandler | undefined; onMouseUpCapture?: React.MouseEventHandler | undefined; onSelect?: React.ReactEventHandler | undefined; onSelectCapture?: React.ReactEventHandler | undefined; onTouchCancel?: React.TouchEventHandler | undefined; onTouchCancelCapture?: React.TouchEventHandler | undefined; onTouchEnd?: React.TouchEventHandler | undefined; onTouchEndCapture?: React.TouchEventHandler | undefined; onTouchMove?: React.TouchEventHandler | undefined; onTouchMoveCapture?: React.TouchEventHandler | undefined; onTouchStart?: React.TouchEventHandler | undefined; onTouchStartCapture?: React.TouchEventHandler | undefined; onPointerDown?: React.PointerEventHandler | undefined; onPointerDownCapture?: React.PointerEventHandler | undefined; onPointerMove?: React.PointerEventHandler | undefined; onPointerMoveCapture?: React.PointerEventHandler | undefined; onPointerUp?: React.PointerEventHandler | undefined; onPointerUpCapture?: React.PointerEventHandler | undefined; onPointerCancel?: React.PointerEventHandler | undefined; onPointerCancelCapture?: React.PointerEventHandler | undefined; onPointerEnter?: React.PointerEventHandler | undefined; onPointerEnterCapture?: React.PointerEventHandler | undefined; onPointerLeave?: React.PointerEventHandler | undefined; onPointerLeaveCapture?: React.PointerEventHandler | undefined; onPointerOver?: React.PointerEventHandler | undefined; onPointerOverCapture?: React.PointerEventHandler | undefined; onPointerOut?: React.PointerEventHandler | undefined; onPointerOutCapture?: React.PointerEventHandler | undefined; onGotPointerCapture?: React.PointerEventHandler | undefined; onGotPointerCaptureCapture?: React.PointerEventHandler | undefined; onLostPointerCapture?: React.PointerEventHandler | undefined; onLostPointerCaptureCapture?: React.PointerEventHandler | undefined; onScroll?: React.UIEventHandler | undefined; onScrollCapture?: React.UIEventHandler | undefined; onWheel?: React.WheelEventHandler | undefined; onWheelCapture?: React.WheelEventHandler | undefined; onAnimationStart?: React.AnimationEventHandler | undefined; onAnimationStartCapture?: React.AnimationEventHandler | undefined; onAnimationEnd?: React.AnimationEventHandler | undefined; onAnimationEndCapture?: React.AnimationEventHandler | undefined; onAnimationIteration?: React.AnimationEventHandler | undefined; onAnimationIterationCapture?: React.AnimationEventHandler | undefined; onTransitionEnd?: React.TransitionEventHandler | undefined; onTransitionEndCapture?: React.TransitionEventHandler | undefined; 'data-test-subj'?: string | undefined; css?: ", + "; headers?: string | undefined; defaultValue?: string | number | readonly string[] | undefined; lang?: string | undefined; defaultChecked?: boolean | undefined; suppressContentEditableWarning?: boolean | undefined; suppressHydrationWarning?: boolean | undefined; accessKey?: string | undefined; contentEditable?: \"inherit\" | Booleanish | undefined; contextMenu?: string | undefined; dir?: string | undefined; draggable?: Booleanish | undefined; placeholder?: string | undefined; slot?: string | undefined; spellCheck?: Booleanish | undefined; style?: React.CSSProperties | undefined; tabIndex?: number | undefined; translate?: \"no\" | \"yes\" | undefined; radioGroup?: string | undefined; role?: React.AriaRole | undefined; about?: string | undefined; datatype?: string | undefined; inlist?: any; prefix?: string | undefined; property?: string | undefined; resource?: string | undefined; typeof?: string | undefined; vocab?: string | undefined; autoCapitalize?: string | undefined; autoCorrect?: string | undefined; autoSave?: string | undefined; itemProp?: string | undefined; itemScope?: boolean | undefined; itemType?: string | undefined; itemID?: string | undefined; itemRef?: string | undefined; results?: number | undefined; unselectable?: \"on\" | \"off\" | undefined; inputMode?: \"none\" | \"email\" | \"search\" | \"text\" | \"url\" | \"tel\" | \"numeric\" | \"decimal\" | undefined; is?: string | undefined; 'aria-activedescendant'?: string | undefined; 'aria-atomic'?: Booleanish | undefined; 'aria-autocomplete'?: \"none\" | \"list\" | \"inline\" | \"both\" | undefined; 'aria-busy'?: Booleanish | undefined; 'aria-checked'?: boolean | \"mixed\" | \"false\" | \"true\" | undefined; 'aria-colcount'?: number | undefined; 'aria-colindex'?: number | undefined; 'aria-colspan'?: number | undefined; 'aria-controls'?: string | undefined; 'aria-current'?: boolean | \"date\" | \"location\" | \"time\" | \"page\" | \"false\" | \"true\" | \"step\" | undefined; 'aria-describedby'?: string | undefined; 'aria-details'?: string | undefined; 'aria-disabled'?: Booleanish | undefined; 'aria-dropeffect'?: \"none\" | \"copy\" | \"link\" | \"execute\" | \"move\" | \"popup\" | undefined; 'aria-errormessage'?: string | undefined; 'aria-expanded'?: Booleanish | undefined; 'aria-flowto'?: string | undefined; 'aria-grabbed'?: Booleanish | undefined; 'aria-haspopup'?: boolean | \"grid\" | \"menu\" | \"false\" | \"true\" | \"dialog\" | \"listbox\" | \"tree\" | undefined; 'aria-hidden'?: Booleanish | undefined; 'aria-invalid'?: boolean | \"false\" | \"true\" | \"grammar\" | \"spelling\" | undefined; 'aria-keyshortcuts'?: string | undefined; 'aria-label'?: string | undefined; 'aria-labelledby'?: string | undefined; 'aria-level'?: number | undefined; 'aria-live'?: \"off\" | \"assertive\" | \"polite\" | undefined; 'aria-modal'?: Booleanish | undefined; 'aria-multiline'?: Booleanish | undefined; 'aria-multiselectable'?: Booleanish | undefined; 'aria-orientation'?: \"horizontal\" | \"vertical\" | undefined; 'aria-owns'?: string | undefined; 'aria-placeholder'?: string | undefined; 'aria-posinset'?: number | undefined; 'aria-pressed'?: boolean | \"mixed\" | \"false\" | \"true\" | undefined; 'aria-readonly'?: Booleanish | undefined; 'aria-relevant'?: \"all\" | \"text\" | \"additions\" | \"additions removals\" | \"additions text\" | \"removals\" | \"removals additions\" | \"removals text\" | \"text additions\" | \"text removals\" | undefined; 'aria-required'?: Booleanish | undefined; 'aria-roledescription'?: string | undefined; 'aria-rowcount'?: number | undefined; 'aria-rowindex'?: number | undefined; 'aria-rowspan'?: number | undefined; 'aria-selected'?: Booleanish | undefined; 'aria-setsize'?: number | undefined; 'aria-sort'?: \"none\" | \"other\" | \"ascending\" | \"descending\" | undefined; 'aria-valuemax'?: number | undefined; 'aria-valuemin'?: number | undefined; 'aria-valuenow'?: number | undefined; 'aria-valuetext'?: string | undefined; dangerouslySetInnerHTML?: { __html: string; } | undefined; onCopy?: React.ClipboardEventHandler | undefined; onCopyCapture?: React.ClipboardEventHandler | undefined; onCut?: React.ClipboardEventHandler | undefined; onCutCapture?: React.ClipboardEventHandler | undefined; onPaste?: React.ClipboardEventHandler | undefined; onPasteCapture?: React.ClipboardEventHandler | undefined; onCompositionEnd?: React.CompositionEventHandler | undefined; onCompositionEndCapture?: React.CompositionEventHandler | undefined; onCompositionStart?: React.CompositionEventHandler | undefined; onCompositionStartCapture?: React.CompositionEventHandler | undefined; onCompositionUpdate?: React.CompositionEventHandler | undefined; onCompositionUpdateCapture?: React.CompositionEventHandler | undefined; onFocus?: React.FocusEventHandler | undefined; onFocusCapture?: React.FocusEventHandler | undefined; onBlur?: React.FocusEventHandler | undefined; onBlurCapture?: React.FocusEventHandler | undefined; onChangeCapture?: React.FormEventHandler | undefined; onBeforeInput?: React.FormEventHandler | undefined; onBeforeInputCapture?: React.FormEventHandler | undefined; onInput?: React.FormEventHandler | undefined; onInputCapture?: React.FormEventHandler | undefined; onReset?: React.FormEventHandler | undefined; onResetCapture?: React.FormEventHandler | undefined; onSubmit?: React.FormEventHandler | undefined; onSubmitCapture?: React.FormEventHandler | undefined; onInvalid?: React.FormEventHandler | undefined; onInvalidCapture?: React.FormEventHandler | undefined; onLoad?: React.ReactEventHandler | undefined; onLoadCapture?: React.ReactEventHandler | undefined; onErrorCapture?: React.ReactEventHandler | undefined; onKeyDownCapture?: React.KeyboardEventHandler | undefined; onKeyPress?: React.KeyboardEventHandler | undefined; onKeyPressCapture?: React.KeyboardEventHandler | undefined; onKeyUp?: React.KeyboardEventHandler | undefined; onKeyUpCapture?: React.KeyboardEventHandler | undefined; onAbort?: React.ReactEventHandler | undefined; onAbortCapture?: React.ReactEventHandler | undefined; onCanPlay?: React.ReactEventHandler | undefined; onCanPlayCapture?: React.ReactEventHandler | undefined; onCanPlayThrough?: React.ReactEventHandler | undefined; onCanPlayThroughCapture?: React.ReactEventHandler | undefined; onDurationChange?: React.ReactEventHandler | undefined; onDurationChangeCapture?: React.ReactEventHandler | undefined; onEmptied?: React.ReactEventHandler | undefined; onEmptiedCapture?: React.ReactEventHandler | undefined; onEncrypted?: React.ReactEventHandler | undefined; onEncryptedCapture?: React.ReactEventHandler | undefined; onEnded?: React.ReactEventHandler | undefined; onEndedCapture?: React.ReactEventHandler | undefined; onLoadedData?: React.ReactEventHandler | undefined; onLoadedDataCapture?: React.ReactEventHandler | undefined; onLoadedMetadata?: React.ReactEventHandler | undefined; onLoadedMetadataCapture?: React.ReactEventHandler | undefined; onLoadStart?: React.ReactEventHandler | undefined; onLoadStartCapture?: React.ReactEventHandler | undefined; onPause?: React.ReactEventHandler | undefined; onPauseCapture?: React.ReactEventHandler | undefined; onPlay?: React.ReactEventHandler | undefined; onPlayCapture?: React.ReactEventHandler | undefined; onPlaying?: React.ReactEventHandler | undefined; onPlayingCapture?: React.ReactEventHandler | undefined; onProgress?: React.ReactEventHandler | undefined; onProgressCapture?: React.ReactEventHandler | undefined; onRateChange?: React.ReactEventHandler | undefined; onRateChangeCapture?: React.ReactEventHandler | undefined; onSeeked?: React.ReactEventHandler | undefined; onSeekedCapture?: React.ReactEventHandler | undefined; onSeeking?: React.ReactEventHandler | undefined; onSeekingCapture?: React.ReactEventHandler | undefined; onStalled?: React.ReactEventHandler | undefined; onStalledCapture?: React.ReactEventHandler | undefined; onSuspend?: React.ReactEventHandler | undefined; onSuspendCapture?: React.ReactEventHandler | undefined; onTimeUpdate?: React.ReactEventHandler | undefined; onTimeUpdateCapture?: React.ReactEventHandler | undefined; onVolumeChange?: React.ReactEventHandler | undefined; onVolumeChangeCapture?: React.ReactEventHandler | undefined; onWaiting?: React.ReactEventHandler | undefined; onWaitingCapture?: React.ReactEventHandler | undefined; onAuxClick?: React.MouseEventHandler | undefined; onAuxClickCapture?: React.MouseEventHandler | undefined; onClickCapture?: React.MouseEventHandler | undefined; onContextMenu?: React.MouseEventHandler | undefined; onContextMenuCapture?: React.MouseEventHandler | undefined; onDoubleClick?: React.MouseEventHandler | undefined; onDoubleClickCapture?: React.MouseEventHandler | undefined; onDrag?: React.DragEventHandler | undefined; onDragCapture?: React.DragEventHandler | undefined; onDragEnd?: React.DragEventHandler | undefined; onDragEndCapture?: React.DragEventHandler | undefined; onDragEnter?: React.DragEventHandler | undefined; onDragEnterCapture?: React.DragEventHandler | undefined; onDragExit?: React.DragEventHandler | undefined; onDragExitCapture?: React.DragEventHandler | undefined; onDragLeave?: React.DragEventHandler | undefined; onDragLeaveCapture?: React.DragEventHandler | undefined; onDragOver?: React.DragEventHandler | undefined; onDragOverCapture?: React.DragEventHandler | undefined; onDragStart?: React.DragEventHandler | undefined; onDragStartCapture?: React.DragEventHandler | undefined; onDrop?: React.DragEventHandler | undefined; onDropCapture?: React.DragEventHandler | undefined; onMouseDown?: React.MouseEventHandler | undefined; onMouseDownCapture?: React.MouseEventHandler | undefined; onMouseEnter?: React.MouseEventHandler | undefined; onMouseLeave?: React.MouseEventHandler | undefined; onMouseMove?: React.MouseEventHandler | undefined; onMouseMoveCapture?: React.MouseEventHandler | undefined; onMouseOut?: React.MouseEventHandler | undefined; onMouseOutCapture?: React.MouseEventHandler | undefined; onMouseOver?: React.MouseEventHandler | undefined; onMouseOverCapture?: React.MouseEventHandler | undefined; onMouseUp?: React.MouseEventHandler | undefined; onMouseUpCapture?: React.MouseEventHandler | undefined; onSelect?: React.ReactEventHandler | undefined; onSelectCapture?: React.ReactEventHandler | undefined; onTouchCancel?: React.TouchEventHandler | undefined; onTouchCancelCapture?: React.TouchEventHandler | undefined; onTouchEnd?: React.TouchEventHandler | undefined; onTouchEndCapture?: React.TouchEventHandler | undefined; onTouchMove?: React.TouchEventHandler | undefined; onTouchMoveCapture?: React.TouchEventHandler | undefined; onTouchStart?: React.TouchEventHandler | undefined; onTouchStartCapture?: React.TouchEventHandler | undefined; onPointerDown?: React.PointerEventHandler | undefined; onPointerDownCapture?: React.PointerEventHandler | undefined; onPointerMove?: React.PointerEventHandler | undefined; onPointerMoveCapture?: React.PointerEventHandler | undefined; onPointerUp?: React.PointerEventHandler | undefined; onPointerUpCapture?: React.PointerEventHandler | undefined; onPointerCancel?: React.PointerEventHandler | undefined; onPointerCancelCapture?: React.PointerEventHandler | undefined; onPointerEnter?: React.PointerEventHandler | undefined; onPointerEnterCapture?: React.PointerEventHandler | undefined; onPointerLeave?: React.PointerEventHandler | undefined; onPointerLeaveCapture?: React.PointerEventHandler | undefined; onPointerOver?: React.PointerEventHandler | undefined; onPointerOverCapture?: React.PointerEventHandler | undefined; onPointerOut?: React.PointerEventHandler | undefined; onPointerOutCapture?: React.PointerEventHandler | undefined; onGotPointerCapture?: React.PointerEventHandler | undefined; onGotPointerCaptureCapture?: React.PointerEventHandler | undefined; onLostPointerCapture?: React.PointerEventHandler | undefined; onLostPointerCaptureCapture?: React.PointerEventHandler | undefined; onScroll?: React.UIEventHandler | undefined; onScrollCapture?: React.UIEventHandler | undefined; onWheel?: React.WheelEventHandler | undefined; onWheelCapture?: React.WheelEventHandler | undefined; onAnimationStart?: React.AnimationEventHandler | undefined; onAnimationStartCapture?: React.AnimationEventHandler | undefined; onAnimationEnd?: React.AnimationEventHandler | undefined; onAnimationEndCapture?: React.AnimationEventHandler | undefined; onAnimationIteration?: React.AnimationEventHandler | undefined; onAnimationIterationCapture?: React.AnimationEventHandler | undefined; onTransitionEnd?: React.TransitionEventHandler | undefined; onTransitionEndCapture?: React.TransitionEventHandler | undefined; 'data-test-subj'?: string | undefined; css?: ", "Interpolation", "<", "Theme", @@ -326,7 +326,7 @@ "section": "def-public.SavedObjectsManagementRecord", "text": "SavedObjectsManagementRecord" }, - ">) => React.ReactNode) | undefined; colSpan?: number | undefined; rowSpan?: number | undefined; scope?: string | undefined; valign?: \"top\" | \"bottom\" | \"middle\" | \"baseline\" | undefined; dataType?: ", + ">) => React.ReactNode) | undefined; colSpan?: number | undefined; rowSpan?: number | undefined; valign?: \"top\" | \"bottom\" | \"middle\" | \"baseline\" | undefined; dataType?: ", "EuiTableDataType", " | undefined; isExpander?: boolean | undefined; textOnly?: boolean | undefined; truncateText?: boolean | undefined; mobileOptions?: (Omit<", "EuiTableRowCellMobileOptionsShape", diff --git a/api_docs/saved_objects_management.mdx b/api_docs/saved_objects_management.mdx index 5bf08fa5fed71..0c4f162dfc831 100644 --- a/api_docs/saved_objects_management.mdx +++ b/api_docs/saved_objects_management.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/savedObjectsManagement title: "savedObjectsManagement" image: https://source.unsplash.com/400x175/?github description: API docs for the savedObjectsManagement plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'savedObjectsManagement'] --- import savedObjectsManagementObj from './saved_objects_management.devdocs.json'; diff --git a/api_docs/saved_objects_tagging.mdx b/api_docs/saved_objects_tagging.mdx index 0b20d4eaf1012..724c78dd97095 100644 --- a/api_docs/saved_objects_tagging.mdx +++ b/api_docs/saved_objects_tagging.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/savedObjectsTagging title: "savedObjectsTagging" image: https://source.unsplash.com/400x175/?github description: API docs for the savedObjectsTagging plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'savedObjectsTagging'] --- import savedObjectsTaggingObj from './saved_objects_tagging.devdocs.json'; diff --git a/api_docs/saved_objects_tagging_oss.mdx b/api_docs/saved_objects_tagging_oss.mdx index 74c83b74d95e7..b2786772448d3 100644 --- a/api_docs/saved_objects_tagging_oss.mdx +++ b/api_docs/saved_objects_tagging_oss.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/savedObjectsTaggingOss title: "savedObjectsTaggingOss" image: https://source.unsplash.com/400x175/?github description: API docs for the savedObjectsTaggingOss plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'savedObjectsTaggingOss'] --- import savedObjectsTaggingOssObj from './saved_objects_tagging_oss.devdocs.json'; diff --git a/api_docs/saved_search.devdocs.json b/api_docs/saved_search.devdocs.json index 8b7345d50d881..ba9542747b9ab 100644 --- a/api_docs/saved_search.devdocs.json +++ b/api_docs/saved_search.devdocs.json @@ -819,6 +819,20 @@ "path": "src/plugins/saved_search/public/services/saved_searches/types.ts", "deprecated": false, "trackAdoption": false + }, + { + "parentPluginId": "savedSearch", + "id": "def-public.SavedSearch.breakdownField", + "type": "string", + "tags": [], + "label": "breakdownField", + "description": [], + "signature": [ + "string | undefined" + ], + "path": "src/plugins/saved_search/public/services/saved_searches/types.ts", + "deprecated": false, + "trackAdoption": false } ], "initialIsOpen": false diff --git a/api_docs/saved_search.mdx b/api_docs/saved_search.mdx index f818263e78ebd..f512252f05e82 100644 --- a/api_docs/saved_search.mdx +++ b/api_docs/saved_search.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/savedSearch title: "savedSearch" image: https://source.unsplash.com/400x175/?github description: API docs for the savedSearch plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'savedSearch'] --- import savedSearchObj from './saved_search.devdocs.json'; @@ -21,7 +21,7 @@ Contact [Data Discovery](https://github.com/orgs/elastic/teams/kibana-data-disco | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 44 | 0 | 44 | 1 | +| 45 | 0 | 45 | 1 | ## Client diff --git a/api_docs/screenshot_mode.mdx b/api_docs/screenshot_mode.mdx index 431f3771981e0..fc38600f82a41 100644 --- a/api_docs/screenshot_mode.mdx +++ b/api_docs/screenshot_mode.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/screenshotMode title: "screenshotMode" image: https://source.unsplash.com/400x175/?github description: API docs for the screenshotMode plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'screenshotMode'] --- import screenshotModeObj from './screenshot_mode.devdocs.json'; diff --git a/api_docs/screenshotting.mdx b/api_docs/screenshotting.mdx index 21865c350fd9c..b8fc625e69eeb 100644 --- a/api_docs/screenshotting.mdx +++ b/api_docs/screenshotting.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/screenshotting title: "screenshotting" image: https://source.unsplash.com/400x175/?github description: API docs for the screenshotting plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'screenshotting'] --- import screenshottingObj from './screenshotting.devdocs.json'; diff --git a/api_docs/security.mdx b/api_docs/security.mdx index 4692dde0222b8..0f5e59b7548b4 100644 --- a/api_docs/security.mdx +++ b/api_docs/security.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/security title: "security" image: https://source.unsplash.com/400x175/?github description: API docs for the security plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'security'] --- import securityObj from './security.devdocs.json'; diff --git a/api_docs/security_solution.devdocs.json b/api_docs/security_solution.devdocs.json index 519589842552b..8138c82cd6b77 100644 --- a/api_docs/security_solution.devdocs.json +++ b/api_docs/security_solution.devdocs.json @@ -64,7 +64,7 @@ "label": "experimentalFeatures", "description": [], "signature": [ - "{ readonly tGridEnabled: boolean; readonly tGridEventRenderedViewEnabled: boolean; readonly excludePoliciesInFilterEnabled: boolean; readonly kubernetesEnabled: boolean; readonly disableIsolationUIPendingStatuses: boolean; readonly pendingActionResponsesWithAck: boolean; readonly policyListEnabled: boolean; readonly policyResponseInFleetEnabled: boolean; readonly chartEmbeddablesEnabled: boolean; readonly previewTelemetryUrlEnabled: boolean; readonly responseActionsConsoleEnabled: boolean; readonly insightsRelatedAlertsByProcessAncestry: boolean; readonly extendedRuleExecutionLoggingEnabled: boolean; readonly socTrendsEnabled: boolean; readonly responseActionsEnabled: boolean; readonly endpointRbacEnabled: boolean; readonly endpointRbacV1Enabled: boolean; readonly alertDetailsPageEnabled: boolean; readonly responseActionGetFileEnabled: boolean; }" + "{ readonly tGridEnabled: boolean; readonly tGridEventRenderedViewEnabled: boolean; readonly excludePoliciesInFilterEnabled: boolean; readonly kubernetesEnabled: boolean; readonly disableIsolationUIPendingStatuses: boolean; readonly pendingActionResponsesWithAck: boolean; readonly policyListEnabled: boolean; readonly policyResponseInFleetEnabled: boolean; readonly chartEmbeddablesEnabled: boolean; readonly previewTelemetryUrlEnabled: boolean; readonly responseActionsConsoleEnabled: boolean; readonly insightsRelatedAlertsByProcessAncestry: boolean; readonly extendedRuleExecutionLoggingEnabled: boolean; readonly socTrendsEnabled: boolean; readonly responseActionsEnabled: boolean; readonly endpointRbacEnabled: boolean; readonly endpointRbacV1Enabled: boolean; readonly alertDetailsPageEnabled: boolean; readonly responseActionGetFileEnabled: boolean; readonly riskyHostsEnabled: boolean; readonly riskyUsersEnabled: boolean; }" ], "path": "x-pack/plugins/security_solution/public/plugin.tsx", "deprecated": false, @@ -859,13 +859,7 @@ "The columns displayed in the data table" ], "signature": [ - { - "pluginId": "timelines", - "scope": "common", - "docId": "kibTimelinesPluginApi", - "section": "def-common.ColumnHeaderOptions", - "text": "ColumnHeaderOptions" - }, + "ColumnHeaderOptions", "[]" ], "path": "x-pack/plugins/security_solution/public/timelines/store/timeline/model.ts", @@ -880,13 +874,7 @@ "label": "defaultColumns", "description": [], "signature": [ - { - "pluginId": "timelines", - "scope": "common", - "docId": "kibTimelinesPluginApi", - "section": "def-common.ColumnHeaderOptions", - "text": "ColumnHeaderOptions" - }, + "ColumnHeaderOptions", "[]" ], "path": "x-pack/plugins/security_solution/public/timelines/store/timeline/model.ts", @@ -1048,17 +1036,17 @@ ], "signature": [ "{ query?: ", - "TimelineExpandedDetailType", + "ExpandedDetailType", " | undefined; graph?: ", - "TimelineExpandedDetailType", + "ExpandedDetailType", " | undefined; notes?: ", - "TimelineExpandedDetailType", + "ExpandedDetailType", " | undefined; pinned?: ", - "TimelineExpandedDetailType", + "ExpandedDetailType", " | undefined; eql?: ", - "TimelineExpandedDetailType", + "ExpandedDetailType", " | undefined; session?: ", - "TimelineExpandedDetailType", + "ExpandedDetailType", " | undefined; }" ], "path": "x-pack/plugins/security_solution/public/timelines/store/timeline/model.ts", @@ -2008,7 +1996,7 @@ "label": "ConfigType", "description": [], "signature": [ - "Readonly<{} & { signalsIndex: string; maxRuleImportExportSize: number; maxRuleImportPayloadBytes: number; maxTimelineImportExportSize: number; maxTimelineImportPayloadBytes: number; alertMergeStrategy: \"allFields\" | \"missingFields\" | \"noFields\"; alertIgnoreFields: string[]; enableExperimental: string[]; packagerTaskInterval: string; prebuiltRulesFromFileSystem: boolean; prebuiltRulesFromSavedObjects: boolean; }> & { experimentalFeatures: Readonly<{ tGridEnabled: boolean; tGridEventRenderedViewEnabled: boolean; excludePoliciesInFilterEnabled: boolean; kubernetesEnabled: boolean; disableIsolationUIPendingStatuses: boolean; pendingActionResponsesWithAck: boolean; policyListEnabled: boolean; policyResponseInFleetEnabled: boolean; chartEmbeddablesEnabled: boolean; previewTelemetryUrlEnabled: boolean; responseActionsConsoleEnabled: boolean; insightsRelatedAlertsByProcessAncestry: boolean; extendedRuleExecutionLoggingEnabled: boolean; socTrendsEnabled: boolean; responseActionsEnabled: boolean; endpointRbacEnabled: boolean; endpointRbacV1Enabled: boolean; alertDetailsPageEnabled: boolean; responseActionGetFileEnabled: boolean; }>; }" + "Readonly<{} & { signalsIndex: string; maxRuleImportExportSize: number; maxRuleImportPayloadBytes: number; maxTimelineImportExportSize: number; maxTimelineImportPayloadBytes: number; alertMergeStrategy: \"allFields\" | \"missingFields\" | \"noFields\"; alertIgnoreFields: string[]; enableExperimental: string[]; packagerTaskInterval: string; }> & { experimentalFeatures: Readonly<{ tGridEnabled: boolean; tGridEventRenderedViewEnabled: boolean; excludePoliciesInFilterEnabled: boolean; kubernetesEnabled: boolean; disableIsolationUIPendingStatuses: boolean; pendingActionResponsesWithAck: boolean; policyListEnabled: boolean; policyResponseInFleetEnabled: boolean; chartEmbeddablesEnabled: boolean; previewTelemetryUrlEnabled: boolean; responseActionsConsoleEnabled: boolean; insightsRelatedAlertsByProcessAncestry: boolean; extendedRuleExecutionLoggingEnabled: boolean; socTrendsEnabled: boolean; responseActionsEnabled: boolean; endpointRbacEnabled: boolean; endpointRbacV1Enabled: boolean; alertDetailsPageEnabled: boolean; responseActionGetFileEnabled: boolean; riskyHostsEnabled: boolean; riskyUsersEnabled: boolean; }>; }" ], "path": "x-pack/plugins/security_solution/server/config.ts", "deprecated": false, diff --git a/api_docs/security_solution.mdx b/api_docs/security_solution.mdx index 4ac560aadaa66..0bbe550101489 100644 --- a/api_docs/security_solution.mdx +++ b/api_docs/security_solution.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/securitySolution title: "securitySolution" image: https://source.unsplash.com/400x175/?github description: API docs for the securitySolution plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'securitySolution'] --- import securitySolutionObj from './security_solution.devdocs.json'; @@ -21,7 +21,7 @@ Contact [Security solution](https://github.com/orgs/elastic/teams/security-solut | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 112 | 0 | 75 | 27 | +| 112 | 0 | 75 | 28 | ## Client diff --git a/api_docs/session_view.mdx b/api_docs/session_view.mdx index 86ee61e842930..dcebd55498fe5 100644 --- a/api_docs/session_view.mdx +++ b/api_docs/session_view.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/sessionView title: "sessionView" image: https://source.unsplash.com/400x175/?github description: API docs for the sessionView plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'sessionView'] --- import sessionViewObj from './session_view.devdocs.json'; diff --git a/api_docs/share.mdx b/api_docs/share.mdx index 9ffaad06aebc1..f7dd29463c106 100644 --- a/api_docs/share.mdx +++ b/api_docs/share.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/share title: "share" image: https://source.unsplash.com/400x175/?github description: API docs for the share plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'share'] --- import shareObj from './share.devdocs.json'; diff --git a/api_docs/snapshot_restore.mdx b/api_docs/snapshot_restore.mdx index d13b03480adcb..4ab4583e0a572 100644 --- a/api_docs/snapshot_restore.mdx +++ b/api_docs/snapshot_restore.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/snapshotRestore title: "snapshotRestore" image: https://source.unsplash.com/400x175/?github description: API docs for the snapshotRestore plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'snapshotRestore'] --- import snapshotRestoreObj from './snapshot_restore.devdocs.json'; diff --git a/api_docs/spaces.mdx b/api_docs/spaces.mdx index 09474d4d4a311..d3aec2df50cc4 100644 --- a/api_docs/spaces.mdx +++ b/api_docs/spaces.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/spaces title: "spaces" image: https://source.unsplash.com/400x175/?github description: API docs for the spaces plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'spaces'] --- import spacesObj from './spaces.devdocs.json'; diff --git a/api_docs/stack_alerts.mdx b/api_docs/stack_alerts.mdx index d63ca0c20b1f2..386d4d3912a2b 100644 --- a/api_docs/stack_alerts.mdx +++ b/api_docs/stack_alerts.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/stackAlerts title: "stackAlerts" image: https://source.unsplash.com/400x175/?github description: API docs for the stackAlerts plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'stackAlerts'] --- import stackAlertsObj from './stack_alerts.devdocs.json'; diff --git a/api_docs/stack_connectors.mdx b/api_docs/stack_connectors.mdx index 8b40d5e13121a..2f1889b32389a 100644 --- a/api_docs/stack_connectors.mdx +++ b/api_docs/stack_connectors.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/stackConnectors title: "stackConnectors" image: https://source.unsplash.com/400x175/?github description: API docs for the stackConnectors plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'stackConnectors'] --- import stackConnectorsObj from './stack_connectors.devdocs.json'; diff --git a/api_docs/task_manager.devdocs.json b/api_docs/task_manager.devdocs.json index d9497789ba816..451919e575a12 100644 --- a/api_docs/task_manager.devdocs.json +++ b/api_docs/task_manager.devdocs.json @@ -1232,7 +1232,7 @@ "\nA task instance that has an id and is ready for storage." ], "signature": [ - "{ params: Record; enabled?: boolean | undefined; state: Record; scope?: string[] | undefined; taskType: string; }" + "{ scope?: string[] | undefined; params: Record; enabled?: boolean | undefined; state: Record; taskType: string; }" ], "path": "x-pack/plugins/task_manager/server/task.ts", "deprecated": false, diff --git a/api_docs/task_manager.mdx b/api_docs/task_manager.mdx index 90468be2b3525..3881bcfa94e90 100644 --- a/api_docs/task_manager.mdx +++ b/api_docs/task_manager.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/taskManager title: "taskManager" image: https://source.unsplash.com/400x175/?github description: API docs for the taskManager plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'taskManager'] --- import taskManagerObj from './task_manager.devdocs.json'; diff --git a/api_docs/telemetry.mdx b/api_docs/telemetry.mdx index a1ad361f7b96d..714005b1e43cb 100644 --- a/api_docs/telemetry.mdx +++ b/api_docs/telemetry.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/telemetry title: "telemetry" image: https://source.unsplash.com/400x175/?github description: API docs for the telemetry plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'telemetry'] --- import telemetryObj from './telemetry.devdocs.json'; diff --git a/api_docs/telemetry_collection_manager.mdx b/api_docs/telemetry_collection_manager.mdx index 33c520f84e675..b7a10129ae9a9 100644 --- a/api_docs/telemetry_collection_manager.mdx +++ b/api_docs/telemetry_collection_manager.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/telemetryCollectionManager title: "telemetryCollectionManager" image: https://source.unsplash.com/400x175/?github description: API docs for the telemetryCollectionManager plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'telemetryCollectionManager'] --- import telemetryCollectionManagerObj from './telemetry_collection_manager.devdocs.json'; diff --git a/api_docs/telemetry_collection_xpack.mdx b/api_docs/telemetry_collection_xpack.mdx index 2f1c287225a97..b4b14e5553508 100644 --- a/api_docs/telemetry_collection_xpack.mdx +++ b/api_docs/telemetry_collection_xpack.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/telemetryCollectionXpack title: "telemetryCollectionXpack" image: https://source.unsplash.com/400x175/?github description: API docs for the telemetryCollectionXpack plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'telemetryCollectionXpack'] --- import telemetryCollectionXpackObj from './telemetry_collection_xpack.devdocs.json'; diff --git a/api_docs/telemetry_management_section.mdx b/api_docs/telemetry_management_section.mdx index cd7bf9c6effe6..d2b183ceddbc3 100644 --- a/api_docs/telemetry_management_section.mdx +++ b/api_docs/telemetry_management_section.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/telemetryManagementSection title: "telemetryManagementSection" image: https://source.unsplash.com/400x175/?github description: API docs for the telemetryManagementSection plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'telemetryManagementSection'] --- import telemetryManagementSectionObj from './telemetry_management_section.devdocs.json'; diff --git a/api_docs/threat_intelligence.devdocs.json b/api_docs/threat_intelligence.devdocs.json index 537c8c1268807..bf7f44448dd5f 100644 --- a/api_docs/threat_intelligence.devdocs.json +++ b/api_docs/threat_intelligence.devdocs.json @@ -222,10 +222,10 @@ }, { "parentPluginId": "threatIntelligence", - "id": "def-public.SecuritySolutionPluginContext.getSecuritySolutionStore", + "id": "def-public.SecuritySolutionPluginContext.securitySolutionStore", "type": "Object", "tags": [], - "label": "getSecuritySolutionStore", + "label": "securitySolutionStore", "description": [ "\nSecurity Solution store" ], @@ -389,6 +389,119 @@ "trackAdoption": false } ] + }, + { + "parentPluginId": "threatIntelligence", + "id": "def-public.SecuritySolutionPluginContext.registerQuery", + "type": "Function", + "tags": [], + "label": "registerQuery", + "description": [ + "\nRegister query in security solution store for tracking and centralized refresh support" + ], + "signature": [ + "(query: { id: string; loading: boolean; refetch: VoidFunction; }) => void" + ], + "path": "x-pack/plugins/threat_intelligence/public/types.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "threatIntelligence", + "id": "def-public.SecuritySolutionPluginContext.registerQuery.$1", + "type": "Object", + "tags": [], + "label": "query", + "description": [], + "path": "x-pack/plugins/threat_intelligence/public/types.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "threatIntelligence", + "id": "def-public.SecuritySolutionPluginContext.registerQuery.$1.id", + "type": "string", + "tags": [], + "label": "id", + "description": [], + "path": "x-pack/plugins/threat_intelligence/public/types.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "threatIntelligence", + "id": "def-public.SecuritySolutionPluginContext.registerQuery.$1.loading", + "type": "boolean", + "tags": [], + "label": "loading", + "description": [], + "path": "x-pack/plugins/threat_intelligence/public/types.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "threatIntelligence", + "id": "def-public.SecuritySolutionPluginContext.registerQuery.$1.refetch", + "type": "Function", + "tags": [], + "label": "refetch", + "description": [], + "signature": [ + "VoidFunction" + ], + "path": "x-pack/plugins/threat_intelligence/public/types.ts", + "deprecated": false, + "trackAdoption": false, + "returnComment": [], + "children": [] + } + ] + } + ], + "returnComment": [] + }, + { + "parentPluginId": "threatIntelligence", + "id": "def-public.SecuritySolutionPluginContext.deregisterQuery", + "type": "Function", + "tags": [], + "label": "deregisterQuery", + "description": [ + "\nDeregister stale query" + ], + "signature": [ + "(query: { id: string; }) => void" + ], + "path": "x-pack/plugins/threat_intelligence/public/types.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "threatIntelligence", + "id": "def-public.SecuritySolutionPluginContext.deregisterQuery.$1", + "type": "Object", + "tags": [], + "label": "query", + "description": [], + "path": "x-pack/plugins/threat_intelligence/public/types.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "threatIntelligence", + "id": "def-public.SecuritySolutionPluginContext.deregisterQuery.$1.id", + "type": "string", + "tags": [], + "label": "id", + "description": [], + "path": "x-pack/plugins/threat_intelligence/public/types.ts", + "deprecated": false, + "trackAdoption": false + } + ] + } + ], + "returnComment": [] } ], "initialIsOpen": false diff --git a/api_docs/threat_intelligence.mdx b/api_docs/threat_intelligence.mdx index 3880d20419f7a..b9f51795ca55c 100644 --- a/api_docs/threat_intelligence.mdx +++ b/api_docs/threat_intelligence.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/threatIntelligence title: "threatIntelligence" image: https://source.unsplash.com/400x175/?github description: API docs for the threatIntelligence plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'threatIntelligence'] --- import threatIntelligenceObj from './threat_intelligence.devdocs.json'; @@ -21,7 +21,7 @@ Contact [Protections Experience Team](https://github.com/orgs/elastic/teams/prot | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 26 | 0 | 8 | 3 | +| 34 | 0 | 14 | 3 | ## Client diff --git a/api_docs/timelines.devdocs.json b/api_docs/timelines.devdocs.json index 8b99cb421526f..affca9f590a94 100644 --- a/api_docs/timelines.devdocs.json +++ b/api_docs/timelines.devdocs.json @@ -5,29 +5,31 @@ "functions": [ { "parentPluginId": "timelines", - "id": "def-public.addFieldToTimelineColumns", + "id": "def-public.arrayIndexToAriaIndex", "type": "Function", "tags": [], - "label": "addFieldToTimelineColumns", - "description": [], + "label": "arrayIndexToAriaIndex", + "description": [ + "Converts an array index, which starts at zero, to an aria index, which starts at one" + ], "signature": [ - "({ browserFields, dispatch, result, timelineId, defaultsHeader, }: AddFieldToTimelineColumnsParams) => void" + "(arrayIndex: number) => number" ], - "path": "x-pack/plugins/timelines/public/components/drag_and_drop/helpers.ts", + "path": "x-pack/plugins/timelines/common/utils/accessibility/helpers.ts", "deprecated": false, "trackAdoption": false, "children": [ { "parentPluginId": "timelines", - "id": "def-public.addFieldToTimelineColumns.$1", - "type": "Object", + "id": "def-public.arrayIndexToAriaIndex.$1", + "type": "number", "tags": [], - "label": "{\n browserFields,\n dispatch,\n result,\n timelineId,\n defaultsHeader,\n}", + "label": "arrayIndex", "description": [], "signature": [ - "AddFieldToTimelineColumnsParams" + "number" ], - "path": "x-pack/plugins/timelines/public/components/drag_and_drop/helpers.ts", + "path": "x-pack/plugins/timelines/common/utils/accessibility/helpers.ts", "deprecated": false, "trackAdoption": false, "isRequired": true @@ -38,63 +40,50 @@ }, { "parentPluginId": "timelines", - "id": "def-public.applyDeltaToColumnWidth", + "id": "def-public.elementOrChildrenHasFocus", "type": "Function", "tags": [], - "label": "applyDeltaToColumnWidth", - "description": [], + "label": "elementOrChildrenHasFocus", + "description": [ + "Returns `true` when the element, or one of it's children has focus" + ], "signature": [ - "ActionCreator", - "<{ id: string; columnId: string; delta: number; }>" + "(element: HTMLElement | null | undefined) => boolean" ], - "path": "x-pack/plugins/timelines/public/store/t_grid/actions.ts", + "path": "x-pack/plugins/timelines/common/utils/accessibility/helpers.ts", "deprecated": false, "trackAdoption": false, - "returnComment": [], "children": [ { "parentPluginId": "timelines", - "id": "def-public.applyDeltaToColumnWidth.$1", - "type": "Uncategorized", - "tags": [], - "label": "payload", - "description": [], - "signature": [ - "Payload" - ], - "path": "node_modules/typescript-fsa/lib/index.d.ts", - "deprecated": false, - "trackAdoption": false - }, - { - "parentPluginId": "timelines", - "id": "def-public.applyDeltaToColumnWidth.$2", + "id": "def-public.elementOrChildrenHasFocus.$1", "type": "CompoundType", "tags": [], - "label": "meta", + "label": "element", "description": [], "signature": [ - "Meta", - " | undefined" + "HTMLElement | null | undefined" ], - "path": "node_modules/typescript-fsa/lib/index.d.ts", + "path": "x-pack/plugins/timelines/common/utils/accessibility/helpers.ts", "deprecated": false, - "trackAdoption": false + "trackAdoption": false, + "isRequired": false } ], + "returnComment": [], "initialIsOpen": false }, { "parentPluginId": "timelines", - "id": "def-public.arrayIndexToAriaIndex", + "id": "def-public.focusColumn", "type": "Function", "tags": [], - "label": "arrayIndexToAriaIndex", + "label": "focusColumn", "description": [ - "Converts an array index, which starts at zero, to an aria index, which starts at one" + "\nSIDE EFFECT: mutates the DOM by focusing the specified column\nreturns the `aria-colindex` of the newly-focused column" ], "signature": [ - "(arrayIndex: number) => number" + "({ colindexAttribute, containerElement, ariaColindex, ariaRowindex, rowindexAttribute, }: { colindexAttribute: string; containerElement: Element | null; ariaColindex: number; ariaRowindex: number; rowindexAttribute: string; }) => FocusColumnResult" ], "path": "x-pack/plugins/timelines/common/utils/accessibility/helpers.ts", "deprecated": false, @@ -102,18 +91,74 @@ "children": [ { "parentPluginId": "timelines", - "id": "def-public.arrayIndexToAriaIndex.$1", - "type": "number", + "id": "def-public.focusColumn.$1", + "type": "Object", "tags": [], - "label": "arrayIndex", + "label": "{\n colindexAttribute,\n containerElement,\n ariaColindex,\n ariaRowindex,\n rowindexAttribute,\n}", "description": [], - "signature": [ - "number" - ], "path": "x-pack/plugins/timelines/common/utils/accessibility/helpers.ts", "deprecated": false, "trackAdoption": false, - "isRequired": true + "children": [ + { + "parentPluginId": "timelines", + "id": "def-public.focusColumn.$1.colindexAttribute", + "type": "string", + "tags": [], + "label": "colindexAttribute", + "description": [], + "path": "x-pack/plugins/timelines/common/utils/accessibility/helpers.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "timelines", + "id": "def-public.focusColumn.$1.containerElement", + "type": "CompoundType", + "tags": [], + "label": "containerElement", + "description": [], + "signature": [ + "Element | null" + ], + "path": "x-pack/plugins/timelines/common/utils/accessibility/helpers.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "timelines", + "id": "def-public.focusColumn.$1.ariaColindex", + "type": "number", + "tags": [], + "label": "ariaColindex", + "description": [], + "path": "x-pack/plugins/timelines/common/utils/accessibility/helpers.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "timelines", + "id": "def-public.focusColumn.$1.ariaRowindex", + "type": "number", + "tags": [], + "label": "ariaRowindex", + "description": [], + "path": "x-pack/plugins/timelines/common/utils/accessibility/helpers.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "timelines", + "id": "def-public.focusColumn.$1.rowindexAttribute", + "type": "string", + "tags": [], + "label": "rowindexAttribute", + "description": [], + "path": "x-pack/plugins/timelines/common/utils/accessibility/helpers.ts", + "deprecated": false, + "trackAdoption": false + } + ] } ], "returnComment": [], @@ -121,236 +166,178 @@ }, { "parentPluginId": "timelines", - "id": "def-public.clearEventsDeleted", + "id": "def-public.getFocusedAriaColindexCell", "type": "Function", "tags": [], - "label": "clearEventsDeleted", - "description": [], + "label": "getFocusedAriaColindexCell", + "description": [ + "\nReturns the focused cell for tables that use `aria-colindex`" + ], "signature": [ - "ActionCreator", - "<{ id: string; }>" + "({ containerElement, tableClassName, }: { containerElement: HTMLElement | null; tableClassName: string; }) => HTMLDivElement | null" ], - "path": "x-pack/plugins/timelines/public/store/t_grid/actions.ts", + "path": "x-pack/plugins/timelines/common/utils/accessibility/helpers.ts", "deprecated": false, "trackAdoption": false, - "returnComment": [], "children": [ { "parentPluginId": "timelines", - "id": "def-public.clearEventsDeleted.$1", - "type": "Uncategorized", + "id": "def-public.getFocusedAriaColindexCell.$1", + "type": "Object", "tags": [], - "label": "payload", + "label": "{\n containerElement,\n tableClassName,\n}", "description": [], - "signature": [ - "Payload" - ], - "path": "node_modules/typescript-fsa/lib/index.d.ts", + "path": "x-pack/plugins/timelines/common/utils/accessibility/helpers.ts", "deprecated": false, - "trackAdoption": false - }, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "timelines", + "id": "def-public.getFocusedAriaColindexCell.$1.containerElement", + "type": "CompoundType", + "tags": [], + "label": "containerElement", + "description": [], + "signature": [ + "HTMLElement | null" + ], + "path": "x-pack/plugins/timelines/common/utils/accessibility/helpers.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "timelines", + "id": "def-public.getFocusedAriaColindexCell.$1.tableClassName", + "type": "string", + "tags": [], + "label": "tableClassName", + "description": [], + "path": "x-pack/plugins/timelines/common/utils/accessibility/helpers.ts", + "deprecated": false, + "trackAdoption": false + } + ] + } + ], + "returnComment": [], + "initialIsOpen": false + }, + { + "parentPluginId": "timelines", + "id": "def-public.getFocusedDataColindexCell", + "type": "Function", + "tags": [], + "label": "getFocusedDataColindexCell", + "description": [ + "\nReturns the focused cell for tables that use `data-colindex`" + ], + "signature": [ + "({ containerElement, tableClassName, }: { containerElement: HTMLElement | null; tableClassName: string; }) => HTMLDivElement | null" + ], + "path": "x-pack/plugins/timelines/common/utils/accessibility/helpers.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ { "parentPluginId": "timelines", - "id": "def-public.clearEventsDeleted.$2", - "type": "CompoundType", + "id": "def-public.getFocusedDataColindexCell.$1", + "type": "Object", "tags": [], - "label": "meta", + "label": "{\n containerElement,\n tableClassName,\n}", "description": [], - "signature": [ - "Meta", - " | undefined" - ], - "path": "node_modules/typescript-fsa/lib/index.d.ts", + "path": "x-pack/plugins/timelines/common/utils/accessibility/helpers.ts", "deprecated": false, - "trackAdoption": false + "trackAdoption": false, + "children": [ + { + "parentPluginId": "timelines", + "id": "def-public.getFocusedDataColindexCell.$1.containerElement", + "type": "CompoundType", + "tags": [], + "label": "containerElement", + "description": [], + "signature": [ + "HTMLElement | null" + ], + "path": "x-pack/plugins/timelines/common/utils/accessibility/helpers.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "timelines", + "id": "def-public.getFocusedDataColindexCell.$1.tableClassName", + "type": "string", + "tags": [], + "label": "tableClassName", + "description": [], + "path": "x-pack/plugins/timelines/common/utils/accessibility/helpers.ts", + "deprecated": false, + "trackAdoption": false + } + ] } ], + "returnComment": [], "initialIsOpen": false }, { "parentPluginId": "timelines", - "id": "def-public.clearEventsLoading", + "id": "def-public.getNotesContainerClassName", "type": "Function", "tags": [], - "label": "clearEventsLoading", + "label": "getNotesContainerClassName", "description": [], "signature": [ - "ActionCreator", - "<{ id: string; }>" + "(ariaRowindex: number) => string" ], - "path": "x-pack/plugins/timelines/public/store/t_grid/actions.ts", + "path": "x-pack/plugins/timelines/common/utils/accessibility/helpers.ts", "deprecated": false, "trackAdoption": false, - "returnComment": [], "children": [ { "parentPluginId": "timelines", - "id": "def-public.clearEventsLoading.$1", - "type": "Uncategorized", - "tags": [], - "label": "payload", - "description": [], - "signature": [ - "Payload" - ], - "path": "node_modules/typescript-fsa/lib/index.d.ts", - "deprecated": false, - "trackAdoption": false - }, - { - "parentPluginId": "timelines", - "id": "def-public.clearEventsLoading.$2", - "type": "CompoundType", + "id": "def-public.getNotesContainerClassName.$1", + "type": "number", "tags": [], - "label": "meta", + "label": "ariaRowindex", "description": [], "signature": [ - "Meta", - " | undefined" + "number" ], - "path": "node_modules/typescript-fsa/lib/index.d.ts", + "path": "x-pack/plugins/timelines/common/utils/accessibility/helpers.ts", "deprecated": false, - "trackAdoption": false + "trackAdoption": false, + "isRequired": true } ], + "returnComment": [], "initialIsOpen": false }, { "parentPluginId": "timelines", - "id": "def-public.clearSelected", + "id": "def-public.getRowRendererClassName", "type": "Function", "tags": [], - "label": "clearSelected", + "label": "getRowRendererClassName", "description": [], "signature": [ - "ActionCreator", - "<{ id: string; }>" + "(ariaRowindex: number) => string" ], - "path": "x-pack/plugins/timelines/public/store/t_grid/actions.ts", + "path": "x-pack/plugins/timelines/common/utils/accessibility/helpers.ts", "deprecated": false, "trackAdoption": false, - "returnComment": [], "children": [ { "parentPluginId": "timelines", - "id": "def-public.clearSelected.$1", - "type": "Uncategorized", - "tags": [], - "label": "payload", - "description": [], - "signature": [ - "Payload" - ], - "path": "node_modules/typescript-fsa/lib/index.d.ts", - "deprecated": false, - "trackAdoption": false - }, - { - "parentPluginId": "timelines", - "id": "def-public.clearSelected.$2", - "type": "CompoundType", - "tags": [], - "label": "meta", - "description": [], - "signature": [ - "Meta", - " | undefined" - ], - "path": "node_modules/typescript-fsa/lib/index.d.ts", - "deprecated": false, - "trackAdoption": false - } - ], - "initialIsOpen": false - }, - { - "parentPluginId": "timelines", - "id": "def-public.combineQueries", - "type": "Function", - "tags": [], - "label": "combineQueries", - "description": [], - "signature": [ - "({ config, dataProviders, indexPattern, browserFields, filters, kqlQuery, kqlMode, }: CombineQueries) => { filterQuery: string | undefined; kqlError: Error | undefined; } | null" - ], - "path": "x-pack/plugins/timelines/public/components/t_grid/helpers.tsx", - "deprecated": false, - "trackAdoption": false, - "children": [ - { - "parentPluginId": "timelines", - "id": "def-public.combineQueries.$1", - "type": "Object", - "tags": [], - "label": "{\n config,\n dataProviders,\n indexPattern,\n browserFields,\n filters = [],\n kqlQuery,\n kqlMode,\n}", - "description": [], - "signature": [ - "CombineQueries" - ], - "path": "x-pack/plugins/timelines/public/components/t_grid/helpers.tsx", - "deprecated": false, - "trackAdoption": false, - "isRequired": true - } - ], - "returnComment": [], - "initialIsOpen": false - }, - { - "parentPluginId": "timelines", - "id": "def-public.convertKueryToDslFilter", - "type": "Function", - "tags": [], - "label": "convertKueryToDslFilter", - "description": [], - "signature": [ - "(kueryExpression: string, indexPattern: ", - { - "pluginId": "@kbn/es-query", - "scope": "common", - "docId": "kibKbnEsQueryPluginApi", - "section": "def-common.DataViewBase", - "text": "DataViewBase" - }, - ") => ", - "QueryDslQueryContainer" - ], - "path": "x-pack/plugins/timelines/public/components/utils/keury/index.ts", - "deprecated": false, - "trackAdoption": false, - "children": [ - { - "parentPluginId": "timelines", - "id": "def-public.convertKueryToDslFilter.$1", - "type": "string", - "tags": [], - "label": "kueryExpression", - "description": [], - "signature": [ - "string" - ], - "path": "x-pack/plugins/timelines/public/components/utils/keury/index.ts", - "deprecated": false, - "trackAdoption": false, - "isRequired": true - }, - { - "parentPluginId": "timelines", - "id": "def-public.convertKueryToDslFilter.$2", - "type": "Object", + "id": "def-public.getRowRendererClassName.$1", + "type": "number", "tags": [], - "label": "indexPattern", + "label": "ariaRowindex", "description": [], "signature": [ - { - "pluginId": "@kbn/es-query", - "scope": "common", - "docId": "kibKbnEsQueryPluginApi", - "section": "def-common.DataViewBase", - "text": "DataViewBase" - } + "number" ], - "path": "x-pack/plugins/timelines/public/components/utils/keury/index.ts", + "path": "x-pack/plugins/timelines/common/utils/accessibility/helpers.ts", "deprecated": false, "trackAdoption": false, "isRequired": true @@ -361,221 +348,130 @@ }, { "parentPluginId": "timelines", - "id": "def-public.convertKueryToElasticSearchQuery", + "id": "def-public.getTableSkipFocus", "type": "Function", "tags": [], - "label": "convertKueryToElasticSearchQuery", - "description": [], - "signature": [ - "(kueryExpression: string, indexPattern?: ", - { - "pluginId": "@kbn/es-query", - "scope": "common", - "docId": "kibKbnEsQueryPluginApi", - "section": "def-common.DataViewBase", - "text": "DataViewBase" - }, - " | undefined) => string" - ], - "path": "x-pack/plugins/timelines/public/components/utils/keury/index.ts", - "deprecated": false, - "trackAdoption": false, - "children": [ - { - "parentPluginId": "timelines", - "id": "def-public.convertKueryToElasticSearchQuery.$1", - "type": "string", - "tags": [], - "label": "kueryExpression", - "description": [], - "signature": [ - "string" - ], - "path": "x-pack/plugins/timelines/public/components/utils/keury/index.ts", - "deprecated": false, - "trackAdoption": false, - "isRequired": true - }, - { - "parentPluginId": "timelines", - "id": "def-public.convertKueryToElasticSearchQuery.$2", - "type": "Object", - "tags": [], - "label": "indexPattern", - "description": [], - "signature": [ - { - "pluginId": "@kbn/es-query", - "scope": "common", - "docId": "kibKbnEsQueryPluginApi", - "section": "def-common.DataViewBase", - "text": "DataViewBase" - }, - " | undefined" - ], - "path": "x-pack/plugins/timelines/public/components/utils/keury/index.ts", - "deprecated": false, - "trackAdoption": false, - "isRequired": false - } + "label": "getTableSkipFocus", + "description": [ + "\nThis function, which works with tables that use the `aria-colindex` or\n`data-colindex` attributes, examines the focus state of the table, and\nreturns a `SkipFocus` enumeration.\n\nThe `SkipFocus` return value indicates whether the caller should skip focus\nto \"before\" the table, \"after\" the table, or take no action, and let the\nbrowser's \"natural\" focus management manage focus." ], - "returnComment": [], - "initialIsOpen": false - }, - { - "parentPluginId": "timelines", - "id": "def-public.convertToBuildEsQuery", - "type": "Function", - "tags": [], - "label": "convertToBuildEsQuery", - "description": [], "signature": [ - "({ config, indexPattern, queries, filters, }: { config: ", - { - "pluginId": "@kbn/es-query", - "scope": "common", - "docId": "kibKbnEsQueryPluginApi", - "section": "def-common.EsQueryConfig", - "text": "EsQueryConfig" - }, - "; indexPattern: ", - { - "pluginId": "@kbn/es-query", - "scope": "common", - "docId": "kibKbnEsQueryPluginApi", - "section": "def-common.DataViewBase", - "text": "DataViewBase" - }, - " | undefined; queries: ", - { - "pluginId": "@kbn/es-query", - "scope": "common", - "docId": "kibKbnEsQueryPluginApi", - "section": "def-common.Query", - "text": "Query" - }, - "[]; filters: ", - { - "pluginId": "@kbn/es-query", - "scope": "common", - "docId": "kibKbnEsQueryPluginApi", - "section": "def-common.Filter", - "text": "Filter" - }, - "[]; }) => [string, undefined] | [undefined, Error]" + "({ containerElement, getFocusedCell, shiftKey, tableHasFocus, tableClassName, }: { containerElement: HTMLElement | null; getFocusedCell: ", + "GetFocusedCell", + "; shiftKey: boolean; tableHasFocus: (containerElement: HTMLElement | null) => boolean; tableClassName: string; }) => ", + "SkipFocus" ], - "path": "x-pack/plugins/timelines/public/components/utils/keury/index.ts", + "path": "x-pack/plugins/timelines/common/utils/accessibility/helpers.ts", "deprecated": false, "trackAdoption": false, "children": [ { "parentPluginId": "timelines", - "id": "def-public.convertToBuildEsQuery.$1", + "id": "def-public.getTableSkipFocus.$1", "type": "Object", "tags": [], - "label": "{\n config,\n indexPattern,\n queries,\n filters,\n}", + "label": "{\n containerElement,\n getFocusedCell,\n shiftKey,\n tableHasFocus,\n tableClassName,\n}", "description": [], - "path": "x-pack/plugins/timelines/public/components/utils/keury/index.ts", + "path": "x-pack/plugins/timelines/common/utils/accessibility/helpers.ts", "deprecated": false, "trackAdoption": false, "children": [ { "parentPluginId": "timelines", - "id": "def-public.convertToBuildEsQuery.$1.config", + "id": "def-public.getTableSkipFocus.$1.containerElement", "type": "CompoundType", "tags": [], - "label": "config", + "label": "containerElement", "description": [], "signature": [ - { - "pluginId": "@kbn/es-query", - "scope": "common", - "docId": "kibKbnEsQueryPluginApi", - "section": "def-common.KueryQueryOptions", - "text": "KueryQueryOptions" - }, - " & ", - { - "pluginId": "@kbn/es-query", - "scope": "common", - "docId": "kibKbnEsQueryPluginApi", - "section": "def-common.EsQueryFiltersConfig", - "text": "EsQueryFiltersConfig" - }, - " & { allowLeadingWildcards?: boolean | undefined; queryStringOptions?: ", - { - "pluginId": "@kbn/utility-types", - "scope": "server", - "docId": "kibKbnUtilityTypesPluginApi", - "section": "def-server.SerializableRecord", - "text": "SerializableRecord" - }, - " | undefined; }" + "HTMLElement | null" ], - "path": "x-pack/plugins/timelines/public/components/utils/keury/index.ts", + "path": "x-pack/plugins/timelines/common/utils/accessibility/helpers.ts", "deprecated": false, "trackAdoption": false }, { "parentPluginId": "timelines", - "id": "def-public.convertToBuildEsQuery.$1.indexPattern", - "type": "Object", + "id": "def-public.getTableSkipFocus.$1.getFocusedCell", + "type": "Function", "tags": [], - "label": "indexPattern", + "label": "getFocusedCell", "description": [], "signature": [ - { - "pluginId": "@kbn/es-query", - "scope": "common", - "docId": "kibKbnEsQueryPluginApi", - "section": "def-common.DataViewBase", - "text": "DataViewBase" - }, - " | undefined" + "({ containerElement, tableClassName, }: { containerElement: HTMLElement | null; tableClassName: string; }) => HTMLDivElement | null" ], - "path": "x-pack/plugins/timelines/public/components/utils/keury/index.ts", + "path": "x-pack/plugins/timelines/common/utils/accessibility/helpers.ts", "deprecated": false, - "trackAdoption": false + "trackAdoption": false, + "returnComment": [], + "children": [ + { + "parentPluginId": "timelines", + "id": "def-public.getTableSkipFocus.$1.getFocusedCell.$1", + "type": "Object", + "tags": [], + "label": "__0", + "description": [], + "signature": [ + "{ containerElement: HTMLElement | null; tableClassName: string; }" + ], + "path": "x-pack/plugins/timelines/common/utils/accessibility/helpers.ts", + "deprecated": false, + "trackAdoption": false + } + ] }, { "parentPluginId": "timelines", - "id": "def-public.convertToBuildEsQuery.$1.queries", - "type": "Array", + "id": "def-public.getTableSkipFocus.$1.shiftKey", + "type": "boolean", "tags": [], - "label": "queries", + "label": "shiftKey", "description": [], - "signature": [ - { - "pluginId": "@kbn/es-query", - "scope": "common", - "docId": "kibKbnEsQueryPluginApi", - "section": "def-common.Query", - "text": "Query" - }, - "[]" - ], - "path": "x-pack/plugins/timelines/public/components/utils/keury/index.ts", + "path": "x-pack/plugins/timelines/common/utils/accessibility/helpers.ts", "deprecated": false, "trackAdoption": false }, { "parentPluginId": "timelines", - "id": "def-public.convertToBuildEsQuery.$1.filters", - "type": "Array", + "id": "def-public.getTableSkipFocus.$1.tableHasFocus", + "type": "Function", "tags": [], - "label": "filters", + "label": "tableHasFocus", "description": [], "signature": [ + "(containerElement: HTMLElement | null) => boolean" + ], + "path": "x-pack/plugins/timelines/common/utils/accessibility/helpers.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ { - "pluginId": "@kbn/es-query", - "scope": "common", - "docId": "kibKbnEsQueryPluginApi", - "section": "def-common.Filter", - "text": "Filter" - }, - "[]" + "parentPluginId": "timelines", + "id": "def-public.getTableSkipFocus.$1.tableHasFocus.$1", + "type": "CompoundType", + "tags": [], + "label": "containerElement", + "description": [], + "signature": [ + "HTMLElement | null" + ], + "path": "x-pack/plugins/timelines/common/utils/accessibility/helpers.ts", + "deprecated": false, + "trackAdoption": false, + "isRequired": false + } ], - "path": "x-pack/plugins/timelines/public/components/utils/keury/index.ts", + "returnComment": [] + }, + { + "parentPluginId": "timelines", + "id": "def-public.getTableSkipFocus.$1.tableClassName", + "type": "string", + "tags": [], + "label": "tableClassName", + "description": [], + "path": "x-pack/plugins/timelines/common/utils/accessibility/helpers.ts", "deprecated": false, "trackAdoption": false } @@ -587,65 +483,131 @@ }, { "parentPluginId": "timelines", - "id": "def-public.createTGrid", + "id": "def-public.handleSkipFocus", "type": "Function", "tags": [], - "label": "createTGrid", - "description": [], + "label": "handleSkipFocus", + "description": [ + "\nIf the value of `skipFocus` is `SKIP_FOCUS_BACKWARDS` or `SKIP_FOCUS_FORWARD`\nthis function will invoke the provided `onSkipFocusBackwards` or\n`onSkipFocusForward` functions respectively.\n\nIf `skipFocus` is `SKIP_FOCUS_NOOP`, the `onSkipFocusBackwards` and\n`onSkipFocusForward` functions will not be invoked." + ], "signature": [ - "ActionCreator", - "<", - "TGridPersistInput", - ">" + "({ onSkipFocusBackwards, onSkipFocusForward, skipFocus, }: { onSkipFocusBackwards: () => void; onSkipFocusForward: () => void; skipFocus: ", + "SkipFocus", + "; }) => void" ], - "path": "x-pack/plugins/timelines/public/store/t_grid/actions.ts", + "path": "x-pack/plugins/timelines/common/utils/accessibility/helpers.ts", "deprecated": false, "trackAdoption": false, - "returnComment": [], "children": [ { "parentPluginId": "timelines", - "id": "def-public.createTGrid.$1", - "type": "Uncategorized", + "id": "def-public.handleSkipFocus.$1", + "type": "Object", "tags": [], - "label": "payload", + "label": "{\n onSkipFocusBackwards,\n onSkipFocusForward,\n skipFocus,\n}", "description": [], - "signature": [ - "Payload" - ], - "path": "node_modules/typescript-fsa/lib/index.d.ts", + "path": "x-pack/plugins/timelines/common/utils/accessibility/helpers.ts", "deprecated": false, - "trackAdoption": false - }, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "timelines", + "id": "def-public.handleSkipFocus.$1.onSkipFocusBackwards", + "type": "Function", + "tags": [], + "label": "onSkipFocusBackwards", + "description": [], + "signature": [ + "() => void" + ], + "path": "x-pack/plugins/timelines/common/utils/accessibility/helpers.ts", + "deprecated": false, + "trackAdoption": false, + "children": [], + "returnComment": [] + }, + { + "parentPluginId": "timelines", + "id": "def-public.handleSkipFocus.$1.onSkipFocusForward", + "type": "Function", + "tags": [], + "label": "onSkipFocusForward", + "description": [], + "signature": [ + "() => void" + ], + "path": "x-pack/plugins/timelines/common/utils/accessibility/helpers.ts", + "deprecated": false, + "trackAdoption": false, + "children": [], + "returnComment": [] + }, + { + "parentPluginId": "timelines", + "id": "def-public.handleSkipFocus.$1.skipFocus", + "type": "CompoundType", + "tags": [], + "label": "skipFocus", + "description": [], + "signature": [ + "\"SKIP_FOCUS_BACKWARDS\" | \"SKIP_FOCUS_FORWARD\" | \"SKIP_FOCUS_NOOP\"" + ], + "path": "x-pack/plugins/timelines/common/utils/accessibility/helpers.ts", + "deprecated": false, + "trackAdoption": false + } + ] + } + ], + "returnComment": [], + "initialIsOpen": false + }, + { + "parentPluginId": "timelines", + "id": "def-public.isArrowDownOrArrowUp", + "type": "Function", + "tags": [], + "label": "isArrowDownOrArrowUp", + "description": [ + "Returns `true` if the down or up arrow was pressed" + ], + "signature": [ + "(event: React.KeyboardEvent) => boolean" + ], + "path": "x-pack/plugins/timelines/common/utils/accessibility/helpers.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ { "parentPluginId": "timelines", - "id": "def-public.createTGrid.$2", - "type": "CompoundType", + "id": "def-public.isArrowDownOrArrowUp.$1", + "type": "Object", "tags": [], - "label": "meta", + "label": "event", "description": [], "signature": [ - "Meta", - " | undefined" + "React.KeyboardEvent" ], - "path": "node_modules/typescript-fsa/lib/index.d.ts", + "path": "x-pack/plugins/timelines/common/utils/accessibility/helpers.ts", "deprecated": false, - "trackAdoption": false + "trackAdoption": false, + "isRequired": true } ], + "returnComment": [], "initialIsOpen": false }, { "parentPluginId": "timelines", - "id": "def-public.elementOrChildrenHasFocus", + "id": "def-public.isArrowUp", "type": "Function", "tags": [], - "label": "elementOrChildrenHasFocus", + "label": "isArrowUp", "description": [ - "Returns `true` when the element, or one of it's children has focus" + "Returns `true` if the up arrow key was pressed" ], "signature": [ - "(element: HTMLElement | null | undefined) => boolean" + "(event: React.KeyboardEvent) => boolean" ], "path": "x-pack/plugins/timelines/common/utils/accessibility/helpers.ts", "deprecated": false, @@ -653,18 +615,18 @@ "children": [ { "parentPluginId": "timelines", - "id": "def-public.elementOrChildrenHasFocus.$1", - "type": "CompoundType", + "id": "def-public.isArrowUp.$1", + "type": "Object", "tags": [], - "label": "element", + "label": "event", "description": [], "signature": [ - "HTMLElement | null | undefined" + "React.KeyboardEvent" ], "path": "x-pack/plugins/timelines/common/utils/accessibility/helpers.ts", "deprecated": false, "trackAdoption": false, - "isRequired": false + "isRequired": true } ], "returnComment": [], @@ -672,61 +634,66 @@ }, { "parentPluginId": "timelines", - "id": "def-public.escapeKuery", + "id": "def-public.isEscape", "type": "Function", "tags": [], - "label": "escapeKuery", - "description": [], + "label": "isEscape", + "description": [ + "Returns `true` if the escape key was pressed" + ], "signature": [ - "(val: string) => string" + "(event: React.KeyboardEvent) => boolean" ], - "path": "x-pack/plugins/timelines/public/components/utils/keury/index.ts", + "path": "x-pack/plugins/timelines/common/utils/accessibility/helpers.ts", "deprecated": false, "trackAdoption": false, - "returnComment": [], "children": [ { "parentPluginId": "timelines", - "id": "def-public.escapeKuery.$1", - "type": "Uncategorized", + "id": "def-public.isEscape.$1", + "type": "Object", "tags": [], - "label": "args", + "label": "event", "description": [], "signature": [ - "A" + "React.KeyboardEvent" ], - "path": "node_modules/@types/lodash/ts3.1/fp.d.ts", + "path": "x-pack/plugins/timelines/common/utils/accessibility/helpers.ts", "deprecated": false, - "trackAdoption": false + "trackAdoption": false, + "isRequired": true } ], + "returnComment": [], "initialIsOpen": false }, { "parentPluginId": "timelines", - "id": "def-public.escapeQueryValue", + "id": "def-public.isTab", "type": "Function", "tags": [], - "label": "escapeQueryValue", - "description": [], + "label": "isTab", + "description": [ + "Returns `true` if the tab key was pressed" + ], "signature": [ - "(val?: string | number) => string | number" + "(event: React.KeyboardEvent) => boolean" ], - "path": "x-pack/plugins/timelines/public/components/utils/keury/index.ts", + "path": "x-pack/plugins/timelines/common/utils/accessibility/helpers.ts", "deprecated": false, "trackAdoption": false, "children": [ { "parentPluginId": "timelines", - "id": "def-public.escapeQueryValue.$1", - "type": "CompoundType", + "id": "def-public.isTab.$1", + "type": "Object", "tags": [], - "label": "val", + "label": "event", "description": [], "signature": [ - "string | number" + "React.KeyboardEvent" ], - "path": "x-pack/plugins/timelines/public/components/utils/keury/index.ts", + "path": "x-pack/plugins/timelines/common/utils/accessibility/helpers.ts", "deprecated": false, "trackAdoption": false, "isRequired": true @@ -737,15 +704,17 @@ }, { "parentPluginId": "timelines", - "id": "def-public.focusColumn", + "id": "def-public.onKeyDownFocusHandler", "type": "Function", "tags": [], - "label": "focusColumn", + "label": "onKeyDownFocusHandler", "description": [ - "\nSIDE EFFECT: mutates the DOM by focusing the specified column\nreturns the `aria-colindex` of the newly-focused column" + "\nThis function adds keyboard accessability to any `containerElement` that\nrenders its rows with support for `aria-colindex` and `aria-rowindex`.\n\nTo use this function, invoke it in the `onKeyDown` handler of the specified\n`containerElement`.\n\nSee the `Keyboard Support` section of\nhttps://www.w3.org/TR/wai-aria-practices-1.1/examples/grid/dataGrids.html\nfor details of the behavior." ], "signature": [ - "({ colindexAttribute, containerElement, ariaColindex, ariaRowindex, rowindexAttribute, }: { colindexAttribute: string; containerElement: Element | null; ariaColindex: number; ariaRowindex: number; rowindexAttribute: string; }) => FocusColumnResult" + "({ colindexAttribute, containerElement, event, maxAriaColindex, maxAriaRowindex, onColumnFocused, rowindexAttribute, }: { colindexAttribute: string; containerElement: HTMLDivElement | null; event: React.KeyboardEvent; maxAriaColindex: number; maxAriaRowindex: number; onColumnFocused: ", + "OnColumnFocused", + "; rowindexAttribute: string; }) => void" ], "path": "x-pack/plugins/timelines/common/utils/accessibility/helpers.ts", "deprecated": false, @@ -753,10 +722,10 @@ "children": [ { "parentPluginId": "timelines", - "id": "def-public.focusColumn.$1", + "id": "def-public.onKeyDownFocusHandler.$1", "type": "Object", "tags": [], - "label": "{\n colindexAttribute,\n containerElement,\n ariaColindex,\n ariaRowindex,\n rowindexAttribute,\n}", + "label": "{\n colindexAttribute,\n containerElement,\n event,\n maxAriaColindex,\n maxAriaRowindex,\n onColumnFocused,\n rowindexAttribute,\n}", "description": [], "path": "x-pack/plugins/timelines/common/utils/accessibility/helpers.ts", "deprecated": false, @@ -764,7 +733,7 @@ "children": [ { "parentPluginId": "timelines", - "id": "def-public.focusColumn.$1.colindexAttribute", + "id": "def-public.onKeyDownFocusHandler.$1.colindexAttribute", "type": "string", "tags": [], "label": "colindexAttribute", @@ -775,13 +744,13 @@ }, { "parentPluginId": "timelines", - "id": "def-public.focusColumn.$1.containerElement", + "id": "def-public.onKeyDownFocusHandler.$1.containerElement", "type": "CompoundType", "tags": [], "label": "containerElement", "description": [], "signature": [ - "Element | null" + "HTMLDivElement | null" ], "path": "x-pack/plugins/timelines/common/utils/accessibility/helpers.ts", "deprecated": false, @@ -789,10 +758,24 @@ }, { "parentPluginId": "timelines", - "id": "def-public.focusColumn.$1.ariaColindex", + "id": "def-public.onKeyDownFocusHandler.$1.event", + "type": "Object", + "tags": [], + "label": "event", + "description": [], + "signature": [ + "React.KeyboardEvent" + ], + "path": "x-pack/plugins/timelines/common/utils/accessibility/helpers.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "timelines", + "id": "def-public.onKeyDownFocusHandler.$1.maxAriaColindex", "type": "number", "tags": [], - "label": "ariaColindex", + "label": "maxAriaColindex", "description": [], "path": "x-pack/plugins/timelines/common/utils/accessibility/helpers.ts", "deprecated": false, @@ -800,10 +783,10 @@ }, { "parentPluginId": "timelines", - "id": "def-public.focusColumn.$1.ariaRowindex", + "id": "def-public.onKeyDownFocusHandler.$1.maxAriaRowindex", "type": "number", "tags": [], - "label": "ariaRowindex", + "label": "maxAriaRowindex", "description": [], "path": "x-pack/plugins/timelines/common/utils/accessibility/helpers.ts", "deprecated": false, @@ -811,7 +794,38 @@ }, { "parentPluginId": "timelines", - "id": "def-public.focusColumn.$1.rowindexAttribute", + "id": "def-public.onKeyDownFocusHandler.$1.onColumnFocused", + "type": "Function", + "tags": [], + "label": "onColumnFocused", + "description": [], + "signature": [ + "({ newFocusedColumn, newFocusedColumnAriaColindex, }: { newFocusedColumn: HTMLDivElement | null; newFocusedColumnAriaColindex: number | null; }) => void" + ], + "path": "x-pack/plugins/timelines/common/utils/accessibility/helpers.ts", + "deprecated": false, + "trackAdoption": false, + "returnComment": [], + "children": [ + { + "parentPluginId": "timelines", + "id": "def-public.onKeyDownFocusHandler.$1.onColumnFocused.$1", + "type": "Object", + "tags": [], + "label": "__0", + "description": [], + "signature": [ + "{ newFocusedColumn: HTMLDivElement | null; newFocusedColumnAriaColindex: number | null; }" + ], + "path": "x-pack/plugins/timelines/common/utils/accessibility/helpers.ts", + "deprecated": false, + "trackAdoption": false + } + ] + }, + { + "parentPluginId": "timelines", + "id": "def-public.onKeyDownFocusHandler.$1.rowindexAttribute", "type": "string", "tags": [], "label": "rowindexAttribute", @@ -828,31 +842,31 @@ }, { "parentPluginId": "timelines", - "id": "def-public.getActionsColumnWidth", + "id": "def-public.stopPropagationAndPreventDefault", "type": "Function", "tags": [], - "label": "getActionsColumnWidth", + "label": "stopPropagationAndPreventDefault", "description": [ - "\nReturns the width of the Actions column based on the number of buttons being\ndisplayed\n\nNOTE: This function is necessary because `width` is a required property of\nthe `EuiDataGridControlColumn` interface, so it must be calculated before\ncontent is rendered. (The width of a `EuiDataGridControlColumn` does not\nautomatically size itself to fit all the content.)" + "\nThis function has side effects: It stops propagation of the provided\n`KeyboardEvent` and prevents the browser's default behavior." ], "signature": [ - "(actionButtonCount: number) => number" + "(event: React.KeyboardEvent) => void" ], - "path": "x-pack/plugins/timelines/public/components/t_grid/body/column_headers/helpers.tsx", + "path": "x-pack/plugins/timelines/common/utils/accessibility/helpers.ts", "deprecated": false, "trackAdoption": false, "children": [ { "parentPluginId": "timelines", - "id": "def-public.getActionsColumnWidth.$1", - "type": "number", + "id": "def-public.stopPropagationAndPreventDefault.$1", + "type": "Object", "tags": [], - "label": "actionButtonCount", + "label": "event", "description": [], "signature": [ - "number" + "React.KeyboardEvent" ], - "path": "x-pack/plugins/timelines/public/components/t_grid/body/column_headers/helpers.tsx", + "path": "x-pack/plugins/timelines/common/utils/accessibility/helpers.ts", "deprecated": false, "trackAdoption": false, "isRequired": true @@ -860,3379 +874,492 @@ ], "returnComment": [], "initialIsOpen": false - }, + } + ], + "interfaces": [ { "parentPluginId": "timelines", - "id": "def-public.getFocusedAriaColindexCell", - "type": "Function", + "id": "def-public.AddToTimelineButtonProps", + "type": "Interface", "tags": [], - "label": "getFocusedAriaColindexCell", - "description": [ - "\nReturns the focused cell for tables that use `aria-colindex`" - ], + "label": "AddToTimelineButtonProps", + "description": [], "signature": [ - "({ containerElement, tableClassName, }: { containerElement: HTMLElement | null; tableClassName: string; }) => HTMLDivElement | null" + { + "pluginId": "timelines", + "scope": "public", + "docId": "kibTimelinesPluginApi", + "section": "def-public.AddToTimelineButtonProps", + "text": "AddToTimelineButtonProps" + }, + " extends ", + "HoverActionComponentProps" ], - "path": "x-pack/plugins/timelines/common/utils/accessibility/helpers.ts", + "path": "x-pack/plugins/timelines/public/components/hover_actions/actions/add_to_timeline.tsx", "deprecated": false, "trackAdoption": false, "children": [ { "parentPluginId": "timelines", - "id": "def-public.getFocusedAriaColindexCell.$1", - "type": "Object", + "id": "def-public.AddToTimelineButtonProps.Component", + "type": "CompoundType", "tags": [], - "label": "{\n containerElement,\n tableClassName,\n}", + "label": "Component", + "description": [ + "`Component` is only used with `EuiDataGrid`; the grid keeps a reference to `Component` for show / hide functionality" + ], + "signature": [ + "React.FunctionComponent<(", + "DisambiguateSet", + " & ", + "CommonEuiButtonEmptyProps", + " & { onClick?: React.MouseEventHandler | undefined; } & React.ButtonHTMLAttributes) | (", + "DisambiguateSet", + " & ", + "CommonEuiButtonEmptyProps", + " & { href?: string | undefined; onClick?: React.MouseEventHandler | undefined; } & React.AnchorHTMLAttributes)> | React.FunctionComponent<(", + "DisambiguateSet", + "<", + "EuiButtonIconPropsForAnchor", + ", ", + "EuiButtonIconPropsForButton", + "> & { type?: \"reset\" | \"button\" | \"submit\" | undefined; } & ", + "EuiButtonIconProps", + " & { onClick?: React.MouseEventHandler | undefined; } & React.ButtonHTMLAttributes & { buttonRef?: React.Ref | undefined; }) | (", + "DisambiguateSet", + "<", + "EuiButtonIconPropsForButton", + ", ", + "EuiButtonIconPropsForAnchor", + "> & { type?: string | undefined; } & ", + "EuiButtonIconProps", + " & { href?: string | undefined; onClick?: React.MouseEventHandler | undefined; } & React.AnchorHTMLAttributes & { buttonRef?: React.Ref | undefined; })> | typeof ", + "EuiContextMenuItem", + " | undefined" + ], + "path": "x-pack/plugins/timelines/public/components/hover_actions/actions/add_to_timeline.tsx", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "timelines", + "id": "def-public.AddToTimelineButtonProps.draggableId", + "type": "string", + "tags": [], + "label": "draggableId", "description": [], - "path": "x-pack/plugins/timelines/common/utils/accessibility/helpers.ts", + "signature": [ + "string | undefined" + ], + "path": "x-pack/plugins/timelines/public/components/hover_actions/actions/add_to_timeline.tsx", "deprecated": false, - "trackAdoption": false, - "children": [ + "trackAdoption": false + }, + { + "parentPluginId": "timelines", + "id": "def-public.AddToTimelineButtonProps.dataProvider", + "type": "CompoundType", + "tags": [], + "label": "dataProvider", + "description": [], + "signature": [ { - "parentPluginId": "timelines", - "id": "def-public.getFocusedAriaColindexCell.$1.containerElement", - "type": "CompoundType", - "tags": [], - "label": "containerElement", - "description": [], - "signature": [ - "HTMLElement | null" - ], - "path": "x-pack/plugins/timelines/common/utils/accessibility/helpers.ts", - "deprecated": false, - "trackAdoption": false + "pluginId": "timelines", + "scope": "common", + "docId": "kibTimelinesPluginApi", + "section": "def-common.DataProvider", + "text": "DataProvider" }, + " | ", { - "parentPluginId": "timelines", - "id": "def-public.getFocusedAriaColindexCell.$1.tableClassName", - "type": "string", - "tags": [], - "label": "tableClassName", - "description": [], - "path": "x-pack/plugins/timelines/common/utils/accessibility/helpers.ts", - "deprecated": false, - "trackAdoption": false - } - ] + "pluginId": "timelines", + "scope": "common", + "docId": "kibTimelinesPluginApi", + "section": "def-common.DataProvider", + "text": "DataProvider" + }, + "[] | undefined" + ], + "path": "x-pack/plugins/timelines/public/components/hover_actions/actions/add_to_timeline.tsx", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "timelines", + "id": "def-public.AddToTimelineButtonProps.timelineType", + "type": "string", + "tags": [], + "label": "timelineType", + "description": [], + "signature": [ + "string | undefined" + ], + "path": "x-pack/plugins/timelines/public/components/hover_actions/actions/add_to_timeline.tsx", + "deprecated": false, + "trackAdoption": false } ], - "returnComment": [], "initialIsOpen": false - }, + } + ], + "enums": [], + "misc": [ { "parentPluginId": "timelines", - "id": "def-public.getFocusedDataColindexCell", - "type": "Function", + "id": "def-public.ARIA_COLINDEX_ATTRIBUTE", + "type": "string", "tags": [], - "label": "getFocusedDataColindexCell", + "label": "ARIA_COLINDEX_ATTRIBUTE", "description": [ - "\nReturns the focused cell for tables that use `data-colindex`" + "\nThe name of the ARIA attribute representing a column, used in conjunction with\nthe ARIA: grid role https://www.w3.org/TR/wai-aria-practices-1.1/examples/grid/dataGrids.html" ], "signature": [ - "({ containerElement, tableClassName, }: { containerElement: HTMLElement | null; tableClassName: string; }) => HTMLDivElement | null" + "\"aria-colindex\"" ], "path": "x-pack/plugins/timelines/common/utils/accessibility/helpers.ts", "deprecated": false, "trackAdoption": false, - "children": [ - { - "parentPluginId": "timelines", - "id": "def-public.getFocusedDataColindexCell.$1", - "type": "Object", - "tags": [], - "label": "{\n containerElement,\n tableClassName,\n}", - "description": [], - "path": "x-pack/plugins/timelines/common/utils/accessibility/helpers.ts", - "deprecated": false, - "trackAdoption": false, - "children": [ - { - "parentPluginId": "timelines", - "id": "def-public.getFocusedDataColindexCell.$1.containerElement", - "type": "CompoundType", - "tags": [], - "label": "containerElement", - "description": [], - "signature": [ - "HTMLElement | null" - ], - "path": "x-pack/plugins/timelines/common/utils/accessibility/helpers.ts", - "deprecated": false, - "trackAdoption": false - }, - { - "parentPluginId": "timelines", - "id": "def-public.getFocusedDataColindexCell.$1.tableClassName", - "type": "string", - "tags": [], - "label": "tableClassName", - "description": [], - "path": "x-pack/plugins/timelines/common/utils/accessibility/helpers.ts", - "deprecated": false, - "trackAdoption": false - } - ] - } - ], - "returnComment": [], "initialIsOpen": false }, { "parentPluginId": "timelines", - "id": "def-public.getNotesContainerClassName", - "type": "Function", + "id": "def-public.ARIA_ROWINDEX_ATTRIBUTE", + "type": "string", "tags": [], - "label": "getNotesContainerClassName", - "description": [], + "label": "ARIA_ROWINDEX_ATTRIBUTE", + "description": [ + "\nThe name of the ARIA attribute representing a row, used in conjunction with\nthe ARIA: grid role https://www.w3.org/TR/wai-aria-practices-1.1/examples/grid/dataGrids.html" + ], "signature": [ - "(ariaRowindex: number) => string" + "\"aria-rowindex\"" ], "path": "x-pack/plugins/timelines/common/utils/accessibility/helpers.ts", "deprecated": false, "trackAdoption": false, - "children": [ - { - "parentPluginId": "timelines", - "id": "def-public.getNotesContainerClassName.$1", - "type": "number", - "tags": [], - "label": "ariaRowindex", - "description": [], - "signature": [ - "number" - ], - "path": "x-pack/plugins/timelines/common/utils/accessibility/helpers.ts", - "deprecated": false, - "trackAdoption": false, - "isRequired": true - } - ], - "returnComment": [], "initialIsOpen": false }, { "parentPluginId": "timelines", - "id": "def-public.getPageRowIndex", - "type": "Function", + "id": "def-public.DATA_COLINDEX_ATTRIBUTE", + "type": "string", "tags": [], - "label": "getPageRowIndex", + "label": "DATA_COLINDEX_ATTRIBUTE", "description": [ - "\nrowIndex is bigger than `data.length` for pages with page numbers bigger than one.\nFor that reason, we must calculate `rowIndex % itemsPerPage`.\n\nEx:\nGiven `rowIndex` is `13` and `itemsPerPage` is `10`.\nIt means that the `activePage` is `2` and the `pageRowIndex` is `3`\n\n**Warning**:\nBe careful with array out of bounds. `pageRowIndex` can be bigger or equal to `data.length`\n in the scenario where the user changes the event status (Open, Acknowledged, Closed)." + "\nThis alternative attribute to `aria-colindex` is used to decorate the data\nin existing `EuiTable`s to enable keyboard navigation with minimal\nrefactoring of existing code until we're ready to migrate to `EuiDataGrid`.\nIt may be applied directly to keyboard-focusable elements and thus doesn't\nhave exactly the same semantics as `aria-colindex`." ], "signature": [ - "(rowIndex: number, itemsPerPage: number) => number" + "\"data-colindex\"" ], - "path": "x-pack/plugins/timelines/common/utils/pagination.ts", + "path": "x-pack/plugins/timelines/common/utils/accessibility/helpers.ts", "deprecated": false, "trackAdoption": false, - "children": [ - { - "parentPluginId": "timelines", - "id": "def-public.getPageRowIndex.$1", - "type": "number", - "tags": [], - "label": "rowIndex", - "description": [], - "signature": [ - "number" - ], - "path": "x-pack/plugins/timelines/common/utils/pagination.ts", - "deprecated": false, - "trackAdoption": false, - "isRequired": true - }, - { - "parentPluginId": "timelines", - "id": "def-public.getPageRowIndex.$2", - "type": "number", - "tags": [], - "label": "itemsPerPage", - "description": [], - "signature": [ - "number" - ], - "path": "x-pack/plugins/timelines/common/utils/pagination.ts", - "deprecated": false, - "trackAdoption": false, - "isRequired": true - } - ], - "returnComment": [], "initialIsOpen": false }, { "parentPluginId": "timelines", - "id": "def-public.getRowRendererClassName", - "type": "Function", + "id": "def-public.DATA_ROWINDEX_ATTRIBUTE", + "type": "string", "tags": [], - "label": "getRowRendererClassName", - "description": [], + "label": "DATA_ROWINDEX_ATTRIBUTE", + "description": [ + "\nThis alternative attribute to `aria-rowindex` is used to decorate the data\nin existing `EuiTable`s to enable keyboard navigation with minimal\nrefactoring of existing code until we're ready to migrate to `EuiDataGrid`.\nIt's typically applied to `` elements via `EuiTable`'s `rowProps` prop." + ], "signature": [ - "(ariaRowindex: number) => string" + "\"data-rowindex\"" ], "path": "x-pack/plugins/timelines/common/utils/accessibility/helpers.ts", "deprecated": false, "trackAdoption": false, - "children": [ - { - "parentPluginId": "timelines", - "id": "def-public.getRowRendererClassName.$1", - "type": "number", - "tags": [], - "label": "ariaRowindex", - "description": [], - "signature": [ - "number" - ], - "path": "x-pack/plugins/timelines/common/utils/accessibility/helpers.ts", - "deprecated": false, - "trackAdoption": false, - "isRequired": true - } - ], - "returnComment": [], "initialIsOpen": false }, { "parentPluginId": "timelines", - "id": "def-public.getTableSkipFocus", - "type": "Function", + "id": "def-public.FIRST_ARIA_INDEX", + "type": "number", "tags": [], - "label": "getTableSkipFocus", + "label": "FIRST_ARIA_INDEX", "description": [ - "\nThis function, which works with tables that use the `aria-colindex` or\n`data-colindex` attributes, examines the focus state of the table, and\nreturns a `SkipFocus` enumeration.\n\nThe `SkipFocus` return value indicates whether the caller should skip focus\nto \"before\" the table, \"after\" the table, or take no action, and let the\nbrowser's \"natural\" focus management manage focus." + "`aria-colindex` and `aria-rowindex` start at one" ], "signature": [ - "({ containerElement, getFocusedCell, shiftKey, tableHasFocus, tableClassName, }: { containerElement: HTMLElement | null; getFocusedCell: ", - "GetFocusedCell", - "; shiftKey: boolean; tableHasFocus: (containerElement: HTMLElement | null) => boolean; tableClassName: string; }) => ", - "SkipFocus" + "1" ], "path": "x-pack/plugins/timelines/common/utils/accessibility/helpers.ts", "deprecated": false, "trackAdoption": false, - "children": [ - { - "parentPluginId": "timelines", - "id": "def-public.getTableSkipFocus.$1", - "type": "Object", - "tags": [], - "label": "{\n containerElement,\n getFocusedCell,\n shiftKey,\n tableHasFocus,\n tableClassName,\n}", - "description": [], - "path": "x-pack/plugins/timelines/common/utils/accessibility/helpers.ts", - "deprecated": false, - "trackAdoption": false, - "children": [ - { - "parentPluginId": "timelines", - "id": "def-public.getTableSkipFocus.$1.containerElement", - "type": "CompoundType", - "tags": [], - "label": "containerElement", - "description": [], - "signature": [ - "HTMLElement | null" - ], - "path": "x-pack/plugins/timelines/common/utils/accessibility/helpers.ts", - "deprecated": false, - "trackAdoption": false - }, - { - "parentPluginId": "timelines", - "id": "def-public.getTableSkipFocus.$1.getFocusedCell", - "type": "Function", - "tags": [], - "label": "getFocusedCell", - "description": [], - "signature": [ - "({ containerElement, tableClassName, }: { containerElement: HTMLElement | null; tableClassName: string; }) => HTMLDivElement | null" - ], - "path": "x-pack/plugins/timelines/common/utils/accessibility/helpers.ts", - "deprecated": false, - "trackAdoption": false, - "returnComment": [], - "children": [ - { - "parentPluginId": "timelines", - "id": "def-public.getTableSkipFocus.$1.getFocusedCell.$1", - "type": "Object", - "tags": [], - "label": "__0", - "description": [], - "signature": [ - "{ containerElement: HTMLElement | null; tableClassName: string; }" - ], - "path": "x-pack/plugins/timelines/common/utils/accessibility/helpers.ts", - "deprecated": false, - "trackAdoption": false - } - ] - }, - { - "parentPluginId": "timelines", - "id": "def-public.getTableSkipFocus.$1.shiftKey", - "type": "boolean", - "tags": [], - "label": "shiftKey", - "description": [], - "path": "x-pack/plugins/timelines/common/utils/accessibility/helpers.ts", - "deprecated": false, - "trackAdoption": false - }, - { - "parentPluginId": "timelines", - "id": "def-public.getTableSkipFocus.$1.tableHasFocus", - "type": "Function", - "tags": [], - "label": "tableHasFocus", - "description": [], - "signature": [ - "(containerElement: HTMLElement | null) => boolean" - ], - "path": "x-pack/plugins/timelines/common/utils/accessibility/helpers.ts", - "deprecated": false, - "trackAdoption": false, - "children": [ - { - "parentPluginId": "timelines", - "id": "def-public.getTableSkipFocus.$1.tableHasFocus.$1", - "type": "CompoundType", - "tags": [], - "label": "containerElement", - "description": [], - "signature": [ - "HTMLElement | null" - ], - "path": "x-pack/plugins/timelines/common/utils/accessibility/helpers.ts", - "deprecated": false, - "trackAdoption": false, - "isRequired": false - } - ], - "returnComment": [] - }, - { - "parentPluginId": "timelines", - "id": "def-public.getTableSkipFocus.$1.tableClassName", - "type": "string", - "tags": [], - "label": "tableClassName", - "description": [], - "path": "x-pack/plugins/timelines/common/utils/accessibility/helpers.ts", - "deprecated": false, - "trackAdoption": false - } - ] - } - ], - "returnComment": [], "initialIsOpen": false }, { "parentPluginId": "timelines", - "id": "def-public.getTimelineIdFromColumnDroppableId", - "type": "Function", + "id": "def-public.OnColumnFocused", + "type": "Type", "tags": [], - "label": "getTimelineIdFromColumnDroppableId", + "label": "OnColumnFocused", "description": [], "signature": [ - "(droppableId: string) => string" - ], - "path": "x-pack/plugins/timelines/public/components/drag_and_drop/helpers.ts", - "deprecated": false, - "trackAdoption": false, - "children": [ - { - "parentPluginId": "timelines", - "id": "def-public.getTimelineIdFromColumnDroppableId.$1", - "type": "string", - "tags": [], - "label": "droppableId", - "description": [], - "signature": [ - "string" - ], - "path": "x-pack/plugins/timelines/public/components/drag_and_drop/helpers.ts", - "deprecated": false, - "trackAdoption": false, - "isRequired": true - } - ], - "returnComment": [], - "initialIsOpen": false - }, - { - "parentPluginId": "timelines", - "id": "def-public.handleSkipFocus", - "type": "Function", - "tags": [], - "label": "handleSkipFocus", - "description": [ - "\nIf the value of `skipFocus` is `SKIP_FOCUS_BACKWARDS` or `SKIP_FOCUS_FORWARD`\nthis function will invoke the provided `onSkipFocusBackwards` or\n`onSkipFocusForward` functions respectively.\n\nIf `skipFocus` is `SKIP_FOCUS_NOOP`, the `onSkipFocusBackwards` and\n`onSkipFocusForward` functions will not be invoked." - ], - "signature": [ - "({ onSkipFocusBackwards, onSkipFocusForward, skipFocus, }: { onSkipFocusBackwards: () => void; onSkipFocusForward: () => void; skipFocus: ", - "SkipFocus", - "; }) => void" + "({ newFocusedColumn, newFocusedColumnAriaColindex, }: { newFocusedColumn: HTMLDivElement | null; newFocusedColumnAriaColindex: number | null; }) => void" ], "path": "x-pack/plugins/timelines/common/utils/accessibility/helpers.ts", "deprecated": false, "trackAdoption": false, + "returnComment": [], "children": [ { "parentPluginId": "timelines", - "id": "def-public.handleSkipFocus.$1", + "id": "def-public.OnColumnFocused.$1", "type": "Object", "tags": [], - "label": "{\n onSkipFocusBackwards,\n onSkipFocusForward,\n skipFocus,\n}", + "label": "__0", "description": [], + "signature": [ + "{ newFocusedColumn: HTMLDivElement | null; newFocusedColumnAriaColindex: number | null; }" + ], "path": "x-pack/plugins/timelines/common/utils/accessibility/helpers.ts", "deprecated": false, - "trackAdoption": false, - "children": [ - { - "parentPluginId": "timelines", - "id": "def-public.handleSkipFocus.$1.onSkipFocusBackwards", - "type": "Function", - "tags": [], - "label": "onSkipFocusBackwards", - "description": [], - "signature": [ - "() => void" - ], - "path": "x-pack/plugins/timelines/common/utils/accessibility/helpers.ts", - "deprecated": false, - "trackAdoption": false, - "children": [], - "returnComment": [] - }, - { - "parentPluginId": "timelines", - "id": "def-public.handleSkipFocus.$1.onSkipFocusForward", - "type": "Function", - "tags": [], - "label": "onSkipFocusForward", - "description": [], - "signature": [ - "() => void" - ], - "path": "x-pack/plugins/timelines/common/utils/accessibility/helpers.ts", - "deprecated": false, - "trackAdoption": false, - "children": [], - "returnComment": [] - }, - { - "parentPluginId": "timelines", - "id": "def-public.handleSkipFocus.$1.skipFocus", - "type": "CompoundType", - "tags": [], - "label": "skipFocus", - "description": [], - "signature": [ - "\"SKIP_FOCUS_BACKWARDS\" | \"SKIP_FOCUS_FORWARD\" | \"SKIP_FOCUS_NOOP\"" - ], - "path": "x-pack/plugins/timelines/common/utils/accessibility/helpers.ts", - "deprecated": false, - "trackAdoption": false - } - ] + "trackAdoption": false } ], - "returnComment": [], "initialIsOpen": false - }, - { - "parentPluginId": "timelines", - "id": "def-public.initializeTGridSettings", - "type": "Function", - "tags": [], - "label": "initializeTGridSettings", - "description": [], - "signature": [ - "ActionCreator", - "<", - "InitialyzeTGridSettings", - ">" - ], - "path": "x-pack/plugins/timelines/public/store/t_grid/actions.ts", - "deprecated": false, - "trackAdoption": false, - "returnComment": [], - "children": [ - { - "parentPluginId": "timelines", - "id": "def-public.initializeTGridSettings.$1", - "type": "Uncategorized", - "tags": [], - "label": "payload", - "description": [], - "signature": [ - "Payload" - ], - "path": "node_modules/typescript-fsa/lib/index.d.ts", - "deprecated": false, - "trackAdoption": false - }, - { - "parentPluginId": "timelines", - "id": "def-public.initializeTGridSettings.$2", - "type": "CompoundType", - "tags": [], - "label": "meta", - "description": [], - "signature": [ - "Meta", - " | undefined" - ], - "path": "node_modules/typescript-fsa/lib/index.d.ts", - "deprecated": false, - "trackAdoption": false - } - ], - "initialIsOpen": false - }, - { - "parentPluginId": "timelines", - "id": "def-public.isArrowDownOrArrowUp", - "type": "Function", - "tags": [], - "label": "isArrowDownOrArrowUp", - "description": [ - "Returns `true` if the down or up arrow was pressed" - ], - "signature": [ - "(event: React.KeyboardEvent) => boolean" - ], - "path": "x-pack/plugins/timelines/common/utils/accessibility/helpers.ts", - "deprecated": false, - "trackAdoption": false, - "children": [ - { - "parentPluginId": "timelines", - "id": "def-public.isArrowDownOrArrowUp.$1", - "type": "Object", - "tags": [], - "label": "event", - "description": [], - "signature": [ - "React.KeyboardEvent" - ], - "path": "x-pack/plugins/timelines/common/utils/accessibility/helpers.ts", - "deprecated": false, - "trackAdoption": false, - "isRequired": true - } - ], - "returnComment": [], - "initialIsOpen": false - }, - { - "parentPluginId": "timelines", - "id": "def-public.isArrowUp", - "type": "Function", - "tags": [], - "label": "isArrowUp", - "description": [ - "Returns `true` if the up arrow key was pressed" - ], - "signature": [ - "(event: React.KeyboardEvent) => boolean" - ], - "path": "x-pack/plugins/timelines/common/utils/accessibility/helpers.ts", - "deprecated": false, - "trackAdoption": false, - "children": [ - { - "parentPluginId": "timelines", - "id": "def-public.isArrowUp.$1", - "type": "Object", - "tags": [], - "label": "event", - "description": [], - "signature": [ - "React.KeyboardEvent" - ], - "path": "x-pack/plugins/timelines/common/utils/accessibility/helpers.ts", - "deprecated": false, - "trackAdoption": false, - "isRequired": true - } - ], - "returnComment": [], - "initialIsOpen": false - }, - { - "parentPluginId": "timelines", - "id": "def-public.isEscape", - "type": "Function", - "tags": [], - "label": "isEscape", - "description": [ - "Returns `true` if the escape key was pressed" - ], - "signature": [ - "(event: React.KeyboardEvent) => boolean" - ], - "path": "x-pack/plugins/timelines/common/utils/accessibility/helpers.ts", - "deprecated": false, - "trackAdoption": false, - "children": [ - { - "parentPluginId": "timelines", - "id": "def-public.isEscape.$1", - "type": "Object", - "tags": [], - "label": "event", - "description": [], - "signature": [ - "React.KeyboardEvent" - ], - "path": "x-pack/plugins/timelines/common/utils/accessibility/helpers.ts", - "deprecated": false, - "trackAdoption": false, - "isRequired": true - } - ], - "returnComment": [], - "initialIsOpen": false - }, - { - "parentPluginId": "timelines", - "id": "def-public.isTab", - "type": "Function", - "tags": [], - "label": "isTab", - "description": [ - "Returns `true` if the tab key was pressed" - ], - "signature": [ - "(event: React.KeyboardEvent) => boolean" - ], - "path": "x-pack/plugins/timelines/common/utils/accessibility/helpers.ts", - "deprecated": false, - "trackAdoption": false, - "children": [ - { - "parentPluginId": "timelines", - "id": "def-public.isTab.$1", - "type": "Object", - "tags": [], - "label": "event", - "description": [], - "signature": [ - "React.KeyboardEvent" - ], - "path": "x-pack/plugins/timelines/common/utils/accessibility/helpers.ts", - "deprecated": false, - "trackAdoption": false, - "isRequired": true - } - ], - "returnComment": [], - "initialIsOpen": false - }, - { - "parentPluginId": "timelines", - "id": "def-public.onKeyDownFocusHandler", - "type": "Function", - "tags": [], - "label": "onKeyDownFocusHandler", - "description": [ - "\nThis function adds keyboard accessability to any `containerElement` that\nrenders its rows with support for `aria-colindex` and `aria-rowindex`.\n\nTo use this function, invoke it in the `onKeyDown` handler of the specified\n`containerElement`.\n\nSee the `Keyboard Support` section of\nhttps://www.w3.org/TR/wai-aria-practices-1.1/examples/grid/dataGrids.html\nfor details of the behavior." - ], - "signature": [ - "({ colindexAttribute, containerElement, event, maxAriaColindex, maxAriaRowindex, onColumnFocused, rowindexAttribute, }: { colindexAttribute: string; containerElement: HTMLDivElement | null; event: React.KeyboardEvent; maxAriaColindex: number; maxAriaRowindex: number; onColumnFocused: ", - "OnColumnFocused", - "; rowindexAttribute: string; }) => void" - ], - "path": "x-pack/plugins/timelines/common/utils/accessibility/helpers.ts", - "deprecated": false, - "trackAdoption": false, - "children": [ - { - "parentPluginId": "timelines", - "id": "def-public.onKeyDownFocusHandler.$1", - "type": "Object", - "tags": [], - "label": "{\n colindexAttribute,\n containerElement,\n event,\n maxAriaColindex,\n maxAriaRowindex,\n onColumnFocused,\n rowindexAttribute,\n}", - "description": [], - "path": "x-pack/plugins/timelines/common/utils/accessibility/helpers.ts", - "deprecated": false, - "trackAdoption": false, - "children": [ - { - "parentPluginId": "timelines", - "id": "def-public.onKeyDownFocusHandler.$1.colindexAttribute", - "type": "string", - "tags": [], - "label": "colindexAttribute", - "description": [], - "path": "x-pack/plugins/timelines/common/utils/accessibility/helpers.ts", - "deprecated": false, - "trackAdoption": false - }, - { - "parentPluginId": "timelines", - "id": "def-public.onKeyDownFocusHandler.$1.containerElement", - "type": "CompoundType", - "tags": [], - "label": "containerElement", - "description": [], - "signature": [ - "HTMLDivElement | null" - ], - "path": "x-pack/plugins/timelines/common/utils/accessibility/helpers.ts", - "deprecated": false, - "trackAdoption": false - }, - { - "parentPluginId": "timelines", - "id": "def-public.onKeyDownFocusHandler.$1.event", - "type": "Object", - "tags": [], - "label": "event", - "description": [], - "signature": [ - "React.KeyboardEvent" - ], - "path": "x-pack/plugins/timelines/common/utils/accessibility/helpers.ts", - "deprecated": false, - "trackAdoption": false - }, - { - "parentPluginId": "timelines", - "id": "def-public.onKeyDownFocusHandler.$1.maxAriaColindex", - "type": "number", - "tags": [], - "label": "maxAriaColindex", - "description": [], - "path": "x-pack/plugins/timelines/common/utils/accessibility/helpers.ts", - "deprecated": false, - "trackAdoption": false - }, - { - "parentPluginId": "timelines", - "id": "def-public.onKeyDownFocusHandler.$1.maxAriaRowindex", - "type": "number", - "tags": [], - "label": "maxAriaRowindex", - "description": [], - "path": "x-pack/plugins/timelines/common/utils/accessibility/helpers.ts", - "deprecated": false, - "trackAdoption": false - }, - { - "parentPluginId": "timelines", - "id": "def-public.onKeyDownFocusHandler.$1.onColumnFocused", - "type": "Function", - "tags": [], - "label": "onColumnFocused", - "description": [], - "signature": [ - "({ newFocusedColumn, newFocusedColumnAriaColindex, }: { newFocusedColumn: HTMLDivElement | null; newFocusedColumnAriaColindex: number | null; }) => void" - ], - "path": "x-pack/plugins/timelines/common/utils/accessibility/helpers.ts", - "deprecated": false, - "trackAdoption": false, - "returnComment": [], - "children": [ - { - "parentPluginId": "timelines", - "id": "def-public.onKeyDownFocusHandler.$1.onColumnFocused.$1", - "type": "Object", - "tags": [], - "label": "__0", - "description": [], - "signature": [ - "{ newFocusedColumn: HTMLDivElement | null; newFocusedColumnAriaColindex: number | null; }" - ], - "path": "x-pack/plugins/timelines/common/utils/accessibility/helpers.ts", - "deprecated": false, - "trackAdoption": false - } - ] - }, - { - "parentPluginId": "timelines", - "id": "def-public.onKeyDownFocusHandler.$1.rowindexAttribute", - "type": "string", - "tags": [], - "label": "rowindexAttribute", - "description": [], - "path": "x-pack/plugins/timelines/common/utils/accessibility/helpers.ts", - "deprecated": false, - "trackAdoption": false - } - ] - } - ], - "returnComment": [], - "initialIsOpen": false - }, - { - "parentPluginId": "timelines", - "id": "def-public.removeColumn", - "type": "Function", - "tags": [], - "label": "removeColumn", - "description": [], - "signature": [ - "ActionCreator", - "<{ id: string; columnId: string; }>" - ], - "path": "x-pack/plugins/timelines/public/store/t_grid/actions.ts", - "deprecated": false, - "trackAdoption": false, - "returnComment": [], - "children": [ - { - "parentPluginId": "timelines", - "id": "def-public.removeColumn.$1", - "type": "Uncategorized", - "tags": [], - "label": "payload", - "description": [], - "signature": [ - "Payload" - ], - "path": "node_modules/typescript-fsa/lib/index.d.ts", - "deprecated": false, - "trackAdoption": false - }, - { - "parentPluginId": "timelines", - "id": "def-public.removeColumn.$2", - "type": "CompoundType", - "tags": [], - "label": "meta", - "description": [], - "signature": [ - "Meta", - " | undefined" - ], - "path": "node_modules/typescript-fsa/lib/index.d.ts", - "deprecated": false, - "trackAdoption": false - } - ], - "initialIsOpen": false - }, - { - "parentPluginId": "timelines", - "id": "def-public.setEventsDeleted", - "type": "Function", - "tags": [], - "label": "setEventsDeleted", - "description": [], - "signature": [ - "ActionCreator", - "<{ id: string; eventIds: string[]; isDeleted: boolean; }>" - ], - "path": "x-pack/plugins/timelines/public/store/t_grid/actions.ts", - "deprecated": false, - "trackAdoption": false, - "returnComment": [], - "children": [ - { - "parentPluginId": "timelines", - "id": "def-public.setEventsDeleted.$1", - "type": "Uncategorized", - "tags": [], - "label": "payload", - "description": [], - "signature": [ - "Payload" - ], - "path": "node_modules/typescript-fsa/lib/index.d.ts", - "deprecated": false, - "trackAdoption": false - }, - { - "parentPluginId": "timelines", - "id": "def-public.setEventsDeleted.$2", - "type": "CompoundType", - "tags": [], - "label": "meta", - "description": [], - "signature": [ - "Meta", - " | undefined" - ], - "path": "node_modules/typescript-fsa/lib/index.d.ts", - "deprecated": false, - "trackAdoption": false - } - ], - "initialIsOpen": false - }, - { - "parentPluginId": "timelines", - "id": "def-public.setEventsLoading", - "type": "Function", - "tags": [], - "label": "setEventsLoading", - "description": [], - "signature": [ - "ActionCreator", - "<{ id: string; eventIds: string[]; isLoading: boolean; }>" - ], - "path": "x-pack/plugins/timelines/public/store/t_grid/actions.ts", - "deprecated": false, - "trackAdoption": false, - "returnComment": [], - "children": [ - { - "parentPluginId": "timelines", - "id": "def-public.setEventsLoading.$1", - "type": "Uncategorized", - "tags": [], - "label": "payload", - "description": [], - "signature": [ - "Payload" - ], - "path": "node_modules/typescript-fsa/lib/index.d.ts", - "deprecated": false, - "trackAdoption": false - }, - { - "parentPluginId": "timelines", - "id": "def-public.setEventsLoading.$2", - "type": "CompoundType", - "tags": [], - "label": "meta", - "description": [], - "signature": [ - "Meta", - " | undefined" - ], - "path": "node_modules/typescript-fsa/lib/index.d.ts", - "deprecated": false, - "trackAdoption": false - } - ], - "initialIsOpen": false - }, - { - "parentPluginId": "timelines", - "id": "def-public.setSelected", - "type": "Function", - "tags": [], - "label": "setSelected", - "description": [], - "signature": [ - "ActionCreator", - "<{ id: string; eventIds: Readonly>; isSelected: boolean; isSelectAllChecked: boolean; }>" - ], - "path": "x-pack/plugins/timelines/public/store/t_grid/actions.ts", - "deprecated": false, - "trackAdoption": false, - "returnComment": [], - "children": [ - { - "parentPluginId": "timelines", - "id": "def-public.setSelected.$1", - "type": "Uncategorized", - "tags": [], - "label": "payload", - "description": [], - "signature": [ - "Payload" - ], - "path": "node_modules/typescript-fsa/lib/index.d.ts", - "deprecated": false, - "trackAdoption": false - }, - { - "parentPluginId": "timelines", - "id": "def-public.setSelected.$2", - "type": "CompoundType", - "tags": [], - "label": "meta", - "description": [], - "signature": [ - "Meta", - " | undefined" - ], - "path": "node_modules/typescript-fsa/lib/index.d.ts", - "deprecated": false, - "trackAdoption": false - } - ], - "initialIsOpen": false - }, - { - "parentPluginId": "timelines", - "id": "def-public.setTGridSelectAll", - "type": "Function", - "tags": [], - "label": "setTGridSelectAll", - "description": [], - "signature": [ - "ActionCreator", - "<{ id: string; selectAll: boolean; }>" - ], - "path": "x-pack/plugins/timelines/public/store/t_grid/actions.ts", - "deprecated": false, - "trackAdoption": false, - "returnComment": [], - "children": [ - { - "parentPluginId": "timelines", - "id": "def-public.setTGridSelectAll.$1", - "type": "Uncategorized", - "tags": [], - "label": "payload", - "description": [], - "signature": [ - "Payload" - ], - "path": "node_modules/typescript-fsa/lib/index.d.ts", - "deprecated": false, - "trackAdoption": false - }, - { - "parentPluginId": "timelines", - "id": "def-public.setTGridSelectAll.$2", - "type": "CompoundType", - "tags": [], - "label": "meta", - "description": [], - "signature": [ - "Meta", - " | undefined" - ], - "path": "node_modules/typescript-fsa/lib/index.d.ts", - "deprecated": false, - "trackAdoption": false - } - ], - "initialIsOpen": false - }, - { - "parentPluginId": "timelines", - "id": "def-public.stopPropagationAndPreventDefault", - "type": "Function", - "tags": [], - "label": "stopPropagationAndPreventDefault", - "description": [ - "\nThis function has side effects: It stops propagation of the provided\n`KeyboardEvent` and prevents the browser's default behavior." - ], - "signature": [ - "(event: React.KeyboardEvent) => void" - ], - "path": "x-pack/plugins/timelines/common/utils/accessibility/helpers.ts", - "deprecated": false, - "trackAdoption": false, - "children": [ - { - "parentPluginId": "timelines", - "id": "def-public.stopPropagationAndPreventDefault.$1", - "type": "Object", - "tags": [], - "label": "event", - "description": [], - "signature": [ - "React.KeyboardEvent" - ], - "path": "x-pack/plugins/timelines/common/utils/accessibility/helpers.ts", - "deprecated": false, - "trackAdoption": false, - "isRequired": true - } - ], - "returnComment": [], - "initialIsOpen": false - }, - { - "parentPluginId": "timelines", - "id": "def-public.tGridReducer", - "type": "Function", - "tags": [], - "label": "tGridReducer", - "description": [ - "The reducer for all data table actions" - ], - "signature": [ - "(state: ", - { - "pluginId": "timelines", - "scope": "public", - "docId": "kibTimelinesPluginApi", - "section": "def-public.TableState", - "text": "TableState" - }, - " | undefined, action: { type: any; }) => ", - { - "pluginId": "timelines", - "scope": "public", - "docId": "kibTimelinesPluginApi", - "section": "def-public.TableState", - "text": "TableState" - } - ], - "path": "x-pack/plugins/timelines/public/store/t_grid/reducer.ts", - "deprecated": false, - "trackAdoption": false, - "returnComment": [], - "children": [ - { - "parentPluginId": "timelines", - "id": "def-public.tGridReducer.$1", - "type": "Uncategorized", - "tags": [], - "label": "state", - "description": [], - "signature": [ - "PassedS" - ], - "path": "node_modules/typescript-fsa-reducers/dist/index.d.ts", - "deprecated": false, - "trackAdoption": false - }, - { - "parentPluginId": "timelines", - "id": "def-public.tGridReducer.$2", - "type": "Object", - "tags": [], - "label": "action", - "description": [], - "signature": [ - "{ type: any; }" - ], - "path": "node_modules/typescript-fsa-reducers/dist/index.d.ts", - "deprecated": false, - "trackAdoption": false - } - ], - "initialIsOpen": false - }, - { - "parentPluginId": "timelines", - "id": "def-public.toggleDetailPanel", - "type": "Function", - "tags": [], - "label": "toggleDetailPanel", - "description": [], - "signature": [ - "ActionCreator", - "<", - "TableToggleDetailPanel", - ">" - ], - "path": "x-pack/plugins/timelines/public/store/t_grid/actions.ts", - "deprecated": false, - "trackAdoption": false, - "returnComment": [], - "children": [ - { - "parentPluginId": "timelines", - "id": "def-public.toggleDetailPanel.$1", - "type": "Uncategorized", - "tags": [], - "label": "payload", - "description": [], - "signature": [ - "Payload" - ], - "path": "node_modules/typescript-fsa/lib/index.d.ts", - "deprecated": false, - "trackAdoption": false - }, - { - "parentPluginId": "timelines", - "id": "def-public.toggleDetailPanel.$2", - "type": "CompoundType", - "tags": [], - "label": "meta", - "description": [], - "signature": [ - "Meta", - " | undefined" - ], - "path": "node_modules/typescript-fsa/lib/index.d.ts", - "deprecated": false, - "trackAdoption": false - } - ], - "initialIsOpen": false - }, - { - "parentPluginId": "timelines", - "id": "def-public.updateColumnOrder", - "type": "Function", - "tags": [], - "label": "updateColumnOrder", - "description": [], - "signature": [ - "ActionCreator", - "<{ columnIds: string[]; id: string; }>" - ], - "path": "x-pack/plugins/timelines/public/store/t_grid/actions.ts", - "deprecated": false, - "trackAdoption": false, - "returnComment": [], - "children": [ - { - "parentPluginId": "timelines", - "id": "def-public.updateColumnOrder.$1", - "type": "Uncategorized", - "tags": [], - "label": "payload", - "description": [], - "signature": [ - "Payload" - ], - "path": "node_modules/typescript-fsa/lib/index.d.ts", - "deprecated": false, - "trackAdoption": false - }, - { - "parentPluginId": "timelines", - "id": "def-public.updateColumnOrder.$2", - "type": "CompoundType", - "tags": [], - "label": "meta", - "description": [], - "signature": [ - "Meta", - " | undefined" - ], - "path": "node_modules/typescript-fsa/lib/index.d.ts", - "deprecated": false, - "trackAdoption": false - } - ], - "initialIsOpen": false - }, - { - "parentPluginId": "timelines", - "id": "def-public.updateColumns", - "type": "Function", - "tags": [], - "label": "updateColumns", - "description": [], - "signature": [ - "ActionCreator", - "<{ id: string; columns: ", - { - "pluginId": "timelines", - "scope": "common", - "docId": "kibTimelinesPluginApi", - "section": "def-common.ColumnHeaderOptions", - "text": "ColumnHeaderOptions" - }, - "[]; }>" - ], - "path": "x-pack/plugins/timelines/public/store/t_grid/actions.ts", - "deprecated": false, - "trackAdoption": false, - "returnComment": [], - "children": [ - { - "parentPluginId": "timelines", - "id": "def-public.updateColumns.$1", - "type": "Uncategorized", - "tags": [], - "label": "payload", - "description": [], - "signature": [ - "Payload" - ], - "path": "node_modules/typescript-fsa/lib/index.d.ts", - "deprecated": false, - "trackAdoption": false - }, - { - "parentPluginId": "timelines", - "id": "def-public.updateColumns.$2", - "type": "CompoundType", - "tags": [], - "label": "meta", - "description": [], - "signature": [ - "Meta", - " | undefined" - ], - "path": "node_modules/typescript-fsa/lib/index.d.ts", - "deprecated": false, - "trackAdoption": false - } - ], - "initialIsOpen": false - }, - { - "parentPluginId": "timelines", - "id": "def-public.updateColumnWidth", - "type": "Function", - "tags": [], - "label": "updateColumnWidth", - "description": [], - "signature": [ - "ActionCreator", - "<{ columnId: string; id: string; width: number; }>" - ], - "path": "x-pack/plugins/timelines/public/store/t_grid/actions.ts", - "deprecated": false, - "trackAdoption": false, - "returnComment": [], - "children": [ - { - "parentPluginId": "timelines", - "id": "def-public.updateColumnWidth.$1", - "type": "Uncategorized", - "tags": [], - "label": "payload", - "description": [], - "signature": [ - "Payload" - ], - "path": "node_modules/typescript-fsa/lib/index.d.ts", - "deprecated": false, - "trackAdoption": false - }, - { - "parentPluginId": "timelines", - "id": "def-public.updateColumnWidth.$2", - "type": "CompoundType", - "tags": [], - "label": "meta", - "description": [], - "signature": [ - "Meta", - " | undefined" - ], - "path": "node_modules/typescript-fsa/lib/index.d.ts", - "deprecated": false, - "trackAdoption": false - } - ], - "initialIsOpen": false - }, - { - "parentPluginId": "timelines", - "id": "def-public.updateGraphEventId", - "type": "Function", - "tags": [], - "label": "updateGraphEventId", - "description": [], - "signature": [ - "ActionCreator", - "<{ id: string; graphEventId: string; }>" - ], - "path": "x-pack/plugins/timelines/public/store/t_grid/actions.ts", - "deprecated": false, - "trackAdoption": false, - "returnComment": [], - "children": [ - { - "parentPluginId": "timelines", - "id": "def-public.updateGraphEventId.$1", - "type": "Uncategorized", - "tags": [], - "label": "payload", - "description": [], - "signature": [ - "Payload" - ], - "path": "node_modules/typescript-fsa/lib/index.d.ts", - "deprecated": false, - "trackAdoption": false - }, - { - "parentPluginId": "timelines", - "id": "def-public.updateGraphEventId.$2", - "type": "CompoundType", - "tags": [], - "label": "meta", - "description": [], - "signature": [ - "Meta", - " | undefined" - ], - "path": "node_modules/typescript-fsa/lib/index.d.ts", - "deprecated": false, - "trackAdoption": false - } - ], - "initialIsOpen": false - }, - { - "parentPluginId": "timelines", - "id": "def-public.updateIsLoading", - "type": "Function", - "tags": [], - "label": "updateIsLoading", - "description": [], - "signature": [ - "ActionCreator", - "<{ id: string; isLoading: boolean; }>" - ], - "path": "x-pack/plugins/timelines/public/store/t_grid/actions.ts", - "deprecated": false, - "trackAdoption": false, - "returnComment": [], - "children": [ - { - "parentPluginId": "timelines", - "id": "def-public.updateIsLoading.$1", - "type": "Uncategorized", - "tags": [], - "label": "payload", - "description": [], - "signature": [ - "Payload" - ], - "path": "node_modules/typescript-fsa/lib/index.d.ts", - "deprecated": false, - "trackAdoption": false - }, - { - "parentPluginId": "timelines", - "id": "def-public.updateIsLoading.$2", - "type": "CompoundType", - "tags": [], - "label": "meta", - "description": [], - "signature": [ - "Meta", - " | undefined" - ], - "path": "node_modules/typescript-fsa/lib/index.d.ts", - "deprecated": false, - "trackAdoption": false - } - ], - "initialIsOpen": false - }, - { - "parentPluginId": "timelines", - "id": "def-public.updateItemsPerPage", - "type": "Function", - "tags": [], - "label": "updateItemsPerPage", - "description": [], - "signature": [ - "ActionCreator", - "<{ id: string; itemsPerPage: number; }>" - ], - "path": "x-pack/plugins/timelines/public/store/t_grid/actions.ts", - "deprecated": false, - "trackAdoption": false, - "returnComment": [], - "children": [ - { - "parentPluginId": "timelines", - "id": "def-public.updateItemsPerPage.$1", - "type": "Uncategorized", - "tags": [], - "label": "payload", - "description": [], - "signature": [ - "Payload" - ], - "path": "node_modules/typescript-fsa/lib/index.d.ts", - "deprecated": false, - "trackAdoption": false - }, - { - "parentPluginId": "timelines", - "id": "def-public.updateItemsPerPage.$2", - "type": "CompoundType", - "tags": [], - "label": "meta", - "description": [], - "signature": [ - "Meta", - " | undefined" - ], - "path": "node_modules/typescript-fsa/lib/index.d.ts", - "deprecated": false, - "trackAdoption": false - } - ], - "initialIsOpen": false - }, - { - "parentPluginId": "timelines", - "id": "def-public.updateItemsPerPageOptions", - "type": "Function", - "tags": [], - "label": "updateItemsPerPageOptions", - "description": [], - "signature": [ - "ActionCreator", - "<{ id: string; itemsPerPageOptions: number[]; }>" - ], - "path": "x-pack/plugins/timelines/public/store/t_grid/actions.ts", - "deprecated": false, - "trackAdoption": false, - "returnComment": [], - "children": [ - { - "parentPluginId": "timelines", - "id": "def-public.updateItemsPerPageOptions.$1", - "type": "Uncategorized", - "tags": [], - "label": "payload", - "description": [], - "signature": [ - "Payload" - ], - "path": "node_modules/typescript-fsa/lib/index.d.ts", - "deprecated": false, - "trackAdoption": false - }, - { - "parentPluginId": "timelines", - "id": "def-public.updateItemsPerPageOptions.$2", - "type": "CompoundType", - "tags": [], - "label": "meta", - "description": [], - "signature": [ - "Meta", - " | undefined" - ], - "path": "node_modules/typescript-fsa/lib/index.d.ts", - "deprecated": false, - "trackAdoption": false - } - ], - "initialIsOpen": false - }, - { - "parentPluginId": "timelines", - "id": "def-public.updateSessionViewConfig", - "type": "Function", - "tags": [], - "label": "updateSessionViewConfig", - "description": [], - "signature": [ - "ActionCreator", - "<{ id: string; sessionViewConfig: ", - "SessionViewConfig", - " | null; }>" - ], - "path": "x-pack/plugins/timelines/public/store/t_grid/actions.ts", - "deprecated": false, - "trackAdoption": false, - "returnComment": [], - "children": [ - { - "parentPluginId": "timelines", - "id": "def-public.updateSessionViewConfig.$1", - "type": "Uncategorized", - "tags": [], - "label": "payload", - "description": [], - "signature": [ - "Payload" - ], - "path": "node_modules/typescript-fsa/lib/index.d.ts", - "deprecated": false, - "trackAdoption": false - }, - { - "parentPluginId": "timelines", - "id": "def-public.updateSessionViewConfig.$2", - "type": "CompoundType", - "tags": [], - "label": "meta", - "description": [], - "signature": [ - "Meta", - " | undefined" - ], - "path": "node_modules/typescript-fsa/lib/index.d.ts", - "deprecated": false, - "trackAdoption": false - } - ], - "initialIsOpen": false - }, - { - "parentPluginId": "timelines", - "id": "def-public.updateSort", - "type": "Function", - "tags": [], - "label": "updateSort", - "description": [], - "signature": [ - "ActionCreator", - "<{ id: string; sort: ", - "SortColumnTable", - "[]; }>" - ], - "path": "x-pack/plugins/timelines/public/store/t_grid/actions.ts", - "deprecated": false, - "trackAdoption": false, - "returnComment": [], - "children": [ - { - "parentPluginId": "timelines", - "id": "def-public.updateSort.$1", - "type": "Uncategorized", - "tags": [], - "label": "payload", - "description": [], - "signature": [ - "Payload" - ], - "path": "node_modules/typescript-fsa/lib/index.d.ts", - "deprecated": false, - "trackAdoption": false - }, - { - "parentPluginId": "timelines", - "id": "def-public.updateSort.$2", - "type": "CompoundType", - "tags": [], - "label": "meta", - "description": [], - "signature": [ - "Meta", - " | undefined" - ], - "path": "node_modules/typescript-fsa/lib/index.d.ts", - "deprecated": false, - "trackAdoption": false - } - ], - "initialIsOpen": false - }, - { - "parentPluginId": "timelines", - "id": "def-public.updateTotalCount", - "type": "Function", - "tags": [], - "label": "updateTotalCount", - "description": [], - "signature": [ - "ActionCreator", - "<{ id: string; totalCount: number; }>" - ], - "path": "x-pack/plugins/timelines/public/store/t_grid/actions.ts", - "deprecated": false, - "trackAdoption": false, - "returnComment": [], - "children": [ - { - "parentPluginId": "timelines", - "id": "def-public.updateTotalCount.$1", - "type": "Uncategorized", - "tags": [], - "label": "payload", - "description": [], - "signature": [ - "Payload" - ], - "path": "node_modules/typescript-fsa/lib/index.d.ts", - "deprecated": false, - "trackAdoption": false - }, - { - "parentPluginId": "timelines", - "id": "def-public.updateTotalCount.$2", - "type": "CompoundType", - "tags": [], - "label": "meta", - "description": [], - "signature": [ - "Meta", - " | undefined" - ], - "path": "node_modules/typescript-fsa/lib/index.d.ts", - "deprecated": false, - "trackAdoption": false - } - ], - "initialIsOpen": false - }, - { - "parentPluginId": "timelines", - "id": "def-public.upsertColumn", - "type": "Function", - "tags": [], - "label": "upsertColumn", - "description": [], - "signature": [ - "ActionCreator", - "<{ column: ", - { - "pluginId": "timelines", - "scope": "common", - "docId": "kibTimelinesPluginApi", - "section": "def-common.ColumnHeaderOptions", - "text": "ColumnHeaderOptions" - }, - "; id: string; index: number; }>" - ], - "path": "x-pack/plugins/timelines/public/store/t_grid/actions.ts", - "deprecated": false, - "trackAdoption": false, - "returnComment": [], - "children": [ - { - "parentPluginId": "timelines", - "id": "def-public.upsertColumn.$1", - "type": "Uncategorized", - "tags": [], - "label": "payload", - "description": [], - "signature": [ - "Payload" - ], - "path": "node_modules/typescript-fsa/lib/index.d.ts", - "deprecated": false, - "trackAdoption": false - }, - { - "parentPluginId": "timelines", - "id": "def-public.upsertColumn.$2", - "type": "CompoundType", - "tags": [], - "label": "meta", - "description": [], - "signature": [ - "Meta", - " | undefined" - ], - "path": "node_modules/typescript-fsa/lib/index.d.ts", - "deprecated": false, - "trackAdoption": false - } - ], - "initialIsOpen": false - }, - { - "parentPluginId": "timelines", - "id": "def-public.useBulkActionItems", - "type": "Function", - "tags": [], - "label": "useBulkActionItems", - "description": [], - "signature": [ - "({ eventIds, currentStatus, query, indexName, setEventsLoading, showAlertStatusActions, setEventsDeleted, onUpdateSuccess, onUpdateFailure, customBulkActions, }: ", - "BulkActionsProps", - ") => JSX.Element[]" - ], - "path": "x-pack/plugins/timelines/public/hooks/use_bulk_action_items.tsx", - "deprecated": false, - "trackAdoption": false, - "children": [ - { - "parentPluginId": "timelines", - "id": "def-public.useBulkActionItems.$1", - "type": "Object", - "tags": [], - "label": "{\n eventIds,\n currentStatus,\n query,\n indexName,\n setEventsLoading,\n showAlertStatusActions = true,\n setEventsDeleted,\n onUpdateSuccess,\n onUpdateFailure,\n customBulkActions,\n}", - "description": [], - "signature": [ - "BulkActionsProps" - ], - "path": "x-pack/plugins/timelines/public/hooks/use_bulk_action_items.tsx", - "deprecated": false, - "trackAdoption": false, - "isRequired": true - } - ], - "returnComment": [], - "initialIsOpen": false - } - ], - "interfaces": [ - { - "parentPluginId": "timelines", - "id": "def-public.AddToTimelineButtonProps", - "type": "Interface", - "tags": [], - "label": "AddToTimelineButtonProps", - "description": [], - "signature": [ - { - "pluginId": "timelines", - "scope": "public", - "docId": "kibTimelinesPluginApi", - "section": "def-public.AddToTimelineButtonProps", - "text": "AddToTimelineButtonProps" - }, - " extends ", - "HoverActionComponentProps" - ], - "path": "x-pack/plugins/timelines/public/components/hover_actions/actions/add_to_timeline.tsx", - "deprecated": false, - "trackAdoption": false, - "children": [ - { - "parentPluginId": "timelines", - "id": "def-public.AddToTimelineButtonProps.Component", - "type": "CompoundType", - "tags": [], - "label": "Component", - "description": [ - "`Component` is only used with `EuiDataGrid`; the grid keeps a reference to `Component` for show / hide functionality" - ], - "signature": [ - "React.FunctionComponent<(", - "DisambiguateSet", - " & ", - "CommonEuiButtonEmptyProps", - " & { onClick?: React.MouseEventHandler | undefined; } & React.ButtonHTMLAttributes) | (", - "DisambiguateSet", - " & ", - "CommonEuiButtonEmptyProps", - " & { href?: string | undefined; onClick?: React.MouseEventHandler | undefined; } & React.AnchorHTMLAttributes)> | React.FunctionComponent<(", - "DisambiguateSet", - "<", - "EuiButtonIconPropsForAnchor", - ", ", - "EuiButtonIconPropsForButton", - "> & { type?: \"reset\" | \"button\" | \"submit\" | undefined; } & ", - "EuiButtonIconProps", - " & { onClick?: React.MouseEventHandler | undefined; } & React.ButtonHTMLAttributes & { buttonRef?: React.Ref | undefined; }) | (", - "DisambiguateSet", - "<", - "EuiButtonIconPropsForButton", - ", ", - "EuiButtonIconPropsForAnchor", - "> & { type?: string | undefined; } & ", - "EuiButtonIconProps", - " & { href?: string | undefined; onClick?: React.MouseEventHandler | undefined; } & React.AnchorHTMLAttributes & { buttonRef?: React.Ref | undefined; })> | typeof ", - "EuiContextMenuItem", - " | undefined" - ], - "path": "x-pack/plugins/timelines/public/components/hover_actions/actions/add_to_timeline.tsx", - "deprecated": false, - "trackAdoption": false - }, - { - "parentPluginId": "timelines", - "id": "def-public.AddToTimelineButtonProps.draggableId", - "type": "string", - "tags": [], - "label": "draggableId", - "description": [], - "signature": [ - "string | undefined" - ], - "path": "x-pack/plugins/timelines/public/components/hover_actions/actions/add_to_timeline.tsx", - "deprecated": false, - "trackAdoption": false - }, - { - "parentPluginId": "timelines", - "id": "def-public.AddToTimelineButtonProps.dataProvider", - "type": "CompoundType", - "tags": [], - "label": "dataProvider", - "description": [], - "signature": [ - { - "pluginId": "timelines", - "scope": "common", - "docId": "kibTimelinesPluginApi", - "section": "def-common.DataProvider", - "text": "DataProvider" - }, - " | ", - { - "pluginId": "timelines", - "scope": "common", - "docId": "kibTimelinesPluginApi", - "section": "def-common.DataProvider", - "text": "DataProvider" - }, - "[] | undefined" - ], - "path": "x-pack/plugins/timelines/public/components/hover_actions/actions/add_to_timeline.tsx", - "deprecated": false, - "trackAdoption": false - }, - { - "parentPluginId": "timelines", - "id": "def-public.AddToTimelineButtonProps.timelineType", - "type": "string", - "tags": [], - "label": "timelineType", - "description": [], - "signature": [ - "string | undefined" - ], - "path": "x-pack/plugins/timelines/public/components/hover_actions/actions/add_to_timeline.tsx", - "deprecated": false, - "trackAdoption": false - } - ], - "initialIsOpen": false - }, - { - "parentPluginId": "timelines", - "id": "def-public.TableById", - "type": "Interface", - "tags": [], - "label": "TableById", - "description": [ - "A map of id to data table" - ], - "path": "x-pack/plugins/timelines/public/store/t_grid/types.ts", - "deprecated": false, - "trackAdoption": false, - "children": [ - { - "parentPluginId": "timelines", - "id": "def-public.TableById.Unnamed", - "type": "IndexSignature", - "tags": [], - "label": "[id: string]: TGridModel", - "description": [], - "signature": [ - "[id: string]: ", - { - "pluginId": "timelines", - "scope": "public", - "docId": "kibTimelinesPluginApi", - "section": "def-public.TGridModel", - "text": "TGridModel" - } - ], - "path": "x-pack/plugins/timelines/public/store/t_grid/types.ts", - "deprecated": false, - "trackAdoption": false - } - ], - "initialIsOpen": false - }, - { - "parentPluginId": "timelines", - "id": "def-public.TableState", - "type": "Interface", - "tags": [], - "label": "TableState", - "description": [ - "The state of all data tables is stored here" - ], - "path": "x-pack/plugins/timelines/public/store/t_grid/types.ts", - "deprecated": false, - "trackAdoption": false, - "children": [ - { - "parentPluginId": "timelines", - "id": "def-public.TableState.tableById", - "type": "Object", - "tags": [], - "label": "tableById", - "description": [], - "signature": [ - { - "pluginId": "timelines", - "scope": "public", - "docId": "kibTimelinesPluginApi", - "section": "def-public.TableById", - "text": "TableById" - } - ], - "path": "x-pack/plugins/timelines/public/store/t_grid/types.ts", - "deprecated": false, - "trackAdoption": false - } - ], - "initialIsOpen": false - }, - { - "parentPluginId": "timelines", - "id": "def-public.TGridModel", - "type": "Interface", - "tags": [], - "label": "TGridModel", - "description": [], - "signature": [ - { - "pluginId": "timelines", - "scope": "public", - "docId": "kibTimelinesPluginApi", - "section": "def-public.TGridModel", - "text": "TGridModel" - }, - " extends ", - "TGridModelSettings" - ], - "path": "x-pack/plugins/timelines/public/store/t_grid/model.ts", - "deprecated": false, - "trackAdoption": false, - "children": [ - { - "parentPluginId": "timelines", - "id": "def-public.TGridModel.columns", - "type": "Array", - "tags": [], - "label": "columns", - "description": [ - "The columns displayed in the data table" - ], - "signature": [ - "(Pick<", - "EuiDataGridColumn", - ", \"id\" | \"display\" | \"displayAsText\" | \"initialWidth\"> & Pick<", - "EuiDataGridColumn", - ", \"schema\" | \"id\" | \"actions\" | \"display\" | \"defaultSortDirection\" | \"displayAsText\" | \"initialWidth\" | \"isSortable\"> & { aggregatable?: boolean | undefined; tGridCellActions?: ", - "TGridCellAction", - "[] | undefined; category?: string | undefined; columnHeaderType: ", - { - "pluginId": "timelines", - "scope": "common", - "docId": "kibTimelinesPluginApi", - "section": "def-common.ColumnHeaderType", - "text": "ColumnHeaderType" - }, - "; description?: string | null | undefined; esTypes?: string[] | undefined; example?: string | number | null | undefined; format?: string | undefined; linkField?: string | undefined; placeholder?: string | undefined; subType?: ", - { - "pluginId": "@kbn/es-query", - "scope": "common", - "docId": "kibKbnEsQueryPluginApi", - "section": "def-common.IFieldSubType", - "text": "IFieldSubType" - }, - " | undefined; type?: string | undefined; })[]" - ], - "path": "x-pack/plugins/timelines/public/store/t_grid/model.ts", - "deprecated": false, - "trackAdoption": false - }, - { - "parentPluginId": "timelines", - "id": "def-public.TGridModel.dataViewId", - "type": "CompoundType", - "tags": [], - "label": "dataViewId", - "description": [ - "Kibana data view id" - ], - "signature": [ - "string | null" - ], - "path": "x-pack/plugins/timelines/public/store/t_grid/model.ts", - "deprecated": false, - "trackAdoption": false - }, - { - "parentPluginId": "timelines", - "id": "def-public.TGridModel.deletedEventIds", - "type": "Array", - "tags": [], - "label": "deletedEventIds", - "description": [ - "Events to not be rendered" - ], - "signature": [ - "string[]" - ], - "path": "x-pack/plugins/timelines/public/store/t_grid/model.ts", - "deprecated": false, - "trackAdoption": false - }, - { - "parentPluginId": "timelines", - "id": "def-public.TGridModel.expandedDetail", - "type": "Object", - "tags": [], - "label": "expandedDetail", - "description": [ - "This holds the view information for the flyout when viewing data in a consuming view (i.e. hosts page) or the side panel in the primary data view" - ], - "signature": [ - "{ [x: string]: ", - "DataExpandedDetailType", - " | undefined; }" - ], - "path": "x-pack/plugins/timelines/public/store/t_grid/model.ts", - "deprecated": false, - "trackAdoption": false - }, - { - "parentPluginId": "timelines", - "id": "def-public.TGridModel.filters", - "type": "Array", - "tags": [], - "label": "filters", - "description": [], - "signature": [ - { - "pluginId": "@kbn/es-query", - "scope": "common", - "docId": "kibKbnEsQueryPluginApi", - "section": "def-common.Filter", - "text": "Filter" - }, - "[] | undefined" - ], - "path": "x-pack/plugins/timelines/public/store/t_grid/model.ts", - "deprecated": false, - "trackAdoption": false - }, - { - "parentPluginId": "timelines", - "id": "def-public.TGridModel.graphEventId", - "type": "string", - "tags": [], - "label": "graphEventId", - "description": [ - "When non-empty, display a graph view for this event" - ], - "signature": [ - "string | undefined" - ], - "path": "x-pack/plugins/timelines/public/store/t_grid/model.ts", - "deprecated": false, - "trackAdoption": false - }, - { - "parentPluginId": "timelines", - "id": "def-public.TGridModel.id", - "type": "string", - "tags": [], - "label": "id", - "description": [ - "Uniquely identifies the data table" - ], - "path": "x-pack/plugins/timelines/public/store/t_grid/model.ts", - "deprecated": false, - "trackAdoption": false - }, - { - "parentPluginId": "timelines", - "id": "def-public.TGridModel.indexNames", - "type": "Array", - "tags": [], - "label": "indexNames", - "description": [], - "signature": [ - "string[]" - ], - "path": "x-pack/plugins/timelines/public/store/t_grid/model.ts", - "deprecated": false, - "trackAdoption": false - }, - { - "parentPluginId": "timelines", - "id": "def-public.TGridModel.isLoading", - "type": "boolean", - "tags": [], - "label": "isLoading", - "description": [], - "path": "x-pack/plugins/timelines/public/store/t_grid/model.ts", - "deprecated": false, - "trackAdoption": false - }, - { - "parentPluginId": "timelines", - "id": "def-public.TGridModel.isSelectAllChecked", - "type": "boolean", - "tags": [], - "label": "isSelectAllChecked", - "description": [ - "If selectAll checkbox in header is checked" - ], - "path": "x-pack/plugins/timelines/public/store/t_grid/model.ts", - "deprecated": false, - "trackAdoption": false - }, - { - "parentPluginId": "timelines", - "id": "def-public.TGridModel.itemsPerPage", - "type": "number", - "tags": [], - "label": "itemsPerPage", - "description": [ - "The number of items to show in a single page of results" - ], - "path": "x-pack/plugins/timelines/public/store/t_grid/model.ts", - "deprecated": false, - "trackAdoption": false - }, - { - "parentPluginId": "timelines", - "id": "def-public.TGridModel.itemsPerPageOptions", - "type": "Array", - "tags": [], - "label": "itemsPerPageOptions", - "description": [ - "Displays a series of choices that when selected, become the value of `itemsPerPage`" - ], - "signature": [ - "number[]" - ], - "path": "x-pack/plugins/timelines/public/store/t_grid/model.ts", - "deprecated": false, - "trackAdoption": false - }, - { - "parentPluginId": "timelines", - "id": "def-public.TGridModel.loadingEventIds", - "type": "Array", - "tags": [], - "label": "loadingEventIds", - "description": [ - "Events to be rendered as loading" - ], - "signature": [ - "string[]" - ], - "path": "x-pack/plugins/timelines/public/store/t_grid/model.ts", - "deprecated": false, - "trackAdoption": false - }, - { - "parentPluginId": "timelines", - "id": "def-public.TGridModel.selectedEventIds", - "type": "Object", - "tags": [], - "label": "selectedEventIds", - "description": [ - "Events selected on this timeline -- eventId to TimelineNonEcsData[] mapping of data required for bulk actions" - ], - "signature": [ - "{ [x: string]: ", - { - "pluginId": "timelines", - "scope": "common", - "docId": "kibTimelinesPluginApi", - "section": "def-common.TimelineNonEcsData", - "text": "TimelineNonEcsData" - }, - "[]; }" - ], - "path": "x-pack/plugins/timelines/public/store/t_grid/model.ts", - "deprecated": false, - "trackAdoption": false - }, - { - "parentPluginId": "timelines", - "id": "def-public.TGridModel.initialized", - "type": "CompoundType", - "tags": [], - "label": "initialized", - "description": [], - "signature": [ - "boolean | undefined" - ], - "path": "x-pack/plugins/timelines/public/store/t_grid/model.ts", - "deprecated": false, - "trackAdoption": false - }, - { - "parentPluginId": "timelines", - "id": "def-public.TGridModel.sessionViewConfig", - "type": "CompoundType", - "tags": [], - "label": "sessionViewConfig", - "description": [], - "signature": [ - "SessionViewConfig", - " | null" - ], - "path": "x-pack/plugins/timelines/public/store/t_grid/model.ts", - "deprecated": false, - "trackAdoption": false - }, - { - "parentPluginId": "timelines", - "id": "def-public.TGridModel.updated", - "type": "number", - "tags": [], - "label": "updated", - "description": [ - "updated saved object timestamp" - ], - "signature": [ - "number | undefined" - ], - "path": "x-pack/plugins/timelines/public/store/t_grid/model.ts", - "deprecated": false, - "trackAdoption": false - }, - { - "parentPluginId": "timelines", - "id": "def-public.TGridModel.totalCount", - "type": "number", - "tags": [], - "label": "totalCount", - "description": [ - "Total number of fetched events/alerts" - ], - "path": "x-pack/plugins/timelines/public/store/t_grid/model.ts", - "deprecated": false, - "trackAdoption": false - } - ], - "initialIsOpen": false - } - ], - "enums": [], - "misc": [ - { - "parentPluginId": "timelines", - "id": "def-public.ARIA_COLINDEX_ATTRIBUTE", - "type": "string", - "tags": [], - "label": "ARIA_COLINDEX_ATTRIBUTE", - "description": [ - "\nThe name of the ARIA attribute representing a column, used in conjunction with\nthe ARIA: grid role https://www.w3.org/TR/wai-aria-practices-1.1/examples/grid/dataGrids.html" - ], - "signature": [ - "\"aria-colindex\"" - ], - "path": "x-pack/plugins/timelines/common/utils/accessibility/helpers.ts", - "deprecated": false, - "trackAdoption": false, - "initialIsOpen": false - }, - { - "parentPluginId": "timelines", - "id": "def-public.ARIA_ROWINDEX_ATTRIBUTE", - "type": "string", - "tags": [], - "label": "ARIA_ROWINDEX_ATTRIBUTE", - "description": [ - "\nThe name of the ARIA attribute representing a row, used in conjunction with\nthe ARIA: grid role https://www.w3.org/TR/wai-aria-practices-1.1/examples/grid/dataGrids.html" - ], - "signature": [ - "\"aria-rowindex\"" - ], - "path": "x-pack/plugins/timelines/common/utils/accessibility/helpers.ts", - "deprecated": false, - "trackAdoption": false, - "initialIsOpen": false - }, - { - "parentPluginId": "timelines", - "id": "def-public.DATA_COLINDEX_ATTRIBUTE", - "type": "string", - "tags": [], - "label": "DATA_COLINDEX_ATTRIBUTE", - "description": [ - "\nThis alternative attribute to `aria-colindex` is used to decorate the data\nin existing `EuiTable`s to enable keyboard navigation with minimal\nrefactoring of existing code until we're ready to migrate to `EuiDataGrid`.\nIt may be applied directly to keyboard-focusable elements and thus doesn't\nhave exactly the same semantics as `aria-colindex`." - ], - "signature": [ - "\"data-colindex\"" - ], - "path": "x-pack/plugins/timelines/common/utils/accessibility/helpers.ts", - "deprecated": false, - "trackAdoption": false, - "initialIsOpen": false - }, - { - "parentPluginId": "timelines", - "id": "def-public.DATA_ROWINDEX_ATTRIBUTE", - "type": "string", - "tags": [], - "label": "DATA_ROWINDEX_ATTRIBUTE", - "description": [ - "\nThis alternative attribute to `aria-rowindex` is used to decorate the data\nin existing `EuiTable`s to enable keyboard navigation with minimal\nrefactoring of existing code until we're ready to migrate to `EuiDataGrid`.\nIt's typically applied to `` elements via `EuiTable`'s `rowProps` prop." - ], - "signature": [ - "\"data-rowindex\"" - ], - "path": "x-pack/plugins/timelines/common/utils/accessibility/helpers.ts", - "deprecated": false, - "trackAdoption": false, - "initialIsOpen": false - }, - { - "parentPluginId": "timelines", - "id": "def-public.DEFAULT_ACTION_BUTTON_WIDTH", - "type": "number", - "tags": [], - "label": "DEFAULT_ACTION_BUTTON_WIDTH", - "description": [ - "\nThis is the effective width in pixels of an action button used with\n`EuiDataGrid` `leadingControlColumns`. (See Notes below for details)\n\nNotes:\n1) This constant is necessary because `width` is a required property of\n the `EuiDataGridControlColumn` interface, so it must be calculated before\n content is rendered. (The width of a `EuiDataGridControlColumn` does not\n automatically size itself to fit all the content.)\n\n2) This is the *effective* width, because at the time of this writing,\n `EuiButtonIcon` has a `margin-left: -4px`, which is subtracted from the\n `width`" - ], - "path": "x-pack/plugins/timelines/public/components/t_grid/body/constants.ts", - "deprecated": false, - "trackAdoption": false, - "initialIsOpen": false - }, - { - "parentPluginId": "timelines", - "id": "def-public.FIRST_ARIA_INDEX", - "type": "number", - "tags": [], - "label": "FIRST_ARIA_INDEX", - "description": [ - "`aria-colindex` and `aria-rowindex` start at one" - ], - "signature": [ - "1" - ], - "path": "x-pack/plugins/timelines/common/utils/accessibility/helpers.ts", - "deprecated": false, - "trackAdoption": false, - "initialIsOpen": false - }, - { - "parentPluginId": "timelines", - "id": "def-public.OnColumnFocused", - "type": "Type", - "tags": [], - "label": "OnColumnFocused", - "description": [], - "signature": [ - "({ newFocusedColumn, newFocusedColumnAriaColindex, }: { newFocusedColumn: HTMLDivElement | null; newFocusedColumnAriaColindex: number | null; }) => void" - ], - "path": "x-pack/plugins/timelines/common/utils/accessibility/helpers.ts", - "deprecated": false, - "trackAdoption": false, - "returnComment": [], - "children": [ - { - "parentPluginId": "timelines", - "id": "def-public.OnColumnFocused.$1", - "type": "Object", - "tags": [], - "label": "__0", - "description": [], - "signature": [ - "{ newFocusedColumn: HTMLDivElement | null; newFocusedColumnAriaColindex: number | null; }" - ], - "path": "x-pack/plugins/timelines/common/utils/accessibility/helpers.ts", - "deprecated": false, - "trackAdoption": false - } - ], - "initialIsOpen": false - }, - { - "parentPluginId": "timelines", - "id": "def-public.SortDirection", - "type": "Type", - "tags": [], - "label": "SortDirection", - "description": [], - "signature": [ - "\"none\" | \"asc\" | \"desc\" | ", - { - "pluginId": "timelines", - "scope": "common", - "docId": "kibTimelinesPluginApi", - "section": "def-common.Direction", - "text": "Direction" - } - ], - "path": "x-pack/plugins/timelines/common/types/timeline/store.ts", - "deprecated": false, - "trackAdoption": false, - "initialIsOpen": false - }, - { - "parentPluginId": "timelines", - "id": "def-public.State", - "type": "Type", - "tags": [], - "label": "State", - "description": [], - "signature": [ - "EmptyObject", - " & ", - { - "pluginId": "timelines", - "scope": "public", - "docId": "kibTimelinesPluginApi", - "section": "def-public.TableState", - "text": "TableState" - } - ], - "path": "x-pack/plugins/timelines/public/store/t_grid/index.ts", - "deprecated": false, - "trackAdoption": false, - "initialIsOpen": false - }, - { - "parentPluginId": "timelines", - "id": "def-public.SubsetTGridModel", - "type": "Type", - "tags": [], - "label": "SubsetTGridModel", - "description": [], - "signature": [ - "{ readonly columns: (Pick<", - "EuiDataGridColumn", - ", \"id\" | \"display\" | \"displayAsText\" | \"initialWidth\"> & Pick<", - "EuiDataGridColumn", - ", \"schema\" | \"id\" | \"actions\" | \"display\" | \"defaultSortDirection\" | \"displayAsText\" | \"initialWidth\" | \"isSortable\"> & { aggregatable?: boolean | undefined; tGridCellActions?: ", - "TGridCellAction", - "[] | undefined; category?: string | undefined; columnHeaderType: ", - { - "pluginId": "timelines", - "scope": "common", - "docId": "kibTimelinesPluginApi", - "section": "def-common.ColumnHeaderType", - "text": "ColumnHeaderType" - }, - "; description?: string | null | undefined; esTypes?: string[] | undefined; example?: string | number | null | undefined; format?: string | undefined; linkField?: string | undefined; placeholder?: string | undefined; subType?: ", - { - "pluginId": "@kbn/es-query", - "scope": "common", - "docId": "kibKbnEsQueryPluginApi", - "section": "def-common.IFieldSubType", - "text": "IFieldSubType" - }, - " | undefined; type?: string | undefined; })[]; readonly title?: string | undefined; readonly filters?: ", - { - "pluginId": "@kbn/es-query", - "scope": "common", - "docId": "kibKbnEsQueryPluginApi", - "section": "def-common.Filter", - "text": "Filter" - }, - "[] | undefined; readonly dataViewId: string | null; readonly sort: ", - "SortColumnTable", - "[]; readonly defaultColumns: (Pick<", - "EuiDataGridColumn", - ", \"id\" | \"display\" | \"displayAsText\" | \"initialWidth\"> & Pick<", - "EuiDataGridColumn", - ", \"schema\" | \"id\" | \"actions\" | \"display\" | \"defaultSortDirection\" | \"displayAsText\" | \"initialWidth\" | \"isSortable\"> & { aggregatable?: boolean | undefined; tGridCellActions?: ", - "TGridCellAction", - "[] | undefined; category?: string | undefined; columnHeaderType: ", - { - "pluginId": "timelines", - "scope": "common", - "docId": "kibTimelinesPluginApi", - "section": "def-common.ColumnHeaderType", - "text": "ColumnHeaderType" - }, - "; description?: string | null | undefined; esTypes?: string[] | undefined; example?: string | number | null | undefined; format?: string | undefined; linkField?: string | undefined; placeholder?: string | undefined; subType?: ", - { - "pluginId": "@kbn/es-query", - "scope": "common", - "docId": "kibKbnEsQueryPluginApi", - "section": "def-common.IFieldSubType", - "text": "IFieldSubType" - }, - " | undefined; type?: string | undefined; })[]; readonly isLoading: boolean; readonly queryFields: string[]; readonly selectAll: boolean; readonly showCheckboxes: boolean; readonly deletedEventIds: string[]; readonly expandedDetail: Partial>; readonly graphEventId?: string | undefined; readonly indexNames: string[]; readonly isSelectAllChecked: boolean; readonly itemsPerPage: number; readonly itemsPerPageOptions: number[]; readonly loadingEventIds: string[]; readonly selectedEventIds: Record; readonly sessionViewConfig: ", - "SessionViewConfig", - " | null; readonly totalCount: number; }" - ], - "path": "x-pack/plugins/timelines/public/store/t_grid/model.ts", - "deprecated": false, - "trackAdoption": false, - "initialIsOpen": false - }, - { - "parentPluginId": "timelines", - "id": "def-public.TGridType", - "type": "Type", - "tags": [], - "label": "TGridType", - "description": [], - "signature": [ - "\"embedded\"" - ], - "path": "x-pack/plugins/timelines/public/types.ts", - "deprecated": false, - "trackAdoption": false, - "initialIsOpen": false - } - ], - "objects": [ - { - "parentPluginId": "timelines", - "id": "def-public.StatefulEventContext", - "type": "Object", - "tags": [], - "label": "StatefulEventContext", - "description": [], - "signature": [ - "React.Context<", - "StatefulEventContextType", - " | null>" - ], - "path": "x-pack/plugins/timelines/public/components/stateful_event_context.ts", - "deprecated": false, - "trackAdoption": false, - "initialIsOpen": false - }, - { - "parentPluginId": "timelines", - "id": "def-public.TableContext", - "type": "Object", - "tags": [], - "label": "TableContext", - "description": [], - "signature": [ - "React.Context<{ tableId: string | null; }>" - ], - "path": "x-pack/plugins/timelines/public/components/t_grid/shared/index.tsx", - "deprecated": false, - "trackAdoption": false, - "initialIsOpen": false - } - ], - "start": { - "parentPluginId": "timelines", - "id": "def-public.TimelinesUIStart", - "type": "Interface", - "tags": [], - "label": "TimelinesUIStart", - "description": [], - "path": "x-pack/plugins/timelines/public/types.ts", - "deprecated": false, - "trackAdoption": false, - "children": [ - { - "parentPluginId": "timelines", - "id": "def-public.TimelinesUIStart.getHoverActions", - "type": "Function", - "tags": [], - "label": "getHoverActions", - "description": [], - "signature": [ - "() => ", - "HoverActionsConfig" - ], - "path": "x-pack/plugins/timelines/public/types.ts", - "deprecated": false, - "trackAdoption": false, - "children": [], - "returnComment": [] - }, - { - "parentPluginId": "timelines", - "id": "def-public.TimelinesUIStart.getTGrid", - "type": "Function", - "tags": [], - "label": "getTGrid", - "description": [], - "signature": [ - "(props: ", - "GetTGridProps", - ") => React.ReactElement<", - "GetTGridProps", - ", string | React.JSXElementConstructor>" - ], - "path": "x-pack/plugins/timelines/public/types.ts", - "deprecated": false, - "trackAdoption": false, - "children": [ - { - "parentPluginId": "timelines", - "id": "def-public.TimelinesUIStart.getTGrid.$1", - "type": "Uncategorized", - "tags": [], - "label": "props", - "description": [], - "signature": [ - "GetTGridProps", - "" - ], - "path": "x-pack/plugins/timelines/public/types.ts", - "deprecated": false, - "trackAdoption": false, - "isRequired": true - } - ], - "returnComment": [] - }, - { - "parentPluginId": "timelines", - "id": "def-public.TimelinesUIStart.getTGridReducer", - "type": "Function", - "tags": [], - "label": "getTGridReducer", - "description": [], - "signature": [ - "() => any" - ], - "path": "x-pack/plugins/timelines/public/types.ts", - "deprecated": false, - "trackAdoption": false, - "children": [], - "returnComment": [] - }, - { - "parentPluginId": "timelines", - "id": "def-public.TimelinesUIStart.getTimelineReducer", - "type": "Function", - "tags": [], - "label": "getTimelineReducer", - "description": [], - "signature": [ - "() => any" - ], - "path": "x-pack/plugins/timelines/public/types.ts", - "deprecated": false, - "trackAdoption": false, - "children": [], - "returnComment": [] - }, - { - "parentPluginId": "timelines", - "id": "def-public.TimelinesUIStart.getLoadingPanel", - "type": "Function", - "tags": [], - "label": "getLoadingPanel", - "description": [], - "signature": [ - "(props: ", - "LoadingPanelProps", - ") => React.ReactElement<", - "LoadingPanelProps", - ", string | React.JSXElementConstructor>" - ], - "path": "x-pack/plugins/timelines/public/types.ts", - "deprecated": false, - "trackAdoption": false, - "children": [ - { - "parentPluginId": "timelines", - "id": "def-public.TimelinesUIStart.getLoadingPanel.$1", - "type": "Object", - "tags": [], - "label": "props", - "description": [], - "signature": [ - "LoadingPanelProps" - ], - "path": "x-pack/plugins/timelines/public/types.ts", - "deprecated": false, - "trackAdoption": false, - "isRequired": true - } - ], - "returnComment": [] - }, - { - "parentPluginId": "timelines", - "id": "def-public.TimelinesUIStart.getLastUpdated", - "type": "Function", - "tags": [], - "label": "getLastUpdated", - "description": [], - "signature": [ - "(props: ", - "LastUpdatedAtProps", - ") => React.ReactElement<", - "LastUpdatedAtProps", - ", string | React.JSXElementConstructor>" - ], - "path": "x-pack/plugins/timelines/public/types.ts", - "deprecated": false, - "trackAdoption": false, - "children": [ - { - "parentPluginId": "timelines", - "id": "def-public.TimelinesUIStart.getLastUpdated.$1", - "type": "Object", - "tags": [], - "label": "props", - "description": [], - "signature": [ - "LastUpdatedAtProps" - ], - "path": "x-pack/plugins/timelines/public/types.ts", - "deprecated": false, - "trackAdoption": false, - "isRequired": true - } - ], - "returnComment": [] - }, - { - "parentPluginId": "timelines", - "id": "def-public.TimelinesUIStart.getUseAddToTimeline", - "type": "Function", - "tags": [], - "label": "getUseAddToTimeline", - "description": [], - "signature": [ - "() => (props: ", - "UseAddToTimelineProps", - ") => ", - "UseAddToTimeline" - ], - "path": "x-pack/plugins/timelines/public/types.ts", - "deprecated": false, - "trackAdoption": false, - "children": [], - "returnComment": [] - }, - { - "parentPluginId": "timelines", - "id": "def-public.TimelinesUIStart.getUseAddToTimelineSensor", - "type": "Function", - "tags": [], - "label": "getUseAddToTimelineSensor", - "description": [], - "signature": [ - "() => (api: ", - "SensorAPI", - ") => void" - ], - "path": "x-pack/plugins/timelines/public/types.ts", - "deprecated": false, - "trackAdoption": false, - "children": [], - "returnComment": [] - }, - { - "parentPluginId": "timelines", - "id": "def-public.TimelinesUIStart.getUseDraggableKeyboardWrapper", - "type": "Function", - "tags": [], - "label": "getUseDraggableKeyboardWrapper", - "description": [], - "signature": [ - "() => (props: ", - "UseDraggableKeyboardWrapperProps", - ") => ", - "UseDraggableKeyboardWrapper" - ], - "path": "x-pack/plugins/timelines/public/types.ts", - "deprecated": false, - "trackAdoption": false, - "children": [], - "returnComment": [] - }, - { - "parentPluginId": "timelines", - "id": "def-public.TimelinesUIStart.setTGridEmbeddedStore", - "type": "Function", - "tags": [], - "label": "setTGridEmbeddedStore", - "description": [], - "signature": [ - "(store: ", - "Store", - ") => void" - ], - "path": "x-pack/plugins/timelines/public/types.ts", - "deprecated": false, - "trackAdoption": false, - "children": [ - { - "parentPluginId": "timelines", - "id": "def-public.TimelinesUIStart.setTGridEmbeddedStore.$1", - "type": "Object", - "tags": [], - "label": "store", - "description": [], - "signature": [ - "Store", - "" - ], - "path": "x-pack/plugins/timelines/public/types.ts", - "deprecated": false, - "trackAdoption": false, - "isRequired": true - } - ], - "returnComment": [] - } - ], - "lifecycle": "start", - "initialIsOpen": true - } - }, - "server": { - "classes": [], - "functions": [], - "interfaces": [], - "enums": [], - "misc": [], - "objects": [], - "setup": { - "parentPluginId": "timelines", - "id": "def-server.TimelinesPluginUI", - "type": "Interface", - "tags": [], - "label": "TimelinesPluginUI", - "description": [], - "path": "x-pack/plugins/timelines/server/types.ts", - "deprecated": false, - "trackAdoption": false, - "children": [], - "lifecycle": "setup", - "initialIsOpen": true - }, - "start": { - "parentPluginId": "timelines", - "id": "def-server.TimelinesPluginStart", - "type": "Interface", - "tags": [], - "label": "TimelinesPluginStart", - "description": [], - "path": "x-pack/plugins/timelines/server/types.ts", - "deprecated": false, - "trackAdoption": false, - "children": [], - "lifecycle": "start", - "initialIsOpen": true - } - }, - "common": { - "classes": [], - "functions": [], - "interfaces": [ - { - "parentPluginId": "timelines", - "id": "def-common.ActionProps", - "type": "Interface", - "tags": [], - "label": "ActionProps", - "description": [], - "path": "x-pack/plugins/timelines/common/types/timeline/actions/index.ts", - "deprecated": false, - "trackAdoption": false, - "children": [ - { - "parentPluginId": "timelines", - "id": "def-common.ActionProps.action", - "type": "CompoundType", - "tags": [], - "label": "action", - "description": [], - "signature": [ - { - "pluginId": "timelines", - "scope": "common", - "docId": "kibTimelinesPluginApi", - "section": "def-common.RowCellRender", - "text": "RowCellRender" - }, - " | undefined" - ], - "path": "x-pack/plugins/timelines/common/types/timeline/actions/index.ts", - "deprecated": false, - "trackAdoption": false - }, - { - "parentPluginId": "timelines", - "id": "def-common.ActionProps.ariaRowindex", - "type": "number", - "tags": [], - "label": "ariaRowindex", - "description": [], - "path": "x-pack/plugins/timelines/common/types/timeline/actions/index.ts", - "deprecated": false, - "trackAdoption": false - }, - { - "parentPluginId": "timelines", - "id": "def-common.ActionProps.checked", - "type": "boolean", - "tags": [], - "label": "checked", - "description": [], - "path": "x-pack/plugins/timelines/common/types/timeline/actions/index.ts", - "deprecated": false, - "trackAdoption": false - }, - { - "parentPluginId": "timelines", - "id": "def-common.ActionProps.columnId", - "type": "string", - "tags": [], - "label": "columnId", - "description": [], - "path": "x-pack/plugins/timelines/common/types/timeline/actions/index.ts", - "deprecated": false, - "trackAdoption": false - }, - { - "parentPluginId": "timelines", - "id": "def-common.ActionProps.columnValues", - "type": "string", - "tags": [], - "label": "columnValues", - "description": [], - "path": "x-pack/plugins/timelines/common/types/timeline/actions/index.ts", - "deprecated": false, - "trackAdoption": false - }, - { - "parentPluginId": "timelines", - "id": "def-common.ActionProps.data", - "type": "Array", - "tags": [], - "label": "data", - "description": [], - "signature": [ - { - "pluginId": "timelines", - "scope": "common", - "docId": "kibTimelinesPluginApi", - "section": "def-common.TimelineNonEcsData", - "text": "TimelineNonEcsData" - }, - "[]" - ], - "path": "x-pack/plugins/timelines/common/types/timeline/actions/index.ts", - "deprecated": false, - "trackAdoption": false - }, - { - "parentPluginId": "timelines", - "id": "def-common.ActionProps.disabled", - "type": "CompoundType", - "tags": [], - "label": "disabled", - "description": [], - "signature": [ - "boolean | undefined" - ], - "path": "x-pack/plugins/timelines/common/types/timeline/actions/index.ts", - "deprecated": false, - "trackAdoption": false - }, - { - "parentPluginId": "timelines", - "id": "def-common.ActionProps.ecsData", - "type": "Object", - "tags": [], - "label": "ecsData", - "description": [], - "signature": [ - "Ecs" - ], - "path": "x-pack/plugins/timelines/common/types/timeline/actions/index.ts", - "deprecated": false, - "trackAdoption": false - }, - { - "parentPluginId": "timelines", - "id": "def-common.ActionProps.eventId", - "type": "string", - "tags": [], - "label": "eventId", - "description": [], - "path": "x-pack/plugins/timelines/common/types/timeline/actions/index.ts", - "deprecated": false, - "trackAdoption": false - }, - { - "parentPluginId": "timelines", - "id": "def-common.ActionProps.eventIdToNoteIds", - "type": "Object", - "tags": [], - "label": "eventIdToNoteIds", - "description": [], - "signature": [ - "Readonly> | undefined" - ], - "path": "x-pack/plugins/timelines/common/types/timeline/actions/index.ts", - "deprecated": false, - "trackAdoption": false - }, - { - "parentPluginId": "timelines", - "id": "def-common.ActionProps.index", - "type": "number", - "tags": [], - "label": "index", - "description": [], - "path": "x-pack/plugins/timelines/common/types/timeline/actions/index.ts", - "deprecated": false, - "trackAdoption": false - }, - { - "parentPluginId": "timelines", - "id": "def-common.ActionProps.isEventPinned", - "type": "CompoundType", - "tags": [], - "label": "isEventPinned", - "description": [], - "signature": [ - "boolean | undefined" - ], - "path": "x-pack/plugins/timelines/common/types/timeline/actions/index.ts", - "deprecated": false, - "trackAdoption": false - }, - { - "parentPluginId": "timelines", - "id": "def-common.ActionProps.isEventViewer", - "type": "CompoundType", - "tags": [], - "label": "isEventViewer", - "description": [], - "signature": [ - "boolean | undefined" - ], - "path": "x-pack/plugins/timelines/common/types/timeline/actions/index.ts", - "deprecated": false, - "trackAdoption": false - }, - { - "parentPluginId": "timelines", - "id": "def-common.ActionProps.loadingEventIds", - "type": "Object", - "tags": [], - "label": "loadingEventIds", - "description": [], - "signature": [ - "readonly string[]" - ], - "path": "x-pack/plugins/timelines/common/types/timeline/actions/index.ts", - "deprecated": false, - "trackAdoption": false - }, - { - "parentPluginId": "timelines", - "id": "def-common.ActionProps.onEventDetailsPanelOpened", - "type": "Function", - "tags": [], - "label": "onEventDetailsPanelOpened", - "description": [], - "signature": [ - "() => void" - ], - "path": "x-pack/plugins/timelines/common/types/timeline/actions/index.ts", - "deprecated": false, - "trackAdoption": false, - "children": [], - "returnComment": [] - }, - { - "parentPluginId": "timelines", - "id": "def-common.ActionProps.onRowSelected", - "type": "Function", - "tags": [], - "label": "onRowSelected", - "description": [], - "signature": [ - "({ eventIds, isSelected, }: { eventIds: string[]; isSelected: boolean; }) => void" - ], - "path": "x-pack/plugins/timelines/common/types/timeline/actions/index.ts", - "deprecated": false, - "trackAdoption": false, - "returnComment": [], - "children": [ - { - "parentPluginId": "timelines", - "id": "def-common.ActionProps.onRowSelected.$1", - "type": "Object", - "tags": [], - "label": "__0", - "description": [], - "signature": [ - "{ eventIds: string[]; isSelected: boolean; }" - ], - "path": "x-pack/plugins/timelines/common/types/timeline/store.ts", - "deprecated": false, - "trackAdoption": false - } - ] - }, - { - "parentPluginId": "timelines", - "id": "def-common.ActionProps.onRuleChange", - "type": "Function", - "tags": [], - "label": "onRuleChange", - "description": [], - "signature": [ - "(() => void) | undefined" - ], - "path": "x-pack/plugins/timelines/common/types/timeline/actions/index.ts", - "deprecated": false, - "trackAdoption": false, - "children": [], - "returnComment": [] - }, - { - "parentPluginId": "timelines", - "id": "def-common.ActionProps.refetch", - "type": "Function", - "tags": [], - "label": "refetch", - "description": [], - "signature": [ - "(() => void) | undefined" - ], - "path": "x-pack/plugins/timelines/common/types/timeline/actions/index.ts", - "deprecated": false, - "trackAdoption": false, - "children": [], - "returnComment": [] - }, - { - "parentPluginId": "timelines", - "id": "def-common.ActionProps.rowIndex", - "type": "number", - "tags": [], - "label": "rowIndex", - "description": [], - "path": "x-pack/plugins/timelines/common/types/timeline/actions/index.ts", - "deprecated": false, - "trackAdoption": false - }, - { - "parentPluginId": "timelines", - "id": "def-common.ActionProps.setEventsDeleted", - "type": "Function", - "tags": [], - "label": "setEventsDeleted", - "description": [], - "signature": [ - "(params: { eventIds: string[]; isDeleted: boolean; }) => void" - ], - "path": "x-pack/plugins/timelines/common/types/timeline/actions/index.ts", - "deprecated": false, - "trackAdoption": false, - "returnComment": [], - "children": [ - { - "parentPluginId": "timelines", - "id": "def-common.ActionProps.setEventsDeleted.$1", - "type": "Object", - "tags": [], - "label": "params", - "description": [], - "signature": [ - "{ eventIds: string[]; isDeleted: boolean; }" - ], - "path": "x-pack/plugins/timelines/common/types/timeline/actions/index.ts", - "deprecated": false, - "trackAdoption": false - } - ] - }, - { - "parentPluginId": "timelines", - "id": "def-common.ActionProps.setEventsLoading", - "type": "Function", - "tags": [], - "label": "setEventsLoading", - "description": [], - "signature": [ - "(params: { eventIds: string[]; isLoading: boolean; }) => void" - ], - "path": "x-pack/plugins/timelines/common/types/timeline/actions/index.ts", - "deprecated": false, - "trackAdoption": false, - "returnComment": [], - "children": [ - { - "parentPluginId": "timelines", - "id": "def-common.ActionProps.setEventsLoading.$1", - "type": "Object", - "tags": [], - "label": "params", - "description": [], - "signature": [ - "{ eventIds: string[]; isLoading: boolean; }" - ], - "path": "x-pack/plugins/timelines/common/types/timeline/actions/index.ts", - "deprecated": false, - "trackAdoption": false - } - ] - }, - { - "parentPluginId": "timelines", - "id": "def-common.ActionProps.showCheckboxes", - "type": "boolean", - "tags": [], - "label": "showCheckboxes", - "description": [], - "path": "x-pack/plugins/timelines/common/types/timeline/actions/index.ts", - "deprecated": false, - "trackAdoption": false - }, - { - "parentPluginId": "timelines", - "id": "def-common.ActionProps.showNotes", - "type": "CompoundType", - "tags": [], - "label": "showNotes", - "description": [], - "signature": [ - "boolean | undefined" - ], - "path": "x-pack/plugins/timelines/common/types/timeline/actions/index.ts", - "deprecated": false, - "trackAdoption": false - }, - { - "parentPluginId": "timelines", - "id": "def-common.ActionProps.tabType", - "type": "string", - "tags": [], - "label": "tabType", - "description": [], - "signature": [ - "string | undefined" - ], - "path": "x-pack/plugins/timelines/common/types/timeline/actions/index.ts", - "deprecated": false, - "trackAdoption": false - }, - { - "parentPluginId": "timelines", - "id": "def-common.ActionProps.timelineId", - "type": "string", - "tags": [], - "label": "timelineId", - "description": [], - "path": "x-pack/plugins/timelines/common/types/timeline/actions/index.ts", - "deprecated": false, - "trackAdoption": false - }, - { - "parentPluginId": "timelines", - "id": "def-common.ActionProps.toggleShowNotes", - "type": "Function", - "tags": [], - "label": "toggleShowNotes", - "description": [], - "signature": [ - "(() => void) | undefined" - ], - "path": "x-pack/plugins/timelines/common/types/timeline/actions/index.ts", - "deprecated": false, - "trackAdoption": false, - "children": [], - "returnComment": [] - }, - { - "parentPluginId": "timelines", - "id": "def-common.ActionProps.width", - "type": "number", - "tags": [], - "label": "width", - "description": [], - "signature": [ - "number | undefined" - ], - "path": "x-pack/plugins/timelines/common/types/timeline/actions/index.ts", - "deprecated": false, - "trackAdoption": false - } - ], - "initialIsOpen": false - }, + } + ], + "objects": [], + "start": { + "parentPluginId": "timelines", + "id": "def-public.TimelinesUIStart", + "type": "Interface", + "tags": [], + "label": "TimelinesUIStart", + "description": [], + "path": "x-pack/plugins/timelines/public/types.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "timelines", + "id": "def-public.TimelinesUIStart.getHoverActions", + "type": "Function", + "tags": [], + "label": "getHoverActions", + "description": [], + "signature": [ + "() => ", + "HoverActionsConfig" + ], + "path": "x-pack/plugins/timelines/public/types.ts", + "deprecated": false, + "trackAdoption": false, + "children": [], + "returnComment": [] + }, + { + "parentPluginId": "timelines", + "id": "def-public.TimelinesUIStart.getTimelineReducer", + "type": "Function", + "tags": [], + "label": "getTimelineReducer", + "description": [], + "signature": [ + "() => any" + ], + "path": "x-pack/plugins/timelines/public/types.ts", + "deprecated": false, + "trackAdoption": false, + "children": [], + "returnComment": [] + }, + { + "parentPluginId": "timelines", + "id": "def-public.TimelinesUIStart.getLoadingPanel", + "type": "Function", + "tags": [], + "label": "getLoadingPanel", + "description": [], + "signature": [ + "(props: ", + "LoadingPanelProps", + ") => React.ReactElement<", + "LoadingPanelProps", + ", string | React.JSXElementConstructor>" + ], + "path": "x-pack/plugins/timelines/public/types.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "timelines", + "id": "def-public.TimelinesUIStart.getLoadingPanel.$1", + "type": "Object", + "tags": [], + "label": "props", + "description": [], + "signature": [ + "LoadingPanelProps" + ], + "path": "x-pack/plugins/timelines/public/types.ts", + "deprecated": false, + "trackAdoption": false, + "isRequired": true + } + ], + "returnComment": [] + }, + { + "parentPluginId": "timelines", + "id": "def-public.TimelinesUIStart.getLastUpdated", + "type": "Function", + "tags": [], + "label": "getLastUpdated", + "description": [], + "signature": [ + "(props: ", + "LastUpdatedAtProps", + ") => React.ReactElement<", + "LastUpdatedAtProps", + ", string | React.JSXElementConstructor>" + ], + "path": "x-pack/plugins/timelines/public/types.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "timelines", + "id": "def-public.TimelinesUIStart.getLastUpdated.$1", + "type": "Object", + "tags": [], + "label": "props", + "description": [], + "signature": [ + "LastUpdatedAtProps" + ], + "path": "x-pack/plugins/timelines/public/types.ts", + "deprecated": false, + "trackAdoption": false, + "isRequired": true + } + ], + "returnComment": [] + }, + { + "parentPluginId": "timelines", + "id": "def-public.TimelinesUIStart.getUseAddToTimeline", + "type": "Function", + "tags": [], + "label": "getUseAddToTimeline", + "description": [], + "signature": [ + "() => (props: ", + "UseAddToTimelineProps", + ") => ", + "UseAddToTimeline" + ], + "path": "x-pack/plugins/timelines/public/types.ts", + "deprecated": false, + "trackAdoption": false, + "children": [], + "returnComment": [] + }, + { + "parentPluginId": "timelines", + "id": "def-public.TimelinesUIStart.getUseAddToTimelineSensor", + "type": "Function", + "tags": [], + "label": "getUseAddToTimelineSensor", + "description": [], + "signature": [ + "() => (api: ", + "SensorAPI", + ") => void" + ], + "path": "x-pack/plugins/timelines/public/types.ts", + "deprecated": false, + "trackAdoption": false, + "children": [], + "returnComment": [] + }, + { + "parentPluginId": "timelines", + "id": "def-public.TimelinesUIStart.setTimelineEmbeddedStore", + "type": "Function", + "tags": [], + "label": "setTimelineEmbeddedStore", + "description": [], + "signature": [ + "(store: ", + "Store", + ") => void" + ], + "path": "x-pack/plugins/timelines/public/types.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "timelines", + "id": "def-public.TimelinesUIStart.setTimelineEmbeddedStore.$1", + "type": "Object", + "tags": [], + "label": "store", + "description": [], + "signature": [ + "Store", + "" + ], + "path": "x-pack/plugins/timelines/public/types.ts", + "deprecated": false, + "trackAdoption": false, + "isRequired": true + } + ], + "returnComment": [] + } + ], + "lifecycle": "start", + "initialIsOpen": true + } + }, + "server": { + "classes": [], + "functions": [], + "interfaces": [], + "enums": [], + "misc": [], + "objects": [], + "setup": { + "parentPluginId": "timelines", + "id": "def-server.TimelinesPluginUI", + "type": "Interface", + "tags": [], + "label": "TimelinesPluginUI", + "description": [], + "path": "x-pack/plugins/timelines/server/types.ts", + "deprecated": false, + "trackAdoption": false, + "children": [], + "lifecycle": "setup", + "initialIsOpen": true + }, + "start": { + "parentPluginId": "timelines", + "id": "def-server.TimelinesPluginStart", + "type": "Interface", + "tags": [], + "label": "TimelinesPluginStart", + "description": [], + "path": "x-pack/plugins/timelines/server/types.ts", + "deprecated": false, + "trackAdoption": false, + "children": [], + "lifecycle": "start", + "initialIsOpen": true + } + }, + "common": { + "classes": [], + "functions": [], + "interfaces": [ { "parentPluginId": "timelines", "id": "def-common.BrowserField", @@ -4372,297 +1499,74 @@ "description": [], "path": "x-pack/plugins/timelines/common/search_strategy/index_fields/index.ts", "deprecated": false, - "trackAdoption": false - }, - { - "parentPluginId": "timelines", - "id": "def-common.BrowserField.esTypes", - "type": "Array", - "tags": [], - "label": "esTypes", - "description": [], - "signature": [ - "string[] | undefined" - ], - "path": "x-pack/plugins/timelines/common/search_strategy/index_fields/index.ts", - "deprecated": false, - "trackAdoption": false - }, - { - "parentPluginId": "timelines", - "id": "def-common.BrowserField.subType", - "type": "CompoundType", - "tags": [], - "label": "subType", - "description": [], - "signature": [ - { - "pluginId": "@kbn/es-query", - "scope": "common", - "docId": "kibKbnEsQueryPluginApi", - "section": "def-common.IFieldSubType", - "text": "IFieldSubType" - }, - " | undefined" - ], - "path": "x-pack/plugins/timelines/common/search_strategy/index_fields/index.ts", - "deprecated": false, - "trackAdoption": false - }, - { - "parentPluginId": "timelines", - "id": "def-common.BrowserField.readFromDocValues", - "type": "boolean", - "tags": [], - "label": "readFromDocValues", - "description": [], - "path": "x-pack/plugins/timelines/common/search_strategy/index_fields/index.ts", - "deprecated": false, - "trackAdoption": false - }, - { - "parentPluginId": "timelines", - "id": "def-common.BrowserField.runtimeField", - "type": "Object", - "tags": [], - "label": "runtimeField", - "description": [], - "signature": [ - { - "pluginId": "dataViews", - "scope": "common", - "docId": "kibDataViewsPluginApi", - "section": "def-common.RuntimeField", - "text": "RuntimeField" - }, - " | undefined" - ], - "path": "x-pack/plugins/timelines/common/search_strategy/index_fields/index.ts", - "deprecated": false, - "trackAdoption": false - } - ], - "initialIsOpen": false - }, - { - "parentPluginId": "timelines", - "id": "def-common.ColumnRenderer", - "type": "Interface", - "tags": [], - "label": "ColumnRenderer", - "description": [], - "path": "x-pack/plugins/timelines/common/types/timeline/columns/index.tsx", - "deprecated": false, - "trackAdoption": false, - "children": [ - { - "parentPluginId": "timelines", - "id": "def-common.ColumnRenderer.isInstance", - "type": "Function", - "tags": [], - "label": "isInstance", - "description": [], - "signature": [ - "(columnName: string, data: ", - { - "pluginId": "timelines", - "scope": "common", - "docId": "kibTimelinesPluginApi", - "section": "def-common.TimelineNonEcsData", - "text": "TimelineNonEcsData" - }, - "[]) => boolean" - ], - "path": "x-pack/plugins/timelines/common/types/timeline/columns/index.tsx", - "deprecated": false, - "trackAdoption": false, - "children": [ - { - "parentPluginId": "timelines", - "id": "def-common.ColumnRenderer.isInstance.$1", - "type": "string", - "tags": [], - "label": "columnName", - "description": [], - "signature": [ - "string" - ], - "path": "x-pack/plugins/timelines/common/types/timeline/columns/index.tsx", - "deprecated": false, - "trackAdoption": false, - "isRequired": true - }, - { - "parentPluginId": "timelines", - "id": "def-common.ColumnRenderer.isInstance.$2", - "type": "Array", - "tags": [], - "label": "data", - "description": [], - "signature": [ - { - "pluginId": "timelines", - "scope": "common", - "docId": "kibTimelinesPluginApi", - "section": "def-common.TimelineNonEcsData", - "text": "TimelineNonEcsData" - }, - "[]" - ], - "path": "x-pack/plugins/timelines/common/types/timeline/columns/index.tsx", - "deprecated": false, - "trackAdoption": false, - "isRequired": true - } - ], - "returnComment": [] - }, - { - "parentPluginId": "timelines", - "id": "def-common.ColumnRenderer.renderColumn", - "type": "Function", - "tags": [], - "label": "renderColumn", - "description": [], - "signature": [ - "({ columnName, eventId, field, timelineId, truncate, values, linkValues, }: { columnName: string; eventId: string; field: ", - { - "pluginId": "timelines", - "scope": "common", - "docId": "kibTimelinesPluginApi", - "section": "def-common.ColumnHeaderOptions", - "text": "ColumnHeaderOptions" - }, - "; timelineId: string; truncate?: boolean | undefined; values: string[] | null | undefined; linkValues?: string[] | null | undefined; }) => React.ReactNode" - ], - "path": "x-pack/plugins/timelines/common/types/timeline/columns/index.tsx", - "deprecated": false, - "trackAdoption": false, - "children": [ - { - "parentPluginId": "timelines", - "id": "def-common.ColumnRenderer.renderColumn.$1", - "type": "Object", - "tags": [], - "label": "{\n columnName,\n eventId,\n field,\n timelineId,\n truncate,\n values,\n linkValues,\n }", - "description": [], - "path": "x-pack/plugins/timelines/common/types/timeline/columns/index.tsx", - "deprecated": false, - "trackAdoption": false, - "children": [ - { - "parentPluginId": "timelines", - "id": "def-common.ColumnRenderer.renderColumn.$1.columnName", - "type": "string", - "tags": [], - "label": "columnName", - "description": [], - "path": "x-pack/plugins/timelines/common/types/timeline/columns/index.tsx", - "deprecated": false, - "trackAdoption": false - }, - { - "parentPluginId": "timelines", - "id": "def-common.ColumnRenderer.renderColumn.$1.eventId", - "type": "string", - "tags": [], - "label": "eventId", - "description": [], - "path": "x-pack/plugins/timelines/common/types/timeline/columns/index.tsx", - "deprecated": false, - "trackAdoption": false - }, - { - "parentPluginId": "timelines", - "id": "def-common.ColumnRenderer.renderColumn.$1.field", - "type": "CompoundType", - "tags": [], - "label": "field", - "description": [], - "signature": [ - "Pick<", - "EuiDataGridColumn", - ", \"schema\" | \"id\" | \"actions\" | \"display\" | \"defaultSortDirection\" | \"displayAsText\" | \"initialWidth\" | \"isSortable\"> & { aggregatable?: boolean | undefined; tGridCellActions?: ", - "TGridCellAction", - "[] | undefined; category?: string | undefined; columnHeaderType: ", - { - "pluginId": "timelines", - "scope": "common", - "docId": "kibTimelinesPluginApi", - "section": "def-common.ColumnHeaderType", - "text": "ColumnHeaderType" - }, - "; description?: string | null | undefined; esTypes?: string[] | undefined; example?: string | number | null | undefined; format?: string | undefined; linkField?: string | undefined; placeholder?: string | undefined; subType?: ", - { - "pluginId": "@kbn/es-query", - "scope": "common", - "docId": "kibKbnEsQueryPluginApi", - "section": "def-common.IFieldSubType", - "text": "IFieldSubType" - }, - " | undefined; type?: string | undefined; }" - ], - "path": "x-pack/plugins/timelines/common/types/timeline/columns/index.tsx", - "deprecated": false, - "trackAdoption": false - }, - { - "parentPluginId": "timelines", - "id": "def-common.ColumnRenderer.renderColumn.$1.timelineId", - "type": "string", - "tags": [], - "label": "timelineId", - "description": [], - "path": "x-pack/plugins/timelines/common/types/timeline/columns/index.tsx", - "deprecated": false, - "trackAdoption": false - }, - { - "parentPluginId": "timelines", - "id": "def-common.ColumnRenderer.renderColumn.$1.truncate", - "type": "CompoundType", - "tags": [], - "label": "truncate", - "description": [], - "signature": [ - "boolean | undefined" - ], - "path": "x-pack/plugins/timelines/common/types/timeline/columns/index.tsx", - "deprecated": false, - "trackAdoption": false - }, - { - "parentPluginId": "timelines", - "id": "def-common.ColumnRenderer.renderColumn.$1.values", - "type": "CompoundType", - "tags": [], - "label": "values", - "description": [], - "signature": [ - "string[] | null | undefined" - ], - "path": "x-pack/plugins/timelines/common/types/timeline/columns/index.tsx", - "deprecated": false, - "trackAdoption": false - }, - { - "parentPluginId": "timelines", - "id": "def-common.ColumnRenderer.renderColumn.$1.linkValues", - "type": "CompoundType", - "tags": [], - "label": "linkValues", - "description": [], - "signature": [ - "string[] | null | undefined" - ], - "path": "x-pack/plugins/timelines/common/types/timeline/columns/index.tsx", - "deprecated": false, - "trackAdoption": false - } - ] - } + "trackAdoption": false + }, + { + "parentPluginId": "timelines", + "id": "def-common.BrowserField.esTypes", + "type": "Array", + "tags": [], + "label": "esTypes", + "description": [], + "signature": [ + "string[] | undefined" ], - "returnComment": [] + "path": "x-pack/plugins/timelines/common/search_strategy/index_fields/index.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "timelines", + "id": "def-common.BrowserField.subType", + "type": "CompoundType", + "tags": [], + "label": "subType", + "description": [], + "signature": [ + { + "pluginId": "@kbn/es-query", + "scope": "common", + "docId": "kibKbnEsQueryPluginApi", + "section": "def-common.IFieldSubType", + "text": "IFieldSubType" + }, + " | undefined" + ], + "path": "x-pack/plugins/timelines/common/search_strategy/index_fields/index.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "timelines", + "id": "def-common.BrowserField.readFromDocValues", + "type": "boolean", + "tags": [], + "label": "readFromDocValues", + "description": [], + "path": "x-pack/plugins/timelines/common/search_strategy/index_fields/index.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "timelines", + "id": "def-common.BrowserField.runtimeField", + "type": "Object", + "tags": [], + "label": "runtimeField", + "description": [], + "signature": [ + { + "pluginId": "dataViews", + "scope": "common", + "docId": "kibDataViewsPluginApi", + "section": "def-common.RuntimeField", + "text": "RuntimeField" + }, + " | undefined" + ], + "path": "x-pack/plugins/timelines/common/search_strategy/index_fields/index.ts", + "deprecated": false, + "trackAdoption": false } ], "initialIsOpen": false @@ -4972,335 +1876,121 @@ }, { "parentPluginId": "timelines", - "id": "def-common.EqlOptionsSelected.query", - "type": "string", - "tags": [], - "label": "query", - "description": [], - "signature": [ - "string | undefined" - ], - "path": "x-pack/plugins/timelines/common/search_strategy/timeline/events/eql/index.ts", - "deprecated": false, - "trackAdoption": false - }, - { - "parentPluginId": "timelines", - "id": "def-common.EqlOptionsSelected.size", - "type": "number", - "tags": [], - "label": "size", - "description": [], - "signature": [ - "number | undefined" - ], - "path": "x-pack/plugins/timelines/common/search_strategy/timeline/events/eql/index.ts", - "deprecated": false, - "trackAdoption": false - } - ], - "initialIsOpen": false - }, - { - "parentPluginId": "timelines", - "id": "def-common.FieldInfo", - "type": "Interface", - "tags": [], - "label": "FieldInfo", - "description": [], - "path": "x-pack/plugins/timelines/common/search_strategy/index_fields/index.ts", - "deprecated": false, - "trackAdoption": false, - "children": [ - { - "parentPluginId": "timelines", - "id": "def-common.FieldInfo.category", - "type": "string", - "tags": [], - "label": "category", - "description": [], - "path": "x-pack/plugins/timelines/common/search_strategy/index_fields/index.ts", - "deprecated": false, - "trackAdoption": false - }, - { - "parentPluginId": "timelines", - "id": "def-common.FieldInfo.description", - "type": "string", - "tags": [], - "label": "description", - "description": [], - "signature": [ - "string | undefined" - ], - "path": "x-pack/plugins/timelines/common/search_strategy/index_fields/index.ts", - "deprecated": false, - "trackAdoption": false - }, - { - "parentPluginId": "timelines", - "id": "def-common.FieldInfo.example", - "type": "CompoundType", - "tags": [], - "label": "example", - "description": [], - "signature": [ - "string | number | undefined" - ], - "path": "x-pack/plugins/timelines/common/search_strategy/index_fields/index.ts", - "deprecated": false, - "trackAdoption": false - }, - { - "parentPluginId": "timelines", - "id": "def-common.FieldInfo.format", - "type": "string", - "tags": [], - "label": "format", - "description": [], - "signature": [ - "string | undefined" - ], - "path": "x-pack/plugins/timelines/common/search_strategy/index_fields/index.ts", - "deprecated": false, - "trackAdoption": false - }, - { - "parentPluginId": "timelines", - "id": "def-common.FieldInfo.name", - "type": "string", - "tags": [], - "label": "name", - "description": [], - "path": "x-pack/plugins/timelines/common/search_strategy/index_fields/index.ts", - "deprecated": false, - "trackAdoption": false - }, - { - "parentPluginId": "timelines", - "id": "def-common.FieldInfo.type", - "type": "string", - "tags": [], - "label": "type", - "description": [], - "signature": [ - "string | undefined" - ], - "path": "x-pack/plugins/timelines/common/search_strategy/index_fields/index.ts", - "deprecated": false, - "trackAdoption": false - } - ], - "initialIsOpen": false - }, - { - "parentPluginId": "timelines", - "id": "def-common.HeaderActionProps", - "type": "Interface", - "tags": [], - "label": "HeaderActionProps", - "description": [], - "path": "x-pack/plugins/timelines/common/types/timeline/actions/index.ts", - "deprecated": false, - "trackAdoption": false, - "children": [ - { - "parentPluginId": "timelines", - "id": "def-common.HeaderActionProps.width", - "type": "number", - "tags": [], - "label": "width", - "description": [], - "path": "x-pack/plugins/timelines/common/types/timeline/actions/index.ts", - "deprecated": false, - "trackAdoption": false - }, - { - "parentPluginId": "timelines", - "id": "def-common.HeaderActionProps.browserFields", - "type": "Object", - "tags": [], - "label": "browserFields", - "description": [], - "signature": [ - "{ readonly [x: string]: Partial<", - { - "pluginId": "timelines", - "scope": "common", - "docId": "kibTimelinesPluginApi", - "section": "def-common.BrowserField", - "text": "BrowserField" - }, - ">; }" - ], - "path": "x-pack/plugins/timelines/common/types/timeline/actions/index.ts", - "deprecated": false, - "trackAdoption": false - }, - { - "parentPluginId": "timelines", - "id": "def-common.HeaderActionProps.columnHeaders", - "type": "Array", - "tags": [], - "label": "columnHeaders", - "description": [], - "signature": [ - { - "pluginId": "timelines", - "scope": "common", - "docId": "kibTimelinesPluginApi", - "section": "def-common.ColumnHeaderOptions", - "text": "ColumnHeaderOptions" - }, - "[]" - ], - "path": "x-pack/plugins/timelines/common/types/timeline/actions/index.ts", - "deprecated": false, - "trackAdoption": false - }, - { - "parentPluginId": "timelines", - "id": "def-common.HeaderActionProps.fieldBrowserOptions", - "type": "Object", - "tags": [], - "label": "fieldBrowserOptions", - "description": [], - "signature": [ - { - "pluginId": "triggersActionsUi", - "scope": "public", - "docId": "kibTriggersActionsUiPluginApi", - "section": "def-public.FieldBrowserOptions", - "text": "FieldBrowserOptions" - }, - " | undefined" - ], - "path": "x-pack/plugins/timelines/common/types/timeline/actions/index.ts", - "deprecated": false, - "trackAdoption": false - }, - { - "parentPluginId": "timelines", - "id": "def-common.HeaderActionProps.isEventViewer", - "type": "CompoundType", + "id": "def-common.EqlOptionsSelected.query", + "type": "string", "tags": [], - "label": "isEventViewer", + "label": "query", "description": [], "signature": [ - "boolean | undefined" + "string | undefined" ], - "path": "x-pack/plugins/timelines/common/types/timeline/actions/index.ts", + "path": "x-pack/plugins/timelines/common/search_strategy/timeline/events/eql/index.ts", "deprecated": false, "trackAdoption": false }, { "parentPluginId": "timelines", - "id": "def-common.HeaderActionProps.isSelectAllChecked", - "type": "boolean", + "id": "def-common.EqlOptionsSelected.size", + "type": "number", "tags": [], - "label": "isSelectAllChecked", + "label": "size", "description": [], - "path": "x-pack/plugins/timelines/common/types/timeline/actions/index.ts", + "signature": [ + "number | undefined" + ], + "path": "x-pack/plugins/timelines/common/search_strategy/timeline/events/eql/index.ts", "deprecated": false, "trackAdoption": false - }, + } + ], + "initialIsOpen": false + }, + { + "parentPluginId": "timelines", + "id": "def-common.FieldInfo", + "type": "Interface", + "tags": [], + "label": "FieldInfo", + "description": [], + "path": "x-pack/plugins/timelines/common/search_strategy/index_fields/index.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ { "parentPluginId": "timelines", - "id": "def-common.HeaderActionProps.onSelectAll", - "type": "Function", + "id": "def-common.FieldInfo.category", + "type": "string", "tags": [], - "label": "onSelectAll", + "label": "category", "description": [], - "signature": [ - "({ isSelected }: { isSelected: boolean; }) => void" - ], - "path": "x-pack/plugins/timelines/common/types/timeline/actions/index.ts", + "path": "x-pack/plugins/timelines/common/search_strategy/index_fields/index.ts", "deprecated": false, - "trackAdoption": false, - "children": [ - { - "parentPluginId": "timelines", - "id": "def-common.HeaderActionProps.onSelectAll.$1", - "type": "Object", - "tags": [], - "label": "{ isSelected }", - "description": [], - "path": "x-pack/plugins/timelines/common/types/timeline/actions/index.ts", - "deprecated": false, - "trackAdoption": false, - "children": [ - { - "parentPluginId": "timelines", - "id": "def-common.HeaderActionProps.onSelectAll.$1.isSelected", - "type": "boolean", - "tags": [], - "label": "isSelected", - "description": [], - "path": "x-pack/plugins/timelines/common/types/timeline/actions/index.ts", - "deprecated": false, - "trackAdoption": false - } - ] - } - ], - "returnComment": [] + "trackAdoption": false }, { "parentPluginId": "timelines", - "id": "def-common.HeaderActionProps.showEventsSelect", - "type": "boolean", + "id": "def-common.FieldInfo.description", + "type": "string", "tags": [], - "label": "showEventsSelect", + "label": "description", "description": [], - "path": "x-pack/plugins/timelines/common/types/timeline/actions/index.ts", + "signature": [ + "string | undefined" + ], + "path": "x-pack/plugins/timelines/common/search_strategy/index_fields/index.ts", "deprecated": false, "trackAdoption": false }, { "parentPluginId": "timelines", - "id": "def-common.HeaderActionProps.showSelectAllCheckbox", - "type": "boolean", + "id": "def-common.FieldInfo.example", + "type": "CompoundType", "tags": [], - "label": "showSelectAllCheckbox", + "label": "example", "description": [], - "path": "x-pack/plugins/timelines/common/types/timeline/actions/index.ts", + "signature": [ + "string | number | undefined" + ], + "path": "x-pack/plugins/timelines/common/search_strategy/index_fields/index.ts", "deprecated": false, "trackAdoption": false }, { "parentPluginId": "timelines", - "id": "def-common.HeaderActionProps.sort", - "type": "Array", + "id": "def-common.FieldInfo.format", + "type": "string", "tags": [], - "label": "sort", + "label": "format", "description": [], "signature": [ - "SortColumnTable", - "[]" + "string | undefined" ], - "path": "x-pack/plugins/timelines/common/types/timeline/actions/index.ts", + "path": "x-pack/plugins/timelines/common/search_strategy/index_fields/index.ts", "deprecated": false, "trackAdoption": false }, { "parentPluginId": "timelines", - "id": "def-common.HeaderActionProps.tabType", + "id": "def-common.FieldInfo.name", "type": "string", "tags": [], - "label": "tabType", + "label": "name", "description": [], - "path": "x-pack/plugins/timelines/common/types/timeline/actions/index.ts", + "path": "x-pack/plugins/timelines/common/search_strategy/index_fields/index.ts", "deprecated": false, "trackAdoption": false }, { "parentPluginId": "timelines", - "id": "def-common.HeaderActionProps.timelineId", + "id": "def-common.FieldInfo.type", "type": "string", "tags": [], - "label": "timelineId", + "label": "type", "description": [], - "path": "x-pack/plugins/timelines/common/types/timeline/actions/index.ts", + "signature": [ + "string | undefined" + ], + "path": "x-pack/plugins/timelines/common/search_strategy/index_fields/index.ts", "deprecated": false, "trackAdoption": false } @@ -6312,8 +3002,7 @@ "label": "filterStatus", "description": [], "signature": [ - "AlertStatus", - " | undefined" + "AlertWorkflowStatus | undefined" ], "path": "x-pack/plugins/timelines/common/search_strategy/timeline/events/all/index.ts", "deprecated": false, @@ -7211,21 +3900,6 @@ } ], "misc": [ - { - "parentPluginId": "timelines", - "id": "def-common.AlertWorkflowStatus", - "type": "Type", - "tags": [], - "label": "AlertWorkflowStatus", - "description": [], - "signature": [ - "\"open\" | \"closed\" | \"acknowledged\"" - ], - "path": "x-pack/plugins/timelines/common/types/timeline/actions/index.ts", - "deprecated": false, - "trackAdoption": false, - "initialIsOpen": false - }, { "parentPluginId": "timelines", "id": "def-common.BeatFields", @@ -7325,7 +3999,7 @@ "section": "def-common.RowRenderer", "text": "RowRenderer" }, - "[] | undefined; setFlyoutAlert?: ((data: any) => void) | undefined; scopeId: string; truncate?: boolean | undefined; key?: string | undefined; closeCellPopover?: (() => void) | undefined; }" + "[] | undefined; setFlyoutAlert?: ((data: any) => void) | undefined; scopeId: string; truncate?: boolean | undefined; key?: string | undefined; closeCellPopover?: (() => void) | undefined; enableActions?: boolean | undefined; }" ], "path": "x-pack/plugins/timelines/common/types/timeline/cells/index.ts", "deprecated": false, @@ -7344,16 +4018,10 @@ "signature": [ "Pick<", "EuiDataGridColumn", - ", \"schema\" | \"id\" | \"actions\" | \"display\" | \"defaultSortDirection\" | \"displayAsText\" | \"initialWidth\" | \"isSortable\"> & { aggregatable?: boolean | undefined; tGridCellActions?: ", - "TGridCellAction", + ", \"schema\" | \"id\" | \"actions\" | \"display\" | \"defaultSortDirection\" | \"displayAsText\" | \"initialWidth\" | \"isSortable\"> & { aggregatable?: boolean | undefined; dataTableCellActions?: ", + "DataTableCellAction", "[] | undefined; category?: string | undefined; columnHeaderType: ", - { - "pluginId": "timelines", - "scope": "common", - "docId": "kibTimelinesPluginApi", - "section": "def-common.ColumnHeaderType", - "text": "ColumnHeaderType" - }, + "ColumnHeaderType", "; description?: string | null | undefined; esTypes?: string[] | undefined; example?: string | number | null | undefined; format?: string | undefined; linkField?: string | undefined; placeholder?: string | undefined; subType?: ", { "pluginId": "@kbn/es-query", @@ -7364,55 +4032,6 @@ }, " | undefined; type?: string | undefined; }" ], - "path": "x-pack/plugins/timelines/common/types/timeline/columns/index.tsx", - "deprecated": false, - "trackAdoption": false, - "initialIsOpen": false - }, - { - "parentPluginId": "timelines", - "id": "def-common.ColumnHeaderType", - "type": "Type", - "tags": [], - "label": "ColumnHeaderType", - "description": [], - "signature": [ - "\"not-filtered\" | \"text-filter\"" - ], - "path": "x-pack/plugins/timelines/common/types/timeline/columns/index.tsx", - "deprecated": false, - "trackAdoption": false, - "initialIsOpen": false - }, - { - "parentPluginId": "timelines", - "id": "def-common.ColumnId", - "type": "Type", - "tags": [], - "label": "ColumnId", - "description": [ - "Uniquely identifies a column" - ], - "signature": [ - "string" - ], - "path": "x-pack/plugins/timelines/common/types/timeline/columns/index.tsx", - "deprecated": false, - "trackAdoption": false, - "initialIsOpen": false - }, - { - "parentPluginId": "timelines", - "id": "def-common.ControlColumnProps", - "type": "Type", - "tags": [], - "label": "ControlColumnProps", - "description": [], - "signature": [ - "Omit<", - "EuiDataGridControlColumn", - ", keyof AdditionalControlColumnProps> & Partial" - ], "path": "x-pack/plugins/timelines/common/types/timeline/actions/index.ts", "deprecated": false, "trackAdoption": false, @@ -7540,44 +4159,6 @@ "trackAdoption": false, "initialIsOpen": false }, - { - "parentPluginId": "timelines", - "id": "def-common.GenericActionRowCellRenderProps", - "type": "Type", - "tags": [], - "label": "GenericActionRowCellRenderProps", - "description": [], - "signature": [ - "{ columnId: string; rowIndex: number; }" - ], - "path": "x-pack/plugins/timelines/common/types/timeline/actions/index.ts", - "deprecated": false, - "trackAdoption": false, - "initialIsOpen": false - }, - { - "parentPluginId": "timelines", - "id": "def-common.HeaderCellRender", - "type": "Type", - "tags": [], - "label": "HeaderCellRender", - "description": [], - "signature": [ - "React.ComponentType<{}> | React.ComponentType<", - { - "pluginId": "timelines", - "scope": "common", - "docId": "kibTimelinesPluginApi", - "section": "def-common.HeaderActionProps", - "text": "HeaderActionProps" - }, - ">" - ], - "path": "x-pack/plugins/timelines/common/types/timeline/actions/index.ts", - "deprecated": false, - "trackAdoption": false, - "initialIsOpen": false - }, { "parentPluginId": "timelines", "id": "def-common.IndexFieldsStrategyRequest", @@ -7630,117 +4211,6 @@ "trackAdoption": false, "initialIsOpen": false }, - { - "parentPluginId": "timelines", - "id": "def-common.RowCellRender", - "type": "Type", - "tags": [], - "label": "RowCellRender", - "description": [], - "signature": [ - "React.JSXElementConstructor<", - { - "pluginId": "timelines", - "scope": "common", - "docId": "kibTimelinesPluginApi", - "section": "def-common.GenericActionRowCellRenderProps", - "text": "GenericActionRowCellRenderProps" - }, - "> | ((props: ", - { - "pluginId": "timelines", - "scope": "common", - "docId": "kibTimelinesPluginApi", - "section": "def-common.GenericActionRowCellRenderProps", - "text": "GenericActionRowCellRenderProps" - }, - ") => JSX.Element) | React.JSXElementConstructor<", - { - "pluginId": "timelines", - "scope": "common", - "docId": "kibTimelinesPluginApi", - "section": "def-common.ActionProps", - "text": "ActionProps" - }, - "> | ((props: ", - { - "pluginId": "timelines", - "scope": "common", - "docId": "kibTimelinesPluginApi", - "section": "def-common.ActionProps", - "text": "ActionProps" - }, - ") => JSX.Element)" - ], - "path": "x-pack/plugins/timelines/common/types/timeline/actions/index.ts", - "deprecated": false, - "trackAdoption": false, - "initialIsOpen": false - }, - { - "parentPluginId": "timelines", - "id": "def-common.SetEventsDeleted", - "type": "Type", - "tags": [], - "label": "SetEventsDeleted", - "description": [], - "signature": [ - "(params: { eventIds: string[]; isDeleted: boolean; }) => void" - ], - "path": "x-pack/plugins/timelines/common/types/timeline/actions/index.ts", - "deprecated": false, - "trackAdoption": false, - "returnComment": [], - "children": [ - { - "parentPluginId": "timelines", - "id": "def-common.SetEventsDeleted.$1", - "type": "Object", - "tags": [], - "label": "params", - "description": [], - "signature": [ - "{ eventIds: string[]; isDeleted: boolean; }" - ], - "path": "x-pack/plugins/timelines/common/types/timeline/actions/index.ts", - "deprecated": false, - "trackAdoption": false - } - ], - "initialIsOpen": false - }, - { - "parentPluginId": "timelines", - "id": "def-common.SetEventsLoading", - "type": "Type", - "tags": [], - "label": "SetEventsLoading", - "description": [], - "signature": [ - "(params: { eventIds: string[]; isLoading: boolean; }) => void" - ], - "path": "x-pack/plugins/timelines/common/types/timeline/actions/index.ts", - "deprecated": false, - "trackAdoption": false, - "returnComment": [], - "children": [ - { - "parentPluginId": "timelines", - "id": "def-common.SetEventsLoading.$1", - "type": "Object", - "tags": [], - "label": "params", - "description": [], - "signature": [ - "{ eventIds: string[]; isLoading: boolean; }" - ], - "path": "x-pack/plugins/timelines/common/types/timeline/actions/index.ts", - "deprecated": false, - "trackAdoption": false - } - ], - "initialIsOpen": false - }, { "parentPluginId": "timelines", "id": "def-common.TimelineKpiStrategyRequest", diff --git a/api_docs/timelines.mdx b/api_docs/timelines.mdx index f1e3e753295fb..f1a1335702f73 100644 --- a/api_docs/timelines.mdx +++ b/api_docs/timelines.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/timelines title: "timelines" image: https://source.unsplash.com/400x175/?github description: API docs for the timelines plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'timelines'] --- import timelinesObj from './timelines.devdocs.json'; @@ -21,16 +21,13 @@ Contact [Security solution](https://github.com/orgs/elastic/teams/security-solut | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 462 | 1 | 350 | 33 | +| 257 | 1 | 214 | 21 | ## Client ### Start -### Objects - - ### Functions diff --git a/api_docs/transform.mdx b/api_docs/transform.mdx index ae9b85338cdeb..5663d3575d211 100644 --- a/api_docs/transform.mdx +++ b/api_docs/transform.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/transform title: "transform" image: https://source.unsplash.com/400x175/?github description: API docs for the transform plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'transform'] --- import transformObj from './transform.devdocs.json'; diff --git a/api_docs/triggers_actions_ui.devdocs.json b/api_docs/triggers_actions_ui.devdocs.json index 4a808e78a85a8..489d1bc568121 100644 --- a/api_docs/triggers_actions_ui.devdocs.json +++ b/api_docs/triggers_actions_ui.devdocs.json @@ -3489,7 +3489,7 @@ "description": [], "signature": [ "BasicFields", - " & { tags?: string[] | undefined; kibana?: string[] | undefined; \"@timestamp\"?: string[] | undefined; \"kibana.alert.rule.rule_type_id\"?: string[] | undefined; \"kibana.alert.rule.consumer\"?: string[] | undefined; \"event.action\"?: string[] | undefined; \"kibana.alert.rule.execution.uuid\"?: string[] | undefined; \"kibana.alert\"?: string[] | undefined; \"kibana.alert.rule\"?: string[] | undefined; \"kibana.alert.rule.parameters\"?: string[] | undefined; \"kibana.alert.rule.producer\"?: string[] | undefined; \"kibana.space_ids\"?: string[] | undefined; \"kibana.alert.uuid\"?: string[] | undefined; \"kibana.alert.instance.id\"?: string[] | undefined; \"kibana.alert.start\"?: string[] | undefined; \"kibana.alert.time_range\"?: string[] | undefined; \"kibana.alert.end\"?: string[] | undefined; \"kibana.alert.duration.us\"?: string[] | undefined; \"kibana.alert.severity\"?: string[] | undefined; \"kibana.alert.status\"?: string[] | undefined; \"kibana.alert.flapping\"?: string[] | undefined; \"kibana.version\"?: string[] | undefined; \"ecs.version\"?: string[] | undefined; \"kibana.alert.risk_score\"?: string[] | undefined; \"kibana.alert.workflow_status\"?: string[] | undefined; \"kibana.alert.workflow_user\"?: string[] | undefined; \"kibana.alert.workflow_reason\"?: string[] | undefined; \"kibana.alert.system_status\"?: string[] | undefined; \"kibana.alert.action_group\"?: string[] | undefined; \"kibana.alert.reason\"?: string[] | undefined; \"kibana.alert.rule.author\"?: string[] | undefined; \"kibana.alert.rule.category\"?: string[] | undefined; \"kibana.alert.rule.uuid\"?: string[] | undefined; \"kibana.alert.rule.created_at\"?: string[] | undefined; \"kibana.alert.rule.created_by\"?: string[] | undefined; \"kibana.alert.rule.description\"?: string[] | undefined; \"kibana.alert.rule.enabled\"?: string[] | undefined; \"kibana.alert.rule.from\"?: string[] | undefined; \"kibana.alert.rule.interval\"?: string[] | undefined; \"kibana.alert.rule.license\"?: string[] | undefined; \"kibana.alert.rule.name\"?: string[] | undefined; \"kibana.alert.rule.note\"?: string[] | undefined; \"kibana.alert.rule.references\"?: string[] | undefined; \"kibana.alert.rule.rule_id\"?: string[] | undefined; \"kibana.alert.rule.rule_name_override\"?: string[] | undefined; \"kibana.alert.rule.tags\"?: string[] | undefined; \"kibana.alert.rule.to\"?: string[] | undefined; \"kibana.alert.rule.type\"?: string[] | undefined; \"kibana.alert.rule.updated_at\"?: string[] | undefined; \"kibana.alert.rule.updated_by\"?: string[] | undefined; \"kibana.alert.rule.version\"?: string[] | undefined; \"kibana.alert.suppression.terms\"?: string[] | undefined; \"kibana.alert.suppression.terms.field\"?: string[] | undefined; \"kibana.alert.suppression.terms.value\"?: string[] | undefined; \"kibana.alert.suppression.start\"?: string[] | undefined; \"kibana.alert.suppression.end\"?: string[] | undefined; \"kibana.alert.suppression.docs_count\"?: string[] | undefined; \"event.kind\"?: string[] | undefined; \"event.module\"?: string[] | undefined; \"kibana.alert.evaluation.threshold\"?: string[] | undefined; \"kibana.alert.evaluation.value\"?: string[] | undefined; \"kibana.alert.building_block_type\"?: string[] | undefined; \"kibana.alert.rule.exceptions_list\"?: string[] | undefined; \"kibana.alert.rule.namespace\"?: string[] | undefined; \"kibana.alert.rule.threat.framework\"?: string[] | undefined; \"kibana.alert.rule.threat.tactic.id\"?: string[] | undefined; \"kibana.alert.rule.threat.tactic.name\"?: string[] | undefined; \"kibana.alert.rule.threat.tactic.reference\"?: string[] | undefined; \"kibana.alert.rule.threat.technique.id\"?: string[] | undefined; \"kibana.alert.rule.threat.technique.name\"?: string[] | undefined; \"kibana.alert.rule.threat.technique.reference\"?: string[] | undefined; \"kibana.alert.rule.threat.technique.subtechnique.id\"?: string[] | undefined; \"kibana.alert.rule.threat.technique.subtechnique.name\"?: string[] | undefined; \"kibana.alert.rule.threat.technique.subtechnique.reference\"?: string[] | undefined; } & { [x: string]: unknown[]; }" + " & { tags?: string[] | undefined; kibana?: string[] | undefined; \"@timestamp\"?: string[] | undefined; \"event.action\"?: string[] | undefined; \"kibana.alert.rule.execution.uuid\"?: string[] | undefined; \"kibana.alert.rule.rule_type_id\"?: string[] | undefined; \"kibana.alert.rule.consumer\"?: string[] | undefined; \"kibana.alert\"?: string[] | undefined; \"kibana.alert.rule\"?: string[] | undefined; \"kibana.alert.rule.parameters\"?: string[] | undefined; \"kibana.alert.rule.producer\"?: string[] | undefined; \"kibana.space_ids\"?: string[] | undefined; \"kibana.alert.uuid\"?: string[] | undefined; \"kibana.alert.instance.id\"?: string[] | undefined; \"kibana.alert.start\"?: string[] | undefined; \"kibana.alert.time_range\"?: string[] | undefined; \"kibana.alert.end\"?: string[] | undefined; \"kibana.alert.duration.us\"?: string[] | undefined; \"kibana.alert.severity\"?: string[] | undefined; \"kibana.alert.status\"?: string[] | undefined; \"kibana.alert.flapping\"?: string[] | undefined; \"kibana.version\"?: string[] | undefined; \"ecs.version\"?: string[] | undefined; \"kibana.alert.risk_score\"?: string[] | undefined; \"kibana.alert.workflow_status\"?: string[] | undefined; \"kibana.alert.workflow_user\"?: string[] | undefined; \"kibana.alert.workflow_reason\"?: string[] | undefined; \"kibana.alert.system_status\"?: string[] | undefined; \"kibana.alert.action_group\"?: string[] | undefined; \"kibana.alert.reason\"?: string[] | undefined; \"kibana.alert.rule.author\"?: string[] | undefined; \"kibana.alert.rule.category\"?: string[] | undefined; \"kibana.alert.rule.uuid\"?: string[] | undefined; \"kibana.alert.rule.created_at\"?: string[] | undefined; \"kibana.alert.rule.created_by\"?: string[] | undefined; \"kibana.alert.rule.description\"?: string[] | undefined; \"kibana.alert.rule.enabled\"?: string[] | undefined; \"kibana.alert.rule.from\"?: string[] | undefined; \"kibana.alert.rule.interval\"?: string[] | undefined; \"kibana.alert.rule.license\"?: string[] | undefined; \"kibana.alert.rule.name\"?: string[] | undefined; \"kibana.alert.rule.note\"?: string[] | undefined; \"kibana.alert.rule.references\"?: string[] | undefined; \"kibana.alert.rule.rule_id\"?: string[] | undefined; \"kibana.alert.rule.rule_name_override\"?: string[] | undefined; \"kibana.alert.rule.tags\"?: string[] | undefined; \"kibana.alert.rule.to\"?: string[] | undefined; \"kibana.alert.rule.type\"?: string[] | undefined; \"kibana.alert.rule.updated_at\"?: string[] | undefined; \"kibana.alert.rule.updated_by\"?: string[] | undefined; \"kibana.alert.rule.version\"?: string[] | undefined; \"kibana.alert.suppression.terms\"?: string[] | undefined; \"kibana.alert.suppression.terms.field\"?: string[] | undefined; \"kibana.alert.suppression.terms.value\"?: string[] | undefined; \"kibana.alert.suppression.start\"?: string[] | undefined; \"kibana.alert.suppression.end\"?: string[] | undefined; \"kibana.alert.suppression.docs_count\"?: string[] | undefined; \"event.kind\"?: string[] | undefined; \"event.module\"?: string[] | undefined; \"kibana.alert.evaluation.threshold\"?: string[] | undefined; \"kibana.alert.evaluation.value\"?: string[] | undefined; \"kibana.alert.building_block_type\"?: string[] | undefined; \"kibana.alert.rule.exceptions_list\"?: string[] | undefined; \"kibana.alert.rule.namespace\"?: string[] | undefined; \"kibana.alert.rule.threat.framework\"?: string[] | undefined; \"kibana.alert.rule.threat.tactic.id\"?: string[] | undefined; \"kibana.alert.rule.threat.tactic.name\"?: string[] | undefined; \"kibana.alert.rule.threat.tactic.reference\"?: string[] | undefined; \"kibana.alert.rule.threat.technique.id\"?: string[] | undefined; \"kibana.alert.rule.threat.technique.name\"?: string[] | undefined; \"kibana.alert.rule.threat.technique.reference\"?: string[] | undefined; \"kibana.alert.rule.threat.technique.subtechnique.id\"?: string[] | undefined; \"kibana.alert.rule.threat.technique.subtechnique.name\"?: string[] | undefined; \"kibana.alert.rule.threat.technique.subtechnique.reference\"?: string[] | undefined; } & { [x: string]: unknown[]; }" ], "path": "x-pack/plugins/triggers_actions_ui/public/types.ts", "deprecated": false, @@ -5396,6 +5396,20 @@ "path": "x-pack/plugins/triggers_actions_ui/public/types.ts", "deprecated": false, "trackAdoption": false + }, + { + "parentPluginId": "triggersActionsUi", + "id": "def-public.RuleTypeModel.alertDetailsAppSection", + "type": "CompoundType", + "tags": [], + "label": "alertDetailsAppSection", + "description": [], + "signature": [ + "React.FunctionComponent | React.LazyExoticComponent> | undefined" + ], + "path": "x-pack/plugins/triggers_actions_ui/public/types.ts", + "deprecated": false, + "trackAdoption": false } ], "initialIsOpen": false diff --git a/api_docs/triggers_actions_ui.mdx b/api_docs/triggers_actions_ui.mdx index d0c15420b460a..138d4d4686528 100644 --- a/api_docs/triggers_actions_ui.mdx +++ b/api_docs/triggers_actions_ui.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/triggersActionsUi title: "triggersActionsUi" image: https://source.unsplash.com/400x175/?github description: API docs for the triggersActionsUi plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'triggersActionsUi'] --- import triggersActionsUiObj from './triggers_actions_ui.devdocs.json'; @@ -21,7 +21,7 @@ Contact [Response Ops](https://github.com/orgs/elastic/teams/response-ops) for q | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 531 | 11 | 502 | 51 | +| 532 | 11 | 503 | 51 | ## Client diff --git a/api_docs/ui_actions.mdx b/api_docs/ui_actions.mdx index eaab5ce1c4271..e759076d5b4d6 100644 --- a/api_docs/ui_actions.mdx +++ b/api_docs/ui_actions.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/uiActions title: "uiActions" image: https://source.unsplash.com/400x175/?github description: API docs for the uiActions plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'uiActions'] --- import uiActionsObj from './ui_actions.devdocs.json'; diff --git a/api_docs/ui_actions_enhanced.mdx b/api_docs/ui_actions_enhanced.mdx index b4ac931365962..03b2981c53bd9 100644 --- a/api_docs/ui_actions_enhanced.mdx +++ b/api_docs/ui_actions_enhanced.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/uiActionsEnhanced title: "uiActionsEnhanced" image: https://source.unsplash.com/400x175/?github description: API docs for the uiActionsEnhanced plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'uiActionsEnhanced'] --- import uiActionsEnhancedObj from './ui_actions_enhanced.devdocs.json'; diff --git a/api_docs/unified_field_list.devdocs.json b/api_docs/unified_field_list.devdocs.json index 116fac3c5a0a5..60a10d66079c0 100644 --- a/api_docs/unified_field_list.devdocs.json +++ b/api_docs/unified_field_list.devdocs.json @@ -530,6 +530,79 @@ "returnComment": [], "initialIsOpen": false }, + { + "parentPluginId": "unifiedFieldList", + "id": "def-public.hasQuerySubscriberData", + "type": "Function", + "tags": [], + "label": "hasQuerySubscriberData", + "description": [ + "\nChecks if query result is ready to be used" + ], + "signature": [ + "(result: ", + { + "pluginId": "unifiedFieldList", + "scope": "public", + "docId": "kibUnifiedFieldListPluginApi", + "section": "def-public.QuerySubscriberResult", + "text": "QuerySubscriberResult" + }, + ") => result is { query: ", + { + "pluginId": "@kbn/es-query", + "scope": "common", + "docId": "kibKbnEsQueryPluginApi", + "section": "def-common.Query", + "text": "Query" + }, + " | ", + { + "pluginId": "@kbn/es-query", + "scope": "common", + "docId": "kibKbnEsQueryPluginApi", + "section": "def-common.AggregateQuery", + "text": "AggregateQuery" + }, + "; filters: ", + { + "pluginId": "@kbn/es-query", + "scope": "common", + "docId": "kibKbnEsQueryPluginApi", + "section": "def-common.Filter", + "text": "Filter" + }, + "[]; fromDate: string; toDate: string; }" + ], + "path": "src/plugins/unified_field_list/public/hooks/use_query_subscriber.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "unifiedFieldList", + "id": "def-public.hasQuerySubscriberData.$1", + "type": "Object", + "tags": [], + "label": "result", + "description": [], + "signature": [ + { + "pluginId": "unifiedFieldList", + "scope": "public", + "docId": "kibUnifiedFieldListPluginApi", + "section": "def-public.QuerySubscriberResult", + "text": "QuerySubscriberResult" + } + ], + "path": "src/plugins/unified_field_list/public/hooks/use_query_subscriber.ts", + "deprecated": false, + "trackAdoption": false, + "isRequired": true + } + ], + "returnComment": [], + "initialIsOpen": false + }, { "parentPluginId": "unifiedFieldList", "id": "def-public.loadFieldExisting", @@ -975,7 +1048,7 @@ "label": "useGroupedFields", "description": [], "signature": [ - "({\n dataViewId,\n allFields,\n services,\n fieldsExistenceReader,\n onOverrideFieldGroupDetails,\n onSupportedFieldFilter,\n onSelectedFieldFilter,\n onFilterField,\n}: ", + "({\n dataViewId,\n allFields,\n services,\n fieldsExistenceReader,\n isAffectedByGlobalFilter = false,\n popularFieldsLimit,\n sortedSelectedFields,\n onOverrideFieldGroupDetails,\n onSupportedFieldFilter,\n onSelectedFieldFilter,\n onFilterField,\n}: ", { "pluginId": "unifiedFieldList", "scope": "public", @@ -1002,7 +1075,7 @@ "id": "def-public.useGroupedFields.$1", "type": "Object", "tags": [], - "label": "{\n dataViewId,\n allFields,\n services,\n fieldsExistenceReader,\n onOverrideFieldGroupDetails,\n onSupportedFieldFilter,\n onSelectedFieldFilter,\n onFilterField,\n}", + "label": "{\n dataViewId,\n allFields,\n services,\n fieldsExistenceReader,\n isAffectedByGlobalFilter = false,\n popularFieldsLimit,\n sortedSelectedFields,\n onOverrideFieldGroupDetails,\n onSupportedFieldFilter,\n onSelectedFieldFilter,\n onFilterField,\n}", "description": [], "signature": [ { @@ -1030,7 +1103,7 @@ "tags": [], "label": "useQuerySubscriber", "description": [ - "\nMemorizes current query and filters" + "\nMemorizes current query, filters and absolute date range" ], "signature": [ "({ data }: ", @@ -1181,6 +1254,20 @@ "deprecated": false, "trackAdoption": false, "children": [ + { + "parentPluginId": "unifiedFieldList", + "id": "def-public.ExistingFieldsFetcherParams.disableAutoFetching", + "type": "CompoundType", + "tags": [], + "label": "disableAutoFetching", + "description": [], + "signature": [ + "boolean | undefined" + ], + "path": "src/plugins/unified_field_list/public/hooks/use_existing_fields.ts", + "deprecated": false, + "trackAdoption": false + }, { "parentPluginId": "unifiedFieldList", "id": "def-public.ExistingFieldsFetcherParams.dataViews", @@ -1209,6 +1296,9 @@ "tags": [], "label": "fromDate", "description": [], + "signature": [ + "string | undefined" + ], "path": "src/plugins/unified_field_list/public/hooks/use_existing_fields.ts", "deprecated": false, "trackAdoption": false @@ -1220,6 +1310,9 @@ "tags": [], "label": "toDate", "description": [], + "signature": [ + "string | undefined" + ], "path": "src/plugins/unified_field_list/public/hooks/use_existing_fields.ts", "deprecated": false, "trackAdoption": false @@ -1246,7 +1339,8 @@ "docId": "kibKbnEsQueryPluginApi", "section": "def-common.AggregateQuery", "text": "AggregateQuery" - } + }, + " | undefined" ], "path": "src/plugins/unified_field_list/public/hooks/use_existing_fields.ts", "deprecated": false, @@ -1267,7 +1361,7 @@ "section": "def-common.Filter", "text": "Filter" }, - "[]" + "[] | undefined" ], "path": "src/plugins/unified_field_list/public/hooks/use_existing_fields.ts", "deprecated": false, @@ -1584,12 +1678,16 @@ "FieldsGroup", " | undefined; SelectedFields?: ", "FieldsGroup", + " | undefined; PopularFields?: ", + "FieldsGroup", " | undefined; AvailableFields?: ", "FieldsGroup", " | undefined; EmptyFields?: ", "FieldsGroup", " | undefined; MetaFields?: ", "FieldsGroup", + " | undefined; UnmappedFields?: ", + "FieldsGroup", " | undefined; }" ], "path": "src/plugins/unified_field_list/public/components/field_list/field_list_grouped.tsx", @@ -1635,7 +1733,15 @@ "label": "renderFieldItem", "description": [], "signature": [ - "(params: { field: T; hideDetails?: boolean | undefined; itemIndex: number; groupIndex: number; }) => JSX.Element" + "(params: { field: T; hideDetails?: boolean | undefined; itemIndex: number; groupIndex: number; groupName: ", + { + "pluginId": "unifiedFieldList", + "scope": "public", + "docId": "kibUnifiedFieldListPluginApi", + "section": "def-public.FieldsGroupNames", + "text": "FieldsGroupNames" + }, + "; }) => JSX.Element" ], "path": "src/plugins/unified_field_list/public/components/field_list/field_list_grouped.tsx", "deprecated": false, @@ -1650,7 +1756,15 @@ "label": "params", "description": [], "signature": [ - "{ field: T; hideDetails?: boolean | undefined; itemIndex: number; groupIndex: number; }" + "{ field: T; hideDetails?: boolean | undefined; itemIndex: number; groupIndex: number; groupName: ", + { + "pluginId": "unifiedFieldList", + "scope": "public", + "docId": "kibUnifiedFieldListPluginApi", + "section": "def-public.FieldsGroupNames", + "text": "FieldsGroupNames" + }, + "; }" ], "path": "src/plugins/unified_field_list/public/components/field_list/fields_accordion.tsx", "deprecated": false, @@ -1658,6 +1772,17 @@ } ] }, + { + "parentPluginId": "unifiedFieldList", + "id": "def-public.FieldListGroupedProps.scrollToTopResetCounter", + "type": "number", + "tags": [], + "label": "scrollToTopResetCounter", + "description": [], + "path": "src/plugins/unified_field_list/public/components/field_list/field_list_grouped.tsx", + "deprecated": false, + "trackAdoption": false + }, { "parentPluginId": "unifiedFieldList", "id": "def-public.FieldListGroupedProps.screenReaderDescriptionForSearchInputId", @@ -2952,12 +3077,12 @@ { "parentPluginId": "unifiedFieldList", "id": "def-public.GroupedFieldsParams.allFields", - "type": "Array", + "type": "CompoundType", "tags": [], "label": "allFields", "description": [], "signature": [ - "T[]" + "T[] | null" ], "path": "src/plugins/unified_field_list/public/hooks/use_grouped_fields.ts", "deprecated": false, @@ -3006,6 +3131,48 @@ "deprecated": false, "trackAdoption": false }, + { + "parentPluginId": "unifiedFieldList", + "id": "def-public.GroupedFieldsParams.isAffectedByGlobalFilter", + "type": "CompoundType", + "tags": [], + "label": "isAffectedByGlobalFilter", + "description": [], + "signature": [ + "boolean | undefined" + ], + "path": "src/plugins/unified_field_list/public/hooks/use_grouped_fields.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "unifiedFieldList", + "id": "def-public.GroupedFieldsParams.popularFieldsLimit", + "type": "number", + "tags": [], + "label": "popularFieldsLimit", + "description": [], + "signature": [ + "number | undefined" + ], + "path": "src/plugins/unified_field_list/public/hooks/use_grouped_fields.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "unifiedFieldList", + "id": "def-public.GroupedFieldsParams.sortedSelectedFields", + "type": "Array", + "tags": [], + "label": "sortedSelectedFields", + "description": [], + "signature": [ + "T[] | undefined" + ], + "path": "src/plugins/unified_field_list/public/hooks/use_grouped_fields.ts", + "deprecated": false, + "trackAdoption": false + }, { "parentPluginId": "unifiedFieldList", "id": "def-public.GroupedFieldsParams.onOverrideFieldGroupDetails", @@ -3192,17 +3359,63 @@ "FieldsGroup", " | undefined; SelectedFields?: ", "FieldsGroup", + " | undefined; PopularFields?: ", + "FieldsGroup", " | undefined; AvailableFields?: ", "FieldsGroup", " | undefined; EmptyFields?: ", "FieldsGroup", " | undefined; MetaFields?: ", "FieldsGroup", + " | undefined; UnmappedFields?: ", + "FieldsGroup", " | undefined; }" ], "path": "src/plugins/unified_field_list/public/hooks/use_grouped_fields.ts", "deprecated": false, "trackAdoption": false + }, + { + "parentPluginId": "unifiedFieldList", + "id": "def-public.GroupedFieldsResult.scrollToTopResetCounter", + "type": "number", + "tags": [], + "label": "scrollToTopResetCounter", + "description": [], + "path": "src/plugins/unified_field_list/public/hooks/use_grouped_fields.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "unifiedFieldList", + "id": "def-public.GroupedFieldsResult.fieldsExistenceStatus", + "type": "Enum", + "tags": [], + "label": "fieldsExistenceStatus", + "description": [], + "signature": [ + { + "pluginId": "unifiedFieldList", + "scope": "public", + "docId": "kibUnifiedFieldListPluginApi", + "section": "def-public.ExistenceFetchStatus", + "text": "ExistenceFetchStatus" + } + ], + "path": "src/plugins/unified_field_list/public/hooks/use_grouped_fields.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "unifiedFieldList", + "id": "def-public.GroupedFieldsResult.fieldsExistInIndex", + "type": "boolean", + "tags": [], + "label": "fieldsExistInIndex", + "description": [], + "path": "src/plugins/unified_field_list/public/hooks/use_grouped_fields.ts", + "deprecated": false, + "trackAdoption": false } ], "initialIsOpen": false @@ -3360,6 +3573,34 @@ "path": "src/plugins/unified_field_list/public/hooks/use_query_subscriber.ts", "deprecated": false, "trackAdoption": false + }, + { + "parentPluginId": "unifiedFieldList", + "id": "def-public.QuerySubscriberResult.fromDate", + "type": "string", + "tags": [], + "label": "fromDate", + "description": [], + "signature": [ + "string | undefined" + ], + "path": "src/plugins/unified_field_list/public/hooks/use_query_subscriber.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "unifiedFieldList", + "id": "def-public.QuerySubscriberResult.toDate", + "type": "string", + "tags": [], + "label": "toDate", + "description": [], + "signature": [ + "string | undefined" + ], + "path": "src/plugins/unified_field_list/public/hooks/use_query_subscriber.ts", + "deprecated": false, + "trackAdoption": false } ], "initialIsOpen": false @@ -3567,12 +3808,16 @@ "FieldsGroup", " | undefined; SelectedFields?: ", "FieldsGroup", + " | undefined; PopularFields?: ", + "FieldsGroup", " | undefined; AvailableFields?: ", "FieldsGroup", " | undefined; EmptyFields?: ", "FieldsGroup", " | undefined; MetaFields?: ", "FieldsGroup", + " | undefined; UnmappedFields?: ", + "FieldsGroup", " | undefined; }" ], "path": "src/plugins/unified_field_list/public/types.ts", diff --git a/api_docs/unified_field_list.mdx b/api_docs/unified_field_list.mdx index 6d89274017cdb..e2ad04763af23 100644 --- a/api_docs/unified_field_list.mdx +++ b/api_docs/unified_field_list.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/unifiedFieldList title: "unifiedFieldList" image: https://source.unsplash.com/400x175/?github description: API docs for the unifiedFieldList plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'unifiedFieldList'] --- import unifiedFieldListObj from './unified_field_list.devdocs.json'; @@ -21,7 +21,7 @@ Contact [Data Discovery](https://github.com/orgs/elastic/teams/kibana-data-disco | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 203 | 0 | 192 | 7 | +| 215 | 0 | 203 | 7 | ## Client diff --git a/api_docs/unified_histogram.devdocs.json b/api_docs/unified_histogram.devdocs.json index 5ea1d379e5aea..7a881fb33eed3 100644 --- a/api_docs/unified_histogram.devdocs.json +++ b/api_docs/unified_histogram.devdocs.json @@ -3,253 +3,6 @@ "client": { "classes": [], "functions": [ - { - "parentPluginId": "unifiedHistogram", - "id": "def-public.buildChartData", - "type": "Function", - "tags": [], - "label": "buildChartData", - "description": [ - "\nConvert the response from the chart request into a format that can be used\nby the unified histogram chart. The returned object should be used to update\n{@link UnifiedHistogramChartContext.bucketInterval} and {@link UnifiedHistogramChartContext.data}." - ], - "signature": [ - "({ data, dataView, timeInterval, response, }: { data: ", - { - "pluginId": "data", - "scope": "public", - "docId": "kibDataPluginApi", - "section": "def-public.DataPublicPluginStart", - "text": "DataPublicPluginStart" - }, - "; dataView: ", - { - "pluginId": "dataViews", - "scope": "common", - "docId": "kibDataViewsPluginApi", - "section": "def-common.DataView", - "text": "DataView" - }, - "; timeInterval?: string | undefined; response?: ", - "SearchResponse", - "> | undefined; }) => { bucketInterval?: undefined; chartData?: undefined; } | { bucketInterval: ", - { - "pluginId": "unifiedHistogram", - "scope": "public", - "docId": "kibUnifiedHistogramPluginApi", - "section": "def-public.UnifiedHistogramBucketInterval", - "text": "UnifiedHistogramBucketInterval" - }, - " | undefined; chartData: ", - { - "pluginId": "unifiedHistogram", - "scope": "public", - "docId": "kibUnifiedHistogramPluginApi", - "section": "def-public.UnifiedHistogramChartData", - "text": "UnifiedHistogramChartData" - }, - "; }" - ], - "path": "src/plugins/unified_histogram/public/chart/build_chart_data.ts", - "deprecated": false, - "trackAdoption": false, - "children": [ - { - "parentPluginId": "unifiedHistogram", - "id": "def-public.buildChartData.$1", - "type": "Object", - "tags": [], - "label": "{\n data,\n dataView,\n timeInterval,\n response,\n}", - "description": [], - "path": "src/plugins/unified_histogram/public/chart/build_chart_data.ts", - "deprecated": false, - "trackAdoption": false, - "children": [ - { - "parentPluginId": "unifiedHistogram", - "id": "def-public.buildChartData.$1.data", - "type": "Object", - "tags": [], - "label": "data", - "description": [], - "signature": [ - { - "pluginId": "data", - "scope": "public", - "docId": "kibDataPluginApi", - "section": "def-public.DataPublicPluginStart", - "text": "DataPublicPluginStart" - } - ], - "path": "src/plugins/unified_histogram/public/chart/build_chart_data.ts", - "deprecated": false, - "trackAdoption": false - }, - { - "parentPluginId": "unifiedHistogram", - "id": "def-public.buildChartData.$1.dataView", - "type": "Object", - "tags": [], - "label": "dataView", - "description": [], - "signature": [ - { - "pluginId": "dataViews", - "scope": "common", - "docId": "kibDataViewsPluginApi", - "section": "def-common.DataView", - "text": "DataView" - } - ], - "path": "src/plugins/unified_histogram/public/chart/build_chart_data.ts", - "deprecated": false, - "trackAdoption": false - }, - { - "parentPluginId": "unifiedHistogram", - "id": "def-public.buildChartData.$1.timeInterval", - "type": "string", - "tags": [], - "label": "timeInterval", - "description": [], - "signature": [ - "string | undefined" - ], - "path": "src/plugins/unified_histogram/public/chart/build_chart_data.ts", - "deprecated": false, - "trackAdoption": false - }, - { - "parentPluginId": "unifiedHistogram", - "id": "def-public.buildChartData.$1.response", - "type": "Object", - "tags": [], - "label": "response", - "description": [], - "signature": [ - "SearchResponse", - "> | undefined" - ], - "path": "src/plugins/unified_histogram/public/chart/build_chart_data.ts", - "deprecated": false, - "trackAdoption": false - } - ] - } - ], - "returnComment": [], - "initialIsOpen": false - }, - { - "parentPluginId": "unifiedHistogram", - "id": "def-public.getChartAggConfigs", - "type": "Function", - "tags": [], - "label": "getChartAggConfigs", - "description": [ - "\nHelper function to get the agg configs required for the unified histogram chart request" - ], - "signature": [ - "({\n dataView,\n timeInterval,\n data,\n}: { dataView: ", - { - "pluginId": "dataViews", - "scope": "common", - "docId": "kibDataViewsPluginApi", - "section": "def-common.DataView", - "text": "DataView" - }, - "; timeInterval: string; data: ", - { - "pluginId": "data", - "scope": "public", - "docId": "kibDataPluginApi", - "section": "def-public.DataPublicPluginStart", - "text": "DataPublicPluginStart" - }, - "; }) => ", - { - "pluginId": "data", - "scope": "common", - "docId": "kibDataSearchPluginApi", - "section": "def-common.AggConfigs", - "text": "AggConfigs" - } - ], - "path": "src/plugins/unified_histogram/public/chart/get_chart_agg_configs.ts", - "deprecated": false, - "trackAdoption": false, - "children": [ - { - "parentPluginId": "unifiedHistogram", - "id": "def-public.getChartAggConfigs.$1", - "type": "Object", - "tags": [], - "label": "{\n dataView,\n timeInterval,\n data,\n}", - "description": [], - "path": "src/plugins/unified_histogram/public/chart/get_chart_agg_configs.ts", - "deprecated": false, - "trackAdoption": false, - "children": [ - { - "parentPluginId": "unifiedHistogram", - "id": "def-public.getChartAggConfigs.$1.dataView", - "type": "Object", - "tags": [], - "label": "dataView", - "description": [], - "signature": [ - { - "pluginId": "dataViews", - "scope": "common", - "docId": "kibDataViewsPluginApi", - "section": "def-common.DataView", - "text": "DataView" - } - ], - "path": "src/plugins/unified_histogram/public/chart/get_chart_agg_configs.ts", - "deprecated": false, - "trackAdoption": false - }, - { - "parentPluginId": "unifiedHistogram", - "id": "def-public.getChartAggConfigs.$1.timeInterval", - "type": "string", - "tags": [], - "label": "timeInterval", - "description": [], - "path": "src/plugins/unified_histogram/public/chart/get_chart_agg_configs.ts", - "deprecated": false, - "trackAdoption": false - }, - { - "parentPluginId": "unifiedHistogram", - "id": "def-public.getChartAggConfigs.$1.data", - "type": "Object", - "tags": [], - "label": "data", - "description": [], - "signature": [ - { - "pluginId": "data", - "scope": "public", - "docId": "kibDataPluginApi", - "section": "def-public.DataPublicPluginStart", - "text": "DataPublicPluginStart" - } - ], - "path": "src/plugins/unified_histogram/public/chart/get_chart_agg_configs.ts", - "deprecated": false, - "trackAdoption": false - } - ] - } - ], - "returnComment": [], - "initialIsOpen": false - }, { "parentPluginId": "unifiedHistogram", "id": "def-public.UnifiedHistogramLayout", @@ -296,12 +49,12 @@ "interfaces": [ { "parentPluginId": "unifiedHistogram", - "id": "def-public.UnifiedHistogramBucketInterval", + "id": "def-public.UnifiedHistogramBreakdownContext", "type": "Interface", "tags": [], - "label": "UnifiedHistogramBucketInterval", + "label": "UnifiedHistogramBreakdownContext", "description": [ - "\nThe bucketInterval object returned by {@link buildChartData} that\nshould be used to set {@link UnifiedHistogramChartContext.bucketInterval}" + "\nContext object for the histogram breakdown" ], "path": "src/plugins/unified_histogram/public/types.ts", "deprecated": false, @@ -309,41 +62,22 @@ "children": [ { "parentPluginId": "unifiedHistogram", - "id": "def-public.UnifiedHistogramBucketInterval.scaled", - "type": "CompoundType", - "tags": [], - "label": "scaled", - "description": [], - "signature": [ - "boolean | undefined" - ], - "path": "src/plugins/unified_histogram/public/types.ts", - "deprecated": false, - "trackAdoption": false - }, - { - "parentPluginId": "unifiedHistogram", - "id": "def-public.UnifiedHistogramBucketInterval.description", - "type": "string", + "id": "def-public.UnifiedHistogramBreakdownContext.field", + "type": "Object", "tags": [], - "label": "description", - "description": [], - "signature": [ - "string | undefined" + "label": "field", + "description": [ + "\nThe field used for the breakdown" ], - "path": "src/plugins/unified_histogram/public/types.ts", - "deprecated": false, - "trackAdoption": false - }, - { - "parentPluginId": "unifiedHistogram", - "id": "def-public.UnifiedHistogramBucketInterval.scale", - "type": "number", - "tags": [], - "label": "scale", - "description": [], "signature": [ - "number | undefined" + { + "pluginId": "dataViews", + "scope": "common", + "docId": "kibDataViewsPluginApi", + "section": "def-common.DataViewField", + "text": "DataViewField" + }, + " | undefined" ], "path": "src/plugins/unified_histogram/public/types.ts", "deprecated": false, @@ -365,22 +99,6 @@ "deprecated": false, "trackAdoption": false, "children": [ - { - "parentPluginId": "unifiedHistogram", - "id": "def-public.UnifiedHistogramChartContext.status", - "type": "CompoundType", - "tags": [], - "label": "status", - "description": [ - "\nThe fetch status of the chart request" - ], - "signature": [ - "\"loading\" | \"error\" | \"complete\" | \"partial\" | \"uninitialized\"" - ], - "path": "src/plugins/unified_histogram/public/types.ts", - "deprecated": false, - "trackAdoption": false - }, { "parentPluginId": "unifiedHistogram", "id": "def-public.UnifiedHistogramChartContext.hidden", @@ -415,61 +133,15 @@ }, { "parentPluginId": "unifiedHistogram", - "id": "def-public.UnifiedHistogramChartContext.bucketInterval", - "type": "Object", - "tags": [], - "label": "bucketInterval", - "description": [ - "\nThe bucketInterval object returned by {@link buildChartData}" - ], - "signature": [ - { - "pluginId": "unifiedHistogram", - "scope": "public", - "docId": "kibUnifiedHistogramPluginApi", - "section": "def-public.UnifiedHistogramBucketInterval", - "text": "UnifiedHistogramBucketInterval" - }, - " | undefined" - ], - "path": "src/plugins/unified_histogram/public/types.ts", - "deprecated": false, - "trackAdoption": false - }, - { - "parentPluginId": "unifiedHistogram", - "id": "def-public.UnifiedHistogramChartContext.data", - "type": "Object", - "tags": [], - "label": "data", - "description": [ - "\nThe chartData object returned by {@link buildChartData}" - ], - "signature": [ - { - "pluginId": "unifiedHistogram", - "scope": "public", - "docId": "kibUnifiedHistogramPluginApi", - "section": "def-public.UnifiedHistogramChartData", - "text": "UnifiedHistogramChartData" - }, - " | undefined" - ], - "path": "src/plugins/unified_histogram/public/types.ts", - "deprecated": false, - "trackAdoption": false - }, - { - "parentPluginId": "unifiedHistogram", - "id": "def-public.UnifiedHistogramChartContext.error", - "type": "Object", + "id": "def-public.UnifiedHistogramChartContext.title", + "type": "string", "tags": [], - "label": "error", + "label": "title", "description": [ - "\nError from failed chart request" + "\nThe chart title -- sets the title property on the Lens chart input" ], "signature": [ - "Error | undefined" + "string | undefined" ], "path": "src/plugins/unified_histogram/public/types.ts", "deprecated": false, @@ -480,12 +152,12 @@ }, { "parentPluginId": "unifiedHistogram", - "id": "def-public.UnifiedHistogramChartData", + "id": "def-public.UnifiedHistogramChartLoadEvent", "type": "Interface", "tags": [], - "label": "UnifiedHistogramChartData", + "label": "UnifiedHistogramChartLoadEvent", "description": [ - "\nThe chartData object returned by {@link buildChartData} that\nshould be used to set {@link UnifiedHistogramChartContext.data}" + "\nEmitted when the histogram loading status changes" ], "path": "src/plugins/unified_histogram/public/types.ts", "deprecated": false, @@ -493,27 +165,12 @@ "children": [ { "parentPluginId": "unifiedHistogram", - "id": "def-public.UnifiedHistogramChartData.values", - "type": "Array", + "id": "def-public.UnifiedHistogramChartLoadEvent.complete", + "type": "boolean", "tags": [], - "label": "values", - "description": [], - "signature": [ - "{ x: number; y: number; }[]" - ], - "path": "src/plugins/unified_histogram/public/types.ts", - "deprecated": false, - "trackAdoption": false - }, - { - "parentPluginId": "unifiedHistogram", - "id": "def-public.UnifiedHistogramChartData.xAxisOrderedValues", - "type": "Array", - "tags": [], - "label": "xAxisOrderedValues", - "description": [], - "signature": [ - "number[]" + "label": "complete", + "description": [ + "\nTrue if loading is complete" ], "path": "src/plugins/unified_histogram/public/types.ts", "deprecated": false, @@ -521,82 +178,39 @@ }, { "parentPluginId": "unifiedHistogram", - "id": "def-public.UnifiedHistogramChartData.xAxisFormat", + "id": "def-public.UnifiedHistogramChartLoadEvent.adapters", "type": "Object", "tags": [], - "label": "xAxisFormat", - "description": [], + "label": "adapters", + "description": [ + "\nInspector adapters for the request" + ], "signature": [ - "{ id?: string | undefined; params?: ", + "{ requests?: ", { - "pluginId": "fieldFormats", + "pluginId": "inspector", "scope": "common", - "docId": "kibFieldFormatsPluginApi", - "section": "def-common.FieldFormatParams", - "text": "FieldFormatParams" + "docId": "kibInspectorPluginApi", + "section": "def-common.RequestAdapter", + "text": "RequestAdapter" }, - "<{ pattern: string; }> | undefined; }" - ], - "path": "src/plugins/unified_histogram/public/types.ts", - "deprecated": false, - "trackAdoption": false - }, - { - "parentPluginId": "unifiedHistogram", - "id": "def-public.UnifiedHistogramChartData.yAxisFormat", - "type": "Object", - "tags": [], - "label": "yAxisFormat", - "description": [], - "signature": [ - "{ id?: string | undefined; params?: ", + " | undefined; tables?: ", { - "pluginId": "fieldFormats", + "pluginId": "expressions", "scope": "common", - "docId": "kibFieldFormatsPluginApi", - "section": "def-common.FieldFormatParams", - "text": "FieldFormatParams" + "docId": "kibExpressionsPluginApi", + "section": "def-common.TablesAdapter", + "text": "TablesAdapter" }, - "<{ pattern: string; }> | undefined; }" - ], - "path": "src/plugins/unified_histogram/public/types.ts", - "deprecated": false, - "trackAdoption": false - }, - { - "parentPluginId": "unifiedHistogram", - "id": "def-public.UnifiedHistogramChartData.xAxisLabel", - "type": "string", - "tags": [], - "label": "xAxisLabel", - "description": [], - "path": "src/plugins/unified_histogram/public/types.ts", - "deprecated": false, - "trackAdoption": false - }, - { - "parentPluginId": "unifiedHistogram", - "id": "def-public.UnifiedHistogramChartData.yAxisLabel", - "type": "string", - "tags": [], - "label": "yAxisLabel", - "description": [], - "signature": [ - "string | undefined" - ], - "path": "src/plugins/unified_histogram/public/types.ts", - "deprecated": false, - "trackAdoption": false - }, - { - "parentPluginId": "unifiedHistogram", - "id": "def-public.UnifiedHistogramChartData.ordered", - "type": "Object", - "tags": [], - "label": "ordered", - "description": [], - "signature": [ - "Ordered" + " | undefined; expression?: ", + { + "pluginId": "expressions", + "scope": "common", + "docId": "kibExpressionsPluginApi", + "section": "def-common.ExpressionsInspectorAdapter", + "text": "ExpressionsInspectorAdapter" + }, + " | undefined; }" ], "path": "src/plugins/unified_histogram/public/types.ts", "deprecated": false, @@ -628,7 +242,14 @@ "\nThe fetch status of the hits count request" ], "signature": [ - "\"loading\" | \"error\" | \"complete\" | \"partial\" | \"uninitialized\"" + { + "pluginId": "unifiedHistogram", + "scope": "public", + "docId": "kibUnifiedHistogramPluginApi", + "section": "def-public.UnifiedHistogramFetchStatus", + "text": "UnifiedHistogramFetchStatus" + }, + " | undefined" ], "path": "src/plugins/unified_histogram/public/types.ts", "deprecated": false, @@ -680,7 +301,9 @@ "type": "string", "tags": [], "label": "className", - "description": [], + "description": [ + "\nOptional class name to add to the layout container" + ], "signature": [ "string | undefined" ], @@ -694,7 +317,9 @@ "type": "Object", "tags": [], "label": "services", - "description": [], + "description": [ + "\nRequired services" + ], "signature": [ { "pluginId": "unifiedHistogram", @@ -708,6 +333,67 @@ "deprecated": false, "trackAdoption": false }, + { + "parentPluginId": "unifiedHistogram", + "id": "def-public.UnifiedHistogramLayoutProps.dataView", + "type": "Object", + "tags": [], + "label": "dataView", + "description": [ + "\nThe current data view" + ], + "signature": [ + { + "pluginId": "dataViews", + "scope": "common", + "docId": "kibDataViewsPluginApi", + "section": "def-common.DataView", + "text": "DataView" + } + ], + "path": "src/plugins/unified_histogram/public/layout/layout.tsx", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "unifiedHistogram", + "id": "def-public.UnifiedHistogramLayoutProps.lastReloadRequestTime", + "type": "number", + "tags": [], + "label": "lastReloadRequestTime", + "description": [ + "\nCan be updated to `Date.now()` to force a refresh" + ], + "signature": [ + "number | undefined" + ], + "path": "src/plugins/unified_histogram/public/layout/layout.tsx", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "unifiedHistogram", + "id": "def-public.UnifiedHistogramLayoutProps.request", + "type": "Object", + "tags": [], + "label": "request", + "description": [ + "\nContext object for requests made by unified histogram components -- optional" + ], + "signature": [ + { + "pluginId": "unifiedHistogram", + "scope": "public", + "docId": "kibUnifiedHistogramPluginApi", + "section": "def-public.UnifiedHistogramRequestContext", + "text": "UnifiedHistogramRequestContext" + }, + " | undefined" + ], + "path": "src/plugins/unified_histogram/public/layout/layout.tsx", + "deprecated": false, + "trackAdoption": false + }, { "parentPluginId": "unifiedHistogram", "id": "def-public.UnifiedHistogramLayoutProps.hits", @@ -754,6 +440,29 @@ "deprecated": false, "trackAdoption": false }, + { + "parentPluginId": "unifiedHistogram", + "id": "def-public.UnifiedHistogramLayoutProps.breakdown", + "type": "Object", + "tags": [], + "label": "breakdown", + "description": [ + "\nContext object for the breakdown -- leave undefined to hide the breakdown" + ], + "signature": [ + { + "pluginId": "unifiedHistogram", + "scope": "public", + "docId": "kibUnifiedHistogramPluginApi", + "section": "def-public.UnifiedHistogramBreakdownContext", + "text": "UnifiedHistogramBreakdownContext" + }, + " | undefined" + ], + "path": "src/plugins/unified_histogram/public/layout/layout.tsx", + "deprecated": false, + "trackAdoption": false + }, { "parentPluginId": "unifiedHistogram", "id": "def-public.UnifiedHistogramLayoutProps.resizeRef", @@ -846,12 +555,128 @@ "\nCallback to invoke when the user clicks the edit visualization button -- leave undefined to hide the button" ], "signature": [ - "(() => void) | undefined" + "((lensAttributes: LensAttributes<\"lnsXY\", ", + { + "pluginId": "lens", + "scope": "public", + "docId": "kibLensPluginApi", + "section": "def-public.XYState", + "text": "XYState" + }, + "> | LensAttributes<\"lnsPie\", ", + { + "pluginId": "lens", + "scope": "common", + "docId": "kibLensPluginApi", + "section": "def-common.PieVisualizationState", + "text": "PieVisualizationState" + }, + "> | LensAttributes<\"lnsDatatable\", ", + { + "pluginId": "lens", + "scope": "public", + "docId": "kibLensPluginApi", + "section": "def-public.DatatableVisualizationState", + "text": "DatatableVisualizationState" + }, + "> | LensAttributes<\"lnsLegacyMetric\", ", + { + "pluginId": "lens", + "scope": "common", + "docId": "kibLensPluginApi", + "section": "def-common.LegacyMetricState", + "text": "LegacyMetricState" + }, + "> | LensAttributes<\"lnsMetric\", ", + "MetricVisualizationState", + "> | LensAttributes<\"lnsHeatmap\", ", + { + "pluginId": "lens", + "scope": "public", + "docId": "kibLensPluginApi", + "section": "def-public.HeatmapVisualizationState", + "text": "HeatmapVisualizationState" + }, + "> | LensAttributes<\"lnsGauge\", ", + { + "pluginId": "lens", + "scope": "public", + "docId": "kibLensPluginApi", + "section": "def-public.GaugeVisualizationState", + "text": "GaugeVisualizationState" + }, + "> | LensAttributes) => void) | undefined" ], "path": "src/plugins/unified_histogram/public/layout/layout.tsx", "deprecated": false, "trackAdoption": false, - "children": [], + "children": [ + { + "parentPluginId": "unifiedHistogram", + "id": "def-public.UnifiedHistogramLayoutProps.onEditVisualization.$1", + "type": "CompoundType", + "tags": [], + "label": "lensAttributes", + "description": [], + "signature": [ + "LensAttributes<\"lnsXY\", ", + { + "pluginId": "lens", + "scope": "public", + "docId": "kibLensPluginApi", + "section": "def-public.XYState", + "text": "XYState" + }, + "> | LensAttributes<\"lnsPie\", ", + { + "pluginId": "lens", + "scope": "common", + "docId": "kibLensPluginApi", + "section": "def-common.PieVisualizationState", + "text": "PieVisualizationState" + }, + "> | LensAttributes<\"lnsDatatable\", ", + { + "pluginId": "lens", + "scope": "public", + "docId": "kibLensPluginApi", + "section": "def-public.DatatableVisualizationState", + "text": "DatatableVisualizationState" + }, + "> | LensAttributes<\"lnsLegacyMetric\", ", + { + "pluginId": "lens", + "scope": "common", + "docId": "kibLensPluginApi", + "section": "def-common.LegacyMetricState", + "text": "LegacyMetricState" + }, + "> | LensAttributes<\"lnsMetric\", ", + "MetricVisualizationState", + "> | LensAttributes<\"lnsHeatmap\", ", + { + "pluginId": "lens", + "scope": "public", + "docId": "kibLensPluginApi", + "section": "def-public.HeatmapVisualizationState", + "text": "HeatmapVisualizationState" + }, + "> | LensAttributes<\"lnsGauge\", ", + { + "pluginId": "lens", + "scope": "public", + "docId": "kibLensPluginApi", + "section": "def-public.GaugeVisualizationState", + "text": "GaugeVisualizationState" + }, + "> | LensAttributes" + ], + "path": "src/plugins/unified_histogram/public/layout/layout.tsx", + "deprecated": false, + "trackAdoption": false, + "isRequired": true + } + ], "returnComment": [] }, { @@ -921,6 +746,221 @@ } ], "returnComment": [] + }, + { + "parentPluginId": "unifiedHistogram", + "id": "def-public.UnifiedHistogramLayoutProps.onBreakdownFieldChange", + "type": "Function", + "tags": [], + "label": "onBreakdownFieldChange", + "description": [ + "\nCallback to update the breakdown field -- should set {@link UnifiedHistogramBreakdownContext.field} to breakdownField" + ], + "signature": [ + "((breakdownField: ", + { + "pluginId": "dataViews", + "scope": "common", + "docId": "kibDataViewsPluginApi", + "section": "def-common.DataViewField", + "text": "DataViewField" + }, + " | undefined) => void) | undefined" + ], + "path": "src/plugins/unified_histogram/public/layout/layout.tsx", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "unifiedHistogram", + "id": "def-public.UnifiedHistogramLayoutProps.onBreakdownFieldChange.$1", + "type": "Object", + "tags": [], + "label": "breakdownField", + "description": [], + "signature": [ + { + "pluginId": "dataViews", + "scope": "common", + "docId": "kibDataViewsPluginApi", + "section": "def-common.DataViewField", + "text": "DataViewField" + }, + " | undefined" + ], + "path": "src/plugins/unified_histogram/public/layout/layout.tsx", + "deprecated": false, + "trackAdoption": false, + "isRequired": false + } + ], + "returnComment": [] + }, + { + "parentPluginId": "unifiedHistogram", + "id": "def-public.UnifiedHistogramLayoutProps.onTotalHitsChange", + "type": "Function", + "tags": [], + "label": "onTotalHitsChange", + "description": [ + "\nCallback to update the total hits -- should set {@link UnifiedHistogramHitsContext.status} to status\nand {@link UnifiedHistogramHitsContext.total} to result" + ], + "signature": [ + "((status: ", + { + "pluginId": "unifiedHistogram", + "scope": "public", + "docId": "kibUnifiedHistogramPluginApi", + "section": "def-public.UnifiedHistogramFetchStatus", + "text": "UnifiedHistogramFetchStatus" + }, + ", result?: number | Error | undefined) => void) | undefined" + ], + "path": "src/plugins/unified_histogram/public/layout/layout.tsx", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "unifiedHistogram", + "id": "def-public.UnifiedHistogramLayoutProps.onTotalHitsChange.$1", + "type": "Enum", + "tags": [], + "label": "status", + "description": [], + "signature": [ + { + "pluginId": "unifiedHistogram", + "scope": "public", + "docId": "kibUnifiedHistogramPluginApi", + "section": "def-public.UnifiedHistogramFetchStatus", + "text": "UnifiedHistogramFetchStatus" + } + ], + "path": "src/plugins/unified_histogram/public/layout/layout.tsx", + "deprecated": false, + "trackAdoption": false, + "isRequired": true + }, + { + "parentPluginId": "unifiedHistogram", + "id": "def-public.UnifiedHistogramLayoutProps.onTotalHitsChange.$2", + "type": "CompoundType", + "tags": [], + "label": "result", + "description": [], + "signature": [ + "number | Error | undefined" + ], + "path": "src/plugins/unified_histogram/public/layout/layout.tsx", + "deprecated": false, + "trackAdoption": false, + "isRequired": false + } + ], + "returnComment": [] + }, + { + "parentPluginId": "unifiedHistogram", + "id": "def-public.UnifiedHistogramLayoutProps.onChartLoad", + "type": "Function", + "tags": [], + "label": "onChartLoad", + "description": [ + "\nCalled when the histogram loading status changes" + ], + "signature": [ + "((event: ", + { + "pluginId": "unifiedHistogram", + "scope": "public", + "docId": "kibUnifiedHistogramPluginApi", + "section": "def-public.UnifiedHistogramChartLoadEvent", + "text": "UnifiedHistogramChartLoadEvent" + }, + ") => void) | undefined" + ], + "path": "src/plugins/unified_histogram/public/layout/layout.tsx", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "unifiedHistogram", + "id": "def-public.UnifiedHistogramLayoutProps.onChartLoad.$1", + "type": "Object", + "tags": [], + "label": "event", + "description": [], + "signature": [ + { + "pluginId": "unifiedHistogram", + "scope": "public", + "docId": "kibUnifiedHistogramPluginApi", + "section": "def-public.UnifiedHistogramChartLoadEvent", + "text": "UnifiedHistogramChartLoadEvent" + } + ], + "path": "src/plugins/unified_histogram/public/layout/layout.tsx", + "deprecated": false, + "trackAdoption": false, + "isRequired": true + } + ], + "returnComment": [] + } + ], + "initialIsOpen": false + }, + { + "parentPluginId": "unifiedHistogram", + "id": "def-public.UnifiedHistogramRequestContext", + "type": "Interface", + "tags": [], + "label": "UnifiedHistogramRequestContext", + "description": [ + "\nContext object for requests made by unified histogram components" + ], + "path": "src/plugins/unified_histogram/public/types.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "unifiedHistogram", + "id": "def-public.UnifiedHistogramRequestContext.searchSessionId", + "type": "string", + "tags": [], + "label": "searchSessionId", + "description": [ + "\nCurrent search session ID" + ], + "signature": [ + "string | undefined" + ], + "path": "src/plugins/unified_histogram/public/types.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "unifiedHistogram", + "id": "def-public.UnifiedHistogramRequestContext.adapter", + "type": "Object", + "tags": [], + "label": "adapter", + "description": [ + "\nThe adapter to use for requests (does not apply to Lens requests)" + ], + "signature": [ + { + "pluginId": "inspector", + "scope": "common", + "docId": "kibInspectorPluginApi", + "section": "def-common.RequestAdapter", + "text": "RequestAdapter" + }, + " | undefined" + ], + "path": "src/plugins/unified_histogram/public/types.ts", + "deprecated": false, + "trackAdoption": false } ], "initialIsOpen": false @@ -1045,24 +1085,81 @@ "path": "src/plugins/unified_histogram/public/types.ts", "deprecated": false, "trackAdoption": false + }, + { + "parentPluginId": "unifiedHistogram", + "id": "def-public.UnifiedHistogramServices.lens", + "type": "Object", + "tags": [], + "label": "lens", + "description": [], + "signature": [ + { + "pluginId": "lens", + "scope": "public", + "docId": "kibLensPluginApi", + "section": "def-public.LensPublicStart", + "text": "LensPublicStart" + } + ], + "path": "src/plugins/unified_histogram/public/types.ts", + "deprecated": false, + "trackAdoption": false } ], "initialIsOpen": false } ], - "enums": [], - "misc": [ + "enums": [ { "parentPluginId": "unifiedHistogram", "id": "def-public.UnifiedHistogramFetchStatus", - "type": "Type", + "type": "Enum", "tags": [], "label": "UnifiedHistogramFetchStatus", "description": [ "\nThe fetch status of a unified histogram request" ], + "path": "src/plugins/unified_histogram/public/types.ts", + "deprecated": false, + "trackAdoption": false, + "initialIsOpen": false + } + ], + "misc": [ + { + "parentPluginId": "unifiedHistogram", + "id": "def-public.UnifiedHistogramAdapters", + "type": "Type", + "tags": [], + "label": "UnifiedHistogramAdapters", + "description": [], "signature": [ - "\"loading\" | \"error\" | \"complete\" | \"partial\" | \"uninitialized\"" + "{ requests?: ", + { + "pluginId": "inspector", + "scope": "common", + "docId": "kibInspectorPluginApi", + "section": "def-common.RequestAdapter", + "text": "RequestAdapter" + }, + " | undefined; tables?: ", + { + "pluginId": "expressions", + "scope": "common", + "docId": "kibExpressionsPluginApi", + "section": "def-common.TablesAdapter", + "text": "TablesAdapter" + }, + " | undefined; expression?: ", + { + "pluginId": "expressions", + "scope": "common", + "docId": "kibExpressionsPluginApi", + "section": "def-common.ExpressionsInspectorAdapter", + "text": "ExpressionsInspectorAdapter" + }, + " | undefined; }" ], "path": "src/plugins/unified_histogram/public/types.ts", "deprecated": false, diff --git a/api_docs/unified_histogram.mdx b/api_docs/unified_histogram.mdx index 64a855788c7b6..c7a8e59b0c1e7 100644 --- a/api_docs/unified_histogram.mdx +++ b/api_docs/unified_histogram.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/unifiedHistogram title: "unifiedHistogram" image: https://source.unsplash.com/400x175/?github description: API docs for the unifiedHistogram plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'unifiedHistogram'] --- import unifiedHistogramObj from './unified_histogram.devdocs.json'; @@ -21,7 +21,7 @@ Contact [Data Discovery](https://github.com/orgs/elastic/teams/kibana-data-disco | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 56 | 0 | 29 | 0 | +| 52 | 0 | 15 | 0 | ## Client @@ -31,6 +31,9 @@ Contact [Data Discovery](https://github.com/orgs/elastic/teams/kibana-data-disco ### Interfaces +### Enums + + ### Consts, variables and types diff --git a/api_docs/unified_search.devdocs.json b/api_docs/unified_search.devdocs.json index 67bef40170498..15e716b82ecad 100644 --- a/api_docs/unified_search.devdocs.json +++ b/api_docs/unified_search.devdocs.json @@ -2212,7 +2212,63 @@ }, ">, {}: ", "UnifiedSearchServerPluginSetupDependencies", - ") => { autocomplete: { getAutocompleteSettings: () => { terminateAfter: number; timeout: number; }; }; }" + ") => { autocomplete: { getAutocompleteSettings: () => { terminateAfter: number; timeout: number; }; getInitializerContextConfig: () => { legacy: { globalConfig$: ", + "Observable", + " moment.Duration; humanize: { (argWithSuffix?: boolean | undefined, argThresholds?: moment.argThresholdOpts | undefined): string; (argThresholds?: moment.argThresholdOpts | undefined): string; }; abs: () => moment.Duration; as: (units: moment.unitOfTime.Base) => number; get: (units: moment.unitOfTime.Base) => number; milliseconds: () => number; asMilliseconds: () => number; seconds: () => number; asSeconds: () => number; minutes: () => number; asMinutes: () => number; hours: () => number; asHours: () => number; days: () => number; asDays: () => number; weeks: () => number; asWeeks: () => number; months: () => number; asMonths: () => number; years: () => number; asYears: () => number; add: (inp?: moment.DurationInputArg1, unit?: moment.unitOfTime.DurationConstructor | undefined) => moment.Duration; subtract: (inp?: moment.DurationInputArg1, unit?: moment.unitOfTime.DurationConstructor | undefined) => moment.Duration; locale: { (): string; (locale: moment.LocaleSpecifier): moment.Duration; }; localeData: () => moment.Locale; toISOString: () => string; toJSON: () => string; isValid: () => boolean; lang: { (locale: moment.LocaleSpecifier): moment.Moment; (): moment.Locale; }; toIsoString: () => string; format: moment.Format; }>; readonly shardTimeout: Readonly<{ clone: () => moment.Duration; humanize: { (argWithSuffix?: boolean | undefined, argThresholds?: moment.argThresholdOpts | undefined): string; (argThresholds?: moment.argThresholdOpts | undefined): string; }; abs: () => moment.Duration; as: (units: moment.unitOfTime.Base) => number; get: (units: moment.unitOfTime.Base) => number; milliseconds: () => number; asMilliseconds: () => number; seconds: () => number; asSeconds: () => number; minutes: () => number; asMinutes: () => number; hours: () => number; asHours: () => number; days: () => number; asDays: () => number; weeks: () => number; asWeeks: () => number; months: () => number; asMonths: () => number; years: () => number; asYears: () => number; add: (inp?: moment.DurationInputArg1, unit?: moment.unitOfTime.DurationConstructor | undefined) => moment.Duration; subtract: (inp?: moment.DurationInputArg1, unit?: moment.unitOfTime.DurationConstructor | undefined) => moment.Duration; locale: { (): string; (locale: moment.LocaleSpecifier): moment.Duration; }; localeData: () => moment.Locale; toISOString: () => string; toJSON: () => string; isValid: () => boolean; lang: { (locale: moment.LocaleSpecifier): moment.Moment; (): moment.Locale; }; toIsoString: () => string; format: moment.Format; }>; readonly pingTimeout: Readonly<{ clone: () => moment.Duration; humanize: { (argWithSuffix?: boolean | undefined, argThresholds?: moment.argThresholdOpts | undefined): string; (argThresholds?: moment.argThresholdOpts | undefined): string; }; abs: () => moment.Duration; as: (units: moment.unitOfTime.Base) => number; get: (units: moment.unitOfTime.Base) => number; milliseconds: () => number; asMilliseconds: () => number; seconds: () => number; asSeconds: () => number; minutes: () => number; asMinutes: () => number; hours: () => number; asHours: () => number; days: () => number; asDays: () => number; weeks: () => number; asWeeks: () => number; months: () => number; asMonths: () => number; years: () => number; asYears: () => number; add: (inp?: moment.DurationInputArg1, unit?: moment.unitOfTime.DurationConstructor | undefined) => moment.Duration; subtract: (inp?: moment.DurationInputArg1, unit?: moment.unitOfTime.DurationConstructor | undefined) => moment.Duration; locale: { (): string; (locale: moment.LocaleSpecifier): moment.Duration; }; localeData: () => moment.Locale; toISOString: () => string; toJSON: () => string; isValid: () => boolean; lang: { (locale: moment.LocaleSpecifier): moment.Moment; (): moment.Locale; }; toIsoString: () => string; format: moment.Format; }>; }>; path: Readonly<{ readonly data: string; }>; savedObjects: Readonly<{ readonly maxImportPayloadBytes: Readonly<{ isGreaterThan: (other: ", + { + "pluginId": "@kbn/config-schema", + "scope": "server", + "docId": "kibKbnConfigSchemaPluginApi", + "section": "def-server.ByteSizeValue", + "text": "ByteSizeValue" + }, + ") => boolean; isLessThan: (other: ", + { + "pluginId": "@kbn/config-schema", + "scope": "server", + "docId": "kibKbnConfigSchemaPluginApi", + "section": "def-server.ByteSizeValue", + "text": "ByteSizeValue" + }, + ") => boolean; isEqualTo: (other: ", + { + "pluginId": "@kbn/config-schema", + "scope": "server", + "docId": "kibKbnConfigSchemaPluginApi", + "section": "def-server.ByteSizeValue", + "text": "ByteSizeValue" + }, + ") => boolean; getValueInBytes: () => number; toString: (returnUnit?: ", + "ByteSizeValueUnit", + " | undefined) => string; }>; }>; }>>; get: () => Readonly<{ elasticsearch: Readonly<{ readonly requestTimeout: Readonly<{ clone: () => moment.Duration; humanize: { (argWithSuffix?: boolean | undefined, argThresholds?: moment.argThresholdOpts | undefined): string; (argThresholds?: moment.argThresholdOpts | undefined): string; }; abs: () => moment.Duration; as: (units: moment.unitOfTime.Base) => number; get: (units: moment.unitOfTime.Base) => number; milliseconds: () => number; asMilliseconds: () => number; seconds: () => number; asSeconds: () => number; minutes: () => number; asMinutes: () => number; hours: () => number; asHours: () => number; days: () => number; asDays: () => number; weeks: () => number; asWeeks: () => number; months: () => number; asMonths: () => number; years: () => number; asYears: () => number; add: (inp?: moment.DurationInputArg1, unit?: moment.unitOfTime.DurationConstructor | undefined) => moment.Duration; subtract: (inp?: moment.DurationInputArg1, unit?: moment.unitOfTime.DurationConstructor | undefined) => moment.Duration; locale: { (): string; (locale: moment.LocaleSpecifier): moment.Duration; }; localeData: () => moment.Locale; toISOString: () => string; toJSON: () => string; isValid: () => boolean; lang: { (locale: moment.LocaleSpecifier): moment.Moment; (): moment.Locale; }; toIsoString: () => string; format: moment.Format; }>; readonly shardTimeout: Readonly<{ clone: () => moment.Duration; humanize: { (argWithSuffix?: boolean | undefined, argThresholds?: moment.argThresholdOpts | undefined): string; (argThresholds?: moment.argThresholdOpts | undefined): string; }; abs: () => moment.Duration; as: (units: moment.unitOfTime.Base) => number; get: (units: moment.unitOfTime.Base) => number; milliseconds: () => number; asMilliseconds: () => number; seconds: () => number; asSeconds: () => number; minutes: () => number; asMinutes: () => number; hours: () => number; asHours: () => number; days: () => number; asDays: () => number; weeks: () => number; asWeeks: () => number; months: () => number; asMonths: () => number; years: () => number; asYears: () => number; add: (inp?: moment.DurationInputArg1, unit?: moment.unitOfTime.DurationConstructor | undefined) => moment.Duration; subtract: (inp?: moment.DurationInputArg1, unit?: moment.unitOfTime.DurationConstructor | undefined) => moment.Duration; locale: { (): string; (locale: moment.LocaleSpecifier): moment.Duration; }; localeData: () => moment.Locale; toISOString: () => string; toJSON: () => string; isValid: () => boolean; lang: { (locale: moment.LocaleSpecifier): moment.Moment; (): moment.Locale; }; toIsoString: () => string; format: moment.Format; }>; readonly pingTimeout: Readonly<{ clone: () => moment.Duration; humanize: { (argWithSuffix?: boolean | undefined, argThresholds?: moment.argThresholdOpts | undefined): string; (argThresholds?: moment.argThresholdOpts | undefined): string; }; abs: () => moment.Duration; as: (units: moment.unitOfTime.Base) => number; get: (units: moment.unitOfTime.Base) => number; milliseconds: () => number; asMilliseconds: () => number; seconds: () => number; asSeconds: () => number; minutes: () => number; asMinutes: () => number; hours: () => number; asHours: () => number; days: () => number; asDays: () => number; weeks: () => number; asWeeks: () => number; months: () => number; asMonths: () => number; years: () => number; asYears: () => number; add: (inp?: moment.DurationInputArg1, unit?: moment.unitOfTime.DurationConstructor | undefined) => moment.Duration; subtract: (inp?: moment.DurationInputArg1, unit?: moment.unitOfTime.DurationConstructor | undefined) => moment.Duration; locale: { (): string; (locale: moment.LocaleSpecifier): moment.Duration; }; localeData: () => moment.Locale; toISOString: () => string; toJSON: () => string; isValid: () => boolean; lang: { (locale: moment.LocaleSpecifier): moment.Moment; (): moment.Locale; }; toIsoString: () => string; format: moment.Format; }>; }>; path: Readonly<{ readonly data: string; }>; savedObjects: Readonly<{ readonly maxImportPayloadBytes: Readonly<{ isGreaterThan: (other: ", + { + "pluginId": "@kbn/config-schema", + "scope": "server", + "docId": "kibKbnConfigSchemaPluginApi", + "section": "def-server.ByteSizeValue", + "text": "ByteSizeValue" + }, + ") => boolean; isLessThan: (other: ", + { + "pluginId": "@kbn/config-schema", + "scope": "server", + "docId": "kibKbnConfigSchemaPluginApi", + "section": "def-server.ByteSizeValue", + "text": "ByteSizeValue" + }, + ") => boolean; isEqualTo: (other: ", + { + "pluginId": "@kbn/config-schema", + "scope": "server", + "docId": "kibKbnConfigSchemaPluginApi", + "section": "def-server.ByteSizeValue", + "text": "ByteSizeValue" + }, + ") => boolean; getValueInBytes: () => number; toString: (returnUnit?: ", + "ByteSizeValueUnit", + " | undefined) => string; }>; }>; }>; }; create: ; valueSuggestions: Readonly<{} & { timeout: moment.Duration; enabled: boolean; tiers: string[]; terminateAfter: moment.Duration; }>; }>; }>>() => ", + "Observable", + "; get: ; valueSuggestions: Readonly<{} & { timeout: moment.Duration; enabled: boolean; tiers: string[]; terminateAfter: moment.Duration; }>; }>; }>>() => T; }; }; }" ], "path": "src/plugins/unified_search/server/plugin.ts", "deprecated": false, @@ -2389,7 +2445,63 @@ "label": "autocomplete", "description": [], "signature": [ - "{ getAutocompleteSettings: () => { terminateAfter: number; timeout: number; }; }" + "{ getAutocompleteSettings: () => { terminateAfter: number; timeout: number; }; getInitializerContextConfig: () => { legacy: { globalConfig$: ", + "Observable", + " moment.Duration; humanize: { (argWithSuffix?: boolean | undefined, argThresholds?: moment.argThresholdOpts | undefined): string; (argThresholds?: moment.argThresholdOpts | undefined): string; }; abs: () => moment.Duration; as: (units: moment.unitOfTime.Base) => number; get: (units: moment.unitOfTime.Base) => number; milliseconds: () => number; asMilliseconds: () => number; seconds: () => number; asSeconds: () => number; minutes: () => number; asMinutes: () => number; hours: () => number; asHours: () => number; days: () => number; asDays: () => number; weeks: () => number; asWeeks: () => number; months: () => number; asMonths: () => number; years: () => number; asYears: () => number; add: (inp?: moment.DurationInputArg1, unit?: moment.unitOfTime.DurationConstructor | undefined) => moment.Duration; subtract: (inp?: moment.DurationInputArg1, unit?: moment.unitOfTime.DurationConstructor | undefined) => moment.Duration; locale: { (): string; (locale: moment.LocaleSpecifier): moment.Duration; }; localeData: () => moment.Locale; toISOString: () => string; toJSON: () => string; isValid: () => boolean; lang: { (locale: moment.LocaleSpecifier): moment.Moment; (): moment.Locale; }; toIsoString: () => string; format: moment.Format; }>; readonly shardTimeout: Readonly<{ clone: () => moment.Duration; humanize: { (argWithSuffix?: boolean | undefined, argThresholds?: moment.argThresholdOpts | undefined): string; (argThresholds?: moment.argThresholdOpts | undefined): string; }; abs: () => moment.Duration; as: (units: moment.unitOfTime.Base) => number; get: (units: moment.unitOfTime.Base) => number; milliseconds: () => number; asMilliseconds: () => number; seconds: () => number; asSeconds: () => number; minutes: () => number; asMinutes: () => number; hours: () => number; asHours: () => number; days: () => number; asDays: () => number; weeks: () => number; asWeeks: () => number; months: () => number; asMonths: () => number; years: () => number; asYears: () => number; add: (inp?: moment.DurationInputArg1, unit?: moment.unitOfTime.DurationConstructor | undefined) => moment.Duration; subtract: (inp?: moment.DurationInputArg1, unit?: moment.unitOfTime.DurationConstructor | undefined) => moment.Duration; locale: { (): string; (locale: moment.LocaleSpecifier): moment.Duration; }; localeData: () => moment.Locale; toISOString: () => string; toJSON: () => string; isValid: () => boolean; lang: { (locale: moment.LocaleSpecifier): moment.Moment; (): moment.Locale; }; toIsoString: () => string; format: moment.Format; }>; readonly pingTimeout: Readonly<{ clone: () => moment.Duration; humanize: { (argWithSuffix?: boolean | undefined, argThresholds?: moment.argThresholdOpts | undefined): string; (argThresholds?: moment.argThresholdOpts | undefined): string; }; abs: () => moment.Duration; as: (units: moment.unitOfTime.Base) => number; get: (units: moment.unitOfTime.Base) => number; milliseconds: () => number; asMilliseconds: () => number; seconds: () => number; asSeconds: () => number; minutes: () => number; asMinutes: () => number; hours: () => number; asHours: () => number; days: () => number; asDays: () => number; weeks: () => number; asWeeks: () => number; months: () => number; asMonths: () => number; years: () => number; asYears: () => number; add: (inp?: moment.DurationInputArg1, unit?: moment.unitOfTime.DurationConstructor | undefined) => moment.Duration; subtract: (inp?: moment.DurationInputArg1, unit?: moment.unitOfTime.DurationConstructor | undefined) => moment.Duration; locale: { (): string; (locale: moment.LocaleSpecifier): moment.Duration; }; localeData: () => moment.Locale; toISOString: () => string; toJSON: () => string; isValid: () => boolean; lang: { (locale: moment.LocaleSpecifier): moment.Moment; (): moment.Locale; }; toIsoString: () => string; format: moment.Format; }>; }>; path: Readonly<{ readonly data: string; }>; savedObjects: Readonly<{ readonly maxImportPayloadBytes: Readonly<{ isGreaterThan: (other: ", + { + "pluginId": "@kbn/config-schema", + "scope": "server", + "docId": "kibKbnConfigSchemaPluginApi", + "section": "def-server.ByteSizeValue", + "text": "ByteSizeValue" + }, + ") => boolean; isLessThan: (other: ", + { + "pluginId": "@kbn/config-schema", + "scope": "server", + "docId": "kibKbnConfigSchemaPluginApi", + "section": "def-server.ByteSizeValue", + "text": "ByteSizeValue" + }, + ") => boolean; isEqualTo: (other: ", + { + "pluginId": "@kbn/config-schema", + "scope": "server", + "docId": "kibKbnConfigSchemaPluginApi", + "section": "def-server.ByteSizeValue", + "text": "ByteSizeValue" + }, + ") => boolean; getValueInBytes: () => number; toString: (returnUnit?: ", + "ByteSizeValueUnit", + " | undefined) => string; }>; }>; }>>; get: () => Readonly<{ elasticsearch: Readonly<{ readonly requestTimeout: Readonly<{ clone: () => moment.Duration; humanize: { (argWithSuffix?: boolean | undefined, argThresholds?: moment.argThresholdOpts | undefined): string; (argThresholds?: moment.argThresholdOpts | undefined): string; }; abs: () => moment.Duration; as: (units: moment.unitOfTime.Base) => number; get: (units: moment.unitOfTime.Base) => number; milliseconds: () => number; asMilliseconds: () => number; seconds: () => number; asSeconds: () => number; minutes: () => number; asMinutes: () => number; hours: () => number; asHours: () => number; days: () => number; asDays: () => number; weeks: () => number; asWeeks: () => number; months: () => number; asMonths: () => number; years: () => number; asYears: () => number; add: (inp?: moment.DurationInputArg1, unit?: moment.unitOfTime.DurationConstructor | undefined) => moment.Duration; subtract: (inp?: moment.DurationInputArg1, unit?: moment.unitOfTime.DurationConstructor | undefined) => moment.Duration; locale: { (): string; (locale: moment.LocaleSpecifier): moment.Duration; }; localeData: () => moment.Locale; toISOString: () => string; toJSON: () => string; isValid: () => boolean; lang: { (locale: moment.LocaleSpecifier): moment.Moment; (): moment.Locale; }; toIsoString: () => string; format: moment.Format; }>; readonly shardTimeout: Readonly<{ clone: () => moment.Duration; humanize: { (argWithSuffix?: boolean | undefined, argThresholds?: moment.argThresholdOpts | undefined): string; (argThresholds?: moment.argThresholdOpts | undefined): string; }; abs: () => moment.Duration; as: (units: moment.unitOfTime.Base) => number; get: (units: moment.unitOfTime.Base) => number; milliseconds: () => number; asMilliseconds: () => number; seconds: () => number; asSeconds: () => number; minutes: () => number; asMinutes: () => number; hours: () => number; asHours: () => number; days: () => number; asDays: () => number; weeks: () => number; asWeeks: () => number; months: () => number; asMonths: () => number; years: () => number; asYears: () => number; add: (inp?: moment.DurationInputArg1, unit?: moment.unitOfTime.DurationConstructor | undefined) => moment.Duration; subtract: (inp?: moment.DurationInputArg1, unit?: moment.unitOfTime.DurationConstructor | undefined) => moment.Duration; locale: { (): string; (locale: moment.LocaleSpecifier): moment.Duration; }; localeData: () => moment.Locale; toISOString: () => string; toJSON: () => string; isValid: () => boolean; lang: { (locale: moment.LocaleSpecifier): moment.Moment; (): moment.Locale; }; toIsoString: () => string; format: moment.Format; }>; readonly pingTimeout: Readonly<{ clone: () => moment.Duration; humanize: { (argWithSuffix?: boolean | undefined, argThresholds?: moment.argThresholdOpts | undefined): string; (argThresholds?: moment.argThresholdOpts | undefined): string; }; abs: () => moment.Duration; as: (units: moment.unitOfTime.Base) => number; get: (units: moment.unitOfTime.Base) => number; milliseconds: () => number; asMilliseconds: () => number; seconds: () => number; asSeconds: () => number; minutes: () => number; asMinutes: () => number; hours: () => number; asHours: () => number; days: () => number; asDays: () => number; weeks: () => number; asWeeks: () => number; months: () => number; asMonths: () => number; years: () => number; asYears: () => number; add: (inp?: moment.DurationInputArg1, unit?: moment.unitOfTime.DurationConstructor | undefined) => moment.Duration; subtract: (inp?: moment.DurationInputArg1, unit?: moment.unitOfTime.DurationConstructor | undefined) => moment.Duration; locale: { (): string; (locale: moment.LocaleSpecifier): moment.Duration; }; localeData: () => moment.Locale; toISOString: () => string; toJSON: () => string; isValid: () => boolean; lang: { (locale: moment.LocaleSpecifier): moment.Moment; (): moment.Locale; }; toIsoString: () => string; format: moment.Format; }>; }>; path: Readonly<{ readonly data: string; }>; savedObjects: Readonly<{ readonly maxImportPayloadBytes: Readonly<{ isGreaterThan: (other: ", + { + "pluginId": "@kbn/config-schema", + "scope": "server", + "docId": "kibKbnConfigSchemaPluginApi", + "section": "def-server.ByteSizeValue", + "text": "ByteSizeValue" + }, + ") => boolean; isLessThan: (other: ", + { + "pluginId": "@kbn/config-schema", + "scope": "server", + "docId": "kibKbnConfigSchemaPluginApi", + "section": "def-server.ByteSizeValue", + "text": "ByteSizeValue" + }, + ") => boolean; isEqualTo: (other: ", + { + "pluginId": "@kbn/config-schema", + "scope": "server", + "docId": "kibKbnConfigSchemaPluginApi", + "section": "def-server.ByteSizeValue", + "text": "ByteSizeValue" + }, + ") => boolean; getValueInBytes: () => number; toString: (returnUnit?: ", + "ByteSizeValueUnit", + " | undefined) => string; }>; }>; }>; }; create: ; valueSuggestions: Readonly<{} & { timeout: moment.Duration; enabled: boolean; tiers: string[]; terminateAfter: moment.Duration; }>; }>; }>>() => ", + "Observable", + "; get: ; valueSuggestions: Readonly<{} & { timeout: moment.Duration; enabled: boolean; tiers: string[]; terminateAfter: moment.Duration; }>; }>; }>>() => T; }; }" ], "path": "src/plugins/unified_search/server/plugin.ts", "deprecated": false, diff --git a/api_docs/unified_search.mdx b/api_docs/unified_search.mdx index 06ec05f163526..ac8feec060974 100644 --- a/api_docs/unified_search.mdx +++ b/api_docs/unified_search.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/unifiedSearch title: "unifiedSearch" image: https://source.unsplash.com/400x175/?github description: API docs for the unifiedSearch plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'unifiedSearch'] --- import unifiedSearchObj from './unified_search.devdocs.json'; diff --git a/api_docs/unified_search_autocomplete.mdx b/api_docs/unified_search_autocomplete.mdx index cef0c73e18cb0..45e161625ae54 100644 --- a/api_docs/unified_search_autocomplete.mdx +++ b/api_docs/unified_search_autocomplete.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/unifiedSearch-autocomplete title: "unifiedSearch.autocomplete" image: https://source.unsplash.com/400x175/?github description: API docs for the unifiedSearch.autocomplete plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'unifiedSearch.autocomplete'] --- import unifiedSearchAutocompleteObj from './unified_search_autocomplete.devdocs.json'; diff --git a/api_docs/url_forwarding.mdx b/api_docs/url_forwarding.mdx index 382418e03b4da..512e730b0aee9 100644 --- a/api_docs/url_forwarding.mdx +++ b/api_docs/url_forwarding.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/urlForwarding title: "urlForwarding" image: https://source.unsplash.com/400x175/?github description: API docs for the urlForwarding plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'urlForwarding'] --- import urlForwardingObj from './url_forwarding.devdocs.json'; diff --git a/api_docs/usage_collection.mdx b/api_docs/usage_collection.mdx index 904484c918c7e..a4a8b6452de58 100644 --- a/api_docs/usage_collection.mdx +++ b/api_docs/usage_collection.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/usageCollection title: "usageCollection" image: https://source.unsplash.com/400x175/?github description: API docs for the usageCollection plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'usageCollection'] --- import usageCollectionObj from './usage_collection.devdocs.json'; diff --git a/api_docs/ux.mdx b/api_docs/ux.mdx index 4084a58ab0d93..b8b7d815d8a82 100644 --- a/api_docs/ux.mdx +++ b/api_docs/ux.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/ux title: "ux" image: https://source.unsplash.com/400x175/?github description: API docs for the ux plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'ux'] --- import uxObj from './ux.devdocs.json'; diff --git a/api_docs/vis_default_editor.mdx b/api_docs/vis_default_editor.mdx index c6a121a640ed6..7c165dc46da7b 100644 --- a/api_docs/vis_default_editor.mdx +++ b/api_docs/vis_default_editor.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/visDefaultEditor title: "visDefaultEditor" image: https://source.unsplash.com/400x175/?github description: API docs for the visDefaultEditor plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'visDefaultEditor'] --- import visDefaultEditorObj from './vis_default_editor.devdocs.json'; diff --git a/api_docs/vis_type_gauge.mdx b/api_docs/vis_type_gauge.mdx index 2371c03d99166..9ffb9ff4d0d63 100644 --- a/api_docs/vis_type_gauge.mdx +++ b/api_docs/vis_type_gauge.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/visTypeGauge title: "visTypeGauge" image: https://source.unsplash.com/400x175/?github description: API docs for the visTypeGauge plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'visTypeGauge'] --- import visTypeGaugeObj from './vis_type_gauge.devdocs.json'; diff --git a/api_docs/vis_type_heatmap.mdx b/api_docs/vis_type_heatmap.mdx index 65c8234849502..53d494c4f3403 100644 --- a/api_docs/vis_type_heatmap.mdx +++ b/api_docs/vis_type_heatmap.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/visTypeHeatmap title: "visTypeHeatmap" image: https://source.unsplash.com/400x175/?github description: API docs for the visTypeHeatmap plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'visTypeHeatmap'] --- import visTypeHeatmapObj from './vis_type_heatmap.devdocs.json'; diff --git a/api_docs/vis_type_pie.mdx b/api_docs/vis_type_pie.mdx index 0d2f6ccdd72b3..f8a66456be7fa 100644 --- a/api_docs/vis_type_pie.mdx +++ b/api_docs/vis_type_pie.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/visTypePie title: "visTypePie" image: https://source.unsplash.com/400x175/?github description: API docs for the visTypePie plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'visTypePie'] --- import visTypePieObj from './vis_type_pie.devdocs.json'; diff --git a/api_docs/vis_type_table.mdx b/api_docs/vis_type_table.mdx index 87b7a881b7c4d..eae1fd7e013e7 100644 --- a/api_docs/vis_type_table.mdx +++ b/api_docs/vis_type_table.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/visTypeTable title: "visTypeTable" image: https://source.unsplash.com/400x175/?github description: API docs for the visTypeTable plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'visTypeTable'] --- import visTypeTableObj from './vis_type_table.devdocs.json'; diff --git a/api_docs/vis_type_timelion.mdx b/api_docs/vis_type_timelion.mdx index 5b6ae76f18196..d0efbcd35420e 100644 --- a/api_docs/vis_type_timelion.mdx +++ b/api_docs/vis_type_timelion.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/visTypeTimelion title: "visTypeTimelion" image: https://source.unsplash.com/400x175/?github description: API docs for the visTypeTimelion plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'visTypeTimelion'] --- import visTypeTimelionObj from './vis_type_timelion.devdocs.json'; diff --git a/api_docs/vis_type_timeseries.mdx b/api_docs/vis_type_timeseries.mdx index 797f555481bf0..57956d3525bfe 100644 --- a/api_docs/vis_type_timeseries.mdx +++ b/api_docs/vis_type_timeseries.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/visTypeTimeseries title: "visTypeTimeseries" image: https://source.unsplash.com/400x175/?github description: API docs for the visTypeTimeseries plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'visTypeTimeseries'] --- import visTypeTimeseriesObj from './vis_type_timeseries.devdocs.json'; diff --git a/api_docs/vis_type_vega.mdx b/api_docs/vis_type_vega.mdx index a74920d81362f..2659d394fa84d 100644 --- a/api_docs/vis_type_vega.mdx +++ b/api_docs/vis_type_vega.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/visTypeVega title: "visTypeVega" image: https://source.unsplash.com/400x175/?github description: API docs for the visTypeVega plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'visTypeVega'] --- import visTypeVegaObj from './vis_type_vega.devdocs.json'; diff --git a/api_docs/vis_type_vislib.mdx b/api_docs/vis_type_vislib.mdx index 119cdad8ab3ad..c8dae0305ce90 100644 --- a/api_docs/vis_type_vislib.mdx +++ b/api_docs/vis_type_vislib.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/visTypeVislib title: "visTypeVislib" image: https://source.unsplash.com/400x175/?github description: API docs for the visTypeVislib plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'visTypeVislib'] --- import visTypeVislibObj from './vis_type_vislib.devdocs.json'; diff --git a/api_docs/vis_type_xy.mdx b/api_docs/vis_type_xy.mdx index cf7624685ea13..f5e81ee08a2bd 100644 --- a/api_docs/vis_type_xy.mdx +++ b/api_docs/vis_type_xy.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/visTypeXy title: "visTypeXy" image: https://source.unsplash.com/400x175/?github description: API docs for the visTypeXy plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'visTypeXy'] --- import visTypeXyObj from './vis_type_xy.devdocs.json'; diff --git a/api_docs/visualizations.mdx b/api_docs/visualizations.mdx index eeda7338e6620..c4b86fdc63492 100644 --- a/api_docs/visualizations.mdx +++ b/api_docs/visualizations.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/visualizations title: "visualizations" image: https://source.unsplash.com/400x175/?github description: API docs for the visualizations plugin -date: 2022-11-28 +date: 2022-12-07 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'visualizations'] --- import visualizationsObj from './visualizations.devdocs.json'; diff --git a/docs/api-generated/cases/case-apis-passthru.asciidoc b/docs/api-generated/cases/case-apis-passthru.asciidoc index 16394ac263cee..80e8c7eb9cac7 100644 --- a/docs/api-generated/cases/case-apis-passthru.asciidoc +++ b/docs/api-generated/cases/case-apis-passthru.asciidoc @@ -1085,7 +1085,9 @@ Any modifications made to this file will be overwritten.

Query parameters

-

getCases_reporters_parameter - Up

+

getCases_owner_parameter - Up

diff --git a/docs/api/cases/cases-api-find-cases.asciidoc b/docs/api/cases/cases-api-find-cases.asciidoc index ff79471a677fa..4f80086e8f8c4 100644 --- a/docs/api/cases/cases-api-find-cases.asciidoc +++ b/docs/api/cases/cases-api-find-cases.asciidoc @@ -33,6 +33,12 @@ default space is used. === {api-query-parms-title} +`assignees`:: +(Optional, string or array of strings) Filters the returned cases by assignees. +Valid values are `none` or unique identifiers for the user profiles. These +identifiers can be found by using the +{ref}/security-api-suggest-user-profile.html[suggest user profile API]. + `defaultSearchOperator`:: (Optional, string) The default operator to use for the `simple_query_string`. Defaults to `OR`. diff --git a/docs/apm/apm-spaces.asciidoc b/docs/apm/apm-spaces.asciidoc index c43a512768fad..093b7560cd5aa 100644 --- a/docs/apm/apm-spaces.asciidoc +++ b/docs/apm/apm-spaces.asciidoc @@ -56,7 +56,7 @@ aliases for each service environment: [options="header"] |==== -| Index setting | `production` env | `staging` evn +| Index setting | `production` env | `staging` env | Error | `production-logs-apm` | `staging-logs-apm` | Span/Transaction | `production-traces-apm` | `staging-traces-apm` | Metrics | `production-metrics-apm` | `staging-metrics-apm` diff --git a/docs/canvas/canvas-tutorial.asciidoc b/docs/canvas/canvas-tutorial.asciidoc index 73d808a183920..f8d2297df18b9 100644 --- a/docs/canvas/canvas-tutorial.asciidoc +++ b/docs/canvas/canvas-tutorial.asciidoc @@ -44,11 +44,11 @@ Customize your data by connecting it to the Sample eCommerce orders data. . Click *Add element > Chart > Metric*. + -By default, the element is connected to the demo data, which enables you to experiment with the element before you connect it to your own data source. +By default, the element is connected to the demo data, which enables you to experiment with the element before you connect it to your own. -. To connect the element to your own data source, make sure that the element is selected, click *Data > Demo data > Elasticsearch SQL*. +. To connect the element to your own data, make sure the element is selected, then click *Data > Demo data > Elasticsearch SQL*. -.. In the *Query* field, enter: +.. To select the total price field and set it to the sum_total_price field, enter the following in the *Query* field: + [source,text] -- @@ -57,13 +57,13 @@ SELECT sum(taxless_total_price) AS sum_total_price FROM "kibana_sample_data_ecom .. Click *Save*. + -The query selects the total price field and sets it to the sum_total_price field. All fields are pulled from the kibana_sample_data_ecommerce index. +All fields are pulled from the sample eCommerce orders {data-source}. . At this point, the element appears as an error, so you need to change the element display options. .. Click *Display* -.. From the *Value* dropdowns, make sure *Unique* is selected, then select *sum_total_price*. +.. From the *Value* dropdowns, make sure *Unique* and *sum_total_price* are selected. .. Change the *Label* to `Total sales`. @@ -102,9 +102,9 @@ SELECT order_date, taxless_total_price FROM "kibana_sample_data_ecommerce" ORDER .. Click *Display* -.. From the *X-axis* drop-down lists, select *Value*, then select *order_date*. +.. From the *X-axis* dropdown, make sure *Value* and *order_date* are selected. -.. From the *Y-axis* drop-down lists, select *Value*, then select *taxless_total_price*. +.. From the *Y-axis* dropdown, select *Value*, then select *taxless_total_price*. [role="screenshot"] image::images/canvas_tutorialCustomChart_7.17.0.png[Custom line chart added to the workpad using Elasticsearch SQL] diff --git a/docs/concepts/data-views.asciidoc b/docs/concepts/data-views.asciidoc index 2ce89474a003a..d2155820545f9 100644 --- a/docs/concepts/data-views.asciidoc +++ b/docs/concepts/data-views.asciidoc @@ -2,8 +2,6 @@ === Create a {data-source} {kib} requires a {data-source} to access the {es} data that you want to explore. -A {data-source} selects the data to use and allows you to define properties of the fields. - A {data-source} can point to one or more indices, {ref}/data-streams.html[data streams], or {ref}/alias.html[index aliases]. For example, a {data-source} can point to your log data from yesterday, or all indices that contain your data. @@ -19,7 +17,7 @@ or all indices that contain your data. `view_index_metadata`. * If a read-only indicator appears in {kib}, you have insufficient privileges -to create or save {data-sources}. The buttons to create {data-sources} or +to create or save {data-sources}. In addition, the buttons to create {data-sources} or save existing {data-sources} are not visible. For more information, refer to <>. @@ -32,51 +30,67 @@ uploaded a file, or added sample data, you get a {data-source} for free, and can start exploring your data. If you loaded your own data, follow these steps to create a {data-source}. -. Open the main menu, then click *Stack Management > Data Views*. +. Open *Lens* or *Discover*, and then open the data view menu. ++ +[role="screenshot"] +image::images/discover-data-view.png[How to set the {data-source} in Discover, width="40%"] + +. Click *Create a {data-source}*. -. Click *Create {data-source}*. +. Give your {data-source} a name. -. Start typing in the *name* field, and {kib} looks for the names of +. Start typing in the *Index pattern* field, and {kib} looks for the names of indices, data streams, and aliases that match your input. + [role="screenshot"] image:management/index-patterns/images/create-data-view.png["Create data view"] + -** To match multiple sources, use a wildcard (*). For example, `filebeat-*` matches +** To match multiple sources, use a wildcard (*). `filebeat-*` matches `filebeat-apache-a`, `filebeat-apache-b`, and so on. + ** To match multiple single sources, enter their names, separated by a comma. Do not include a space after the comma. -`filebeat-a,filebeat-b` matches two indices, but does not match `filebeat-c`. +`filebeat-a,filebeat-b` matches two indices. + ** To exclude a source, use a minus sign (-), for example, `-test3`. -. If {kib} detects an index with a timestamp, expand the *Timestamp field* menu, +. Open the *Timestamp field* dropdown, and then select the default field for filtering your data by time. + -** If your index doesn’t have time-based data, choose *I don’t want to use the time filter*. -+ ** If you don’t set a default time field, you can't use global time filters on your dashboards. This is useful if you have multiple time fields and want to create dashboards that combine visualizations based on different timestamps. ++ +** If your index doesn’t have time-based data, choose *I don’t want to use the time filter*. -. To display all indices, click *Show advanced settings*, then select *Allow hidden and system indices*. +. Click *Show advanced settings* to: +** Display hidden and system indices. +** Specify your own {data-source} name. For example, enter your {es} index alias name. -. To specify your own {data-source} name, click *Show advanced settings*, then enter the name in the *Custom {data-source} ID* field. For example, enter your {es} index alias name. +. [[reload-fields]] Click *Save {data-source} to {kib}*. ++ +You can manage your data view from *Stack Management*. + +[float] +==== Create a temporary {data-source} -. [[reload-fields]] To use this data in searches and visualizations that you intend to save, click *Save {data-source} to {kib}*. +Want to explore your data or create a visualization without saving it as a data view? +Select *Use without saving* in the *Create {data-source}* form in *Discover* +or *Lens*. With a temporary {data-source}, you can add fields and create an {es} +query alert, just like you would a regular {data-source}. Your work won't be visible to others in your space. -. To explore your data without creating a {data-source}, click *Use without saving*. -+ -This allows you to explore your data in *Discover*, *Lens*, and *Maps* -without making it visible to others in your space. You can save the {data-source} later -if you create a search or visualization that you want to share. +A temporary {data-source} remains in your space until you change apps, or until you save it. + + +[role="screenshot"] +image::https://images.contentstack.io/v3/assets/bltefdd0b53724fa2ce/blte3a4f3994c44c0cc/637eb0c95834861044c21a25/ad-hoc-data-view.gif[how to create an ad-hoc data view] +NOTE: Temporary {data-sources} are not available in *Stack Management.* [float] [[rollup-data-view]] -==== Create a data view for rolled up data +==== Use {data-sources} with rolled up data A {data-source} can match one rollup index. For a combination rollup {data-source} with both raw and rolled up data, use the standard notation: @@ -88,11 +102,11 @@ For an example, refer to < Data Views*. +. Open the main menu, and then click *Stack Management > Data Views*. . Find the {data-source} that you want to delete, and then click image:management/index-patterns/images/delete.png[Delete icon] in the *Actions* column. diff --git a/docs/concepts/kuery.asciidoc b/docs/concepts/kuery.asciidoc index 4e8b6bc4043e0..4eb95fa444058 100644 --- a/docs/concepts/kuery.asciidoc +++ b/docs/concepts/kuery.asciidoc @@ -184,3 +184,65 @@ documents where any sub-field of `http.response` contains “error”, use the f ------------------- http.response.*: error ------------------- + +[discrete] +=== Querying nested fields + +Querying {ref}/nested.html[nested fields] requires a special syntax. Consider the +following document, where `user` is a nested field: + +[source,yaml] +------------------- +{ + "user" : [ + { + "first" : "John", + "last" : "Smith" + }, + { + "first" : "Alice", + "last" : "White" + } + ] +} +------------------- + +To find documents where a single value inside the `user` array contains a first name of +“Alice” and last name of “White”, use the following: + +[source,yaml] +------------------- +user:{ first: "Alice" and last: "White" } +------------------- + +Because nested fields can be inside other nested fields, +you must specify the full path of the nested field you want to query. +For example, consider the following document where `user` and `names` are both nested fields: + +[source,yaml] +------------------- +{ + "user": [ + { + "names": [ + { + "first": "John", + "last": "Smith" + }, + { + "first": "Alice", + "last": "White" + } + ] + } + ] +} +------------------- + +To find documents where a single value inside the `user.names` array contains a first name of “Alice” *and* +last name of “White”, use the following: + +[source,yaml] +------------------- +user.names:{ first: "Alice" and last: "White" } +------------------- diff --git a/docs/getting-started/images/addData_sampleDataCards_8.4.0.png b/docs/getting-started/images/addData_sampleDataCards_8.4.0.png deleted file mode 100644 index 49a83dbbdc10b..0000000000000 Binary files a/docs/getting-started/images/addData_sampleDataCards_8.4.0.png and /dev/null differ diff --git a/docs/getting-started/images/addData_sampleDataCards_8.6.0.png b/docs/getting-started/images/addData_sampleDataCards_8.6.0.png new file mode 100644 index 0000000000000..54a4ffdd84b69 Binary files /dev/null and b/docs/getting-started/images/addData_sampleDataCards_8.6.0.png differ diff --git a/docs/getting-started/images/addFilterOptions_dashboard_8.4.0.png b/docs/getting-started/images/addFilterOptions_dashboard_8.4.0.png deleted file mode 100644 index eb16edb2b0764..0000000000000 Binary files a/docs/getting-started/images/addFilterOptions_dashboard_8.4.0.png and /dev/null differ diff --git a/docs/getting-started/images/addFilterOptions_dashboard_8.6.0.png b/docs/getting-started/images/addFilterOptions_dashboard_8.6.0.png new file mode 100644 index 0000000000000..fe81284499179 Binary files /dev/null and b/docs/getting-started/images/addFilterOptions_dashboard_8.6.0.png differ diff --git a/docs/getting-started/images/addFilter_dashboard_8.4.0.png b/docs/getting-started/images/addFilter_dashboard_8.4.0.png deleted file mode 100644 index eeec26daad89d..0000000000000 Binary files a/docs/getting-started/images/addFilter_dashboard_8.4.0.png and /dev/null differ diff --git a/docs/getting-started/images/addFilter_dashboard_8.6.0.png b/docs/getting-started/images/addFilter_dashboard_8.6.0.png new file mode 100644 index 0000000000000..45bac56284773 Binary files /dev/null and b/docs/getting-started/images/addFilter_dashboard_8.6.0.png differ diff --git a/docs/getting-started/images/dashboard_ecommerceRevenueDashboard_7.15.0.png b/docs/getting-started/images/dashboard_ecommerceRevenueDashboard_7.15.0.png deleted file mode 100644 index 7fbc0a9bad411..0000000000000 Binary files a/docs/getting-started/images/dashboard_ecommerceRevenueDashboard_7.15.0.png and /dev/null differ diff --git a/docs/getting-started/images/dashboard_ecommerceRevenueDashboard_8.6.0.png b/docs/getting-started/images/dashboard_ecommerceRevenueDashboard_8.6.0.png new file mode 100644 index 0000000000000..ceaedbbd20642 Binary files /dev/null and b/docs/getting-started/images/dashboard_ecommerceRevenueDashboard_8.6.0.png differ diff --git a/docs/getting-started/images/dashboard_sampleDataAddFilter_8.4.0.png b/docs/getting-started/images/dashboard_sampleDataAddFilter_8.4.0.png deleted file mode 100644 index 4c253d3fc3ac5..0000000000000 Binary files a/docs/getting-started/images/dashboard_sampleDataAddFilter_8.4.0.png and /dev/null differ diff --git a/docs/getting-started/images/dashboard_sampleDataAddFilter_8.6.0.png b/docs/getting-started/images/dashboard_sampleDataAddFilter_8.6.0.png new file mode 100644 index 0000000000000..2ae7b29099011 Binary files /dev/null and b/docs/getting-started/images/dashboard_sampleDataAddFilter_8.6.0.png differ diff --git a/docs/getting-started/images/sampleDataFilter_dashboard_8.4.0.png b/docs/getting-started/images/sampleDataFilter_dashboard_8.4.0.png deleted file mode 100644 index 7b5af0ad39395..0000000000000 Binary files a/docs/getting-started/images/sampleDataFilter_dashboard_8.4.0.png and /dev/null differ diff --git a/docs/getting-started/images/sampleDataFilter_dashboard_8.6.0.png b/docs/getting-started/images/sampleDataFilter_dashboard_8.6.0.png new file mode 100644 index 0000000000000..018a5a2174c75 Binary files /dev/null and b/docs/getting-started/images/sampleDataFilter_dashboard_8.6.0.png differ diff --git a/docs/getting-started/quick-start-guide.asciidoc b/docs/getting-started/quick-start-guide.asciidoc index bc98fbf5af737..ae57a9a32b553 100644 --- a/docs/getting-started/quick-start-guide.asciidoc +++ b/docs/getting-started/quick-start-guide.asciidoc @@ -33,7 +33,7 @@ Sample data sets come with sample visualizations, dashboards, and more to help y . On the *Sample eCommerce orders* card, click *Add data*. + [role="screenshot"] -image::images/addData_sampleDataCards_8.4.0.png[Add data UI for the sample data sets] +image::images/addData_sampleDataCards_8.6.0.png[Add data UI for the sample data sets] [float] [[explore-the-data]] @@ -72,7 +72,7 @@ A dashboard is a collection of panels that you can use to visualize the data. Pa . Click *[eCommerce] Revenue Dashboard*. + [role="screenshot"] -image::images/dashboard_ecommerceRevenueDashboard_7.15.0.png[The [eCommerce] Revenue Dashboard that comes with the Sample eCommerce order data set] +image::images/dashboard_ecommerceRevenueDashboard_8.6.0.png[The [eCommerce] Revenue Dashboard that comes with the Sample eCommerce order data set] [float] [[create-a-visualization]] @@ -115,7 +115,7 @@ You can interact with the dashboard data using controls that allow you to apply . Click *Apply changes*. + [role="screenshot"] -image::images/sampleDataFilter_dashboard_8.4.0.png[The [eCommerce] Revenue Dashboard that shows only the women's clothing data from the Gnomehouse manufacturer] +image::images/sampleDataFilter_dashboard_8.6.0.png[The [eCommerce] Revenue Dashboard that shows only the women's clothing data from the Gnomehouse manufacturer] [float] [[filter-and-query-the-data]] @@ -126,7 +126,7 @@ To view a subset of the data, you can apply filters to the dashboard data. Apply . Click *Add filter*. + [role="screenshot"] -image::images/addFilter_dashboard_8.4.0.png[The Add filter action that applies dashboard-level filters] +image::images/addFilter_dashboard_8.6.0.png[The Add filter action that applies dashboard-level filters] . From the *Field* dropdown, select *day_of_week*. @@ -135,12 +135,12 @@ image::images/addFilter_dashboard_8.4.0.png[The Add filter action that applies d . From the *Value* dropdown, select *Wednesday*. + [role="screenshot"] -image::images/addFilterOptions_dashboard_8.4.0.png[The Add filter options configured to display only the women's clothing data generated on Wednesday from the Gnomehouse manufacturer] +image::images/addFilterOptions_dashboard_8.6.0.png[The Add filter options configured to display only the women's clothing data generated on Wednesday from the Gnomehouse manufacturer] . Click *Add filter*. + [role="screenshot"] -image::images/dashboard_sampleDataAddFilter_8.4.0.png[The [eCommerce] Revenue Dashboard that shows only the women's clothing data generated on Wednesday from the Gnomehouse manufacturer] +image::images/dashboard_sampleDataAddFilter_8.6.0.png[The [eCommerce] Revenue Dashboard that shows only the women's clothing data generated on Wednesday from the Gnomehouse manufacturer] [float] [[quick-start-whats-next]] diff --git a/docs/index-custom-title-page.html b/docs/index-custom-title-page.html index f605cfce3dee9..7af50716913b4 100644 --- a/docs/index-custom-title-page.html +++ b/docs/index-custom-title-page.html @@ -63,8 +63,8 @@

Bring your data to life

- What's new - Release notes + What's new + Release notes How-to videos

@@ -113,6 +113,29 @@

Get to know Kibana

+
+
+

+ + Get started +

+
+ +
+

diff --git a/docs/maps/connect-to-ems.asciidoc b/docs/maps/connect-to-ems.asciidoc index 6ea879e64d14d..f3e0709f7f506 100644 --- a/docs/maps/connect-to-ems.asciidoc +++ b/docs/maps/connect-to-ems.asciidoc @@ -130,7 +130,7 @@ endif::[] | `ssl.supportedProtocols` | An array of supported protocols with versions. -Valid protocols: `TLSv1`, `TLSv1.1`, `TLSv1.2`, `TLSv1.3`. *Default: `TLSv1.1`, `TLSv1.2`, `TLSv1.3`*. <>. +Valid protocols: `TLSv1`, `TLSv1.1`, `TLSv1.2`. *Default: `TLSv1.1`, `TLSv1.2`*. <>. | `ssl.cipherSuites` | Details on the format, and the valid options, are available via the diff --git a/docs/maps/vector-style-properties.asciidoc b/docs/maps/vector-style-properties.asciidoc index e702b6d548cd6..431dbac6130e2 100644 --- a/docs/maps/vector-style-properties.asciidoc +++ b/docs/maps/vector-style-properties.asciidoc @@ -14,6 +14,8 @@ You can add text labels to your Point features by configuring label style proper |=== |*Label* |Specifies label content. +|*Label position* +|Place label above, in the center of, or below the Point feature. |*Label visibility* |Specifies the zoom range for which labels are displayed. |*Label color* diff --git a/docs/user/alerting/create-and-manage-rules.asciidoc b/docs/user/alerting/create-and-manage-rules.asciidoc index 15de5944b59fc..d9d5c7bd3d5b0 100644 --- a/docs/user/alerting/create-and-manage-rules.asciidoc +++ b/docs/user/alerting/create-and-manage-rules.asciidoc @@ -104,7 +104,7 @@ image::images/rule-flyout-rule-conditions.png[UI for defining rule conditions on [[defining-rules-actions-details]] ==== Action type and details -To receive notifications when a rule meets the defined conditions, you must add one or more actions. Start by selecting a type of connector for your action: +Actions are optional when you create a rule. However, to receive notifications when a rule meets the defined conditions, you must add one or more actions. Start by selecting a type of connector for your action: [role="screenshot"] image::images/rule-flyout-connector-type-selection.png[UI for selecting an action type] @@ -118,6 +118,8 @@ Each action type exposes different properties. For example, an email action allo [role="screenshot"] image::images/rule-flyout-action-details.png[UI for defining an email action] +You can attach more than one action. Clicking the *Add action* button will prompt you to select another rule type and repeat the above steps again. + [float] [[defining-rules-actions-variables]] ===== Action variables @@ -145,17 +147,29 @@ Some cases exist where the variable values will be "escaped", when used in a con Mustache also supports "triple braces" of the form `{{{variable name}}}`, which indicates no escaping should be done at all. Care should be used when using this form, as it could end up rendering the variable content in such a way as to make the resulting parameter invalid or formatted incorrectly. +[float] +[[defining-rules-actions-variable-context]] +===== Action variable context + Each rule type defines additional variables as properties of the variable `context`. For example, if a rule type defines a variable `value`, it can be used in an action parameter as `{{context.value}}`. -For diagnostic or exploratory purposes, action variables whose values are objects, such as `context`, can be referenced directly as variables. The resulting value will be a JSON representation of the object. For example, if an action parameter includes `{{context}}`, it will expand to the JSON representation of all the variables and values provided by the rule type. +For diagnostic or exploratory purposes, action variables whose values are objects, such as `context`, can be referenced directly as variables. The resulting value will be a JSON representation of the object. For example, if an action parameter includes `{{context}}`, it will expand to the JSON representation of all the variables and values provided by the rule type. To see alert-specific variables, use `{{.}} `. -You can attach more than one action. Clicking the *Add action* button will prompt you to select another rule type and repeat the above steps again. +For situations where your rule response returns arrays of data, you can loop through the `context` by -[NOTE] -============================================== -Actions are not required on rules. You can run a rule without actions to -understand its behavior, then <> later. -============================================== +[source] +-------------------------------------------------- +{{#context}}{{.}}{{/context}} +-------------------------------------------------- + +For example, looping through search result hits may appear + +[source] +-------------------------------------------------- +triggering data was: +{{#context.hits}} - {{_source.message}} +{{/context.hits}} +-------------------------------------------------- [float] [[controlling-rules]] diff --git a/docs/user/canvas.asciidoc b/docs/user/canvas.asciidoc index 372a8285fdbe0..77466de4e8cc3 100644 --- a/docs/user/canvas.asciidoc +++ b/docs/user/canvas.asciidoc @@ -51,7 +51,7 @@ To open *Canvas*, open the main menu, then click *Canvas*. To use the background colors, images, and data of your choice, start with a blank workpad. -. On the *Canvas workpads* page, click *Create workpad*. +. On the *Canvas* page, click *Create workpad*. . Specify the *Workpad settings*. @@ -67,7 +67,7 @@ To use the background colors, images, and data of your choice, start with a blan If you're unsure about where to start, you can use one of the preconfigured templates that come with *Canvas*. -. On the *Canvas workpads* page, select *Templates*. +. On the *Canvas* page, select *Templates*. . Click the preconfigured template that you want to use. @@ -79,7 +79,7 @@ If you're unsure about where to start, you can use one of the preconfigured temp When you want to use a workpad that someone else has already started, import the JSON file. -To begin, drag the file to the *Import workpad JSON file* field on the *Canvas workpads* page. +On the *Canvas* page, drag the file to the *Import workpad JSON file* field. [float] [[use-sample-data-workpads]] @@ -87,9 +87,9 @@ To begin, drag the file to the *Import workpad JSON file* field on the *Canvas w Each of the {kib} sample data sets comes with a workpad that you can use for your own workpad inspiration. -. Add a {kibana-ref}/add-sample-data.html[sample data set]. +. Add a <>. -. On the *Add Data* page, click *View data*, then select *Canvas*. +. On a sample data card, click *View data*, then select *Canvas*. [float] [[add-canvas-elements]] @@ -106,16 +106,16 @@ By default, most of the elements you create use the demo data until you change t . Click *Add element*, then select the element you want to use. -. To connect the element to your data, select *Data*, then select one of the following data sources: +. To connect the element to your data, select *Data > Demo data*, then select one of the following data sources: * *{es} SQL* — Access your data in {es} using {ref}/sql-spec.html[SQL syntax]. -* *{es} documents* — Access your data in {es} without using aggregations. To use, select an index and fields, and optionally enter a query using the <>. -Use the *{es} documents* data source when you have low volume datasets, to view raw documents, or to plot exact, non-aggregated values on a chart. +* *{es} documents* — Access your data in {es} without using aggregations. To use, select a {data-source} and fields. +Use *{es} documents* when you have low-volume datasets, and you want to view raw documents or to plot exact, non-aggregated values on a chart. * *Timelion* — Access your time series data using <> queries. To use *Timelion* queries, you can enter a query using the <>. + -Each element can display a different data source, and pages and workpads often contain multiple data sources. +Each element can display a different {data-source}, and pages and workpads often contain multiple {data-sources}. . To save, use the following options: diff --git a/docs/user/dashboard/images/dashboard_controlsOptionsList_8.3.0.png b/docs/user/dashboard/images/dashboard_controlsOptionsList_8.3.0.png deleted file mode 100644 index a145f428474fa..0000000000000 Binary files a/docs/user/dashboard/images/dashboard_controlsOptionsList_8.3.0.png and /dev/null differ diff --git a/docs/user/dashboard/images/dashboard_controlsOptionsList_8.6.0.png b/docs/user/dashboard/images/dashboard_controlsOptionsList_8.6.0.png new file mode 100644 index 0000000000000..0002dc2ab784f Binary files /dev/null and b/docs/user/dashboard/images/dashboard_controlsOptionsList_8.6.0.png differ diff --git a/docs/user/dashboard/make-dashboards-interactive.asciidoc b/docs/user/dashboard/make-dashboards-interactive.asciidoc index 127c0a4a79e05..507f706b9a9cf 100644 --- a/docs/user/dashboard/make-dashboards-interactive.asciidoc +++ b/docs/user/dashboard/make-dashboards-interactive.asciidoc @@ -35,7 +35,7 @@ There are three types of controls: For example, if you are using the *[Logs] Web Traffic* dashboard from the sample web logs data, you can add an options list for the `machine.os.keyword` field that allows you to display only the logs generated from `osx` and `ios` operating systems. + [role="screenshot"] -image::images/dashboard_controlsOptionsList_8.3.0.png[Options list control for the `machine.os.keyword` field with the `osx` and `ios` options selected] +image::images/dashboard_controlsOptionsList_8.6.0.png[Options list control for the `machine.os.keyword` field with the `osx` and `ios` options selected] * *Range slider* — Adds a slider that allows you to filter the data within a specified range of values. + @@ -76,11 +76,15 @@ The *Control type* is automatically applied for the field you selected. * To expand the width of the control to fit the available space on the dashboard, select *Expand width to fit available space*. -. If you are creating an *Options list*, specify the options. +. If you are creating an *Options list*, specify the additional settings: -.. To allow users to select multiple options, select *Allow multiple selections in dropdown*. +* To allow multiple options to be selected in the dropdown, select *Allow multiple selections in dropdown*. -.. To populate the entire list of options, even when the Options list takes longer to populate than expected, select *Run past timeout*. +* To allow options to be inlcluded or excluded on the dashboard, select *Allow selections to be excluded*. + +* To allow an exists query to be created, select *Allow exists query*. + +* To populate the entire list of options, even when the list takes longer to populate than expected, select *Ignore timeout for results*. . Click *Save and close*. @@ -105,11 +109,17 @@ Filter the data with one or more options that you select. . Open the Options list dropdown. -. Select the options for the data you want to display on the dashboard. +. Select the available options. + -The dashboard displays only the data for the options you selected. +The *Exists* query returns all documents that contain an indexed value for the field. + +. Select how to filter the options. + +* To display only the data for the options you selected, click *Include*. + +* To exclude the data for the options you selected, click *Exclude*. -. To clear the selection, click image:images/dashboard_controlsClearSelections_8.3.0.png[The icon to clear all selected options in the Options list]. +. To clear the selections, click image:images/dashboard_controlsClearSelections_8.3.0.png[The icon to clear all selected options in the Options list]. . To display only the options you selected in the dropdown, click image:images/dashboard_showOnlySelectedOptions_8.3.0.png[The icon to display only the options you have selected in the Options list]. @@ -188,7 +198,7 @@ There are three types of *Discover* interactions you can add to dashboards: * *Panel interactions* — Opens panel data in *Discover*, including the dashboard-level filters, but not the panel-level filters. + -To enable panel interactions, configure <> in kibana.yml. If you are using 7.13.0 and earlier, panel interactions are enabled by default. +To enable panel interactions, configure <> in kibana.yml. If you are using 7.13.0 and earlier, panel interactions are enabled by default. + To use panel interactions, open the panel menu, then click *Explore underlying data*. diff --git a/examples/controls_example/public/app.tsx b/examples/controls_example/public/app.tsx index 831635b8af4da..501d48af70656 100644 --- a/examples/controls_example/public/app.tsx +++ b/examples/controls_example/public/app.tsx @@ -9,23 +9,23 @@ import React from 'react'; import ReactDOM from 'react-dom'; -import type { DataView } from '@kbn/data-views-plugin/public'; import { AppMountParameters } from '@kbn/core/public'; import { KibanaPageTemplate } from '@kbn/shared-ux-page-kibana-template'; import { ControlsExampleStartDeps } from './plugin'; import { BasicReduxExample } from './basic_redux_example'; -interface Props { - dataView: DataView; -} - -const ControlsExamples = ({ dataView }: Props) => { +const ControlsExamples = ({ dataViewId }: { dataViewId?: string }) => { + const examples = dataViewId ? ( + <> + + + ) : ( +
{'Please install e-commerce sample data to run controls examples.'}
+ ); return ( - - - + {examples} ); }; @@ -35,8 +35,7 @@ export const renderApp = async ( { element }: AppMountParameters ) => { const dataViews = await data.dataViews.find('kibana_sample_data_ecommerce'); - if (dataViews.length > 0) { - ReactDOM.render(, element); - } + const dataViewId = dataViews.length > 0 ? dataViews[0].id : undefined; + ReactDOM.render(, element); return () => ReactDOM.unmountComponentAtNode(element); }; diff --git a/examples/controls_example/public/basic_redux_example.tsx b/examples/controls_example/public/basic_redux_example.tsx index bca34e61042f6..03edcd82b71a2 100644 --- a/examples/controls_example/public/basic_redux_example.tsx +++ b/examples/controls_example/public/basic_redux_example.tsx @@ -11,12 +11,10 @@ import React, { useMemo, useState } from 'react'; import { LazyControlGroupRenderer, ControlGroupContainer, - ControlGroupInput, useControlGroupContainerContext, ControlStyle, } from '@kbn/controls-plugin/public'; import { withSuspense } from '@kbn/presentation-util-plugin/public'; -import type { DataView } from '@kbn/data-views-plugin/public'; import { EuiButtonGroup, EuiFlexGroup, @@ -26,27 +24,24 @@ import { EuiText, EuiTitle, } from '@elastic/eui'; -import { getDefaultControlGroupInput } from '@kbn/controls-plugin/common'; -interface Props { - dataView: DataView; -} const ControlGroupRenderer = withSuspense(LazyControlGroupRenderer); -export const BasicReduxExample = ({ dataView }: Props) => { - const [myControlGroup, setControlGroup] = useState(); - const [currentControlStyle, setCurrentControlStyle] = useState('oneLine'); +export const BasicReduxExample = ({ dataViewId }: { dataViewId: string }) => { + const [controlGroup, setControlGroup] = useState(); const ControlGroupReduxWrapper = useMemo(() => { - if (myControlGroup) return myControlGroup.getReduxEmbeddableTools().Wrapper; - }, [myControlGroup]); + if (controlGroup) return controlGroup.getReduxEmbeddableTools().Wrapper; + }, [controlGroup]); const ButtonControls = () => { const { useEmbeddableDispatch, + useEmbeddableSelector: select, actions: { setControlStyle }, } = useControlGroupContainerContext(); const dispatch = useEmbeddableDispatch(); + const controlStyle = select((state) => state.explicitInput.controlStyle); return ( <> @@ -71,9 +66,8 @@ export const BasicReduxExample = ({ dataView }: Props) => { value: 'twoLine' as ControlStyle, }, ]} - idSelected={currentControlStyle} + idSelected={controlStyle} onChange={(id, value) => { - setCurrentControlStyle(value); dispatch(setControlStyle(value)); }} type="single" @@ -105,20 +99,17 @@ export const BasicReduxExample = ({ dataView }: Props) => { )} { - setControlGroup(controlGroup); + onLoadComplete={async (newControlGroup) => { + setControlGroup(newControlGroup); }} - getCreationOptions={async (controlGroupInputBuilder) => { - const initialInput: Partial = { - ...getDefaultControlGroupInput(), - defaultControlWidth: 'small', - }; - await controlGroupInputBuilder.addDataControlFromField(initialInput, { - dataViewId: dataView.id ?? 'kibana_sample_data_ecommerce', + getInitialInput={async (initialInput, builder) => { + await builder.addDataControlFromField(initialInput, { + dataViewId, fieldName: 'customer_first_name.keyword', + width: 'small', }); - await controlGroupInputBuilder.addDataControlFromField(initialInput, { - dataViewId: dataView.id ?? 'kibana_sample_data_ecommerce', + await builder.addDataControlFromField(initialInput, { + dataViewId, fieldName: 'customer_last_name.keyword', width: 'medium', grow: false, diff --git a/examples/files_example/common/index.ts b/examples/files_example/common/index.ts index 29b2c97c9532e..b3ef90f9dfd6e 100644 --- a/examples/files_example/common/index.ts +++ b/examples/files_example/common/index.ts @@ -7,7 +7,7 @@ */ import type { FileKind } from '@kbn/files-plugin/common'; -import type { FileImageMetadata } from '@kbn/shared-ux-file-image-types'; +import type { FileImageMetadata } from '@kbn/shared-ux-file-types'; export const PLUGIN_ID = 'filesExample'; export const PLUGIN_NAME = 'Files example'; diff --git a/examples/files_example/public/components/modal.tsx b/examples/files_example/public/components/modal.tsx index a314e5bd8ea4e..1471c469b6351 100644 --- a/examples/files_example/public/components/modal.tsx +++ b/examples/files_example/public/components/modal.tsx @@ -10,7 +10,7 @@ import type { FunctionComponent } from 'react'; import React from 'react'; import { EuiModal, EuiModalHeader, EuiModalBody, EuiText } from '@elastic/eui'; import { exampleFileKind, MyImageMetadata } from '../../common'; -import { FilesClient, UploadFile } from '../imports'; +import { FilesClient, FileUpload } from '../imports'; interface Props { client: FilesClient; @@ -27,7 +27,7 @@ export const Modal: FunctionComponent = ({ onDismiss, onUploaded, client - { }; const updateGuideState = async () => { - const selectedGuideConfig = guidesConfig[selectedGuide!]; + if (!selectedGuide) { + return; + } + + const selectedGuideConfig = await guidedOnboardingApi?.getGuideConfig(selectedGuide); + + if (!selectedGuideConfig) { + return; + } const selectedStepIndex = selectedGuideConfig.steps.findIndex( (step) => step.id === selectedStep! ); @@ -199,7 +206,7 @@ export const Main = (props: MainProps) => { - {(Object.keys(guidesConfig) as GuideId[]).map((guideId) => { + {(['search', 'security', 'observability', 'testGuide'] as GuideId[]).map((guideId) => { const guideState = guidesState?.find((guide) => guide.guideId === guideId); return ( diff --git a/examples/screenshot_mode_example/.i18nrc.json b/examples/screenshot_mode_example/.i18nrc.json index cce0f6b34fea2..520a0f81bd832 100644 --- a/examples/screenshot_mode_example/.i18nrc.json +++ b/examples/screenshot_mode_example/.i18nrc.json @@ -3,5 +3,5 @@ "paths": { "screenshotModeExample": "." }, - "translations": ["translations/ja-JP.json"] + "translations": [] } diff --git a/fleet_packages.json b/fleet_packages.json index 94d383ff9f6e6..6a8508f04ca22 100644 --- a/fleet_packages.json +++ b/fleet_packages.json @@ -20,7 +20,7 @@ [ { "name": "apm", - "version": "8.6.0-preview-1663775281", + "version": "8.7.0-preview-1670292270", "forceAlignStackVersion": true }, { @@ -29,7 +29,7 @@ }, { "name": "endpoint", - "version": "8.5.0" + "version": "8.7.0-next" }, { "name": "fleet_server", @@ -37,6 +37,10 @@ }, { "name": "synthetics", - "version": "0.10.3" + "version": "0.11.4" + }, + { + "name": "security_detection_engine", + "version": "8.4.1" } ] diff --git a/package.json b/package.json index 724ec9b99ed92..2a97978a24dbb 100644 --- a/package.json +++ b/package.json @@ -72,7 +72,6 @@ "yarn": "^1.22.19" }, "resolutions": { - "**/@tanstack/match-sorter-utils": "8.1.1", "**/@types/node": "16.11.41", "**/chokidar": "^3.5.3", "**/deepmerge": "^4.2.2", @@ -98,17 +97,17 @@ }, "dependencies": { "@appland/sql-parser": "^1.5.1", - "@babel/runtime": "^7.20.1", + "@babel/runtime": "^7.20.6", "@dnd-kit/core": "^3.1.1", "@dnd-kit/sortable": "^4.0.0", "@dnd-kit/utilities": "^2.0.0", "@elastic/apm-rum": "^5.12.0", "@elastic/apm-rum-react": "^1.4.2", - "@elastic/charts": "50.2.1", + "@elastic/charts": "51.1.1", "@elastic/datemath": "5.0.3", "@elastic/elasticsearch": "npm:@elastic/elasticsearch-canary@8.5.0-canary.1", "@elastic/ems-client": "8.3.3", - "@elastic/eui": "70.2.4", + "@elastic/eui": "70.4.0", "@elastic/filesaver": "1.1.2", "@elastic/node-crypto": "1.2.1", "@elastic/numeral": "^2.5.1", @@ -356,6 +355,7 @@ "@kbn/osquery-io-ts-types": "link:bazel-bin/packages/kbn-osquery-io-ts-types", "@kbn/plugin-discovery": "link:bazel-bin/packages/kbn-plugin-discovery", "@kbn/react-field": "link:bazel-bin/packages/kbn-react-field", + "@kbn/rison": "link:bazel-bin/packages/kbn-rison", "@kbn/rule-data-utils": "link:bazel-bin/packages/kbn-rule-data-utils", "@kbn/safer-lodash-set": "link:bazel-bin/packages/kbn-safer-lodash-set", "@kbn/securitysolution-autocomplete": "link:bazel-bin/packages/kbn-securitysolution-autocomplete", @@ -385,9 +385,13 @@ "@kbn/shared-ux-card-no-data": "link:bazel-bin/packages/shared-ux/card/no_data/impl", "@kbn/shared-ux-card-no-data-mocks": "link:bazel-bin/packages/shared-ux/card/no_data/mocks", "@kbn/shared-ux-card-no-data-types": "link:bazel-bin/packages/shared-ux/card/no_data/types", + "@kbn/shared-ux-file-context": "link:bazel-bin/packages/shared-ux/file/context", "@kbn/shared-ux-file-image": "link:bazel-bin/packages/shared-ux/file/image/impl", "@kbn/shared-ux-file-image-mocks": "link:bazel-bin/packages/shared-ux/file/image/mocks", - "@kbn/shared-ux-file-image-types": "link:bazel-bin/packages/shared-ux/file/image/types", + "@kbn/shared-ux-file-mocks": "link:bazel-bin/packages/shared-ux/file/mocks", + "@kbn/shared-ux-file-picker": "link:bazel-bin/packages/shared-ux/file/file_picker/impl", + "@kbn/shared-ux-file-types": "link:bazel-bin/packages/shared-ux/file/types", + "@kbn/shared-ux-file-upload": "link:bazel-bin/packages/shared-ux/file/file_upload/impl", "@kbn/shared-ux-file-util": "link:bazel-bin/packages/shared-ux/file/util", "@kbn/shared-ux-link-redirect-app": "link:bazel-bin/packages/shared-ux/link/redirect_app/impl", "@kbn/shared-ux-link-redirect-app-mocks": "link:bazel-bin/packages/shared-ux/link/redirect_app/mocks", @@ -414,6 +418,7 @@ "@kbn/shared-ux-prompt-no-data-views": "link:bazel-bin/packages/shared-ux/prompt/no_data_views/impl", "@kbn/shared-ux-prompt-no-data-views-mocks": "link:bazel-bin/packages/shared-ux/prompt/no_data_views/mocks", "@kbn/shared-ux-prompt-no-data-views-types": "link:bazel-bin/packages/shared-ux/prompt/no_data_views/types", + "@kbn/shared-ux-prompt-not-found": "link:bazel-bin/packages/shared-ux/prompt/not_found", "@kbn/shared-ux-router-mocks": "link:bazel-bin/packages/shared-ux/router/mocks", "@kbn/shared-ux-services": "link:bazel-bin/packages/kbn-shared-ux-services", "@kbn/shared-ux-storybook": "link:bazel-bin/packages/kbn-shared-ux-storybook", @@ -447,8 +452,8 @@ "@opentelemetry/semantic-conventions": "^1.4.0", "@reduxjs/toolkit": "1.7.2", "@slack/webhook": "^5.0.4", - "@tanstack/react-query": "^4.13.4", - "@tanstack/react-query-devtools": "^4.13.4", + "@tanstack/react-query": "^4.18.0", + "@tanstack/react-query-devtools": "^4.18.0", "@turf/along": "6.0.1", "@turf/area": "6.0.1", "@turf/bbox": "6.0.1", @@ -607,9 +612,9 @@ "react-dom": "^17.0.2", "react-dropzone": "^4.2.9", "react-fast-compare": "^2.0.4", - "react-focus-on": "^3.6.0", + "react-focus-on": "^3.7.0", "react-grid-layout": "^1.3.4", - "react-hook-form": "^7.39.1", + "react-hook-form": "^7.39.7", "react-intl": "^2.8.0", "react-is": "^17.0.2", "react-markdown": "^6.0.3", @@ -689,12 +694,12 @@ "devDependencies": { "@apidevtools/swagger-parser": "^10.0.3", "@babel/cli": "^7.19.3", - "@babel/core": "^7.20.2", + "@babel/core": "^7.20.5", "@babel/eslint-parser": "^7.19.1", "@babel/eslint-plugin": "^7.19.1", - "@babel/generator": "^7.20.4", + "@babel/generator": "^7.20.5", "@babel/helper-plugin-utils": "^7.20.2", - "@babel/parser": "^7.20.3", + "@babel/parser": "^7.20.5", "@babel/plugin-proposal-class-properties": "^7.18.6", "@babel/plugin-proposal-export-namespace-from": "^7.18.9", "@babel/plugin-proposal-nullish-coalescing-operator": "^7.18.6", @@ -706,8 +711,8 @@ "@babel/preset-react": "^7.18.6", "@babel/preset-typescript": "^7.18.6", "@babel/register": "^7.18.9", - "@babel/traverse": "^7.20.1", - "@babel/types": "^7.20.2", + "@babel/traverse": "^7.20.5", + "@babel/types": "^7.20.5", "@bazel/ibazel": "^0.16.2", "@bazel/typescript": "4.6.2", "@cypress/code-coverage": "^3.10.0", @@ -898,7 +903,7 @@ "@types/nock": "^10.0.3", "@types/node": "16.11.41", "@types/node-fetch": "^2.6.0", - "@types/node-forge": "^1.3.0", + "@types/node-forge": "^1.3.1", "@types/nodemailer": "^6.4.0", "@types/normalize-path": "^3.0.0", "@types/object-hash": "^1.3.0", @@ -976,7 +981,7 @@ "argsplit": "^1.0.5", "autoprefixer": "^10.4.7", "axe-core": "^4.0.2", - "babel-jest": "^29.2.2", + "babel-jest": "^29.3.1", "babel-loader": "^8.2.5", "babel-plugin-add-module-exports": "^1.0.4", "babel-plugin-istanbul": "^6.1.1", @@ -1069,7 +1074,7 @@ "jsondiffpatch": "0.4.1", "license-checker": "^25.0.1", "listr": "^0.14.1", - "lmdb-store": "^1", + "lmdb": "^2.6.9", "loader-utils": "^2.0.4", "marge": "^1.0.1", "micromatch": "^4.0.5", diff --git a/packages/BUILD.bazel b/packages/BUILD.bazel index 17a515a5b04a0..78e7a74fa0b32 100644 --- a/packages/BUILD.bazel +++ b/packages/BUILD.bazel @@ -276,6 +276,7 @@ filegroup( "//packages/kbn-react-field:build", "//packages/kbn-repo-source-classifier:build", "//packages/kbn-repo-source-classifier-cli:build", + "//packages/kbn-rison:build", "//packages/kbn-rule-data-utils:build", "//packages/kbn-safer-lodash-set:build", "//packages/kbn-securitysolution-autocomplete:build", @@ -333,9 +334,13 @@ filegroup( "//packages/shared-ux/card/no_data/impl:build", "//packages/shared-ux/card/no_data/mocks:build", "//packages/shared-ux/card/no_data/types:build", + "//packages/shared-ux/file/context:build", + "//packages/shared-ux/file/file_picker/impl:build", + "//packages/shared-ux/file/file_upload/impl:build", "//packages/shared-ux/file/image/impl:build", "//packages/shared-ux/file/image/mocks:build", - "//packages/shared-ux/file/image/types:build", + "//packages/shared-ux/file/mocks:build", + "//packages/shared-ux/file/types:build", "//packages/shared-ux/file/util:build", "//packages/shared-ux/link/redirect_app/impl:build", "//packages/shared-ux/link/redirect_app/mocks:build", @@ -362,6 +367,7 @@ filegroup( "//packages/shared-ux/prompt/no_data_views/impl:build", "//packages/shared-ux/prompt/no_data_views/mocks:build", "//packages/shared-ux/prompt/no_data_views/types:build", + "//packages/shared-ux/prompt/not_found:build", "//packages/shared-ux/router/impl:build", "//packages/shared-ux/router/mocks:build", "//packages/shared-ux/router/types:build", @@ -635,6 +641,7 @@ filegroup( "//packages/kbn-react-field:build_types", "//packages/kbn-repo-source-classifier:build_types", "//packages/kbn-repo-source-classifier-cli:build_types", + "//packages/kbn-rison:build_types", "//packages/kbn-rule-data-utils:build_types", "//packages/kbn-safer-lodash-set:build_types", "//packages/kbn-securitysolution-autocomplete:build_types", @@ -685,8 +692,12 @@ filegroup( "//packages/shared-ux/button/exit_full_screen/mocks:build_types", "//packages/shared-ux/card/no_data/impl:build_types", "//packages/shared-ux/card/no_data/mocks:build_types", + "//packages/shared-ux/file/context:build_types", + "//packages/shared-ux/file/file_picker/impl:build_types", + "//packages/shared-ux/file/file_upload/impl:build_types", "//packages/shared-ux/file/image/impl:build_types", "//packages/shared-ux/file/image/mocks:build_types", + "//packages/shared-ux/file/mocks:build_types", "//packages/shared-ux/file/util:build_types", "//packages/shared-ux/link/redirect_app/impl:build_types", "//packages/shared-ux/link/redirect_app/mocks:build_types", @@ -706,6 +717,7 @@ filegroup( "//packages/shared-ux/page/solution_nav:build_types", "//packages/shared-ux/prompt/no_data_views/impl:build_types", "//packages/shared-ux/prompt/no_data_views/mocks:build_types", + "//packages/shared-ux/prompt/not_found:build_types", "//packages/shared-ux/router/impl:build_types", "//packages/shared-ux/router/mocks:build_types", "//packages/shared-ux/storybook/config:build_types", diff --git a/packages/core/overlays/core-overlays-browser-internal/src/modal/__snapshots__/modal_service.test.tsx.snap b/packages/core/overlays/core-overlays-browser-internal/src/modal/__snapshots__/modal_service.test.tsx.snap index 2b5f564dfb3ee..7105dcea02869 100644 --- a/packages/core/overlays/core-overlays-browser-internal/src/modal/__snapshots__/modal_service.test.tsx.snap +++ b/packages/core/overlays/core-overlays-browser-internal/src/modal/__snapshots__/modal_service.test.tsx.snap @@ -94,12 +94,12 @@ exports[`ModalService openConfirm() renders a mountpoint confirm message 2`] = ` data-eui="EuiFocusTrap" >
+
+
+
+
+ , + "container":
+
+
+
+ +
+
+
+
, + "debug": [Function], + "findAllByAltText": [Function], + "findAllByDisplayValue": [Function], + "findAllByLabelText": [Function], + "findAllByPlaceholderText": [Function], + "findAllByRole": [Function], + "findAllByTestId": [Function], + "findAllByText": [Function], + "findAllByTitle": [Function], + "findByAltText": [Function], + "findByDisplayValue": [Function], + "findByLabelText": [Function], + "findByPlaceholderText": [Function], + "findByRole": [Function], + "findByTestId": [Function], + "findByText": [Function], + "findByTitle": [Function], + "getAllByAltText": [Function], + "getAllByDisplayValue": [Function], + "getAllByLabelText": [Function], + "getAllByPlaceholderText": [Function], + "getAllByRole": [Function], + "getAllByTestId": [Function], + "getAllByText": [Function], + "getAllByTitle": [Function], + "getByAltText": [Function], + "getByDisplayValue": [Function], + "getByLabelText": [Function], + "getByPlaceholderText": [Function], + "getByRole": [Function], + "getByTestId": [Function], + "getByText": [Function], + "getByTitle": [Function], + "queryAllByAltText": [Function], + "queryAllByDisplayValue": [Function], + "queryAllByLabelText": [Function], + "queryAllByPlaceholderText": [Function], + "queryAllByRole": [Function], + "queryAllByTestId": [Function], + "queryAllByText": [Function], + "queryAllByTitle": [Function], + "queryByAltText": [Function], + "queryByDisplayValue": [Function], + "queryByLabelText": [Function], + "queryByPlaceholderText": [Function], + "queryByRole": [Function], + "queryByTestId": [Function], + "queryByText": [Function], + "queryByTitle": [Function], + "rerender": [Function], + "unmount": [Function], +} +`; + exports[`HeaderMenu should render button icon disabled 1`] = ` Object { "asFragment": [Function], @@ -61,7 +176,7 @@ Object {

diff --git a/packages/kbn-securitysolution-exception-list-components/src/header_menu/header_menu.test.tsx b/packages/kbn-securitysolution-exception-list-components/src/header_menu/header_menu.test.tsx index 509c9e81b3d7d..a22bd79f5a984 100644 --- a/packages/kbn-securitysolution-exception-list-components/src/header_menu/header_menu.test.tsx +++ b/packages/kbn-securitysolution-exception-list-components/src/header_menu/header_menu.test.tsx @@ -13,6 +13,17 @@ import { getSecurityLinkAction } from '../mocks/security_link_component.mock'; describe('HeaderMenu', () => { it('should render button icon with default settings', () => { + const wrapper = render( + + ); + + expect(wrapper).toMatchSnapshot(); + + expect(wrapper.getByTestId('ButtonIcon')).toBeInTheDocument(); + expect(wrapper.queryByTestId('EmptyButton')).not.toBeInTheDocument(); + expect(wrapper.queryByTestId('MenuPanel')).not.toBeInTheDocument(); + }); + it('should not render icon', () => { const wrapper = render(); expect(wrapper).toMatchSnapshot(); @@ -23,7 +34,11 @@ describe('HeaderMenu', () => { }); it('should render button icon disabled', () => { const wrapper = render( - + ); fireEvent.click(wrapper.getByTestId('ButtonIcon')); @@ -103,7 +118,13 @@ describe('HeaderMenu', () => { it('should render custom Actions', () => { const customActions = getSecurityLinkAction('headerMenuTest'); const wrapper = render( - + ); expect(wrapper).toMatchSnapshot(); @@ -117,7 +138,12 @@ describe('HeaderMenu', () => { const customAction = [...actions]; customAction[0].onClick = onEdit; const wrapper = render( - + ); const headerMenu = wrapper.getByTestId('headerMenuItems'); const click = createEvent.click(headerMenu); diff --git a/packages/kbn-securitysolution-exception-list-components/src/header_menu/index.tsx b/packages/kbn-securitysolution-exception-list-components/src/header_menu/index.tsx index a8e44a03473c5..f042be34a8cee 100644 --- a/packages/kbn-securitysolution-exception-list-components/src/header_menu/index.tsx +++ b/packages/kbn-securitysolution-exception-list-components/src/header_menu/index.tsx @@ -18,7 +18,9 @@ import { PanelPaddingSize, PopoverAnchorPosition, } from '@elastic/eui'; + import { ButtonContentIconSide } from '@elastic/eui/src/components/button/_button_content_deprecated'; +import { css } from '@emotion/react'; export interface Action { key: string; @@ -27,6 +29,7 @@ export interface Action { disabled?: boolean; onClick: (e: React.MouseEvent) => void; } + interface HeaderMenuComponentProps { disableActions: boolean; actions: Action[] | ReactElement[] | null; @@ -40,6 +43,12 @@ interface HeaderMenuComponentProps { panelPaddingSize?: PanelPaddingSize; } +const popoverHeightStyle = css` + max-height: 300px; + height: 100%; + overflow-x: hidden; + overflow-y: auto; +`; const HeaderMenuComponent: FC = ({ text, dataTestSubj, @@ -47,7 +56,7 @@ const HeaderMenuComponent: FC = ({ disableActions, emptyButton, useCustomActions, - iconType = 'boxesHorizontal', + iconType, iconSide = 'left', anchorPosition = 'downCenter', panelPaddingSize = 's', @@ -84,7 +93,7 @@ const HeaderMenuComponent: FC = ({ = ({ @@ -112,6 +121,8 @@ const HeaderMenuComponent: FC = ({ > {!itemActions ? null : (
-
-

- Edit List Name -

-
+ Edit List Name +

-
-

- Edit list name -

-
+ Edit list name +
]; + const component = render(); + expect(component.text()).toContain('I am a button'); + }); +}); diff --git a/packages/shared-ux/prompt/not_found/src/not_found_prompt.tsx b/packages/shared-ux/prompt/not_found/src/not_found_prompt.tsx new file mode 100644 index 0000000000000..bd607e1beae07 --- /dev/null +++ b/packages/shared-ux/prompt/not_found/src/not_found_prompt.tsx @@ -0,0 +1,79 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React, { useEffect, useState, useCallback, useMemo } from 'react'; +import { + EuiButtonEmpty, + EuiEmptyPrompt, + EuiEmptyPromptProps, + EuiImage, + useEuiTheme, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +const NOT_FOUND_TITLE = i18n.translate('sharedUXPackages.prompt.errors.notFound.title', { + defaultMessage: 'Page not found', +}); + +const NOT_FOUND_BODY = i18n.translate('sharedUXPackages.prompt.errors.notFound.body', { + defaultMessage: + "Sorry, the page you're looking for can't be found. It might have been removed or renamed, or maybe it never existed at all.", +}); + +const NOT_FOUND_GO_BACK = i18n.translate('sharedUXPackages.prompt.errors.notFound.goBacklabel', { + defaultMessage: 'Go back', +}); + +interface NotFoundProps { + /** Array of buttons, links and other actions to show at the bottom of the `EuiEmptyPrompt`. Defaults to a "Back" button. */ + actions?: EuiEmptyPromptProps['actions']; +} + +/** + * Predefined `EuiEmptyPrompt` for 404 pages. + */ +export const NotFoundPrompt = ({ actions }: NotFoundProps) => { + const { colorMode } = useEuiTheme(); + const [imageSrc, setImageSrc] = useState(); + const goBack = useCallback(() => history.back(), []); + + const DEFAULT_ACTIONS = useMemo( + () => [ + + {NOT_FOUND_GO_BACK} + , + ], + [goBack] + ); + + useEffect(() => { + const loadImage = async () => { + if (colorMode === 'DARK') { + const { default: imgSrc } = await import(`./assets/404_astronaut_dark.png`); + setImageSrc(imgSrc); + } else { + const { default: imgSrc } = await import(`./assets/404_astronaut_light.png`); + setImageSrc(imgSrc); + } + }; + loadImage(); + }, [colorMode]); + + const icon = imageSrc ? : null; + + return ( + {NOT_FOUND_TITLE}} + body={NOT_FOUND_BODY} + actions={actions ?? DEFAULT_ACTIONS} + /> + ); +}; diff --git a/packages/shared-ux/prompt/not_found/tsconfig.json b/packages/shared-ux/prompt/not_found/tsconfig.json new file mode 100644 index 0000000000000..044531bb66de4 --- /dev/null +++ b/packages/shared-ux/prompt/not_found/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../../../../tsconfig.bazel.json", + "compilerOptions": { + "declaration": true, + "emitDeclarationOnly": true, + "outDir": "target_types", + "types": ["jest", "node", "react", "@kbn/ambient-ui-types"] + }, + "include": ["**/*.ts", "**/*.tsx"] +} diff --git a/renovate.json b/renovate.json index 9c0f4150b9c80..35a0f95d406af 100644 --- a/renovate.json +++ b/renovate.json @@ -158,6 +158,29 @@ "labels": ["Team:Operations", "release_note:skip"], "enabled": true }, + { + "groupName": "jest", + "packageNames": [ + "@jest/console", + "@jest/reporters", + "@jest/types", + "babel-jest", + "expect", + "jest", + "jest-cli", + "jest-config", + "jest-diff", + "jest-environment-jsdom", + "jest-matcher-utils", + "jest-mock", + "jest-runtime", + "jest-snapshot" + ], + "reviewers": ["team:kibana-operations"], + "matchBaseBranches": ["main"], + "labels": ["Team:Operations", "release_note:skip"], + "enabled": true + }, { "groupName": "@storybook", "reviewers": ["team:kibana-operations"], diff --git a/scripts/read_jest_help.mjs b/scripts/read_jest_help.mjs new file mode 100644 index 0000000000000..0a3ce69c02c93 --- /dev/null +++ b/scripts/read_jest_help.mjs @@ -0,0 +1,131 @@ +/* + * Copyright 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 Fsp from 'fs/promises'; +import Path from 'path'; + +import { createFailError } from '@kbn/dev-cli-errors'; +import { run } from '@kbn/dev-cli-runner'; +import { REPO_ROOT } from '@kbn/utils'; + +const FLAGS_FILE = 'packages/kbn-test/src/jest/jest_flags.json'; + +function readStdin() { + return new Promise((resolve, reject) => { + let buffer = ''; + let timer = setTimeout(() => { + reject( + createFailError('you must pipe the output of `yarn jest --help` to this script', { + showHelp: true, + }) + ); + }, 1000); + + process.stdin + .on('data', (chunk) => { + if (timer) { + clearTimeout(timer); + timer = undefined; + } + + buffer += chunk; + }) + .on('end', () => resolve(buffer)) + .on('error', reject); + }); +} + +run( + async ({ log }) => { + const lines = (await readStdin()).split('\n'); + + /** @type {{ string: string[], boolean: string[], alias: Record }} */ + const flags = { string: [], boolean: [], alias: {} }; + + /** @type {string | undefined} */ + let currentFlag; + + for (const line of lines) { + const flagMatch = line.match(/^\s+(?:-(\w), )?--(\w+)\s+/); + const typeMatch = line.match(/\[(boolean|string|array|number|choices: [^\]]+)\]/); + + if (flagMatch && currentFlag) { + throw createFailError(`unable to determine type for flag [${currentFlag}]`); + } + + if (flagMatch) { + currentFlag = flagMatch[2]; + if (flagMatch[1]) { + flags.alias[flagMatch[1]] = flagMatch[2]; + } + } + + if (currentFlag && typeMatch) { + switch (typeMatch[1]) { + case 'string': + case 'array': + case 'number': + flags.string.push(currentFlag); + break; + case 'boolean': + flags.boolean.push(currentFlag); + break; + default: + if (typeMatch[1].startsWith('choices: ')) { + flags.string.push(currentFlag); + break; + } + + throw createFailError(`unexpected flag type [${typeMatch[1]}]`); + } + currentFlag = undefined; + } + } + + await Fsp.writeFile( + Path.resolve(REPO_ROOT, FLAGS_FILE), + JSON.stringify( + { + string: flags.string.sort(function (a, b) { + return a.localeCompare(b); + }), + boolean: flags.boolean.sort(function (a, b) { + return a.localeCompare(b); + }), + alias: Object.fromEntries( + Object.entries(flags.alias).sort(function (a, b) { + return a[0].localeCompare(b[0]); + }) + ), + }, + null, + 2 + ) + ); + + log.success('wrote jest flag info to', FLAGS_FILE); + log.warning('make sure you bootstrap to rebuild @kbn/test'); + }, + { + usage: `yarn jest --help | node scripts/read_jest_help.mjs`, + description: ` + Jest no longer exposes the ability to parse CLI flags externally, so we use this + script to read the help output and convert it into parameters we can pass to getopts() + which will parse the flags similar to how Jest does it. + + getopts() doesn't support things like enums, or number flags, but if we use the generated + config then it will at least interpret which flags are expected, which are invalid, and + allow us to determine the correct config path based on the provided path while passing + the rest of the args directly to jest. + `, + flags: { + allowUnexpected: true, + guessTypesForUnexpectedFlags: false, + }, + } +); diff --git a/scripts/run_performance.js b/scripts/run_performance.js new file mode 100644 index 0000000000000..2b1bffa39ab4d --- /dev/null +++ b/scripts/run_performance.js @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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. + */ + +require('../src/setup_node_env'); +require('../src/dev/performance/run_performance_cli'); diff --git a/scripts/run_scalability.js b/scripts/run_scalability.js new file mode 100644 index 0000000000000..1524beb7e9401 --- /dev/null +++ b/scripts/run_scalability.js @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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. + */ + +require('../src/setup_node_env'); +require('../src/dev/performance/run_scalability_cli'); diff --git a/src/core/server/integration_tests/saved_objects/migrations/actions/actions.test.ts b/src/core/server/integration_tests/saved_objects/migrations/actions/actions.test.ts index 9e182731c0a89..2314e654607bc 100644 --- a/src/core/server/integration_tests/saved_objects/migrations/actions/actions.test.ts +++ b/src/core/server/integration_tests/saved_objects/migrations/actions/actions.test.ts @@ -34,6 +34,7 @@ import { type UpdateByQueryResponse, updateAndPickupMappings, type UpdateAndPickupMappingsResponse, + updateTargetMappingsMeta, removeWriteBlock, transformDocs, waitForIndexStatus, @@ -70,6 +71,11 @@ describe('migration actions', () => { mappings: { dynamic: true, properties: {}, + _meta: { + migrationMappingPropertyHashes: { + references: '7997cf5a56cc02bdc9c93361bde732b0', + }, + }, }, })(); const sourceDocs = [ @@ -147,6 +153,32 @@ describe('migration actions', () => { }) ); }); + it('includes the _meta data of the indices in the response', async () => { + expect.assertions(1); + const res = (await initAction({ + client, + indices: ['existing_index_with_docs'], + })()) as Either.Right; + + expect(res.right).toEqual( + expect.objectContaining({ + existing_index_with_docs: { + aliases: {}, + mappings: { + // FIXME https://github.com/elastic/elasticsearch-js/issues/1796 + dynamic: 'true', + properties: expect.anything(), + _meta: { + migrationMappingPropertyHashes: { + references: '7997cf5a56cc02bdc9c93361bde732b0', + }, + }, + }, + settings: expect.anything(), + }, + }) + ); + }); it('resolves left when cluster.routing.allocation.enabled is incompatible', async () => { expect.assertions(3); await client.cluster.putSettings({ @@ -1453,6 +1485,47 @@ describe('migration actions', () => { }); }); + describe('updateTargetMappingsMeta', () => { + it('rejects if ES throws an error', async () => { + const task = updateTargetMappingsMeta({ + client, + index: 'no_such_index', + meta: { + migrationMappingPropertyHashes: { + references: 'updateda56cc02bdc9c93361bupdated', + newReferences: 'fooBarHashMd509387420934879300d9', + }, + }, + })(); + + await expect(task).rejects.toThrow('index_not_found_exception'); + }); + + it('resolves right when mappings._meta are correctly updated', async () => { + const res = await updateTargetMappingsMeta({ + client, + index: 'existing_index_with_docs', + meta: { + migrationMappingPropertyHashes: { + newReferences: 'fooBarHashMd509387420934879300d9', + }, + }, + })(); + + expect(Either.isRight(res)).toBe(true); + + const indices = await client.indices.get({ + index: ['existing_index_with_docs'], + }); + + expect(indices.existing_index_with_docs.mappings?._meta).toEqual({ + migrationMappingPropertyHashes: { + newReferences: 'fooBarHashMd509387420934879300d9', + }, + }); + }); + }); + describe('updateAliases', () => { describe('remove', () => { it('resolves left index_not_found_exception when the index does not exist', async () => { diff --git a/src/core/server/integration_tests/saved_objects/migrations/archives/8.4.0_with_sample_data_logs.zip b/src/core/server/integration_tests/saved_objects/migrations/archives/8.4.0_with_sample_data_logs.zip new file mode 100644 index 0000000000000..4304e138f161a Binary files /dev/null and b/src/core/server/integration_tests/saved_objects/migrations/archives/8.4.0_with_sample_data_logs.zip differ diff --git a/src/core/server/integration_tests/saved_objects/migrations/check_registered_types.test.ts b/src/core/server/integration_tests/saved_objects/migrations/check_registered_types.test.ts index bf67572828796..a08633fc32039 100644 --- a/src/core/server/integration_tests/saved_objects/migrations/check_registered_types.test.ts +++ b/src/core/server/integration_tests/saved_objects/migrations/check_registered_types.test.ts @@ -76,6 +76,7 @@ describe('checking migration metadata changes on all registered SO types', () => "cases-telemetry": "16e261e7378a72acd0806f18df92525dd1da4f37", "cases-user-actions": "3973dfcaacbe6ae147d7331699cfc25d2a27ca30", "config": "e3f0408976dbdd453641f5699927b28b188f6b8c", + "config-global": "b8f559884931609a349e129c717af73d23e7bc76", "connector_token": "fa5301aa5a2914795d3b1b82d0a49939444009da", "core-usage-stats": "f40a213da2c597b0de94e364a4326a5a1baa4ca9", "csp-rule-template": "3679c5f2431da8153878db79c78a4e695357fb61", @@ -104,7 +105,7 @@ describe('checking migration metadata changes on all registered SO types', () => "ingest-agent-policies": "9170cdad95d887c036b87adf0ff38a3f12800c05", "ingest-download-sources": "1e69dabd6db5e320fe08c5bda8f35f29bafc6b54", "ingest-outputs": "4888b16d55a452bf5fff2bb407e0361567eae63a", - "ingest-package-policies": "e8707a8c7821ea085e67c2d213e24efa56307393", + "ingest-package-policies": "d93048bf153f9043946e8965065a88014f7ccb41", "ingest_manager_settings": "6f36714825cc15ea8d7cda06fde7851611a532b4", "inventory-view": "bc2bd1e7ec7c186159447ab228d269f22bd39056", "kql-telemetry": "29544cd7d3b767c5399878efae6bd724d24c03fd", @@ -124,7 +125,7 @@ describe('checking migration metadata changes on all registered SO types', () => "osquery-saved-query": "7b213b4b7a3e59350e99c50e8df9948662ed493a", "query": "4640ef356321500a678869f24117b7091a911cb6", "sample-data-telemetry": "8b10336d9efae6f3d5593c4cc89fb4abcdf84e04", - "search": "d26771bcf7cd271162aab3a610b75249631ef6b1", + "search": "c48f5ab5d94545780ea98de1bff9e39f17f3606b", "search-session": "ba383309da68a15be3765977f7a44c84f0ec7964", "search-telemetry": "beb3fc25488c753f2a6dcff1845d667558712b66", "security-rule": "e0dfdba5d66139d0300723b2e6672993cd4a11f3", @@ -136,7 +137,7 @@ describe('checking migration metadata changes on all registered SO types', () => "siem-ui-timeline-pinned-event": "e2697b38751506c7fce6e8b7207a830483dc4283", "space": "c4a0acce1bd4b9cce85154f2a350624a53111c59", "spaces-usage-stats": "922d3235bbf519e3fb3b260e27248b1df8249b79", - "synthetics-monitor": "d784b64a3def47d3f3d1f367df71ae41ef33cb3c", + "synthetics-monitor": "7c1e5a78fb3b88cc03b441d3bf3714d9967ab214", "synthetics-privates-locations": "dd00385f4a27ef062c3e57312eeb3799872fa4af", "tag": "39413f4578cc2128c9a0fda97d0acd1c8862c47a", "task": "ef53d0f070bd54957b8fe22fae3b1ff208913f76", diff --git a/src/core/server/integration_tests/saved_objects/migrations/check_target_mappings.test.ts b/src/core/server/integration_tests/saved_objects/migrations/check_target_mappings.test.ts new file mode 100644 index 0000000000000..4acebc4c527f4 --- /dev/null +++ b/src/core/server/integration_tests/saved_objects/migrations/check_target_mappings.test.ts @@ -0,0 +1,190 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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 Path from 'path'; +import fs from 'fs/promises'; +import JSON5 from 'json5'; +import { Env } from '@kbn/config'; +import { REPO_ROOT } from '@kbn/utils'; +import { getEnvOptions } from '@kbn/config-mocks'; +import { Root } from '@kbn/core-root-server-internal'; +import { LogRecord } from '@kbn/logging'; +import { + createRootWithCorePlugins, + createTestServers, + type TestElasticsearchUtils, +} from '@kbn/core-test-helpers-kbn-server'; + +const logFilePath = Path.join(__dirname, 'check_target_mappings.log'); + +const delay = (seconds: number) => new Promise((resolve) => setTimeout(resolve, seconds * 1000)); + +async function removeLogFile() { + // ignore errors if it doesn't exist + await fs.unlink(logFilePath).catch(() => void 0); +} + +async function parseLogFile() { + const logFileContent = await fs.readFile(logFilePath, 'utf-8'); + + return logFileContent + .split('\n') + .filter(Boolean) + .map((str) => JSON5.parse(str)) as LogRecord[]; +} + +function logIncludes(logs: LogRecord[], message: string): boolean { + return Boolean(logs?.find((rec) => rec.message.includes(message))); +} + +describe('migration v2 - CHECK_TARGET_MAPPINGS', () => { + let esServer: TestElasticsearchUtils; + let root: Root; + let logs: LogRecord[]; + + beforeEach(async () => await removeLogFile()); + + afterEach(async () => { + await root?.shutdown(); + await esServer?.stop(); + await delay(10); + }); + + it('is not run for new installations', async () => { + const { startES } = createTestServers({ + adjustTimeout: (t: number) => jest.setTimeout(t), + settings: { + es: { + license: 'basic', + }, + }, + }); + + root = createRoot(); + esServer = await startES(); + await root.preboot(); + await root.setup(); + await root.start(); + + // Check for migration steps present in the logs + logs = await parseLogFile(); + expect(logIncludes(logs, 'CREATE_NEW_TARGET')).toEqual(true); + expect(logIncludes(logs, 'CHECK_TARGET_MAPPINGS')).toEqual(false); + }); + + it('skips UPDATE_TARGET_MAPPINGS for up-to-date deployments, when there are no changes in the mappings', async () => { + const { startES } = createTestServers({ + adjustTimeout: (t: number) => jest.setTimeout(t), + settings: { + es: { + license: 'basic', + }, + }, + }); + + esServer = await startES(); + + // start Kibana a first time to create the system indices + root = createRoot(); + await root.preboot(); + await root.setup(); + await root.start(); + + // stop Kibana and remove logs + await root.shutdown(); + await delay(10); + await removeLogFile(); + + root = createRoot(); + await root.preboot(); + await root.setup(); + await root.start(); + + // Check for migration steps present in the logs + logs = await parseLogFile(); + expect(logIncludes(logs, 'CREATE_NEW_TARGET')).toEqual(false); + expect(logIncludes(logs, 'CHECK_TARGET_MAPPINGS -> CHECK_VERSION_INDEX_READY_ACTIONS')).toEqual( + true + ); + expect(logIncludes(logs, 'UPDATE_TARGET_MAPPINGS')).toEqual(false); + expect(logIncludes(logs, 'UPDATE_TARGET_MAPPINGS_WAIT_FOR_TASK')).toEqual(false); + expect(logIncludes(logs, 'UPDATE_TARGET_MAPPINGS_META')).toEqual(false); + }); + + it('runs UPDATE_TARGET_MAPPINGS when mappings have changed', async () => { + const currentVersion = Env.createDefault(REPO_ROOT, getEnvOptions()).packageInfo.version; + + const { startES } = createTestServers({ + adjustTimeout: (t: number) => jest.setTimeout(t), + settings: { + es: { + license: 'basic', + dataArchive: Path.join(__dirname, 'archives', '8.4.0_with_sample_data_logs.zip'), + }, + }, + }); + + esServer = await startES(); + + // start Kibana a first time to create the system indices + root = createRoot(currentVersion); // we discard a bunch of SO that have become unknown since 8.4.0 + await root.preboot(); + await root.setup(); + await root.start(); + + // Check for migration steps present in the logs + logs = await parseLogFile(); + expect(logIncludes(logs, 'CREATE_NEW_TARGET')).toEqual(false); + expect(logIncludes(logs, 'CHECK_TARGET_MAPPINGS -> UPDATE_TARGET_MAPPINGS')).toEqual(true); + expect( + logIncludes(logs, 'UPDATE_TARGET_MAPPINGS -> UPDATE_TARGET_MAPPINGS_WAIT_FOR_TASK') + ).toEqual(true); + expect( + logIncludes(logs, 'UPDATE_TARGET_MAPPINGS_WAIT_FOR_TASK -> UPDATE_TARGET_MAPPINGS_META') + ).toEqual(true); + expect( + logIncludes(logs, 'UPDATE_TARGET_MAPPINGS_META -> CHECK_VERSION_INDEX_READY_ACTIONS') + ).toEqual(true); + expect( + logIncludes(logs, 'CHECK_VERSION_INDEX_READY_ACTIONS -> MARK_VERSION_INDEX_READY') + ).toEqual(true); + expect(logIncludes(logs, 'MARK_VERSION_INDEX_READY -> DONE')).toEqual(true); + expect(logIncludes(logs, 'Migration completed')).toEqual(true); + }); +}); + +function createRoot(discardUnknownObjects?: string) { + return createRootWithCorePlugins( + { + migrations: { + discardUnknownObjects, + }, + logging: { + appenders: { + file: { + type: 'file', + fileName: logFilePath, + layout: { + type: 'json', + }, + }, + }, + loggers: [ + { + name: 'root', + level: 'info', + appenders: ['file'], + }, + ], + }, + }, + { + oss: true, + } + ); +} diff --git a/src/core/server/integration_tests/saved_objects/migrations/type_registrations.test.ts b/src/core/server/integration_tests/saved_objects/migrations/type_registrations.test.ts index 188473320435d..ab4498a79941c 100644 --- a/src/core/server/integration_tests/saved_objects/migrations/type_registrations.test.ts +++ b/src/core/server/integration_tests/saved_objects/migrations/type_registrations.test.ts @@ -36,6 +36,7 @@ const previouslyRegisteredTypes = [ 'cases-user-actions', 'cases-telemetry', 'config', + 'config-global', 'connector_token', 'core-usage-stats', 'csp-rule-template', diff --git a/src/core/server/integration_tests/ui_settings/create_or_upgrade.test.ts b/src/core/server/integration_tests/ui_settings/create_or_upgrade.test.ts index c58821b467c78..2964d69010c8a 100644 --- a/src/core/server/integration_tests/ui_settings/create_or_upgrade.test.ts +++ b/src/core/server/integration_tests/ui_settings/create_or_upgrade.test.ts @@ -80,6 +80,7 @@ describe('createOrUpgradeSavedConfig()', () => { buildNum: 54099, log: logger, handleWriteErrors: false, + type: 'config', }); const config540 = await savedObjectsClient.get('config', '5.4.0'); @@ -108,6 +109,7 @@ describe('createOrUpgradeSavedConfig()', () => { buildNum: 54199, log: logger, handleWriteErrors: false, + type: 'config', }); const config541 = await savedObjectsClient.get('config', '5.4.1'); @@ -136,6 +138,7 @@ describe('createOrUpgradeSavedConfig()', () => { buildNum: 70010, log: logger, handleWriteErrors: false, + type: 'config', }); const config700rc1 = await savedObjectsClient.get('config', '7.0.0-rc1'); @@ -165,6 +168,7 @@ describe('createOrUpgradeSavedConfig()', () => { buildNum: 70099, log: logger, handleWriteErrors: false, + type: 'config', }); const config700 = await savedObjectsClient.get('config', '7.0.0'); @@ -195,6 +199,7 @@ describe('createOrUpgradeSavedConfig()', () => { buildNum: 62310, log: logger, handleWriteErrors: false, + type: 'config', }); const config623rc1 = await savedObjectsClient.get('config', '6.2.3-rc1'); diff --git a/src/dev/bazel/ts_project.bzl b/src/dev/bazel/ts_project.bzl index afd28fa513164..5c4009d46fd19 100644 --- a/src/dev/bazel/ts_project.bzl +++ b/src/dev/bazel/ts_project.bzl @@ -2,7 +2,13 @@ load("@npm//@bazel/typescript:index.bzl", _ts_project = "ts_project") -def ts_project(validate = False, **kwargs): +def contains(list, item): + for i in list: + if i == item: + return True + return False + +def ts_project(validate = False, deps = [], **kwargs): """A macro around the upstream ts_project rule. Args: @@ -10,7 +16,11 @@ def ts_project(validate = False, **kwargs): **kwargs: the rest """ + if contains(deps, "@npm//tslib") == False: + deps = deps + ["@npm//tslib"] + _ts_project( validate = validate, + deps = deps, **kwargs ) diff --git a/src/dev/build/tasks/bundle_fleet_packages.ts b/src/dev/build/tasks/bundle_fleet_packages.ts index 30cfc1d22b0e6..f7fe9ffc76573 100644 --- a/src/dev/build/tasks/bundle_fleet_packages.ts +++ b/src/dev/build/tasks/bundle_fleet_packages.ts @@ -70,8 +70,12 @@ export const BundleFleetPackages: Task = { const archivePath = `${fleetPackage.name}-${versionToWrite}.zip`; let archiveUrl = `${eprUrl}/epr/${fleetPackage.name}/${fleetPackage.name}-${fleetPackage.version}.zip`; - // Point APM to package storage v2 - if (fleetPackage.name === 'apm') { + // Point APM, Endpoint and Synthetics packages to package storage v2 + if ( + fleetPackage.name === 'apm' || + fleetPackage.name === 'endpoint' || + fleetPackage.name === 'synthetics' + ) { archiveUrl = `${PACKAGE_STORAGE_V2_URL}/epr/${fleetPackage.name}/${fleetPackage.name}-${fleetPackage.version}.zip`; } diff --git a/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker b/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker index 377bffcfdacc9..a7fba2f23a50b 100755 --- a/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker +++ b/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker @@ -392,8 +392,6 @@ kibana_vars=( xpack.securitySolution.maxTimelineImportExportSize xpack.securitySolution.maxTimelineImportPayloadBytes xpack.securitySolution.packagerTaskInterval - xpack.securitySolution.prebuiltRulesFromFileSystem - xpack.securitySolution.prebuiltRulesFromSavedObjects xpack.spaces.maxSpaces xpack.task_manager.max_attempts xpack.task_manager.max_poll_inactivity_cycles diff --git a/src/dev/build/tasks/os_packages/docker_generator/templates/ironbank/Dockerfile b/src/dev/build/tasks/os_packages/docker_generator/templates/ironbank/Dockerfile index 4dc44d861f104..184d2cfb9414d 100644 --- a/src/dev/build/tasks/os_packages/docker_generator/templates/ironbank/Dockerfile +++ b/src/dev/build/tasks/os_packages/docker_generator/templates/ironbank/Dockerfile @@ -4,7 +4,7 @@ ################################################################################ ARG BASE_REGISTRY=registry1.dso.mil ARG BASE_IMAGE=redhat/ubi/ubi8 -ARG BASE_TAG=8.6 +ARG BASE_TAG=8.7 FROM ${BASE_REGISTRY}/${BASE_IMAGE}:${BASE_TAG} as prep_files diff --git a/src/dev/license_checker/config.ts b/src/dev/license_checker/config.ts index eaa968f3c94e8..eebb7e8eabe58 100644 --- a/src/dev/license_checker/config.ts +++ b/src/dev/license_checker/config.ts @@ -84,6 +84,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.3': ['Elastic License 2.0'], - '@elastic/eui@70.2.4': ['SSPL-1.0 OR Elastic License 2.0'], + '@elastic/eui@70.4.0': ['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/performance/run_performance_cli.ts b/src/dev/performance/run_performance_cli.ts new file mode 100644 index 0000000000000..ac0e708dcee0a --- /dev/null +++ b/src/dev/performance/run_performance_cli.ts @@ -0,0 +1,110 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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 { run } from '@kbn/dev-cli-runner'; +import { REPO_ROOT } from '@kbn/utils'; +import Fsp from 'fs/promises'; +import path from 'path'; + +run( + async ({ log, flagsReader, procRunner }) => { + async function runFunctionalTest(journey: string, phase: 'TEST' | 'WARMUP') { + // Pass in a clean APM environment, so that FTR can later + // set it's own values. + const cleanApmEnv = { + ELASTIC_APM_TRANSACTION_SAMPLE_RATE: undefined, + ELASTIC_APM_SERVER_URL: undefined, + ELASTIC_APM_SECRET_TOKEN: undefined, + ELASTIC_APM_ACTIVE: undefined, + ELASTIC_APM_CONTEXT_PROPAGATION_ONLY: undefined, + ELASTIC_APM_GLOBAL_LABELS: undefined, + }; + + await procRunner.run('functional-tests', { + cmd: 'node', + args: [ + 'scripts/functional_tests', + ['--config', path.join(journeyBasePath, journey)], + ['--kibana-install-dir', kibanaInstallDir], + '--debug', + '--bail', + ].flat(), + cwd: REPO_ROOT, + wait: true, + env: { + TEST_PERFORMANCE_PHASE: phase, + TEST_ES_URL: 'http://elastic:changeme@localhost:9200', + TEST_ES_DISABLE_STARTUP: 'true', + ...cleanApmEnv, + }, + }); + } + + async function startEs() { + process.stdout.write(`--- Starting ES\n`); + await procRunner.run('es', { + cmd: 'node', + args: ['scripts/es', 'snapshot'], + cwd: REPO_ROOT, + wait: /kbn\/es setup complete/, + }); + + log.info(`✅ ES is ready and will run in the background`); + } + + async function runWarmup(journey: string) { + try { + process.stdout.write(`--- Running warmup ${journey}\n`); + // Set the phase to WARMUP, this will prevent the functional test server from starting Elasticsearch, opt in to telemetry, etc. + await runFunctionalTest(journey, 'WARMUP'); + } catch (e) { + log.warning(`Warmup for ${journey} failed`); + throw e; + } + } + + async function runTest(journey: string) { + try { + process.stdout.write(`--- Running test ${journey}\n`); + await runFunctionalTest(journey, 'TEST'); + } catch (e) { + log.warning(`Journey ${journey} failed. Retrying once...`); + await runFunctionalTest(journey, 'TEST'); + } + } + + const journeyBasePath = path.resolve(REPO_ROOT, 'x-pack/performance/journeys/'); + const kibanaInstallDir = flagsReader.requiredPath('kibana-install-dir'); + const journeys = await Fsp.readdir(journeyBasePath); + log.info(`Found ${journeys.length} journeys to run`); + + const failedJourneys = []; + + for (const journey of journeys) { + try { + await startEs(); + await runWarmup(journey); + await runTest(journey); + } catch (e) { + log.error(e); + failedJourneys.push(journey); + } finally { + await procRunner.stop('es'); + } + } + + if (failedJourneys.length > 0) { + throw new Error(`${failedJourneys.length} journeys failed: ${failedJourneys.join(',')}`); + } + }, + { + flags: { + string: ['kibana-install-dir'], + }, + } +); diff --git a/src/dev/performance/run_scalability_cli.ts b/src/dev/performance/run_scalability_cli.ts new file mode 100644 index 0000000000000..eec11e611f3e1 --- /dev/null +++ b/src/dev/performance/run_scalability_cli.ts @@ -0,0 +1,104 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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 { createFlagError } from '@kbn/dev-cli-errors'; +import { run } from '@kbn/dev-cli-runner'; +import { REPO_ROOT } from '@kbn/utils'; +import fs from 'fs'; +import path from 'path'; + +run( + async ({ log, flagsReader, procRunner }) => { + const kibanaInstallDir = flagsReader.path('kibana-install-dir'); + const journeyConfigPath = flagsReader.requiredPath('journey-config-path'); + + if (kibanaInstallDir && !fs.existsSync(kibanaInstallDir)) { + throw createFlagError('--kibana-install-dir must be an existing directory'); + } + if ( + !fs.existsSync(journeyConfigPath) || + (!fs.statSync(journeyConfigPath).isDirectory() && path.extname(journeyConfigPath) !== '.json') + ) { + throw createFlagError( + '--journey-config-path must be an existing directory or scalability json path' + ); + } + + const journeys = fs.statSync(journeyConfigPath).isDirectory() + ? fs + .readdirSync(journeyConfigPath) + .filter((fileName) => path.extname(fileName) === '.json') + .map((fileName) => path.resolve(journeyConfigPath, fileName)) + : [journeyConfigPath]; + + log.info(`Found ${journeys.length} journeys to run:\n${JSON.stringify(journeys)}`); + + const failedJourneys = []; + + for (const journey of journeys) { + try { + process.stdout.write(`--- Running scalability journey: ${journey}\n`); + await runScalabilityJourney(journey, kibanaInstallDir); + } catch (e) { + log.error(e); + failedJourneys.push(journey); + } + } + + if (failedJourneys.length > 0) { + throw new Error(`${failedJourneys.length} journeys failed: ${failedJourneys.join(',')}`); + } + + async function runScalabilityJourney(filePath: string, kibanaDir?: string) { + // Pass in a clean APM environment, so that FTR can later + // set it's own values. + const cleanApmEnv = { + ELASTIC_APM_ACTIVE: undefined, + ELASTIC_APM_BREAKDOWN_METRICS: undefined, + ELASTIC_APM_CONTEXT_PROPAGATION_ONLY: undefined, + ELASTIC_APM_CAPTURE_SPAN_STACK_TRACES: undefined, + ELASTIC_APM_ENVIRONMENT: undefined, + ELASTIC_APM_GLOBAL_LABELS: undefined, + ELASTIC_APM_MAX_QUEUE_SIZE: undefined, + ELASTIC_APM_METRICS_INTERVAL: undefined, + ELASTIC_APM_SERVER_URL: undefined, + ELASTIC_APM_SECRET_TOKEN: undefined, + ELASTIC_APM_TRANSACTION_SAMPLE_RATE: undefined, + }; + + await procRunner.run('scalability-tests', { + cmd: 'node', + args: [ + 'scripts/functional_tests', + ['--config', 'x-pack/test/scalability/config.ts'], + kibanaDir ? ['--kibana-install-dir', kibanaDir] : [], + '--debug', + '--logToFile', + '--bail', + ].flat(), + cwd: REPO_ROOT, + wait: true, + env: { + ...cleanApmEnv, + SCALABILITY_JOURNEY_PATH: filePath, // journey json file for Gatling test runner + KIBANA_DIR: REPO_ROOT, // Gatling test runner use it to find kbn/es archives + }, + }); + } + }, + { + flags: { + string: ['kibana-install-dir', 'journey-config-path'], + help: ` + --kibana-install-dir Run Kibana from existing install directory instead of from source + --journey-config-path Define a scalability journey config or directory with multiple + configs that should be executed + `, + }, + } +); diff --git a/src/dev/storybook/aliases.ts b/src/dev/storybook/aliases.ts index 7ad2c16224d62..7c37284bc3e77 100644 --- a/src/dev/storybook/aliases.ts +++ b/src/dev/storybook/aliases.ts @@ -33,7 +33,6 @@ export const storybookAliases = { expression_reveal_image: 'src/plugins/expression_reveal_image/.storybook', expression_shape: 'src/plugins/expression_shape/.storybook', expression_tagcloud: 'src/plugins/chart_expressions/expression_tagcloud/.storybook', - files: 'src/plugins/files/.storybook', fleet: 'x-pack/plugins/fleet/.storybook', home: 'src/plugins/home/.storybook', infra: 'x-pack/plugins/infra/.storybook', diff --git a/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/__snapshots__/mosaic_vis_function.test.ts.snap b/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/__snapshots__/mosaic_vis_function.test.ts.snap index c00de511b8afb..f8b999c2bb764 100644 --- a/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/__snapshots__/mosaic_vis_function.test.ts.snap +++ b/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/__snapshots__/mosaic_vis_function.test.ts.snap @@ -103,6 +103,7 @@ Object { "splitRow": undefined, }, "labels": Object { + "colorOverrides": Object {}, "last_level": false, "percentDecimals": 2, "position": "default", diff --git a/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/__snapshots__/pie_vis_function.test.ts.snap b/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/__snapshots__/pie_vis_function.test.ts.snap index 65cd755d51a07..9e71fcec0c8fa 100644 --- a/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/__snapshots__/pie_vis_function.test.ts.snap +++ b/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/__snapshots__/pie_vis_function.test.ts.snap @@ -104,6 +104,7 @@ Object { "emptySizeRatio": 0.3, "isDonut": true, "labels": Object { + "colorOverrides": Object {}, "last_level": false, "percentDecimals": 2, "position": "default", @@ -244,6 +245,7 @@ Object { "emptySizeRatio": 0.3, "isDonut": false, "labels": Object { + "colorOverrides": Object {}, "last_level": false, "percentDecimals": 2, "position": "default", diff --git a/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/__snapshots__/treemap_vis_function.test.ts.snap b/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/__snapshots__/treemap_vis_function.test.ts.snap index 5388a47242fb4..891b217df37f0 100644 --- a/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/__snapshots__/treemap_vis_function.test.ts.snap +++ b/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/__snapshots__/treemap_vis_function.test.ts.snap @@ -103,6 +103,7 @@ Object { "splitRow": undefined, }, "labels": Object { + "colorOverrides": Object {}, "last_level": false, "percentDecimals": 2, "position": "default", diff --git a/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/__snapshots__/waffle_vis_function.test.ts.snap b/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/__snapshots__/waffle_vis_function.test.ts.snap index 180c3221240ce..50400b3839b57 100644 --- a/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/__snapshots__/waffle_vis_function.test.ts.snap +++ b/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/__snapshots__/waffle_vis_function.test.ts.snap @@ -77,6 +77,7 @@ Object { "splitRow": undefined, }, "labels": Object { + "colorOverrides": Object {}, "last_level": false, "percentDecimals": 2, "position": "default", diff --git a/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/mosaic_vis_function.test.ts b/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/mosaic_vis_function.test.ts index 46816ee1f34b8..fd2951a2f1fb6 100644 --- a/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/mosaic_vis_function.test.ts +++ b/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/mosaic_vis_function.test.ts @@ -52,6 +52,7 @@ describe('interpreter/functions#mosaicVis', () => { percentDecimals: 2, truncate: 100, last_level: false, + colorOverrides: {}, }, metric: { type: 'vis_dimension', diff --git a/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/partition_labels_function.ts b/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/partition_labels_function.ts index 48ecab49dc23b..fbd8fa84d3a20 100644 --- a/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/partition_labels_function.ts +++ b/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/partition_labels_function.ts @@ -89,6 +89,15 @@ export const partitionLabelsFunction = (): ExpressionFunctionDefinition< ), options: [ValueFormats.PERCENT, ValueFormats.VALUE], }, + colorOverrides: { + types: ['string'], + help: i18n.translate( + 'expressionPartitionVis.partitionLabels.function.args.colorOverrides.help', + { + defaultMessage: 'Defines specific colors for specific labels.', + } + ), + }, }, fn: (context, args) => { return { @@ -97,8 +106,9 @@ export const partitionLabelsFunction = (): ExpressionFunctionDefinition< position: args.position, percentDecimals: args.percentDecimals, values: args.values, - truncate: args.truncate, valuesFormat: args.valuesFormat, + colorOverrides: args.colorOverrides ? JSON.parse(args.colorOverrides) : {}, + truncate: args.truncate, last_level: args.last_level, }; }, diff --git a/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/pie_vis_function.test.ts b/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/pie_vis_function.test.ts index 0c222758d912a..dc975e9a92758 100644 --- a/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/pie_vis_function.test.ts +++ b/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/pie_vis_function.test.ts @@ -53,6 +53,7 @@ describe('interpreter/functions#pieVis', () => { percentDecimals: 2, truncate: 100, last_level: false, + colorOverrides: {}, }, metrics: [ { diff --git a/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/treemap_vis_function.test.ts b/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/treemap_vis_function.test.ts index e5bc4115c1461..edc8ec8b99100 100644 --- a/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/treemap_vis_function.test.ts +++ b/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/treemap_vis_function.test.ts @@ -53,6 +53,7 @@ describe('interpreter/functions#treemapVis', () => { percentDecimals: 2, truncate: 100, last_level: false, + colorOverrides: {}, }, metrics: [ { diff --git a/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/waffle_vis_function.test.ts b/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/waffle_vis_function.test.ts index 4c81f64428a74..606ff2c9b84c2 100644 --- a/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/waffle_vis_function.test.ts +++ b/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/waffle_vis_function.test.ts @@ -53,6 +53,7 @@ describe('interpreter/functions#waffleVis', () => { percentDecimals: 2, truncate: 100, last_level: false, + colorOverrides: {}, }, metrics: [ { diff --git a/src/plugins/chart_expressions/expression_partition_vis/common/types/expression_functions.ts b/src/plugins/chart_expressions/expression_partition_vis/common/types/expression_functions.ts index 30c5aba33ebf1..f5f2f0ef7f3cd 100644 --- a/src/plugins/chart_expressions/expression_partition_vis/common/types/expression_functions.ts +++ b/src/plugins/chart_expressions/expression_partition_vis/common/types/expression_functions.ts @@ -36,6 +36,7 @@ export interface PartitionLabelsArguments { values: boolean; valuesFormat: ValueFormats; percentDecimals: number; + colorOverrides?: string; /** @deprecated This field is deprecated and going to be removed in the futher release versions. */ truncate?: number | null; /** @deprecated This field is deprecated and going to be removed in the futher release versions. */ @@ -50,6 +51,7 @@ export type ExpressionValuePartitionLabels = ExpressionValueBoxed< values: boolean; valuesFormat: ValueFormats; percentDecimals: number; + colorOverrides: Record; /** @deprecated This field is deprecated and going to be removed in the futher release versions. */ truncate?: number | null; /** @deprecated This field is deprecated and going to be removed in the futher release versions. */ diff --git a/src/plugins/chart_expressions/expression_partition_vis/common/types/expression_renderers.ts b/src/plugins/chart_expressions/expression_partition_vis/common/types/expression_renderers.ts index 9584a810d7ca4..b5c9ad985dd49 100644 --- a/src/plugins/chart_expressions/expression_partition_vis/common/types/expression_renderers.ts +++ b/src/plugins/chart_expressions/expression_partition_vis/common/types/expression_renderers.ts @@ -41,6 +41,7 @@ export interface LabelsParams { values: boolean; valuesFormat: ValueFormats; percentDecimals: number; + colorOverrides: Record; /** @deprecated This field is deprecated and going to be removed in the futher release versions. */ truncate?: number | null; /** @deprecated This field is deprecated and going to be removed in the futher release versions. */ @@ -95,7 +96,8 @@ export interface TreemapVisConfig extends VisCommonConfig { nestedLegend: boolean; } -export interface MosaicVisConfig extends Omit { +export interface MosaicVisConfig + extends Omit { metric: ExpressionValueVisDimension | string; nestedLegend: boolean; } diff --git a/src/plugins/chart_expressions/expression_partition_vis/public/__stories__/shared/config.ts b/src/plugins/chart_expressions/expression_partition_vis/public/__stories__/shared/config.ts index aa4023006d486..544e5ea0ce593 100644 --- a/src/plugins/chart_expressions/expression_partition_vis/public/__stories__/shared/config.ts +++ b/src/plugins/chart_expressions/expression_partition_vis/public/__stories__/shared/config.ts @@ -34,6 +34,7 @@ export const config: RenderValue['visConfig'] = { truncate: 0, valuesFormat: ValueFormats.PERCENT, last_level: false, + colorOverrides: {}, }, dimensions: { metrics: [ diff --git a/src/plugins/chart_expressions/expression_partition_vis/public/components/partition_vis_component.tsx b/src/plugins/chart_expressions/expression_partition_vis/public/components/partition_vis_component.tsx index ed1789f2ae4a9..352f03f59e619 100644 --- a/src/plugins/chart_expressions/expression_partition_vis/public/components/partition_vis_component.tsx +++ b/src/plugins/chart_expressions/expression_partition_vis/public/components/partition_vis_component.tsx @@ -290,7 +290,7 @@ const PartitionVisComponent = (props: PartitionVisComponentProps) => { bucketColumns, visParams, visData, - props.uiState?.get('vis.colors', {}), + { ...props.uiState?.get('vis.colors', {}), ...props.visParams.labels.colorOverrides }, visData.rows, props.palettesRegistry, formatters, @@ -304,6 +304,7 @@ const PartitionVisComponent = (props: PartitionVisComponentProps) => { visParams, visData, props.uiState, + props.visParams.labels.colorOverrides, props.palettesRegistry, formatters, services.fieldFormats, diff --git a/src/plugins/chart_expressions/expression_partition_vis/public/mocks.ts b/src/plugins/chart_expressions/expression_partition_vis/public/mocks.ts index c125243f3a09a..89c61b6e7818c 100644 --- a/src/plugins/chart_expressions/expression_partition_vis/public/mocks.ts +++ b/src/plugins/chart_expressions/expression_partition_vis/public/mocks.ts @@ -282,6 +282,7 @@ export const createMockPartitionVisParams = (): PartitionVisParams => { values: true, valuesFormat: ValueFormats.PERCENT, percentDecimals: 2, + colorOverrides: {}, }, legendPosition: 'right', nestedLegend: false, diff --git a/src/plugins/chart_expressions/expression_partition_vis/public/utils/layers/get_color.ts b/src/plugins/chart_expressions/expression_partition_vis/public/utils/layers/get_color.ts index 9e55bec92fbaf..16c49f1c31889 100644 --- a/src/plugins/chart_expressions/expression_partition_vis/public/utils/layers/get_color.ts +++ b/src/plugins/chart_expressions/expression_partition_vis/public/utils/layers/get_color.ts @@ -9,10 +9,12 @@ import { ShapeTreeNode } from '@elastic/charts'; import { isEqual } from 'lodash'; import type { PaletteRegistry, SeriesLayer, PaletteOutput, PaletteDefinition } from '@kbn/coloring'; import type { FieldFormatsStart } from '@kbn/field-formats-plugin/public'; +import type { FieldFormat } from '@kbn/field-formats-plugin/common'; import { lightenColor } from '@kbn/charts-plugin/public'; import type { Datatable, DatatableRow } from '@kbn/expressions-plugin/public'; import { BucketColumns, ChartTypes, PartitionVisParams } from '../../../common/types'; import { DistinctSeries, getDistinctSeries } from '../get_distinct_series'; +import { getNodeLabel } from './get_node_labels'; const isTreemapOrMosaicChart = (shape: ChartTypes) => [ChartTypes.MOSAIC, ChartTypes.TREEMAP].includes(shape); @@ -109,15 +111,24 @@ const getDistinctColor = ( const createSeriesLayers = ( d: ShapeTreeNode, parentSeries: DistinctSeries['parentSeries'], - isSplitChart: boolean + isSplitChart: boolean, + formatters: Record, + formatter: FieldFormatsStart, + column: Partial ) => { const seriesLayers: SeriesLayer[] = []; let tempParent: typeof d | typeof d['parent'] = d; while (tempParent.parent && tempParent.depth > 0) { const seriesName = String(tempParent.parent.children[tempParent.sortIndex][0]); const isSplitParentLayer = isSplitChart && parentSeries.includes(seriesName); + const formattedName = getNodeLabel( + tempParent.parent.children[tempParent.sortIndex][0], + column, + formatters, + formatter.deserialize + ); seriesLayers.unshift({ - name: seriesName, + name: formattedName ?? seriesName, rankAtDepth: isSplitParentLayer ? parentSeries.findIndex((name) => name === seriesName) : tempParent.sortIndex, @@ -130,15 +141,13 @@ const createSeriesLayers = ( return seriesLayers; }; -const overrideColorForOldVisualization = ( +const overrideColors = ( seriesLayers: SeriesLayer[], overwriteColors: { [key: string]: string }, name: string ) => { let overwriteColor; - // this is for supporting old visualizations (created by vislib plugin) - // it seems that there for some aggs, the uiState saved from vislib is - // different than the es-charts handle it + if (overwriteColors.hasOwnProperty(name)) { overwriteColor = overwriteColors[name]; } @@ -166,7 +175,8 @@ export const getColor = ( syncColors: boolean, isDarkMode: boolean, formatter: FieldFormatsStart, - format?: BucketColumns['format'] + column: Partial, + formatters: Record ) => { const distinctSeries = getDistinctSeries(rows, columns); const { parentSeries } = distinctSeries; @@ -177,8 +187,8 @@ export const getColor = ( const defaultColor = isDarkMode ? 'rgba(0,0,0,0)' : 'rgba(255,255,255,0)'; let name = ''; - if (format) { - name = formatter.deserialize(format).convert(dataName) ?? ''; + if (column.format) { + name = formatter.deserialize(column.format).convert(dataName) ?? ''; } if (visParams.distinctColors) { @@ -196,11 +206,19 @@ export const getColor = ( ); } - const seriesLayers = createSeriesLayers(d, parentSeries, isSplitChart); + const seriesLayers = createSeriesLayers( + d, + parentSeries, + isSplitChart, + formatters, + formatter, + column + ); - const overwriteColor = overrideColorForOldVisualization(seriesLayers, overwriteColors, name); - if (overwriteColor) { - return lightenColor(overwriteColor, seriesLayers.length, columns.length); + const overriddenColor = overrideColors(seriesLayers, overwriteColors, name); + if (overriddenColor) { + // this is necessary for supporting some old visualizations that defined their own colors (created by vislib plugin) + return lightenColor(overriddenColor, seriesLayers.length, columns.length); } if (chartType === ChartTypes.MOSAIC && byDataPalette && seriesLayers[1]) { diff --git a/src/plugins/chart_expressions/expression_partition_vis/public/utils/layers/get_layers.test.ts b/src/plugins/chart_expressions/expression_partition_vis/public/utils/layers/get_layers.test.ts index cab4beda3f794..d96ada51ba47a 100644 --- a/src/plugins/chart_expressions/expression_partition_vis/public/utils/layers/get_layers.test.ts +++ b/src/plugins/chart_expressions/expression_partition_vis/public/utils/layers/get_layers.test.ts @@ -8,9 +8,11 @@ import { ShapeTreeNode } from '@elastic/charts'; import type { PaletteDefinition, SeriesLayer } from '@kbn/coloring'; import { dataPluginMock } from '@kbn/data-plugin/public/mocks'; +import { fieldFormatsMock } from '@kbn/field-formats-plugin/common/mocks'; import type { DataPublicPluginStart } from '@kbn/data-plugin/public'; import { getColor } from './get_color'; import { createMockVisData, createMockBucketColumns, createMockPieParams } from '../../mocks'; +import { generateFormatters } from '../formatters'; import { ChartTypes } from '../../../common/types'; const visData = createMockVisData(); @@ -22,6 +24,8 @@ interface RangeProps { gte: number; lt: number; } +const defaultFormatter = jest.fn((...args) => fieldFormatsMock.deserialize(...args)); +const formatters = generateFormatters(visData, defaultFormatter); dataMock.fieldFormats = { deserialize: jest.fn(() => ({ @@ -82,7 +86,9 @@ describe('computeColor', () => { { getColor: () => undefined }, false, false, - dataMock.fieldFormats + dataMock.fieldFormats, + visData.columns[0], + formatters ); expect(color).toEqual(colors[0]); }); @@ -111,7 +117,9 @@ describe('computeColor', () => { { getColor: () => undefined }, false, false, - dataMock.fieldFormats + dataMock.fieldFormats, + visData.columns[0], + formatters ); expect(color).toEqual('color3'); }); @@ -139,7 +147,9 @@ describe('computeColor', () => { { getColor: () => undefined }, false, false, - dataMock.fieldFormats + dataMock.fieldFormats, + visData.columns[0], + formatters ); expect(color).toEqual('#000028'); }); @@ -175,6 +185,15 @@ describe('computeColor', () => { ...visParams, distinctColors: true, }; + const column = { + ...visData.columns[0], + format: { + id: 'range', + params: { + id: 'number', + }, + }, + }; const color = getColor( ChartTypes.PIE, d, @@ -189,12 +208,8 @@ describe('computeColor', () => { false, false, dataMock.fieldFormats, - { - id: 'range', - params: { - id: 'number', - }, - } + column, + formatters ); expect(color).toEqual('#3F6833'); }); @@ -229,7 +244,9 @@ describe('computeColor', () => { undefined, true, false, - dataMock.fieldFormats + dataMock.fieldFormats, + visData.columns[0], + formatters ); expect(registry.get().getCategoricalColor).toHaveBeenCalledWith( [expect.objectContaining({ name: 'Second level 1' })], @@ -268,7 +285,9 @@ describe('computeColor', () => { undefined, true, false, - dataMock.fieldFormats + dataMock.fieldFormats, + visData.columns[0], + formatters ); expect(registry.get().getCategoricalColor).toHaveBeenCalledWith( [expect.objectContaining({ name: 'First level' })], diff --git a/src/plugins/chart_expressions/expression_partition_vis/public/utils/layers/get_layers.ts b/src/plugins/chart_expressions/expression_partition_vis/public/utils/layers/get_layers.ts index c8d7d1f30b4d5..646a0e4b45d2e 100644 --- a/src/plugins/chart_expressions/expression_partition_vis/public/utils/layers/get_layers.ts +++ b/src/plugins/chart_expressions/expression_partition_vis/public/utils/layers/get_layers.ts @@ -79,7 +79,8 @@ export const getLayers = ( syncColors, isDarkMode, formatter, - col.format + col, + formatters ), }, }; diff --git a/src/plugins/controls/public/control_group/control_group_input_builder.ts b/src/plugins/controls/public/control_group/control_group_input_builder.ts new file mode 100644 index 0000000000000..a128025d52548 --- /dev/null +++ b/src/plugins/controls/public/control_group/control_group_input_builder.ts @@ -0,0 +1,157 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { i18n } from '@kbn/i18n'; +import uuid from 'uuid'; +import { ControlPanelState, OptionsListEmbeddableInput } from '../../common'; +import { + DEFAULT_CONTROL_GROW, + DEFAULT_CONTROL_WIDTH, +} from '../../common/control_group/control_group_constants'; +import { RangeValue } from '../../common/range_slider/types'; +import { + ControlInput, + ControlWidth, + DataControlInput, + OPTIONS_LIST_CONTROL, + RANGE_SLIDER_CONTROL, + TIME_SLIDER_CONTROL, +} from '..'; +import { ControlGroupInput } from './types'; +import { getCompatibleControlType, getNextPanelOrder } from './embeddable/control_group_helpers'; + +export interface AddDataControlProps { + controlId?: string; + dataViewId: string; + fieldName: string; + grow?: boolean; + title?: string; + width?: ControlWidth; +} + +export type AddOptionsListControlProps = AddDataControlProps & Partial; + +export type AddRangeSliderControlProps = AddDataControlProps & { + value?: RangeValue; +}; + +export const controlGroupInputBuilder = { + addDataControlFromField: async ( + initialInput: Partial, + controlProps: AddDataControlProps + ) => { + const panelState = await getDataControlPanelState(initialInput, controlProps); + initialInput.panels = { + ...initialInput.panels, + [panelState.explicitInput.id]: panelState, + }; + }, + addOptionsListControl: ( + initialInput: Partial, + controlProps: AddOptionsListControlProps + ) => { + const panelState = getOptionsListPanelState(initialInput, controlProps); + initialInput.panels = { + ...initialInput.panels, + [panelState.explicitInput.id]: panelState, + }; + }, + addRangeSliderControl: ( + initialInput: Partial, + controlProps: AddRangeSliderControlProps + ) => { + const panelState = getRangeSliderPanelState(initialInput, controlProps); + initialInput.panels = { + ...initialInput.panels, + [panelState.explicitInput.id]: panelState, + }; + }, + addTimeSliderControl: (initialInput: Partial) => { + const panelState = getTimeSliderPanelState(initialInput); + initialInput.panels = { + ...initialInput.panels, + [panelState.explicitInput.id]: panelState, + }; + }, +}; + +export async function getDataControlPanelState( + input: Partial, + controlProps: AddDataControlProps +) { + const { controlId, dataViewId, fieldName, title } = controlProps; + return { + type: await getCompatibleControlType({ dataViewId, fieldName }), + ...getPanelState(input, controlProps), + explicitInput: { + id: controlId ? controlId : uuid.v4(), + dataViewId, + fieldName, + title: title ?? fieldName, + }, + } as ControlPanelState; +} + +export function getOptionsListPanelState( + input: Partial, + controlProps: AddOptionsListControlProps +) { + const { controlId, dataViewId, fieldName, title, ...rest } = controlProps; + return { + type: OPTIONS_LIST_CONTROL, + ...getPanelState(input, controlProps), + explicitInput: { + id: controlId ? controlId : uuid.v4(), + dataViewId, + fieldName, + title: title ?? fieldName, + ...rest, + }, + } as ControlPanelState; +} + +export function getRangeSliderPanelState( + input: Partial, + controlProps: AddRangeSliderControlProps +) { + const { controlId, dataViewId, fieldName, title, ...rest } = controlProps; + return { + type: RANGE_SLIDER_CONTROL, + ...getPanelState(input, controlProps), + explicitInput: { + id: controlId ? controlId : uuid.v4(), + dataViewId, + fieldName, + title: title ?? fieldName, + ...rest, + }, + } as ControlPanelState; +} + +export function getTimeSliderPanelState(input: Partial) { + return { + type: TIME_SLIDER_CONTROL, + order: getNextPanelOrder(input.panels), + grow: true, + width: 'large', + explicitInput: { + id: uuid.v4(), + title: i18n.translate('controls.controlGroup.timeSlider.title', { + defaultMessage: 'Time slider', + }), + }, + } as ControlPanelState; +} + +function getPanelState(input: Partial, controlProps: AddDataControlProps) { + return { + order: getNextPanelOrder(input.panels), + grow: controlProps.grow ?? input.defaultControlGrow ?? DEFAULT_CONTROL_GROW, + width: controlProps.width ?? input.defaultControlWidth ?? DEFAULT_CONTROL_WIDTH, + }; +} diff --git a/src/plugins/controls/public/control_group/control_group_renderer.tsx b/src/plugins/controls/public/control_group/control_group_renderer.tsx index 57245e23b2a1a..fc5852cff18e8 100644 --- a/src/plugins/controls/public/control_group/control_group_renderer.tsx +++ b/src/plugins/controls/public/control_group/control_group_renderer.tsx @@ -14,7 +14,7 @@ import { IEmbeddable } from '@kbn/embeddable-plugin/public'; import { useReduxContainerContext } from '@kbn/presentation-util-plugin/public'; import { pluginServices } from '../services'; -import { ControlPanelState, getDefaultControlGroupInput } from '../../common'; +import { getDefaultControlGroupInput } from '../../common'; import { ControlGroupInput, ControlGroupOutput, @@ -22,60 +22,29 @@ import { CONTROL_GROUP_TYPE, } from './types'; import { ControlGroupContainer } from './embeddable/control_group_container'; -import { DataControlInput } from '../types'; -import { getCompatibleControlType, getNextPanelOrder } from './embeddable/control_group_helpers'; import { controlGroupReducers } from './state/control_group_reducers'; - -const ControlGroupInputBuilder = { - addDataControlFromField: async ( - initialInput: Partial, - newPanelInput: { - title?: string; - panelId?: string; - fieldName: string; - dataViewId: string; - } & Partial - ) => { - const { defaultControlGrow, defaultControlWidth } = getDefaultControlGroupInput(); - const controlGrow = initialInput.defaultControlGrow ?? defaultControlGrow; - const controlWidth = initialInput.defaultControlWidth ?? defaultControlWidth; - - const { panelId, dataViewId, fieldName, title, grow, width } = newPanelInput; - const newPanelId = panelId || uuid.v4(); - const nextOrder = getNextPanelOrder(initialInput); - const controlType = await getCompatibleControlType({ dataViewId, fieldName }); - - initialInput.panels = { - ...initialInput.panels, - [newPanelId]: { - order: nextOrder, - type: controlType, - grow: grow ?? controlGrow, - width: width ?? controlWidth, - explicitInput: { id: newPanelId, dataViewId, fieldName, title: title ?? fieldName }, - } as ControlPanelState, - }; - }, -}; +import { controlGroupInputBuilder } from './control_group_input_builder'; export interface ControlGroupRendererProps { - onEmbeddableLoad: (controlGroupContainer: ControlGroupContainer) => void; - getCreationOptions: ( - builder: typeof ControlGroupInputBuilder + onLoadComplete?: (controlGroup: ControlGroupContainer) => void; + getInitialInput: ( + initialInput: Partial, + builder: typeof controlGroupInputBuilder ) => Promise>; } export const ControlGroupRenderer = ({ - onEmbeddableLoad, - getCreationOptions, + onLoadComplete, + getInitialInput, }: ControlGroupRendererProps) => { - const controlsRoot = useRef(null); - const [controlGroupContainer, setControlGroupContainer] = useState(); + const controlGroupRef = useRef(null); + const [controlGroup, setControlGroup] = useState(); const id = useMemo(() => uuid.v4(), []); /** * Use Lifecycles to load initial control group container */ useLifecycles( + // onMount () => { const { embeddable } = pluginServices.getServices(); (async () => { @@ -84,25 +53,28 @@ export const ControlGroupRenderer = ({ ControlGroupOutput, IEmbeddable >(CONTROL_GROUP_TYPE); - const container = (await factory?.create({ + const newControlGroup = (await factory?.create({ id, ...getDefaultControlGroupInput(), - ...(await getCreationOptions(ControlGroupInputBuilder)), + ...(await getInitialInput(getDefaultControlGroupInput(), controlGroupInputBuilder)), })) as ControlGroupContainer; - if (controlsRoot.current) { - container.render(controlsRoot.current); + if (controlGroupRef.current) { + newControlGroup.render(controlGroupRef.current); + } + setControlGroup(newControlGroup); + if (onLoadComplete) { + onLoadComplete(newControlGroup); } - setControlGroupContainer(container); - onEmbeddableLoad(container); })(); }, + // onUnmount () => { - controlGroupContainer?.destroy(); + controlGroup?.destroy(); } ); - return
; + return
; }; export const useControlGroupContainerContext = () => diff --git a/src/plugins/controls/public/control_group/editor/create_time_slider_control.tsx b/src/plugins/controls/public/control_group/editor/create_time_slider_control.tsx index 9137bab8b8068..3688422a2eebb 100644 --- a/src/plugins/controls/public/control_group/editor/create_time_slider_control.tsx +++ b/src/plugins/controls/public/control_group/editor/create_time_slider_control.tsx @@ -9,17 +9,15 @@ import { i18n } from '@kbn/i18n'; import React from 'react'; import { EuiContextMenuItem } from '@elastic/eui'; -import type { ControlInput } from '../../types'; -import { TIME_SLIDER_CONTROL } from '../../time_slider/types'; interface Props { - addNewEmbeddable: (type: string, input: Omit) => void; + onCreate: () => void; closePopover?: () => void; hasTimeSliderControl: boolean; } export const CreateTimeSliderControlButton = ({ - addNewEmbeddable, + onCreate, closePopover, hasTimeSliderControl, }: Props) => { @@ -27,11 +25,7 @@ export const CreateTimeSliderControlButton = ({ { - addNewEmbeddable(TIME_SLIDER_CONTROL, { - title: i18n.translate('controls.controlGroup.timeSlider.title', { - defaultMessage: 'Time slider', - }), - }); + onCreate(); if (closePopover) { closePopover(); } diff --git a/src/plugins/controls/public/control_group/embeddable/control_group_container.tsx b/src/plugins/controls/public/control_group/embeddable/control_group_container.tsx index 3e95100d95cfe..46774a0747b89 100644 --- a/src/plugins/controls/public/control_group/embeddable/control_group_container.tsx +++ b/src/plugins/controls/public/control_group/embeddable/control_group_container.tsx @@ -40,11 +40,22 @@ import { ControlGroupStrings } from '../control_group_strings'; import { EditControlGroup } from '../editor/edit_control_group'; import { ControlGroup } from '../component/control_group_component'; import { controlGroupReducers } from '../state/control_group_reducers'; -import { ControlEmbeddable, ControlInput, ControlOutput, DataControlInput } from '../../types'; +import { OPTIONS_LIST_CONTROL, RANGE_SLIDER_CONTROL, TIME_SLIDER_CONTROL } from '../..'; +import { ControlEmbeddable, ControlInput, ControlOutput } from '../../types'; import { CreateControlButton, CreateControlButtonTypes } from '../editor/create_control'; import { CreateTimeSliderControlButton } from '../editor/create_time_slider_control'; -import { TIME_SLIDER_CONTROL } from '../../time_slider'; -import { getCompatibleControlType, getNextPanelOrder } from './control_group_helpers'; +import { getNextPanelOrder } from './control_group_helpers'; +import type { + AddDataControlProps, + AddOptionsListControlProps, + AddRangeSliderControlProps, +} from '../control_group_input_builder'; +import { + getDataControlPanelState, + getOptionsListPanelState, + getRangeSliderPanelState, + getTimeSliderPanelState, +} from '../control_group_input_builder'; let flyoutRef: OverlayRef | undefined; export const setFlyoutRef = (newRef: OverlayRef | undefined) => { @@ -96,23 +107,24 @@ export class ControlGroupContainer extends Container< flyoutRef = undefined; } - public async addDataControlFromField({ - uuid, - dataViewId, - fieldName, - title, - }: { - uuid?: string; - dataViewId: string; - fieldName: string; - title?: string; - }) { - return this.addNewEmbeddable(await getCompatibleControlType({ dataViewId, fieldName }), { - id: uuid, - dataViewId, - fieldName, - title: title ?? fieldName, - } as DataControlInput); + public async addDataControlFromField(controlProps: AddDataControlProps) { + const panelState = await getDataControlPanelState(this.getInput(), controlProps); + return this.createAndSaveEmbeddable(panelState.type, panelState); + } + + public addOptionsListControl(controlProps: AddOptionsListControlProps) { + const panelState = getOptionsListPanelState(this.getInput(), controlProps); + return this.createAndSaveEmbeddable(panelState.type, panelState); + } + + public addRangeSliderControl(controlProps: AddRangeSliderControlProps) { + const panelState = getRangeSliderPanelState(this.getInput(), controlProps); + return this.createAndSaveEmbeddable(panelState.type, panelState); + } + + public addTimeSliderControl() { + const panelState = getTimeSliderPanelState(this.getInput()); + return this.createAndSaveEmbeddable(panelState.type, panelState); } /** @@ -138,7 +150,19 @@ export class ControlGroupContainer extends Container< updateDefaultGrow={(defaultControlGrow: boolean) => this.updateInput({ defaultControlGrow }) } - addNewEmbeddable={(type, input) => this.addNewEmbeddable(type, input)} + addNewEmbeddable={(type, input) => { + if (type === OPTIONS_LIST_CONTROL) { + this.addOptionsListControl(input as AddOptionsListControlProps); + return; + } + + if (type === RANGE_SLIDER_CONTROL) { + this.addRangeSliderControl(input as AddRangeSliderControlProps); + return; + } + + this.addDataControlFromField(input as AddDataControlProps); + }} closePopover={closePopover} getRelevantDataViewId={() => this.getMostRelevantDataViewId()} setLastUsedDataViewId={(newId) => this.setLastUsedDataViewId(newId)} @@ -155,7 +179,9 @@ export class ControlGroupContainer extends Container< }); return ( this.addNewEmbeddable(type, input)} + onCreate={() => { + this.addTimeSliderControl(); + }} closePopover={closePopover} hasTimeSliderControl={hasTimeSliderControl} /> @@ -317,12 +343,10 @@ export class ControlGroupContainer extends Container< partial: Partial = {} ): ControlPanelState { const panelState = super.createNewPanelState(factory, partial); - const nextOrder = getNextPanelOrder(this.getInput()); return { - order: nextOrder, - width: - panelState.type === TIME_SLIDER_CONTROL ? 'large' : this.getInput().defaultControlWidth, - grow: panelState.type === TIME_SLIDER_CONTROL ? true : this.getInput().defaultControlGrow, + order: getNextPanelOrder(this.getInput().panels), + width: this.getInput().defaultControlWidth, + grow: this.getInput().defaultControlGrow, ...panelState, } as ControlPanelState; } diff --git a/src/plugins/controls/public/control_group/embeddable/control_group_helpers.ts b/src/plugins/controls/public/control_group/embeddable/control_group_helpers.ts index 817cf9c280155..1afcdc539bf87 100644 --- a/src/plugins/controls/public/control_group/embeddable/control_group_helpers.ts +++ b/src/plugins/controls/public/control_group/embeddable/control_group_helpers.ts @@ -6,15 +6,15 @@ * Side Public License, v 1. */ -import { ControlGroupInput } from '../types'; +import { ControlsPanels } from '../types'; import { pluginServices } from '../../services'; import { getDataControlFieldRegistry } from '../editor/data_control_editor_tools'; -export const getNextPanelOrder = (initialInput: Partial) => { +export const getNextPanelOrder = (panels?: ControlsPanels) => { let nextOrder = 0; - if (Object.keys(initialInput.panels ?? {}).length > 0) { + if (Object.keys(panels ?? {}).length > 0) { nextOrder = - Object.values(initialInput.panels ?? {}).reduce((highestSoFar, panel) => { + Object.values(panels ?? {}).reduce((highestSoFar, panel) => { if (panel.order > highestSoFar) highestSoFar = panel.order; return highestSoFar; }, 0) + 1; diff --git a/src/plugins/controls/public/control_group/index.ts b/src/plugins/controls/public/control_group/index.ts index b55d63134439b..1967a8074beab 100644 --- a/src/plugins/controls/public/control_group/index.ts +++ b/src/plugins/controls/public/control_group/index.ts @@ -14,6 +14,12 @@ export type { ControlGroupInput, ControlGroupOutput } from './types'; export { CONTROL_GROUP_TYPE } from './types'; export { ControlGroupContainerFactory } from './embeddable/control_group_container_factory'; +export { + type AddDataControlProps, + type AddOptionsListControlProps, + controlGroupInputBuilder, +} from './control_group_input_builder'; + export { type ControlGroupRendererProps, useControlGroupContainerContext, diff --git a/src/plugins/controls/public/index.ts b/src/plugins/controls/public/index.ts index 2b6732335472a..29c2af9ad46f5 100644 --- a/src/plugins/controls/public/index.ts +++ b/src/plugins/controls/public/index.ts @@ -22,6 +22,7 @@ export type { ControlStyle, ParentIgnoreSettings, ControlInput, + DataControlInput, } from '../common/types'; export { @@ -32,9 +33,12 @@ export { } from '../common'; export { + type AddDataControlProps, + type AddOptionsListControlProps, type ControlGroupContainer, ControlGroupContainerFactory, type ControlGroupInput, + controlGroupInputBuilder, type ControlGroupOutput, } from './control_group'; diff --git a/src/plugins/dashboard/public/application/actions/filters_notification_action.test.tsx b/src/plugins/dashboard/public/application/actions/filters_notification_action.test.tsx new file mode 100644 index 0000000000000..be9dc25f69fb9 --- /dev/null +++ b/src/plugins/dashboard/public/application/actions/filters_notification_action.test.tsx @@ -0,0 +1,103 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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 { ErrorEmbeddable, isErrorEmbeddable, ViewMode } from '@kbn/embeddable-plugin/public'; +import { + ContactCardEmbeddable, + CONTACT_CARD_EMBEDDABLE, + ContactCardEmbeddableInput, + ContactCardEmbeddableOutput, + ContactCardEmbeddableFactory, +} from '@kbn/embeddable-plugin/public/lib/test_samples/embeddables'; +import { type Query, type AggregateQuery, Filter } from '@kbn/es-query'; +import { embeddablePluginMock } from '@kbn/embeddable-plugin/public/mocks'; + +import { getSampleDashboardInput } from '../test_helpers'; +import { pluginServices } from '../../services/plugin_services'; +import { DashboardContainer } from '../embeddable/dashboard_container'; +import { FiltersNotificationAction } from './filters_notification_action'; + +const mockEmbeddableFactory = new ContactCardEmbeddableFactory((() => null) as any, {} as any); +pluginServices.getServices().embeddable.getEmbeddableFactory = jest + .fn() + .mockReturnValue(mockEmbeddableFactory); + +const mockGetFilters = jest.fn(async () => [] as Filter[]); +const mockGetQuery = jest.fn(async () => undefined as Query | AggregateQuery | undefined); + +const getMockPhraseFilter = (key: string, value: string) => { + return { + meta: { + type: 'phrase', + key, + params: { + query: value, + }, + }, + query: { + match_phrase: { + [key]: value, + }, + }, + $state: { + store: 'appState', + }, + }; +}; + +const buildEmbeddable = async (input?: Partial) => { + const container = new DashboardContainer(getSampleDashboardInput()); + const contactCardEmbeddable = await container.addNewEmbeddable< + ContactCardEmbeddableInput, + ContactCardEmbeddableOutput, + ContactCardEmbeddable + >(CONTACT_CARD_EMBEDDABLE, { + firstName: 'Kibanana', + viewMode: ViewMode.EDIT, + ...input, + }); + if (isErrorEmbeddable(contactCardEmbeddable)) { + throw new Error('Failed to create embeddable'); + } + + const embeddable = embeddablePluginMock.mockFilterableEmbeddable(contactCardEmbeddable, { + getFilters: () => mockGetFilters(), + getQuery: () => mockGetQuery(), + }); + + return embeddable; +}; + +const action = new FiltersNotificationAction(); + +test('Badge is incompatible with Error Embeddables', async () => { + const errorEmbeddable = new ErrorEmbeddable('Wow what an awful error', { id: ' 404' }); + expect(await action.isCompatible({ embeddable: errorEmbeddable })).toBe(false); +}); + +test('Badge is not shown when panel has no app-level filters or queries', async () => { + const embeddable = await buildEmbeddable(); + expect(await action.isCompatible({ embeddable })).toBe(false); +}); + +test('Badge is shown when panel has at least one app-level filter', async () => { + const embeddable = await buildEmbeddable(); + mockGetFilters.mockResolvedValue([getMockPhraseFilter('fieldName', 'someValue')] as Filter[]); + expect(await action.isCompatible({ embeddable })).toBe(true); +}); + +test('Badge is shown when panel has at least one app-level query', async () => { + const embeddable = await buildEmbeddable(); + mockGetQuery.mockResolvedValue({ sql: 'SELECT * FROM test_dataview' } as AggregateQuery); + expect(await action.isCompatible({ embeddable })).toBe(true); +}); + +test('Badge is not shown in view mode', async () => { + const embeddable = await buildEmbeddable({ viewMode: ViewMode.VIEW }); + expect(await action.isCompatible({ embeddable })).toBe(false); +}); diff --git a/src/plugins/dashboard/public/application/actions/filters_notification_action.tsx b/src/plugins/dashboard/public/application/actions/filters_notification_action.tsx new file mode 100644 index 0000000000000..b7ee2311ebd18 --- /dev/null +++ b/src/plugins/dashboard/public/application/actions/filters_notification_action.tsx @@ -0,0 +1,117 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; + +import { EditPanelAction, isFilterableEmbeddable, ViewMode } from '@kbn/embeddable-plugin/public'; +import { type IEmbeddable, isErrorEmbeddable } from '@kbn/embeddable-plugin/public'; +import { KibanaThemeProvider, reactToUiComponent } from '@kbn/kibana-react-plugin/public'; +import { Action, IncompatibleActionError } from '@kbn/ui-actions-plugin/public'; +import { createKibanaReactContext } from '@kbn/kibana-react-plugin/public'; +import type { ApplicationStart } from '@kbn/core/public'; +import { type AggregateQuery } from '@kbn/es-query'; +import { I18nProvider } from '@kbn/i18n-react'; + +import { FiltersNotificationPopover } from './filters_notification_popover'; +import { dashboardFilterNotificationAction } from '../../dashboard_strings'; +import { pluginServices } from '../../services/plugin_services'; + +export const BADGE_FILTERS_NOTIFICATION = 'ACTION_FILTERS_NOTIFICATION'; + +export interface FiltersNotificationActionContext { + embeddable: IEmbeddable; +} + +export class FiltersNotificationAction implements Action { + public readonly id = BADGE_FILTERS_NOTIFICATION; + public readonly type = BADGE_FILTERS_NOTIFICATION; + public readonly order = 2; + + private displayName = dashboardFilterNotificationAction.getDisplayName(); + private icon = 'filter'; + private applicationService; + private embeddableService; + private settingsService; + + constructor() { + ({ + application: this.applicationService, + embeddable: this.embeddableService, + settings: this.settingsService, + } = pluginServices.getServices()); + } + + private FilterIconButton = ({ context }: { context: FiltersNotificationActionContext }) => { + const { embeddable } = context; + + const editPanelAction = new EditPanelAction( + this.embeddableService.getEmbeddableFactory, + this.applicationService as unknown as ApplicationStart, + this.embeddableService.getStateTransfer() + ); + + const { Provider: KibanaReactContextProvider } = createKibanaReactContext({ + uiSettings: this.settingsService.uiSettings, + }); + + return ( + + + + + + + + ); + }; + + public readonly MenuItem = reactToUiComponent(this.FilterIconButton); + + public getDisplayName({ embeddable }: FiltersNotificationActionContext) { + if (!embeddable.getRoot() || !embeddable.getRoot().isContainer) { + throw new IncompatibleActionError(); + } + return this.displayName; + } + + public getIconType({ embeddable }: FiltersNotificationActionContext) { + if (!embeddable.getRoot() || !embeddable.getRoot().isContainer) { + throw new IncompatibleActionError(); + } + return this.icon; + } + + public isCompatible = async ({ embeddable }: FiltersNotificationActionContext) => { + // add all possible early returns to avoid the async import unless absolutely necessary + if ( + isErrorEmbeddable(embeddable) || + !embeddable.getRoot().isContainer || + embeddable.getInput()?.viewMode !== ViewMode.EDIT || + !isFilterableEmbeddable(embeddable) + ) { + return false; + } + if ((await embeddable.getFilters()).length > 0) return true; + + // all early returns failed, so go ahead and check the query now + const { isOfQueryType, isOfAggregateQueryType } = await import('@kbn/es-query'); + const query = await embeddable.getQuery(); + return ( + (isOfQueryType(query) && query.query !== '') || + isOfAggregateQueryType(query as AggregateQuery) + ); + }; + + public execute = async () => {}; +} diff --git a/src/plugins/dashboard/public/application/actions/filters_notification_badge.test.tsx b/src/plugins/dashboard/public/application/actions/filters_notification_badge.test.tsx deleted file mode 100644 index 3b3fb5dde0497..0000000000000 --- a/src/plugins/dashboard/public/application/actions/filters_notification_badge.test.tsx +++ /dev/null @@ -1,103 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 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 { - IContainer, - ErrorEmbeddable, - isErrorEmbeddable, - FilterableEmbeddable, -} from '@kbn/embeddable-plugin/public'; -import { - ContactCardEmbeddable, - CONTACT_CARD_EMBEDDABLE, - ContactCardEmbeddableInput, - ContactCardEmbeddableOutput, - ContactCardEmbeddableFactory, -} from '@kbn/embeddable-plugin/public/lib/test_samples/embeddables'; -import { type Query, type AggregateQuery, Filter } from '@kbn/es-query'; -import { embeddablePluginMock } from '@kbn/embeddable-plugin/public/mocks'; - -import { getSampleDashboardInput } from '../test_helpers'; -import { pluginServices } from '../../services/plugin_services'; -import { DashboardContainer } from '../embeddable/dashboard_container'; -import { FiltersNotificationBadge } from './filters_notification_badge'; - -const mockEmbeddableFactory = new ContactCardEmbeddableFactory((() => null) as any, {} as any); -pluginServices.getServices().embeddable.getEmbeddableFactory = jest - .fn() - .mockReturnValue(mockEmbeddableFactory); - -let action: FiltersNotificationBadge; -let container: DashboardContainer; -let embeddable: ContactCardEmbeddable & FilterableEmbeddable; -const mockGetFilters = jest.fn(async () => [] as Filter[]); -const mockGetQuery = jest.fn(async () => undefined as Query | AggregateQuery | undefined); - -const getMockPhraseFilter = (key: string, value: string) => { - return { - meta: { - type: 'phrase', - key, - params: { - query: value, - }, - }, - query: { - match_phrase: { - [key]: value, - }, - }, - $state: { - store: 'appState', - }, - }; -}; - -beforeEach(async () => { - container = new DashboardContainer(getSampleDashboardInput()); - - const contactCardEmbeddable = await container.addNewEmbeddable< - ContactCardEmbeddableInput, - ContactCardEmbeddableOutput, - ContactCardEmbeddable - >(CONTACT_CARD_EMBEDDABLE, { - firstName: 'Kibanana', - }); - if (isErrorEmbeddable(contactCardEmbeddable)) { - throw new Error('Failed to create embeddable'); - } - - action = new FiltersNotificationBadge(); - embeddable = embeddablePluginMock.mockFilterableEmbeddable(contactCardEmbeddable, { - getFilters: () => mockGetFilters(), - getQuery: () => mockGetQuery(), - }); -}); - -test('Badge is incompatible with Error Embeddables', async () => { - const errorEmbeddable = new ErrorEmbeddable( - 'Wow what an awful error', - { id: ' 404' }, - embeddable.getRoot() as IContainer - ); - expect(await action.isCompatible({ embeddable: errorEmbeddable })).toBe(false); -}); - -test('Badge is not shown when panel has no app-level filters or queries', async () => { - expect(await action.isCompatible({ embeddable })).toBe(false); -}); - -test('Badge is shown when panel has at least one app-level filter', async () => { - mockGetFilters.mockResolvedValue([getMockPhraseFilter('fieldName', 'someValue')] as Filter[]); - expect(await action.isCompatible({ embeddable })).toBe(true); -}); - -test('Badge is shown when panel has at least one app-level query', async () => { - mockGetQuery.mockResolvedValue({ sql: 'SELECT * FROM test_dataview' } as AggregateQuery); - expect(await action.isCompatible({ embeddable })).toBe(true); -}); diff --git a/src/plugins/dashboard/public/application/actions/filters_notification_badge.tsx b/src/plugins/dashboard/public/application/actions/filters_notification_badge.tsx deleted file mode 100644 index 6dbe7d5dbe3c9..0000000000000 --- a/src/plugins/dashboard/public/application/actions/filters_notification_badge.tsx +++ /dev/null @@ -1,127 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import React from 'react'; - -import { EditPanelAction, isFilterableEmbeddable } from '@kbn/embeddable-plugin/public'; -import { type AggregateQuery } from '@kbn/es-query'; -import { toMountPoint } from '@kbn/kibana-react-plugin/public'; -import type { ApplicationStart } from '@kbn/core/public'; -import { createKibanaReactContext } from '@kbn/kibana-react-plugin/public'; -import { Action, IncompatibleActionError } from '@kbn/ui-actions-plugin/public'; -import { type IEmbeddable, isErrorEmbeddable } from '@kbn/embeddable-plugin/public'; - -import { dashboardFilterNotificationBadge } from '../../dashboard_strings'; -import { pluginServices } from '../../services/plugin_services'; - -export const BADGE_FILTERS_NOTIFICATION = 'ACTION_FILTERS_NOTIFICATION'; - -export interface FiltersNotificationActionContext { - embeddable: IEmbeddable; -} - -export class FiltersNotificationBadge implements Action { - public readonly id = BADGE_FILTERS_NOTIFICATION; - public readonly type = BADGE_FILTERS_NOTIFICATION; - public readonly order = 2; - - private displayName = dashboardFilterNotificationBadge.getDisplayName(); - private icon = 'filter'; - private applicationService; - private embeddableService; - private settingsService; - private openModal; - - constructor() { - ({ - application: this.applicationService, - embeddable: this.embeddableService, - overlays: { openModal: this.openModal }, - settings: this.settingsService, - } = pluginServices.getServices()); - } - - public getDisplayName({ embeddable }: FiltersNotificationActionContext) { - if (!embeddable.getRoot() || !embeddable.getRoot().isContainer) { - throw new IncompatibleActionError(); - } - return this.displayName; - } - - public getIconType({ embeddable }: FiltersNotificationActionContext) { - if (!embeddable.getRoot() || !embeddable.getRoot().isContainer) { - throw new IncompatibleActionError(); - } - return this.icon; - } - - public isCompatible = async ({ embeddable }: FiltersNotificationActionContext) => { - // add all possible early returns to avoid the async import unless absolutely necessary - if ( - isErrorEmbeddable(embeddable) || - !embeddable.getRoot().isContainer || - !isFilterableEmbeddable(embeddable) - ) { - return false; - } - if ((await embeddable.getFilters()).length > 0) return true; - - // all early returns failed, so go ahead and check the query now - const { isOfQueryType, isOfAggregateQueryType } = await import('@kbn/es-query'); - const query = await embeddable.getQuery(); - return ( - (isOfQueryType(query) && query.query !== '') || - isOfAggregateQueryType(query as AggregateQuery) - ); - }; - - public execute = async (context: FiltersNotificationActionContext) => { - const { embeddable } = context; - - const isCompatible = await this.isCompatible({ embeddable }); - if (!isCompatible || !isFilterableEmbeddable(embeddable)) { - throw new IncompatibleActionError(); - } - - const { - uiSettings, - theme: { theme$ }, - } = this.settingsService; - const { getEmbeddableFactory, getStateTransfer } = this.embeddableService; - - const { Provider: KibanaReactContextProvider } = createKibanaReactContext({ - uiSettings, - }); - const editPanelAction = new EditPanelAction( - getEmbeddableFactory, - this.applicationService as unknown as ApplicationStart, - getStateTransfer() - ); - const FiltersNotificationModal = await import('./filters_notification_modal').then( - (m) => m.FiltersNotificationModal - ); - - const session = this.openModal( - toMountPoint( - - session.close()} - /> - , - { theme$ } - ), - { - 'data-test-subj': 'filtersNotificationModal', - } - ); - }; -} diff --git a/src/plugins/dashboard/public/application/actions/filters_notification_modal.test.tsx b/src/plugins/dashboard/public/application/actions/filters_notification_modal.test.tsx deleted file mode 100644 index 9a83bf52e1092..0000000000000 --- a/src/plugins/dashboard/public/application/actions/filters_notification_modal.test.tsx +++ /dev/null @@ -1,107 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import React from 'react'; -import { findTestSubject, mountWithIntl } from '@kbn/test-jest-helpers'; -import { FilterableEmbeddable, isErrorEmbeddable, ViewMode } from '@kbn/embeddable-plugin/public'; - -import { DashboardContainer } from '../embeddable/dashboard_container'; -import { embeddablePluginMock } from '@kbn/embeddable-plugin/public/mocks'; -import { getSampleDashboardInput } from '../test_helpers'; -import { EuiModalFooter } from '@elastic/eui'; -import { FiltersNotificationModal, FiltersNotificationProps } from './filters_notification_modal'; -import { - ContactCardEmbeddable, - ContactCardEmbeddableFactory, - ContactCardEmbeddableInput, - ContactCardEmbeddableOutput, - CONTACT_CARD_EMBEDDABLE, -} from '@kbn/embeddable-plugin/public/lib/test_samples/embeddables'; -import { act } from 'react-dom/test-utils'; -import { pluginServices } from '../../services/plugin_services'; - -describe('LibraryNotificationPopover', () => { - const mockEmbeddableFactory = new ContactCardEmbeddableFactory((() => null) as any, {} as any); - pluginServices.getServices().embeddable.getEmbeddableFactory = jest - .fn() - .mockReturnValue(mockEmbeddableFactory); - - let container: DashboardContainer; - let embeddable: ContactCardEmbeddable & FilterableEmbeddable; - let defaultProps: FiltersNotificationProps; - - beforeEach(async () => { - container = new DashboardContainer(getSampleDashboardInput()); - const contactCardEmbeddable = await container.addNewEmbeddable< - ContactCardEmbeddableInput, - ContactCardEmbeddableOutput, - ContactCardEmbeddable - >(CONTACT_CARD_EMBEDDABLE, { - firstName: 'Kibanana', - }); - if (isErrorEmbeddable(contactCardEmbeddable)) { - throw new Error('Failed to create embeddable'); - } - embeddable = embeddablePluginMock.mockFilterableEmbeddable(contactCardEmbeddable, { - getFilters: jest.fn(), - getQuery: jest.fn(), - }); - - defaultProps = { - context: { embeddable: contactCardEmbeddable }, - displayName: 'test display', - id: 'testId', - editPanelAction: { - execute: jest.fn(), - } as unknown as FiltersNotificationProps['editPanelAction'], - onClose: jest.fn(), - }; - }); - - function mountComponent(props?: Partial) { - return mountWithIntl(); - } - - test('show modal footer in edit mode', async () => { - embeddable.updateInput({ viewMode: ViewMode.EDIT }); - await act(async () => { - const component = mountComponent(); - const footer = component.find(EuiModalFooter); - expect(footer.exists()).toBe(true); - }); - }); - - test('hide modal footer in view mode', async () => { - embeddable.updateInput({ viewMode: ViewMode.VIEW }); - await act(async () => { - const component = mountComponent(); - const footer = component.find(EuiModalFooter); - expect(footer.exists()).toBe(false); - }); - }); - - test('clicking edit button executes edit panel action', async () => { - embeddable.updateInput({ viewMode: ViewMode.EDIT }); - await act(async () => { - const component = mountComponent(); - const editButton = findTestSubject(component, 'filtersNotificationModal__editButton'); - editButton.simulate('click'); - expect(defaultProps.editPanelAction.execute).toHaveBeenCalled(); - }); - }); - - test('clicking close button calls onClose', async () => { - embeddable.updateInput({ viewMode: ViewMode.EDIT }); - await act(async () => { - const component = mountComponent(); - const editButton = findTestSubject(component, 'filtersNotificationModal__closeButton'); - editButton.simulate('click'); - expect(defaultProps.onClose).toHaveBeenCalled(); - }); - }); -}); diff --git a/src/plugins/dashboard/public/application/actions/filters_notification_modal.tsx b/src/plugins/dashboard/public/application/actions/filters_notification_modal.tsx deleted file mode 100644 index b188674a85b69..0000000000000 --- a/src/plugins/dashboard/public/application/actions/filters_notification_modal.tsx +++ /dev/null @@ -1,162 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import React, { useState } from 'react'; -import useMount from 'react-use/lib/useMount'; - -import { - EuiButton, - EuiButtonEmpty, - EuiCodeBlock, - EuiFlexGroup, - EuiFlexItem, - EuiForm, - EuiFormRow, - EuiLoadingContent, - EuiModalBody, - EuiModalFooter, - EuiModalHeader, - EuiModalHeaderTitle, -} from '@elastic/eui'; -import { css } from '@emotion/react'; -import { DataView } from '@kbn/data-views-plugin/public'; -import { - EditPanelAction, - FilterableEmbeddable, - IEmbeddable, - ViewMode, -} from '@kbn/embeddable-plugin/public'; -import { - type AggregateQuery, - type Filter, - getAggregateQueryMode, - isOfQueryType, -} from '@kbn/es-query'; -import { FilterItems } from '@kbn/unified-search-plugin/public'; - -import { FiltersNotificationActionContext } from './filters_notification_badge'; -import { DashboardContainer } from '../embeddable'; -import { dashboardFilterNotificationBadge } from '../../dashboard_strings'; - -export interface FiltersNotificationProps { - context: FiltersNotificationActionContext; - displayName: string; - id: string; - editPanelAction: EditPanelAction; - onClose: () => void; -} - -export function FiltersNotificationModal({ - context, - displayName, - id, - editPanelAction, - onClose, -}: FiltersNotificationProps) { - const { embeddable } = context; - const [isLoading, setIsLoading] = useState(true); - const [filters, setFilters] = useState([]); - const [queryString, setQueryString] = useState(''); - const [queryLanguage, setQueryLanguage] = useState<'sql' | 'esql' | undefined>(); - - useMount(() => { - Promise.all([ - (embeddable as IEmbeddable & FilterableEmbeddable).getFilters(), - (embeddable as IEmbeddable & FilterableEmbeddable).getQuery(), - ]).then(([embeddableFilters, embeddableQuery]) => { - setFilters(embeddableFilters); - if (embeddableQuery) { - if (isOfQueryType(embeddableQuery)) { - setQueryString(embeddableQuery.query as string); - } else { - const language = getAggregateQueryMode(embeddableQuery); - setQueryLanguage(language); - setQueryString(embeddableQuery[language as keyof AggregateQuery]); - } - } - setIsLoading(false); - }); - }); - - const dataViewList: DataView[] = (embeddable.getRoot() as DashboardContainer)?.getAllDataViews(); - const viewMode = embeddable.getInput().viewMode; - - return ( - <> - - -

{displayName}

-
-
- - - {isLoading ? ( - - ) : ( - - {queryString !== '' && ( - - - {queryString} - - - )} - {filters && filters.length > 0 && ( - - - - - - )} - - )} - - - {viewMode !== ViewMode.VIEW && ( - - - - - {dashboardFilterNotificationBadge.getCloseButtonTitle()} - - - - { - onClose(); - editPanelAction.execute(context); - }} - fill - > - {dashboardFilterNotificationBadge.getEditButtonTitle()} - - - - - )} - - ); -} diff --git a/src/plugins/dashboard/public/application/actions/filters_notification_popover.test.tsx b/src/plugins/dashboard/public/application/actions/filters_notification_popover.test.tsx new file mode 100644 index 0000000000000..92608264125ef --- /dev/null +++ b/src/plugins/dashboard/public/application/actions/filters_notification_popover.test.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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { mountWithIntl } from '@kbn/test-jest-helpers'; +import { findTestSubject } from '@elastic/eui/lib/test'; +import { FilterableEmbeddable, isErrorEmbeddable, ViewMode } from '@kbn/embeddable-plugin/public'; + +import { DashboardContainer } from '../embeddable/dashboard_container'; +import { embeddablePluginMock } from '@kbn/embeddable-plugin/public/mocks'; +import { getSampleDashboardInput } from '../test_helpers'; +import { EuiPopover } from '@elastic/eui'; +import { + FiltersNotificationPopover, + FiltersNotificationProps, +} from './filters_notification_popover'; +import { + ContactCardEmbeddable, + ContactCardEmbeddableFactory, + ContactCardEmbeddableInput, + ContactCardEmbeddableOutput, + CONTACT_CARD_EMBEDDABLE, +} from '@kbn/embeddable-plugin/public/lib/test_samples/embeddables'; +import { act } from 'react-dom/test-utils'; +import { pluginServices } from '../../services/plugin_services'; + +describe('LibraryNotificationPopover', () => { + const mockEmbeddableFactory = new ContactCardEmbeddableFactory((() => null) as any, {} as any); + pluginServices.getServices().embeddable.getEmbeddableFactory = jest + .fn() + .mockReturnValue(mockEmbeddableFactory); + + let container: DashboardContainer; + let embeddable: ContactCardEmbeddable & FilterableEmbeddable; + let defaultProps: FiltersNotificationProps; + + beforeEach(async () => { + container = new DashboardContainer(getSampleDashboardInput()); + const contactCardEmbeddable = await container.addNewEmbeddable< + ContactCardEmbeddableInput, + ContactCardEmbeddableOutput, + ContactCardEmbeddable + >(CONTACT_CARD_EMBEDDABLE, { + firstName: 'Kibanana', + }); + if (isErrorEmbeddable(contactCardEmbeddable)) { + throw new Error('Failed to create embeddable'); + } + embeddable = embeddablePluginMock.mockFilterableEmbeddable(contactCardEmbeddable, { + getFilters: jest.fn(), + getQuery: jest.fn(), + }); + + defaultProps = { + icon: 'test', + context: { embeddable: contactCardEmbeddable }, + displayName: 'test display', + id: 'testId', + editPanelAction: { + execute: jest.fn(), + } as unknown as FiltersNotificationProps['editPanelAction'], + }; + }); + + function mountComponent(props?: Partial) { + return mountWithIntl(); + } + + test('clicking edit button executes edit panel action', async () => { + embeddable.updateInput({ viewMode: ViewMode.EDIT }); + const component = mountComponent(); + + await act(async () => { + findTestSubject(component, `embeddablePanelNotification-${defaultProps.id}`).simulate( + 'click' + ); + }); + await act(async () => { + component.update(); + }); + + const popover = component.find(EuiPopover); + const editButton = findTestSubject(popover, 'filtersNotificationModal__editButton'); + editButton.simulate('click'); + expect(defaultProps.editPanelAction.execute).toHaveBeenCalled(); + }); +}); diff --git a/src/plugins/dashboard/public/application/actions/filters_notification_popover.tsx b/src/plugins/dashboard/public/application/actions/filters_notification_popover.tsx new file mode 100644 index 0000000000000..974c7280f8968 --- /dev/null +++ b/src/plugins/dashboard/public/application/actions/filters_notification_popover.tsx @@ -0,0 +1,83 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React, { useState } from 'react'; + +import { + EuiButton, + EuiPopover, + EuiFlexItem, + EuiFlexGroup, + EuiButtonIcon, + EuiPopoverTitle, + EuiPopoverFooter, +} from '@elastic/eui'; +import { EditPanelAction } from '@kbn/embeddable-plugin/public'; + +import { dashboardFilterNotificationAction } from '../../dashboard_strings'; +import { FiltersNotificationActionContext } from './filters_notification_action'; +import { FiltersNotificationPopoverContents } from './filters_notification_popover_contents'; + +export interface FiltersNotificationProps { + context: FiltersNotificationActionContext; + editPanelAction: EditPanelAction; + displayName: string; + icon: string; + id: string; +} + +export function FiltersNotificationPopover({ + editPanelAction, + displayName, + context, + icon, + id, +}: FiltersNotificationProps) { + const { embeddable } = context; + const [isPopoverOpen, setIsPopoverOpen] = useState(false); + + return ( + setIsPopoverOpen(!isPopoverOpen)} + data-test-subj={`embeddablePanelNotification-${id}`} + aria-label={displayName} + /> + } + isOpen={isPopoverOpen} + closePopover={() => setIsPopoverOpen(false)} + anchorPosition="upCenter" + > + {displayName} + + + + + editPanelAction.execute({ embeddable })} + > + {dashboardFilterNotificationAction.getEditButtonTitle()} + + + + + + ); +} diff --git a/src/plugins/dashboard/public/application/actions/filters_notification_popover_contents.tsx b/src/plugins/dashboard/public/application/actions/filters_notification_popover_contents.tsx new file mode 100644 index 0000000000000..b3c37f40d6c6c --- /dev/null +++ b/src/plugins/dashboard/public/application/actions/filters_notification_popover_contents.tsx @@ -0,0 +1,100 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React, { useMemo, useState } from 'react'; +import useMount from 'react-use/lib/useMount'; + +import { EuiCodeBlock, EuiFlexGroup, EuiForm, EuiFormRow, EuiLoadingContent } from '@elastic/eui'; +import { FilterableEmbeddable, IEmbeddable } from '@kbn/embeddable-plugin/public'; +import { FilterItems } from '@kbn/unified-search-plugin/public'; +import { css } from '@emotion/react'; +import { + type AggregateQuery, + type Filter, + getAggregateQueryMode, + isOfQueryType, +} from '@kbn/es-query'; + +import { FiltersNotificationActionContext } from './filters_notification_action'; +import { dashboardFilterNotificationAction } from '../../dashboard_strings'; +import { DashboardContainer } from '../embeddable'; + +export interface FiltersNotificationProps { + context: FiltersNotificationActionContext; +} + +export function FiltersNotificationPopoverContents({ context }: FiltersNotificationProps) { + const { embeddable } = context; + const [isLoading, setIsLoading] = useState(true); + const [filters, setFilters] = useState([]); + const [queryString, setQueryString] = useState(''); + const [queryLanguage, setQueryLanguage] = useState<'sql' | 'esql' | undefined>(); + + const dataViews = useMemo( + () => (embeddable.getRoot() as DashboardContainer)?.getAllDataViews(), + [embeddable] + ); + + useMount(() => { + Promise.all([ + (embeddable as IEmbeddable & FilterableEmbeddable).getFilters(), + (embeddable as IEmbeddable & FilterableEmbeddable).getQuery(), + ]).then(([embeddableFilters, embeddableQuery]) => { + setFilters(embeddableFilters); + if (embeddableQuery) { + if (isOfQueryType(embeddableQuery)) { + setQueryString(embeddableQuery.query as string); + } else { + const language = getAggregateQueryMode(embeddableQuery); + setQueryLanguage(language); + setQueryString(embeddableQuery[language as keyof AggregateQuery]); + } + } + setIsLoading(false); + }); + }); + + return ( + <> + {isLoading ? ( + + ) : ( + + {queryString !== '' && ( + + + {queryString} + + + )} + {filters && filters.length > 0 && ( + + + + + + )} + + )} + + ); +} diff --git a/src/plugins/dashboard/public/application/actions/index.ts b/src/plugins/dashboard/public/application/actions/index.ts index a238ce05e1017..7793c28037544 100644 --- a/src/plugins/dashboard/public/application/actions/index.ts +++ b/src/plugins/dashboard/public/application/actions/index.ts @@ -6,11 +6,7 @@ * Side Public License, v 1. */ -import { - CONTEXT_MENU_TRIGGER, - PANEL_BADGE_TRIGGER, - PANEL_NOTIFICATION_TRIGGER, -} from '@kbn/embeddable-plugin/public'; +import { CONTEXT_MENU_TRIGGER, PANEL_NOTIFICATION_TRIGGER } from '@kbn/embeddable-plugin/public'; import { CoreStart } from '@kbn/core/public'; import { getSavedObjectFinder } from '@kbn/saved-objects-plugin/public'; @@ -22,7 +18,7 @@ import { ReplacePanelAction } from './replace_panel_action'; import { AddToLibraryAction } from './add_to_library_action'; import { CopyToDashboardAction } from './copy_to_dashboard_action'; import { UnlinkFromLibraryAction } from './unlink_from_library_action'; -import { FiltersNotificationBadge } from './filters_notification_badge'; +import { FiltersNotificationAction } from './filters_notification_action'; import { LibraryNotificationAction } from './library_notification_action'; interface BuildAllDashboardActionsProps { @@ -48,14 +44,14 @@ export const buildAllDashboardActions = async ({ uiActions.registerAction(changeViewAction); uiActions.attachAction(CONTEXT_MENU_TRIGGER, changeViewAction.id); - const panelLevelFiltersNotification = new FiltersNotificationBadge(); - uiActions.registerAction(panelLevelFiltersNotification); - uiActions.attachAction(PANEL_BADGE_TRIGGER, panelLevelFiltersNotification.id); - const expandPanelAction = new ExpandPanelAction(); uiActions.registerAction(expandPanelAction); uiActions.attachAction(CONTEXT_MENU_TRIGGER, expandPanelAction.id); + const panelLevelFiltersNotificationAction = new FiltersNotificationAction(); + uiActions.registerAction(panelLevelFiltersNotificationAction); + uiActions.attachAction(PANEL_NOTIFICATION_TRIGGER, panelLevelFiltersNotificationAction.id); + if (share) { const ExportCSVPlugin = new ExportCSVAction(); uiActions.addTriggerAction(CONTEXT_MENU_TRIGGER, ExportCSVPlugin); diff --git a/src/plugins/dashboard/public/dashboard_strings.ts b/src/plugins/dashboard/public/dashboard_strings.ts index fcb7f3cb2a7c1..73c1969ae1996 100644 --- a/src/plugins/dashboard/public/dashboard_strings.ts +++ b/src/plugins/dashboard/public/dashboard_strings.ts @@ -191,7 +191,7 @@ export const dashboardReplacePanelAction = { }), }; -export const dashboardFilterNotificationBadge = { +export const dashboardFilterNotificationAction = { getDisplayName: () => i18n.translate('dashboard.panel.filters', { defaultMessage: 'Panel filters', diff --git a/src/plugins/dashboard/tsconfig.json b/src/plugins/dashboard/tsconfig.json index 96a2757909c12..390fc6f6a0a5e 100644 --- a/src/plugins/dashboard/tsconfig.json +++ b/src/plugins/dashboard/tsconfig.json @@ -26,7 +26,6 @@ { "path": "../screenshot_mode/tsconfig.json" }, { "path": "../ui_actions/tsconfig.json" }, { "path": "../charts/tsconfig.json" }, - { "path": "../discover/tsconfig.json" }, { "path": "../visualizations/tsconfig.json" }, { "path": "../../../x-pack/plugins/spaces/tsconfig.json" } ] diff --git a/src/plugins/data/common/search/expressions/esaggs/request_handler.test.ts b/src/plugins/data/common/search/expressions/esaggs/request_handler.test.ts index e0fbb10c05e3f..e0555fbd24076 100644 --- a/src/plugins/data/common/search/expressions/esaggs/request_handler.test.ts +++ b/src/plugins/data/common/search/expressions/esaggs/request_handler.test.ts @@ -143,6 +143,26 @@ describe('esaggs expression function - public', () => { }); }); + test('calls searchSource.fetch with custom inspector params', async () => { + await handleRequest({ + ...mockParams, + title: 'MyTitle', + description: 'MyDescription', + }).toPromise(); + const searchSource = await mockParams.searchSourceService.create(); + + expect(searchSource.fetch$).toHaveBeenCalledWith({ + abortSignal: mockParams.abortSignal, + sessionId: mockParams.searchSessionId, + inspector: { + title: 'MyTitle', + description: 'MyDescription', + adapter: undefined, + }, + disableShardFailureWarning: false, + }); + }); + test('tabifies response data', async () => { await handleRequest(mockParams).toPromise(); expect(tabifyAggResponse).toHaveBeenCalledWith( diff --git a/src/plugins/data/common/search/expressions/esaggs/request_handler.ts b/src/plugins/data/common/search/expressions/esaggs/request_handler.ts index 03512bcd2e270..1497dd9aa1a37 100644 --- a/src/plugins/data/common/search/expressions/esaggs/request_handler.ts +++ b/src/plugins/data/common/search/expressions/esaggs/request_handler.ts @@ -33,6 +33,8 @@ export interface RequestHandlerParams { disableShardWarnings?: boolean; getNow?: () => Date; executionContext?: KibanaExecutionContext; + title?: string; + description?: string; } export const handleRequest = ({ @@ -49,6 +51,8 @@ export const handleRequest = ({ disableShardWarnings, getNow, executionContext, + title, + description, }: RequestHandlerParams) => { return defer(async () => { const forceNow = getNow?.(); @@ -117,13 +121,17 @@ export const handleRequest = ({ sessionId: searchSessionId, inspector: { adapter: inspectorAdapters.requests, - title: i18n.translate('data.functions.esaggs.inspector.dataRequest.title', { - defaultMessage: 'Data', - }), - description: i18n.translate('data.functions.esaggs.inspector.dataRequest.description', { - defaultMessage: - 'This request queries Elasticsearch to fetch the data for the visualization.', - }), + title: + title ?? + i18n.translate('data.functions.esaggs.inspector.dataRequest.title', { + defaultMessage: 'Data', + }), + description: + description ?? + i18n.translate('data.functions.esaggs.inspector.dataRequest.description', { + defaultMessage: + 'This request queries Elasticsearch to fetch the data for the visualization.', + }), }, executionContext, }) diff --git a/src/plugins/data/server/ui_settings.ts b/src/plugins/data/server/ui_settings.ts index 8d666590b3d30..cfd664e2e53c8 100644 --- a/src/plugins/data/server/ui_settings.ts +++ b/src/plugins/data/server/ui_settings.ts @@ -324,8 +324,8 @@ export function getUiSettings( defaultMessage: 'Time filter refresh interval', }), value: `{ - "pause": false, - "value": 0 + "pause": true, + "value": 60000 }`, type: 'json', description: i18n.translate('data.advancedSettings.timepicker.refreshIntervalDefaultsText', { diff --git a/src/plugins/data_view_editor/public/components/data_view_editor_flyout_content.tsx b/src/plugins/data_view_editor/public/components/data_view_editor_flyout_content.tsx index 9b992be84e29e..d046ad3bb2990 100644 --- a/src/plugins/data_view_editor/public/components/data_view_editor_flyout_content.tsx +++ b/src/plugins/data_view_editor/public/components/data_view_editor_flyout_content.tsx @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import React, { useEffect, useCallback, useRef, useContext } from 'react'; +import React, { useEffect, useCallback } from 'react'; import { EuiTitle, EuiFlexGroup, @@ -17,10 +17,8 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import memoizeOne from 'memoize-one'; -import { BehaviorSubject } from 'rxjs'; import useObservable from 'react-use/lib/useObservable'; -import { INDEX_PATTERN_TYPE, MatchedItem } from '@kbn/data-views-plugin/public'; +import { INDEX_PATTERN_TYPE } from '@kbn/data-views-plugin/public'; import { DataView, @@ -32,7 +30,6 @@ import { UseField, } from '../shared_imports'; -import { ensureMinimumTime, getMatchedIndices } from '../lib'; import { FlyoutPanels } from './flyout_panels'; import { removeSpaces } from '../lib'; @@ -41,7 +38,6 @@ import { DataViewEditorContext, RollupIndicesCapsResponse, IndexPatternConfig, - MatchedIndicesSet, FormInternal, } from '../types'; @@ -57,7 +53,6 @@ import { RollupBetaWarning, } from '.'; import { editDataViewModal } from './confirm_modals/edit_data_view_changed_modal'; -import { DataViewEditorServiceContext } from './data_view_flyout_content_container'; import { DataViewEditorService } from '../data_view_editor_service'; export interface Props { @@ -70,19 +65,12 @@ export interface Props { */ onCancel: () => void; defaultTypeIsRollup?: boolean; - requireTimestampField?: boolean; editData?: DataView; showManagementLink?: boolean; allowAdHoc: boolean; + dataViewEditorService: DataViewEditorService; } -export const matchedIndiciesDefault = { - allIndices: [], - exactMatchedIndices: [], - partialMatchedIndices: [], - visibleIndices: [], -}; - const editorTitle = i18n.translate('indexPatternEditor.title', { defaultMessage: 'Create data view', }); @@ -95,17 +83,15 @@ const IndexPatternEditorFlyoutContentComponent = ({ onSave, onCancel, defaultTypeIsRollup, - requireTimestampField = false, editData, allowAdHoc, showManagementLink, + dataViewEditorService, }: Props) => { const { services: { application, dataViews, uiSettings, overlays }, } = useKibana(); - const { dataViewEditorService } = useContext(DataViewEditorServiceContext); - const canSave = dataViews.getCanSaveSync(); const { form } = useForm({ @@ -132,15 +118,12 @@ const IndexPatternEditorFlyoutContentComponent = ({ return; } - const rollupIndicesCapabilities = dataViewEditorService.rollupIndicesCapabilities$.getValue(); - const indexPatternStub: DataViewSpec = { title: removeSpaces(formData.title), timeFieldName: formData.timestampField?.value, id: formData.id, name: formData.name, }; - const rollupIndex = rollupIndex$.current.getValue(); if (type === INDEX_PATTERN_TYPE.ROLLUP && rollupIndex) { indexPatternStub.type = INDEX_PATTERN_TYPE.ROLLUP; @@ -175,110 +158,29 @@ const IndexPatternEditorFlyoutContentComponent = ({ allowHidden = schema.allowHidden.defaultValue, type = schema.type.defaultValue, }, - ] = useFormData({ form }); - - const currentLoadingMatchedIndicesRef = useRef(0); + ] = useFormData({ + form, + }); const isLoadingSources = useObservable(dataViewEditorService.isLoadingSources$, true); + const existingDataViewNames = useObservable(dataViewEditorService.dataViewNames$); + const rollupIndex = useObservable(dataViewEditorService.rollupIndex$); + const rollupIndicesCapabilities = useObservable(dataViewEditorService.rollupIndicesCaps$, {}); - const loadingMatchedIndices$ = useRef(new BehaviorSubject(false)); - - const isLoadingDataViewNames$ = useRef(new BehaviorSubject(true)); - const existingDataViewNames$ = useRef(new BehaviorSubject([])); - const isLoadingDataViewNames = useObservable(isLoadingDataViewNames$.current, true); - - const rollupIndicesCapabilities = useObservable( - dataViewEditorService.rollupIndicesCapabilities$, - {} - ); - - const rollupIndex$ = useRef(new BehaviorSubject(undefined)); - - // initial loading of indicies and data view names useEffect(() => { - let isCancelled = false; - const matchedIndicesSub = dataViewEditorService.matchedIndices$.subscribe((matchedIndices) => { - const timeFieldQuery = editData ? editData.title : title; - dataViewEditorService.loadTimestampFields( - removeSpaces(timeFieldQuery), - type, - requireTimestampField, - rollupIndex$.current.getValue() - ); - }); - - dataViewEditorService.loadIndices(title, allowHidden).then((matchedIndices) => { - if (isCancelled) return; - dataViewEditorService.matchedIndices$.next(matchedIndices); - }); + dataViewEditorService.setIndexPattern(title); + }, [dataViewEditorService, title]); - dataViewEditorService.loadDataViewNames(title).then((names) => { - if (isCancelled) return; - const filteredNames = editData ? names.filter((name) => name !== editData?.name) : names; - existingDataViewNames$.current.next(filteredNames); - isLoadingDataViewNames$.current.next(false); - }); + useEffect(() => { + dataViewEditorService.setAllowHidden(allowHidden); + }, [dataViewEditorService, allowHidden]); - return () => { - isCancelled = true; - matchedIndicesSub.unsubscribe(); - }; - }, [editData, type, title, allowHidden, requireTimestampField, dataViewEditorService]); + useEffect(() => { + dataViewEditorService.setType(type); + }, [dataViewEditorService, type]); const getRollupIndices = (rollupCaps: RollupIndicesCapsResponse) => Object.keys(rollupCaps); - // used in title field validation - const reloadMatchedIndices = useCallback( - async (newTitle: string) => { - let newRollupIndexName: string | undefined; - - const fetchIndices = async (query: string = '') => { - const currentLoadingMatchedIndicesIdx = ++currentLoadingMatchedIndicesRef.current; - - loadingMatchedIndices$.current.next(true); - - const allSrcs = await dataViewEditorService.getIndicesCached({ - pattern: '*', - showAllIndices: allowHidden, - }); - - const { matchedIndicesResult, exactMatched } = !isLoadingSources - ? await loadMatchedIndices(query, allowHidden, allSrcs, dataViewEditorService) - : { - matchedIndicesResult: matchedIndiciesDefault, - exactMatched: [], - }; - - if (currentLoadingMatchedIndicesIdx === currentLoadingMatchedIndicesRef.current) { - // we are still interested in this result - if (type === INDEX_PATTERN_TYPE.ROLLUP) { - const isRollupIndex = await dataViewEditorService.getIsRollupIndex(); - const rollupIndices = exactMatched.filter((index) => isRollupIndex(index.name)); - newRollupIndexName = rollupIndices.length === 1 ? rollupIndices[0].name : undefined; - rollupIndex$.current.next(newRollupIndexName); - } else { - rollupIndex$.current.next(undefined); - } - - dataViewEditorService.matchedIndices$.next(matchedIndicesResult); - loadingMatchedIndices$.current.next(false); - } - - return { matchedIndicesResult, newRollupIndexName }; - }; - - return fetchIndices(newTitle); - }, - [ - allowHidden, - type, - dataViewEditorService, - rollupIndex$, - isLoadingSources, - loadingMatchedIndices$, - ] - ); - const onTypeChange = useCallback( (newType) => { form.setFieldValue('title', ''); @@ -291,7 +193,7 @@ const IndexPatternEditorFlyoutContentComponent = ({ [form] ); - if (isLoadingSources || isLoadingDataViewNames) { + if (isLoadingSources || !existingDataViewNames) { return ; } @@ -343,14 +245,14 @@ const IndexPatternEditorFlyoutContentComponent = ({ form={form} className="indexPatternEditor__form" error={form.getErrors()} - isInvalid={form.isSubmitted && !form.isValid} + isInvalid={form.isSubmitted && !form.isValid && form.getErrors().length} > {indexPatternTypeSelect} - + @@ -358,9 +260,11 @@ const IndexPatternEditorFlyoutContentComponent = ({ @@ -370,7 +274,6 @@ const IndexPatternEditorFlyoutContentComponent = ({ @@ -415,59 +318,3 @@ const IndexPatternEditorFlyoutContentComponent = ({ }; export const IndexPatternEditorFlyoutContent = React.memo(IndexPatternEditorFlyoutContentComponent); - -// loadMatchedIndices is called both as an side effect inside of a parent component and the inside forms validation functions -// that are challenging to synchronize without a larger refactor -// Use memoizeOne as a caching layer to avoid excessive network requests on each key type -// TODO: refactor to remove `memoize` when https://github.com/elastic/kibana/pull/109238 is done -const loadMatchedIndices = memoizeOne( - async ( - query: string, - allowHidden: boolean, - allSources: MatchedItem[], - dataViewEditorService: DataViewEditorService - ): Promise<{ - matchedIndicesResult: MatchedIndicesSet; - exactMatched: MatchedItem[]; - partialMatched: MatchedItem[]; - }> => { - const indexRequests = []; - - if (query?.endsWith('*')) { - const exactMatchedQuery = dataViewEditorService.getIndicesCached({ - pattern: query, - showAllIndices: allowHidden, - }); - indexRequests.push(exactMatchedQuery); - // provide default value when not making a request for the partialMatchQuery - indexRequests.push(Promise.resolve([])); - } else { - const exactMatchQuery = dataViewEditorService.getIndicesCached({ - pattern: query, - showAllIndices: allowHidden, - }); - const partialMatchQuery = dataViewEditorService.getIndicesCached({ - pattern: `${query}*`, - showAllIndices: allowHidden, - }); - - indexRequests.push(exactMatchQuery); - indexRequests.push(partialMatchQuery); - } - - const [exactMatched, partialMatched] = (await ensureMinimumTime( - indexRequests - )) as MatchedItem[][]; - - const matchedIndicesResult = getMatchedIndices( - allSources, - partialMatched, - exactMatched, - allowHidden - ); - - return { matchedIndicesResult, exactMatched, partialMatched }; - }, - // compare only query and allowHidden - (newArgs, oldArgs) => newArgs[0] === oldArgs[0] && newArgs[1] === oldArgs[1] -); diff --git a/src/plugins/data_view_editor/public/components/data_view_flyout_content_container.tsx b/src/plugins/data_view_editor/public/components/data_view_flyout_content_container.tsx index 16e58d52624c3..52b311a98f30f 100644 --- a/src/plugins/data_view_editor/public/components/data_view_flyout_content_container.tsx +++ b/src/plugins/data_view_editor/public/components/data_view_flyout_content_container.tsx @@ -6,19 +6,15 @@ * Side Public License, v 1. */ -import React from 'react'; +import React, { useEffect, useState } from 'react'; import { i18n } from '@kbn/i18n'; +import { INDEX_PATTERN_TYPE } from '@kbn/data-views-plugin/public'; import { DataViewSpec, useKibana } from '../shared_imports'; import { IndexPatternEditorFlyoutContent } from './data_view_editor_flyout_content'; import { DataViewEditorContext, DataViewEditorProps } from '../types'; import { DataViewEditorService } from '../data_view_editor_service'; -// @ts-ignore -export const DataViewEditorServiceContext = React.createContext<{ - dataViewEditorService: DataViewEditorService; -}>(); - const DataViewFlyoutContentContainer = ({ onSave, onCancel = () => {}, @@ -32,6 +28,24 @@ const DataViewFlyoutContentContainer = ({ services: { dataViews, notifications, http }, } = useKibana(); + const [dataViewEditorService] = useState( + () => + new DataViewEditorService({ + services: { http, dataViews }, + initialValues: { + name: editData?.name, + type: editData?.type as INDEX_PATTERN_TYPE, + indexPattern: editData?.getIndexPattern(), + }, + requireTimestampField, + }) + ); + + useEffect(() => { + const service = dataViewEditorService; + return service.destroy; + }, [dataViewEditorService]); + const onSaveClick = async (dataViewSpec: DataViewSpec, persist: boolean = true) => { try { let saveResponse; @@ -69,19 +83,15 @@ const DataViewFlyoutContentContainer = ({ }; return ( - - - + ); }; diff --git a/src/plugins/data_view_editor/public/components/form_fields/name_field.tsx b/src/plugins/data_view_editor/public/components/form_fields/name_field.tsx index 425178d452c5a..750b54f689361 100644 --- a/src/plugins/data_view_editor/public/components/form_fields/name_field.tsx +++ b/src/plugins/data_view_editor/public/components/form_fields/name_field.tsx @@ -9,8 +9,6 @@ import React, { ChangeEvent, useMemo } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiFormRow, EuiFieldText } from '@elastic/eui'; -import { BehaviorSubject } from 'rxjs'; -import useObservable from 'react-use/lib/useObservable'; import { UseField, ValidationConfig, @@ -21,7 +19,7 @@ import { IndexPatternConfig } from '../../types'; import { schema } from '../form_schema'; interface NameFieldProps { - existingDataViewNames$: BehaviorSubject; + namesNotAllowed: string[]; } interface GetNameConfigArgs { @@ -53,8 +51,7 @@ const getNameConfig = ({ namesNotAllowed }: GetNameConfigArgs): FieldConfig { - const namesNotAllowed = useObservable(existingDataViewNames$, []); +export const NameField = ({ namesNotAllowed }: NameFieldProps) => { const config = useMemo( () => getNameConfig({ diff --git a/src/plugins/data_view_editor/public/components/form_fields/timestamp_field.tsx b/src/plugins/data_view_editor/public/components/form_fields/timestamp_field.tsx index 846a9db09ee80..310cb9a3e5835 100644 --- a/src/plugins/data_view_editor/public/components/form_fields/timestamp_field.tsx +++ b/src/plugins/data_view_editor/public/components/form_fields/timestamp_field.tsx @@ -9,9 +9,9 @@ import React, { useMemo } from 'react'; import { i18n } from '@kbn/i18n'; import useObservable from 'react-use/lib/useObservable'; -import { BehaviorSubject, Subject } from 'rxjs'; +import { Observable } from 'rxjs'; import { EuiFormRow, EuiComboBox, EuiFormHelpText, EuiComboBoxOptionOption } from '@elastic/eui'; -import { matchedIndiciesDefault } from '../data_view_editor_flyout_content'; +import { matchedIndiciesDefault } from '../../data_view_editor_service'; import { UseField, @@ -24,10 +24,9 @@ import { TimestampOption, MatchedIndicesSet } from '../../types'; import { schema } from '../form_schema'; interface Props { - options$: Subject; - isLoadingOptions$: BehaviorSubject; - isLoadingMatchedIndices$: BehaviorSubject; - matchedIndices$: Subject; + options$: Observable; + isLoadingOptions$: Observable; + matchedIndices$: Observable; } const requireTimestampOptionValidator = (options: TimestampOption[]): ValidationConfig => ({ @@ -71,15 +70,9 @@ const timestampFieldHelp = i18n.translate('indexPatternEditor.editor.form.timeFi defaultMessage: 'Select a timestamp field for use with the global time filter.', }); -export const TimestampField = ({ - options$, - isLoadingOptions$, - isLoadingMatchedIndices$, - matchedIndices$, -}: Props) => { +export const TimestampField = ({ options$, isLoadingOptions$, matchedIndices$ }: Props) => { const options = useObservable(options$, []); const isLoadingOptions = useObservable(isLoadingOptions$, false); - const isLoadingMatchedIndices = useObservable(isLoadingMatchedIndices$, false); const hasMatchedIndices = !!useObservable(matchedIndices$, matchedIndiciesDefault) .exactMatchedIndices.length; @@ -92,9 +85,7 @@ export const TimestampField = ({ const selectTimestampHelp = options.length ? timestampFieldHelp : ''; const timestampNoFieldsHelp = - options.length === 0 && !isLoadingMatchedIndices && !isLoadingOptions && hasMatchedIndices - ? noTimestampOptionText - : ''; + options.length === 0 && !isLoadingOptions && hasMatchedIndices ? noTimestampOptionText : ''; return ( > config={timestampConfig} path="timestampField"> @@ -106,7 +97,7 @@ export const TimestampField = ({ } const { isInvalid, errorMessage } = getFieldValidityAndErrorMessage(field); - const isDisabled = !optionsAsComboBoxOptions.length; + const isDisabled = !optionsAsComboBoxOptions.length || isLoadingOptions; // if the value isn't in the list then don't use it. const valueInList = !!optionsAsComboBoxOptions.find( (option) => option.value === value.value diff --git a/src/plugins/data_view_editor/public/components/form_fields/title_field.tsx b/src/plugins/data_view_editor/public/components/form_fields/title_field.tsx index 6f443871dd66e..b2ea9c78e9fca 100644 --- a/src/plugins/data_view_editor/public/components/form_fields/title_field.tsx +++ b/src/plugins/data_view_editor/public/components/form_fields/title_field.tsx @@ -9,7 +9,7 @@ import React, { ChangeEvent, useState, useMemo } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiFormRow, EuiFieldText } from '@elastic/eui'; -import { Subject } from 'rxjs'; +import { Observable } from 'rxjs'; import useObservable from 'react-use/lib/useObservable'; import { MatchedItem } from '@kbn/data-views-plugin/public'; import { @@ -18,21 +18,19 @@ import { ValidationConfig, FieldConfig, } from '../../shared_imports'; -import { canAppendWildcard, removeSpaces } from '../../lib'; +import { canAppendWildcard } from '../../lib'; import { schema } from '../form_schema'; import { RollupIndicesCapsResponse, IndexPatternConfig, MatchedIndicesSet } from '../../types'; -import { matchedIndiciesDefault } from '../data_view_editor_flyout_content'; - -interface RefreshMatchedIndicesResult { - matchedIndicesResult: MatchedIndicesSet; - newRollupIndexName?: string; -} +import { matchedIndiciesDefault } from '../../data_view_editor_service'; interface TitleFieldProps { isRollup: boolean; - matchedIndices$: Subject; + matchedIndices$: Observable; rollupIndicesCapabilities: RollupIndicesCapsResponse; - refreshMatchedIndices: (title: string) => Promise; + indexPatternValidationProvider: () => Promise<{ + matchedIndices: MatchedIndicesSet; + rollupIndex: string | null | undefined; + }>; } const rollupIndexPatternNoMatchError = { @@ -55,22 +53,23 @@ const mustMatchError = { interface MatchesValidatorArgs { rollupIndicesCapabilities: Record; - refreshMatchedIndices: (title: string) => Promise; isRollup: boolean; } const createMatchesIndicesValidator = ({ rollupIndicesCapabilities, - refreshMatchedIndices, isRollup, }: MatchesValidatorArgs): ValidationConfig<{}, string, string> => ({ - validator: async ({ value }) => { - const { matchedIndicesResult, newRollupIndexName } = await refreshMatchedIndices( - removeSpaces(value) - ); + validator: async ({ customData: { provider } }) => { + const { matchedIndices, rollupIndex } = (await provider()) as { + matchedIndices: MatchedIndicesSet; + rollupIndex?: string; + }; + + // verifies that the title matches at least one index, alias, or data stream const rollupIndices = Object.keys(rollupIndicesCapabilities); - if (matchedIndicesResult.exactMatchedIndices.length === 0) { + if (matchedIndices.exactMatchedIndices.length === 0) { return mustMatchError; } @@ -79,7 +78,7 @@ const createMatchesIndicesValidator = ({ } // A rollup index pattern needs to match one and only one rollup index. - const rollupIndexMatches = matchedIndicesResult.exactMatchedIndices.filter((matchedIndex) => + const rollupIndexMatches = matchedIndices.exactMatchedIndices.filter((matchedIndex) => rollupIndices.includes(matchedIndex.name) ); @@ -90,7 +89,7 @@ const createMatchesIndicesValidator = ({ } // Error info is potentially provided via the rollup indices caps request - const error = newRollupIndexName && rollupIndicesCapabilities[newRollupIndexName].error; + const error = rollupIndex && rollupIndicesCapabilities[rollupIndex].error; if (error) { return { @@ -109,13 +108,11 @@ interface GetTitleConfigArgs { isRollup: boolean; matchedIndices: MatchedItem[]; rollupIndicesCapabilities: RollupIndicesCapsResponse; - refreshMatchedIndices: (title: string) => Promise; } const getTitleConfig = ({ isRollup, rollupIndicesCapabilities, - refreshMatchedIndices, }: GetTitleConfigArgs): FieldConfig => { const titleFieldConfig = schema.title; @@ -124,7 +121,6 @@ const getTitleConfig = ({ // note this is responsible for triggering the state update for the selected source list. createMatchesIndicesValidator({ rollupIndicesCapabilities, - refreshMatchedIndices, isRollup, }), ]; @@ -139,7 +135,7 @@ export const TitleField = ({ isRollup, matchedIndices$, rollupIndicesCapabilities, - refreshMatchedIndices, + indexPatternValidationProvider, }: TitleFieldProps) => { const [appendedWildcard, setAppendedWildcard] = useState(false); const matchedIndices = useObservable(matchedIndices$, matchedIndiciesDefault).exactMatchedIndices; @@ -150,15 +146,15 @@ export const TitleField = ({ isRollup, matchedIndices, rollupIndicesCapabilities, - refreshMatchedIndices, }), - [isRollup, matchedIndices, rollupIndicesCapabilities, refreshMatchedIndices] + [isRollup, matchedIndices, rollupIndicesCapabilities] ); return ( path="title" config={fieldConfig} + validationDataProvider={indexPatternValidationProvider} componentProps={{ euiFieldProps: { 'aria-label': i18n.translate('indexPatternEditor.form.titleAriaLabel', { diff --git a/src/plugins/data_view_editor/public/components/form_schema.ts b/src/plugins/data_view_editor/public/components/form_schema.ts index 59e195a1f1280..69993f17ecb35 100644 --- a/src/plugins/data_view_editor/public/components/form_schema.ts +++ b/src/plugins/data_view_editor/public/components/form_schema.ts @@ -36,7 +36,7 @@ export const schema = { { validator: fieldValidators.emptyField( i18n.translate('indexPatternEditor.validations.titleIsRequiredErrorMessage', { - defaultMessage: 'An Index pattern is required.', + defaultMessage: 'An index pattern is required.', }) ), }, diff --git a/src/plugins/data_view_editor/public/components/preview_panel/preview_panel.tsx b/src/plugins/data_view_editor/public/components/preview_panel/preview_panel.tsx index 28163384ca0f8..07b1fd91b85b6 100644 --- a/src/plugins/data_view_editor/public/components/preview_panel/preview_panel.tsx +++ b/src/plugins/data_view_editor/public/components/preview_panel/preview_panel.tsx @@ -9,11 +9,11 @@ import React from 'react'; import { EuiSpacer } from '@elastic/eui'; import useObservable from 'react-use/lib/useObservable'; -import { Subject } from 'rxjs'; +import { Observable } from 'rxjs'; import { INDEX_PATTERN_TYPE } from '@kbn/data-views-plugin/public'; import { StatusMessage } from './status_message'; import { IndicesList } from './indices_list'; -import { matchedIndiciesDefault } from '../data_view_editor_flyout_content'; +import { matchedIndiciesDefault } from '../../data_view_editor_service'; import { MatchedIndicesSet } from '../../types'; @@ -21,7 +21,7 @@ interface Props { type: INDEX_PATTERN_TYPE; allowHidden: boolean; title: string; - matchedIndices$: Subject; + matchedIndices$: Observable; } export const PreviewPanel = ({ type, allowHidden, title = '', matchedIndices$ }: Props) => { diff --git a/src/plugins/data_view_editor/public/data_view_editor_service.ts b/src/plugins/data_view_editor/public/data_view_editor_service.ts index 57963830149db..4bfd04c912a6c 100644 --- a/src/plugins/data_view_editor/public/data_view_editor_service.ts +++ b/src/plugins/data_view_editor/public/data_view_editor_service.ts @@ -7,7 +7,17 @@ */ import { HttpSetup } from '@kbn/core/public'; -import { BehaviorSubject, Subject } from 'rxjs'; +import { + BehaviorSubject, + Subject, + first, + firstValueFrom, + from, + Observable, + Subscription, + map, + distinctUntilChanged, +} from 'rxjs'; import { DataViewsServicePublic, @@ -17,65 +27,184 @@ import { } from '@kbn/data-views-plugin/public'; import { RollupIndicesCapsResponse, MatchedIndicesSet, TimestampOption } from './types'; -import { getMatchedIndices, ensureMinimumTime, extractTimeFields } from './lib'; +import { getMatchedIndices, ensureMinimumTime, extractTimeFields, removeSpaces } from './lib'; import { GetFieldsOptions } from './shared_imports'; +export const matchedIndiciesDefault = { + allIndices: [], + exactMatchedIndices: [], + partialMatchedIndices: [], + visibleIndices: [], +}; + +export interface DataViewEditorServiceConstructorArgs { + services: { + http: HttpSetup; + dataViews: DataViewsServicePublic; + }; + requireTimestampField?: boolean; + initialValues: { + name?: string; + type?: INDEX_PATTERN_TYPE; + indexPattern?: string; + }; +} + +interface DataViewEditorState { + matchedIndices: MatchedIndicesSet; + rollupIndicesCaps: RollupIndicesCapsResponse; + isLoadingSourcesInternal: boolean; + loadingTimestampFields: boolean; + timestampFieldOptions: TimestampOption[]; + rollupIndexName?: string | null; +} + +const defaultDataViewEditorState: DataViewEditorState = { + matchedIndices: { ...matchedIndiciesDefault }, + rollupIndicesCaps: {}, + isLoadingSourcesInternal: false, + loadingTimestampFields: false, + timestampFieldOptions: [], + rollupIndexName: undefined, +}; + +export const stateSelectorFactory = + (state$: Observable) => + (selector: (state: S) => R, equalityFn?: (arg0: R, arg1: R) => boolean) => + state$.pipe(map(selector), distinctUntilChanged(equalityFn)); + export class DataViewEditorService { - constructor(private http: HttpSetup, private dataViews: DataViewsServicePublic) { + constructor({ + services: { http, dataViews }, + initialValues: { + type: initialType = INDEX_PATTERN_TYPE.DEFAULT, + indexPattern: initialIndexPattern = '', + name: initialName = '', + }, + requireTimestampField = false, + }: DataViewEditorServiceConstructorArgs) { + this.http = http; + this.dataViews = dataViews; + this.requireTimestampField = requireTimestampField; + this.type = initialType; + this.indexPattern = removeSpaces(initialIndexPattern); + + // fire off a couple of requests that we know we'll need this.rollupCapsResponse = this.getRollupIndexCaps(); + this.dataViewNames$ = from(this.loadDataViewNames(initialName)); + + this.state$ = new BehaviorSubject({ + ...defaultDataViewEditorState, + }); + + const stateSelector = stateSelectorFactory(this.state$); + + // public observables + this.matchedIndices$ = stateSelector((state) => state.matchedIndices); + this.rollupIndicesCaps$ = stateSelector((state) => state.rollupIndicesCaps); + this.isLoadingSources$ = stateSelector((state) => state.isLoadingSourcesInternal); + this.loadingTimestampFields$ = stateSelector((state) => state.loadingTimestampFields); + this.timestampFieldOptions$ = stateSelector((state) => state.timestampFieldOptions); + this.rollupIndex$ = stateSelector((state) => state.rollupIndexName); + + // when list of matched indices is updated always update timestamp fields + this.loadTimestampFieldsSub = this.matchedIndices$.subscribe(() => this.loadTimestampFields()); + + // alternate value with undefined so validation knows when its getting a fresh value + this.matchedIndicesForProviderSub = this.matchedIndices$.subscribe((matchedIndices) => { + this.matchedIndicesForProvider$.next(matchedIndices); + this.matchedIndicesForProvider$.next(undefined); + }); + + // alternate value with undefined so validation knows when its getting a fresh value + this.rollupIndexForProviderSub = this.rollupIndex$.subscribe((rollupIndex) => { + this.rollupIndexForProvider$.next(rollupIndex); + this.rollupIndexForProvider$.next(undefined); + }); } - rollupIndicesCapabilities$ = new BehaviorSubject({}); - isLoadingSources$ = new BehaviorSubject(false); + private http: HttpSetup; + private dataViews: DataViewsServicePublic; + // config + private requireTimestampField: boolean; + private type = INDEX_PATTERN_TYPE.DEFAULT; + + // state + private state = { ...defaultDataViewEditorState }; + private indexPattern = ''; + private allowHidden = false; + + // used for data view name validation - no dupes! + dataViewNames$: Observable; + + private loadTimestampFieldsSub: Subscription; + private matchedIndicesForProviderSub: Subscription; + private rollupIndexForProviderSub: Subscription; + + private state$: BehaviorSubject; - loadingTimestampFields$ = new BehaviorSubject(false); - timestampFieldOptions$ = new Subject(); + // used for validating rollup data views - must match one and only one data view + rollupIndicesCaps$: Observable; + isLoadingSources$: Observable; + loadingTimestampFields$: Observable; + timestampFieldOptions$: Observable; - matchedIndices$ = new BehaviorSubject({ - allIndices: [], - exactMatchedIndices: [], - partialMatchedIndices: [], - visibleIndices: [], - }); + // current matched rollup index + rollupIndex$: Observable; + // alernates between value and undefined so validation can treat new value as thought its a promise + private rollupIndexForProvider$ = new Subject(); + + matchedIndices$: Observable; + + // alernates between value and undefined so validation can treat new value as thought its a promise + private matchedIndicesForProvider$ = new Subject(); private rollupCapsResponse: Promise; private currentLoadingTimestampFields = 0; + private currentLoadingMatchedIndices = 0; + + private updateState = (newState: Partial) => { + this.state = { ...this.state, ...newState }; + this.state$.next(this.state); + }; private getRollupIndexCaps = async () => { - let response: RollupIndicesCapsResponse = {}; + let rollupIndicesCaps: RollupIndicesCapsResponse = {}; try { - response = await this.http.get('/api/rollup/indices'); + rollupIndicesCaps = await this.http.get('/api/rollup/indices'); } catch (e) { // Silently swallow failure responses such as expired trials } - this.rollupIndicesCapabilities$.next(response); - return response; + this.updateState({ rollupIndicesCaps }); + return rollupIndicesCaps; }; - private getRollupIndices = (rollupCaps: RollupIndicesCapsResponse) => Object.keys(rollupCaps); - - getIsRollupIndex = async () => { + private getIsRollupIndex = async () => { const response = await this.rollupCapsResponse; - return (indexName: string) => this.getRollupIndices(response).includes(indexName); + const indices = Object.keys(response); + return (indexName: string) => indices.includes(indexName); }; - loadMatchedIndices = async ( + private loadMatchedIndices = async ( query: string, allowHidden: boolean, - allSources: MatchedItem[] - ): Promise<{ - matchedIndicesResult: MatchedIndicesSet; - exactMatched: MatchedItem[]; - partialMatched: MatchedItem[]; - }> => { + allSources: MatchedItem[], + type: INDEX_PATTERN_TYPE + ): Promise => { + const currentLoadingMatchedIndicesIdx = ++this.currentLoadingMatchedIndices; + const isRollupIndex = await this.getIsRollupIndex(); const indexRequests = []; + let newRollupIndexName: string | undefined | null; + + this.updateState({ loadingTimestampFields: true }); if (query?.endsWith('*')) { const exactMatchedQuery = this.getIndicesCached({ pattern: query, showAllIndices: allowHidden, }); + indexRequests.push(exactMatchedQuery); // provide default value when not making a request for the partialMatchQuery indexRequests.push(Promise.resolve([])); @@ -97,60 +226,75 @@ export class DataViewEditorService { indexRequests )) as MatchedItem[][]; - const matchedIndicesResult = getMatchedIndices( - allSources, - partialMatched, - exactMatched, - allowHidden - ); + const matchedIndices = getMatchedIndices(allSources, partialMatched, exactMatched, allowHidden); + + // verify we're looking at the current result + if (currentLoadingMatchedIndicesIdx === this.currentLoadingMatchedIndices) { + if (type === INDEX_PATTERN_TYPE.ROLLUP) { + const rollupIndices = exactMatched.filter((index) => isRollupIndex(index.name)); + newRollupIndexName = rollupIndices.length === 1 ? rollupIndices[0].name : null; + this.updateState({ rollupIndexName: newRollupIndexName }); + } else { + this.updateState({ rollupIndexName: null }); + } + + this.updateState({ matchedIndices }); + } + }; + + setIndexPattern = (indexPattern: string) => { + this.indexPattern = removeSpaces(indexPattern); + this.loadIndices(); + }; + + setAllowHidden = (allowHidden: boolean) => { + this.allowHidden = allowHidden; + this.loadIndices(); + }; - this.matchedIndices$.next(matchedIndicesResult); - return { matchedIndicesResult, exactMatched, partialMatched }; + setType = (type: INDEX_PATTERN_TYPE) => { + this.type = type; + this.loadIndices(); }; - loadIndices = async (title: string, allowHidden: boolean) => { + private loadIndices = async () => { const allSrcs = await this.getIndicesCached({ pattern: '*', - showAllIndices: allowHidden, + showAllIndices: this.allowHidden, }); + await this.loadMatchedIndices(this.indexPattern, this.allowHidden, allSrcs, this.type); - const matchedSet = await this.loadMatchedIndices(title, allowHidden, allSrcs); - - this.isLoadingSources$.next(false); - const matchedIndices = getMatchedIndices( - allSrcs, - matchedSet.partialMatched, - matchedSet.exactMatched, - allowHidden - ); - - this.matchedIndices$.next(matchedIndices); - return matchedIndices; + this.updateState({ isLoadingSourcesInternal: false }); }; - loadDataViewNames = async (dataViewName?: string) => { - const dataViewListItems = await this.dataViews.getIdsWithTitle(dataViewName ? true : false); + private loadDataViewNames = async (initialName?: string) => { + const dataViewListItems = await this.dataViews.getIdsWithTitle(true); const dataViewNames = dataViewListItems.map((item) => item.name || item.title); - return dataViewName ? dataViewNames.filter((v) => v !== dataViewName) : dataViewNames; + return initialName ? dataViewNames.filter((v) => v !== initialName) : dataViewNames; }; private getIndicesMemory: Record> = {}; - getIndicesCached = async (props: { pattern: string; showAllIndices?: boolean | undefined }) => { + + private getIndicesCached = async (props: { + pattern: string; + showAllIndices?: boolean | undefined; + }) => { const key = JSON.stringify(props); - const getIndicesPromise = this.getIsRollupIndex().then((isRollupIndex) => - this.dataViews.getIndices({ ...props, isRollupIndex }) - ); - this.getIndicesMemory[key] = this.getIndicesMemory[key] || getIndicesPromise; + this.getIndicesMemory[key] = + this.getIndicesMemory[key] || + this.getIsRollupIndex().then((isRollupIndex) => + this.dataViews.getIndices({ ...props, isRollupIndex }) + ); - getIndicesPromise.catch(() => { + this.getIndicesMemory[key].catch(() => { delete this.getIndicesMemory[key]; }); - return await getIndicesPromise; + return await this.getIndicesMemory[key]; }; - private timeStampOptionsMemory: Record> = {}; + private timestampOptionsMemory: Record> = {}; private getTimestampOptionsForWildcard = async ( getFieldsOptions: GetFieldsOptions, requireTimestampField: boolean @@ -169,47 +313,70 @@ export class DataViewEditorService { getFieldsOptions, requireTimestampField ); - this.timeStampOptionsMemory[key] = - this.timeStampOptionsMemory[key] || getTimestampOptionsPromise; + this.timestampOptionsMemory[key] = + this.timestampOptionsMemory[key] || getTimestampOptionsPromise; getTimestampOptionsPromise.catch(() => { - delete this.timeStampOptionsMemory[key]; + delete this.timestampOptionsMemory[key]; }); return await getTimestampOptionsPromise; }; - loadTimestampFields = async ( - index: string, - type: INDEX_PATTERN_TYPE, - requireTimestampField: boolean, - rollupIndex?: string - ) => { - if (this.matchedIndices$.getValue().exactMatchedIndices.length === 0) { - this.timestampFieldOptions$.next([]); + private loadTimestampFields = async () => { + if (this.state.matchedIndices.exactMatchedIndices.length === 0) { + this.updateState({ timestampFieldOptions: [], loadingTimestampFields: false }); return; } const currentLoadingTimestampFieldsIdx = ++this.currentLoadingTimestampFields; - this.loadingTimestampFields$.next(true); + const getFieldsOptions: GetFieldsOptions = { - pattern: index, + pattern: this.indexPattern, }; - if (type === INDEX_PATTERN_TYPE.ROLLUP) { + if (this.type === INDEX_PATTERN_TYPE.ROLLUP) { getFieldsOptions.type = INDEX_PATTERN_TYPE.ROLLUP; - getFieldsOptions.rollupIndex = rollupIndex; + getFieldsOptions.rollupIndex = this.state.rollupIndexName || ''; } - let timestampOptions: TimestampOption[] = []; + let timestampFieldOptions: TimestampOption[] = []; try { - timestampOptions = await this.getTimestampOptionsForWildcardCached( + timestampFieldOptions = await this.getTimestampOptionsForWildcardCached( getFieldsOptions, - requireTimestampField + this.requireTimestampField ); } finally { if (currentLoadingTimestampFieldsIdx === this.currentLoadingTimestampFields) { - this.timestampFieldOptions$.next(timestampOptions); - this.loadingTimestampFields$.next(false); + this.updateState({ timestampFieldOptions, loadingTimestampFields: false }); } } }; + + // provides info necessary for validation of index pattern in required async format + indexPatternValidationProvider = async () => { + const rollupIndexPromise = firstValueFrom( + this.rollupIndex$.pipe(first((data) => data !== undefined)) + ); + + const matchedIndicesPromise = firstValueFrom( + this.matchedIndicesForProvider$.pipe(first((data) => data !== undefined)) + ); + + // necessary to get new observable value if the field hasn't changed + this.loadIndices(); + + // Wait until we have fetched the indices. + // The result will then be sent to the field validator(s) (when calling await provider();); + const [rollupIndex, matchedIndices] = await Promise.all([ + rollupIndexPromise, + matchedIndicesPromise, + ]); + + return { rollupIndex, matchedIndices: matchedIndices || matchedIndiciesDefault }; + }; + + destroy = () => { + this.loadTimestampFieldsSub.unsubscribe(); + this.matchedIndicesForProviderSub.unsubscribe(); + this.rollupIndexForProviderSub.unsubscribe(); + }; } diff --git a/src/plugins/data_view_editor/public/shared_imports.ts b/src/plugins/data_view_editor/public/shared_imports.ts index 9f805feedeca1..57fb7fc6f7031 100644 --- a/src/plugins/data_view_editor/public/shared_imports.ts +++ b/src/plugins/data_view_editor/public/shared_imports.ts @@ -35,6 +35,7 @@ export { Form, UseField, getFieldValidityAndErrorMessage, + useBehaviorSubject, } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib'; export { fieldValidators } from '@kbn/es-ui-shared-plugin/static/forms/helpers'; diff --git a/src/plugins/data_views/common/types.ts b/src/plugins/data_views/common/types.ts index 54c8c61635f63..44158e5e560c2 100644 --- a/src/plugins/data_views/common/types.ts +++ b/src/plugins/data_views/common/types.ts @@ -314,6 +314,7 @@ export interface GetFieldsOptions { rollupIndex?: string; allowNoIndex?: boolean; filter?: QueryDslQueryContainer; + includeUnmapped?: boolean; } /** diff --git a/src/plugins/data_views/public/data_views/data_views_api_client.ts b/src/plugins/data_views/public/data_views/data_views_api_client.ts index a45b9f29e595f..66f1c05b224fd 100644 --- a/src/plugins/data_views/public/data_views/data_views_api_client.ts +++ b/src/plugins/data_views/public/data_views/data_views_api_client.ts @@ -49,7 +49,8 @@ export class DataViewsApiClient implements IDataViewsApiClient { * @param options options for fields request */ getFieldsForWildcard(options: GetFieldsOptions) { - const { pattern, metaFields, type, rollupIndex, allowNoIndex, filter } = options; + const { pattern, metaFields, type, rollupIndex, allowNoIndex, filter, includeUnmapped } = + options; return this._request( this._getUrl(['_fields_for_wildcard']), { @@ -58,6 +59,7 @@ export class DataViewsApiClient implements IDataViewsApiClient { type, rollup_index: rollupIndex, allow_no_index: allowNoIndex, + include_unmapped: includeUnmapped, }, filter ? JSON.stringify({ index_filter: filter }) : undefined ).then((response) => { diff --git a/src/plugins/data_views/server/fetcher/index_patterns_fetcher.ts b/src/plugins/data_views/server/fetcher/index_patterns_fetcher.ts index 3049c2d1a3dec..a24783123a74b 100644 --- a/src/plugins/data_views/server/fetcher/index_patterns_fetcher.ts +++ b/src/plugins/data_views/server/fetcher/index_patterns_fetcher.ts @@ -57,7 +57,7 @@ export class IndexPatternsFetcher { async getFieldsForWildcard(options: { pattern: string | string[]; metaFields?: string[]; - fieldCapsOptions?: { allow_no_indices: boolean }; + fieldCapsOptions?: { allow_no_indices: boolean; includeUnmapped?: boolean }; type?: string; rollupIndex?: string; filter?: QueryDslQueryContainer; @@ -78,6 +78,7 @@ export class IndexPatternsFetcher { metaFields, fieldCapsOptions: { allow_no_indices: allowNoIndices, + include_unmapped: fieldCapsOptions?.includeUnmapped, }, filter, }); diff --git a/src/plugins/data_views/server/fetcher/lib/es_api.ts b/src/plugins/data_views/server/fetcher/lib/es_api.ts index 6e611994ee394..74cc43cc5a20e 100644 --- a/src/plugins/data_views/server/fetcher/lib/es_api.ts +++ b/src/plugins/data_views/server/fetcher/lib/es_api.ts @@ -42,7 +42,7 @@ export async function callIndexAliasApi( interface FieldCapsApiParams { callCluster: ElasticsearchClient; indices: string[] | string; - fieldCapsOptions?: { allow_no_indices: boolean }; + fieldCapsOptions?: { allow_no_indices: boolean; include_unmapped?: boolean }; filter?: QueryDslQueryContainer; } @@ -65,6 +65,7 @@ export async function callFieldCapsApi(params: FieldCapsApiParams) { filter, fieldCapsOptions = { allow_no_indices: false, + include_unmapped: false, }, } = params; try { diff --git a/src/plugins/data_views/server/fetcher/lib/field_capabilities/field_capabilities.ts b/src/plugins/data_views/server/fetcher/lib/field_capabilities/field_capabilities.ts index 7a050c8b9d80a..0511801a65688 100644 --- a/src/plugins/data_views/server/fetcher/lib/field_capabilities/field_capabilities.ts +++ b/src/plugins/data_views/server/fetcher/lib/field_capabilities/field_capabilities.ts @@ -19,7 +19,7 @@ interface FieldCapabilitiesParams { callCluster: ElasticsearchClient; indices: string | string[]; metaFields: string[]; - fieldCapsOptions?: { allow_no_indices: boolean }; + fieldCapsOptions?: { allow_no_indices: boolean; include_unmapped?: boolean }; filter?: QueryDslQueryContainer; } diff --git a/src/plugins/data_views/server/routes/fields_for.ts b/src/plugins/data_views/server/routes/fields_for.ts index 67b58148dcf6b..096f0f40b06ff 100644 --- a/src/plugins/data_views/server/routes/fields_for.ts +++ b/src/plugins/data_views/server/routes/fields_for.ts @@ -36,6 +36,7 @@ interface IQuery { type?: string; rollup_index?: string; allow_no_index?: boolean; + include_unmapped?: boolean; } const validate: RouteValidatorFullConfig<{}, IQuery, IBody> = { @@ -47,6 +48,7 @@ const validate: RouteValidatorFullConfig<{}, IQuery, IBody> = { type: schema.maybe(schema.string()), rollup_index: schema.maybe(schema.string()), allow_no_index: schema.maybe(schema.boolean()), + include_unmapped: schema.maybe(schema.boolean()), }), // not available to get request body: schema.maybe(schema.object({ index_filter: schema.any() })), @@ -60,6 +62,7 @@ const handler: RequestHandler<{}, IQuery, IBody> = async (context, request, resp type, rollup_index: rollupIndex, allow_no_index: allowNoIndex, + include_unmapped: includeUnmapped, } = request.query; // not available to get request @@ -80,6 +83,7 @@ const handler: RequestHandler<{}, IQuery, IBody> = async (context, request, resp rollupIndex, fieldCapsOptions: { allow_no_indices: allowNoIndex || false, + includeUnmapped, }, filter, }); diff --git a/src/plugins/discover/kibana.json b/src/plugins/discover/kibana.json index e9d78d844a6e0..238de02475307 100644 --- a/src/plugins/discover/kibana.json +++ b/src/plugins/discover/kibana.json @@ -29,7 +29,8 @@ "usageCollection", "spaces", "triggersActionsUi", - "savedObjectsTaggingOss" + "savedObjectsTaggingOss", + "lens" ], "requiredBundles": ["kibanaUtils", "kibanaReact", "unifiedSearch", "savedSearch"], "extraPublicDirs": ["common"], diff --git a/src/plugins/discover/public/__mocks__/data_view_with_timefield.ts b/src/plugins/discover/public/__mocks__/data_view_with_timefield.ts index 803fb7c6f70db..a481c648aad20 100644 --- a/src/plugins/discover/public/__mocks__/data_view_with_timefield.ts +++ b/src/plugins/discover/public/__mocks__/data_view_with_timefield.ts @@ -18,6 +18,7 @@ const fields = [ }, { name: 'timestamp', + displayName: 'timestamp', type: 'date', scripted: false, filterable: true, @@ -26,12 +27,14 @@ const fields = [ }, { name: 'message', + displayName: 'message', type: 'string', scripted: false, filterable: false, }, { name: 'extension', + displayName: 'extension', type: 'string', scripted: false, filterable: true, @@ -39,6 +42,7 @@ const fields = [ }, { name: 'bytes', + displayName: 'bytes', type: 'number', scripted: false, filterable: true, @@ -46,6 +50,7 @@ const fields = [ }, { name: 'scripted', + displayName: 'scripted', type: 'number', scripted: true, filterable: false, diff --git a/src/plugins/discover/public/__mocks__/data_views.ts b/src/plugins/discover/public/__mocks__/data_views.ts index 7832a4c0f4e39..1bc8d791d53fb 100644 --- a/src/plugins/discover/public/__mocks__/data_views.ts +++ b/src/plugins/discover/public/__mocks__/data_views.ts @@ -11,22 +11,27 @@ import { dataViewMock } from './data_view'; import { dataViewComplexMock } from './data_view_complex'; import { dataViewWithTimefieldMock } from './data_view_with_timefield'; -export const dataViewsMock = { - getCache: async () => { - return [dataViewMock]; - }, - get: async (id: string) => { - if (id === 'the-data-view-id') { - return Promise.resolve(dataViewMock); - } else if (id === 'invalid-data-view-id') { - return Promise.reject('Invald'); - } - }, - updateSavedObject: jest.fn(), - getIdsWithTitle: jest.fn(() => { - return Promise.resolve([dataViewMock, dataViewComplexMock, dataViewWithTimefieldMock]); - }), - createFilter: jest.fn(), - create: jest.fn(), - clearInstanceCache: jest.fn(), -} as unknown as jest.Mocked; +export function createDiscoverDataViewsMock() { + return { + getCache: async () => { + return [dataViewMock]; + }, + get: async (id: string) => { + if (id === 'the-data-view-id') { + return Promise.resolve(dataViewMock); + } else if (id === 'invalid-data-view-id') { + return Promise.reject('Invald'); + } + }, + updateSavedObject: jest.fn(), + getIdsWithTitle: jest.fn(() => { + return Promise.resolve([dataViewMock, dataViewComplexMock, dataViewWithTimefieldMock]); + }), + createFilter: jest.fn(), + create: jest.fn(), + clearInstanceCache: jest.fn(), + getFieldsForIndexPattern: jest.fn((dataView) => dataView.fields), + } as unknown as jest.Mocked; +} + +export const dataViewsMock = createDiscoverDataViewsMock(); diff --git a/src/plugins/discover/public/__mocks__/search_session.ts b/src/plugins/discover/public/__mocks__/search_session.ts index 9763ff7089e0a..abcd5e92a1cbd 100644 --- a/src/plugins/discover/public/__mocks__/search_session.ts +++ b/src/plugins/discover/public/__mocks__/search_session.ts @@ -10,11 +10,12 @@ import { dataPluginMock } from '@kbn/data-plugin/public/mocks'; import { DataPublicPluginStart } from '@kbn/data-plugin/public'; import { DiscoverSearchSessionManager } from '../application/main/services/discover_search_session'; -export function createSearchSessionMock() { - const history = createMemoryHistory(); - const session = dataPluginMock.createStartContract().search.session as jest.Mocked< +export function createSearchSessionMock( + session = dataPluginMock.createStartContract().search.session as jest.Mocked< DataPublicPluginStart['search']['session'] - >; + > +) { + const history = createMemoryHistory(); const searchSessionManager = new DiscoverSearchSessionManager({ history, session, diff --git a/src/plugins/discover/public/__mocks__/services.ts b/src/plugins/discover/public/__mocks__/services.ts index eaae356c03c1a..d21bc4fc115b3 100644 --- a/src/plugins/discover/public/__mocks__/services.ts +++ b/src/plugins/discover/public/__mocks__/services.ts @@ -5,6 +5,7 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ +import { Observable, of } from 'rxjs'; import { EUI_CHARTS_THEME_LIGHT } from '@elastic/eui/dist/eui_charts_theme'; import { DiscoverServices } from '../build_services'; import { dataPluginMock } from '@kbn/data-plugin/public/mocks'; @@ -20,117 +21,138 @@ import { SORT_DEFAULT_ORDER_SETTING, HIDE_ANNOUNCEMENTS, } from '../../common'; -import { UI_SETTINGS } from '@kbn/data-plugin/public'; +import { UI_SETTINGS, calculateBounds } from '@kbn/data-plugin/public'; import { TopNavMenu } from '@kbn/navigation-plugin/public'; import { FORMATS_UI_SETTINGS } from '@kbn/field-formats-plugin/common'; -import { LocalStorageMock } from './local_storage_mock'; +import { chartPluginMock } from '@kbn/charts-plugin/public/mocks'; import { fieldFormatsMock } from '@kbn/field-formats-plugin/common/mocks'; -import { dataViewsMock } from './data_views'; -import { Observable, of } from 'rxjs'; -const dataPlugin = dataPluginMock.createStartContract(); -const expressionsPlugin = expressionsPluginMock.createStartContract(); +import { LocalStorageMock } from './local_storage_mock'; +import { createDiscoverDataViewsMock } from './data_views'; + +export function createDiscoverServicesMock(): DiscoverServices { + const dataPlugin = dataPluginMock.createStartContract(); + const expressionsPlugin = expressionsPluginMock.createStartContract(); -dataPlugin.query.filterManager.getFilters = jest.fn(() => []); -dataPlugin.query.filterManager.getUpdates$ = jest.fn(() => of({}) as unknown as Observable); + dataPlugin.query.filterManager.getFilters = jest.fn(() => []); + dataPlugin.query.filterManager.getUpdates$ = jest.fn(() => of({}) as unknown as Observable); + dataPlugin.query.timefilter.timefilter.createFilter = jest.fn(); + dataPlugin.query.timefilter.timefilter.getAbsoluteTime = jest.fn(() => ({ + from: '2021-08-31T22:00:00.000Z', + to: '2022-09-01T09:16:29.553Z', + })); + dataPlugin.query.timefilter.timefilter.getTime = jest.fn(() => { + return { from: 'now-15m', to: 'now' }; + }); + dataPlugin.query.timefilter.timefilter.calculateBounds = jest.fn(calculateBounds); + dataPlugin.query.getState = jest.fn(() => ({ + query: { query: '', language: 'lucene' }, + filters: [], + })); + dataPlugin.dataViews = createDiscoverDataViewsMock(); -export const discoverServiceMock = { - core: coreMock.createStart(), - chrome: chromeServiceMock.createStartContract(), - history: () => ({ - location: { - search: '', + return { + core: coreMock.createStart(), + charts: chartPluginMock.createSetupContract(), + chrome: chromeServiceMock.createStartContract(), + history: () => ({ + location: { + search: '', + }, + listen: jest.fn(), + }), + data: dataPlugin, + docLinks: docLinksServiceMock.createStartContract(), + capabilities: { + visualize: { + show: true, + }, + discover: { + save: false, + }, + advancedSettings: { + save: true, + }, }, - listen: jest.fn(), - }), - data: dataPlugin, - docLinks: docLinksServiceMock.createStartContract(), - capabilities: { - visualize: { - show: true, + fieldFormats: fieldFormatsMock, + filterManager: dataPlugin.query.filterManager, + inspector: { + open: jest.fn(), }, - discover: { - save: false, + uiSettings: { + get: jest.fn((key: string) => { + if (key === 'fields:popularLimit') { + return 5; + } else if (key === DEFAULT_COLUMNS_SETTING) { + return ['default_column']; + } else if (key === UI_SETTINGS.META_FIELDS) { + return []; + } else if (key === DOC_HIDE_TIME_COLUMN_SETTING) { + return false; + } else if (key === CONTEXT_STEP_SETTING) { + return 5; + } else if (key === SORT_DEFAULT_ORDER_SETTING) { + return 'desc'; + } else if (key === FORMATS_UI_SETTINGS.SHORT_DOTS_ENABLE) { + return false; + } else if (key === SAMPLE_SIZE_SETTING) { + return 250; + } else if (key === SAMPLE_ROWS_PER_PAGE_SETTING) { + return 150; + } else if (key === MAX_DOC_FIELDS_DISPLAYED) { + return 50; + } else if (key === HIDE_ANNOUNCEMENTS) { + return false; + } + }), + isDefault: (key: string) => { + return true; + }, }, - advancedSettings: { - save: true, + http: { + basePath: '/', }, - }, - fieldFormats: fieldFormatsMock, - filterManager: dataPlugin.query.filterManager, - inspector: { - open: jest.fn(), - }, - uiSettings: { - get: jest.fn((key: string) => { - if (key === 'fields:popularLimit') { - return 5; - } else if (key === DEFAULT_COLUMNS_SETTING) { - return ['default_column']; - } else if (key === UI_SETTINGS.META_FIELDS) { - return []; - } else if (key === DOC_HIDE_TIME_COLUMN_SETTING) { - return false; - } else if (key === CONTEXT_STEP_SETTING) { - return 5; - } else if (key === SORT_DEFAULT_ORDER_SETTING) { - return 'desc'; - } else if (key === FORMATS_UI_SETTINGS.SHORT_DOTS_ENABLE) { - return false; - } else if (key === SAMPLE_SIZE_SETTING) { - return 250; - } else if (key === SAMPLE_ROWS_PER_PAGE_SETTING) { - return 150; - } else if (key === MAX_DOC_FIELDS_DISPLAYED) { - return 50; - } else if (key === HIDE_ANNOUNCEMENTS) { - return false; - } - }), - isDefault: (key: string) => { - return true; + dataViewEditor: { + userPermissions: { + editDataView: () => true, + }, + }, + dataViewFieldEditor: { + openEditor: jest.fn(), + userPermissions: { + editIndexPattern: jest.fn(), + }, + }, + navigation: { + ui: { TopNavMenu, AggregateQueryTopNavMenu: TopNavMenu }, }, - }, - http: { - basePath: '/', - }, - dataViewEditor: { - userPermissions: { - editDataView: () => true, + metadata: { + branch: 'test', }, - }, - dataViewFieldEditor: { - openEditor: jest.fn(), - userPermissions: { - editIndexPattern: jest.fn(), + theme: { + useChartsTheme: jest.fn(() => EUI_CHARTS_THEME_LIGHT.theme), + useChartsBaseTheme: jest.fn(() => EUI_CHARTS_THEME_LIGHT.theme), }, - }, - navigation: { - ui: { TopNavMenu, AggregateQueryTopNavMenu: TopNavMenu }, - }, - metadata: { - branch: 'test', - }, - theme: { - useChartsTheme: jest.fn(() => EUI_CHARTS_THEME_LIGHT.theme), - useChartsBaseTheme: jest.fn(() => EUI_CHARTS_THEME_LIGHT.theme), - }, - storage: new LocalStorageMock({}) as unknown as Storage, - addBasePath: jest.fn(), - toastNotifications: { - addInfo: jest.fn(), - addWarning: jest.fn(), - addDanger: jest.fn(), - addSuccess: jest.fn(), - }, - expressions: expressionsPlugin, - savedObjectsTagging: {}, - dataViews: dataViewsMock, - timefilter: { createFilter: jest.fn() }, - locator: { - useUrl: jest.fn(() => ''), - navigate: jest.fn(), - getUrl: jest.fn(() => Promise.resolve('')), - }, - contextLocator: { getRedirectUrl: jest.fn(() => '') }, - singleDocLocator: { getRedirectUrl: jest.fn(() => '') }, -} as unknown as DiscoverServices; + storage: new LocalStorageMock({}) as unknown as Storage, + addBasePath: jest.fn(), + toastNotifications: { + addInfo: jest.fn(), + addWarning: jest.fn(), + addDanger: jest.fn(), + addSuccess: jest.fn(), + }, + expressions: expressionsPlugin, + savedObjectsTagging: {}, + dataViews: dataPlugin.dataViews, + timefilter: dataPlugin.query.timefilter.timefilter, + lens: { EmbeddableComponent: jest.fn(() => null) }, + locator: { + useUrl: jest.fn(() => ''), + navigate: jest.fn(), + getUrl: jest.fn(() => Promise.resolve('')), + }, + contextLocator: { getRedirectUrl: jest.fn(() => '') }, + singleDocLocator: { getRedirectUrl: jest.fn(() => '') }, + } as unknown as DiscoverServices; +} + +export const discoverServiceMock = createDiscoverServicesMock(); diff --git a/src/plugins/discover/public/application/main/components/layout/__stories__/get_layout_props.ts b/src/plugins/discover/public/application/main/components/layout/__stories__/get_layout_props.ts index 56c0f349615e0..334e899b04aee 100644 --- a/src/plugins/discover/public/application/main/components/layout/__stories__/get_layout_props.ts +++ b/src/plugins/discover/public/application/main/components/layout/__stories__/get_layout_props.ts @@ -11,11 +11,9 @@ import { SearchSource } from '@kbn/data-plugin/common'; import { BehaviorSubject, Subject } from 'rxjs'; import { RequestAdapter } from '@kbn/inspector-plugin/common'; import { action } from '@storybook/addon-actions'; -import type { SearchResponse } from '@elastic/elasticsearch/lib/api/types'; import { FetchStatus } from '../../../../types'; import { AvailableFields$, - DataCharts$, DataDocuments$, DataMain$, DataTotalHits$, @@ -47,11 +45,6 @@ const documentObservables = { fetchStatus: FetchStatus.COMPLETE, result: Number(esHits.length), }) as DataTotalHits$, - - charts$: new BehaviorSubject({ - fetchStatus: FetchStatus.COMPLETE, - response: {} as unknown as SearchResponse, - }) as DataCharts$, }; const plainRecordObservables = { @@ -77,11 +70,6 @@ const plainRecordObservables = { fetchStatus: FetchStatus.COMPLETE, recordRawType: RecordRawType.PLAIN, }) as DataTotalHits$, - - charts$: new BehaviorSubject({ - fetchStatus: FetchStatus.COMPLETE, - recordRawType: RecordRawType.PLAIN, - }) as DataCharts$, }; const getCommonProps = (dataView: DataView) => { diff --git a/src/plugins/discover/public/application/main/components/layout/discover_histogram_layout.test.tsx b/src/plugins/discover/public/application/main/components/layout/discover_histogram_layout.test.tsx new file mode 100644 index 0000000000000..5f2b0f9e434d7 --- /dev/null +++ b/src/plugins/discover/public/application/main/components/layout/discover_histogram_layout.test.tsx @@ -0,0 +1,220 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { Subject, BehaviorSubject, of } from 'rxjs'; +import { mountWithIntl } from '@kbn/test-jest-helpers'; +import { esHits } from '../../../../__mocks__/es_hits'; +import { dataViewMock } from '../../../../__mocks__/data_view'; +import { savedSearchMock } from '../../../../__mocks__/saved_search'; +import { GetStateReturn } from '../../services/discover_state'; +import { + AvailableFields$, + DataDocuments$, + DataMain$, + DataTotalHits$, + RecordRawType, +} from '../../hooks/use_saved_search'; +import { discoverServiceMock } from '../../../../__mocks__/services'; +import { FetchStatus } from '../../../types'; +import { KibanaContextProvider, KibanaThemeProvider } from '@kbn/kibana-react-plugin/public'; +import { buildDataTableRecord } from '../../../../utils/build_data_record'; +import { DiscoverHistogramLayout, DiscoverHistogramLayoutProps } from './discover_histogram_layout'; +import { SavedSearch, VIEW_MODE } from '@kbn/saved-search-plugin/public'; +import { CoreTheme } from '@kbn/core/public'; +import { act } from 'react-dom/test-utils'; +import { setTimeout } from 'timers/promises'; +import { Storage } from '@kbn/kibana-utils-plugin/public'; +import { LocalStorageMock } from '../../../../__mocks__/local_storage_mock'; +import { HISTOGRAM_HEIGHT_KEY } from './use_discover_histogram'; +import { createSearchSessionMock } from '../../../../__mocks__/search_session'; +import { RequestAdapter } from '@kbn/inspector-plugin/public'; +import { searchSourceInstanceMock } from '@kbn/data-plugin/common/search/search_source/mocks'; +import { UnifiedHistogramLayout } from '@kbn/unified-histogram-plugin/public'; +import { getSessionServiceMock } from '@kbn/data-plugin/public/search/session/mocks'; +import { ResetSearchButton } from './reset_search_button'; + +const mountComponent = async ({ + isPlainRecord = false, + hideChart = false, + isTimeBased = true, + storage, + savedSearch = savedSearchMock, + resetSavedSearch = jest.fn(), +}: { + isPlainRecord?: boolean; + hideChart?: boolean; + isTimeBased?: boolean; + storage?: Storage; + savedSearch?: SavedSearch; + resetSavedSearch?(): void; +} = {}) => { + let services = discoverServiceMock; + services.data.query.timefilter.timefilter.getAbsoluteTime = () => { + return { from: '2020-05-14T11:05:13.590', to: '2020-05-14T11:20:13.590' }; + }; + + (services.data.query.queryString.getDefaultQuery as jest.Mock).mockReturnValue({ + language: 'kuery', + query: '', + }); + (searchSourceInstanceMock.fetch$ as jest.Mock).mockImplementation( + jest.fn().mockReturnValue(of({ rawResponse: { hits: { total: 2 } } })) + ); + + if (storage) { + services = { ...services, storage }; + } + + const main$ = new BehaviorSubject({ + fetchStatus: FetchStatus.COMPLETE, + recordRawType: isPlainRecord ? RecordRawType.PLAIN : RecordRawType.DOCUMENT, + foundDocuments: true, + }) as DataMain$; + + const documents$ = new BehaviorSubject({ + fetchStatus: FetchStatus.COMPLETE, + result: esHits.map((esHit) => buildDataTableRecord(esHit, dataViewMock)), + }) as DataDocuments$; + + const availableFields$ = new BehaviorSubject({ + fetchStatus: FetchStatus.COMPLETE, + fields: [] as string[], + }) as AvailableFields$; + + const totalHits$ = new BehaviorSubject({ + fetchStatus: FetchStatus.COMPLETE, + result: Number(esHits.length), + }) as DataTotalHits$; + + const savedSearchData$ = { + main$, + documents$, + totalHits$, + availableFields$, + }; + + const session = getSessionServiceMock(); + + session.getSession$.mockReturnValue(new BehaviorSubject('123')); + + const props: DiscoverHistogramLayoutProps = { + isPlainRecord, + dataView: dataViewMock, + navigateTo: jest.fn(), + setExpandedDoc: jest.fn(), + savedSearch, + savedSearchData$, + savedSearchRefetch$: new Subject(), + state: { columns: [], hideChart }, + stateContainer: { + setAppState: () => {}, + appStateContainer: { + getState: () => ({ + interval: 'auto', + }), + }, + } as unknown as GetStateReturn, + onFieldEdited: jest.fn(), + columns: [], + viewMode: VIEW_MODE.DOCUMENT_LEVEL, + onAddFilter: jest.fn(), + resetSavedSearch, + isTimeBased, + resizeRef: { current: null }, + searchSessionManager: createSearchSessionMock(session).searchSessionManager, + inspectorAdapters: { requests: new RequestAdapter() }, + }; + + const coreTheme$ = new BehaviorSubject({ darkMode: false }); + + const component = mountWithIntl( + + + + + + ); + + // DiscoverMainContent uses UnifiedHistogramLayout which + // is lazy loaded, so we need to wait for it to be loaded + await act(() => setTimeout(0)); + component.update(); + + return component; +}; + +describe('Discover histogram layout component', () => { + describe('topPanelHeight persistence', () => { + it('should try to get the initial topPanelHeight for UnifiedHistogramLayout from storage', async () => { + const storage = new LocalStorageMock({}) as unknown as Storage; + const originalGet = storage.get; + storage.get = jest.fn().mockImplementation(originalGet); + await mountComponent({ storage }); + expect(storage.get).toHaveBeenCalledWith(HISTOGRAM_HEIGHT_KEY); + }); + + it('should pass undefined to UnifiedHistogramLayout if no value is found in storage', async () => { + const storage = new LocalStorageMock({}) as unknown as Storage; + const originalGet = storage.get; + storage.get = jest.fn().mockImplementation(originalGet); + const component = await mountComponent({ storage }); + expect(storage.get).toHaveBeenCalledWith(HISTOGRAM_HEIGHT_KEY); + expect(storage.get).toHaveReturnedWith(null); + expect(component.find(UnifiedHistogramLayout).prop('topPanelHeight')).toBe(undefined); + }); + + it('should pass the stored topPanelHeight to UnifiedHistogramLayout if a value is found in storage', async () => { + const storage = new LocalStorageMock({}) as unknown as Storage; + const topPanelHeight = 123; + storage.get = jest.fn().mockImplementation(() => topPanelHeight); + const component = await mountComponent({ storage }); + expect(storage.get).toHaveBeenCalledWith(HISTOGRAM_HEIGHT_KEY); + expect(storage.get).toHaveReturnedWith(topPanelHeight); + expect(component.find(UnifiedHistogramLayout).prop('topPanelHeight')).toBe(topPanelHeight); + }); + + it('should update the topPanelHeight in storage and pass the new value to UnifiedHistogramLayout when the topPanelHeight changes', async () => { + const storage = new LocalStorageMock({}) as unknown as Storage; + const originalSet = storage.set; + storage.set = jest.fn().mockImplementation(originalSet); + const component = await mountComponent({ storage }); + const newTopPanelHeight = 123; + expect(component.find(UnifiedHistogramLayout).prop('topPanelHeight')).not.toBe( + newTopPanelHeight + ); + act(() => { + component.find(UnifiedHistogramLayout).prop('onTopPanelHeightChange')!(newTopPanelHeight); + }); + component.update(); + expect(storage.set).toHaveBeenCalledWith(HISTOGRAM_HEIGHT_KEY, newTopPanelHeight); + expect(component.find(UnifiedHistogramLayout).prop('topPanelHeight')).toBe(newTopPanelHeight); + }); + }); + + describe('reset search button', () => { + it('renders the button when there is a saved search', async () => { + const component = await mountComponent(); + expect(component.find(ResetSearchButton).exists()).toBe(true); + }); + + it('does not render the button when there is no saved search', async () => { + const component = await mountComponent({ + savedSearch: { ...savedSearchMock, id: undefined }, + }); + expect(component.find(ResetSearchButton).exists()).toBe(false); + }); + + it('should call resetSavedSearch when clicked', async () => { + const resetSavedSearch = jest.fn(); + const component = await mountComponent({ resetSavedSearch }); + component.find(ResetSearchButton).find('button').simulate('click'); + expect(resetSavedSearch).toHaveBeenCalled(); + }); + }); +}); diff --git a/src/plugins/discover/public/application/main/components/layout/discover_histogram_layout.tsx b/src/plugins/discover/public/application/main/components/layout/discover_histogram_layout.tsx new file mode 100644 index 0000000000000..e6225a26ffea1 --- /dev/null +++ b/src/plugins/discover/public/application/main/components/layout/discover_histogram_layout.tsx @@ -0,0 +1,87 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React, { RefObject } from 'react'; +import { UnifiedHistogramLayout } from '@kbn/unified-histogram-plugin/public'; +import { css } from '@emotion/react'; +import { useDiscoverServices } from '../../../../hooks/use_discover_services'; +import { useDiscoverHistogram } from './use_discover_histogram'; +import type { DiscoverSearchSessionManager } from '../../services/discover_search_session'; +import type { InspectorAdapters } from '../../hooks/use_inspector'; +import { type DiscoverMainContentProps, DiscoverMainContent } from './discover_main_content'; +import { ResetSearchButton } from './reset_search_button'; + +export interface DiscoverHistogramLayoutProps extends DiscoverMainContentProps { + resetSavedSearch: () => void; + isTimeBased: boolean; + resizeRef: RefObject; + inspectorAdapters: InspectorAdapters; + searchSessionManager: DiscoverSearchSessionManager; +} + +export const DiscoverHistogramLayout = ({ + isPlainRecord, + dataView, + resetSavedSearch, + savedSearch, + savedSearchData$, + state, + stateContainer, + isTimeBased, + resizeRef, + inspectorAdapters, + searchSessionManager, + ...mainContentProps +}: DiscoverHistogramLayoutProps) => { + const services = useDiscoverServices(); + + const commonProps = { + dataView, + isPlainRecord, + stateContainer, + savedSearch, + state, + savedSearchData$, + }; + + const histogramProps = useDiscoverHistogram({ + isTimeBased, + inspectorAdapters, + searchSessionManager, + ...commonProps, + }); + + if (!histogramProps) { + return null; + } + + const histogramLayoutCss = css` + height: 100%; + `; + + return ( + : undefined + } + css={histogramLayoutCss} + {...histogramProps} + > + + + ); +}; diff --git a/src/plugins/discover/public/application/main/components/layout/discover_layout.scss b/src/plugins/discover/public/application/main/components/layout/discover_layout.scss index 70963f50b96a7..a65c22b57b2ab 100644 --- a/src/plugins/discover/public/application/main/components/layout/discover_layout.scss +++ b/src/plugins/discover/public/application/main/components/layout/discover_layout.scss @@ -31,6 +31,10 @@ discover-app { overflow: hidden; } +.dscPageBody__sidebar { + position: relative; +} + .dscPageContent__wrapper { padding: $euiSizeS $euiSizeS $euiSizeS 0; overflow: hidden; // Ensures horizontal scroll of table @@ -41,11 +45,9 @@ discover-app { } .dscPageContent { + position: relative; + overflow: hidden; border: $euiBorderThin; -} - -.dscPageContent, -.dscPageContent__inner { height: 100%; } diff --git a/src/plugins/discover/public/application/main/components/layout/discover_layout.test.tsx b/src/plugins/discover/public/application/main/components/layout/discover_layout.test.tsx index 94b2cec7e00f5..5b58cda5e0b96 100644 --- a/src/plugins/discover/public/application/main/components/layout/discover_layout.test.tsx +++ b/src/plugins/discover/public/application/main/components/layout/discover_layout.test.tsx @@ -7,7 +7,7 @@ */ import React from 'react'; -import { Subject, BehaviorSubject } from 'rxjs'; +import { Subject, BehaviorSubject, of } from 'rxjs'; import { mountWithIntl } from '@kbn/test-jest-helpers'; import type { Query, AggregateQuery } from '@kbn/es-query'; import { setHeaderActionMenuMounter } from '../../../../kibana_services'; @@ -15,20 +15,22 @@ import { DiscoverLayout, SIDEBAR_CLOSED_KEY } from './discover_layout'; import { esHits } from '../../../../__mocks__/es_hits'; import { dataViewMock } from '../../../../__mocks__/data_view'; import { savedSearchMock } from '../../../../__mocks__/saved_search'; -import { createSearchSourceMock } from '@kbn/data-plugin/common/search/search_source/mocks'; +import { + createSearchSourceMock, + searchSourceInstanceMock, +} from '@kbn/data-plugin/common/search/search_source/mocks'; import type { DataView } from '@kbn/data-views-plugin/public'; import { dataViewWithTimefieldMock } from '../../../../__mocks__/data_view_with_timefield'; import { GetStateReturn } from '../../services/discover_state'; import { DiscoverLayoutProps } from './types'; import { AvailableFields$, - DataCharts$, DataDocuments$, DataMain$, DataTotalHits$, RecordRawType, } from '../../hooks/use_saved_search'; -import { discoverServiceMock } from '../../../../__mocks__/services'; +import { createDiscoverServicesMock } from '../../../../__mocks__/services'; import { FetchStatus } from '../../../types'; import { RequestAdapter } from '@kbn/inspector-plugin/common'; import { DiscoverSidebar } from '../sidebar/discover_sidebar'; @@ -37,66 +39,11 @@ import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; import { DiscoverServices } from '../../../../build_services'; import { buildDataTableRecord } from '../../../../utils/build_data_record'; import { DiscoverAppStateProvider } from '../../services/discover_app_state_container'; -import type { UnifiedHistogramChartData } from '@kbn/unified-histogram-plugin/public'; import { getDiscoverStateMock } from '../../../../__mocks__/discover_state.mock'; -import type { SearchResponse } from '@elastic/elasticsearch/lib/api/types'; import { setTimeout } from 'timers/promises'; import { act } from 'react-dom/test-utils'; - -jest.mock('@kbn/unified-histogram-plugin/public', () => { - const originalModule = jest.requireActual('@kbn/unified-histogram-plugin/public'); - - const chartData = { - xAxisOrderedValues: [ - 1623880800000, 1623967200000, 1624053600000, 1624140000000, 1624226400000, 1624312800000, - 1624399200000, 1624485600000, 1624572000000, 1624658400000, 1624744800000, 1624831200000, - 1624917600000, 1625004000000, 1625090400000, - ], - xAxisFormat: { id: 'date', params: { pattern: 'YYYY-MM-DD' } }, - xAxisLabel: 'order_date per day', - yAxisFormat: { id: 'number' }, - ordered: { - date: true, - interval: { - asMilliseconds: jest.fn(), - }, - intervalESUnit: 'd', - intervalESValue: 1, - min: '2021-03-18T08:28:56.411Z', - max: '2021-07-01T07:28:56.411Z', - }, - yAxisLabel: 'Count', - values: [ - { x: 1623880800000, y: 134 }, - { x: 1623967200000, y: 152 }, - { x: 1624053600000, y: 141 }, - { x: 1624140000000, y: 138 }, - { x: 1624226400000, y: 142 }, - { x: 1624312800000, y: 157 }, - { x: 1624399200000, y: 149 }, - { x: 1624485600000, y: 146 }, - { x: 1624572000000, y: 170 }, - { x: 1624658400000, y: 137 }, - { x: 1624744800000, y: 150 }, - { x: 1624831200000, y: 144 }, - { x: 1624917600000, y: 147 }, - { x: 1625004000000, y: 137 }, - { x: 1625090400000, y: 66 }, - ], - } as unknown as UnifiedHistogramChartData; - - return { - ...originalModule, - buildChartData: jest.fn().mockImplementation(() => ({ - chartData, - bucketInterval: { - scaled: true, - description: 'test', - scale: 2, - }, - })), - }; -}); +import { createSearchSessionMock } from '../../../../__mocks__/search_session'; +import { getSessionServiceMock } from '@kbn/data-plugin/public/search/session/mocks'; function getAppStateContainer() { const appStateContainer = getDiscoverStateMock({ isTimeBased: true }).appStateContainer; @@ -118,14 +65,19 @@ async function mountComponent( ) { const searchSourceMock = createSearchSourceMock({}); const services = { - ...discoverServiceMock, + ...createDiscoverServicesMock(), storage: new LocalStorageMock({ [SIDEBAR_CLOSED_KEY]: prevSidebarClosed, }) as unknown as Storage, } as unknown as DiscoverServices; - services.data.query.timefilter.timefilter.getAbsoluteTime = () => { - return { from: '2020-05-14T11:05:13.590', to: '2020-05-14T11:20:13.590' }; - }; + + (services.data.query.queryString.getDefaultQuery as jest.Mock).mockReturnValue({ + language: 'kuery', + query: '', + }); + (searchSourceInstanceMock.fetch$ as jest.Mock).mockImplementation( + jest.fn().mockReturnValue(of({ rawResponse: { hits: { total: 2 } } })) + ); const dataViewList = [dataView]; @@ -150,19 +102,17 @@ async function mountComponent( result: Number(esHits.length), }) as DataTotalHits$; - const charts$ = new BehaviorSubject({ - fetchStatus: FetchStatus.COMPLETE, - response: {} as unknown as SearchResponse, - }) as DataCharts$; - const savedSearchData$ = { main$, documents$, totalHits$, - charts$, availableFields$, }; + const session = getSessionServiceMock(); + + session.getSession$.mockReturnValue(new BehaviorSubject('123')); + const props = { dataView, dataViewList, @@ -175,7 +125,7 @@ async function mountComponent( savedSearchData$, savedSearchRefetch$: new Subject(), searchSource: searchSourceMock, - state: { columns: [], query }, + state: { columns: [], query, hideChart: false, interval: 'auto' }, stateContainer: { setAppState: () => {}, appStateContainer: { @@ -188,6 +138,7 @@ async function mountComponent( persistDataView: jest.fn(), updateAdHocDataViewId: jest.fn(), adHocDataViewList: [], + searchSessionManager: createSearchSessionMock(session).searchSessionManager, savedDataViewList: [], updateDataViewList: jest.fn(), }; @@ -204,6 +155,7 @@ async function mountComponent( // DiscoverMainContent uses UnifiedHistogramLayout which // is lazy loaded, so we need to wait for it to be loaded await act(() => setTimeout(0)); + await component.update(); return component; } @@ -223,7 +175,7 @@ describe('Discover component', () => { expect( container.querySelector('[data-test-subj="unifiedHistogramChartOptionsToggle"]') ).not.toBeNull(); - }); + }, 10000); test('sql query displays no chart toggle', async () => { const container = document.createElement('div'); @@ -248,7 +200,7 @@ describe('Discover component', () => { expect( component.find('[data-test-subj="discoverSavedSearchTitle"]').getDOMNode() ).toHaveFocus(); - }); + }, 10000); describe('sidebar', () => { test('should be opened if discover:sidebarClosed was not set', async () => { diff --git a/src/plugins/discover/public/application/main/components/layout/discover_layout.tsx b/src/plugins/discover/public/application/main/components/layout/discover_layout.tsx index 57a0a3c733e71..d8c5445fd5409 100644 --- a/src/plugins/discover/public/application/main/components/layout/discover_layout.tsx +++ b/src/plugins/discover/public/application/main/components/layout/discover_layout.tsx @@ -43,7 +43,7 @@ import { VIEW_MODE } from '../../../../components/view_mode_toggle'; import { hasActiveFilter } from './utils'; import { getRawRecordType } from '../../utils/get_raw_record_type'; import { SavedSearchURLConflictCallout } from '../../../../components/saved_search_url_conflict_callout/saved_search_url_conflict_callout'; -import { DiscoverMainContent } from './discover_main_content'; +import { DiscoverHistogramLayout } from './discover_histogram_layout'; /** * Local storage key for sidebar persistence state @@ -72,6 +72,7 @@ export function DiscoverLayout({ persistDataView, updateAdHocDataViewId, adHocDataViewList, + searchSessionManager, savedDataViewList, updateDataViewList, }: DiscoverLayoutProps) { @@ -197,6 +198,75 @@ export function DiscoverLayout({ const resizeRef = useRef(null); + const mainDisplay = useMemo(() => { + if (resultState === 'none') { + return ( + + ); + } + + if (resultState === 'uninitialized') { + return savedSearchRefetch$.next(undefined)} />; + } + + return ( + <> + + {resultState === 'loading' && } + + ); + }, [ + columns, + data, + dataState.error, + dataView, + expandedDoc, + inspectorAdapters, + isPlainRecord, + isTimeBased, + navigateTo, + onAddFilter, + onDisableFilters, + onFieldEdited, + resetSavedSearch, + resultState, + savedSearch, + savedSearchData$, + savedSearchRefetch$, + searchSessionManager, + setExpandedDoc, + state, + stateContainer, + viewMode, + ]); + return (

- + - {resultState === 'none' && ( - - )} - {resultState === 'uninitialized' && ( - savedSearchRefetch$.next(undefined)} /> - )} - {resultState === 'loading' && } - {resultState === 'ready' && ( - - )} + {mainDisplay} diff --git a/src/plugins/discover/public/application/main/components/layout/discover_main_content.test.tsx b/src/plugins/discover/public/application/main/components/layout/discover_main_content.test.tsx index 54e3fa0b19ca5..dd30d6b119fd3 100644 --- a/src/plugins/discover/public/application/main/components/layout/discover_main_content.test.tsx +++ b/src/plugins/discover/public/application/main/components/layout/discover_main_content.test.tsx @@ -7,117 +7,49 @@ */ import React from 'react'; -import { Subject, BehaviorSubject } from 'rxjs'; -import { findTestSubject, mountWithIntl } from '@kbn/test-jest-helpers'; +import { Subject, BehaviorSubject, of } from 'rxjs'; +import { mountWithIntl } from '@kbn/test-jest-helpers'; import { esHits } from '../../../../__mocks__/es_hits'; import { dataViewMock } from '../../../../__mocks__/data_view'; -import { savedSearchMock } from '../../../../__mocks__/saved_search'; import { GetStateReturn } from '../../services/discover_state'; +import { savedSearchMock } from '../../../../__mocks__/saved_search'; import { AvailableFields$, - DataCharts$, DataDocuments$, DataMain$, DataTotalHits$, RecordRawType, } from '../../hooks/use_saved_search'; -import { discoverServiceMock } from '../../../../__mocks__/services'; +import { createDiscoverServicesMock } from '../../../../__mocks__/services'; import { FetchStatus } from '../../../types'; import { KibanaContextProvider, KibanaThemeProvider } from '@kbn/kibana-react-plugin/public'; import { buildDataTableRecord } from '../../../../utils/build_data_record'; import { DiscoverMainContent, DiscoverMainContentProps } from './discover_main_content'; -import { SavedSearch, VIEW_MODE } from '@kbn/saved-search-plugin/public'; +import { VIEW_MODE } from '@kbn/saved-search-plugin/public'; import { CoreTheme } from '@kbn/core/public'; import { act } from 'react-dom/test-utils'; import { setTimeout } from 'timers/promises'; import { DocumentViewModeToggle } from '../../../../components/view_mode_toggle'; -import { Storage } from '@kbn/kibana-utils-plugin/public'; -import { LocalStorageMock } from '../../../../__mocks__/local_storage_mock'; -import { - UnifiedHistogramChartData, - UnifiedHistogramLayout, -} from '@kbn/unified-histogram-plugin/public'; -import { HISTOGRAM_HEIGHT_KEY } from './use_discover_histogram'; -import type { SearchResponse } from '@elastic/elasticsearch/lib/api/types'; - -jest.mock('@kbn/unified-histogram-plugin/public', () => { - const originalModule = jest.requireActual('@kbn/unified-histogram-plugin/public'); - - const chartData = { - xAxisOrderedValues: [ - 1623880800000, 1623967200000, 1624053600000, 1624140000000, 1624226400000, 1624312800000, - 1624399200000, 1624485600000, 1624572000000, 1624658400000, 1624744800000, 1624831200000, - 1624917600000, 1625004000000, 1625090400000, - ], - xAxisFormat: { id: 'date', params: { pattern: 'YYYY-MM-DD' } }, - xAxisLabel: 'order_date per day', - yAxisFormat: { id: 'number' }, - ordered: { - date: true, - interval: { - asMilliseconds: jest.fn(), - }, - intervalESUnit: 'd', - intervalESValue: 1, - min: '2021-03-18T08:28:56.411Z', - max: '2021-07-01T07:28:56.411Z', - }, - yAxisLabel: 'Count', - values: [ - { x: 1623880800000, y: 134 }, - { x: 1623967200000, y: 152 }, - { x: 1624053600000, y: 141 }, - { x: 1624140000000, y: 138 }, - { x: 1624226400000, y: 142 }, - { x: 1624312800000, y: 157 }, - { x: 1624399200000, y: 149 }, - { x: 1624485600000, y: 146 }, - { x: 1624572000000, y: 170 }, - { x: 1624658400000, y: 137 }, - { x: 1624744800000, y: 150 }, - { x: 1624831200000, y: 144 }, - { x: 1624917600000, y: 147 }, - { x: 1625004000000, y: 137 }, - { x: 1625090400000, y: 66 }, - ], - } as unknown as UnifiedHistogramChartData; - - return { - ...originalModule, - buildChartData: jest.fn().mockImplementation(() => ({ - chartData, - bucketInterval: { - scaled: true, - description: 'test', - scale: 2, - }, - })), - }; -}); +import { searchSourceInstanceMock } from '@kbn/data-plugin/common/search/search_source/mocks'; +import { DiscoverDocuments } from './discover_documents'; +import { FieldStatisticsTab } from '../field_stats_table'; const mountComponent = async ({ isPlainRecord = false, - hideChart = false, - isTimeBased = true, - storage, - savedSearch = savedSearchMock, - resetSavedSearch = jest.fn(), + viewMode = VIEW_MODE.DOCUMENT_LEVEL, }: { isPlainRecord?: boolean; - hideChart?: boolean; - isTimeBased?: boolean; - storage?: Storage; - savedSearch?: SavedSearch; - resetSavedSearch?: () => void; + viewMode?: VIEW_MODE; } = {}) => { - let services = discoverServiceMock; - services.data.query.timefilter.timefilter.getAbsoluteTime = () => { - return { from: '2020-05-14T11:05:13.590', to: '2020-05-14T11:20:13.590' }; - }; + const services = createDiscoverServicesMock(); - if (storage) { - services = { ...services, storage }; - } + (services.data.query.queryString.getDefaultQuery as jest.Mock).mockReturnValue({ + language: 'kuery', + query: '', + }); + (searchSourceInstanceMock.fetch$ as jest.Mock).mockImplementation( + jest.fn().mockReturnValue(of({ rawResponse: { hits: { total: 2 } } })) + ); const main$ = new BehaviorSubject({ fetchStatus: FetchStatus.COMPLETE, @@ -140,16 +72,10 @@ const mountComponent = async ({ result: Number(esHits.length), }) as DataTotalHits$; - const charts$ = new BehaviorSubject({ - fetchStatus: FetchStatus.COMPLETE, - response: {} as unknown as SearchResponse, - }) as DataCharts$; - const savedSearchData$ = { main$, documents$, totalHits$, - charts$, availableFields$, }; @@ -157,12 +83,11 @@ const mountComponent = async ({ isPlainRecord, dataView: dataViewMock, navigateTo: jest.fn(), - resetSavedSearch, setExpandedDoc: jest.fn(), - savedSearch, + savedSearch: savedSearchMock, savedSearchData$, savedSearchRefetch$: new Subject(), - state: { columns: [], hideChart }, + state: { columns: [], hideChart: false }, stateContainer: { setAppState: () => {}, appStateContainer: { @@ -171,12 +96,10 @@ const mountComponent = async ({ }), }, } as unknown as GetStateReturn, - isTimeBased, - viewMode: VIEW_MODE.DOCUMENT_LEVEL, - onAddFilter: jest.fn(), onFieldEdited: jest.fn(), columns: [], - resizeRef: { current: null }, + viewMode, + onAddFilter: jest.fn(), }; const coreTheme$ = new BehaviorSubject({ darkMode: false }); @@ -192,6 +115,7 @@ const mountComponent = async ({ // DiscoverMainContent uses UnifiedHistogramLayout which // is lazy loaded, so we need to wait for it to be loaded await act(() => setTimeout(0)); + component.update(); return component; }; @@ -200,82 +124,26 @@ describe('Discover main content component', () => { describe('DocumentViewModeToggle', () => { it('should show DocumentViewModeToggle when isPlainRecord is false', async () => { const component = await mountComponent(); - component.update(); expect(component.find(DocumentViewModeToggle).exists()).toBe(true); }); it('should not show DocumentViewModeToggle when isPlainRecord is true', async () => { const component = await mountComponent({ isPlainRecord: true }); - component.update(); expect(component.find(DocumentViewModeToggle).exists()).toBe(false); }); }); - describe('topPanelHeight persistence', () => { - it('should try to get the initial topPanelHeight for UnifiedHistogramLayout from storage', async () => { - const storage = new LocalStorageMock({}) as unknown as Storage; - const originalGet = storage.get; - storage.get = jest.fn().mockImplementation(originalGet); - await mountComponent({ storage }); - expect(storage.get).toHaveBeenCalledWith(HISTOGRAM_HEIGHT_KEY); - }); - - it('should pass undefined to UnifiedHistogramLayout if no value is found in storage', async () => { - const storage = new LocalStorageMock({}) as unknown as Storage; - const originalGet = storage.get; - storage.get = jest.fn().mockImplementation(originalGet); - const component = await mountComponent({ storage }); - expect(storage.get).toHaveBeenCalledWith(HISTOGRAM_HEIGHT_KEY); - expect(storage.get).toHaveReturnedWith(null); - expect(component.find(UnifiedHistogramLayout).prop('topPanelHeight')).toBe(undefined); - }); - - it('should pass the stored topPanelHeight to UnifiedHistogramLayout if a value is found in storage', async () => { - const storage = new LocalStorageMock({}) as unknown as Storage; - const topPanelHeight = 123; - storage.get = jest.fn().mockImplementation(() => topPanelHeight); - const component = await mountComponent({ storage }); - expect(storage.get).toHaveBeenCalledWith(HISTOGRAM_HEIGHT_KEY); - expect(storage.get).toHaveReturnedWith(topPanelHeight); - expect(component.find(UnifiedHistogramLayout).prop('topPanelHeight')).toBe(topPanelHeight); - }); - - it('should update the topPanelHeight in storage and pass the new value to UnifiedHistogramLayout when the topPanelHeight changes', async () => { - const storage = new LocalStorageMock({}) as unknown as Storage; - const originalSet = storage.set; - storage.set = jest.fn().mockImplementation(originalSet); - const component = await mountComponent({ storage }); - const newTopPanelHeight = 123; - expect(component.find(UnifiedHistogramLayout).prop('topPanelHeight')).not.toBe( - newTopPanelHeight - ); - act(() => { - component.find(UnifiedHistogramLayout).prop('onTopPanelHeightChange')!(newTopPanelHeight); - }); - component.update(); - expect(storage.set).toHaveBeenCalledWith(HISTOGRAM_HEIGHT_KEY, newTopPanelHeight); - expect(component.find(UnifiedHistogramLayout).prop('topPanelHeight')).toBe(newTopPanelHeight); - }); - }); - - describe('reset search button', () => { - it('renders the button when there is a saved search', async () => { + describe('Document view', () => { + it('should show DiscoverDocuments when VIEW_MODE is DOCUMENT_LEVEL', async () => { const component = await mountComponent(); - expect(findTestSubject(component, 'resetSavedSearch').length).toBe(1); - }); - - it('does not render the button when there is no saved search', async () => { - const component = await mountComponent({ - savedSearch: { ...savedSearchMock, id: undefined }, - }); - expect(findTestSubject(component, 'resetSavedSearch').length).toBe(0); + expect(component.find(DiscoverDocuments).exists()).toBe(true); + expect(component.find(FieldStatisticsTab).exists()).toBe(false); }); - it('should call resetSavedSearch when clicked', async () => { - const resetSavedSearch = jest.fn(); - const component = await mountComponent({ resetSavedSearch }); - findTestSubject(component, 'resetSavedSearch').simulate('click'); - expect(resetSavedSearch).toHaveBeenCalled(); + it('should show FieldStatisticsTableMemoized when VIEW_MODE is not DOCUMENT_LEVEL', async () => { + const component = await mountComponent({ viewMode: VIEW_MODE.AGGREGATED_LEVEL }); + expect(component.find(DiscoverDocuments).exists()).toBe(false); + expect(component.find(FieldStatisticsTab).exists()).toBe(true); }); }); }); diff --git a/src/plugins/discover/public/application/main/components/layout/discover_main_content.tsx b/src/plugins/discover/public/application/main/components/layout/discover_main_content.tsx index 86428fff1ed91..98b15fe9e5999 100644 --- a/src/plugins/discover/public/application/main/components/layout/discover_main_content.tsx +++ b/src/plugins/discover/public/application/main/components/layout/discover_main_content.tsx @@ -6,14 +6,11 @@ * Side Public License, v 1. */ -import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiHorizontalRule } from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem, EuiHorizontalRule } from '@elastic/eui'; import { SavedSearch } from '@kbn/saved-search-plugin/public'; -import React, { RefObject, useCallback } from 'react'; +import React, { useCallback } from 'react'; import { DataView } from '@kbn/data-views-plugin/common'; import { METRIC_TYPE } from '@kbn/analytics'; -import { UnifiedHistogramLayout } from '@kbn/unified-histogram-plugin/public'; -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n-react'; import { useDiscoverServices } from '../../../../hooks/use_discover_services'; import { DataTableRecord } from '../../../../types'; import { DocumentViewModeToggle, VIEW_MODE } from '../../../../components/view_mode_toggle'; @@ -23,49 +20,41 @@ import { AppState, GetStateReturn } from '../../services/discover_state'; import { FieldStatisticsTab } from '../field_stats_table'; import { DiscoverDocuments } from './discover_documents'; import { DOCUMENTS_VIEW_CLICK, FIELD_STATISTICS_VIEW_CLICK } from '../field_stats_table/constants'; -import { useDiscoverHistogram } from './use_discover_histogram'; export interface DiscoverMainContentProps { - isPlainRecord: boolean; dataView: DataView; - navigateTo: (url: string) => void; - resetSavedSearch: () => void; - expandedDoc?: DataTableRecord; - setExpandedDoc: (doc?: DataTableRecord) => void; savedSearch: SavedSearch; + isPlainRecord: boolean; + navigateTo: (url: string) => void; savedSearchData$: SavedSearchData; savedSearchRefetch$: DataRefetch$; - state: AppState; - stateContainer: GetStateReturn; - isTimeBased: boolean; + expandedDoc?: DataTableRecord; + setExpandedDoc: (doc?: DataTableRecord) => void; viewMode: VIEW_MODE; onAddFilter: DocViewFilterFn | undefined; onFieldEdited: () => Promise; columns: string[]; - resizeRef: RefObject; + state: AppState; + stateContainer: GetStateReturn; } export const DiscoverMainContent = ({ - isPlainRecord, dataView, + isPlainRecord, navigateTo, - resetSavedSearch, - expandedDoc, - setExpandedDoc, - savedSearch, savedSearchData$, savedSearchRefetch$, - state, - stateContainer, - isTimeBased, + expandedDoc, + setExpandedDoc, viewMode, onAddFilter, onFieldEdited, columns, - resizeRef, + state, + stateContainer, + savedSearch, }: DiscoverMainContentProps) => { - const services = useDiscoverServices(); - const { trackUiMetric } = services; + const { trackUiMetric } = useDiscoverServices(); const setDiscoverViewMode = useCallback( (mode: VIEW_MODE) => { @@ -82,96 +71,45 @@ export const DiscoverMainContent = ({ [trackUiMetric, stateContainer] ); - const { - topPanelHeight, - hits, - chart, - onEditVisualization, - onTopPanelHeightChange, - onChartHiddenChange, - onTimeIntervalChange, - } = useDiscoverHistogram({ - stateContainer, - state, - savedSearchData$, - dataView, - savedSearch, - isTimeBased, - isPlainRecord, - }); - return ( - - - - - - ) : undefined - } - onTopPanelHeightChange={onTopPanelHeightChange} - onEditVisualization={onEditVisualization} - onChartHiddenChange={onChartHiddenChange} - onTimeIntervalChange={onTimeIntervalChange} + - - {!isPlainRecord && ( - - - - - )} - {viewMode === VIEW_MODE.DOCUMENT_LEVEL ? ( - - ) : ( - - )} - - + {!isPlainRecord && ( + + + + + )} + {viewMode === VIEW_MODE.DOCUMENT_LEVEL ? ( + + ) : ( + + )} + ); }; diff --git a/src/plugins/discover/public/application/main/components/layout/reset_search_button.test.tsx b/src/plugins/discover/public/application/main/components/layout/reset_search_button.test.tsx new file mode 100644 index 0000000000000..cde96ff72050f --- /dev/null +++ b/src/plugins/discover/public/application/main/components/layout/reset_search_button.test.tsx @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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 { mountWithIntl } from '@kbn/test-jest-helpers'; +import React from 'react'; +import { ResetSearchButton } from './reset_search_button'; + +describe('ResetSearchButton', () => { + it('should call resetSavedSearch when the button is clicked', () => { + const resetSavedSearch = jest.fn(); + const component = mountWithIntl(); + component.find('button[data-test-subj="resetSavedSearch"]').simulate('click'); + expect(resetSavedSearch).toHaveBeenCalled(); + }); +}); diff --git a/src/plugins/discover/public/application/main/components/layout/reset_search_button.tsx b/src/plugins/discover/public/application/main/components/layout/reset_search_button.tsx new file mode 100644 index 0000000000000..e9b0cc2417d97 --- /dev/null +++ b/src/plugins/discover/public/application/main/components/layout/reset_search_button.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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { EuiButtonEmpty, EuiFlexItem } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { css } from '@emotion/react'; + +const resetSearchButtonWrapper = css` + overflow: hidden; +`; + +export const ResetSearchButton = ({ resetSavedSearch }: { resetSavedSearch?: () => void }) => { + return ( + + + + + + ); +}; diff --git a/src/plugins/discover/public/application/main/components/layout/types.ts b/src/plugins/discover/public/application/main/components/layout/types.ts index 2ce822b28d150..24b0a47f6f2a6 100644 --- a/src/plugins/discover/public/application/main/components/layout/types.ts +++ b/src/plugins/discover/public/application/main/components/layout/types.ts @@ -9,16 +9,17 @@ import type { Query, TimeRange, AggregateQuery } from '@kbn/es-query'; import type { DataView } from '@kbn/data-views-plugin/public'; import type { DataViewListItem, ISearchSource } from '@kbn/data-plugin/public'; -import { RequestAdapter } from '@kbn/inspector-plugin/common'; import { SavedSearch } from '@kbn/saved-search-plugin/public'; import { DataTableRecord } from '../../../../types'; import { AppState, GetStateReturn } from '../../services/discover_state'; import { DataRefetch$, SavedSearchData } from '../../hooks/use_saved_search'; +import type { DiscoverSearchSessionManager } from '../../services/discover_search_session'; +import type { InspectorAdapters } from '../../hooks/use_inspector'; export interface DiscoverLayoutProps { dataView: DataView; dataViewList: DataViewListItem[]; - inspectorAdapters: { requests: RequestAdapter }; + inspectorAdapters: InspectorAdapters; navigateTo: (url: string) => void; onChangeDataView: (id: string) => void; onUpdateQuery: ( @@ -38,5 +39,6 @@ export interface DiscoverLayoutProps { updateDataViewList: (dataViews: DataView[]) => Promise; updateAdHocDataViewId: (dataView: DataView) => Promise; adHocDataViewList: DataView[]; + searchSessionManager: DiscoverSearchSessionManager; savedDataViewList: DataViewListItem[]; } diff --git a/src/plugins/discover/public/application/main/components/layout/use_discover_histogram.test.ts b/src/plugins/discover/public/application/main/components/layout/use_discover_histogram.test.ts index 12fde6a5b1061..b562ed1f6df07 100644 --- a/src/plugins/discover/public/application/main/components/layout/use_discover_histogram.test.ts +++ b/src/plugins/discover/public/application/main/components/layout/use_discover_histogram.test.ts @@ -6,7 +6,6 @@ * Side Public License, v 1. */ -import type { SearchResponse } from '@elastic/elasticsearch/lib/api/types'; import { buildDataTableRecord } from '../../../../utils/build_data_record'; import { esHits } from '../../../../__mocks__/es_hits'; import { act, renderHook } from '@testing-library/react-hooks'; @@ -14,13 +13,12 @@ import { BehaviorSubject } from 'rxjs'; import { FetchStatus } from '../../../types'; import { AvailableFields$, - DataCharts$, DataDocuments$, DataMain$, DataTotalHits$, RecordRawType, } from '../../hooks/use_saved_search'; -import type { GetStateReturn } from '../../services/discover_state'; +import type { AppState, GetStateReturn } from '../../services/discover_state'; import { savedSearchMock } from '../../../../__mocks__/saved_search'; import type { Storage } from '@kbn/kibana-utils-plugin/public'; import { LocalStorageMock } from '../../../../__mocks__/local_storage_mock'; @@ -33,6 +31,14 @@ import { } from './use_discover_histogram'; import { setTimeout } from 'timers/promises'; import { calculateBounds } from '@kbn/data-plugin/public'; +import { createSearchSessionMock } from '../../../../__mocks__/search_session'; +import { RequestAdapter } from '@kbn/inspector-plugin/public'; +import { getSessionServiceMock } from '@kbn/data-plugin/public/search/session/mocks'; +import { UnifiedHistogramFetchStatus } from '@kbn/unified-histogram-plugin/public'; +import { checkHitCount, sendErrorTo } from '../../hooks/use_saved_search_messages'; +import type { InspectorAdapters } from '../../hooks/use_inspector'; +import type { TypedLensByValueInput } from '@kbn/lens-plugin/public'; +import { DiscoverSearchSessionManager } from '../../services/discover_search_session'; const mockData = dataPluginMock.createStartContract(); @@ -43,6 +49,10 @@ mockData.query.timefilter.timefilter.calculateBounds = (timeRange) => { return calculateBounds(timeRange); }; +const mockLens = { + navigateToPrefilledEditor: jest.fn(), +}; + let mockStorage = new LocalStorageMock({}) as unknown as Storage; let mockCanVisualize = true; @@ -50,7 +60,7 @@ jest.mock('../../../../hooks/use_discover_services', () => { const originalModule = jest.requireActual('../../../../hooks/use_discover_services'); return { ...originalModule, - useDiscoverServices: () => ({ storage: mockStorage, data: mockData }), + useDiscoverServices: () => ({ storage: mockStorage, data: mockData, lens: mockLens }), }; }); @@ -62,29 +72,53 @@ jest.mock('@kbn/unified-field-list-plugin/public', () => { }; }); +jest.mock('../../hooks/use_saved_search_messages', () => { + const originalModule = jest.requireActual('../../hooks/use_saved_search_messages'); + return { + ...originalModule, + checkHitCount: jest.fn(originalModule.checkHitCount), + sendErrorTo: jest.fn(originalModule.sendErrorTo), + }; +}); + +const mockCheckHitCount = checkHitCount as jest.MockedFunction; + describe('useDiscoverHistogram', () => { const renderUseDiscoverHistogram = async ({ isPlainRecord = false, isTimeBased = true, canVisualize = true, storage = new LocalStorageMock({}) as unknown as Storage, + state = { interval: 'auto', hideChart: false, breakdownField: 'extension' }, stateContainer = {}, + searchSessionManager, + searchSessionId = '123', + inspectorAdapters = { requests: new RequestAdapter() }, + totalHits$ = new BehaviorSubject({ + fetchStatus: FetchStatus.COMPLETE, + result: Number(esHits.length), + }) as DataTotalHits$, + main$ = new BehaviorSubject({ + fetchStatus: FetchStatus.COMPLETE, + recordRawType: isPlainRecord ? RecordRawType.PLAIN : RecordRawType.DOCUMENT, + foundDocuments: true, + }) as DataMain$, }: { isPlainRecord?: boolean; isTimeBased?: boolean; canVisualize?: boolean; storage?: Storage; + state?: AppState; stateContainer?: unknown; + searchSessionManager?: DiscoverSearchSessionManager; + searchSessionId?: string | null; + inspectorAdapters?: InspectorAdapters; + totalHits$?: DataTotalHits$; + main$?: DataMain$; } = {}) => { mockStorage = storage; mockCanVisualize = canVisualize; - const main$ = new BehaviorSubject({ - fetchStatus: FetchStatus.COMPLETE, - recordRawType: isPlainRecord ? RecordRawType.PLAIN : RecordRawType.DOCUMENT, - foundDocuments: true, - }) as DataMain$; - const documents$ = new BehaviorSubject({ fetchStatus: FetchStatus.COMPLETE, result: esHits.map((esHit) => buildDataTableRecord(esHit, dataViewWithTimefieldMock)), @@ -95,148 +129,142 @@ describe('useDiscoverHistogram', () => { fields: [] as string[], }) as AvailableFields$; - const totalHits$ = new BehaviorSubject({ - fetchStatus: FetchStatus.COMPLETE, - result: Number(esHits.length), - }) as DataTotalHits$; - - const charts$ = new BehaviorSubject({ - fetchStatus: FetchStatus.COMPLETE, - response: { - took: 0, - timed_out: false, - _shards: { - total: 1, - successful: 1, - skipped: 0, - failed: 0, - }, - hits: { - total: 29, - max_score: null, - hits: [], - }, - aggregations: { - '2': { - buckets: [ - { - key_as_string: '2022-10-05T16:00:00.000-03:00', - key: 1664996400000, - doc_count: 6, - }, - { - key_as_string: '2022-10-05T16:30:00.000-03:00', - key: 1664998200000, - doc_count: 2, - }, - { - key_as_string: '2022-10-05T17:00:00.000-03:00', - key: 1665000000000, - doc_count: 3, - }, - { - key_as_string: '2022-10-05T17:30:00.000-03:00', - key: 1665001800000, - doc_count: 8, - }, - { - key_as_string: '2022-10-05T18:00:00.000-03:00', - key: 1665003600000, - doc_count: 10, - }, - ], - }, - }, - } as SearchResponse, - }) as DataCharts$; - const savedSearchData$ = { main$, documents$, totalHits$, - charts$, availableFields$, }; - const hook = renderHook(() => { - return useDiscoverHistogram({ - stateContainer: stateContainer as GetStateReturn, - state: { interval: 'auto', hideChart: false }, - savedSearchData$, - dataView: dataViewWithTimefieldMock, - savedSearch: savedSearchMock, - isTimeBased, - isPlainRecord, - }); - }); + if (!searchSessionManager) { + const session = getSessionServiceMock(); + session.getSession$.mockReturnValue(new BehaviorSubject(searchSessionId ?? undefined)); + searchSessionManager = createSearchSessionMock(session).searchSessionManager; + } + + const initialProps = { + stateContainer: stateContainer as GetStateReturn, + state, + savedSearchData$, + dataView: dataViewWithTimefieldMock, + savedSearch: savedSearchMock, + isTimeBased, + isPlainRecord, + inspectorAdapters, + searchSessionManager: searchSessionManager!, + }; + + const hook = renderHook( + (props: Parameters[0]) => useDiscoverHistogram(props), + { initialProps } + ); await act(() => setTimeout(0)); - return hook; + return { hook, initialProps }; }; - const expectedChartData = { - xAxisOrderedValues: [1664996400000, 1664998200000, 1665000000000, 1665001800000, 1665003600000], - xAxisFormat: { id: 'date', params: { pattern: 'HH:mm:ss.SSS' } }, - xAxisLabel: 'timestamp per 0 milliseconds', - yAxisFormat: { id: 'number' }, - ordered: { - date: true, - interval: 'P0D', - intervalESUnit: 'ms', - intervalESValue: 0, - min: '1991-03-29T08:04:00.694Z', - max: '2021-03-29T07:04:00.695Z', - }, - yAxisLabel: 'Count', - values: [ - { x: 1664996400000, y: 6 }, - { x: 1664998200000, y: 2 }, - { x: 1665000000000, y: 3 }, - { x: 1665001800000, y: 8 }, - { x: 1665003600000, y: 10 }, - ], - }; + it('should return undefined if there is no search session', async () => { + const { + hook: { result }, + } = await renderUseDiscoverHistogram({ searchSessionId: null }); + expect(result.current).toBeUndefined(); + }); describe('contexts', () => { it('should output the correct hits context', async () => { - const { result } = await renderUseDiscoverHistogram(); - expect(result.current.hits?.status).toBe(FetchStatus.COMPLETE); - expect(result.current.hits?.total).toEqual(esHits.length); + const { + hook: { result }, + } = await renderUseDiscoverHistogram(); + expect(result.current?.hits?.status).toBe(UnifiedHistogramFetchStatus.complete); + expect(result.current?.hits?.total).toEqual(esHits.length); }); it('should output the correct chart context', async () => { - const { result } = await renderUseDiscoverHistogram(); - expect(result.current.chart?.status).toBe(FetchStatus.COMPLETE); - expect(result.current.chart?.hidden).toBe(false); - expect(result.current.chart?.timeInterval).toBe('auto'); - expect(result.current.chart?.bucketInterval?.toString()).toBe('P0D'); - expect(JSON.stringify(result.current.chart?.data)).toBe(JSON.stringify(expectedChartData)); - expect(result.current.chart?.error).toBeUndefined(); + const { + hook: { result }, + } = await renderUseDiscoverHistogram(); + expect(result.current?.chart?.hidden).toBe(false); + expect(result.current?.chart?.timeInterval).toBe('auto'); + }); + + it('should output the correct breakdown context', async () => { + const { + hook: { result }, + } = await renderUseDiscoverHistogram(); + expect(result.current?.breakdown?.field?.name).toBe('extension'); + }); + + it('should output the correct request context', async () => { + const requestAdapter = new RequestAdapter(); + const { + hook: { result }, + } = await renderUseDiscoverHistogram({ + searchSessionId: '321', + inspectorAdapters: { requests: requestAdapter }, + }); + expect(result.current?.request.adapter).toBe(requestAdapter); + expect(result.current?.request.searchSessionId).toBe('321'); + }); + + it('should output undefined for hits and chart and breakdown if isPlainRecord is true', async () => { + const { + hook: { result }, + } = await renderUseDiscoverHistogram({ isPlainRecord: true }); + expect(result.current?.hits).toBeUndefined(); + expect(result.current?.chart).toBeUndefined(); + expect(result.current?.breakdown).toBeUndefined(); }); - it('should output undefined for hits and chart if isPlainRecord is true', async () => { - const { result } = await renderUseDiscoverHistogram({ isPlainRecord: true }); - expect(result.current.hits).toBeUndefined(); - expect(result.current.chart).toBeUndefined(); + it('should output undefined for chart and breakdown if isTimeBased is false', async () => { + const { + hook: { result }, + } = await renderUseDiscoverHistogram({ isTimeBased: false }); + expect(result.current?.hits).not.toBeUndefined(); + expect(result.current?.chart).toBeUndefined(); + expect(result.current?.breakdown).toBeUndefined(); }); - it('should output undefined for chart if isTimeBased is false', async () => { - const { result } = await renderUseDiscoverHistogram({ isTimeBased: false }); - expect(result.current.hits).not.toBeUndefined(); - expect(result.current.chart).toBeUndefined(); + it('should clear lensRequests when chart is undefined', async () => { + const inspectorAdapters = { + requests: new RequestAdapter(), + lensRequests: new RequestAdapter(), + }; + const { hook, initialProps } = await renderUseDiscoverHistogram({ + inspectorAdapters, + }); + expect(inspectorAdapters.lensRequests).toBeDefined(); + hook.rerender({ ...initialProps, isPlainRecord: true }); + expect(inspectorAdapters.lensRequests).toBeUndefined(); }); }); describe('onEditVisualization', () => { it('returns a callback for onEditVisualization when the data view can be visualized', async () => { - const { result } = await renderUseDiscoverHistogram(); - expect(result.current.onEditVisualization).toBeDefined(); + const { + hook: { result }, + } = await renderUseDiscoverHistogram(); + expect(result.current?.onEditVisualization).toBeDefined(); }); it('returns undefined for onEditVisualization when the data view cannot be visualized', async () => { - const { result } = await renderUseDiscoverHistogram({ canVisualize: false }); - expect(result.current.onEditVisualization).toBeUndefined(); + const { + hook: { result }, + } = await renderUseDiscoverHistogram({ canVisualize: false }); + expect(result.current?.onEditVisualization).toBeUndefined(); + }); + + it('should call lens.navigateToPrefilledEditor when onEditVisualization is called', async () => { + const { + hook: { result }, + } = await renderUseDiscoverHistogram(); + const attributes = { title: 'test' } as TypedLensByValueInput['attributes']; + result.current?.onEditVisualization!(attributes); + expect(mockLens.navigateToPrefilledEditor).toHaveBeenCalledWith({ + id: '', + timeRange: mockData.query.timefilter.timefilter.getTime(), + attributes, + }); }); }); @@ -244,38 +272,104 @@ describe('useDiscoverHistogram', () => { it('should try to get the topPanelHeight from storage', async () => { const storage = new LocalStorageMock({}) as unknown as Storage; storage.get = jest.fn(() => 100); - const { result } = await renderUseDiscoverHistogram({ storage }); + const { + hook: { result }, + } = await renderUseDiscoverHistogram({ storage }); expect(storage.get).toHaveBeenCalledWith(HISTOGRAM_HEIGHT_KEY); - expect(result.current.topPanelHeight).toBe(100); + expect(result.current?.topPanelHeight).toBe(100); }); it('should update topPanelHeight when onTopPanelHeightChange is called', async () => { const storage = new LocalStorageMock({}) as unknown as Storage; storage.get = jest.fn(() => 100); storage.set = jest.fn(); - const { result } = await renderUseDiscoverHistogram({ storage }); - expect(result.current.topPanelHeight).toBe(100); + const { + hook: { result }, + } = await renderUseDiscoverHistogram({ storage }); + expect(result.current?.topPanelHeight).toBe(100); act(() => { - result.current.onTopPanelHeightChange(200); + result.current?.onTopPanelHeightChange(200); }); expect(storage.set).toHaveBeenCalledWith(HISTOGRAM_HEIGHT_KEY, 200); - expect(result.current.topPanelHeight).toBe(200); + expect(result.current?.topPanelHeight).toBe(200); }); }); describe('callbacks', () => { it('should update chartHidden when onChartHiddenChange is called', async () => { + const storage = new LocalStorageMock({}) as unknown as Storage; + storage.set = jest.fn(); + const state = { interval: 'auto', hideChart: false, breakdownField: 'extension' }; + const stateContainer = { + setAppState: jest.fn((newState) => { + Object.assign(state, newState); + }), + }; + const session = getSessionServiceMock(); + const session$ = new BehaviorSubject('123'); + session.getSession$.mockReturnValue(session$); + const inspectorAdapters = { + requests: new RequestAdapter(), + lensRequests: new RequestAdapter(), + }; + const { hook } = await renderUseDiscoverHistogram({ + storage, + state, + stateContainer, + searchSessionManager: createSearchSessionMock(session).searchSessionManager, + inspectorAdapters, + }); + act(() => { + hook.result.current?.onChartHiddenChange(false); + }); + expect(inspectorAdapters.lensRequests).toBeDefined(); + expect(storage.set).toHaveBeenCalledWith(CHART_HIDDEN_KEY, false); + expect(stateContainer.setAppState).toHaveBeenCalledWith({ hideChart: false }); + act(() => { + hook.result.current?.onChartHiddenChange(true); + session$.next('321'); + }); + hook.rerender(); + expect(inspectorAdapters.lensRequests).toBeUndefined(); + expect(storage.set).toHaveBeenCalledWith(CHART_HIDDEN_KEY, true); + expect(stateContainer.setAppState).toHaveBeenCalledWith({ hideChart: true }); + }); + + it('should set lensRequests when onChartLoad is called', async () => { + const lensRequests = new RequestAdapter(); + const inspectorAdapters = { + requests: new RequestAdapter(), + lensRequests: undefined as RequestAdapter | undefined, + }; + const { + hook: { result }, + } = await renderUseDiscoverHistogram({ inspectorAdapters }); + expect(inspectorAdapters.lensRequests).toBeUndefined(); + act(() => { + result.current?.onChartLoad({ complete: true, adapters: { requests: lensRequests } }); + }); + expect(inspectorAdapters.lensRequests).toBeDefined(); + }); + + it('should update chart hidden when onChartHiddenChange is called', async () => { const storage = new LocalStorageMock({}) as unknown as Storage; storage.set = jest.fn(); const stateContainer = { setAppState: jest.fn(), }; - const { result } = await renderUseDiscoverHistogram({ + const inspectorAdapters = { + requests: new RequestAdapter(), + lensRequests: new RequestAdapter(), + }; + const { + hook: { result }, + } = await renderUseDiscoverHistogram({ storage, stateContainer, + inspectorAdapters, }); act(() => { - result.current.onChartHiddenChange(true); + result.current?.onChartHiddenChange(true); }); expect(storage.set).toHaveBeenCalledWith(CHART_HIDDEN_KEY, true); expect(stateContainer.setAppState).toHaveBeenCalledWith({ hideChart: true }); @@ -285,13 +379,99 @@ describe('useDiscoverHistogram', () => { const stateContainer = { setAppState: jest.fn(), }; - const { result } = await renderUseDiscoverHistogram({ + const { + hook: { result }, + } = await renderUseDiscoverHistogram({ stateContainer, }); act(() => { - result.current.onTimeIntervalChange('auto'); + result.current?.onTimeIntervalChange('auto'); }); expect(stateContainer.setAppState).toHaveBeenCalledWith({ interval: 'auto' }); }); + + it('should update breakdownField when onBreakdownFieldChange is called', async () => { + const stateContainer = { + setAppState: jest.fn(), + }; + const { + hook: { result }, + } = await renderUseDiscoverHistogram({ + stateContainer, + }); + act(() => { + result.current?.onBreakdownFieldChange( + dataViewWithTimefieldMock.getFieldByName('extension') + ); + }); + expect(stateContainer.setAppState).toHaveBeenCalledWith({ breakdownField: 'extension' }); + }); + + it('should update total hits when onTotalHitsChange is called', async () => { + mockCheckHitCount.mockClear(); + const totalHits$ = new BehaviorSubject({ + fetchStatus: FetchStatus.LOADING, + result: undefined, + }) as DataTotalHits$; + const main$ = new BehaviorSubject({ + fetchStatus: FetchStatus.COMPLETE, + recordRawType: RecordRawType.DOCUMENT, + foundDocuments: true, + }) as DataMain$; + const { hook } = await renderUseDiscoverHistogram({ totalHits$, main$ }); + act(() => { + hook.result.current?.onTotalHitsChange(UnifiedHistogramFetchStatus.complete, 100); + }); + hook.rerender(); + expect(hook.result.current?.hits?.status).toBe(UnifiedHistogramFetchStatus.complete); + expect(hook.result.current?.hits?.total).toBe(100); + expect(totalHits$.value).toEqual({ + fetchStatus: FetchStatus.COMPLETE, + result: 100, + }); + expect(mockCheckHitCount).toHaveBeenCalledWith(main$, 100); + }); + + it('should not update total hits when onTotalHitsChange is called with an error', async () => { + mockCheckHitCount.mockClear(); + const totalHits$ = new BehaviorSubject({ + fetchStatus: FetchStatus.UNINITIALIZED, + result: undefined, + }) as DataTotalHits$; + const { hook } = await renderUseDiscoverHistogram({ totalHits$ }); + const error = new Error('test'); + act(() => { + hook.result.current?.onTotalHitsChange(UnifiedHistogramFetchStatus.error, error); + }); + hook.rerender(); + expect(sendErrorTo).toHaveBeenCalledWith(mockData, totalHits$); + expect(hook.result.current?.hits?.status).toBe(UnifiedHistogramFetchStatus.error); + expect(hook.result.current?.hits?.total).toBeUndefined(); + expect(totalHits$.value).toEqual({ + fetchStatus: FetchStatus.ERROR, + error, + }); + expect(mockCheckHitCount).not.toHaveBeenCalled(); + }); + + it('should not update total hits when onTotalHitsChange is called with a loading status while totalHits$ has a partial status', async () => { + mockCheckHitCount.mockClear(); + const totalHits$ = new BehaviorSubject({ + fetchStatus: FetchStatus.PARTIAL, + result: undefined, + }) as DataTotalHits$; + const { hook } = await renderUseDiscoverHistogram({ totalHits$ }); + act(() => { + hook.result.current?.onTotalHitsChange(UnifiedHistogramFetchStatus.loading, undefined); + }); + hook.rerender(); + expect(hook.result.current?.hits?.status).toBe(UnifiedHistogramFetchStatus.partial); + expect(hook.result.current?.hits?.total).toBeUndefined(); + expect(totalHits$.value).toEqual({ + fetchStatus: FetchStatus.PARTIAL, + result: undefined, + }); + expect(mockCheckHitCount).not.toHaveBeenCalled(); + }); }); }); diff --git a/src/plugins/discover/public/application/main/components/layout/use_discover_histogram.ts b/src/plugins/discover/public/application/main/components/layout/use_discover_histogram.ts index 3032fa13af043..2141ab8cd21ef 100644 --- a/src/plugins/discover/public/application/main/components/layout/use_discover_histogram.ts +++ b/src/plugins/discover/public/application/main/components/layout/use_discover_histogram.ts @@ -6,23 +6,30 @@ * Side Public License, v 1. */ -import type { DataView } from '@kbn/data-views-plugin/common'; +import type { DataView, DataViewField } from '@kbn/data-views-plugin/common'; import type { SavedSearch } from '@kbn/saved-search-plugin/public'; -import { - getVisualizeInformation, - triggerVisualizeActions, -} from '@kbn/unified-field-list-plugin/public'; -import { buildChartData } from '@kbn/unified-histogram-plugin/public'; +import { getVisualizeInformation } from '@kbn/unified-field-list-plugin/public'; import { useCallback, useEffect, useMemo, useState } from 'react'; +import { + UnifiedHistogramFetchStatus, + UnifiedHistogramHitsContext, +} from '@kbn/unified-histogram-plugin/public'; +import type { UnifiedHistogramChartLoadEvent } from '@kbn/unified-histogram-plugin/public'; +import useObservable from 'react-use/lib/useObservable'; +import type { TypedLensByValueInput } from '@kbn/lens-plugin/public'; import { getUiActions } from '../../../../kibana_services'; -import { PLUGIN_ID } from '../../../../../common'; import { useDiscoverServices } from '../../../../hooks/use_discover_services'; import { useDataState } from '../../hooks/use_data_state'; import type { SavedSearchData } from '../../hooks/use_saved_search'; import type { AppState, GetStateReturn } from '../../services/discover_state'; +import { FetchStatus } from '../../../types'; +import type { DiscoverSearchSessionManager } from '../../services/discover_search_session'; +import type { InspectorAdapters } from '../../hooks/use_inspector'; +import { checkHitCount, sendErrorTo } from '../../hooks/use_saved_search_messages'; export const CHART_HIDDEN_KEY = 'discover:chartHidden'; export const HISTOGRAM_HEIGHT_KEY = 'discover:histogramHeight'; +export const HISTOGRAM_BREAKDOWN_FIELD_KEY = 'discover:histogramBreakdownField'; export const useDiscoverHistogram = ({ stateContainer, @@ -32,6 +39,8 @@ export const useDiscoverHistogram = ({ savedSearch, isTimeBased, isPlainRecord, + inspectorAdapters, + searchSessionManager, }: { stateContainer: GetStateReturn; state: AppState; @@ -40,8 +49,10 @@ export const useDiscoverHistogram = ({ savedSearch: SavedSearch; isTimeBased: boolean; isPlainRecord: boolean; + inspectorAdapters: InspectorAdapters; + searchSessionManager: DiscoverSearchSessionManager; }) => { - const { storage, data } = useDiscoverServices(); + const { storage, data, lens } = useDiscoverServices(); /** * Visualize @@ -65,18 +76,19 @@ export const useDiscoverHistogram = ({ }); }, [dataView, savedSearch.columns, timeField]); - const onEditVisualization = useCallback(() => { - if (!timeField) { - return; - } - triggerVisualizeActions( - getUiActions(), - timeField, - savedSearch.columns || [], - PLUGIN_ID, - dataView - ); - }, [dataView, savedSearch.columns, timeField]); + const onEditVisualization = useCallback( + (lensAttributes: TypedLensByValueInput['attributes']) => { + if (!timeField) { + return; + } + lens.navigateToPrefilledEditor({ + id: '', + timeRange: data.query.timefilter.timefilter.getTime(), + attributes: lensAttributes, + }); + }, + [data.query.timefilter.timefilter, lens, timeField] + ); /** * Height @@ -96,17 +108,9 @@ export const useDiscoverHistogram = ({ ); /** - * Other callbacks + * Time interval */ - const onChartHiddenChange = useCallback( - (chartHidden: boolean) => { - storage.set(CHART_HIDDEN_KEY, chartHidden); - stateContainer.setAppState({ hideChart: chartHidden }); - }, - [stateContainer, storage] - ); - const onTimeIntervalChange = useCallback( (newInterval: string) => { stateContainer.setAppState({ interval: newInterval }); @@ -115,9 +119,62 @@ export const useDiscoverHistogram = ({ ); /** - * Data + * Request + */ + + // The searchSessionId will be updated whenever a new search + // is started and will trigger a unified histogram refetch + const searchSessionId = useObservable(searchSessionManager.searchSessionId$); + const request = useMemo( + () => ({ + searchSessionId, + adapter: inspectorAdapters.requests, + }), + [inspectorAdapters.requests, searchSessionId] + ); + + /** + * Total hits */ + const [localHitsContext, setLocalHitsContext] = useState(); + + const onTotalHitsChange = useCallback( + (status: UnifiedHistogramFetchStatus, result?: number | Error) => { + if (result instanceof Error) { + // Display the error and set totalHits$ to an error state + sendErrorTo(data, savedSearchData$.totalHits$)(result); + return; + } + + const { fetchStatus, recordRawType } = savedSearchData$.totalHits$.getValue(); + + // If we have a partial result already, we don't want to update the total hits back to loading + if (fetchStatus === FetchStatus.PARTIAL && status === UnifiedHistogramFetchStatus.loading) { + return; + } + + // Set a local copy of the hits context to pass to unified histogram + setLocalHitsContext({ status, total: result }); + + // Sync the totalHits$ observable with the unified histogram state + savedSearchData$.totalHits$.next({ + fetchStatus: status.toString() as FetchStatus, + result, + recordRawType, + }); + + // Check the hits count to set a partial or no results state + if (status === UnifiedHistogramFetchStatus.complete && typeof result === 'number') { + checkHitCount(savedSearchData$.main$, result); + } + }, + [data, savedSearchData$.main$, savedSearchData$.totalHits$] + ); + + // We only rely on the totalHits$ observable if we don't have a local hits context yet, + // since we only want to show the partial results on the first load, or there will be + // a flickering effect as the loading spinner is quickly shown and hidden again on fetches const { fetchStatus: hitsFetchStatus, result: hitsTotal } = useDataState( savedSearchData$.totalHits$ ); @@ -126,57 +183,96 @@ export const useDiscoverHistogram = ({ () => isPlainRecord ? undefined - : { - status: hitsFetchStatus, + : localHitsContext ?? { + status: hitsFetchStatus.toString() as UnifiedHistogramFetchStatus, total: hitsTotal, }, - [hitsFetchStatus, hitsTotal, isPlainRecord] + [hitsFetchStatus, hitsTotal, isPlainRecord, localHitsContext] ); - const { fetchStatus: chartFetchStatus, response, error } = useDataState(savedSearchData$.charts$); + /** + * Chart + */ - const { bucketInterval, chartData } = useMemo( - () => - buildChartData({ - data, - dataView, - timeInterval: state.interval, - response, - }), - [data, dataView, response, state.interval] + const onChartHiddenChange = useCallback( + (chartHidden: boolean) => { + storage.set(CHART_HIDDEN_KEY, chartHidden); + stateContainer.setAppState({ hideChart: chartHidden }); + }, + [stateContainer, storage] ); + const onChartLoad = useCallback( + (event: UnifiedHistogramChartLoadEvent) => { + // We need to store the Lens request adapter in order to inspect its requests + inspectorAdapters.lensRequests = event.adapters.requests; + }, + [inspectorAdapters] + ); + + const [chartHidden, setChartHidden] = useState(state.hideChart); const chart = useMemo( () => isPlainRecord || !isTimeBased ? undefined : { - status: chartFetchStatus, - hidden: state.hideChart, + hidden: chartHidden, timeInterval: state.interval, - bucketInterval, - data: chartData, - error, }, - [ - bucketInterval, - chartData, - chartFetchStatus, - error, - isPlainRecord, - isTimeBased, - state.hideChart, - state.interval, - ] + [chartHidden, isPlainRecord, isTimeBased, state.interval] + ); + + // Clear the Lens request adapter when the chart is hidden + useEffect(() => { + if (chartHidden || !chart) { + inspectorAdapters.lensRequests = undefined; + } + }, [chart, chartHidden, inspectorAdapters]); + + // state.chartHidden is updated before searchSessionId, which can trigger duplicate + // requests, so instead of using state.chartHidden directly, we update chartHidden + // when searchSessionId changes + useEffect(() => { + setChartHidden(state.hideChart); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [searchSessionId]); + + /** + * Breakdown + */ + + const onBreakdownFieldChange = useCallback( + (breakdownField: DataViewField | undefined) => { + stateContainer.setAppState({ breakdownField: breakdownField?.name }); + }, + [stateContainer] + ); + + const field = useMemo( + () => (state.breakdownField ? dataView.getFieldByName(state.breakdownField) : undefined), + [dataView, state.breakdownField] + ); + + const breakdown = useMemo( + () => (isPlainRecord || !isTimeBased ? undefined : { field }), + [field, isPlainRecord, isTimeBased] ); - return { - topPanelHeight, - hits, - chart, - onEditVisualization: canVisualize ? onEditVisualization : undefined, - onTopPanelHeightChange, - onChartHiddenChange, - onTimeIntervalChange, - }; + // Don't render the unified histogram layout until the first search has been requested + return searchSessionId + ? { + topPanelHeight, + request, + hits, + chart, + breakdown, + onEditVisualization: canVisualize ? onEditVisualization : undefined, + onTopPanelHeightChange, + onChartHiddenChange, + onTimeIntervalChange, + onBreakdownFieldChange, + onTotalHitsChange, + onChartLoad, + } + : undefined; }; diff --git a/src/plugins/discover/public/application/main/components/loading_spinner/discover_field_visualize.stories.tsx b/src/plugins/discover/public/application/main/components/loading_spinner/discover_field_visualize.stories.tsx deleted file mode 100644 index e61ac272c182f..0000000000000 --- a/src/plugins/discover/public/application/main/components/loading_spinner/discover_field_visualize.stories.tsx +++ /dev/null @@ -1,15 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 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 { storiesOf } from '@storybook/react'; -import React from 'react'; -import { LoadingSpinner } from './loading_spinner'; - -storiesOf('components/loading_spinner/LoadingSpinner', module).add('default', () => ( - -)); diff --git a/src/plugins/discover/public/application/main/components/loading_spinner/loading_spinner.scss b/src/plugins/discover/public/application/main/components/loading_spinner/loading_spinner.scss deleted file mode 100644 index a58897e43b615..0000000000000 --- a/src/plugins/discover/public/application/main/components/loading_spinner/loading_spinner.scss +++ /dev/null @@ -1,4 +0,0 @@ -.dscLoading { - text-align: center; - padding: $euiSizeL 0; -} diff --git a/src/plugins/discover/public/application/main/components/loading_spinner/loading_spinner.tsx b/src/plugins/discover/public/application/main/components/loading_spinner/loading_spinner.tsx index 949880d6c27d0..1879b5267bbf5 100644 --- a/src/plugins/discover/public/application/main/components/loading_spinner/loading_spinner.tsx +++ b/src/plugins/discover/public/application/main/components/loading_spinner/loading_spinner.tsx @@ -6,15 +6,32 @@ * Side Public License, v 1. */ -import './loading_spinner.scss'; - import React from 'react'; -import { EuiLoadingSpinner, EuiTitle, EuiSpacer } from '@elastic/eui'; +import { + EuiLoadingSpinner, + EuiTitle, + EuiSpacer, + useEuiPaddingSize, + useEuiBackgroundColor, +} from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; +import { css } from '@emotion/react'; export function LoadingSpinner() { + const loadingSpinnerCss = css` + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + text-align: center; + padding: ${useEuiPaddingSize('l')} 0; + background-color: ${useEuiBackgroundColor('plain')}; + z-index: 3; + `; + return ( -
+

diff --git a/src/plugins/discover/public/application/main/components/sidebar/__stories__/discover_field_details.stories.tsx b/src/plugins/discover/public/application/main/components/sidebar/__stories__/discover_field_details.stories.tsx deleted file mode 100644 index 9b02ffd15a282..0000000000000 --- a/src/plugins/discover/public/application/main/components/sidebar/__stories__/discover_field_details.stories.tsx +++ /dev/null @@ -1,94 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 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 { storiesOf } from '@storybook/react'; -import React from 'react'; -import { KBN_FIELD_TYPES } from '@kbn/field-types'; -import { DataViewField } from '@kbn/data-views-plugin/public'; -import { DataView } from '@kbn/data-views-plugin/public'; -import { DiscoverFieldDetails } from '../discover_field_details'; -import { fieldSpecMap } from './fields'; -import { numericField as field } from './fields'; -import { Bucket } from '../types'; - -const buckets = [ - { count: 1, display: 'Stewart', percent: 50.0, value: 'Stewart' }, - { count: 1, display: 'Perry', percent: 50.0, value: 'Perry' }, -] as Bucket[]; -const details = { buckets, error: '', exists: 1, total: 2, columns: [] }; - -const fieldFormatInstanceType = {}; -const defaultMap = { - [KBN_FIELD_TYPES.NUMBER]: { id: KBN_FIELD_TYPES.NUMBER, params: {} }, -}; - -const fieldFormat = { - getByFieldType: (fieldType: KBN_FIELD_TYPES) => { - return [fieldFormatInstanceType]; - }, - getDefaultConfig: () => { - return defaultMap.number; - }, - defaultMap, -}; - -const scriptedField = new DataViewField({ - name: 'machine.os', - type: 'string', - esTypes: ['long'], - count: 10, - scripted: true, - searchable: true, - aggregatable: true, - readFromDocValues: true, -}); - -const dataView = new DataView({ - spec: { - id: 'logstash-*', - fields: fieldSpecMap, - title: 'logstash-*', - timeFieldName: '@timestamp', - }, - metaFields: ['_id', '_type', '_source'], - shortDotsEnable: false, - // @ts-expect-error - fieldFormats: fieldFormat, -}); - -storiesOf('components/sidebar/DiscoverFieldDetails', module) - .add('default', () => ( -
- { - alert('On add filter clicked'); - }} - /> -
- )) - .add('scripted', () => ( -
- {}} - /> -
- )) - .add('error', () => ( - {}} - /> - )); diff --git a/src/plugins/discover/public/application/main/components/sidebar/__stories__/fields.ts b/src/plugins/discover/public/application/main/components/sidebar/__stories__/fields.ts deleted file mode 100644 index 04f1cb9eb618b..0000000000000 --- a/src/plugins/discover/public/application/main/components/sidebar/__stories__/fields.ts +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 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 { DataViewField, FieldSpec } from '@kbn/data-views-plugin/public'; - -export const fieldSpecMap: Record = { - 'machine.os': { - name: 'machine.os', - esTypes: ['text'], - type: 'string', - aggregatable: false, - searchable: false, - }, - 'machine.os.raw': { - name: 'machine.os.raw', - type: 'string', - esTypes: ['keyword'], - aggregatable: true, - searchable: true, - }, - 'not.filterable': { - name: 'not.filterable', - type: 'string', - esTypes: ['text'], - aggregatable: true, - searchable: false, - }, - bytes: { - name: 'bytes', - type: 'number', - esTypes: ['long'], - aggregatable: true, - searchable: true, - }, -}; - -export const numericField = new DataViewField({ - name: 'bytes', - type: 'number', - esTypes: ['long'], - count: 10, - scripted: false, - searchable: true, - aggregatable: true, - readFromDocValues: true, -}); diff --git a/src/plugins/discover/public/application/main/components/sidebar/discover_field_bucket.scss b/src/plugins/discover/public/application/main/components/sidebar/deprecated_stats/discover_field_bucket.scss similarity index 100% rename from src/plugins/discover/public/application/main/components/sidebar/discover_field_bucket.scss rename to src/plugins/discover/public/application/main/components/sidebar/deprecated_stats/discover_field_bucket.scss diff --git a/src/plugins/discover/public/application/main/components/sidebar/discover_field_bucket.tsx b/src/plugins/discover/public/application/main/components/sidebar/deprecated_stats/discover_field_bucket.tsx similarity index 100% rename from src/plugins/discover/public/application/main/components/sidebar/discover_field_bucket.tsx rename to src/plugins/discover/public/application/main/components/sidebar/deprecated_stats/discover_field_bucket.tsx diff --git a/src/plugins/discover/public/application/main/components/sidebar/deprecated_stats/discover_field_details.test.tsx b/src/plugins/discover/public/application/main/components/sidebar/deprecated_stats/discover_field_details.test.tsx new file mode 100644 index 0000000000000..535459c880988 --- /dev/null +++ b/src/plugins/discover/public/application/main/components/sidebar/deprecated_stats/discover_field_details.test.tsx @@ -0,0 +1,55 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { findTestSubject } from '@elastic/eui/lib/test'; +import { mountWithIntl } from '@kbn/test-jest-helpers'; + +import { DiscoverFieldDetails } from './discover_field_details'; +import { DataViewField } from '@kbn/data-views-plugin/public'; +import { stubDataView, stubLogstashDataView } from '@kbn/data-views-plugin/common/data_view.stub'; +import { BehaviorSubject } from 'rxjs'; +import { FetchStatus } from '../../../../types'; +import { DataDocuments$ } from '../../../hooks/use_saved_search'; +import { getDataTableRecords } from '../../../../../__fixtures__/real_hits'; + +describe('discover sidebar field details', function () { + const onAddFilter = jest.fn(); + const defaultProps = { + dataView: stubDataView, + details: { buckets: [], error: '', exists: 1, total: 2, columns: [] }, + onAddFilter, + }; + const hits = getDataTableRecords(stubLogstashDataView); + const documents$ = new BehaviorSubject({ + fetchStatus: FetchStatus.COMPLETE, + result: hits, + }) as DataDocuments$; + + function mountComponent(field: DataViewField) { + const compProps = { ...defaultProps, field, documents$ }; + return mountWithIntl(); + } + + it('click on addFilter calls the function', function () { + const visualizableField = new DataViewField({ + name: 'bytes', + type: 'number', + esTypes: ['long'], + count: 10, + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }); + const component = mountComponent(visualizableField); + const onAddButton = findTestSubject(component, 'onAddFilterButton'); + onAddButton.simulate('click'); + expect(onAddFilter).toHaveBeenCalledWith('_exists_', visualizableField.name, '+'); + }); +}); diff --git a/src/plugins/discover/public/application/main/components/sidebar/deprecated_stats/discover_field_details.tsx b/src/plugins/discover/public/application/main/components/sidebar/deprecated_stats/discover_field_details.tsx new file mode 100644 index 0000000000000..58db010c025c9 --- /dev/null +++ b/src/plugins/discover/public/application/main/components/sidebar/deprecated_stats/discover_field_details.tsx @@ -0,0 +1,99 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React, { useMemo } from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiText, EuiSpacer, EuiLink, EuiTitle } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { DataViewField, DataView } from '@kbn/data-views-plugin/public'; +import { DiscoverFieldBucket } from './discover_field_bucket'; +import { Bucket, FieldDetails } from './types'; +import { getDetails } from './get_details'; +import { DataDocuments$ } from '../../../hooks/use_saved_search'; +import { FetchStatus } from '../../../../types'; + +interface DiscoverFieldDetailsProps { + /** + * hits fetched from ES, displayed in the doc table + */ + documents$: DataDocuments$; + field: DataViewField; + dataView: DataView; + onAddFilter?: (field: DataViewField | string, value: string, type: '+' | '-') => void; +} + +export function DiscoverFieldDetails({ + documents$, + field, + dataView, + onAddFilter, +}: DiscoverFieldDetailsProps) { + const details: FieldDetails = useMemo(() => { + const data = documents$.getValue(); + const documents = data.fetchStatus === FetchStatus.COMPLETE ? data.result : undefined; + return getDetails(field, documents, dataView); + }, [field, documents$, dataView]); + + if (!details?.error && !details?.buckets) { + return null; + } + + return ( +
+ +
+ {i18n.translate('discover.fieldChooser.discoverField.fieldTopValuesLabel', { + defaultMessage: 'Top 5 values', + })} +
+
+ {details.error && {details.error}} + {!details.error && ( + <> +
+ {details.buckets.map((bucket: Bucket, idx: number) => ( + + ))} +
+ + + {onAddFilter && !dataView.metaFields.includes(field.name) && !field.scripted ? ( + onAddFilter('_exists_', field.name, '+')} + data-test-subj="onAddFilterButton" + > + + + ) : ( + + )} + + + )} +
+ ); +} diff --git a/src/plugins/discover/public/application/main/components/sidebar/lib/field_calculator.js b/src/plugins/discover/public/application/main/components/sidebar/deprecated_stats/field_calculator.js similarity index 100% rename from src/plugins/discover/public/application/main/components/sidebar/lib/field_calculator.js rename to src/plugins/discover/public/application/main/components/sidebar/deprecated_stats/field_calculator.js diff --git a/src/plugins/discover/public/application/main/components/sidebar/lib/field_calculator.test.ts b/src/plugins/discover/public/application/main/components/sidebar/deprecated_stats/field_calculator.test.ts similarity index 100% rename from src/plugins/discover/public/application/main/components/sidebar/lib/field_calculator.test.ts rename to src/plugins/discover/public/application/main/components/sidebar/deprecated_stats/field_calculator.test.ts diff --git a/src/plugins/discover/public/application/main/components/sidebar/lib/get_details.ts b/src/plugins/discover/public/application/main/components/sidebar/deprecated_stats/get_details.ts similarity index 100% rename from src/plugins/discover/public/application/main/components/sidebar/lib/get_details.ts rename to src/plugins/discover/public/application/main/components/sidebar/deprecated_stats/get_details.ts diff --git a/src/plugins/discover/public/application/main/components/sidebar/string_progress_bar.tsx b/src/plugins/discover/public/application/main/components/sidebar/deprecated_stats/string_progress_bar.tsx similarity index 100% rename from src/plugins/discover/public/application/main/components/sidebar/string_progress_bar.tsx rename to src/plugins/discover/public/application/main/components/sidebar/deprecated_stats/string_progress_bar.tsx diff --git a/src/plugins/discover/public/application/main/components/sidebar/deprecated_stats/types.ts b/src/plugins/discover/public/application/main/components/sidebar/deprecated_stats/types.ts new file mode 100644 index 0000000000000..1f7d40418fe7b --- /dev/null +++ b/src/plugins/discover/public/application/main/components/sidebar/deprecated_stats/types.ts @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export interface FieldDetails { + error: string; + exists: number; + total: number; + buckets: Bucket[]; +} + +export interface Bucket { + display: string; + value: string; + percent: number; + count: number; +} diff --git a/src/plugins/discover/public/application/main/components/sidebar/discover_field.test.tsx b/src/plugins/discover/public/application/main/components/sidebar/discover_field.test.tsx index c147289af983c..82e3e462dbd3a 100644 --- a/src/plugins/discover/public/application/main/components/sidebar/discover_field.test.tsx +++ b/src/plugins/discover/public/application/main/components/sidebar/discover_field.test.tsx @@ -9,18 +9,22 @@ import { act } from 'react-dom/test-utils'; import { EuiPopover, EuiProgress, EuiButtonIcon } from '@elastic/eui'; import React from 'react'; +import { BehaviorSubject } from 'rxjs'; import { findTestSubject } from '@elastic/eui/lib/test'; import { mountWithIntl } from '@kbn/test-jest-helpers'; -import { chartPluginMock } from '@kbn/charts-plugin/public/mocks'; -import { dataViewPluginMocks } from '@kbn/data-views-plugin/public/mocks'; -import { fieldFormatsServiceMock } from '@kbn/field-formats-plugin/public/mocks'; -import { dataPluginMock } from '@kbn/data-plugin/public/mocks'; import { DiscoverField, DiscoverFieldProps } from './discover_field'; import { DataViewField } from '@kbn/data-views-plugin/public'; import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; import { stubDataView } from '@kbn/data-views-plugin/common/data_view.stub'; import { DiscoverAppStateProvider } from '../../services/discover_app_state_container'; import { getDiscoverStateMock } from '../../../../__mocks__/discover_state.mock'; +import { FetchStatus } from '../../../types'; +import { DataDocuments$ } from '../../hooks/use_saved_search'; +import { getDataTableRecords } from '../../../../__fixtures__/real_hits'; +import * as DetailsUtil from './deprecated_stats/get_details'; +import { createDiscoverServicesMock } from '../../../../__mocks__/services'; + +jest.spyOn(DetailsUtil, 'getDetails'); jest.mock('@kbn/unified-field-list-plugin/public/services/field_stats', () => ({ loadFieldStats: jest.fn().mockResolvedValue({ @@ -42,8 +46,6 @@ jest.mock('@kbn/unified-field-list-plugin/public/services/field_stats', () => ({ }), })); -const dataServiceMock = dataPluginMock.createStartContract(); - jest.mock('../../../../kibana_services', () => ({ getUiActions: jest.fn(() => { return { @@ -80,10 +82,16 @@ async function getComponent({ const dataView = stubDataView; dataView.toSpec = () => ({}); + const hits = getDataTableRecords(dataView); + const documents$ = new BehaviorSubject({ + fetchStatus: FetchStatus.COMPLETE, + result: hits, + }) as DataDocuments$; + const props: DiscoverFieldProps = { + documents$, dataView: stubDataView, field: finalField, - getDetails: jest.fn(() => ({ buckets: [], error: '', exists: 1, total: 2 })), ...(onAddFilterExists && { onAddFilter: jest.fn() }), onAddField: jest.fn(), onEditField: jest.fn(), @@ -93,11 +101,7 @@ async function getComponent({ contextualFields: [], }; const services = { - history: () => ({ - location: { - search: '', - }, - }), + ...createDiscoverServicesMock(), capabilities: { visualize: { show: true, @@ -113,29 +117,6 @@ async function getComponent({ } }, }, - data: { - ...dataServiceMock, - query: { - ...dataServiceMock.query, - timefilter: { - ...dataServiceMock.query.timefilter, - timefilter: { - ...dataServiceMock.query.timefilter.timefilter, - getAbsoluteTime: () => ({ - from: '2021-08-31T22:00:00.000Z', - to: '2022-09-01T09:16:29.553Z', - }), - }, - }, - getState: () => ({ - query: { query: '', language: 'lucene' }, - filters: [], - }), - }, - }, - dataViews: dataViewPluginMocks.createStartContract(), - fieldFormats: fieldFormatsServiceMock.createStartContract(), - charts: chartPluginMock.createSetupContract(), }; const appStateContainer = getDiscoverStateMock({ isTimeBased: true }).appStateContainer; appStateContainer.set({ @@ -156,6 +137,10 @@ async function getComponent({ } describe('discover sidebar field', function () { + beforeEach(() => { + (DetailsUtil.getDetails as jest.Mock).mockClear(); + }); + it('should allow selecting fields', async function () { const { comp, props } = await getComponent({}); findTestSubject(comp, 'fieldToggle-bytes').simulate('click'); @@ -166,14 +151,15 @@ describe('discover sidebar field', function () { findTestSubject(comp, 'fieldToggle-bytes').simulate('click'); expect(props.onRemoveField).toHaveBeenCalledWith('bytes'); }); - it('should trigger getDetails', async function () { + it('should trigger getDetails for showing the deprecated field stats', async function () { const { comp, props } = await getComponent({ selected: true, showFieldStats: true, showLegacyFieldTopValues: true, }); findTestSubject(comp, 'field-bytes-showDetails').simulate('click'); - expect(props.getDetails).toHaveBeenCalledWith(props.field); + expect(DetailsUtil.getDetails).toHaveBeenCalledTimes(1); + expect(findTestSubject(comp, `discoverFieldDetails-${props.field.name}`).exists()).toBeTruthy(); }); it('should not allow clicking on _source', async function () { const field = new DataViewField({ @@ -184,13 +170,13 @@ describe('discover sidebar field', function () { aggregatable: true, readFromDocValues: true, }); - const { comp, props } = await getComponent({ + const { comp } = await getComponent({ selected: true, field, showLegacyFieldTopValues: true, }); findTestSubject(comp, 'field-_source-showDetails').simulate('click'); - expect(props.getDetails).not.toHaveBeenCalled(); + expect(DetailsUtil.getDetails).not.toHaveBeenCalledWith(); }); it('displays warning for conflicting fields', async function () { const field = new DataViewField({ @@ -209,16 +195,16 @@ describe('discover sidebar field', function () { expect(dscField.find('.kbnFieldButton__infoIcon').length).toEqual(1); }); it('should not execute getDetails when rendered, since it can be expensive', async function () { - const { props } = await getComponent({}); - expect(props.getDetails).toHaveBeenCalledTimes(0); + await getComponent({}); + expect(DetailsUtil.getDetails).toHaveBeenCalledTimes(0); }); it('should execute getDetails when show details is requested', async function () { - const { props, comp } = await getComponent({ + const { comp } = await getComponent({ showFieldStats: true, showLegacyFieldTopValues: true, }); findTestSubject(comp, 'field-bytes-showDetails').simulate('click'); - expect(props.getDetails).toHaveBeenCalledTimes(1); + expect(DetailsUtil.getDetails).toHaveBeenCalledTimes(1); }); it('should not return the popover if onAddFilter is not provided', async function () { const field = new DataViewField({ diff --git a/src/plugins/discover/public/application/main/components/sidebar/discover_field.tsx b/src/plugins/discover/public/application/main/components/sidebar/discover_field.tsx index 5513483b2b03c..18cbabf97e058 100644 --- a/src/plugins/discover/public/application/main/components/sidebar/discover_field.tsx +++ b/src/plugins/discover/public/application/main/components/sidebar/discover_field.tsx @@ -9,7 +9,14 @@ import './discover_field.scss'; import React, { useState, useCallback, memo, useMemo } from 'react'; -import { EuiButtonIcon, EuiToolTip, EuiTitle, EuiIcon, EuiSpacer } from '@elastic/eui'; +import { + EuiButtonIcon, + EuiToolTip, + EuiTitle, + EuiIcon, + EuiSpacer, + EuiHighlight, +} from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { UiCounterMetricType } from '@kbn/analytics'; import classNames from 'classnames'; @@ -23,12 +30,12 @@ import { } from '@kbn/unified-field-list-plugin/public'; import { DiscoverFieldStats } from './discover_field_stats'; import { getTypeForFieldIcon } from '../../../../utils/get_type_for_field_icon'; -import { DiscoverFieldDetails } from './discover_field_details'; -import { FieldDetails } from './types'; +import { DiscoverFieldDetails } from './deprecated_stats/discover_field_details'; import { getFieldTypeName } from '../../../../utils/get_field_type_name'; import { useDiscoverServices } from '../../../../hooks/use_discover_services'; import { SHOW_LEGACY_FIELD_TOP_VALUES, PLUGIN_ID } from '../../../../../common'; import { getUiActions } from '../../../../kibana_services'; +import { type DataDocuments$ } from '../../hooks/use_saved_search'; function wrapOnDot(str?: string) { // u200B is a non-width white-space character, which allows @@ -64,24 +71,31 @@ const DiscoverFieldTypeIcon: React.FC<{ field: DataViewField }> = memo(({ field ); }); -const FieldName: React.FC<{ field: DataViewField }> = memo(({ field }) => { - const title = - field.displayName !== field.name - ? i18n.translate('discover.field.title', { - defaultMessage: '{fieldName} ({fieldDisplayName})', - values: { - fieldName: field.name, - fieldDisplayName: field.displayName, - }, - }) - : field.displayName; +const FieldName: React.FC<{ field: DataViewField; highlight?: string }> = memo( + ({ field, highlight }) => { + const title = + field.displayName !== field.name + ? i18n.translate('discover.field.title', { + defaultMessage: '{fieldName} ({fieldDisplayName})', + values: { + fieldName: field.name, + fieldDisplayName: field.displayName, + }, + }) + : field.displayName; - return ( - - {wrapOnDot(field.displayName)} - - ); -}); + return ( + + {wrapOnDot(field.displayName)} + + ); + } +); interface ActionButtonProps { field: DataViewField; @@ -128,6 +142,7 @@ const ActionButton: React.FC = memo( } else { return ( ; toggleDisplay: (field: DataViewField) => void; alwaysShowActionButton: boolean; - isDocumentRecord: boolean; } const MultiFields: React.FC = memo( - ({ multiFields, toggleDisplay, alwaysShowActionButton, isDocumentRecord }) => ( + ({ multiFields, toggleDisplay, alwaysShowActionButton }) => (
@@ -184,7 +198,7 @@ const MultiFields: React.FC = memo( className="dscSidebarItem dscSidebarItem--multi" isActive={false} dataTestSubj={`field-${entry.field.name}-showDetails`} - fieldIcon={isDocumentRecord && } + fieldIcon={} fieldAction={ = memo( ); export interface DiscoverFieldProps { + /** + * hits fetched from ES, displayed in the doc table + */ + documents$: DataDocuments$; /** * Determines whether add/remove button is displayed not only when focused */ @@ -227,10 +245,6 @@ export interface DiscoverFieldProps { * @param fieldName */ onRemoveField: (fieldName: string) => void; - /** - * Retrieve details data for the field - */ - getDetails: (field: DataViewField) => FieldDetails; /** * Determines whether the field is selected */ @@ -264,16 +278,22 @@ export interface DiscoverFieldProps { * Columns */ contextualFields: string[]; + + /** + * Search by field name + */ + highlight?: string; } function DiscoverFieldComponent({ + documents$, alwaysShowActionButton = false, field, + highlight, dataView, onAddField, onRemoveField, onAddFilter, - getDetails, selected, trackUiMetric, multiFields, @@ -345,7 +365,7 @@ function DiscoverFieldComponent({ size="s" className="dscSidebarItem" dataTestSubj={`field-${field.name}-showDetails`} - fieldIcon={isDocumentRecord && } + fieldIcon={} fieldAction={ } + fieldIcon={} fieldAction={ } - fieldName={} + fieldName={} fieldInfoIcon={field.type === 'conflict' && } /> ); @@ -389,24 +409,15 @@ function DiscoverFieldComponent({ return ( <> - {showLegacyFieldStats ? ( + {showLegacyFieldStats ? ( // TODO: Deprecate and remove after ~v8.7 <> {showFieldStats && ( - <> - -
- {i18n.translate('discover.fieldChooser.discoverField.fieldTopValuesLabel', { - defaultMessage: 'Top 5 values', - })} -
-
- - + )} ) : ( @@ -425,7 +436,6 @@ function DiscoverFieldComponent({ multiFields={multiFields} alwaysShowActionButton={alwaysShowActionButton} toggleDisplay={toggleDisplay} - isDocumentRecord={isDocumentRecord} /> )} diff --git a/src/plugins/discover/public/application/main/components/sidebar/discover_field_details.test.tsx b/src/plugins/discover/public/application/main/components/sidebar/discover_field_details.test.tsx deleted file mode 100644 index 9338bd36ceab2..0000000000000 --- a/src/plugins/discover/public/application/main/components/sidebar/discover_field_details.test.tsx +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import React from 'react'; -import { findTestSubject } from '@elastic/eui/lib/test'; -import { mountWithIntl } from '@kbn/test-jest-helpers'; - -import { DiscoverFieldDetails } from './discover_field_details'; -import { DataViewField } from '@kbn/data-views-plugin/public'; -import { stubDataView } from '@kbn/data-views-plugin/common/data_view.stub'; - -describe('discover sidebar field details', function () { - const onAddFilter = jest.fn(); - const defaultProps = { - dataView: stubDataView, - details: { buckets: [], error: '', exists: 1, total: 2, columns: [] }, - onAddFilter, - }; - - function mountComponent(field: DataViewField) { - const compProps = { ...defaultProps, field }; - return mountWithIntl(); - } - - it('click on addFilter calls the function', function () { - const visualizableField = new DataViewField({ - name: 'bytes', - type: 'number', - esTypes: ['long'], - count: 10, - scripted: false, - searchable: true, - aggregatable: true, - readFromDocValues: true, - }); - const component = mountComponent(visualizableField); - const onAddButton = findTestSubject(component, 'onAddFilterButton'); - onAddButton.simulate('click'); - expect(onAddFilter).toHaveBeenCalledWith('_exists_', visualizableField.name, '+'); - }); -}); diff --git a/src/plugins/discover/public/application/main/components/sidebar/discover_field_details.tsx b/src/plugins/discover/public/application/main/components/sidebar/discover_field_details.tsx deleted file mode 100644 index 69e5c01df07e5..0000000000000 --- a/src/plugins/discover/public/application/main/components/sidebar/discover_field_details.tsx +++ /dev/null @@ -1,75 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import React from 'react'; -import { EuiText, EuiSpacer, EuiLink } from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n-react'; -import { DataViewField, DataView } from '@kbn/data-views-plugin/public'; -import { DiscoverFieldBucket } from './discover_field_bucket'; -import { Bucket, FieldDetails } from './types'; - -interface DiscoverFieldDetailsProps { - field: DataViewField; - dataView: DataView; - details: FieldDetails; - onAddFilter?: (field: DataViewField | string, value: string, type: '+' | '-') => void; -} - -export function DiscoverFieldDetails({ - field, - dataView, - details, - onAddFilter, -}: DiscoverFieldDetailsProps) { - return ( - <> - {details.error && {details.error}} - {!details.error && ( - <> -
- {details.buckets.map((bucket: Bucket, idx: number) => ( - - ))} -
- - - {onAddFilter && !dataView.metaFields.includes(field.name) && !field.scripted ? ( - onAddFilter('_exists_', field.name, '+')} - data-test-subj="onAddFilterButton" - > - - - ) : ( - - )} - - - )} - - ); -} diff --git a/src/plugins/discover/public/application/main/components/sidebar/discover_field_search.test.tsx b/src/plugins/discover/public/application/main/components/sidebar/discover_field_search.test.tsx index 55ad5eadaf81d..eafe3fec1eeaf 100644 --- a/src/plugins/discover/public/application/main/components/sidebar/discover_field_search.test.tsx +++ b/src/plugins/discover/public/application/main/components/sidebar/discover_field_search.test.tsx @@ -44,6 +44,7 @@ describe('DiscoverFieldSearch', () => { const input = findTestSubject(component, 'fieldFilterSearchInput'); input.simulate('change', { target: { value: 'new filter' } }); expect(defaultProps.onChange).toBeCalledTimes(1); + expect(defaultProps.onChange).toHaveBeenCalledWith('name', 'new filter'); }); test('change in active filters should change facet selection and call onChange', () => { @@ -97,30 +98,17 @@ describe('DiscoverFieldSearch', () => { expect(badge.text()).toEqual('1'); }); - test('change in missing fields switch should not change filter count', () => { - const component = mountComponent(); - const btn = findTestSubject(component, 'toggleFieldFilterButton'); - btn.simulate('click'); - const badge = btn.find('.euiNotificationBadge').last(); - expect(badge.text()).toEqual('0'); - const missingSwitch = findTestSubject(component, 'missingSwitch'); - missingSwitch.simulate('change', { target: { value: false } }); - expect(badge.text()).toEqual('0'); - }); - test('change in filters triggers onChange', () => { const onChange = jest.fn(); const component = mountComponent({ ...defaultProps, ...{ onChange } }); const btn = findTestSubject(component, 'toggleFieldFilterButton'); btn.simulate('click'); const aggregtableButtonGroup = findButtonGroup(component, 'aggregatable'); - const missingSwitch = findTestSubject(component, 'missingSwitch'); act(() => { // @ts-expect-error (aggregtableButtonGroup.props() as EuiButtonGroupProps).onChange('aggregatable-true', null); }); - missingSwitch.simulate('click'); - expect(onChange).toBeCalledTimes(2); + expect(onChange).toBeCalledTimes(1); }); test('change in type filters triggers onChange with appropriate value', () => { diff --git a/src/plugins/discover/public/application/main/components/sidebar/discover_field_search.tsx b/src/plugins/discover/public/application/main/components/sidebar/discover_field_search.tsx index 59ba2833d94f5..8d7103f70efe9 100644 --- a/src/plugins/discover/public/application/main/components/sidebar/discover_field_search.tsx +++ b/src/plugins/discover/public/application/main/components/sidebar/discover_field_search.tsx @@ -17,11 +17,8 @@ import { EuiFlexGroup, EuiFlexItem, EuiPopover, - EuiPopoverFooter, EuiPopoverTitle, EuiSelect, - EuiSwitch, - EuiSwitchEvent, EuiForm, EuiFormRow, EuiButtonGroup, @@ -43,7 +40,6 @@ export interface State { searchable: string; aggregatable: string; type: string; - missing: boolean; [index: string]: string | boolean; } @@ -68,6 +64,11 @@ export interface Props { * is text base lang mode */ isPlainRecord: boolean; + + /** + * For a11y + */ + fieldSearchDescriptionId?: string; } interface FieldTypeTableItem { @@ -86,6 +87,7 @@ export function DiscoverFieldSearch({ types, presentFieldTypes, isPlainRecord, + fieldSearchDescriptionId, }: Props) { const searchPlaceholder = i18n.translate('discover.fieldChooser.searchPlaceHolder', { defaultMessage: 'Search field names', @@ -112,7 +114,6 @@ export function DiscoverFieldSearch({ searchable: 'any', aggregatable: 'any', type: 'any', - missing: true, }); const { docLinks } = useDiscoverServices(); @@ -191,7 +192,7 @@ export function DiscoverFieldSearch({ }; const isFilterActive = (name: string, filterValue: string | boolean) => { - return name !== 'missing' && filterValue !== 'any'; + return filterValue !== 'any'; }; const handleValueChange = (name: string, filterValue: string | boolean) => { @@ -214,11 +215,6 @@ export function DiscoverFieldSearch({ setActiveFiltersCount(activeFiltersCount + diff); }; - const handleMissingChange = (e: EuiSwitchEvent) => { - const missingValue = e.target.checked; - handleValueChange('missing', missingValue); - }; - const buttonContent = ( { - return ( - - - - ); - }; - const selectionPanel = (
@@ -353,10 +334,11 @@ export function DiscoverFieldSearch({ onChange('name', event.currentTarget.value)} + onChange={(event) => onChange('name', event.target.value)} placeholder={searchPlaceholder} value={value} /> @@ -384,7 +366,6 @@ export function DiscoverFieldSearch({ })} {selectionPanel} - {footer()} = React.memo( ({ field, dataView, multiFields, onAddFilter }) => { const services = useDiscoverServices(); - const dateRange = services.data?.query?.timefilter.timefilter.getAbsoluteTime(); const querySubscriberResult = useQuerySubscriber({ data: services.data, }); @@ -38,7 +38,7 @@ export const DiscoverFieldStats: React.FC = React.memo( [field, multiFields] ); - if (!dateRange || !querySubscriberResult.query || !querySubscriberResult.filters) { + if (!hasQuerySubscriberData(querySubscriberResult)) { return null; } @@ -47,8 +47,8 @@ export const DiscoverFieldStats: React.FC = React.memo( services={services} query={querySubscriberResult.query} filters={querySubscriberResult.filters} - fromDate={dateRange.from} - toDate={dateRange.to} + fromDate={querySubscriberResult.fromDate} + toDate={querySubscriberResult.toDate} dataViewOrDataViewId={dataView} field={fieldForStats} data-test-subj="dscFieldStats" diff --git a/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar.scss b/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar.scss index 4a0f048947706..d7190b61e33f3 100644 --- a/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar.scss +++ b/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar.scss @@ -31,20 +31,6 @@ align-items: center; } -.dscFieldList { - padding: 0 $euiSizeXS $euiSizeXS; -} - -.dscFieldListHeader { - padding: $euiSizeS $euiSizeS 0 $euiSizeS; - background-color: lightOrDarkTheme(tint($euiColorPrimary, 90%), $euiColorLightShade); -} - -.dscFieldList--popular { - padding-bottom: $euiSizeS; - background-color: lightOrDarkTheme(tint($euiColorPrimary, 90%), $euiColorLightShade); -} - .dscSidebarItem { &:hover, &:focus-within, diff --git a/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar.test.tsx b/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar.test.tsx index 0e7e3b22ac3b3..ad59bad82aeb8 100644 --- a/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar.test.tsx +++ b/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar.test.tsx @@ -6,30 +6,38 @@ * Side Public License, v 1. */ import { ReactWrapper } from 'enzyme'; +import { act } from 'react-dom/test-utils'; import { findTestSubject } from '@elastic/eui/lib/test'; import { Action } from '@kbn/ui-actions-plugin/public'; import { getDataTableRecords } from '../../../../__fixtures__/real_hits'; import { mountWithIntl } from '@kbn/test-jest-helpers'; import React from 'react'; -import { DiscoverSidebarProps } from './discover_sidebar'; +import { + DiscoverSidebarComponent as DiscoverSidebar, + DiscoverSidebarProps, +} from './discover_sidebar'; import { DataViewListItem } from '@kbn/data-views-plugin/public'; - +import type { AggregateQuery, Query } from '@kbn/es-query'; import { getDefaultFieldFilter } from './lib/field_filter'; -import { DiscoverSidebarComponent as DiscoverSidebar } from './discover_sidebar'; -import { discoverServiceMock as mockDiscoverServices } from '../../../../__mocks__/services'; +import { createDiscoverServicesMock } from '../../../../__mocks__/services'; import { stubLogstashDataView } from '@kbn/data-plugin/common/stubs'; import { VIEW_MODE } from '../../../../components/view_mode_toggle'; import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; import { BehaviorSubject } from 'rxjs'; import { FetchStatus } from '../../../types'; -import { AvailableFields$ } from '../../hooks/use_saved_search'; +import { AvailableFields$, DataDocuments$ } from '../../hooks/use_saved_search'; import { getDiscoverStateMock } from '../../../../__mocks__/discover_state.mock'; import { DiscoverAppStateProvider } from '../../services/discover_app_state_container'; +import * as ExistingFieldsHookApi from '@kbn/unified-field-list-plugin/public/hooks/use_existing_fields'; +import { ExistenceFetchStatus } from '@kbn/unified-field-list-plugin/public'; +import { getDataViewFieldList } from './lib/get_data_view_field_list'; const mockGetActions = jest.fn>>, [string, { fieldName: string }]>( () => Promise.resolve([]) ); +jest.spyOn(ExistingFieldsHookApi, 'useExistingFieldsReader'); + jest.mock('../../../../kibana_services', () => ({ getUiActions: () => ({ getTriggerCompatibleActions: mockGetActions, @@ -41,12 +49,6 @@ function getCompProps(): DiscoverSidebarProps { dataView.toSpec = jest.fn(() => ({})); const hits = getDataTableRecords(dataView); - const dataViewList = [ - { id: '0', title: 'b' } as DataViewListItem, - { id: '1', title: 'a' } as DataViewListItem, - { id: '2', title: 'c' } as DataViewListItem, - ]; - const fieldCounts: Record = {}; for (const hit of hits) { @@ -54,16 +56,36 @@ function getCompProps(): DiscoverSidebarProps { fieldCounts[key] = (fieldCounts[key] || 0) + 1; } } + + const allFields = getDataViewFieldList(dataView, fieldCounts, false); + + (ExistingFieldsHookApi.useExistingFieldsReader as jest.Mock).mockClear(); + (ExistingFieldsHookApi.useExistingFieldsReader as jest.Mock).mockImplementation(() => ({ + hasFieldData: (dataViewId: string, fieldName: string) => { + return dataViewId === dataView.id && Object.keys(fieldCounts).includes(fieldName); + }, + getFieldsExistenceStatus: (dataViewId: string) => { + return dataViewId === dataView.id + ? ExistenceFetchStatus.succeeded + : ExistenceFetchStatus.unknown; + }, + isFieldsExistenceInfoUnavailable: (dataViewId: string) => dataViewId !== dataView.id, + })); + const availableFields$ = new BehaviorSubject({ fetchStatus: FetchStatus.COMPLETE, fields: [] as string[], }) as AvailableFields$; + const documents$ = new BehaviorSubject({ + fetchStatus: FetchStatus.COMPLETE, + result: hits, + }) as DataDocuments$; + return { columns: ['extension'], - fieldCounts, - documents: hits, - dataViewList, + allFields, + dataViewList: [dataView as DataViewListItem], onChangeDataView: jest.fn(), onAddFilter: jest.fn(), onAddField: jest.fn(), @@ -77,37 +99,38 @@ function getCompProps(): DiscoverSidebarProps { viewMode: VIEW_MODE.DOCUMENT_LEVEL, createNewDataView: jest.fn(), onDataViewCreated: jest.fn(), + documents$, availableFields$, useNewFieldsApi: true, + showFieldList: true, + isAffectedByGlobalFilter: false, }; } -function getAppStateContainer() { +function getAppStateContainer({ query }: { query?: Query | AggregateQuery }) { const appStateContainer = getDiscoverStateMock({ isTimeBased: true }).appStateContainer; appStateContainer.set({ - query: { query: '', language: 'lucene' }, + query: query ?? { query: '', language: 'lucene' }, filters: [], }); return appStateContainer; } -describe('discover sidebar', function () { - let props: DiscoverSidebarProps; +async function mountComponent( + props: DiscoverSidebarProps, + appStateParams: { query?: Query | AggregateQuery } = {} +): Promise> { let comp: ReactWrapper; + const mockedServices = createDiscoverServicesMock(); + mockedServices.data.dataViews.getIdsWithTitle = jest.fn(async () => props.dataViewList); + mockedServices.data.dataViews.get = jest.fn().mockImplementation(async (id) => { + return [props.selectedDataView].find((d) => d!.id === id); + }); - beforeAll(async () => { - props = getCompProps(); - mockDiscoverServices.data.dataViews.getIdsWithTitle = jest - .fn() - .mockReturnValue(props.dataViewList); - mockDiscoverServices.data.dataViews.get = jest.fn().mockImplementation((id) => { - const dataView = props.dataViewList.find((d) => d.id === id); - return { ...dataView, isPersisted: () => true }; - }); - + await act(async () => { comp = await mountWithIntl( - - + + @@ -117,24 +140,50 @@ describe('discover sidebar', function () { await comp.update(); }); - it('should have Selected Fields and Available Fields with Popular Fields sections', function () { - const popular = findTestSubject(comp, 'fieldList-popular'); - const selected = findTestSubject(comp, 'fieldList-selected'); - const unpopular = findTestSubject(comp, 'fieldList-unpopular'); - expect(popular.children().length).toBe(1); - expect(unpopular.children().length).toBe(6); - expect(selected.children().length).toBe(1); + await comp!.update(); + + return comp!; +} + +describe('discover sidebar', function () { + let props: DiscoverSidebarProps; + + beforeEach(async () => { + props = getCompProps(); + }); + + it('should hide field list', async function () { + const comp = await mountComponent({ + ...props, + showFieldList: false, + }); + expect(findTestSubject(comp, 'fieldListGroupedFieldGroups').exists()).toBe(false); + }); + it('should have Selected Fields and Available Fields with Popular Fields sections', async function () { + const comp = await mountComponent(props); + const popularFieldsCount = findTestSubject(comp, 'fieldListGroupedPopularFields-count'); + const selectedFieldsCount = findTestSubject(comp, 'fieldListGroupedSelectedFields-count'); + const availableFieldsCount = findTestSubject(comp, 'fieldListGroupedAvailableFields-count'); + expect(popularFieldsCount.text()).toBe('4'); + expect(availableFieldsCount.text()).toBe('3'); + expect(selectedFieldsCount.text()).toBe('1'); + expect(findTestSubject(comp, 'fieldListGroupedFieldGroups').exists()).toBe(true); }); - it('should allow selecting fields', function () { - findTestSubject(comp, 'fieldToggle-bytes').simulate('click'); + it('should allow selecting fields', async function () { + const comp = await mountComponent(props); + const availableFields = findTestSubject(comp, 'fieldListGroupedAvailableFields'); + findTestSubject(availableFields, 'fieldToggle-bytes').simulate('click'); expect(props.onAddField).toHaveBeenCalledWith('bytes'); }); - it('should allow deselecting fields', function () { - findTestSubject(comp, 'fieldToggle-extension').simulate('click'); + it('should allow deselecting fields', async function () { + const comp = await mountComponent(props); + const availableFields = findTestSubject(comp, 'fieldListGroupedAvailableFields'); + findTestSubject(availableFields, 'fieldToggle-extension').simulate('click'); expect(props.onRemoveField).toHaveBeenCalledWith('extension'); }); - it('should render "Add a field" button', () => { + it('should render "Add a field" button', async () => { + const comp = await mountComponent(props); const addFieldButton = findTestSubject(comp, 'dataView-add-field_btn'); expect(addFieldButton.length).toBe(1); addFieldButton.simulate('click'); @@ -142,8 +191,11 @@ describe('discover sidebar', function () { }); it('should render "Edit field" button', async () => { - findTestSubject(comp, 'field-bytes').simulate('click'); - await new Promise((resolve) => setTimeout(resolve, 0)); + const comp = await mountComponent(props); + const availableFields = findTestSubject(comp, 'fieldListGroupedAvailableFields'); + await act(async () => { + findTestSubject(availableFields, 'field-bytes').simulate('click'); + }); await comp.update(); const editFieldButton = findTestSubject(comp, 'discoverFieldListPanelEdit-bytes'); expect(editFieldButton.length).toBe(1); @@ -151,29 +203,27 @@ describe('discover sidebar', function () { expect(props.editField).toHaveBeenCalledWith('bytes'); }); - it('should not render Add/Edit field buttons in viewer mode', () => { - const compInViewerMode = mountWithIntl( - - - - - - ); + it('should not render Add/Edit field buttons in viewer mode', async () => { + const compInViewerMode = await mountComponent({ + ...getCompProps(), + editField: undefined, + }); const addFieldButton = findTestSubject(compInViewerMode, 'dataView-add-field_btn'); expect(addFieldButton.length).toBe(0); - findTestSubject(comp, 'field-bytes').simulate('click'); + const availableFields = findTestSubject(compInViewerMode, 'fieldListGroupedAvailableFields'); + await act(async () => { + findTestSubject(availableFields, 'field-bytes').simulate('click'); + }); const editFieldButton = findTestSubject(compInViewerMode, 'discoverFieldListPanelEdit-bytes'); expect(editFieldButton.length).toBe(0); }); it('should render buttons in data view picker correctly', async () => { - const compWithPicker = mountWithIntl( - - - - - - ); + const propsWithPicker = { + ...getCompProps(), + showDataViewPicker: true, + }; + const compWithPicker = await mountComponent(propsWithPicker); // open data view picker findTestSubject(compWithPicker, 'dataView-switch-link').simulate('click'); expect(findTestSubject(compWithPicker, 'changeDataViewPopover').length).toBe(1); @@ -184,27 +234,21 @@ describe('discover sidebar', function () { ); expect(addFieldButtonInDataViewPicker.length).toBe(1); addFieldButtonInDataViewPicker.simulate('click'); - expect(props.editField).toHaveBeenCalledWith(); + expect(propsWithPicker.editField).toHaveBeenCalledWith(); // click "Create a data view" const createDataViewButton = findTestSubject(compWithPicker, 'dataview-create-new'); expect(createDataViewButton.length).toBe(1); createDataViewButton.simulate('click'); - expect(props.createNewDataView).toHaveBeenCalled(); + expect(propsWithPicker.createNewDataView).toHaveBeenCalled(); }); it('should not render buttons in data view picker when in viewer mode', async () => { - const compWithPickerInViewerMode = mountWithIntl( - - - - - - ); + const compWithPickerInViewerMode = await mountComponent({ + ...getCompProps(), + showDataViewPicker: true, + editField: undefined, + createNewDataView: undefined, + }); // open data view picker findTestSubject(compWithPickerInViewerMode, 'dataView-switch-link').simulate('click'); expect(findTestSubject(compWithPickerInViewerMode, 'changeDataViewPopover').length).toBe(1); @@ -218,14 +262,10 @@ describe('discover sidebar', function () { expect(createDataViewButton.length).toBe(0); }); - it('should render the Visualize in Lens button in text based languages mode', () => { - const compInViewerMode = mountWithIntl( - - - - - - ); + it('should render the Visualize in Lens button in text based languages mode', async () => { + const compInViewerMode = await mountComponent(getCompProps(), { + query: { sql: 'SELECT * FROM test' }, + }); const visualizeField = findTestSubject(compInViewerMode, 'textBased-visualize'); expect(visualizeField.length).toBe(1); }); diff --git a/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar.tsx b/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar.tsx index 4735b75b66e14..109f11615335f 100644 --- a/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar.tsx +++ b/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar.tsx @@ -7,49 +7,48 @@ */ import './discover_sidebar.scss'; -import { throttle } from 'lodash'; -import React, { useCallback, useEffect, useState, useMemo, useRef, memo } from 'react'; +import React, { memo, useCallback, useEffect, useMemo, useState } from 'react'; import { i18n } from '@kbn/i18n'; import { - EuiAccordion, - EuiFlexItem, + EuiButton, EuiFlexGroup, - EuiText, - EuiTitle, - EuiSpacer, - EuiNotificationBadge, + EuiFlexItem, EuiPageSideBar_Deprecated as EuiPageSideBar, - useResizeObserver, - EuiButton, + htmlIdGenerator, } from '@elastic/eui'; import { isOfAggregateQueryType } from '@kbn/es-query'; -import useShallowCompareEffect from 'react-use/lib/useShallowCompareEffect'; -import { isEqual } from 'lodash'; -import { FormattedMessage } from '@kbn/i18n-react'; import { DataViewPicker } from '@kbn/unified-search-plugin/public'; -import { DataViewField, getFieldSubtypeMulti } from '@kbn/data-views-plugin/public'; -import { triggerVisualizeActionsTextBasedLanguages } from '@kbn/unified-field-list-plugin/public'; +import { type DataViewField, getFieldSubtypeMulti } from '@kbn/data-views-plugin/public'; +import { + FieldListGrouped, + FieldListGroupedProps, + FieldsGroupNames, + GroupedFieldsParams, + triggerVisualizeActionsTextBasedLanguages, + useExistingFieldsReader, + useGroupedFields, +} from '@kbn/unified-field-list-plugin/public'; import { useAppStateSelector } from '../../services/discover_app_state_container'; import { useDiscoverServices } from '../../../../hooks/use_discover_services'; import { DiscoverField } from './discover_field'; import { DiscoverFieldSearch } from './discover_field_search'; import { FIELDS_LIMIT_SETTING, PLUGIN_ID } from '../../../../../common'; -import { groupFields } from './lib/group_fields'; -import { getDetails } from './lib/get_details'; -import { FieldFilterState, getDefaultFieldFilter, setFieldFilterProp } from './lib/field_filter'; -import { getDataViewFieldList } from './lib/get_data_view_field_list'; +import { + getSelectedFields, + shouldShowField, + type SelectedFieldsResult, + INITIAL_SELECTED_FIELDS_RESULT, +} from './lib/group_fields'; +import { doesFieldMatchFilters, FieldFilterState, setFieldFilterProp } from './lib/field_filter'; import { DiscoverSidebarResponsiveProps } from './discover_sidebar_responsive'; import { VIEW_MODE } from '../../../../components/view_mode_toggle'; -import { DISCOVER_TOUR_STEP_ANCHOR_IDS } from '../../../../components/discover_tour'; -import type { DataTableRecord } from '../../../../types'; import { getUiActions } from '../../../../kibana_services'; +import { getRawRecordType } from '../../utils/get_raw_record_type'; +import { RecordRawType } from '../../hooks/use_saved_search'; -/** - * Default number of available fields displayed and added on scroll - */ -const FIELDS_PER_PAGE = 50; +const fieldSearchDescriptionId = htmlIdGenerator()(); -export interface DiscoverSidebarProps extends Omit { +export interface DiscoverSidebarProps extends DiscoverSidebarResponsiveProps { /** * Current state of the field filter, filtering fields by name, type, ... */ @@ -84,27 +83,37 @@ export interface DiscoverSidebarProps extends Omit void; /** - * a statistics of the distribution of fields in the given hits + * All fields: fields from data view and unmapped fields */ - fieldCounts?: Record; - /** - * hits fetched from ES, displayed in the doc table - */ - documents?: DataTableRecord[]; + allFields: DataViewField[] | null; + /** * Discover view mode */ viewMode: VIEW_MODE; + /** + * Show data view picker (for mobile view) + */ showDataViewPicker?: boolean; + + /** + * Whether to render the field list or not (we don't show it unless documents are loaded) + */ + showFieldList?: boolean; + + /** + * Whether filters are applied + */ + isAffectedByGlobalFilter: boolean; } export function DiscoverSidebarComponent({ alwaysShowActionButtons = false, columns, - fieldCounts, fieldFilter, - documents, + documents$, + allFields, onAddField, onAddFilter, onRemoveField, @@ -120,108 +129,28 @@ export function DiscoverSidebarComponent({ viewMode, createNewDataView, showDataViewPicker, + showFieldList, + isAffectedByGlobalFilter, }: DiscoverSidebarProps) { - const { uiSettings, dataViewFieldEditor } = useDiscoverServices(); - const [fields, setFields] = useState(null); - const [scrollContainer, setScrollContainer] = useState(null); - const [fieldsToRender, setFieldsToRender] = useState(FIELDS_PER_PAGE); - const [fieldsPerPage, setFieldsPerPage] = useState(FIELDS_PER_PAGE); - const availableFieldsContainer = useRef(null); - const isPlainRecord = !onAddFilter; + const { uiSettings, dataViewFieldEditor, dataViews } = useDiscoverServices(); + const isPlainRecord = useAppStateSelector( + (state) => getRawRecordType(state.query) === RecordRawType.PLAIN + ); const query = useAppStateSelector((state) => state.query); - useEffect(() => { - if (documents) { - const newFields = getDataViewFieldList(selectedDataView, fieldCounts); - setFields(newFields); - } - }, [selectedDataView, fieldCounts, documents]); - - const scrollDimensions = useResizeObserver(scrollContainer); - const onChangeFieldSearch = useCallback( - (field: string, value: string | boolean | undefined) => { - const newState = setFieldFilterProp(fieldFilter, field, value); + (filterName: string, value: string | boolean | undefined) => { + const newState = setFieldFilterProp(fieldFilter, filterName, value); setFieldFilter(newState); - setFieldsToRender(fieldsPerPage); }, - [fieldFilter, setFieldFilter, setFieldsToRender, fieldsPerPage] - ); - - const getDetailsByField = useCallback( - (ipField: DataViewField) => getDetails(ipField, documents, selectedDataView), - [documents, selectedDataView] + [fieldFilter, setFieldFilter] ); - const popularLimit = useMemo(() => uiSettings.get(FIELDS_LIMIT_SETTING), [uiSettings]); - - const { - selected: selectedFields, - popular: popularFields, - unpopular: unpopularFields, - } = useMemo( - () => groupFields(fields, columns, popularLimit, fieldCounts, fieldFilter, useNewFieldsApi), - [fields, columns, popularLimit, fieldCounts, fieldFilter, useNewFieldsApi] - ); - - /** - * Popular fields are not displayed in text based lang mode - */ - const restFields = useMemo( - () => (isPlainRecord ? [...popularFields, ...unpopularFields] : unpopularFields), - [isPlainRecord, popularFields, unpopularFields] - ); - - const paginate = useCallback(() => { - const newFieldsToRender = fieldsToRender + Math.round(fieldsPerPage * 0.5); - setFieldsToRender(Math.max(fieldsPerPage, Math.min(newFieldsToRender, restFields.length))); - }, [setFieldsToRender, fieldsToRender, restFields, fieldsPerPage]); - - useEffect(() => { - if (scrollContainer && restFields.length && availableFieldsContainer.current) { - const { clientHeight, scrollHeight } = scrollContainer; - const isScrollable = scrollHeight > clientHeight; // there is no scrolling currently - const allFieldsRendered = fieldsToRender >= restFields.length; - - if (!isScrollable && !allFieldsRendered) { - // Not all available fields were rendered with the given fieldsPerPage number - // and no scrolling is available due to the a high zoom out factor of the browser - // In this case the fieldsPerPage needs to be adapted - const fieldsRenderedHeight = availableFieldsContainer.current.clientHeight; - const avgHeightPerItem = Math.round(fieldsRenderedHeight / fieldsToRender); - const newFieldsPerPage = - (avgHeightPerItem > 0 ? Math.round(clientHeight / avgHeightPerItem) : 0) + 10; - if (newFieldsPerPage >= FIELDS_PER_PAGE && newFieldsPerPage !== fieldsPerPage) { - setFieldsPerPage(newFieldsPerPage); - setFieldsToRender(newFieldsPerPage); - } - } - } - }, [ - fieldsPerPage, - scrollContainer, - restFields, - fieldsToRender, - setFieldsPerPage, - setFieldsToRender, - scrollDimensions, - ]); - - const lazyScroll = useCallback(() => { - if (scrollContainer) { - const { scrollTop, clientHeight, scrollHeight } = scrollContainer; - const nearBottom = scrollTop + clientHeight > scrollHeight * 0.9; - if (nearBottom && restFields) { - paginate(); - } - } - }, [paginate, scrollContainer, restFields]); - const { fieldTypes, presentFieldTypes } = useMemo(() => { const result = ['any']; const dataViewFieldTypes = new Set(); - if (Array.isArray(fields)) { - for (const field of fields) { + if (Array.isArray(allFields)) { + for (const field of allFields) { if (field.type !== '_source') { // If it's a string type, we want to distinguish between keyword and text // For this purpose we need the ES type @@ -242,37 +171,36 @@ export function DiscoverSidebarComponent({ } } return { fieldTypes: result, presentFieldTypes: Array.from(dataViewFieldTypes) }; - }, [fields]); + }, [allFields]); const showFieldStats = useMemo(() => viewMode === VIEW_MODE.DOCUMENT_LEVEL, [viewMode]); + const [selectedFieldsState, setSelectedFieldsState] = useState( + INITIAL_SELECTED_FIELDS_RESULT + ); + const [multiFieldsMap, setMultiFieldsMap] = useState< + Map> | undefined + >(undefined); - const calculateMultiFields = () => { - if (!useNewFieldsApi || !fields) { - return undefined; - } - const map = new Map>(); - fields.forEach((field) => { - const subTypeMulti = getFieldSubtypeMulti(field); - const parent = subTypeMulti?.multi.parent; - if (!parent) { - return; - } - const multiField = { - field, - isSelected: selectedFields.includes(field), - }; - const value = map.get(parent) ?? []; - value.push(multiField); - map.set(parent, value); - }); - return map; - }; - - const [multiFields, setMultiFields] = useState(() => calculateMultiFields()); + useEffect(() => { + const result = getSelectedFields(selectedDataView, columns); + setSelectedFieldsState(result); + }, [selectedDataView, columns, setSelectedFieldsState]); - useShallowCompareEffect(() => { - setMultiFields(calculateMultiFields()); - }, [fields, selectedFields, useNewFieldsApi]); + useEffect(() => { + if (isPlainRecord || !useNewFieldsApi) { + setMultiFieldsMap(undefined); // we don't have to calculate multifields in this case + } else { + setMultiFieldsMap( + calculateMultiFields(allFields, selectedFieldsState.selectedFieldsMap, useNewFieldsApi) + ); + } + }, [ + selectedFieldsState.selectedFieldsMap, + allFields, + useNewFieldsApi, + setMultiFieldsMap, + isPlainRecord, + ]); const deleteField = useMemo( () => @@ -305,15 +233,6 @@ export function DiscoverSidebarComponent({ ] ); - const getPaginated = useCallback( - (list) => { - return list.slice(0, fieldsToRender); - }, - [fieldsToRender] - ); - - const filterChanged = useMemo(() => isEqual(fieldFilter, getDefaultFieldFilter()), [fieldFilter]); - const visualizeAggregateQuery = useCallback(() => { const aggregateQuery = query && isOfAggregateQueryType(query) ? query : undefined; triggerVisualizeActionsTextBasedLanguages( @@ -325,6 +244,89 @@ export function DiscoverSidebarComponent({ ); }, [columns, selectedDataView, query]); + const popularFieldsLimit = useMemo(() => uiSettings.get(FIELDS_LIMIT_SETTING), [uiSettings]); + const onFilterField: GroupedFieldsParams['onFilterField'] = useCallback( + (field) => { + return doesFieldMatchFilters(field, fieldFilter); + }, + [fieldFilter] + ); + const onSupportedFieldFilter: GroupedFieldsParams['onSupportedFieldFilter'] = + useCallback( + (field) => { + return shouldShowField(field, isPlainRecord); + }, + [isPlainRecord] + ); + const onOverrideFieldGroupDetails: GroupedFieldsParams['onOverrideFieldGroupDetails'] = + useCallback((groupName) => { + if (groupName === FieldsGroupNames.AvailableFields) { + return { + helpText: i18n.translate('discover.fieldChooser.availableFieldsTooltip', { + defaultMessage: 'Fields available for display in the table.', + }), + }; + } + }, []); + const fieldsExistenceReader = useExistingFieldsReader(); + const fieldListGroupedProps = useGroupedFields({ + dataViewId: (!isPlainRecord && selectedDataView?.id) || null, // passing `null` for text-based queries + fieldsExistenceReader: !isPlainRecord ? fieldsExistenceReader : undefined, + allFields, + popularFieldsLimit: !isPlainRecord ? popularFieldsLimit : 0, + sortedSelectedFields: selectedFieldsState.selectedFields, + isAffectedByGlobalFilter, + services: { + dataViews, + }, + onFilterField, + onSupportedFieldFilter, + onOverrideFieldGroupDetails, + }); + + const renderFieldItem: FieldListGroupedProps['renderFieldItem'] = useCallback( + ({ field, groupName }) => ( +
  • + +
  • + ), + [ + alwaysShowActionButtons, + selectedDataView, + onAddField, + onRemoveField, + onAddFilter, + documents$, + trackUiMetric, + multiFieldsMap, + editField, + deleteField, + showFieldStats, + columns, + selectedFieldsState.selectedFieldsMap, + fieldFilter.name, + ] + ); + if (!selectedDataView) { return null; } @@ -367,169 +369,18 @@ export function DiscoverSidebarComponent({ types={fieldTypes} presentFieldTypes={presentFieldTypes} isPlainRecord={isPlainRecord} + fieldSearchDescriptionId={fieldSearchDescriptionId} />
    - -
    { - if (documents && el && !el.dataset.dynamicScroll) { - el.dataset.dynamicScroll = 'true'; - setScrollContainer(el); - } - }} - onScroll={throttle(lazyScroll, 100)} - className="eui-yScroll" - > - {Array.isArray(fields) && fields.length > 0 && ( -
    - {selectedFields && - selectedFields.length > 0 && - selectedFields[0].displayName !== '_source' ? ( - <> - - - - - - } - extraAction={ - - {selectedFields.length} - - } - > - -
      - {selectedFields.map((field: DataViewField) => { - return ( -
    • - -
    • - ); - })} -
    -
    - {' '} - - ) : null} - - - - - - } - extraAction={ - - {restFields.length} - - } - > - - {!isPlainRecord && popularFields.length > 0 && ( - <> - - - -
      - {popularFields.map((field: DataViewField) => { - return ( -
    • - -
    • - ); - })} -
    - - )} -
      - {getPaginated(restFields).map((field: DataViewField) => { - return ( -
    • - -
    • - ); - })} -
    -
    -
    - )} -
    + + {showFieldList && ( + + )} {!!editField && ( @@ -565,3 +416,29 @@ export function DiscoverSidebarComponent({ } export const DiscoverSidebar = memo(DiscoverSidebarComponent); + +function calculateMultiFields( + allFields: DataViewField[] | null, + selectedFieldsMap: SelectedFieldsResult['selectedFieldsMap'] | undefined, + useNewFieldsApi: boolean +) { + if (!useNewFieldsApi || !allFields) { + return undefined; + } + const map = new Map>(); + allFields.forEach((field) => { + const subTypeMulti = getFieldSubtypeMulti(field); + const parent = subTypeMulti?.multi.parent; + if (!parent) { + return; + } + const multiField = { + field, + isSelected: Boolean(selectedFieldsMap?.[field.name]), + }; + const value = map.get(parent) ?? []; + value.push(multiField); + map.set(parent, value); + }); + return map; +} diff --git a/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar_responsive.test.tsx b/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar_responsive.test.tsx index 7f2134a758018..720f3da27ad18 100644 --- a/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar_responsive.test.tsx +++ b/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar_responsive.test.tsx @@ -9,7 +9,8 @@ import { BehaviorSubject } from 'rxjs'; import { ReactWrapper } from 'enzyme'; import { findTestSubject } from '@elastic/eui/lib/test'; -import { getDataTableRecords } from '../../../../__fixtures__/real_hits'; +import { EuiProgress } from '@elastic/eui'; +import { getDataTableRecords, realHits } from '../../../../__fixtures__/real_hits'; import { act } from 'react-dom/test-utils'; import { mountWithIntl } from '@kbn/test-jest-helpers'; import React from 'react'; @@ -24,12 +25,14 @@ import { AvailableFields$, DataDocuments$, RecordRawType } from '../../hooks/use import { stubLogstashDataView } from '@kbn/data-plugin/common/stubs'; import { VIEW_MODE } from '../../../../components/view_mode_toggle'; import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; -import { chartPluginMock } from '@kbn/charts-plugin/public/mocks'; -import { dataViewPluginMocks } from '@kbn/data-views-plugin/public/mocks'; -import { fieldFormatsServiceMock } from '@kbn/field-formats-plugin/public/mocks'; -import { dataPluginMock } from '@kbn/data-plugin/public/mocks'; import { getDiscoverStateMock } from '../../../../__mocks__/discover_state.mock'; import { DiscoverAppStateProvider } from '../../services/discover_app_state_container'; +import * as ExistingFieldsServiceApi from '@kbn/unified-field-list-plugin/public/services/field_existing/load_field_existing'; +import { resetExistingFieldsCache } from '@kbn/unified-field-list-plugin/public/hooks/use_existing_fields'; +import { createDiscoverServicesMock } from '../../../../__mocks__/services'; +import type { AggregateQuery, Query } from '@kbn/es-query'; +import { buildDataTableRecord } from '../../../../utils/build_data_record'; +import { type DataTableRecord } from '../../../../types'; jest.mock('@kbn/unified-field-list-plugin/public/services/field_stats', () => ({ loadFieldStats: jest.fn().mockResolvedValue({ @@ -67,59 +70,30 @@ jest.mock('@kbn/unified-field-list-plugin/public/services/field_stats', () => ({ }), })); -const dataServiceMock = dataPluginMock.createStartContract(); - -const mockServices = { - history: () => ({ - location: { - search: '', - }, - }), - capabilities: { - visualize: { - show: true, - }, - discover: { - save: false, - }, - }, - uiSettings: { - get: (key: string) => { - if (key === 'fields:popularLimit') { - return 5; - } - }, - }, - docLinks: { links: { discover: { fieldTypeHelp: '' } } }, - dataViewEditor: { - userPermissions: { - editDataView: jest.fn(() => true), - }, - }, - data: { - ...dataServiceMock, - query: { - ...dataServiceMock.query, - timefilter: { - ...dataServiceMock.query.timefilter, - timefilter: { - ...dataServiceMock.query.timefilter.timefilter, - getAbsoluteTime: () => ({ - from: '2021-08-31T22:00:00.000Z', - to: '2022-09-01T09:16:29.553Z', - }), - }, +function createMockServices() { + const mockServices = { + ...createDiscoverServicesMock(), + capabilities: { + visualize: { + show: true, + }, + discover: { + save: false, }, getState: () => ({ query: { query: '', language: 'lucene' }, filters: [], }), }, - }, - dataViews: dataViewPluginMocks.createStartContract(), - fieldFormats: fieldFormatsServiceMock.createStartContract(), - charts: chartPluginMock.createSetupContract(), -} as unknown as DiscoverServices; + docLinks: { links: { discover: { fieldTypeHelp: '' } } }, + dataViewEditor: { + userPermissions: { + editDataView: jest.fn(() => true), + }, + }, + } as unknown as DiscoverServices; + return mockServices; +} const mockfieldCounts: Record = {}; const mockCalcFieldCounts = jest.fn(() => { @@ -138,17 +112,13 @@ jest.mock('../../utils/calc_field_counts', () => ({ calcFieldCounts: () => mockCalcFieldCounts(), })); -function getCompProps(): DiscoverSidebarResponsiveProps { +jest.spyOn(ExistingFieldsServiceApi, 'loadFieldExisting'); + +function getCompProps(options?: { hits?: DataTableRecord[] }): DiscoverSidebarResponsiveProps { const dataView = stubLogstashDataView; dataView.toSpec = jest.fn(() => ({})); - const hits = getDataTableRecords(dataView); - - const dataViewList = [ - { id: '0', title: 'b' } as DataViewListItem, - { id: '1', title: 'a' } as DataViewListItem, - { id: '2', title: 'c' } as DataViewListItem, - ]; + const hits = options?.hits ?? getDataTableRecords(dataView); for (const hit of hits) { for (const key of Object.keys(hit.flattened)) { @@ -166,7 +136,7 @@ function getCompProps(): DiscoverSidebarResponsiveProps { fetchStatus: FetchStatus.COMPLETE, fields: [] as string[], }) as AvailableFields$, - dataViewList, + dataViewList: [dataView as DataViewListItem], onChangeDataView: jest.fn(), onAddFilter: jest.fn(), onAddField: jest.fn(), @@ -180,52 +150,220 @@ function getCompProps(): DiscoverSidebarResponsiveProps { }; } +function getAppStateContainer({ query }: { query?: Query | AggregateQuery }) { + const appStateContainer = getDiscoverStateMock({ isTimeBased: true }).appStateContainer; + appStateContainer.set({ + query: query ?? { query: '', language: 'lucene' }, + filters: [], + }); + return appStateContainer; +} + +async function mountComponent( + props: DiscoverSidebarResponsiveProps, + appStateParams: { query?: Query | AggregateQuery } = {}, + services?: DiscoverServices +): Promise> { + let comp: ReactWrapper; + const mockedServices = services ?? createMockServices(); + mockedServices.data.dataViews.getIdsWithTitle = jest.fn(async () => props.dataViewList); + mockedServices.data.dataViews.get = jest.fn().mockImplementation(async (id) => { + return [props.selectedDataView].find((d) => d!.id === id); + }); + + await act(async () => { + comp = await mountWithIntl( + + + + + + ); + // wait for lazy modules + await new Promise((resolve) => setTimeout(resolve, 0)); + await comp.update(); + }); + + await comp!.update(); + + return comp!; +} + describe('discover responsive sidebar', function () { let props: DiscoverSidebarResponsiveProps; - let comp: ReactWrapper; - beforeAll(async () => { + beforeEach(async () => { + (ExistingFieldsServiceApi.loadFieldExisting as jest.Mock).mockImplementation(async () => ({ + indexPatternTitle: 'test', + existingFieldNames: Object.keys(mockfieldCounts), + })); props = getCompProps(); + }); + + afterEach(() => { + mockCalcFieldCounts.mockClear(); + (ExistingFieldsServiceApi.loadFieldExisting as jest.Mock).mockClear(); + resetExistingFieldsCache(); + }); + + it('should have loading indicators during fields existence loading', async function () { + let resolveFunction: (arg: unknown) => void; + (ExistingFieldsServiceApi.loadFieldExisting as jest.Mock).mockReset(); + (ExistingFieldsServiceApi.loadFieldExisting as jest.Mock).mockImplementation(() => { + return new Promise((resolve) => { + resolveFunction = resolve; + }); + }); + + const compLoadingExistence = await mountComponent(props); + + expect( + findTestSubject(compLoadingExistence, 'fieldListGroupedAvailableFields-countLoading').exists() + ).toBe(true); + expect( + findTestSubject(compLoadingExistence, 'fieldListGroupedAvailableFields-count').exists() + ).toBe(false); + + expect(compLoadingExistence.find(EuiProgress).exists()).toBe(true); + await act(async () => { - const appStateContainer = getDiscoverStateMock({ isTimeBased: true }).appStateContainer; - appStateContainer.set({ - query: { query: '', language: 'lucene' }, - filters: [], + resolveFunction!({ + indexPatternTitle: 'test-loaded', + existingFieldNames: Object.keys(mockfieldCounts), }); + await compLoadingExistence.update(); + }); - comp = await mountWithIntl( - - - - - - ); - // wait for lazy modules - await new Promise((resolve) => setTimeout(resolve, 0)); - await comp.update(); + await act(async () => { + await compLoadingExistence.update(); }); + + expect( + findTestSubject(compLoadingExistence, 'fieldListGroupedAvailableFields-countLoading').exists() + ).toBe(false); + expect( + findTestSubject(compLoadingExistence, 'fieldListGroupedAvailableFields-count').exists() + ).toBe(true); + + expect(compLoadingExistence.find(EuiProgress).exists()).toBe(false); + + expect(ExistingFieldsServiceApi.loadFieldExisting).toHaveBeenCalledTimes(1); }); - it('should have Selected Fields and Available Fields with Popular Fields sections', function () { - const popular = findTestSubject(comp, 'fieldList-popular'); - const selected = findTestSubject(comp, 'fieldList-selected'); - const unpopular = findTestSubject(comp, 'fieldList-unpopular'); - expect(popular.children().length).toBe(1); - expect(unpopular.children().length).toBe(6); - expect(selected.children().length).toBe(1); + it('should have Selected Fields, Available Fields, Popular and Meta Fields sections', async function () { + const comp = await mountComponent(props); + const popularFieldsCount = findTestSubject(comp, 'fieldListGroupedPopularFields-count'); + const selectedFieldsCount = findTestSubject(comp, 'fieldListGroupedSelectedFields-count'); + const availableFieldsCount = findTestSubject(comp, 'fieldListGroupedAvailableFields-count'); + const emptyFieldsCount = findTestSubject(comp, 'fieldListGroupedEmptyFields-count'); + const metaFieldsCount = findTestSubject(comp, 'fieldListGroupedMetaFields-count'); + const unmappedFieldsCount = findTestSubject(comp, 'fieldListGroupedUnmappedFields-count'); + + expect(selectedFieldsCount.text()).toBe('1'); + expect(popularFieldsCount.text()).toBe('4'); + expect(availableFieldsCount.text()).toBe('3'); + expect(emptyFieldsCount.text()).toBe('20'); + expect(metaFieldsCount.text()).toBe('2'); + expect(unmappedFieldsCount.exists()).toBe(false); expect(mockCalcFieldCounts.mock.calls.length).toBe(1); + + expect(props.availableFields$.getValue()).toEqual({ + fetchStatus: 'complete', + fields: ['extension'], + }); + + expect(findTestSubject(comp, 'fieldListGrouped__ariaDescription').text()).toBe( + '1 selected field. 4 popular fields. 3 available fields. 20 empty fields. 2 meta fields.' + ); + + expect(ExistingFieldsServiceApi.loadFieldExisting).toHaveBeenCalledTimes(1); + }); + + it('should not have selected fields if no columns selected', async function () { + const propsWithoutColumns = { + ...props, + columns: [], + }; + const compWithoutSelected = await mountComponent(propsWithoutColumns); + const popularFieldsCount = findTestSubject( + compWithoutSelected, + 'fieldListGroupedPopularFields-count' + ); + const selectedFieldsCount = findTestSubject( + compWithoutSelected, + 'fieldListGroupedSelectedFields-count' + ); + const availableFieldsCount = findTestSubject( + compWithoutSelected, + 'fieldListGroupedAvailableFields-count' + ); + const emptyFieldsCount = findTestSubject( + compWithoutSelected, + 'fieldListGroupedEmptyFields-count' + ); + const metaFieldsCount = findTestSubject( + compWithoutSelected, + 'fieldListGroupedMetaFields-count' + ); + const unmappedFieldsCount = findTestSubject( + compWithoutSelected, + 'fieldListGroupedUnmappedFields-count' + ); + + expect(selectedFieldsCount.exists()).toBe(false); + expect(popularFieldsCount.text()).toBe('4'); + expect(availableFieldsCount.text()).toBe('3'); + expect(emptyFieldsCount.text()).toBe('20'); + expect(metaFieldsCount.text()).toBe('2'); + expect(unmappedFieldsCount.exists()).toBe(false); + + expect(propsWithoutColumns.availableFields$.getValue()).toEqual({ + fetchStatus: 'complete', + fields: ['bytes', 'extension', '_id', 'phpmemory'], + }); + + expect(findTestSubject(compWithoutSelected, 'fieldListGrouped__ariaDescription').text()).toBe( + '4 popular fields. 3 available fields. 20 empty fields. 2 meta fields.' + ); + }); + + it('should not calculate counts if documents are not fetched yet', async function () { + const propsWithoutDocuments: DiscoverSidebarResponsiveProps = { + ...props, + documents$: new BehaviorSubject({ + fetchStatus: FetchStatus.UNINITIALIZED, + result: undefined, + }) as DataDocuments$, + }; + const compWithoutDocuments = await mountComponent(propsWithoutDocuments); + const availableFieldsCount = findTestSubject( + compWithoutDocuments, + 'fieldListGroupedAvailableFields-count' + ); + + expect(availableFieldsCount.exists()).toBe(false); + + expect(mockCalcFieldCounts.mock.calls.length).toBe(0); + expect(ExistingFieldsServiceApi.loadFieldExisting).not.toHaveBeenCalled(); }); - it('should allow selecting fields', function () { - findTestSubject(comp, 'fieldToggle-bytes').simulate('click'); + + it('should allow selecting fields', async function () { + const comp = await mountComponent(props); + const availableFields = findTestSubject(comp, 'fieldListGroupedAvailableFields'); + findTestSubject(availableFields, 'fieldToggle-bytes').simulate('click'); expect(props.onAddField).toHaveBeenCalledWith('bytes'); }); - it('should allow deselecting fields', function () { - findTestSubject(comp, 'fieldToggle-extension').simulate('click'); + it('should allow deselecting fields', async function () { + const comp = await mountComponent(props); + const selectedFields = findTestSubject(comp, 'fieldListGroupedSelectedFields'); + findTestSubject(selectedFields, 'fieldToggle-extension').simulate('click'); expect(props.onRemoveField).toHaveBeenCalledWith('extension'); }); it('should allow adding filters', async function () { + const comp = await mountComponent(props); + const availableFields = findTestSubject(comp, 'fieldListGroupedAvailableFields'); await act(async () => { - const button = findTestSubject(comp, 'field-extension-showDetails'); + const button = findTestSubject(availableFields, 'field-extension-showDetails'); await button.simulate('click'); await comp.update(); }); @@ -235,8 +373,10 @@ describe('discover responsive sidebar', function () { expect(props.onAddFilter).toHaveBeenCalled(); }); it('should allow adding "exist" filter', async function () { + const comp = await mountComponent(props); + const availableFields = findTestSubject(comp, 'fieldListGroupedAvailableFields'); await act(async () => { - const button = findTestSubject(comp, 'field-extension-showDetails'); + const button = findTestSubject(availableFields, 'field-extension-showDetails'); await button.simulate('click'); await comp.update(); }); @@ -245,27 +385,38 @@ describe('discover responsive sidebar', function () { findTestSubject(comp, 'discoverFieldListPanelAddExistFilter-extension').simulate('click'); expect(props.onAddFilter).toHaveBeenCalledWith('_exists_', 'extension', '+'); }); - it('should allow filtering by string, and calcFieldCount should just be executed once', function () { - expect(findTestSubject(comp, 'fieldList-unpopular').children().length).toBe(6); - act(() => { - findTestSubject(comp, 'fieldFilterSearchInput').simulate('change', { - target: { value: 'abc' }, + it('should allow filtering by string, and calcFieldCount should just be executed once', async function () { + const comp = await mountComponent(props); + + expect(findTestSubject(comp, 'fieldListGroupedAvailableFields-count').text()).toBe('3'); + expect(findTestSubject(comp, 'fieldListGrouped__ariaDescription').text()).toBe( + '1 selected field. 4 popular fields. 3 available fields. 20 empty fields. 2 meta fields.' + ); + + await act(async () => { + await findTestSubject(comp, 'fieldFilterSearchInput').simulate('change', { + target: { value: 'bytes' }, }); }); - comp.update(); - expect(findTestSubject(comp, 'fieldList-unpopular').children().length).toBe(4); + + expect(findTestSubject(comp, 'fieldListGroupedAvailableFields-count').text()).toBe('1'); + expect(findTestSubject(comp, 'fieldListGrouped__ariaDescription').text()).toBe( + '1 popular field. 1 available field. 0 empty fields. 0 meta fields.' + ); expect(mockCalcFieldCounts.mock.calls.length).toBe(1); }); - it('should show "Add a field" button to create a runtime field', () => { - expect(mockServices.dataViewEditor.userPermissions.editDataView).toHaveBeenCalled(); + it('should show "Add a field" button to create a runtime field', async () => { + const services = createMockServices(); + const comp = await mountComponent(props, {}, services); + expect(services.dataViewEditor.userPermissions.editDataView).toHaveBeenCalled(); expect(findTestSubject(comp, 'dataView-add-field_btn').length).toBe(1); }); - it('should not show "Add a field" button on the sql mode', () => { - const initialProps = getCompProps(); + it('should render correctly in the sql mode', async () => { const propsWithTextBasedMode = { - ...initialProps, + ...props, + columns: ['extension', 'bytes'], onAddFilter: undefined, documents$: new BehaviorSubject({ fetchStatus: FetchStatus.COMPLETE, @@ -273,46 +424,75 @@ describe('discover responsive sidebar', function () { result: getDataTableRecords(stubLogstashDataView), }) as DataDocuments$, }; - const appStateContainer = getDiscoverStateMock({ isTimeBased: true }).appStateContainer; - appStateContainer.set({ + const compInViewerMode = await mountComponent(propsWithTextBasedMode, { query: { sql: 'SELECT * FROM `index`' }, }); - const compInViewerMode = mountWithIntl( - - - - - - ); expect(findTestSubject(compInViewerMode, 'indexPattern-add-field_btn').length).toBe(0); + + const popularFieldsCount = findTestSubject( + compInViewerMode, + 'fieldListGroupedPopularFields-count' + ); + const selectedFieldsCount = findTestSubject( + compInViewerMode, + 'fieldListGroupedSelectedFields-count' + ); + const availableFieldsCount = findTestSubject( + compInViewerMode, + 'fieldListGroupedAvailableFields-count' + ); + const emptyFieldsCount = findTestSubject(compInViewerMode, 'fieldListGroupedEmptyFields-count'); + const metaFieldsCount = findTestSubject(compInViewerMode, 'fieldListGroupedMetaFields-count'); + const unmappedFieldsCount = findTestSubject( + compInViewerMode, + 'fieldListGroupedUnmappedFields-count' + ); + + expect(selectedFieldsCount.text()).toBe('2'); + expect(popularFieldsCount.exists()).toBe(false); + expect(availableFieldsCount.text()).toBe('4'); + expect(emptyFieldsCount.exists()).toBe(false); + expect(metaFieldsCount.exists()).toBe(false); + expect(unmappedFieldsCount.exists()).toBe(false); + + expect(mockCalcFieldCounts.mock.calls.length).toBe(1); + + expect(findTestSubject(compInViewerMode, 'fieldListGrouped__ariaDescription').text()).toBe( + '2 selected fields. 4 available fields.' + ); }); - it('should not show "Add a field" button in viewer mode', () => { - const mockedServicesInViewerMode = { - ...mockServices, - dataViewEditor: { - ...mockServices.dataViewEditor, - userPermissions: { - ...mockServices.dataViewEditor.userPermissions, - editDataView: jest.fn(() => false), - }, - }, - }; - const appStateContainer = getDiscoverStateMock({ isTimeBased: true }).appStateContainer; - appStateContainer.set({ - query: { query: '', language: 'lucene' }, - filters: [], + it('should render correctly unmapped fields', async () => { + const propsWithUnmappedField = getCompProps({ + hits: [ + buildDataTableRecord(realHits[0], stubLogstashDataView), + buildDataTableRecord( + { + _index: 'logstash-2014.09.09', + _id: '1945', + _score: 1, + _source: { + extension: 'gif', + bytes: 10617.2, + test_unmapped: 'show me too', + }, + }, + stubLogstashDataView + ), + ], }); - const compInViewerMode = mountWithIntl( - - - - - + const compWithUnmapped = await mountComponent(propsWithUnmappedField); + + expect(findTestSubject(compWithUnmapped, 'fieldListGrouped__ariaDescription').text()).toBe( + '1 selected field. 4 popular fields. 3 available fields. 1 unmapped field. 20 empty fields. 2 meta fields.' ); - expect( - mockedServicesInViewerMode.dataViewEditor.userPermissions.editDataView - ).toHaveBeenCalled(); + }); + + it('should not show "Add a field" button in viewer mode', async () => { + const services = createMockServices(); + services.dataViewEditor.userPermissions.editDataView = jest.fn(() => false); + const compInViewerMode = await mountComponent(props, {}, services); + expect(services.dataViewEditor.userPermissions.editDataView).toHaveBeenCalled(); expect(findTestSubject(compInViewerMode, 'dataView-add-field_btn').length).toBe(0); }); }); diff --git a/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar_responsive.tsx b/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar_responsive.tsx index 73271a0260709..13e1287022f3d 100644 --- a/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar_responsive.tsx +++ b/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar_responsive.tsx @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import React, { useCallback, useEffect, useMemo, useReducer, useRef, useState } from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; import { UiCounterMetricType } from '@kbn/analytics'; @@ -19,20 +19,31 @@ import { EuiIcon, EuiLink, EuiPortal, + EuiProgress, EuiShowFor, EuiTitle, } from '@elastic/eui'; import type { DataView, DataViewField, DataViewListItem } from '@kbn/data-views-plugin/public'; +import { + useExistingFieldsFetcher, + useQuerySubscriber, +} from '@kbn/unified-field-list-plugin/public'; import { useDiscoverServices } from '../../../../hooks/use_discover_services'; import { getDefaultFieldFilter } from './lib/field_filter'; import { DiscoverSidebar } from './discover_sidebar'; import { AvailableFields$, DataDocuments$, RecordRawType } from '../../hooks/use_saved_search'; -import { calcFieldCounts } from '../../utils/calc_field_counts'; import { VIEW_MODE } from '../../../../components/view_mode_toggle'; import { FetchStatus } from '../../../types'; import { DISCOVER_TOUR_STEP_ANCHOR_IDS } from '../../../../components/discover_tour'; import { getRawRecordType } from '../../utils/get_raw_record_type'; import { useAppStateSelector } from '../../services/discover_app_state_container'; +import { + discoverSidebarReducer, + getInitialState, + DiscoverSidebarReducerActionType, + DiscoverSidebarReducerStatus, +} from './lib/sidebar_reducer'; +import { calcFieldCounts } from '../../utils/calc_field_counts'; export interface DiscoverSidebarResponsiveProps { /** @@ -111,38 +122,94 @@ export interface DiscoverSidebarResponsiveProps { */ export function DiscoverSidebarResponsive(props: DiscoverSidebarResponsiveProps) { const services = useDiscoverServices(); + const { data, dataViews, core } = services; const isPlainRecord = useAppStateSelector( (state) => getRawRecordType(state.query) === RecordRawType.PLAIN ); const { selectedDataView, onFieldEdited, onDataViewCreated } = props; const [fieldFilter, setFieldFilter] = useState(getDefaultFieldFilter()); const [isFlyoutVisible, setIsFlyoutVisible] = useState(false); - /** - * fieldCounts are used to determine which fields are actually used in the given set of documents - */ - const fieldCounts = useRef | null>(null); - if (fieldCounts.current === null) { - fieldCounts.current = calcFieldCounts(props.documents$.getValue().result!, selectedDataView); - } + const [sidebarState, dispatchSidebarStateAction] = useReducer( + discoverSidebarReducer, + selectedDataView, + getInitialState + ); + const selectedDataViewRef = useRef(selectedDataView); + const showFieldList = sidebarState.status !== DiscoverSidebarReducerStatus.INITIAL; - const [documentState, setDocumentState] = useState(props.documents$.getValue()); useEffect(() => { - const subscription = props.documents$.subscribe((next) => { - if (next.fetchStatus !== documentState.fetchStatus) { - if (next.result) { - fieldCounts.current = calcFieldCounts(next.result, selectedDataView!); - } - setDocumentState({ ...documentState, ...next }); + const subscription = props.documents$.subscribe((documentState) => { + const isPlainRecordType = documentState.recordRawType === RecordRawType.PLAIN; + + switch (documentState?.fetchStatus) { + case FetchStatus.UNINITIALIZED: + dispatchSidebarStateAction({ + type: DiscoverSidebarReducerActionType.RESET, + payload: { + dataView: selectedDataViewRef.current, + }, + }); + break; + case FetchStatus.LOADING: + dispatchSidebarStateAction({ + type: DiscoverSidebarReducerActionType.DOCUMENTS_LOADING, + payload: { + isPlainRecord: isPlainRecordType, + }, + }); + break; + case FetchStatus.COMPLETE: + dispatchSidebarStateAction({ + type: DiscoverSidebarReducerActionType.DOCUMENTS_LOADED, + payload: { + dataView: selectedDataViewRef.current, + fieldCounts: calcFieldCounts(documentState.result), + isPlainRecord: isPlainRecordType, + }, + }); + break; + default: + break; } }); return () => subscription.unsubscribe(); - }, [props.documents$, selectedDataView, documentState, setDocumentState]); + }, [props.documents$, dispatchSidebarStateAction, selectedDataViewRef]); useEffect(() => { - // when data view changes fieldCounts needs to be cleaned up to prevent displaying - // fields of the previous data view - fieldCounts.current = {}; - }, [selectedDataView]); + if (selectedDataView !== selectedDataViewRef.current) { + dispatchSidebarStateAction({ + type: DiscoverSidebarReducerActionType.DATA_VIEW_SWITCHED, + payload: { + dataView: selectedDataView, + }, + }); + selectedDataViewRef.current = selectedDataView; + } + }, [selectedDataView, dispatchSidebarStateAction, selectedDataViewRef]); + + const querySubscriberResult = useQuerySubscriber({ data }); + const isAffectedByGlobalFilter = Boolean(querySubscriberResult.filters?.length); + const { isProcessing, refetchFieldsExistenceInfo } = useExistingFieldsFetcher({ + disableAutoFetching: true, + dataViews: !isPlainRecord && sidebarState.dataView ? [sidebarState.dataView] : [], + query: querySubscriberResult.query, + filters: querySubscriberResult.filters, + fromDate: querySubscriberResult.fromDate, + toDate: querySubscriberResult.toDate, + services: { + data, + dataViews, + core, + }, + }); + + useEffect(() => { + if (sidebarState.status === DiscoverSidebarReducerStatus.COMPLETED) { + refetchFieldsExistenceInfo(); + } + // refetching only if status changes + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [sidebarState.status]); const closeFieldEditor = useRef<() => void | undefined>(); const closeDataViewEditor = useRef<() => void | undefined>(); @@ -180,30 +247,18 @@ export function DiscoverSidebarResponsive(props: DiscoverSidebarResponsiveProps) const canEditDataView = Boolean(dataViewEditor?.userPermissions.editDataView()) || !selectedDataView?.isPersisted(); - useEffect( - () => { - // For an external embeddable like the Field stats - // it is useful to know what fields are populated in the docs fetched - // or what fields are selected by the user - - const fieldCnts = fieldCounts.current ?? {}; + useEffect(() => { + // For an external embeddable like the Field stats + // it is useful to know what fields are populated in the docs fetched + // or what fields are selected by the user - const availableFields = props.columns.length > 0 ? props.columns : Object.keys(fieldCnts); - availableFields$.next({ - fetchStatus: FetchStatus.COMPLETE, - fields: availableFields, - }); - }, - // Using columns.length here instead of columns to avoid array reference changing - // eslint-disable-next-line react-hooks/exhaustive-deps - [ - selectedDataView, - availableFields$, - fieldCounts.current, - documentState.result, - props.columns.length, - ] - ); + const availableFields = + props.columns.length > 0 ? props.columns : Object.keys(sidebarState.fieldCounts || {}); + availableFields$.next({ + fetchStatus: FetchStatus.COMPLETE, + fields: availableFields, + }); + }, [selectedDataView, sidebarState.fieldCounts, props.columns, availableFields$]); const editField = useMemo( () => @@ -259,14 +314,17 @@ export function DiscoverSidebarResponsive(props: DiscoverSidebarResponsiveProps) <> {!props.isClosed && ( + {isProcessing && } )} @@ -322,8 +380,8 @@ export function DiscoverSidebarResponsive(props: DiscoverSidebarResponsiveProps) diff --git a/src/plugins/discover/public/application/main/components/sidebar/lib/field_filter.test.ts b/src/plugins/discover/public/application/main/components/sidebar/lib/field_filter.test.ts index 7020dfcc418eb..1d46ea17d5a06 100644 --- a/src/plugins/discover/public/application/main/components/sidebar/lib/field_filter.test.ts +++ b/src/plugins/discover/public/application/main/components/sidebar/lib/field_filter.test.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { getDefaultFieldFilter, setFieldFilterProp, isFieldFiltered } from './field_filter'; +import { getDefaultFieldFilter, setFieldFilterProp, doesFieldMatchFilters } from './field_filter'; import { DataViewField } from '@kbn/data-views-plugin/public'; describe('field_filter', function () { @@ -14,7 +14,6 @@ describe('field_filter', function () { expect(getDefaultFieldFilter()).toMatchInlineSnapshot(` Object { "aggregatable": null, - "missing": true, "name": "", "searchable": null, "type": "any", @@ -25,7 +24,6 @@ describe('field_filter', function () { const state = getDefaultFieldFilter(); const targetState = { aggregatable: true, - missing: true, name: 'test', searchable: true, type: 'string', @@ -36,7 +34,6 @@ describe('field_filter', function () { expect(actualState).toMatchInlineSnapshot(` Object { "aggregatable": true, - "missing": true, "name": "test", "searchable": true, "type": "string", @@ -78,9 +75,7 @@ describe('field_filter', function () { { filter: { type: 'string' }, result: ['extension'] }, ].forEach((test) => { const filtered = fieldList - .filter((field) => - isFieldFiltered(field, { ...defaultState, ...test.filter }, { bytes: 1, extension: 1 }) - ) + .filter((field) => doesFieldMatchFilters(field, { ...defaultState, ...test.filter })) .map((field) => field.name); expect(filtered).toEqual(test.result); diff --git a/src/plugins/discover/public/application/main/components/sidebar/lib/field_filter.ts b/src/plugins/discover/public/application/main/components/sidebar/lib/field_filter.ts index 0ca0cea75ad5c..1f2ab0b9b64cd 100644 --- a/src/plugins/discover/public/application/main/components/sidebar/lib/field_filter.ts +++ b/src/plugins/discover/public/application/main/components/sidebar/lib/field_filter.ts @@ -9,7 +9,6 @@ import { DataViewField } from '@kbn/data-views-plugin/public'; export interface FieldFilterState { - missing: boolean; type: string; name: string; aggregatable: null | boolean; @@ -18,7 +17,6 @@ export interface FieldFilterState { export function getDefaultFieldFilter(): FieldFilterState { return { - missing: true, type: 'any', name: '', aggregatable: null, @@ -32,9 +30,7 @@ export function setFieldFilterProp( value: string | boolean | null | undefined ): FieldFilterState { const newState = { ...state }; - if (name === 'missing') { - newState.missing = Boolean(value); - } else if (name === 'aggregatable') { + if (name === 'aggregatable') { newState.aggregatable = typeof value !== 'boolean' ? null : value; } else if (name === 'searchable') { newState.searchable = typeof value !== 'boolean' ? null : value; @@ -46,25 +42,18 @@ export function setFieldFilterProp( return newState; } -export function isFieldFiltered( +export function doesFieldMatchFilters( field: DataViewField, - filterState: FieldFilterState, - fieldCounts: Record + filterState: FieldFilterState ): boolean { const matchFilter = filterState.type === 'any' || field.type === filterState.type; const isAggregatable = filterState.aggregatable === null || field.aggregatable === filterState.aggregatable; const isSearchable = filterState.searchable === null || field.searchable === filterState.searchable; - const scriptedOrMissing = - !filterState.missing || - field.type === '_source' || - field.type === 'unknown_selected' || - field.scripted || - fieldCounts[field.name] > 0; const needle = filterState.name ? filterState.name.toLowerCase() : ''; const haystack = `${field.name}${field.displayName || ''}`.toLowerCase(); const matchName = !filterState.name || haystack.indexOf(needle) !== -1; - return matchFilter && isAggregatable && isSearchable && scriptedOrMissing && matchName; + return matchFilter && isAggregatable && isSearchable && matchName; } diff --git a/src/plugins/discover/public/application/main/components/sidebar/lib/get_data_view_field_list.ts b/src/plugins/discover/public/application/main/components/sidebar/lib/get_data_view_field_list.ts index 224015a10537e..5d055d94184ed 100644 --- a/src/plugins/discover/public/application/main/components/sidebar/lib/get_data_view_field_list.ts +++ b/src/plugins/discover/public/application/main/components/sidebar/lib/get_data_view_field_list.ts @@ -7,18 +7,37 @@ */ import { difference } from 'lodash'; -import { DataView, DataViewField } from '@kbn/data-views-plugin/public'; +import type { DataView, DataViewField } from '@kbn/data-views-plugin/public'; +import { fieldWildcardFilter } from '@kbn/kibana-utils-plugin/public'; import { isNestedFieldParent } from '../../../utils/nested_fields'; -export function getDataViewFieldList(dataView?: DataView, fieldCounts?: Record) { - if (!dataView || !fieldCounts) return []; +export function getDataViewFieldList( + dataView: DataView | undefined | null, + fieldCounts: Record | undefined | null, + isPlainRecord: boolean +): DataViewField[] | null { + if (isPlainRecord && !fieldCounts) { + // still loading data + return null; + } - const fieldNamesInDocs = Object.keys(fieldCounts); - const fieldNamesInDataView = dataView.fields.getAll().map((fld) => fld.name); + const currentFieldCounts = fieldCounts || {}; + const sourceFiltersValues = dataView?.getSourceFiltering?.()?.excludes; + let dataViewFields: DataViewField[] = dataView?.fields.getAll() || []; + + if (sourceFiltersValues) { + const filter = fieldWildcardFilter(sourceFiltersValues, dataView.metaFields); + dataViewFields = dataViewFields.filter((field) => { + return filter(field.name) || currentFieldCounts[field.name]; // don't filter out a field which was present in hits (ex. for text-based queries, selected fields) + }); + } + + const fieldNamesInDocs = Object.keys(currentFieldCounts); + const fieldNamesInDataView = dataViewFields.map((fld) => fld.name); const unknownFields: DataViewField[] = []; difference(fieldNamesInDocs, fieldNamesInDataView).forEach((unknownFieldName) => { - if (isNestedFieldParent(unknownFieldName, dataView)) { + if (dataView && isNestedFieldParent(unknownFieldName, dataView)) { unknownFields.push({ displayName: String(unknownFieldName), name: String(unknownFieldName), @@ -33,5 +52,10 @@ export function getDataViewFieldList(dataView?: DataView, fieldCounts?: Record currentFieldCounts[field.name]) + : dataViewFields), + ...unknownFields, + ]; } diff --git a/src/plugins/discover/public/application/main/components/sidebar/lib/group_fields.test.ts b/src/plugins/discover/public/application/main/components/sidebar/lib/group_fields.test.ts index 10d9d4face166..7dee06ec512bc 100644 --- a/src/plugins/discover/public/application/main/components/sidebar/lib/group_fields.test.ts +++ b/src/plugins/discover/public/application/main/components/sidebar/lib/group_fields.test.ts @@ -6,271 +6,106 @@ * Side Public License, v 1. */ -import { groupFields } from './group_fields'; -import { getDefaultFieldFilter } from './field_filter'; -import { DataViewField } from '@kbn/data-views-plugin/public'; - -const fields = [ - { - name: 'category', - type: 'string', - esTypes: ['text'], - count: 1, - scripted: false, - searchable: true, - aggregatable: true, - readFromDocValues: true, - }, - { - name: 'currency', - type: 'string', - esTypes: ['keyword'], - count: 0, - scripted: false, - searchable: true, - aggregatable: true, - readFromDocValues: true, - }, - { - name: 'customer_birth_date', - type: 'date', - esTypes: ['date'], - count: 0, - scripted: false, - searchable: true, - aggregatable: true, - readFromDocValues: true, - }, -]; - -const fieldCounts = { - category: 1, - currency: 1, - customer_birth_date: 1, - unknown_field: 1, -}; +import { type DataViewField } from '@kbn/data-plugin/common'; +import { stubLogstashDataView as dataView } from '@kbn/data-views-plugin/common/data_view.stub'; +import { getSelectedFields, shouldShowField, INITIAL_SELECTED_FIELDS_RESULT } from './group_fields'; describe('group_fields', function () { - it('should group fields in selected, popular, unpopular group', function () { - const fieldFilterState = getDefaultFieldFilter(); - - const actual = groupFields( - fields as DataViewField[], - ['currency'], - 5, - fieldCounts, - fieldFilterState, - false - ); + it('should pick fields as unknown_selected if they are unknown', function () { + const actual = getSelectedFields(dataView, ['currency']); expect(actual).toMatchInlineSnapshot(` Object { - "popular": Array [ + "selectedFields": Array [ Object { - "aggregatable": true, - "count": 1, - "esTypes": Array [ - "text", - ], - "name": "category", - "readFromDocValues": true, - "scripted": false, - "searchable": true, - "type": "string", - }, - ], - "selected": Array [ - Object { - "aggregatable": true, - "count": 0, - "esTypes": Array [ - "keyword", - ], + "displayName": "currency", "name": "currency", - "readFromDocValues": true, - "scripted": false, - "searchable": true, - "type": "string", - }, - ], - "unpopular": Array [ - Object { - "aggregatable": true, - "count": 0, - "esTypes": Array [ - "date", - ], - "name": "customer_birth_date", - "readFromDocValues": true, - "scripted": false, - "searchable": true, - "type": "date", + "type": "unknown_selected", }, ], + "selectedFieldsMap": Object { + "currency": true, + }, } `); }); - it('should group fields in selected, popular, unpopular group if they contain multifields', function () { - const category = { - name: 'category', - type: 'string', - esTypes: ['text'], - count: 1, - scripted: false, - searchable: true, - aggregatable: true, - readFromDocValues: true, - }; - const currency = { - name: 'currency', - displayName: 'currency', - kbnFieldType: { - esTypes: ['string', 'text', 'keyword', '_type', '_id'], - filterable: true, - name: 'string', - sortable: true, - }, - spec: { - esTypes: ['text'], - name: 'category', - }, - scripted: false, - searchable: true, - aggregatable: true, - readFromDocValues: true, - }; - const currencyKeyword = { - name: 'currency.keyword', - displayName: 'currency.keyword', - type: 'string', - esTypes: ['keyword'], - kbnFieldType: { - esTypes: ['string', 'text', 'keyword', '_type', '_id'], - filterable: true, - name: 'string', - sortable: true, - }, - spec: { - aggregatable: true, - esTypes: ['keyword'], - name: 'category.keyword', - readFromDocValues: true, - searchable: true, - shortDotsEnable: false, - subType: { - multi: { - parent: 'currency', - }, - }, - }, - scripted: false, - searchable: true, - aggregatable: true, - readFromDocValues: false, - }; - const fieldsToGroup = [category, currency, currencyKeyword] as DataViewField[]; - const fieldFilterState = getDefaultFieldFilter(); - - const actual = groupFields(fieldsToGroup, ['currency'], 5, fieldCounts, fieldFilterState, true); + it('should work correctly if no columns selected', function () { + expect(getSelectedFields(dataView, [])).toBe(INITIAL_SELECTED_FIELDS_RESULT); + expect(getSelectedFields(dataView, ['_source'])).toBe(INITIAL_SELECTED_FIELDS_RESULT); + }); - expect(actual.popular).toEqual([category]); - expect(actual.selected).toEqual([currency]); - expect(actual.unpopular).toEqual([]); + it('should pick fields into selected group', function () { + const actual = getSelectedFields(dataView, ['bytes', '@timestamp']); + expect(actual.selectedFields.map((field) => field.name)).toEqual(['bytes', '@timestamp']); + expect(actual.selectedFieldsMap).toStrictEqual({ + bytes: true, + '@timestamp': true, + }); }); - it('should sort selected fields by columns order ', function () { - const fieldFilterState = getDefaultFieldFilter(); + it('should pick fields into selected group if they contain multifields', function () { + const actual = getSelectedFields(dataView, ['machine.os', 'machine.os.raw']); + expect(actual.selectedFields.map((field) => field.name)).toEqual([ + 'machine.os', + 'machine.os.raw', + ]); + expect(actual.selectedFieldsMap).toStrictEqual({ + 'machine.os': true, + 'machine.os.raw': true, + }); + }); - const actual1 = groupFields( - fields as DataViewField[], - ['customer_birth_date', 'currency', 'unknown'], - 5, - fieldCounts, - fieldFilterState, - false - ); - expect(actual1.selected.map((field) => field.name)).toEqual([ - 'customer_birth_date', - 'currency', + it('should sort selected fields by columns order', function () { + const actual1 = getSelectedFields(dataView, ['bytes', 'extension.keyword', 'unknown']); + expect(actual1.selectedFields.map((field) => field.name)).toEqual([ + 'bytes', + 'extension.keyword', 'unknown', ]); + expect(actual1.selectedFieldsMap).toStrictEqual({ + bytes: true, + 'extension.keyword': true, + unknown: true, + }); - const actual2 = groupFields( - fields as DataViewField[], - ['currency', 'customer_birth_date', 'unknown'], - 5, - fieldCounts, - fieldFilterState, - false - ); - expect(actual2.selected.map((field) => field.name)).toEqual([ - 'currency', - 'customer_birth_date', + const actual2 = getSelectedFields(dataView, ['extension', 'bytes', 'unknown']); + expect(actual2.selectedFields.map((field) => field.name)).toEqual([ + 'extension', + 'bytes', 'unknown', ]); + expect(actual2.selectedFieldsMap).toStrictEqual({ + extension: true, + bytes: true, + unknown: true, + }); }); - it('should filter fields by a given name', function () { - const fieldFilterState = { ...getDefaultFieldFilter(), ...{ name: 'curr' } }; + it('should show any fields if for text-based searches', function () { + expect(shouldShowField(dataView.getFieldByName('bytes'), true)).toBe(true); + expect(shouldShowField({ type: 'unknown', name: 'unknown' } as DataViewField, true)).toBe(true); + expect(shouldShowField({ type: '_source', name: 'source' } as DataViewField, true)).toBe(false); + }); - const actual1 = groupFields( - fields as DataViewField[], - ['customer_birth_date', 'currency', 'unknown'], - 5, - fieldCounts, - fieldFilterState, + it('should show fields excluding subfields when searched from source', function () { + expect(shouldShowField(dataView.getFieldByName('extension'), false)).toBe(true); + expect(shouldShowField(dataView.getFieldByName('extension.keyword'), false)).toBe(false); + expect(shouldShowField({ type: 'unknown', name: 'unknown' } as DataViewField, false)).toBe( + true + ); + expect(shouldShowField({ type: '_source', name: 'source' } as DataViewField, false)).toBe( false ); - expect(actual1.selected.map((field) => field.name)).toEqual(['currency']); }); - it('excludes unmapped fields if showUnmappedFields set to false', function () { - const fieldFilterState = getDefaultFieldFilter(); - const fieldsWithUnmappedField = [...fields]; - fieldsWithUnmappedField.push({ - name: 'unknown_field', - type: 'unknown', - esTypes: ['unknown'], - count: 1, - scripted: false, - searchable: true, - aggregatable: true, - readFromDocValues: true, - }); - - const actual = groupFields( - fieldsWithUnmappedField as DataViewField[], - ['customer_birth_date', 'currency'], - 5, - fieldCounts, - fieldFilterState, + it('should show fields excluding subfields when fields api is used', function () { + expect(shouldShowField(dataView.getFieldByName('extension'), false)).toBe(true); + expect(shouldShowField(dataView.getFieldByName('extension.keyword'), false)).toBe(false); + expect(shouldShowField({ type: 'unknown', name: 'unknown' } as DataViewField, false)).toBe( true ); - expect(actual.unpopular).toEqual([]); - }); - - it('includes unmapped fields when reading from source', function () { - const fieldFilterState = getDefaultFieldFilter(); - const fieldsWithUnmappedField = [...fields]; - fieldsWithUnmappedField.push({ - name: 'unknown_field', - type: 'unknown', - esTypes: ['unknown'], - count: 0, - scripted: false, - searchable: false, - aggregatable: false, - readFromDocValues: false, - }); - - const actual = groupFields( - fieldsWithUnmappedField as DataViewField[], - ['customer_birth_date', 'currency'], - 5, - fieldCounts, - fieldFilterState, + expect(shouldShowField({ type: '_source', name: 'source' } as DataViewField, false)).toBe( false ); - expect(actual.unpopular.map((field) => field.name)).toEqual(['unknown_field']); }); }); diff --git a/src/plugins/discover/public/application/main/components/sidebar/lib/group_fields.tsx b/src/plugins/discover/public/application/main/components/sidebar/lib/group_fields.tsx index 0d7d535538c44..eaae1c90d3833 100644 --- a/src/plugins/discover/public/application/main/components/sidebar/lib/group_fields.tsx +++ b/src/plugins/discover/public/application/main/components/sidebar/lib/group_fields.tsx @@ -6,90 +6,66 @@ * Side Public License, v 1. */ -import { DataViewField, getFieldSubtypeMulti } from '@kbn/data-views-plugin/public'; -import { FieldFilterState, isFieldFiltered } from './field_filter'; +import { uniqBy } from 'lodash'; +import { + type DataViewField, + type DataView, + getFieldSubtypeMulti, +} from '@kbn/data-views-plugin/public'; -interface GroupedFields { - selected: DataViewField[]; - popular: DataViewField[]; - unpopular: DataViewField[]; +export function shouldShowField(field: DataViewField | undefined, isPlainRecord: boolean): boolean { + if (!field?.type || field.type === '_source') { + return false; + } + if (isPlainRecord) { + // exclude only `_source` for plain records + return true; + } + // exclude subfields + return !getFieldSubtypeMulti(field?.spec); } -/** - * group the fields into selected, popular and unpopular, filter by fieldFilterState - */ -export function groupFields( - fields: DataViewField[] | null, - columns: string[], - popularLimit: number, - fieldCounts: Record | undefined, - fieldFilterState: FieldFilterState, - useNewFieldsApi: boolean -): GroupedFields { - const showUnmappedFields = useNewFieldsApi; - const result: GroupedFields = { - selected: [], - popular: [], - unpopular: [], - }; - if (!Array.isArray(fields) || !Array.isArray(columns) || typeof fieldCounts !== 'object') { - return result; - } +// to avoid rerenderings for empty state +export const INITIAL_SELECTED_FIELDS_RESULT = { + selectedFields: [], + selectedFieldsMap: {}, +}; - const popular = fields - .filter((field) => !columns.includes(field.name) && field.count) - .sort((a: DataViewField, b: DataViewField) => (b.count || 0) - (a.count || 0)) - .map((field) => field.name) - .slice(0, popularLimit); +export interface SelectedFieldsResult { + selectedFields: DataViewField[]; + selectedFieldsMap: Record; +} - const compareFn = (a: DataViewField, b: DataViewField) => { - if (!a.displayName) { - return 0; - } - return a.displayName.localeCompare(b.displayName || ''); +export function getSelectedFields( + dataView: DataView | undefined, + columns: string[] +): SelectedFieldsResult { + const result: SelectedFieldsResult = { + selectedFields: [], + selectedFieldsMap: {}, }; - const fieldsSorted = fields.sort(compareFn); - - for (const field of fieldsSorted) { - if (!isFieldFiltered(field, fieldFilterState, fieldCounts)) { - continue; - } - - const subTypeMulti = getFieldSubtypeMulti(field?.spec); - const isSubfield = useNewFieldsApi && subTypeMulti; - if (columns.includes(field.name)) { - result.selected.push(field); - } else if (popular.includes(field.name) && field.type !== '_source') { - if (!isSubfield) { - result.popular.push(field); - } - } else if (field.type !== '_source') { - // do not show unmapped fields unless explicitly specified - // do not add subfields to this list - if (useNewFieldsApi && (field.type !== 'unknown' || showUnmappedFields) && !isSubfield) { - result.unpopular.push(field); - } else if (!useNewFieldsApi) { - result.unpopular.push(field); - } - } + if (!Array.isArray(columns) || !columns.length) { + return INITIAL_SELECTED_FIELDS_RESULT; } + // add selected columns, that are not part of the data view, to be removable for (const column of columns) { - const tmpField = { - name: column, - displayName: column, - type: 'unknown_selected', - } as DataViewField; - if ( - !result.selected.find((field) => field.name === column) && - isFieldFiltered(tmpField, fieldFilterState, fieldCounts) - ) { - result.selected.push(tmpField); - } + const selectedField = + dataView?.getFieldByName?.(column) || + ({ + name: column, + displayName: column, + type: 'unknown_selected', + } as DataViewField); + result.selectedFields.push(selectedField); + result.selectedFieldsMap[selectedField.name] = true; + } + + result.selectedFields = uniqBy(result.selectedFields, 'name'); + + if (result.selectedFields.length === 1 && result.selectedFields[0].name === '_source') { + return INITIAL_SELECTED_FIELDS_RESULT; } - result.selected.sort((a, b) => { - return columns.indexOf(a.name) - columns.indexOf(b.name); - }); return result; } diff --git a/src/plugins/discover/public/application/main/components/sidebar/lib/sidebar_reducer.test.ts b/src/plugins/discover/public/application/main/components/sidebar/lib/sidebar_reducer.test.ts new file mode 100644 index 0000000000000..131f9c358317f --- /dev/null +++ b/src/plugins/discover/public/application/main/components/sidebar/lib/sidebar_reducer.test.ts @@ -0,0 +1,220 @@ +/* + * Copyright 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 { + stubDataViewWithoutTimeField, + stubLogstashDataView as dataView, +} from '@kbn/data-views-plugin/common/data_view.stub'; +import { + discoverSidebarReducer, + DiscoverSidebarReducerActionType, + DiscoverSidebarReducerState, + DiscoverSidebarReducerStatus, + getInitialState, +} from './sidebar_reducer'; +import { DataViewField } from '@kbn/data-views-plugin/common'; + +describe('sidebar reducer', function () { + it('should set an initial state', function () { + expect(getInitialState(dataView)).toEqual( + expect.objectContaining({ + dataView, + allFields: null, + fieldCounts: null, + status: DiscoverSidebarReducerStatus.INITIAL, + }) + ); + }); + + it('should handle "documents loading" action', function () { + const state: DiscoverSidebarReducerState = { + ...getInitialState(dataView), + allFields: [dataView.fields[0]], + }; + const resultForDocuments = discoverSidebarReducer(state, { + type: DiscoverSidebarReducerActionType.DOCUMENTS_LOADING, + payload: { + isPlainRecord: false, + }, + }); + expect(resultForDocuments).toEqual( + expect.objectContaining({ + dataView, + allFields: state.allFields, + fieldCounts: null, + status: DiscoverSidebarReducerStatus.PROCESSING, + }) + ); + const resultForTextBasedQuery = discoverSidebarReducer(state, { + type: DiscoverSidebarReducerActionType.DOCUMENTS_LOADING, + payload: { + isPlainRecord: true, + }, + }); + expect(resultForTextBasedQuery).toEqual( + expect.objectContaining({ + dataView, + allFields: null, + fieldCounts: null, + status: DiscoverSidebarReducerStatus.PROCESSING, + }) + ); + }); + + it('should handle "documents loaded" action', function () { + const dataViewFieldName = stubDataViewWithoutTimeField.fields[0].name; + const unmappedFieldName = 'field1'; + const fieldCounts = { [unmappedFieldName]: 1, [dataViewFieldName]: 1 }; + const state: DiscoverSidebarReducerState = getInitialState(dataView); + const resultForDocuments = discoverSidebarReducer(state, { + type: DiscoverSidebarReducerActionType.DOCUMENTS_LOADED, + payload: { + isPlainRecord: false, + dataView: stubDataViewWithoutTimeField, + fieldCounts, + }, + }); + expect(resultForDocuments).toStrictEqual({ + dataView: stubDataViewWithoutTimeField, + allFields: [ + ...stubDataViewWithoutTimeField.fields, + // merging in unmapped fields + { + displayName: unmappedFieldName, + name: unmappedFieldName, + type: 'unknown', + } as DataViewField, + ], + fieldCounts, + status: DiscoverSidebarReducerStatus.COMPLETED, + }); + + const resultForTextBasedQuery = discoverSidebarReducer(state, { + type: DiscoverSidebarReducerActionType.DOCUMENTS_LOADED, + payload: { + isPlainRecord: true, + dataView: stubDataViewWithoutTimeField, + fieldCounts, + }, + }); + expect(resultForTextBasedQuery).toStrictEqual({ + dataView: stubDataViewWithoutTimeField, + allFields: [ + stubDataViewWithoutTimeField.fields.find((field) => field.name === dataViewFieldName), + // merging in unmapped fields + { + displayName: 'field1', + name: 'field1', + type: 'unknown', + } as DataViewField, + ], + fieldCounts, + status: DiscoverSidebarReducerStatus.COMPLETED, + }); + + const resultForTextBasedQueryWhileLoading = discoverSidebarReducer(state, { + type: DiscoverSidebarReducerActionType.DOCUMENTS_LOADED, + payload: { + isPlainRecord: true, + dataView: stubDataViewWithoutTimeField, + fieldCounts: null, + }, + }); + expect(resultForTextBasedQueryWhileLoading).toStrictEqual({ + dataView: stubDataViewWithoutTimeField, + allFields: null, + fieldCounts: null, + status: DiscoverSidebarReducerStatus.PROCESSING, + }); + }); + + it('should handle "data view switched" action', function () { + const state: DiscoverSidebarReducerState = getInitialState(dataView); + const resultForTheSameDataView = discoverSidebarReducer(state, { + type: DiscoverSidebarReducerActionType.DATA_VIEW_SWITCHED, + payload: { + dataView: state.dataView, + }, + }); + expect(resultForTheSameDataView).toBe(state); + + const resultForAnotherDataView = discoverSidebarReducer(state, { + type: DiscoverSidebarReducerActionType.DATA_VIEW_SWITCHED, + payload: { + dataView: stubDataViewWithoutTimeField, + }, + }); + expect(resultForAnotherDataView).toStrictEqual({ + dataView: stubDataViewWithoutTimeField, + allFields: null, + fieldCounts: null, + status: DiscoverSidebarReducerStatus.INITIAL, + }); + + const resultForAnotherDataViewAfterProcessing = discoverSidebarReducer( + { + ...state, + status: DiscoverSidebarReducerStatus.PROCESSING, + }, + { + type: DiscoverSidebarReducerActionType.DATA_VIEW_SWITCHED, + payload: { + dataView: stubDataViewWithoutTimeField, + }, + } + ); + expect(resultForAnotherDataViewAfterProcessing).toStrictEqual({ + dataView: stubDataViewWithoutTimeField, + allFields: null, + fieldCounts: null, + status: DiscoverSidebarReducerStatus.PROCESSING, + }); + + const resultForAnotherDataViewAfterCompleted = discoverSidebarReducer( + { + ...state, + status: DiscoverSidebarReducerStatus.COMPLETED, + }, + { + type: DiscoverSidebarReducerActionType.DATA_VIEW_SWITCHED, + payload: { + dataView: stubDataViewWithoutTimeField, + }, + } + ); + expect(resultForAnotherDataViewAfterCompleted).toStrictEqual({ + dataView: stubDataViewWithoutTimeField, + allFields: null, + fieldCounts: null, + status: DiscoverSidebarReducerStatus.INITIAL, + }); + }); + + it('should handle "reset" action', function () { + const state: DiscoverSidebarReducerState = { + ...getInitialState(dataView), + allFields: [dataView.fields[0]], + fieldCounts: {}, + status: DiscoverSidebarReducerStatus.COMPLETED, + }; + const resultForDocuments = discoverSidebarReducer(state, { + type: DiscoverSidebarReducerActionType.RESET, + payload: { + dataView: stubDataViewWithoutTimeField, + }, + }); + expect(resultForDocuments).toEqual( + expect.objectContaining({ + dataView: stubDataViewWithoutTimeField, + allFields: null, + fieldCounts: null, + status: DiscoverSidebarReducerStatus.INITIAL, + }) + ); + }); +}); diff --git a/src/plugins/discover/public/application/main/components/sidebar/lib/sidebar_reducer.ts b/src/plugins/discover/public/application/main/components/sidebar/lib/sidebar_reducer.ts new file mode 100644 index 0000000000000..0c579275029b1 --- /dev/null +++ b/src/plugins/discover/public/application/main/components/sidebar/lib/sidebar_reducer.ts @@ -0,0 +1,115 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { type DataView, type DataViewField } from '@kbn/data-views-plugin/common'; +import { getDataViewFieldList } from './get_data_view_field_list'; + +export enum DiscoverSidebarReducerActionType { + RESET = 'RESET', + DATA_VIEW_SWITCHED = 'DATA_VIEW_SWITCHED', + DOCUMENTS_LOADED = 'DOCUMENTS_LOADED', + DOCUMENTS_LOADING = 'DOCUMENTS_LOADING', +} + +type DiscoverSidebarReducerAction = + | { + type: DiscoverSidebarReducerActionType.RESET; + payload: { + dataView: DataView | null | undefined; + }; + } + | { + type: DiscoverSidebarReducerActionType.DATA_VIEW_SWITCHED; + payload: { + dataView: DataView | null | undefined; + }; + } + | { + type: DiscoverSidebarReducerActionType.DOCUMENTS_LOADING; + payload: { + isPlainRecord: boolean; + }; + } + | { + type: DiscoverSidebarReducerActionType.DOCUMENTS_LOADED; + payload: { + fieldCounts: DiscoverSidebarReducerState['fieldCounts']; + isPlainRecord: boolean; + dataView: DataView | null | undefined; + }; + }; + +export enum DiscoverSidebarReducerStatus { + INITIAL = 'INITIAL', + PROCESSING = 'PROCESSING', + COMPLETED = 'COMPLETED', +} + +export interface DiscoverSidebarReducerState { + dataView: DataView | null | undefined; + allFields: DataViewField[] | null; + fieldCounts: Record | null; + status: DiscoverSidebarReducerStatus; +} + +export function getInitialState(dataView?: DataView | null): DiscoverSidebarReducerState { + return { + dataView, + allFields: null, + fieldCounts: null, + status: DiscoverSidebarReducerStatus.INITIAL, + }; +} + +export function discoverSidebarReducer( + state: DiscoverSidebarReducerState, + action: DiscoverSidebarReducerAction +): DiscoverSidebarReducerState { + switch (action.type) { + case DiscoverSidebarReducerActionType.RESET: + return getInitialState(action.payload.dataView); + case DiscoverSidebarReducerActionType.DATA_VIEW_SWITCHED: + return state.dataView === action.payload.dataView + ? state // already updated in `DOCUMENTS_LOADED` + : { + ...state, + dataView: action.payload.dataView, + fieldCounts: null, + allFields: null, + status: + state.status === DiscoverSidebarReducerStatus.COMPLETED + ? DiscoverSidebarReducerStatus.INITIAL + : state.status, + }; + case DiscoverSidebarReducerActionType.DOCUMENTS_LOADING: + return { + ...state, + fieldCounts: null, + allFields: action.payload.isPlainRecord ? null : state.allFields, + status: DiscoverSidebarReducerStatus.PROCESSING, + }; + case DiscoverSidebarReducerActionType.DOCUMENTS_LOADED: + const mappedAndUnmappedFields = getDataViewFieldList( + action.payload.dataView, + action.payload.fieldCounts, + action.payload.isPlainRecord + ); + return { + ...state, + dataView: action.payload.dataView, + fieldCounts: action.payload.fieldCounts, + allFields: mappedAndUnmappedFields, + status: + mappedAndUnmappedFields === null + ? DiscoverSidebarReducerStatus.PROCESSING + : DiscoverSidebarReducerStatus.COMPLETED, + }; + } + + return state; +} diff --git a/src/plugins/discover/public/application/main/components/sidebar/types.ts b/src/plugins/discover/public/application/main/components/sidebar/types.ts deleted file mode 100644 index 45921f676f144..0000000000000 --- a/src/plugins/discover/public/application/main/components/sidebar/types.ts +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -export interface IndexPatternRef { - id: string; - title: string; - name?: string; -} - -export interface FieldDetails { - error: string; - exists: number; - total: number; - buckets: Bucket[]; -} - -export interface Bucket { - display: string; - value: string; - percent: number; - count: number; -} diff --git a/src/plugins/discover/public/application/main/components/top_nav/open_search_panel.tsx b/src/plugins/discover/public/application/main/components/top_nav/open_search_panel.tsx index 81572cf1ebb53..665ed87323188 100644 --- a/src/plugins/discover/public/application/main/components/top_nav/open_search_panel.tsx +++ b/src/plugins/discover/public/application/main/components/top_nav/open_search_panel.tsx @@ -7,7 +7,7 @@ */ import React from 'react'; -import rison from 'rison-node'; +import rison from '@kbn/rison'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; import { diff --git a/src/plugins/discover/public/application/main/discover_main_app.test.tsx b/src/plugins/discover/public/application/main/discover_main_app.test.tsx index 599eaa57aea3d..c487d78836564 100644 --- a/src/plugins/discover/public/application/main/discover_main_app.test.tsx +++ b/src/plugins/discover/public/application/main/discover_main_app.test.tsx @@ -6,6 +6,7 @@ * Side Public License, v 1. */ import React from 'react'; +import { act } from 'react-dom/test-utils'; import { mountWithIntl } from '@kbn/test-jest-helpers'; import { DataViewListItem } from '@kbn/data-views-plugin/public'; import { dataViewMock } from '../../__mocks__/data_view'; @@ -23,7 +24,7 @@ setHeaderActionMenuMounter(jest.fn()); setUrlTracker(urlTrackerMock); describe('DiscoverMainApp', () => { - test('renders', () => { + test('renders', async () => { const dataViewList = [dataViewMock].map((ip) => { return { ...ip, ...{ attributes: { title: ip.title } } }; }) as unknown as DataViewListItem[]; @@ -35,15 +36,21 @@ describe('DiscoverMainApp', () => { initialEntries: ['/'], }); - const component = mountWithIntl( - - - - - - ); + await act(async () => { + const component = await mountWithIntl( + + + + + + ); - expect(component.find(DiscoverTopNav).exists()).toBe(true); - expect(component.find(DiscoverTopNav).prop('dataView')).toEqual(dataViewMock); + // wait for lazy modules + await new Promise((resolve) => setTimeout(resolve, 0)); + await component.update(); + + expect(component.find(DiscoverTopNav).exists()).toBe(true); + expect(component.find(DiscoverTopNav).prop('dataView')).toEqual(dataViewMock); + }); }); }); diff --git a/src/plugins/discover/public/application/main/discover_main_app.tsx b/src/plugins/discover/public/application/main/discover_main_app.tsx index 8ae4bbea6e269..06fe8031829f9 100644 --- a/src/plugins/discover/public/application/main/discover_main_app.tsx +++ b/src/plugins/discover/public/application/main/discover_main_app.tsx @@ -64,6 +64,7 @@ export function DiscoverMainApp(props: DiscoverMainProps) { stateContainer, adHocDataViewList, savedDataViewList, + searchSessionManager, } = useDiscoverState({ services, history: usedHistory, @@ -125,6 +126,7 @@ export function DiscoverMainApp(props: DiscoverMainProps) { updateDataViewList={updateDataViewList} adHocDataViewList={adHocDataViewList} savedDataViewList={savedDataViewList} + searchSessionManager={searchSessionManager} /> ); diff --git a/src/plugins/discover/public/application/main/hooks/use_discover_state.ts b/src/plugins/discover/public/application/main/hooks/use_discover_state.ts index 8d40b20fbc9cd..df61c909c6a8e 100644 --- a/src/plugins/discover/public/application/main/hooks/use_discover_state.ts +++ b/src/plugins/discover/public/application/main/hooks/use_discover_state.ts @@ -194,10 +194,12 @@ export function useDiscoverState({ */ useEffect(() => { const unsubscribe = appStateContainer.subscribe(async (nextState) => { - const { hideChart, interval, sort, index } = state; - // chart was hidden, now it should be displayed, so data is needed - const chartDisplayChanged = nextState.hideChart !== hideChart && hideChart; + const { hideChart, interval, breakdownField, sort, index } = state; + // Cast to boolean to avoid false positives when comparing + // undefined and false, which would trigger a refetch + const chartDisplayChanged = Boolean(nextState.hideChart) !== Boolean(hideChart); const chartIntervalChanged = nextState.interval !== interval; + const breakdownFieldChanged = nextState.breakdownField !== breakdownField; const docTableSortChanged = !isEqual(nextState.sort, sort); const dataViewChanged = !isEqual(nextState.index, index); // NOTE: this is also called when navigating from discover app to context app @@ -230,9 +232,15 @@ export function useDiscoverState({ reset(); } - if (chartDisplayChanged || chartIntervalChanged || docTableSortChanged) { + if ( + chartDisplayChanged || + chartIntervalChanged || + breakdownFieldChanged || + docTableSortChanged + ) { refetch$.next(undefined); } + setState(nextState); }); return () => unsubscribe(); @@ -326,5 +334,6 @@ export function useDiscoverState({ persistDataView, updateAdHocDataViewId, updateDataViewList, + searchSessionManager, }; } diff --git a/src/plugins/discover/public/application/main/hooks/use_inspector.test.ts b/src/plugins/discover/public/application/main/hooks/use_inspector.test.ts index 66c5542f5647a..60327e84f2cd8 100644 --- a/src/plugins/discover/public/application/main/hooks/use_inspector.test.ts +++ b/src/plugins/discover/public/application/main/hooks/use_inspector.test.ts @@ -10,15 +10,23 @@ import { renderHook } from '@testing-library/react-hooks'; import { discoverServiceMock } from '../../../__mocks__/services'; import { savedSearchMock } from '../../../__mocks__/saved_search'; import { useInspector } from './use_inspector'; -import { RequestAdapter } from '@kbn/inspector-plugin/common'; +import { Adapters, RequestAdapter } from '@kbn/inspector-plugin/common'; +import { OverlayRef } from '@kbn/core/public'; +import { AggregateRequestAdapter } from '../utils/aggregate_request_adapter'; describe('test useInspector', () => { test('inspector open function is executed, expanded doc is closed', async () => { const setExpandedDoc = jest.fn(); - + let adapters: Adapters | undefined; + jest.spyOn(discoverServiceMock.inspector, 'open').mockImplementation((localAdapters) => { + adapters = localAdapters; + return {} as OverlayRef; + }); + const requests = new RequestAdapter(); + const lensRequests = new RequestAdapter(); const { result } = renderHook(() => { return useInspector({ - inspectorAdapters: { requests: new RequestAdapter() }, + inspectorAdapters: { requests, lensRequests }, savedSearch: savedSearchMock, inspector: discoverServiceMock.inspector, setExpandedDoc, @@ -27,5 +35,10 @@ describe('test useInspector', () => { result.current(); expect(setExpandedDoc).toHaveBeenCalledWith(undefined); expect(discoverServiceMock.inspector.open).toHaveBeenCalled(); + expect(adapters?.requests).toBeInstanceOf(AggregateRequestAdapter); + expect(adapters?.requests?.getRequests()).toEqual([ + ...requests.getRequests(), + ...lensRequests.getRequests(), + ]); }); }); diff --git a/src/plugins/discover/public/application/main/hooks/use_inspector.ts b/src/plugins/discover/public/application/main/hooks/use_inspector.ts index c7bcc0ba1cb4b..e23ca6425aa71 100644 --- a/src/plugins/discover/public/application/main/hooks/use_inspector.ts +++ b/src/plugins/discover/public/application/main/hooks/use_inspector.ts @@ -14,6 +14,12 @@ import { } from '@kbn/inspector-plugin/public'; import { SavedSearch } from '@kbn/saved-search-plugin/public'; import { DataTableRecord } from '../../../types'; +import { AggregateRequestAdapter } from '../utils/aggregate_request_adapter'; + +export interface InspectorAdapters { + requests: RequestAdapter; + lensRequests?: RequestAdapter; +} export function useInspector({ setExpandedDoc, @@ -21,7 +27,7 @@ export function useInspector({ inspectorAdapters, savedSearch, }: { - inspectorAdapters: { requests: RequestAdapter }; + inspectorAdapters: InspectorAdapters; savedSearch: SavedSearch; setExpandedDoc: (doc?: DataTableRecord) => void; inspector: InspectorPublicPluginStart; @@ -31,11 +37,24 @@ export function useInspector({ const onOpenInspector = useCallback(() => { // prevent overlapping setExpandedDoc(undefined); - const session = inspector.open(inspectorAdapters, { - title: savedSearch.title, - }); + + const requestAdapters = inspectorAdapters.lensRequests + ? [inspectorAdapters.requests, inspectorAdapters.lensRequests] + : [inspectorAdapters.requests]; + + const session = inspector.open( + { requests: new AggregateRequestAdapter(requestAdapters) }, + { title: savedSearch.title } + ); + setInspectorSession(session); - }, [setExpandedDoc, inspectorAdapters, savedSearch, inspector]); + }, [ + setExpandedDoc, + inspectorAdapters.lensRequests, + inspectorAdapters.requests, + inspector, + savedSearch.title, + ]); useEffect(() => { return () => { diff --git a/src/plugins/discover/public/application/main/hooks/use_saved_search.test.ts b/src/plugins/discover/public/application/main/hooks/use_saved_search.test.ts index 34cdeb232be88..f46378053d355 100644 --- a/src/plugins/discover/public/application/main/hooks/use_saved_search.test.ts +++ b/src/plugins/discover/public/application/main/hooks/use_saved_search.test.ts @@ -45,7 +45,6 @@ describe('test useSavedSearch', () => { expect(result.current.data$.main$.getValue().fetchStatus).toBe(FetchStatus.LOADING); expect(result.current.data$.documents$.getValue().fetchStatus).toBe(FetchStatus.LOADING); expect(result.current.data$.totalHits$.getValue().fetchStatus).toBe(FetchStatus.LOADING); - expect(result.current.data$.charts$.getValue().fetchStatus).toBe(FetchStatus.LOADING); }); test('refetch$ triggers a search', async () => { const { history, searchSessionManager } = createSearchSessionMock(); diff --git a/src/plugins/discover/public/application/main/hooks/use_saved_search.ts b/src/plugins/discover/public/application/main/hooks/use_saved_search.ts index 2f097daac982d..4f48945daaad1 100644 --- a/src/plugins/discover/public/application/main/hooks/use_saved_search.ts +++ b/src/plugins/discover/public/application/main/hooks/use_saved_search.ts @@ -25,19 +25,18 @@ import { useBehaviorSubject } from './use_behavior_subject'; import { sendResetMsg } from './use_saved_search_messages'; import { getFetch$ } from '../utils/get_fetch_observable'; import type { DataTableRecord } from '../../../types'; +import type { InspectorAdapters } from './use_inspector'; export interface SavedSearchData { main$: DataMain$; documents$: DataDocuments$; totalHits$: DataTotalHits$; - charts$: DataCharts$; availableFields$: AvailableFields$; } export type DataMain$ = BehaviorSubject; export type DataDocuments$ = BehaviorSubject; export type DataTotalHits$ = BehaviorSubject; -export type DataCharts$ = BehaviorSubject; export type AvailableFields$ = BehaviorSubject; export type DataRefetch$ = Subject; @@ -46,7 +45,7 @@ export interface UseSavedSearch { refetch$: DataRefetch$; data$: SavedSearchData; reset: () => void; - inspectorAdapters: { requests: RequestAdapter }; + inspectorAdapters: InspectorAdapters; } export enum RecordRawType { @@ -78,8 +77,6 @@ export interface DataDocumentsMsg extends DataMsg { } export interface DataTotalHitsMsg extends DataMsg { - fetchStatus: FetchStatus; - error?: Error; result?: number; } @@ -128,7 +125,6 @@ export const useSavedSearch = ({ const main$: DataMain$ = useBehaviorSubject(initialState) as DataMain$; const documents$: DataDocuments$ = useBehaviorSubject(initialState) as DataDocuments$; const totalHits$: DataTotalHits$ = useBehaviorSubject(initialState) as DataTotalHits$; - const charts$: DataCharts$ = useBehaviorSubject(initialState) as DataCharts$; const availableFields$: AvailableFields$ = useBehaviorSubject(initialState) as AvailableFields$; const dataSubjects = useMemo(() => { @@ -136,10 +132,9 @@ export const useSavedSearch = ({ main$, documents$, totalHits$, - charts$, availableFields$, }; - }, [main$, charts$, documents$, totalHits$, availableFields$]); + }, [main$, documents$, totalHits$, availableFields$]); /** * The observable to trigger data fetching in UI diff --git a/src/plugins/discover/public/application/main/hooks/use_saved_search_messages.test.ts b/src/plugins/discover/public/application/main/hooks/use_saved_search_messages.test.ts index 1159aee1c5d13..5973d679b6b1c 100644 --- a/src/plugins/discover/public/application/main/hooks/use_saved_search_messages.test.ts +++ b/src/plugins/discover/public/application/main/hooks/use_saved_search_messages.test.ts @@ -6,8 +6,10 @@ * Side Public License, v 1. */ import { + checkHitCount, sendCompleteMsg, sendErrorMsg, + sendErrorTo, sendLoadingMsg, sendNoResultsFoundMsg, sendPartialMsg, @@ -16,6 +18,7 @@ import { FetchStatus } from '../../types'; import { BehaviorSubject } from 'rxjs'; import { DataMainMsg, RecordRawType } from './use_saved_search'; import { filter } from 'rxjs/operators'; +import { dataPluginMock } from '@kbn/data-plugin/public/mocks'; describe('test useSavedSearch message generators', () => { test('sendCompleteMsg', (done) => { @@ -62,7 +65,10 @@ describe('test useSavedSearch message generators', () => { done(); } }); - sendLoadingMsg(main$, RecordRawType.DOCUMENT); + sendLoadingMsg(main$, { + foundDocuments: true, + recordRawType: RecordRawType.DOCUMENT, + }); }); test('sendErrorMsg', (done) => { const main$ = new BehaviorSubject({ fetchStatus: FetchStatus.PARTIAL }); @@ -92,4 +98,36 @@ describe('test useSavedSearch message generators', () => { }); sendCompleteMsg(main$, false); }); + + test('sendErrorTo', (done) => { + const main$ = new BehaviorSubject({ fetchStatus: FetchStatus.PARTIAL }); + const data = dataPluginMock.createStartContract(); + const error = new Error('Pls help!'); + main$.subscribe((value) => { + expect(data.search.showError).toBeCalledWith(error); + expect(value.fetchStatus).toBe(FetchStatus.ERROR); + expect(value.error).toBe(error); + done(); + }); + sendErrorTo(data, main$)(error); + }); + + test('checkHitCount with hits', (done) => { + const main$ = new BehaviorSubject({ fetchStatus: FetchStatus.LOADING }); + main$.subscribe((value) => { + expect(value.fetchStatus).toBe(FetchStatus.PARTIAL); + done(); + }); + checkHitCount(main$, 100); + }); + + test('checkHitCount without hits', (done) => { + const main$ = new BehaviorSubject({ fetchStatus: FetchStatus.LOADING }); + main$.subscribe((value) => { + expect(value.fetchStatus).toBe(FetchStatus.COMPLETE); + expect(value.foundDocuments).toBe(false); + done(); + }); + checkHitCount(main$, 0); + }); }); diff --git a/src/plugins/discover/public/application/main/hooks/use_saved_search_messages.ts b/src/plugins/discover/public/application/main/hooks/use_saved_search_messages.ts index ae5abb36378a8..ab121f76e15a0 100644 --- a/src/plugins/discover/public/application/main/hooks/use_saved_search_messages.ts +++ b/src/plugins/discover/public/application/main/hooks/use_saved_search_messages.ts @@ -6,14 +6,14 @@ * Side Public License, v 1. */ -import { AggregateQuery, Query } from '@kbn/es-query'; +import { DataPublicPluginStart } from '@kbn/data-plugin/public'; +import type { BehaviorSubject } from 'rxjs'; import { FetchStatus } from '../../types'; -import { - DataCharts$, +import type { DataDocuments$, DataMain$, + DataMsg, DataTotalHits$, - RecordRawType, SavedSearchData, } from './use_saved_search'; @@ -60,27 +60,22 @@ export function sendPartialMsg(main$: DataMain$) { /** * Send LOADING message via main observable */ -export function sendLoadingMsg( - data$: DataMain$ | DataDocuments$ | DataTotalHits$ | DataCharts$, - recordRawType: RecordRawType, - query?: AggregateQuery | Query +export function sendLoadingMsg( + data$: BehaviorSubject, + props: Omit ) { if (data$.getValue().fetchStatus !== FetchStatus.LOADING) { data$.next({ + ...props, fetchStatus: FetchStatus.LOADING, - recordRawType, - query, - }); + } as T); } } /** * Send ERROR message */ -export function sendErrorMsg( - data$: DataMain$ | DataDocuments$ | DataTotalHits$ | DataCharts$, - error: Error -) { +export function sendErrorMsg(data$: DataMain$ | DataDocuments$ | DataTotalHits$, error: Error) { const recordRawType = data$.getValue().recordRawType; data$.next({ fetchStatus: FetchStatus.ERROR, @@ -105,14 +100,43 @@ export function sendResetMsg(data: SavedSearchData, initialFetchStatus: FetchSta result: [], recordRawType, }); - data.charts$.next({ - fetchStatus: initialFetchStatus, - response: undefined, - recordRawType, - }); data.totalHits$.next({ fetchStatus: initialFetchStatus, result: undefined, recordRawType, }); } + +/** + * Method to create an error handler that will forward the received error + * to the specified subjects. It will ignore AbortErrors and will use the data + * plugin to show a toast for the error (e.g. allowing better insights into shard failures). + */ +export const sendErrorTo = ( + data: DataPublicPluginStart, + ...errorSubjects: Array +) => { + return (error: Error) => { + if (error instanceof Error && error.name === 'AbortError') { + return; + } + + data.search.showError(error); + errorSubjects.forEach((subject) => sendErrorMsg(subject, error)); + }; +}; + +/** + * This method checks the passed in hit count and will send a PARTIAL message to main$ + * if there are results, indicating that we have finished some of the requests that have been + * sent. If there are no results we already COMPLETE main$ with no results found, so Discover + * can show the "no results" screen. We know at that point, that the other query returning + * will neither carry any data, since there are no documents. + */ +export const checkHitCount = (main$: DataMain$, hitsCount: number) => { + if (hitsCount > 0) { + sendPartialMsg(main$); + } else { + sendNoResultsFoundMsg(main$); + } +}; diff --git a/src/plugins/discover/public/application/main/services/discover_search_session.ts b/src/plugins/discover/public/application/main/services/discover_search_session.ts index 5797b0381b1bf..0cbaf74159a80 100644 --- a/src/plugins/discover/public/application/main/services/discover_search_session.ts +++ b/src/plugins/discover/public/application/main/services/discover_search_session.ts @@ -31,6 +31,8 @@ export class DiscoverSearchSessionManager { * skips if `searchSessionId` matches current search session id */ readonly newSearchSessionIdFromURL$: Rx.Observable; + readonly searchSessionId$: Rx.Observable; + private readonly deps: DiscoverSearchSessionManagerDeps; constructor(deps: DiscoverSearchSessionManagerDeps) { @@ -44,6 +46,7 @@ export class DiscoverSearchSessionManager { return !this.deps.session.isCurrentSession(searchSessionId); }) ); + this.searchSessionId$ = this.deps.session.getSession$(); } /** diff --git a/src/plugins/discover/public/application/main/services/discover_state.ts b/src/plugins/discover/public/application/main/services/discover_state.ts index 611b59eacdc79..77e870dfc75a5 100644 --- a/src/plugins/discover/public/application/main/services/discover_state.ts +++ b/src/plugins/discover/public/application/main/services/discover_state.ts @@ -98,6 +98,10 @@ export interface AppState { * Number of rows in the grid per page */ rowsPerPage?: number; + /** + * Current histogram breakdown field name + */ + breakdownField?: string; } export interface AppStateUrl extends Omit { @@ -447,5 +451,6 @@ function createUrlGeneratorState({ useHash: false, viewMode: appState.viewMode, hideAggregatedPreview: appState.hideAggregatedPreview, + breakdownField: appState.breakdownField, }; } diff --git a/src/plugins/discover/public/application/main/utils/aggregate_request_adapter.test.ts b/src/plugins/discover/public/application/main/utils/aggregate_request_adapter.test.ts new file mode 100644 index 0000000000000..effbb192e863b --- /dev/null +++ b/src/plugins/discover/public/application/main/utils/aggregate_request_adapter.test.ts @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { RequestAdapter } from '@kbn/inspector-plugin/common'; +import { AggregateRequestAdapter } from './aggregate_request_adapter'; + +describe('AggregateRequestAdapter', () => { + it('should return all requests from all adapters', () => { + const adapter1 = new RequestAdapter(); + const adapter2 = new RequestAdapter(); + const adapter3 = new RequestAdapter(); + const aggregateAdapter = new AggregateRequestAdapter([adapter1, adapter2, adapter3]); + adapter1.start('request1'); + adapter2.start('request2'); + adapter3.start('request3'); + expect(aggregateAdapter.getRequests().map((request) => request.name)).toEqual([ + 'request1', + 'request2', + 'request3', + ]); + }); + + it('should allow adding and removing change listeners for all adapters', () => { + const adapter1 = new RequestAdapter(); + const adapter2 = new RequestAdapter(); + const aggregateAdapter = new AggregateRequestAdapter([adapter1, adapter2]); + const listener = jest.fn(); + aggregateAdapter.addListener('change', listener); + adapter1.start('request1'); + adapter2.start('request2'); + expect(listener).toHaveBeenCalledTimes(2); + aggregateAdapter.removeListener('change', listener); + adapter1.start('request3'); + adapter2.start('request4'); + expect(listener).toHaveBeenCalledTimes(2); + }); +}); diff --git a/src/plugins/discover/public/application/main/utils/aggregate_request_adapter.ts b/src/plugins/discover/public/application/main/utils/aggregate_request_adapter.ts new file mode 100644 index 0000000000000..4ce55c0723a11 --- /dev/null +++ b/src/plugins/discover/public/application/main/utils/aggregate_request_adapter.ts @@ -0,0 +1,132 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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 { RequestAdapter, Request } from '@kbn/inspector-plugin/public'; + +/** + * A request adapter that aggregates multiple separate adapters into one to allow inspection + */ +export class AggregateRequestAdapter extends RequestAdapter { + private readonly adapters: RequestAdapter[]; + + constructor(adapters: RequestAdapter[]) { + super(); + this.adapters = adapters; + } + + public reset(...args: Parameters): void { + super.reset(...args); + this.adapters.forEach((adapter) => adapter.reset(...args)); + } + + public resetRequest(...args: Parameters): void { + super.resetRequest(...args); + this.adapters.forEach((adapter) => adapter.resetRequest(...args)); + } + + public getRequests(...args: Parameters): Request[] { + return [ + ...super.getRequests(), + ...this.adapters.map((adapter) => adapter.getRequests(...args)).flat(), + ]; + } + + public addListener(...args: Parameters): this { + super.addListener(...args); + this.adapters.forEach((adapter) => adapter.addListener(...args)); + return this; + } + + public on(...args: Parameters): this { + super.on(...args); + this.adapters.forEach((adapter) => adapter.on(...args)); + return this; + } + + public once(...args: Parameters): this { + super.once(...args); + this.adapters.forEach((adapter) => adapter.once(...args)); + return this; + } + + public removeListener(...args: Parameters): this { + super.removeListener(...args); + this.adapters.forEach((adapter) => adapter.removeListener(...args)); + return this; + } + + public off(...args: Parameters): this { + super.off(...args); + this.adapters.forEach((adapter) => adapter.off(...args)); + return this; + } + + public removeAllListeners(...args: Parameters): this { + super.removeAllListeners(...args); + this.adapters.forEach((adapter) => adapter.removeAllListeners(...args)); + return this; + } + + public setMaxListeners(...args: Parameters): this { + super.setMaxListeners(...args); + this.adapters.forEach((adapter) => adapter.setMaxListeners(...args)); + return this; + } + + public getMaxListeners(...args: Parameters): number { + return Math.min( + super.getMaxListeners(...args), + ...this.adapters.map((adapter) => adapter.getMaxListeners(...args)) + ); + } + + public listeners(...args: Parameters): Function[] { + return [ + ...super.listeners(...args), + ...this.adapters.map((adapter) => adapter.listeners(...args)).flat(), + ]; + } + + public rawListeners(...args: Parameters): Function[] { + return [ + ...super.rawListeners(...args), + ...this.adapters.map((adapter) => adapter.rawListeners(...args)).flat(), + ]; + } + + public emit(...args: Parameters): boolean { + return [super.emit(...args), ...this.adapters.map((adapter) => adapter.emit(...args))].every( + (result) => result + ); + } + + public listenerCount(...args: Parameters): number { + return this.adapters + .map((adapter) => adapter.listenerCount(...args)) + .reduce((a, b) => a + b, super.listenerCount(...args)); + } + + public prependListener(...args: Parameters): this { + super.prependListener(...args); + this.adapters.forEach((adapter) => adapter.prependListener(...args)); + return this; + } + + public prependOnceListener(...args: Parameters): this { + super.prependOnceListener(...args); + this.adapters.forEach((adapter) => adapter.prependOnceListener(...args)); + return this; + } + + public eventNames(...args: Parameters): Array { + return [ + ...super.eventNames(...args), + ...this.adapters.map((adapter) => adapter.eventNames(...args)).flat(), + ]; + } +} diff --git a/src/plugins/discover/public/application/main/utils/calc_field_counts.test.ts b/src/plugins/discover/public/application/main/utils/calc_field_counts.test.ts index 5116bb78789ca..0aace63eb7754 100644 --- a/src/plugins/discover/public/application/main/utils/calc_field_counts.test.ts +++ b/src/plugins/discover/public/application/main/utils/calc_field_counts.test.ts @@ -7,7 +7,6 @@ */ import { calcFieldCounts } from './calc_field_counts'; -import { dataViewMock } from '../../../__mocks__/data_view'; import { buildDataTableRecord } from '../../../utils/build_data_record'; describe('calcFieldCounts', () => { @@ -16,7 +15,7 @@ describe('calcFieldCounts', () => { { _id: '1', _index: 'test', _source: { message: 'test1', bytes: 20 } }, { _id: '2', _index: 'test', _source: { name: 'test2', extension: 'jpg' } }, ].map((row) => buildDataTableRecord(row)); - const result = calcFieldCounts(rows, dataViewMock); + const result = calcFieldCounts(rows); expect(result).toMatchInlineSnapshot(` Object { "bytes": 1, @@ -31,7 +30,7 @@ describe('calcFieldCounts', () => { { _id: '1', _index: 'test', _source: { message: 'test1', bytes: 20 } }, { _id: '2', _index: 'test', _source: { name: 'test2', extension: 'jpg' } }, ].map((row) => buildDataTableRecord(row)); - const result = calcFieldCounts(rows, dataViewMock); + const result = calcFieldCounts(rows); expect(result).toMatchInlineSnapshot(` Object { "bytes": 1, diff --git a/src/plugins/discover/public/application/main/utils/calc_field_counts.ts b/src/plugins/discover/public/application/main/utils/calc_field_counts.ts index 10cdd92d9a250..2fd089816f9fb 100644 --- a/src/plugins/discover/public/application/main/utils/calc_field_counts.ts +++ b/src/plugins/discover/public/application/main/utils/calc_field_counts.ts @@ -5,24 +5,24 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ -import { DataView } from '@kbn/data-views-plugin/public'; import { DataTableRecord } from '../../../types'; /** * This function is calculating stats of the available fields, for usage in sidebar and sharing * Note that this values aren't displayed, but used for internal calculations */ -export function calcFieldCounts(rows?: DataTableRecord[], dataView?: DataView) { +export function calcFieldCounts(rows?: DataTableRecord[]) { const counts: Record = {}; - if (!rows || !dataView) { + if (!rows) { return {}; } - for (const hit of rows) { + + rows.forEach((hit) => { const fields = Object.keys(hit.flattened); - for (const fieldName of fields) { + fields.forEach((fieldName) => { counts[fieldName] = (counts[fieldName] || 0) + 1; - } - } + }); + }); return counts; } diff --git a/src/plugins/discover/public/application/main/utils/fetch_all.test.ts b/src/plugins/discover/public/application/main/utils/fetch_all.test.ts index 59dbc3ffe73d8..5258de6bdfdad 100644 --- a/src/plugins/discover/public/application/main/utils/fetch_all.test.ts +++ b/src/plugins/discover/public/application/main/utils/fetch_all.test.ts @@ -17,20 +17,17 @@ import { discoverServiceMock } from '../../../__mocks__/services'; import { fetchAll } from './fetch_all'; import { DataAvailableFieldsMsg, - DataChartsMessage, DataDocumentsMsg, DataMainMsg, DataTotalHitsMsg, + RecordRawType, SavedSearchData, } from '../hooks/use_saved_search'; import { fetchDocuments } from './fetch_documents'; import { fetchSql } from './fetch_sql'; -import { fetchChart } from './fetch_chart'; -import { fetchTotalHits } from './fetch_total_hits'; import { buildDataTableRecord } from '../../../utils/build_data_record'; import { dataViewMock } from '../../../__mocks__/data_view'; -import type { SearchResponse } from '@elastic/elasticsearch/lib/api/types'; jest.mock('./fetch_documents', () => ({ fetchDocuments: jest.fn().mockResolvedValue([]), @@ -40,17 +37,7 @@ jest.mock('./fetch_sql', () => ({ fetchSql: jest.fn().mockResolvedValue([]), })); -jest.mock('./fetch_chart', () => ({ - fetchChart: jest.fn(), -})); - -jest.mock('./fetch_total_hits', () => ({ - fetchTotalHits: jest.fn(), -})); - const mockFetchDocuments = fetchDocuments as unknown as jest.MockedFunction; -const mockFetchTotalHits = fetchTotalHits as unknown as jest.MockedFunction; -const mockFetchChart = fetchChart as unknown as jest.MockedFunction; const mockFetchSQL = fetchSql as unknown as jest.MockedFunction; function subjectCollector(subject: Subject): () => Promise { @@ -64,6 +51,8 @@ function subjectCollector(subject: Subject): () => Promise { }; } +const waitForNextTick = () => new Promise((resolve) => setTimeout(resolve, 0)); + describe('test fetchAll', () => { let subjects: SavedSearchData; let deps: Parameters[3]; @@ -73,7 +62,6 @@ describe('test fetchAll', () => { main$: new BehaviorSubject({ fetchStatus: FetchStatus.UNINITIALIZED }), documents$: new BehaviorSubject({ fetchStatus: FetchStatus.UNINITIALIZED }), totalHits$: new BehaviorSubject({ fetchStatus: FetchStatus.UNINITIALIZED }), - charts$: new BehaviorSubject({ fetchStatus: FetchStatus.UNINITIALIZED }), availableFields$: new BehaviorSubject({ fetchStatus: FetchStatus.UNINITIALIZED, }), @@ -97,10 +85,6 @@ describe('test fetchAll', () => { mockFetchDocuments.mockReset().mockResolvedValue([]); mockFetchSQL.mockReset().mockResolvedValue([]); - mockFetchTotalHits.mockReset().mockResolvedValue(42); - mockFetchChart - .mockReset() - .mockResolvedValue({ totalHits: 42, response: {} as unknown as SearchResponse }); }); test('changes of fetchStatus when starting with FetchStatus.UNINITIALIZED', async () => { @@ -108,7 +92,8 @@ describe('test fetchAll', () => { subjects.main$.subscribe((value) => stateArr.push(value.fetchStatus)); - await fetchAll(subjects, searchSource, false, deps); + fetchAll(subjects, searchSource, false, deps); + await waitForNextTick(); expect(stateArr).toEqual([ FetchStatus.UNINITIALIZED, @@ -125,7 +110,8 @@ describe('test fetchAll', () => { ]; const documents = hits.map((hit) => buildDataTableRecord(hit, dataViewMock)); mockFetchDocuments.mockResolvedValue(documents); - await fetchAll(subjects, searchSource, false, deps); + fetchAll(subjects, searchSource, false, deps); + await waitForNextTick(); expect(await collect()).toEqual([ { fetchStatus: FetchStatus.UNINITIALIZED }, { fetchStatus: FetchStatus.LOADING, recordRawType: 'document' }, @@ -147,8 +133,18 @@ describe('test fetchAll', () => { const documents = hits.map((hit) => buildDataTableRecord(hit, dataViewMock)); mockFetchDocuments.mockResolvedValue(documents); - mockFetchTotalHits.mockResolvedValue(42); - await fetchAll(subjects, searchSource, false, deps); + subjects.totalHits$.next({ + fetchStatus: FetchStatus.LOADING, + recordRawType: RecordRawType.DOCUMENT, + }); + fetchAll(subjects, searchSource, false, deps); + await waitForNextTick(); + subjects.totalHits$.next({ + fetchStatus: FetchStatus.COMPLETE, + recordRawType: RecordRawType.DOCUMENT, + result: 42, + }); + expect(await collect()).toEqual([ { fetchStatus: FetchStatus.UNINITIALIZED }, { fetchStatus: FetchStatus.LOADING, recordRawType: 'document' }, @@ -157,44 +153,48 @@ describe('test fetchAll', () => { ]); }); - test('emits loading and response on charts$ correctly', async () => { - const collect = subjectCollector(subjects.charts$); - searchSource.getField('index')!.isTimeBased = () => true; - await fetchAll(subjects, searchSource, false, deps); - expect(await collect()).toEqual([ - { fetchStatus: FetchStatus.UNINITIALIZED }, - { fetchStatus: FetchStatus.LOADING, recordRawType: 'document' }, - { - fetchStatus: FetchStatus.COMPLETE, - recordRawType: 'document', - response: {}, - }, - ]); - }); - test('should use charts query to fetch total hit count when chart is visible', async () => { const collect = subjectCollector(subjects.totalHits$); searchSource.getField('index')!.isTimeBased = () => true; - mockFetchChart.mockResolvedValue({ totalHits: 32, response: {} as unknown as SearchResponse }); - await fetchAll(subjects, searchSource, false, deps); + subjects.totalHits$.next({ + fetchStatus: FetchStatus.LOADING, + recordRawType: RecordRawType.DOCUMENT, + }); + fetchAll(subjects, searchSource, false, deps); + await waitForNextTick(); + subjects.totalHits$.next({ + fetchStatus: FetchStatus.COMPLETE, + recordRawType: RecordRawType.DOCUMENT, + result: 32, + }); + expect(await collect()).toEqual([ { fetchStatus: FetchStatus.UNINITIALIZED }, { fetchStatus: FetchStatus.LOADING, recordRawType: 'document' }, { fetchStatus: FetchStatus.PARTIAL, recordRawType: 'document', result: 0 }, // From documents query { fetchStatus: FetchStatus.COMPLETE, recordRawType: 'document', result: 32 }, ]); - expect(mockFetchTotalHits).not.toHaveBeenCalled(); }); test('should only fail totalHits$ query not main$ for error from that query', async () => { const collectTotalHits = subjectCollector(subjects.totalHits$); const collectMain = subjectCollector(subjects.main$); searchSource.getField('index')!.isTimeBased = () => false; - mockFetchTotalHits.mockRejectedValue({ msg: 'Oh noes!' }); const hits = [{ _id: '1', _index: 'logs' }]; const documents = hits.map((hit) => buildDataTableRecord(hit, dataViewMock)); mockFetchDocuments.mockResolvedValue(documents); - await fetchAll(subjects, searchSource, false, deps); + subjects.totalHits$.next({ + fetchStatus: FetchStatus.LOADING, + recordRawType: RecordRawType.DOCUMENT, + }); + fetchAll(subjects, searchSource, false, deps); + await waitForNextTick(); + subjects.totalHits$.next({ + fetchStatus: FetchStatus.ERROR, + recordRawType: RecordRawType.DOCUMENT, + error: { msg: 'Oh noes!' } as unknown as Error, + }); + expect(await collectTotalHits()).toEqual([ { fetchStatus: FetchStatus.UNINITIALIZED }, { fetchStatus: FetchStatus.LOADING, recordRawType: 'document' }, @@ -218,11 +218,21 @@ describe('test fetchAll', () => { const collectMain = subjectCollector(subjects.main$); searchSource.getField('index')!.isTimeBased = () => false; mockFetchDocuments.mockRejectedValue({ msg: 'This query failed' }); - await fetchAll(subjects, searchSource, false, deps); + subjects.totalHits$.next({ + fetchStatus: FetchStatus.LOADING, + recordRawType: RecordRawType.DOCUMENT, + }); + fetchAll(subjects, searchSource, false, deps); + await waitForNextTick(); + subjects.totalHits$.next({ + fetchStatus: FetchStatus.COMPLETE, + recordRawType: RecordRawType.DOCUMENT, + result: 5, + }); + expect(await collectMain()).toEqual([ { fetchStatus: FetchStatus.UNINITIALIZED }, { fetchStatus: FetchStatus.LOADING, recordRawType: 'document' }, - { fetchStatus: FetchStatus.PARTIAL, recordRawType: 'document' }, // From totalHits query { fetchStatus: FetchStatus.ERROR, error: { msg: 'This query failed' }, @@ -256,7 +266,9 @@ describe('test fetchAll', () => { savedSearch: savedSearchMock, services: discoverServiceMock, }; - await fetchAll(subjects, searchSource, false, deps); + fetchAll(subjects, searchSource, false, deps); + await waitForNextTick(); + expect(await collect()).toEqual([ { fetchStatus: FetchStatus.UNINITIALIZED }, { fetchStatus: FetchStatus.LOADING, recordRawType: 'plain', query }, diff --git a/src/plugins/discover/public/application/main/utils/fetch_all.ts b/src/plugins/discover/public/application/main/utils/fetch_all.ts index d530da1492fac..d782442db3953 100644 --- a/src/plugins/discover/public/application/main/utils/fetch_all.ts +++ b/src/plugins/discover/public/application/main/utils/fetch_all.ts @@ -8,31 +8,22 @@ import { DataPublicPluginStart, ISearchSource } from '@kbn/data-plugin/public'; import { Adapters } from '@kbn/inspector-plugin/common'; import { ReduxLikeStateContainer } from '@kbn/kibana-utils-plugin/common'; -import { DataViewType } from '@kbn/data-views-plugin/public'; import type { SavedSearch, SortOrder } from '@kbn/saved-search-plugin/public'; +import { BehaviorSubject, filter, firstValueFrom, map, merge, scan } from 'rxjs'; import { getRawRecordType } from './get_raw_record_type'; import { + checkHitCount, sendCompleteMsg, sendErrorMsg, + sendErrorTo, sendLoadingMsg, - sendNoResultsFoundMsg, - sendPartialMsg, sendResetMsg, } from '../hooks/use_saved_search_messages'; import { updateSearchSource } from './update_search_source'; import { fetchDocuments } from './fetch_documents'; -import { fetchTotalHits } from './fetch_total_hits'; -import { fetchChart } from './fetch_chart'; import { AppState } from '../services/discover_state'; import { FetchStatus } from '../../types'; -import { - DataCharts$, - DataDocuments$, - DataMain$, - DataTotalHits$, - RecordRawType, - SavedSearchData, -} from '../hooks/use_saved_search'; +import { DataMsg, RecordRawType, SavedSearchData } from '../hooks/use_saved_search'; import { DiscoverServices } from '../../../build_services'; import { fetchSql } from './fetch_sql'; @@ -50,8 +41,7 @@ export interface FetchDeps { /** * This function starts fetching all required queries in Discover. This will be the query to load the individual - * documents, and depending on whether a chart is shown either the aggregation query to load the chart data - * or a query to retrieve just the total hits. + * documents as well as any other requests that might be required to load the main view. * * This method returns a promise, which will resolve (without a value), as soon as all queries that have been started * have been completed (failed or successfully). @@ -64,30 +54,12 @@ export function fetchAll( ): Promise { const { initialFetchStatus, appStateContainer, services, useNewFieldsApi, data } = fetchDeps; - /** - * Method to create an error handler that will forward the received error - * to the specified subjects. It will ignore AbortErrors and will use the data - * plugin to show a toast for the error (e.g. allowing better insights into shard failures). - */ - const sendErrorTo = ( - ...errorSubjects: Array - ) => { - return (error: Error) => { - if (error instanceof Error && error.name === 'AbortError') { - return; - } - - data.search.showError(error); - errorSubjects.forEach((subject) => sendErrorMsg(subject, error)); - }; - }; - try { const dataView = searchSource.getField('index')!; if (reset) { sendResetMsg(dataSubjects, initialFetchStatus); } - const { hideChart, sort, query } = appStateContainer.getState(); + const { sort, query } = appStateContainer.getState(); const recordRawType = getRawRecordType(query); const useSql = recordRawType === RecordRawType.PLAIN; @@ -102,40 +74,17 @@ export function fetchAll( } // Mark all subjects as loading - sendLoadingMsg(dataSubjects.main$, recordRawType); - sendLoadingMsg(dataSubjects.documents$, recordRawType, query); - sendLoadingMsg(dataSubjects.totalHits$, recordRawType); - sendLoadingMsg(dataSubjects.charts$, recordRawType); - - const isChartVisible = - !hideChart && dataView.isTimeBased() && dataView.type !== DataViewType.ROLLUP; + sendLoadingMsg(dataSubjects.main$, { recordRawType }); + sendLoadingMsg(dataSubjects.documents$, { recordRawType, query }); + sendLoadingMsg(dataSubjects.totalHits$, { recordRawType }); // Start fetching all required requests const documents = useSql && query ? fetchSql(query, services.dataViews, data, services.expressions) : fetchDocuments(searchSource.createCopy(), fetchDeps); - const charts = - isChartVisible && !useSql ? fetchChart(searchSource.createCopy(), fetchDeps) : undefined; - const totalHits = - !isChartVisible && !useSql ? fetchTotalHits(searchSource.createCopy(), fetchDeps) : undefined; - /** - * This method checks the passed in hit count and will send a PARTIAL message to main$ - * if there are results, indicating that we have finished some of the requests that have been - * sent. If there are no results we already COMPLETE main$ with no results found, so Discover - * can show the "no results" screen. We know at that point, that the other query returning - * will neither carry any data, since there are no documents. - */ - const checkHitCount = (hitsCount: number) => { - if (hitsCount > 0) { - sendPartialMsg(dataSubjects.main$); - } else { - sendNoResultsFoundMsg(dataSubjects.main$); - } - }; // Handle results of the individual queries and forward the results to the corresponding dataSubjects - documents .then((docs) => { // If the total hits (or chart) query is still loading, emit a partial @@ -155,44 +104,20 @@ export function fetchAll( query, }); - checkHitCount(docs.length); + checkHitCount(dataSubjects.main$, docs.length); }) // Only the document query should send its errors to main$, to cause the full Discover app // to get into an error state. The other queries will not cause all of Discover to error out // but their errors will be shown in-place (e.g. of the chart). - .catch(sendErrorTo(dataSubjects.documents$, dataSubjects.main$)); - - charts - ?.then((chart) => { - dataSubjects.totalHits$.next({ - fetchStatus: FetchStatus.COMPLETE, - result: chart.totalHits, - recordRawType, - }); - - dataSubjects.charts$.next({ - fetchStatus: FetchStatus.COMPLETE, - response: chart.response, - recordRawType, - }); - - checkHitCount(chart.totalHits); - }) - .catch(sendErrorTo(dataSubjects.charts$, dataSubjects.totalHits$)); - - totalHits - ?.then((hitCount) => { - dataSubjects.totalHits$.next({ - fetchStatus: FetchStatus.COMPLETE, - result: hitCount, - recordRawType, - }); - checkHitCount(hitCount); - }) - .catch(sendErrorTo(dataSubjects.totalHits$)); + .catch(sendErrorTo(data, dataSubjects.documents$, dataSubjects.main$)); // Return a promise that will resolve once all the requests have finished or failed - return Promise.allSettled([documents, charts, totalHits]).then(() => { + return firstValueFrom( + merge( + fetchStatusByType(dataSubjects.documents$, 'documents'), + fetchStatusByType(dataSubjects.totalHits$, 'totalHits') + ).pipe(scan(toRequestFinishedMap, {}), filter(allRequestsFinished)) + ).then(() => { // Send a complete message to main$ once all queries are done and if main$ // is not already in an ERROR state, e.g. because the document query has failed. // This will only complete main$, if it hasn't already been completed previously @@ -207,3 +132,17 @@ export function fetchAll( return Promise.resolve(); } } + +const fetchStatusByType = (subject: BehaviorSubject, type: string) => + subject.pipe(map(({ fetchStatus }) => ({ type, fetchStatus }))); + +const toRequestFinishedMap = ( + currentMap: Record, + { type, fetchStatus }: { type: string; fetchStatus: FetchStatus } +) => ({ + ...currentMap, + [type]: [FetchStatus.COMPLETE, FetchStatus.ERROR].includes(fetchStatus), +}); + +const allRequestsFinished = (requests: Record) => + Object.values(requests).every((finished) => finished); diff --git a/src/plugins/discover/public/application/main/utils/fetch_chart.test.ts b/src/plugins/discover/public/application/main/utils/fetch_chart.test.ts deleted file mode 100644 index e1020404d3996..0000000000000 --- a/src/plugins/discover/public/application/main/utils/fetch_chart.test.ts +++ /dev/null @@ -1,134 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 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 { of, throwError as throwErrorRx } from 'rxjs'; -import { RequestAdapter } from '@kbn/inspector-plugin/common'; -import { savedSearchMockWithTimeField } from '../../../__mocks__/saved_search'; -import { fetchChart, updateSearchSource } from './fetch_chart'; -import { ReduxLikeStateContainer } from '@kbn/kibana-utils-plugin/common'; -import { AppState } from '../services/discover_state'; -import { discoverServiceMock } from '../../../__mocks__/services'; -import { calculateBounds } from '@kbn/data-plugin/public'; -import { FetchDeps } from './fetch_all'; - -function getDeps() { - const deps = { - appStateContainer: { - getState: () => { - return { interval: 'auto' }; - }, - } as ReduxLikeStateContainer, - abortController: new AbortController(), - data: discoverServiceMock.data, - inspectorAdapters: { requests: new RequestAdapter() }, - onResults: jest.fn(), - savedSearch: savedSearchMockWithTimeField, - searchSessionId: '123', - } as unknown as FetchDeps; - deps.data.query.timefilter.timefilter.getTime = () => { - return { from: '2021-07-07T00:05:13.590', to: '2021-07-07T11:20:13.590' }; - }; - - deps.data.query.timefilter.timefilter.calculateBounds = (timeRange) => calculateBounds(timeRange); - return deps; -} - -const requestResult = { - id: 'Fjk5bndxTHJWU2FldVRVQ0tYR0VqOFEcRWtWNDhOdG5SUzJYcFhONVVZVTBJQToxMDMwOQ==', - rawResponse: { - took: 2, - timed_out: false, - _shards: { total: 1, successful: 1, skipped: 0, failed: 0 }, - hits: { max_score: null, hits: [], total: 42 }, - aggregations: { - '2': { - buckets: [ - { - key_as_string: '2021-07-07T06:36:00.000+02:00', - key: 1625632560000, - doc_count: 1, - }, - ], - }, - }, - }, - isPartial: false, - isRunning: false, - total: 1, - loaded: 1, - isRestored: false, -}; - -describe('test fetchCharts', () => { - test('updateSearchSource helper function', () => { - const chartAggConfigs = updateSearchSource( - savedSearchMockWithTimeField.searchSource, - 'auto', - discoverServiceMock.data - ); - expect(chartAggConfigs.aggs).toMatchInlineSnapshot(` - Array [ - Object { - "enabled": true, - "id": "1", - "params": Object { - "emptyAsNull": false, - }, - "schema": "metric", - "type": "count", - }, - Object { - "enabled": true, - "id": "2", - "params": Object { - "drop_partials": false, - "extendToTimeRange": false, - "extended_bounds": Object {}, - "field": "timestamp", - "interval": "auto", - "min_doc_count": 1, - "scaleMetricValues": false, - "useNormalizedEsInterval": true, - "used_interval": "0ms", - }, - "schema": "segment", - "type": "date_histogram", - }, - ] - `); - }); - - test('resolves with summarized chart data', async () => { - savedSearchMockWithTimeField.searchSource.fetch$ = () => of(requestResult); - - const result = await fetchChart(savedSearchMockWithTimeField.searchSource, getDeps()); - expect(result).toHaveProperty('totalHits', 42); - expect(result).toHaveProperty('response'); - }); - - test('rejects promise on query failure', async () => { - savedSearchMockWithTimeField.searchSource.fetch$ = () => - throwErrorRx(() => new Error('Oh noes!')); - - await expect(fetchChart(savedSearchMockWithTimeField.searchSource, getDeps())).rejects.toEqual( - new Error('Oh noes!') - ); - }); - - test('fetch$ is called with request specific execution context', async () => { - const fetch$Mock = jest.fn().mockReturnValue(of(requestResult)); - - savedSearchMockWithTimeField.searchSource.fetch$ = fetch$Mock; - - await fetchChart(savedSearchMockWithTimeField.searchSource, getDeps()); - expect(fetch$Mock.mock.calls[0][0].executionContext).toMatchInlineSnapshot(` - Object { - "description": "fetch chart data and total hits", - } - `); - }); -}); diff --git a/src/plugins/discover/public/application/main/utils/fetch_chart.ts b/src/plugins/discover/public/application/main/utils/fetch_chart.ts deleted file mode 100644 index e4e5b67782cb9..0000000000000 --- a/src/plugins/discover/public/application/main/utils/fetch_chart.ts +++ /dev/null @@ -1,75 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { i18n } from '@kbn/i18n'; -import { filter, map } from 'rxjs/operators'; -import { lastValueFrom } from 'rxjs'; -import { DataPublicPluginStart, isCompleteResponse, ISearchSource } from '@kbn/data-plugin/public'; -import type { SearchResponse } from '@elastic/elasticsearch/lib/api/types'; -import { getChartAggConfigs } from '@kbn/unified-histogram-plugin/public'; -import { FetchDeps } from './fetch_all'; - -interface Result { - totalHits: number; - response: SearchResponse; -} - -export function fetchChart( - searchSource: ISearchSource, - { abortController, appStateContainer, data, inspectorAdapters, searchSessionId }: FetchDeps -): Promise { - const timeInterval = appStateContainer.getState().interval ?? 'auto'; - - updateSearchSource(searchSource, timeInterval, data); - - const executionContext = { - description: 'fetch chart data and total hits', - }; - - const fetch$ = searchSource - .fetch$({ - abortSignal: abortController.signal, - sessionId: searchSessionId, - inspector: { - adapter: inspectorAdapters.requests, - title: i18n.translate('discover.inspectorRequestDataTitleChart', { - defaultMessage: 'Chart data', - }), - description: i18n.translate('discover.inspectorRequestDescriptionChart', { - defaultMessage: - 'This request queries Elasticsearch to fetch the aggregation data for the chart.', - }), - }, - executionContext, - }) - .pipe( - filter((res) => isCompleteResponse(res)), - map((res) => ({ - response: res.rawResponse, - totalHits: res.rawResponse.hits.total as number, - })) - ); - - return lastValueFrom(fetch$); -} - -export function updateSearchSource( - searchSource: ISearchSource, - timeInterval: string, - data: DataPublicPluginStart -) { - const dataView = searchSource.getField('index')!; - searchSource.setField('filter', data.query.timefilter.timefilter.createFilter(dataView)); - searchSource.setField('size', 0); - searchSource.setField('trackTotalHits', true); - const chartAggConfigs = getChartAggConfigs({ dataView, timeInterval, data }); - searchSource.setField('aggs', chartAggConfigs.toDsl()); - searchSource.removeField('sort'); - searchSource.removeField('fields'); - return chartAggConfigs; -} diff --git a/src/plugins/discover/public/application/main/utils/fetch_documents.test.ts b/src/plugins/discover/public/application/main/utils/fetch_documents.test.ts index 4809da54655be..28738cdc522c9 100644 --- a/src/plugins/discover/public/application/main/utils/fetch_documents.test.ts +++ b/src/plugins/discover/public/application/main/utils/fetch_documents.test.ts @@ -8,12 +8,11 @@ import { fetchDocuments } from './fetch_documents'; import { throwError as throwErrorRx, of } from 'rxjs'; import { RequestAdapter } from '@kbn/inspector-plugin/common'; -import { savedSearchMock, savedSearchMockWithTimeField } from '../../../__mocks__/saved_search'; +import { savedSearchMock } from '../../../__mocks__/saved_search'; import { discoverServiceMock } from '../../../__mocks__/services'; import { IKibanaSearchResponse } from '@kbn/data-plugin/public'; import { SearchResponse } from '@elastic/elasticsearch/lib/api/types'; import { FetchDeps } from './fetch_all'; -import { fetchTotalHits } from './fetch_total_hits'; import type { EsHitRecord } from '../../../types'; import { buildDataTableRecord } from '../../../utils/build_data_record'; import { dataViewMock } from '../../../__mocks__/data_view'; @@ -47,21 +46,4 @@ describe('test fetchDocuments', () => { new Error('Oh noes!') ); }); - - test('fetch$ is called with execution context containing savedSearch id', async () => { - const fetch$Mock = jest.fn().mockReturnValue( - of({ - rawResponse: { hits: { hits: [] } }, - } as unknown as IKibanaSearchResponse) - ); - - savedSearchMockWithTimeField.searchSource.fetch$ = fetch$Mock; - - await fetchTotalHits(savedSearchMockWithTimeField.searchSource, getDeps()); - expect(fetch$Mock.mock.calls[0][0].executionContext).toMatchInlineSnapshot(` - Object { - "description": "fetch total hits", - } - `); - }); }); diff --git a/src/plugins/discover/public/application/main/utils/fetch_total_hits.test.ts b/src/plugins/discover/public/application/main/utils/fetch_total_hits.test.ts deleted file mode 100644 index f2851a57e7365..0000000000000 --- a/src/plugins/discover/public/application/main/utils/fetch_total_hits.test.ts +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 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 { throwError as throwErrorRx, of } from 'rxjs'; -import { RequestAdapter } from '@kbn/inspector-plugin/common'; -import { savedSearchMock, savedSearchMockWithTimeField } from '../../../__mocks__/saved_search'; -import { fetchTotalHits } from './fetch_total_hits'; -import { discoverServiceMock } from '../../../__mocks__/services'; -import { SearchResponse } from '@elastic/elasticsearch/lib/api/types'; -import { IKibanaSearchResponse } from '@kbn/data-plugin/public'; -import { FetchDeps } from './fetch_all'; - -const getDeps = () => - ({ - abortController: new AbortController(), - inspectorAdapters: { requests: new RequestAdapter() }, - searchSessionId: '123', - data: discoverServiceMock.data, - savedSearch: savedSearchMock, - } as FetchDeps); - -describe('test fetchTotalHits', () => { - test('resolves returned promise with hit count', async () => { - savedSearchMock.searchSource.fetch$ = () => - of({ rawResponse: { hits: { total: 45 } } } as IKibanaSearchResponse>); - - await expect(fetchTotalHits(savedSearchMock.searchSource, getDeps())).resolves.toBe(45); - }); - - test('rejects in case of an error', async () => { - savedSearchMock.searchSource.fetch$ = () => throwErrorRx(() => new Error('Oh noes!')); - - await expect(fetchTotalHits(savedSearchMock.searchSource, getDeps())).rejects.toEqual( - new Error('Oh noes!') - ); - }); - test('fetch$ is called with execution context containing savedSearch id', async () => { - const fetch$Mock = jest - .fn() - .mockReturnValue( - of({ rawResponse: { hits: { total: 45 } } } as IKibanaSearchResponse) - ); - - savedSearchMockWithTimeField.searchSource.fetch$ = fetch$Mock; - - await fetchTotalHits(savedSearchMockWithTimeField.searchSource, getDeps()); - expect(fetch$Mock.mock.calls[0][0].executionContext).toMatchInlineSnapshot(` - Object { - "description": "fetch total hits", - } - `); - }); -}); diff --git a/src/plugins/discover/public/application/main/utils/fetch_total_hits.ts b/src/plugins/discover/public/application/main/utils/fetch_total_hits.ts deleted file mode 100644 index 16bd138e2caf5..0000000000000 --- a/src/plugins/discover/public/application/main/utils/fetch_total_hits.ts +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { i18n } from '@kbn/i18n'; -import { filter, map } from 'rxjs/operators'; -import { lastValueFrom } from 'rxjs'; -import { isCompleteResponse, ISearchSource } from '@kbn/data-plugin/public'; -import { DataViewType } from '@kbn/data-views-plugin/public'; -import { FetchDeps } from './fetch_all'; - -export function fetchTotalHits( - searchSource: ISearchSource, - { abortController, inspectorAdapters, searchSessionId, savedSearch }: FetchDeps -) { - searchSource.setField('trackTotalHits', true); - searchSource.setField('size', 0); - searchSource.removeField('sort'); - searchSource.removeField('fields'); - searchSource.removeField('aggs'); - if (searchSource.getField('index')?.type === DataViewType.ROLLUP) { - // We treat that data view as "normal" even if it was a rollup data view, - // since the rollup endpoint does not support querying individual documents, but we - // can get them from the regular _search API that will be used if the data view - // not a rollup data view. - searchSource.setOverwriteDataViewType(undefined); - } - - const executionContext = { - description: 'fetch total hits', - }; - - const fetch$ = searchSource - .fetch$({ - inspector: { - adapter: inspectorAdapters.requests, - title: i18n.translate('discover.inspectorRequestDataTitleTotalHits', { - defaultMessage: 'Total hits', - }), - description: i18n.translate('discover.inspectorRequestDescriptionTotalHits', { - defaultMessage: 'This request queries Elasticsearch to fetch the total hits.', - }), - }, - abortSignal: abortController.signal, - sessionId: searchSessionId, - executionContext, - }) - .pipe( - filter((res) => isCompleteResponse(res)), - map((res) => res.rawResponse.hits.total as number) - ); - - return lastValueFrom(fetch$); -} diff --git a/src/plugins/discover/public/application/main/utils/get_state_defaults.test.ts b/src/plugins/discover/public/application/main/utils/get_state_defaults.test.ts index 1d5cf07446d60..aed900821c747 100644 --- a/src/plugins/discover/public/application/main/utils/get_state_defaults.test.ts +++ b/src/plugins/discover/public/application/main/utils/get_state_defaults.test.ts @@ -22,6 +22,7 @@ describe('getStateDefaults', () => { }); expect(actual).toMatchInlineSnapshot(` Object { + "breakdownField": undefined, "columns": Array [ "default_column", ], @@ -55,6 +56,7 @@ describe('getStateDefaults', () => { }); expect(actual).toMatchInlineSnapshot(` Object { + "breakdownField": undefined, "columns": Array [ "default_column", ], diff --git a/src/plugins/discover/public/application/main/utils/get_state_defaults.ts b/src/plugins/discover/public/application/main/utils/get_state_defaults.ts index b8b3c3579f343..f32af6ec5f23b 100644 --- a/src/plugins/discover/public/application/main/utils/get_state_defaults.ts +++ b/src/plugins/discover/public/application/main/utils/get_state_defaults.ts @@ -70,6 +70,7 @@ export function getStateDefaults({ rowHeight: undefined, rowsPerPage: undefined, grid: undefined, + breakdownField: undefined, }; if (savedSearch.grid) { defaultState.grid = savedSearch.grid; @@ -90,5 +91,9 @@ export function getStateDefaults({ defaultState.rowsPerPage = savedSearch.rowsPerPage; } + if (savedSearch.breakdownField) { + defaultState.breakdownField = savedSearch.breakdownField; + } + return defaultState; } diff --git a/src/plugins/discover/public/application/main/utils/persist_saved_search.ts b/src/plugins/discover/public/application/main/utils/persist_saved_search.ts index 73859e46eaf24..8162f3650b8b0 100644 --- a/src/plugins/discover/public/application/main/utils/persist_saved_search.ts +++ b/src/plugins/discover/public/application/main/utils/persist_saved_search.ts @@ -56,6 +56,12 @@ export async function persistSavedSearch( savedSearch.viewMode = state.viewMode; } + if (typeof state.breakdownField !== 'undefined') { + savedSearch.breakdownField = state.breakdownField; + } else if (savedSearch.breakdownField) { + savedSearch.breakdownField = ''; + } + if (state.hideAggregatedPreview) { savedSearch.hideAggregatedPreview = state.hideAggregatedPreview; } diff --git a/src/plugins/discover/public/build_services.ts b/src/plugins/discover/public/build_services.ts index 4c25f466b0fdd..27f5d59b07e10 100644 --- a/src/plugins/discover/public/build_services.ts +++ b/src/plugins/discover/public/build_services.ts @@ -46,6 +46,7 @@ import type { TriggersAndActionsUIPublicPluginStart } from '@kbn/triggers-action import type { SavedObjectsTaggingApi } from '@kbn/saved-objects-tagging-oss-plugin/public'; import type { SavedObjectsManagementPluginStart } from '@kbn/saved-objects-management-plugin/public'; import type { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/public'; +import type { LensPublicStart } from '@kbn/lens-plugin/public'; import { DiscoverAppLocator } from './locator'; import { getHistory } from './kibana_services'; import { DiscoverStartPlugins } from './plugin'; @@ -97,6 +98,7 @@ export interface DiscoverServices { savedObjectsManagement: SavedObjectsManagementPluginStart; savedObjectsTagging?: SavedObjectsTaggingApi; unifiedSearch: UnifiedSearchPublicPluginStart; + lens: LensPublicStart; } export const buildServices = memoize(function ( @@ -150,5 +152,6 @@ export const buildServices = memoize(function ( savedObjectsTagging: plugins.savedObjectsTaggingOss?.getTaggingApi(), savedObjectsManagement: plugins.savedObjectsManagement, unifiedSearch: plugins.unifiedSearch, + lens: plugins.lens, }; }); diff --git a/src/plugins/discover/public/components/discover_tour/discover_tour_anchors.tsx b/src/plugins/discover/public/components/discover_tour/discover_tour_anchors.tsx index 030876291aeea..2344acf3e60d9 100644 --- a/src/plugins/discover/public/components/discover_tour/discover_tour_anchors.tsx +++ b/src/plugins/discover/public/components/discover_tour/discover_tour_anchors.tsx @@ -14,7 +14,7 @@ export const DISCOVER_TOUR_STEP_ANCHOR_IDS = { }; export const DISCOVER_TOUR_STEP_ANCHORS = { - addFields: `#${DISCOVER_TOUR_STEP_ANCHOR_IDS.addFields}`, + addFields: `[data-test-subj="fieldListGroupedAvailableFields-count"], #${DISCOVER_TOUR_STEP_ANCHOR_IDS.addFields}`, reorderColumns: '[data-test-subj="dataGridColumnSelectorButton"]', sort: '[data-test-subj="dataGridColumnSortingButton"]', changeRowHeight: '[data-test-subj="dataGridDisplaySelectorButton"]', diff --git a/src/plugins/discover/public/locator.ts b/src/plugins/discover/public/locator.ts index e59c61c98412c..e803433c4d70c 100644 --- a/src/plugins/discover/public/locator.ts +++ b/src/plugins/discover/public/locator.ts @@ -91,6 +91,10 @@ export interface DiscoverAppLocatorParams extends SerializableRecord { * Hide mini distribution/preview charts when in Field Statistics mode */ hideAggregatedPreview?: boolean; + /** + * Breakdown field + */ + breakdownField?: string; } export type DiscoverAppLocator = LocatorPublic; @@ -129,6 +133,7 @@ export class DiscoverAppLocatorDefinition implements LocatorDefinition { } as DataViewField) ).toBe('version'); }); + + it('extracts type for meta fields', () => { + expect( + getTypeForFieldIcon({ + type: 'string', + esTypes: ['_id'], + } as DataViewField) + ).toBe('string'); + }); }); diff --git a/src/plugins/discover/public/utils/get_type_for_field_icon.ts b/src/plugins/discover/public/utils/get_type_for_field_icon.ts index 429fdf87991eb..3d05e8365e59c 100644 --- a/src/plugins/discover/public/utils/get_type_for_field_icon.ts +++ b/src/plugins/discover/public/utils/get_type_for_field_icon.ts @@ -13,5 +13,10 @@ import { DataViewField } from '@kbn/data-views-plugin/common'; * * We define custom logic for Discover in order to distinguish between various "string" types. */ -export const getTypeForFieldIcon = (field: DataViewField) => - field.type === 'string' && field.esTypes ? field.esTypes[0] : field.type; +export const getTypeForFieldIcon = (field: DataViewField) => { + const esType = field.esTypes?.[0] || null; + if (esType && ['_id', '_index'].includes(esType)) { + return field.type; + } + return field.type === 'string' && esType ? esType : field.type; +}; diff --git a/src/plugins/embeddable/public/lib/containers/container.ts b/src/plugins/embeddable/public/lib/containers/container.ts index 843d3a7cd5c99..278494b9df559 100644 --- a/src/plugins/embeddable/public/lib/containers/container.ts +++ b/src/plugins/embeddable/public/lib/containers/container.ts @@ -398,7 +398,7 @@ export abstract class Container< } } - private async createAndSaveEmbeddable< + protected async createAndSaveEmbeddable< TEmbeddableInput extends EmbeddableInput = EmbeddableInput, TEmbeddable extends IEmbeddable = IEmbeddable >(type: string, panelState: PanelState) { diff --git a/src/plugins/event_annotation/common/fetch_event_annotations/request_event_annotations.ts b/src/plugins/event_annotation/common/fetch_event_annotations/request_event_annotations.ts index dbf653ea72f3a..56748495a3456 100644 --- a/src/plugins/event_annotation/common/fetch_event_annotations/request_event_annotations.ts +++ b/src/plugins/event_annotation/common/fetch_event_annotations/request_event_annotations.ts @@ -23,6 +23,7 @@ import { ESCalendarInterval, ESFixedInterval, roundDateToESInterval } from '@ela import { Adapters } from '@kbn/inspector-plugin/common'; import { SerializableRecord } from '@kbn/utility-types'; import { IUiSettingsClient } from '@kbn/core-ui-settings-browser'; +import { i18n } from '@kbn/i18n'; import { handleRequest } from './handle_request'; import { ANNOTATIONS_PER_BUCKET, @@ -134,6 +135,19 @@ export const requestEventAnnotations = ( searchSourceService: searchSource, getNow, executionContext: getExecutionContext(), + title: i18n.translate( + 'eventAnnotation.fetchEventAnnotations.inspector.dataRequest.title', + { + defaultMessage: 'Annotations', + } + ), + description: i18n.translate( + 'eventAnnotation.fetchEventAnnotations.inspector.dataRequest.description', + { + defaultMessage: + 'This request queries Elasticsearch to fetch the data for the annotations.', + } + ), }) ); diff --git a/src/plugins/files/.storybook/main.ts b/src/plugins/files/.storybook/main.ts deleted file mode 100644 index f9d5b3ea3eddc..0000000000000 --- a/src/plugins/files/.storybook/main.ts +++ /dev/null @@ -1,17 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 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 { defaultConfig } from '@kbn/storybook'; - -module.exports = { - ...defaultConfig, - stories: ['../**/*.stories.tsx'], - reactOptions: { - strictMode: true, - }, -}; diff --git a/src/plugins/files/.storybook/manager.ts b/src/plugins/files/.storybook/manager.ts deleted file mode 100644 index d49eea1784792..0000000000000 --- a/src/plugins/files/.storybook/manager.ts +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 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 { addons } from '@storybook/addons'; -import { create } from '@storybook/theming'; -import { PANEL_ID } from '@storybook/addon-actions'; - -addons.setConfig({ - theme: create({ - base: 'light', - brandTitle: 'Kibana React Storybook', - brandUrl: 'https://github.com/elastic/kibana/tree/main/src/plugins/files', - }), - showPanel: true.valueOf, - selectedPanel: PANEL_ID, -}); diff --git a/src/plugins/files/common/api_routes.ts b/src/plugins/files/common/api_routes.ts index 90ddd5d8b6e75..ad6f11ca541d3 100644 --- a/src/plugins/files/common/api_routes.ts +++ b/src/plugins/files/common/api_routes.ts @@ -27,49 +27,46 @@ export interface EndpointInputs< body?: B; } -export interface CreateRouteDefinition { - inputs: { - params: TypeOf>; - query: TypeOf>; - body: TypeOf>; - }; - output: R; -} - -export type AnyEndpoint = CreateRouteDefinition; +type Extends = X extends Y ? Y : unknown; /** - * Abstract type definition for API route inputs and outputs. + * Use this when creating file service endpoints to ensure that the client methods + * are receiving the types they expect as well as providing the expected inputs. + * + * For example, consider create route: + * + * const rt = configSchema.object({...}); * - * These definitions should be shared between the public and server - * as the single source of truth. + * export type Endpoint = CreateRouteDefinition< + * typeof rt, // We pass in our runtime types + * { file: FileJSON }, // We pass in return type + * FilesClient['create'] // We pass in client method + * >; + * + * This will return `unknown` for param, query or body if client-server types + * are out-of-sync. + * + * The very best would be if the client was auto-generated from the server + * endpoint declarations. */ -export interface HttpApiInterfaceEntryDefinition< - P = unknown, - Q = unknown, - B = unknown, - R = unknown +export interface CreateRouteDefinition< + Inputs extends EndpointInputs, + R, + ClientMethod extends (arg: any) => Promise = () => Promise > { inputs: { - params: P; - query: Q; - body: B; + params: Extends[0], TypeOf>>; + query: Extends[0], TypeOf>>; + body: Extends[0], TypeOf>>; }; - output: R; + output: Extends>>; } -export type { Endpoint as CreateFileKindHttpEndpoint } from '../server/routes/file_kind/create'; -export type { Endpoint as DeleteFileKindHttpEndpoint } from '../server/routes/file_kind/delete'; -export type { Endpoint as DownloadFileKindHttpEndpoint } from '../server/routes/file_kind/download'; -export type { Endpoint as GetByIdFileKindHttpEndpoint } from '../server/routes/file_kind/get_by_id'; -export type { Endpoint as ListFileKindHttpEndpoint } from '../server/routes/file_kind/list'; -export type { Endpoint as UpdateFileKindHttpEndpoint } from '../server/routes/file_kind/update'; -export type { Endpoint as UploadFileKindHttpEndpoint } from '../server/routes/file_kind/upload'; -export type { Endpoint as FindFilesHttpEndpoint } from '../server/routes/find'; -export type { Endpoint as FilesMetricsHttpEndpoint } from '../server/routes/metrics'; -export type { Endpoint as FileShareHttpEndpoint } from '../server/routes/file_kind/share/share'; -export type { Endpoint as FileUnshareHttpEndpoint } from '../server/routes/file_kind/share/unshare'; -export type { Endpoint as FileGetShareHttpEndpoint } from '../server/routes/file_kind/share/get'; -export type { Endpoint as FileListSharesHttpEndpoint } from '../server/routes/file_kind/share/list'; -export type { Endpoint as FilePublicDownloadHttpEndpoint } from '../server/routes/public_facing/download'; -export type { Endpoint as BulkDeleteHttpEndpoint } from '../server/routes/bulk_delete'; +export interface AnyEndpoint { + inputs: { + params: any; + query: any; + body: any; + }; + output: any; +} diff --git a/src/plugins/files/common/files_client.ts b/src/plugins/files/common/files_client.ts new file mode 100644 index 0000000000000..5914f93b8ca7e --- /dev/null +++ b/src/plugins/files/common/files_client.ts @@ -0,0 +1,61 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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 { BaseFilesClient } from '@kbn/shared-ux-file-types'; +import type { FilesMetrics } from './types'; + +/** + * A client that can be used to manage a specific {@link FileKind}. + */ +export interface FilesClient extends BaseFilesClient { + /** + * Get metrics of file system, like storage usage. + * + * @param args - Get metrics arguments + */ + getMetrics: () => Promise; + /** + * Download a file, bypassing regular security by way of a + * secret share token. + * + * @param args - Get public download arguments. + */ + publicDownload: (args: { token: string; fileName?: string }) => any; +} + +export type FilesClientResponses = { + [K in keyof FilesClient]: Awaited[K]>>; +}; + +/** + * A files client that is scoped to a specific {@link FileKind}. + * + * More convenient if you want to re-use the same client for the same file kind + * and not specify the kind every time. + */ +export type ScopedFilesClient = { + [K in keyof FilesClient]: K extends 'list' + ? (arg?: Omit[K]>[0], 'kind'>) => ReturnType[K]> + : (arg: Omit[K]>[0], 'kind'>) => ReturnType[K]>; +}; + +/** + * A factory for creating a {@link ScopedFilesClient} + */ +export interface FilesClientFactory { + /** + * Create a files client. + */ + asUnscoped(): FilesClient; + /** + * Create a {@link ScopedFileClient} for a given {@link FileKind}. + * + * @param fileKind - The {@link FileKind} to create a client for. + */ + asScoped(fileKind: string): ScopedFilesClient; +} diff --git a/src/plugins/files/common/types.ts b/src/plugins/files/common/types.ts index 370be9028317c..b0f9b7e0b9b6f 100644 --- a/src/plugins/files/common/types.ts +++ b/src/plugins/files/common/types.ts @@ -9,8 +9,20 @@ import type { SavedObject } from '@kbn/core/server'; import type { Observable } from 'rxjs'; import type { Readable } from 'stream'; +import type { FileJSON, FileStatus, FileMetadata } from '@kbn/shared-ux-file-types'; import type { ES_FIXED_SIZE_INDEX_BLOB_STORE } from './constants'; +export type { + FileKind, + FileJSON, + FileStatus, + FileMetadata, + BaseFilesClient, + FileCompression, + BaseFileMetadata, + FileImageMetadata, +} from '@kbn/shared-ux-file-types'; + /** * Values for paginating through results. */ @@ -25,226 +37,6 @@ export interface Pagination { perPage?: number; } -/** - * Status of a file. - * - * AWAITING_UPLOAD - A file object has been created but does not have any contents. - * UPLOADING - File contents are being uploaded. - * READY - File contents have been uploaded and are ready for to be downloaded. - * UPLOAD_ERROR - An attempt was made to upload file contents but failed. - * DELETED - The file contents have been or are being deleted. - */ -export type FileStatus = 'AWAITING_UPLOAD' | 'UPLOADING' | 'READY' | 'UPLOAD_ERROR' | 'DELETED'; - -/** - * Supported file compression algorithms - */ -export type FileCompression = 'br' | 'gzip' | 'deflate' | 'none'; - -/** - * File metadata fields are defined per the ECS specification: - * - * https://www.elastic.co/guide/en/ecs/current/ecs-file.html - * - * Custom fields are named according to the custom field convention: "CustomFieldName". - */ -// eslint-disable-next-line @typescript-eslint/consistent-type-definitions -export type BaseFileMetadata = { - /** - * Name of the file - * - * @note This field is recommended since it will provide a better UX - */ - name?: string; - - /** - * MIME type of the file contents - */ - mime_type?: string; - - /** - * ISO string representing the file creation date - */ - created?: string; - /** - * Size of the file - */ - size?: number; - /** - * Hash of the file's contents - */ - hash?: { - /** - * UTF-8 string representing MD5 hash - */ - md5?: string; - /** - * UTF-8 string representing sha1 hash - */ - sha1?: string; - /** - * UTF-8 string representing sha256 hash - */ - sha256?: string; - /** - * UTF-8 string representing sha384 hash - */ - sha384?: string; - /** - * UTF-8 string representing sha512 hash - */ - sha512?: string; - /** - * UTF-8 string representing shadeep hash - */ - ssdeep?: string; - /** - * UTF-8 string representing tlsh hash - */ - tlsh?: string; - [hashName: string]: string | undefined; - }; - - /** - * Data about the user that created the file - */ - user?: { - /** - * The human-friendly user name of the owner of the file - * - * @note this field cannot be used to uniquely ID a user. See {@link BaseFileMetadata['user']['id']}. - */ - name?: string; - /** - * The unique ID of the user who created the file, taken from the user profile - * ID. - * - * See https://www.elastic.co/guide/en/elasticsearch/reference/master/user-profile.html. - */ - id?: string; - }; - - /** - * The file extension, for example "jpg", "png", "svg" and so forth - */ - extension?: string; - - /** - * Alternate text that can be used used to describe the contents of the file - * in human-friendly language - */ - Alt?: string; - - /** - * ISO string representing when the file was last updated - */ - Updated?: string; - - /** - * The file's current status - */ - Status?: FileStatus; - - /** - * The maximum number of bytes per file chunk - */ - ChunkSize?: number; - - /** - * Compression algorithm used to transform chunks before they were stored. - */ - Compression?: FileCompression; -}; - -/** - * Extra metadata on a file object specific to Kibana implementation. - */ -export type FileMetadata = Required< - Pick -> & - BaseFileMetadata & { - /** - * Unique identifier of the kind of file. Kibana applications can register - * these at runtime. - */ - FileKind: string; - - /** - * User-defined metadata. - */ - Meta?: Meta; - }; - -/** - * Attributes of a file that represent a serialised version of the file. - */ -export interface FileJSON { - /** - * Unique file ID. - */ - id: string; - /** - * ISO string of when this file was created - */ - created: FileMetadata['created']; - /** - * ISO string of when the file was updated - */ - updated: FileMetadata['Updated']; - /** - * File name. - * - * @note Does not have to be unique. - */ - name: FileMetadata['name']; - /** - * MIME type of the file's contents. - */ - mimeType: FileMetadata['mime_type']; - /** - * The size, in bytes, of the file content. - */ - size: FileMetadata['size']; - /** - * The file extension (dot suffix). - * - * @note this value can be derived from MIME type but is stored for search - * convenience. - */ - extension: FileMetadata['extension']; - - /** - * A consumer defined set of attributes. - * - * Consumers of the file service can add their own tags and identifiers to - * a file using the "meta" object. - */ - meta: FileMetadata['Meta']; - /** - * Use this text to describe the file contents for display and accessibility. - */ - alt: FileMetadata['Alt']; - /** - * A unique kind that governs various aspects of the file. A consumer of the - * files service must register a file kind and link their files to a specific - * kind. - * - * @note This enables stricter access controls to CRUD and other functionality - * exposed by the files service. - */ - fileKind: FileMetadata['FileKind']; - /** - * The current status of the file. - * - * See {@link FileStatus} for more details. - */ - status: FileMetadata['Status']; - /** - * User data associated with this file - */ - user?: FileMetadata['user']; -} - /** * An {@link SavedObject} containing a file object (i.e., metadata only). */ @@ -440,90 +232,6 @@ export interface BlobStorageSettings { // Other blob store settings will go here once available } -interface HttpEndpointDefinition { - /** - * Specify the tags for this endpoint. - * - * @example - * // This will enable access control to this endpoint for users that can access "myApp" only. - * { tags: ['access:myApp'] } - * - */ - tags: string[]; -} - -/** - * A descriptor of meta values associated with a set or "kind" of files. - * - * @note In order to use the file service consumers must register a {@link FileKind} - * in the {@link FileKindsRegistry}. - */ -export interface FileKind { - /** - * Unique file kind ID - */ - id: string; - /** - * Maximum size, in bytes, a file of this kind can be. - * - * @default 4MiB - */ - maxSizeBytes?: number; - - /** - * The MIME type of the file content. - * - * @default accept all mime types - */ - allowedMimeTypes?: string[]; - - /** - * Blob store specific settings that enable configuration of storage - * details. - */ - blobStoreSettings?: BlobStorageSettings; - - /** - * Specify which HTTP routes to create for the file kind. - * - * You can always create your own HTTP routes for working with files but - * this interface allows you to expose basic CRUD operations, upload, download - * and sharing of files over a RESTful-like interface. - * - * @note The public {@link FileClient} uses these endpoints. - */ - http: { - /** - * Expose file creation (and upload) over HTTP. - */ - create?: HttpEndpointDefinition; - /** - * Expose file updates over HTTP. - */ - update?: HttpEndpointDefinition; - /** - * Expose file deletion over HTTP. - */ - delete?: HttpEndpointDefinition; - /** - * Expose "get by ID" functionality over HTTP. - */ - getById?: HttpEndpointDefinition; - /** - * Expose the ability to list all files of this kind over HTTP. - */ - list?: HttpEndpointDefinition; - /** - * Expose the ability to download a file's contents over HTTP. - */ - download?: HttpEndpointDefinition; - /** - * Expose file share functionality over HTTP. - */ - share?: HttpEndpointDefinition; - }; -} - /** * A collection of generally useful metrics about files. */ diff --git a/src/plugins/files/public/components/context.tsx b/src/plugins/files/public/components/context.tsx deleted file mode 100644 index fbf73999b625f..0000000000000 --- a/src/plugins/files/public/components/context.tsx +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import React, { createContext, useContext, type FunctionComponent } from 'react'; -import { FileKindsRegistry, getFileKindsRegistry } from '../../common/file_kinds_registry'; -import type { FilesClient } from '../types'; - -export interface FilesContextValue { - registry: FileKindsRegistry; - /** - * A files client that will be used process uploads. - */ - client: FilesClient; -} - -const FilesContextObject = createContext(null as unknown as FilesContextValue); - -export const useFilesContext = () => { - const ctx = useContext(FilesContextObject); - if (!ctx) { - throw new Error('FilesContext is not found!'); - } - return ctx; -}; - -interface ContextProps { - /** - * A files client that will be used process uploads. - */ - client: FilesClient; -} -export const FilesContext: FunctionComponent = ({ client, children }) => { - return ( - - {children} - - ); -}; diff --git a/src/plugins/files/public/components/file_picker/context.tsx b/src/plugins/files/public/components/file_picker/context.tsx deleted file mode 100644 index c17fe601e487a..0000000000000 --- a/src/plugins/files/public/components/file_picker/context.tsx +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import React, { createContext, useContext, useMemo, useEffect } from 'react'; -import type { FunctionComponent } from 'react'; -import { useFilesContext, FilesContextValue } from '../context'; -import { FilePickerState, createFilePickerState } from './file_picker_state'; - -interface FilePickerContextValue extends FilesContextValue { - state: FilePickerState; - kind: string; -} - -const FilePickerCtx = createContext( - null as unknown as FilePickerContextValue -); - -interface FilePickerContextProps { - kind: string; - pageSize: number; - multiple: boolean; -} -export const FilePickerContext: FunctionComponent = ({ - kind, - pageSize, - multiple, - children, -}) => { - const filesContext = useFilesContext(); - const { client } = filesContext; - const state = useMemo( - () => createFilePickerState({ pageSize, client, kind, selectMultiple: multiple }), - [pageSize, client, kind, multiple] - ); - useEffect(() => state.dispose, [state]); - return ( - - {children} - - ); -}; - -export const useFilePickerContext = (): FilePickerContextValue => { - const ctx = useContext(FilePickerCtx); - if (!ctx) throw new Error('FilePickerContext not found!'); - return ctx; -}; diff --git a/src/plugins/files/public/components/file_picker/file_picker_state.ts b/src/plugins/files/public/components/file_picker/file_picker_state.ts deleted file mode 100644 index a40606509adb8..0000000000000 --- a/src/plugins/files/public/components/file_picker/file_picker_state.ts +++ /dev/null @@ -1,237 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { - map, - tap, - from, - EMPTY, - switchMap, - catchError, - Observable, - shareReplay, - debounceTime, - Subscription, - combineLatest, - BehaviorSubject, - distinctUntilChanged, -} from 'rxjs'; -import type { FileJSON } from '../../../common'; -import type { FilesClient } from '../../types'; - -function naivelyFuzzify(query: string): string { - return query.includes('*') ? query : `*${query}*`; -} - -export class FilePickerState { - /** - * Files the user has selected - */ - public readonly selectedFiles$ = new BehaviorSubject([]); - public readonly selectedFileIds$ = this.selectedFiles$.pipe( - map((files) => files.map((file) => file.id)) - ); - - public readonly isLoading$ = new BehaviorSubject(true); - public readonly loadingError$ = new BehaviorSubject(undefined); - public readonly hasFiles$ = new BehaviorSubject(false); - public readonly hasQuery$ = new BehaviorSubject(false); - public readonly query$ = new BehaviorSubject(undefined); - public readonly queryDebounced$ = this.query$.pipe(debounceTime(100)); - public readonly currentPage$ = new BehaviorSubject(0); - public readonly totalPages$ = new BehaviorSubject(undefined); - public readonly isUploading$ = new BehaviorSubject(false); - - private readonly selectedFiles = new Map(); - private readonly retry$ = new BehaviorSubject(undefined); - private readonly subscriptions: Subscription[] = []; - private readonly internalIsLoading$ = new BehaviorSubject(true); - - constructor( - private readonly client: FilesClient, - private readonly kind: string, - public readonly pageSize: number, - private selectMultiple: boolean - ) { - this.subscriptions = [ - this.query$ - .pipe( - map((query) => Boolean(query)), - distinctUntilChanged() - ) - .subscribe(this.hasQuery$), - this.internalIsLoading$.pipe(distinctUntilChanged()).subscribe(this.isLoading$), - ]; - } - - private readonly requests$ = combineLatest([ - this.currentPage$.pipe(distinctUntilChanged()), - this.query$.pipe(distinctUntilChanged()), - this.retry$, - ]).pipe( - tap(() => this.setIsLoading(true)), // set loading state as early as possible - debounceTime(100) - ); - - /** - * File objects we have loaded on the front end, stored here so that it can - * easily be passed to all relevant UI. - * - * @note This is not explicitly kept in sync with the selected files! - */ - public readonly files$ = this.requests$.pipe( - switchMap(([page, query]) => this.sendRequest(page, query)), - tap(({ total }) => this.updateTotalPages({ total })), - tap(({ total }) => this.hasFiles$.next(Boolean(total))), - map(({ files }) => files), - shareReplay() - ); - - private updateTotalPages = ({ total }: { total: number }): void => { - this.totalPages$.next(Math.ceil(total / this.pageSize)); - }; - - private sendNextSelectedFiles() { - this.selectedFiles$.next(Array.from(this.selectedFiles.values())); - } - - private setIsLoading(value: boolean) { - this.internalIsLoading$.next(value); - } - - /** - * If multiple selection is not configured, this will take the first file id - * if an array of file ids was provided. - */ - public selectFile = (file: FileJSON | FileJSON[]): void => { - const files = Array.isArray(file) ? file : [file]; - if (!this.selectMultiple) { - this.selectedFiles.clear(); - this.selectedFiles.set(files[0].id, files[0]); - } else { - for (const f of files) this.selectedFiles.set(f.id, f); - } - this.sendNextSelectedFiles(); - }; - - private abort: undefined | (() => void) = undefined; - private sendRequest = ( - page: number, - query: undefined | string - ): Observable<{ files: FileJSON[]; total: number }> => { - if (this.isUploading$.getValue()) return EMPTY; - if (this.abort) this.abort(); - this.setIsLoading(true); - this.loadingError$.next(undefined); - - const abortController = new AbortController(); - this.abort = () => { - try { - abortController.abort(); - } catch (e) { - // ignore - } - }; - - const request$ = from( - this.client.list({ - kind: this.kind, - name: query ? [naivelyFuzzify(query)] : undefined, - page: page + 1, - status: ['READY'], - perPage: this.pageSize, - abortSignal: abortController.signal, - }) - ).pipe( - catchError((e) => { - if (e.name !== 'AbortError') { - this.setIsLoading(false); - this.loadingError$.next(e); - } else { - // If the request was aborted, we assume another request is now in progress - } - return EMPTY; - }), - tap(() => { - this.setIsLoading(false); - this.abort = undefined; - }), - shareReplay() - ); - - request$.subscribe(); - - return request$; - }; - - public retry = (): void => { - this.retry$.next(); - }; - - public resetFilters = (): void => { - this.setQuery(undefined); - this.setPage(0); - this.retry(); - }; - - public hasFilesSelected = (): boolean => { - return this.selectedFiles.size > 0; - }; - - public unselectFile = (fileId: string): void => { - if (this.selectedFiles.delete(fileId)) this.sendNextSelectedFiles(); - }; - - public isFileIdSelected = (fileId: string): boolean => { - return this.selectedFiles.has(fileId); - }; - - public getSelectedFileIds = (): string[] => { - return Array.from(this.selectedFiles.keys()); - }; - - public setQuery = (query: undefined | string): void => { - if (query) this.query$.next(query); - else this.query$.next(undefined); - this.currentPage$.next(0); - }; - - public setPage = (page: number): void => { - this.currentPage$.next(page); - }; - - public setIsUploading = (value: boolean): void => { - this.isUploading$.next(value); - }; - - public dispose = (): void => { - for (const sub of this.subscriptions) sub.unsubscribe(); - }; - - watchFileSelected$ = (id: string): Observable => { - return this.selectedFiles$.pipe( - map(() => this.selectedFiles.has(id)), - distinctUntilChanged() - ); - }; -} - -interface CreateFilePickerArgs { - client: FilesClient; - kind: string; - pageSize: number; - selectMultiple: boolean; -} -export const createFilePickerState = ({ - pageSize, - client, - kind, - selectMultiple, -}: CreateFilePickerArgs): FilePickerState => { - return new FilePickerState(client, kind, pageSize, selectMultiple); -}; diff --git a/src/plugins/files/public/components/file_picker/i18n_texts.ts b/src/plugins/files/public/components/file_picker/i18n_texts.ts deleted file mode 100644 index 9bc4b4642cd68..0000000000000 --- a/src/plugins/files/public/components/file_picker/i18n_texts.ts +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { i18n } from '@kbn/i18n'; - -export const i18nTexts = { - title: i18n.translate('files.filePicker.title', { - defaultMessage: 'Select a file', - }), - titleMultiple: i18n.translate('files.filePicker.titleMultiple', { - defaultMessage: 'Select files', - }), - loadingFilesErrorTitle: i18n.translate('files.filePicker.error.loadingTitle', { - defaultMessage: 'Could not load files', - }), - retryButtonLabel: i18n.translate('files.filePicker.error.retryButtonLabel', { - defaultMessage: 'Retry', - }), - emptyStatePrompt: i18n.translate('files.filePicker.emptyStatePromptTitle', { - defaultMessage: 'Upload your first file', - }), - selectFileLabel: i18n.translate('files.filePicker.selectFileButtonLable', { - defaultMessage: 'Select file', - }), - selectFilesLabel: (nrOfFiles: number) => - i18n.translate('files.filePicker.selectFilesButtonLable', { - defaultMessage: 'Select {nrOfFiles} files', - values: { nrOfFiles }, - }), - searchFieldPlaceholder: i18n.translate('files.filePicker.searchFieldPlaceholder', { - defaultMessage: 'my-file-*', - }), - emptyFileGridPrompt: i18n.translate('files.filePicker.emptyGridPrompt', { - defaultMessage: 'No files match your filter', - }), - loadMoreButtonLabel: i18n.translate('files.filePicker.loadMoreButtonLabel', { - defaultMessage: 'Load more', - }), - clearFilterButton: i18n.translate('files.filePicker.clearFilterButtonLabel', { - defaultMessage: 'Clear filter', - }), - uploadFilePlaceholderText: i18n.translate('files.filePicker.uploadFilePlaceholderText', { - defaultMessage: 'Drag and drop to upload new files', - }), -}; diff --git a/src/plugins/files/public/components/index.ts b/src/plugins/files/public/components/index.ts deleted file mode 100644 index 87354bd934f32..0000000000000 --- a/src/plugins/files/public/components/index.ts +++ /dev/null @@ -1,11 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -export { UploadFile, type UploadFileProps } from './upload_file'; -export { FilePicker, type FilePickerProps } from './file_picker'; -export { FilesContext } from './context'; diff --git a/src/plugins/files/public/components/stories_shared.ts b/src/plugins/files/public/components/stories_shared.ts deleted file mode 100644 index f20058ea0f58e..0000000000000 --- a/src/plugins/files/public/components/stories_shared.ts +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 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 { FileKind } from '../../common'; -import { - setFileKindsRegistry, - getFileKindsRegistry, - FileKindsRegistryImpl, -} from '../../common/file_kinds_registry'; - -setFileKindsRegistry(new FileKindsRegistryImpl()); -const fileKindsRegistry = getFileKindsRegistry(); -export const register: FileKindsRegistryImpl['register'] = (fileKind: FileKind) => { - if (!fileKindsRegistry.getAll().find((kind) => kind.id === fileKind.id)) { - getFileKindsRegistry().register(fileKind); - } -}; diff --git a/src/plugins/files/public/components/upload_file/i18n_texts.ts b/src/plugins/files/public/components/upload_file/i18n_texts.ts deleted file mode 100644 index 19ac5b3e0a67d..0000000000000 --- a/src/plugins/files/public/components/upload_file/i18n_texts.ts +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { i18n } from '@kbn/i18n'; - -export const i18nTexts = { - defaultPickerLabel: i18n.translate('files.uploadFile.defaultFilePickerLabel', { - defaultMessage: 'Upload a file', - }), - upload: i18n.translate('files.uploadFile.uploadButtonLabel', { - defaultMessage: 'Upload', - }), - uploading: i18n.translate('files.uploadFile.uploadingButtonLabel', { - defaultMessage: 'Uploading', - }), - uploadComplete: i18n.translate('files.uploadFile.uploadCompleteButtonLabel', { - defaultMessage: 'Upload complete', - }), - retry: i18n.translate('files.uploadFile.retryButtonLabel', { - defaultMessage: 'Retry', - }), - clear: i18n.translate('files.uploadFile.clearButtonLabel', { - defaultMessage: 'Clear', - }), - cancel: i18n.translate('files.uploadFile.cancelButtonLabel', { - defaultMessage: 'Cancel', - }), - uploadDone: i18n.translate('files.uploadFile.uploadDoneToolTipContent', { - defaultMessage: 'Your file was successfully uploaded!', - }), - fileTooLarge: (expectedSize: string) => - i18n.translate('files.uploadFile.fileTooLargeErrorMessage', { - defaultMessage: - 'File is too large. Maximum size is {expectedSize, plural, one {# byte} other {# bytes} }.', - values: { expectedSize }, - }), -}; diff --git a/src/plugins/files/public/components/upload_file/index.tsx b/src/plugins/files/public/components/upload_file/index.tsx deleted file mode 100644 index 3ddde57b71b36..0000000000000 --- a/src/plugins/files/public/components/upload_file/index.tsx +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import React, { lazy, Suspense, ReactNode } from 'react'; -import { EuiLoadingSpinner } from '@elastic/eui'; -import type { Props } from './upload_file'; - -export type { DoneNotification } from './upload_state'; - -export type UploadFileProps = Props & { - /** - * A custom fallback for when component is lazy loading, - * If not provided, is used - */ - lazyLoadFallback?: ReactNode; -}; - -const UploadFileContainer = lazy(() => import('./upload_file')); - -export const UploadFile = (props: UploadFileProps) => ( - }> - - -); diff --git a/src/plugins/files/public/components/upload_file/upload_file.component.tsx b/src/plugins/files/public/components/upload_file/upload_file.component.tsx deleted file mode 100644 index dc6af5ebd8452..0000000000000 --- a/src/plugins/files/public/components/upload_file/upload_file.component.tsx +++ /dev/null @@ -1,162 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import React from 'react'; -import { - EuiText, - EuiSpacer, - EuiFlexItem, - EuiFlexGroup, - EuiFilePicker, - useEuiTheme, - useGeneratedHtmlId, -} from '@elastic/eui'; -import { euiThemeVars } from '@kbn/ui-theme'; -import { css } from '@emotion/react'; -import useObservable from 'react-use/lib/useObservable'; -import { useBehaviorSubject } from '../use_behavior_subject'; -import { i18nTexts } from './i18n_texts'; -import { ControlButton, ClearButton } from './components'; -import { useUploadState } from './context'; - -export interface Props { - meta?: unknown; - accept?: string; - multiple?: boolean; - fullWidth?: boolean; - immediate?: boolean; - allowClear?: boolean; - compressed?: boolean; - initialFilePromptText?: string; - className?: string; -} - -const { euiFormMaxWidth, euiButtonHeightSmall } = euiThemeVars; - -const styles = { - horizontalContainer: css` - display: flex; - flex-direction: row; - `, - fullWidth: css` - width: 100%; - `, -}; - -export const UploadFile = React.forwardRef( - ( - { - compressed, - meta, - accept, - immediate, - allowClear = false, - multiple, - initialFilePromptText, - fullWidth, - className, - }, - ref - ) => { - const { euiTheme } = useEuiTheme(); - const uploadState = useUploadState(); - const uploading = useBehaviorSubject(uploadState.uploading$); - const error = useBehaviorSubject(uploadState.error$); - const done = useObservable(uploadState.done$); - const isInvalid = Boolean(error); - const errorMessage = error?.message; - - const id = useGeneratedHtmlId({ prefix: 'filesUploadFile' }); - const errorId = `${id}_error`; - - return ( -
    - { - uploadState.setFiles(Array.from(fs ?? [])); - if (immediate && uploadState.hasFiles()) uploadState.upload(meta); - }} - multiple={multiple} - initialPromptText={initialFilePromptText} - isLoading={uploading} - isInvalid={isInvalid} - accept={accept} - disabled={Boolean(done?.length || uploading)} - aria-describedby={errorMessage ? errorId : undefined} - display={compressed ? 'default' : 'large'} - /> - - - - - - uploadState.upload(meta)} - /> - - {!compressed && Boolean(!done && !uploading && errorMessage) && ( - - - {errorMessage} - - - )} - {!compressed && done?.length && allowClear && ( - <> - {/* Occupy middle space */} - - - - - )} - -
    - ); - } -); diff --git a/src/plugins/files/public/components/upload_file/upload_file.stories.tsx b/src/plugins/files/public/components/upload_file/upload_file.stories.tsx deleted file mode 100644 index 2a07b8b8f5db5..0000000000000 --- a/src/plugins/files/public/components/upload_file/upload_file.stories.tsx +++ /dev/null @@ -1,207 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import React from 'react'; -import { ComponentMeta, ComponentStory } from '@storybook/react'; -import { action } from '@storybook/addon-actions'; - -import { register } from '../stories_shared'; -import { FilesClient } from '../../types'; -import { FilesContext } from '../context'; -import { UploadFile, Props } from './upload_file'; - -const sleep = (ms: number) => new Promise((res) => setTimeout(res, ms)); -const kind = 'test'; - -const defaultArgs: Props = { - kind, - onDone: action('onDone'), - onError: action('onError'), -}; - -export default { - title: 'stateful/UploadFile', - component: UploadFile, - args: defaultArgs, - decorators: [ - (Story) => ( - ({ file: { id: 'test' } }), - upload: () => sleep(1000), - } as unknown as FilesClient - } - > - - - ), - ], -} as ComponentMeta; - -register({ - id: kind, - http: {}, - allowedMimeTypes: ['*'], -}); - -const miniFile = 'miniFile'; -register({ - id: miniFile, - http: {}, - maxSizeBytes: 1, - allowedMimeTypes: ['*'], -}); - -const zipOnly = 'zipOnly'; -register({ - id: zipOnly, - http: {}, - allowedMimeTypes: ['application/zip'], -}); - -const Template: ComponentStory = (props: Props) => ; - -export const Basic = Template.bind({}); - -export const AllowRepeatedUploads = Template.bind({}); -AllowRepeatedUploads.args = { - allowRepeatedUploads: true, -}; - -export const LongErrorUX = Template.bind({}); -LongErrorUX.decorators = [ - (Story) => ( - ({ file: { id: 'test' } }), - upload: async () => { - await sleep(1000); - throw new Error('Something went wrong while uploading! '.repeat(10).trim()); - }, - delete: async () => {}, - } as unknown as FilesClient - } - > - - - ), -]; - -export const Abort = Template.bind({}); -Abort.decorators = [ - (Story) => ( - ({ file: { id: 'test' } }), - upload: async () => { - await sleep(60000); - }, - delete: async () => {}, - } as unknown as FilesClient - } - > - - - ), -]; - -export const MaxSize = Template.bind({}); -MaxSize.args = { - kind: miniFile, -}; - -export const ZipOnly = Template.bind({}); -ZipOnly.args = { - kind: zipOnly, -}; - -export const AllowClearAfterUpload = Template.bind({}); -AllowClearAfterUpload.args = { - allowClear: true, -}; - -export const ImmediateUpload = Template.bind({}); -ImmediateUpload.args = { - immediate: true, -}; - -export const ImmediateUploadError = Template.bind({}); -ImmediateUploadError.args = { - immediate: true, -}; -ImmediateUploadError.decorators = [ - (Story) => ( - ({ file: { id: 'test' } }), - upload: async () => { - await sleep(1000); - throw new Error('Something went wrong while uploading!'); - }, - delete: async () => {}, - } as unknown as FilesClient - } - > - - - ), -]; - -export const ImmediateUploadAbort = Template.bind({}); -ImmediateUploadAbort.decorators = [ - (Story) => ( - ({ file: { id: 'test' } }), - upload: async () => { - await sleep(60000); - }, - delete: async () => {}, - } as unknown as FilesClient - } - > - - - ), -]; -ImmediateUploadAbort.args = { - immediate: true, -}; - -export const Compressed = Template.bind({}); -Compressed.args = { - compressed: true, -}; - -export const CompressedError = Template.bind({}); -CompressedError.args = { - compressed: true, -}; -CompressedError.decorators = [ - (Story) => ( - ({ file: { id: 'test' } }), - upload: async () => { - await sleep(1000); - throw new Error('Something went wrong while uploading! '.repeat(10).trim()); - }, - delete: async () => {}, - } as unknown as FilesClient - } - > - - - ), -]; diff --git a/src/plugins/files/public/components/upload_file/upload_file.test.tsx b/src/plugins/files/public/components/upload_file/upload_file.test.tsx deleted file mode 100644 index 795ba00d4b678..0000000000000 --- a/src/plugins/files/public/components/upload_file/upload_file.test.tsx +++ /dev/null @@ -1,219 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import React from 'react'; -import { act } from 'react-dom/test-utils'; -import { registerTestBed } from '@kbn/test-jest-helpers'; -import { EuiFilePicker } from '@elastic/eui'; - -import { - FileKindsRegistryImpl, - setFileKindsRegistry, - getFileKindsRegistry, -} from '../../../common/file_kinds_registry'; - -import { createMockFilesClient } from '../../mocks'; - -import { FileJSON } from '../../../common'; -import { FilesContext } from '../context'; -import { UploadFile, Props } from './upload_file'; - -describe('UploadFile', () => { - const sleep = (ms: number) => new Promise((res) => setTimeout(res, ms)); - let onDone: jest.Mock; - let onError: jest.Mock; - let client: ReturnType; - - async function initTestBed(props?: Partial) { - const createTestBed = registerTestBed((p: Props) => ( - - - - )); - - const testBed = await createTestBed({ - kind: 'test', - onDone, - onError, - ...props, - }); - - const baseTestSubj = `filesUploadFile`; - - const testSubjects = { - base: baseTestSubj, - uploadButton: `${baseTestSubj}.uploadButton`, - retryButton: `${baseTestSubj}.retryButton`, - cancelButton: `${baseTestSubj}.cancelButton`, - cancelButtonIcon: `${baseTestSubj}.cancelButtonIcon`, - errorMessage: `${baseTestSubj}.error`, - }; - - return { - ...testBed, - actions: { - addFiles: (files: File[]) => - act(async () => { - testBed.component.find(EuiFilePicker).props().onChange!(files as unknown as FileList); - await sleep(1); - testBed.component.update(); - }), - upload: (retry = false) => - act(async () => { - testBed - .find(retry ? testSubjects.retryButton : testSubjects.uploadButton) - .simulate('click'); - await sleep(1); - testBed.component.update(); - }), - abort: () => - act(() => { - testBed.find(testSubjects.cancelButton).simulate('click'); - testBed.component.update(); - }), - wait: (ms: number) => - act(async () => { - await sleep(ms); - testBed.component.update(); - }), - }, - testSubjects, - }; - } - - beforeAll(() => { - setFileKindsRegistry(new FileKindsRegistryImpl()); - getFileKindsRegistry().register({ - id: 'test', - maxSizeBytes: 10000, - http: {}, - }); - }); - - beforeEach(() => { - client = createMockFilesClient(); - onDone = jest.fn(); - onError = jest.fn(); - }); - - afterEach(() => { - jest.clearAllMocks(); - jest.restoreAllMocks(); - }); - - it('shows the success message when upload completes', async () => { - client.create.mockResolvedValue({ file: { id: 'test', size: 1 } as FileJSON }); - client.upload.mockResolvedValue({ size: 1, ok: true }); - - const { actions, find, exists, testSubjects } = await initTestBed(); - await actions.addFiles([{ name: 'test', size: 1 } as File]); - await actions.upload(); - await sleep(1000); - expect(exists(testSubjects.errorMessage)).toBe(false); - expect(find(testSubjects.uploadButton).text()).toMatch(/upload complete/i); - expect(onDone).toHaveBeenCalledTimes(1); - }); - - it('does not show the upload button for "immediate" uploads', async () => { - client.create.mockResolvedValue({ file: { id: 'test' } as FileJSON }); - client.upload.mockImplementation(() => sleep(100).then(() => ({ ok: true, size: 1 }))); - - const { actions, exists, testSubjects } = await initTestBed({ onDone, immediate: true }); - expect(exists(testSubjects.uploadButton)).toBe(false); - await actions.addFiles([{ name: 'test', size: 1 } as File]); - expect(exists(testSubjects.uploadButton)).toBe(false); - await actions.wait(100); - - expect(onDone).toHaveBeenCalledTimes(1); - expect(onError).not.toHaveBeenCalled(); - }); - - it('allows users to cancel uploads', async () => { - client.create.mockResolvedValue({ file: { id: 'test' } as FileJSON }); - client.upload.mockImplementation(() => sleep(1000).then(() => ({ ok: true, size: 1 }))); - - const { actions, testSubjects, find } = await initTestBed(); - await actions.addFiles([{ name: 'test', size: 1 } as File]); - await actions.upload(); - expect(find(testSubjects.cancelButton).props().disabled).toBe(false); - actions.abort(); - - await sleep(1000); - - expect(onDone).not.toHaveBeenCalled(); - expect(onError).not.toHaveBeenCalled(); - }); - - it('does not show error messages while loading', async () => { - client.create.mockResolvedValue({ file: { id: 'test' } as FileJSON }); - client.upload.mockImplementation(async () => { - await sleep(100); - throw new Error('stop!'); - }); - - const { actions, exists, testSubjects } = await initTestBed(); - expect(exists(testSubjects.errorMessage)).toBe(false); - await actions.addFiles([{ name: 'test', size: 1 } as File]); - expect(exists(testSubjects.errorMessage)).toBe(false); - await actions.upload(); - expect(exists(testSubjects.errorMessage)).toBe(false); - await actions.wait(1000); - expect(exists(testSubjects.uploadButton)).toBe(false); // No upload button - expect(exists(testSubjects.errorMessage)).toBe(true); - await actions.upload(true); - expect(exists(testSubjects.errorMessage)).toBe(false); - await actions.wait(500); - expect(exists(testSubjects.errorMessage)).toBe(true); - - expect(onDone).not.toHaveBeenCalled(); - }); - - it('shows error messages if there are any', async () => { - client.create.mockResolvedValue({ file: { id: 'test', size: 10001 } as FileJSON }); - client.upload.mockImplementation(async () => { - await sleep(100); - throw new Error('stop!'); - }); - - const { actions, exists, testSubjects, find } = await initTestBed(); - expect(exists(testSubjects.errorMessage)).toBe(false); - await actions.addFiles([{ name: 'test', size: 1 } as File]); - await actions.upload(); - await actions.wait(1000); - expect(find(testSubjects.errorMessage).text()).toMatch(/stop/i); - expect(onDone).not.toHaveBeenCalled(); - }); - - it('prevents uploads if there is an issue', async () => { - client.create.mockResolvedValue({ file: { id: 'test', size: 10001 } as FileJSON }); - - const { actions, exists, testSubjects, find } = await initTestBed(); - expect(exists(testSubjects.errorMessage)).toBe(false); - await actions.addFiles([{ name: 'test', size: 10001 } as File]); - expect(exists(testSubjects.errorMessage)).toBe(true); - expect(find(testSubjects.errorMessage).text()).toMatch(/File is too large/); - - expect(onDone).not.toHaveBeenCalled(); - }); - - it('only shows the cancel control in compressed mode', async () => { - const { actions, testSubjects, exists } = await initTestBed({ compressed: true }); - const assertButtons = () => { - expect(exists(testSubjects.cancelButtonIcon)).toBe(true); - expect(exists(testSubjects.cancelButton)).toBe(false); - expect(exists(testSubjects.retryButton)).toBe(false); - expect(exists(testSubjects.uploadButton)).toBe(false); - }; - - assertButtons(); - await actions.addFiles([{ name: 'test', size: 1 } as File]); - assertButtons(); - await actions.wait(1000); - assertButtons(); - }); -}); diff --git a/src/plugins/files/public/components/upload_file/upload_file.tsx b/src/plugins/files/public/components/upload_file/upload_file.tsx deleted file mode 100644 index 7ade7500fde14..0000000000000 --- a/src/plugins/files/public/components/upload_file/upload_file.tsx +++ /dev/null @@ -1,190 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 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 { EuiFilePicker } from '@elastic/eui'; -import React, { type FunctionComponent, useRef, useEffect, useMemo } from 'react'; - -import { useFilesContext } from '../context'; - -import { UploadFile as Component } from './upload_file.component'; -import { createUploadState } from './upload_state'; -import { context } from './context'; -import type { FileJSON } from '../../../common'; - -/** - * An object representing an uploaded file - */ -interface UploadedFile { - /** - * The ID that was generated for the uploaded file - */ - id: string; - /** - * The kind of the file that was passed in to this component - */ - kind: string; - /** - * Attributes of a file that represent a serialised version of the file. - */ - fileJSON: FileJSON; -} - -/** - * UploadFile component props - */ -export interface Props { - /** - * A file kind that should be registered during plugin startup. See {@link FileServiceStart}. - */ - kind: Kind; - /** - * Allow users to clear a file after uploading. - * - * @note this will NOT delete an uploaded file. - */ - allowClear?: boolean; - /** - * Start uploading the file as soon as it is provided - * by the user. - */ - immediate?: boolean; - /** - * Metadata that you want to associate with any uploaded files - */ - meta?: Record; - /** - * Whether to display the file picker with width 100%; - */ - fullWidth?: boolean; - /** - * Whether this component should display a "done" state after processing an - * upload or return to the initial state to allow for another upload. - * - * @default false - */ - allowRepeatedUploads?: boolean; - /** - * The initial text prompt - */ - initialPromptText?: string; - /** - * Called when the an upload process fully completes - */ - onDone: (files: UploadedFile[]) => void; - - /** - * Called when an error occurs during upload - */ - onError?: (e: Error) => void; - - /** - * Will be called whenever an upload starts - */ - onUploadStart?: () => void; - - /** - * Will be called when attempt ends, in error otherwise - */ - onUploadEnd?: () => void; - - /** - * Whether to display the component in it's compact form. - * - * @default false - * - * @note passing "true" here implies true for allowRepeatedUplods and immediate. - */ - compressed?: boolean; - - /** - * Allow upload more than one file at a time - * - * @default false - */ - multiple?: boolean; - /** - * Class name that is passed to the container element - */ - className?: string; -} - -/** - * This component is intended as a wrapper around EuiFilePicker with some opinions - * about upload UX. It is optimised for use in modals, flyouts or forms. - * - * In order to use this component you must register your file kind with {@link FileKindsRegistry} - */ -export const UploadFile = ({ - meta, - onDone, - onError, - fullWidth, - allowClear, - onUploadEnd, - onUploadStart, - compressed = false, - kind: kindId, - multiple = false, - initialPromptText, - immediate = false, - allowRepeatedUploads = false, - className, -}: Props): ReturnType => { - const { registry, client } = useFilesContext(); - const ref = useRef(null); - const fileKind = registry.get(kindId); - const repeatedUploads = compressed || allowRepeatedUploads; - const uploadState = useMemo( - () => - createUploadState({ - client, - fileKind, - allowRepeatedUploads: repeatedUploads, - }), - [client, repeatedUploads, fileKind] - ); - - /** - * Hook state into component callbacks - */ - useEffect(() => { - const subs = [ - uploadState.clear$.subscribe(() => { - ref.current?.removeFiles(); - }), - uploadState.done$.subscribe((n) => n && onDone(n)), - uploadState.error$.subscribe((e) => e && onError?.(e)), - uploadState.uploading$.subscribe((uploading) => - uploading ? onUploadStart?.() : onUploadEnd?.() - ), - ]; - return () => subs.forEach((sub) => sub.unsubscribe()); - }, [uploadState, onDone, onError, onUploadStart, onUploadEnd]); - - useEffect(() => uploadState.dispose, [uploadState]); - - return ( - - - - ); -}; - -/* eslint-disable import/no-default-export */ -export default UploadFile; diff --git a/src/plugins/files/public/components/upload_file/upload_state.ts b/src/plugins/files/public/components/upload_file/upload_state.ts deleted file mode 100644 index 30a0dc38a75d7..0000000000000 --- a/src/plugins/files/public/components/upload_file/upload_state.ts +++ /dev/null @@ -1,268 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 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 { - of, - map, - zip, - from, - race, - take, - filter, - Subject, - finalize, - forkJoin, - mergeMap, - switchMap, - catchError, - shareReplay, - ReplaySubject, - BehaviorSubject, - type Observable, - combineLatest, - distinctUntilChanged, - Subscription, -} from 'rxjs'; -import type { FileKind, FileJSON } from '../../../common/types'; -import type { FilesClient } from '../../types'; -import { ImageMetadataFactory, getImageMetadata, isImage } from '../util'; -import { i18nTexts } from './i18n_texts'; - -import { createStateSubject, type SimpleStateSubject, parseFileName } from './util'; - -interface FileState { - file: File; - status: 'idle' | 'uploading' | 'uploaded' | 'upload_failed'; - id?: string; - fileJSON?: FileJSON; - error?: Error; -} - -type Upload = SimpleStateSubject; - -export interface DoneNotification { - id: string; - kind: string; - fileJSON: FileJSON; -} - -interface UploadOptions { - allowRepeatedUploads?: boolean; -} - -export class UploadState { - private readonly abort$ = new Subject(); - private readonly files$$ = new BehaviorSubject([]); - - public readonly files$ = this.files$$.pipe( - switchMap((files$) => (files$.length ? zip(...files$) : of([]))) - ); - public readonly clear$ = new Subject(); - public readonly error$ = new BehaviorSubject(undefined); - public readonly uploading$ = new BehaviorSubject(false); - public readonly done$ = new Subject(); - - private subscriptions: Subscription[]; - - constructor( - private readonly fileKind: FileKind, - private readonly client: FilesClient, - private readonly opts: UploadOptions = { allowRepeatedUploads: false }, - private readonly loadImageMetadata: ImageMetadataFactory = getImageMetadata - ) { - const latestFiles$ = this.files$$.pipe(switchMap((files$) => combineLatest(files$))); - this.subscriptions = [ - latestFiles$ - .pipe( - map((files) => files.some((file) => file.status === 'uploading')), - distinctUntilChanged() - ) - .subscribe(this.uploading$), - - latestFiles$ - .pipe( - map((files) => { - const errorFile = files.find((file) => Boolean(file.error)); - return errorFile ? errorFile.error : undefined; - }), - filter(Boolean) - ) - .subscribe(this.error$), - - latestFiles$ - .pipe( - filter( - (files) => Boolean(files.length) && files.every((file) => file.status === 'uploaded') - ), - map((files) => - files.map((file) => ({ - id: file.id!, - kind: this.fileKind.id, - fileJSON: file.fileJSON!, - })) - ) - ) - .subscribe(this.done$), - ]; - } - - public isUploading(): boolean { - return this.uploading$.getValue(); - } - - private validateFiles(files: File[]): undefined | string { - if ( - this.fileKind.maxSizeBytes != null && - files.some((file) => file.size > this.fileKind.maxSizeBytes!) - ) { - return i18nTexts.fileTooLarge(String(this.fileKind.maxSizeBytes)); - } - return; - } - - public setFiles = (files: File[]): void => { - if (this.isUploading()) { - throw new Error('Cannot update files while uploading'); - } - - if (!files.length) { - this.done$.next(undefined); - this.error$.next(undefined); - } - - const validationError = this.validateFiles(files); - - this.files$$.next( - files.map((file) => - createStateSubject({ - file, - status: 'idle', - error: validationError ? new Error(validationError) : undefined, - }) - ) - ); - }; - - public abort = (): void => { - if (!this.isUploading()) { - throw new Error('No upload in progress'); - } - this.abort$.next(); - }; - - clear = (): void => { - this.setFiles([]); - this.clear$.next(); - }; - - /** - * Do not throw from this method, it is intended to work with {@link forkJoin} from rxjs which - * unsubscribes from all observables if one of them throws. - */ - private uploadFile = ( - file$: SimpleStateSubject, - abort$: Observable, - meta?: unknown - ): Observable => { - const abortController = new AbortController(); - const abortSignal = abortController.signal; - const { file, status } = file$.getValue(); - if (!['idle', 'upload_failed'].includes(status)) { - return of(undefined); - } - - let uploadTarget: undefined | FileJSON; - - file$.setState({ status: 'uploading', error: undefined }); - - const { name } = parseFileName(file.name); - const mime = file.type || undefined; - const _meta = meta as Record; - - return from(isImage(file) ? this.loadImageMetadata(file) : of(undefined)).pipe( - mergeMap((imageMetadata) => - this.client.create({ - kind: this.fileKind.id, - name, - mimeType: mime, - meta: imageMetadata ? { ...imageMetadata, ..._meta } : _meta, - }) - ), - mergeMap((result) => { - uploadTarget = result.file; - return race( - abort$.pipe( - map(() => { - abortController.abort(); - throw new Error('Abort!'); - }) - ), - this.client.upload({ - body: file, - id: uploadTarget.id, - kind: this.fileKind.id, - abortSignal, - selfDestructOnAbort: true, - contentType: mime, - }) - ); - }), - map(() => { - file$.setState({ status: 'uploaded', id: uploadTarget?.id, fileJSON: uploadTarget }); - }), - catchError((e) => { - const isAbortError = e.message === 'Abort!'; - file$.setState({ status: 'upload_failed', error: isAbortError ? undefined : e }); - return of(isAbortError ? undefined : e); - }) - ); - }; - - public upload = (meta?: unknown): Observable => { - if (this.isUploading()) { - throw new Error('Upload already in progress'); - } - const abort$ = new ReplaySubject(1); - const sub = this.abort$.subscribe(abort$); - const upload$ = this.files$$.pipe( - take(1), - switchMap((files$) => forkJoin(files$.map((file$) => this.uploadFile(file$, abort$, meta)))), - map(() => undefined), - finalize(() => { - if (this.opts.allowRepeatedUploads) this.clear(); - sub.unsubscribe(); - }), - shareReplay() - ); - - upload$.subscribe(); // Kick off the upload - - return upload$; - }; - - public dispose = (): void => { - for (const sub of this.subscriptions) sub.unsubscribe(); - }; - - public hasFiles(): boolean { - return this.files$$.getValue().length > 0; - } -} - -export const createUploadState = ({ - fileKind, - client, - imageMetadataFactory, - ...options -}: { - fileKind: FileKind; - client: FilesClient; - imageMetadataFactory?: ImageMetadataFactory; -} & UploadOptions) => { - return new UploadState(fileKind, client, options, imageMetadataFactory); -}; diff --git a/src/plugins/files/public/components/util/image_metadata.test.ts b/src/plugins/files/public/components/util/image_metadata.test.ts deleted file mode 100644 index d91b9be36f6bb..0000000000000 --- a/src/plugins/files/public/components/util/image_metadata.test.ts +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 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 { fitToBox } from './image_metadata'; -describe('util', () => { - describe('fitToBox', () => { - test('300x300', () => { - expect(fitToBox(300, 300)).toMatchInlineSnapshot(` - Object { - "height": 120, - "width": 120, - } - `); - }); - - test('300x150', () => { - expect(fitToBox(300, 150)).toMatchInlineSnapshot(` - Object { - "height": 60, - "width": 120, - } - `); - }); - - test('4500x9000', () => { - expect(fitToBox(4500, 9000)).toMatchInlineSnapshot(` - Object { - "height": 120, - "width": 60, - } - `); - }); - - test('1000x300', () => { - expect(fitToBox(1000, 300)).toMatchInlineSnapshot(` - Object { - "height": 36, - "width": 120, - } - `); - }); - - test('0x0', () => { - expect(fitToBox(0, 0)).toMatchInlineSnapshot(` - Object { - "height": 0, - "width": 0, - } - `); - }); - }); -}); diff --git a/src/plugins/files/public/components/util/image_metadata.ts b/src/plugins/files/public/components/util/image_metadata.ts deleted file mode 100644 index 891448b99bbbe..0000000000000 --- a/src/plugins/files/public/components/util/image_metadata.ts +++ /dev/null @@ -1,111 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import * as bh from 'blurhash'; -import type { FileImageMetadata } from '@kbn/shared-ux-file-image-types'; - -export function isImage(file: { type?: string }): boolean { - return Boolean(file.type?.startsWith('image/')); -} - -export const boxDimensions = { - width: 120, - height: 120, -}; - -/** - * Calculate the size of an image, fitting to our limits see {@link boxDimensions}, - * while preserving the aspect ratio. - */ -export function fitToBox(width: number, height: number): { width: number; height: number } { - const offsetRatio = Math.abs( - Math.min( - // Find the aspect at which our box is smallest, if less than 1, it means we exceed the limits - Math.min(boxDimensions.width / width, boxDimensions.height / height), - // Values greater than 1 are within our limits - 1 - ) - 1 // Get the percentage we are exceeding. E.g., 0.3 - 1 = -0.7 means the image needs to shrink by 70% to fit - ); - return { - width: Math.floor(width - offsetRatio * width), - height: Math.floor(height - offsetRatio * height), - }; -} - -/** - * Get the native size of the image - */ -function loadImage(src: string): Promise { - return new Promise((res, rej) => { - const image = new window.Image(); - image.src = src; - image.onload = () => res(image); - image.onerror = rej; - }); -} - -/** - * Extract image metadata, assumes that file or blob as an image! - */ -export async function getImageMetadata(file: File | Blob): Promise { - const imgUrl = window.URL.createObjectURL(file); - try { - const image = await loadImage(imgUrl); - const canvas = document.createElement('canvas'); - // blurhash encoding and decoding is an expensive algorithm, - // so we have to shrink the image to speed up the calculation - const { width, height } = fitToBox(image.width, image.height); - canvas.width = width; - canvas.height = height; - const ctx = canvas.getContext('2d'); - if (!ctx) throw new Error('Could not get 2d canvas context!'); - ctx.drawImage(image, 0, 0, width, height); - const imgData = ctx.getImageData(0, 0, width, height); - return { - blurhash: bh.encode(imgData.data, imgData.width, imgData.height, 4, 4), - width: image.width, - height: image.height, - }; - } catch (e) { - // Don't error out if we cannot generate the blurhash - return undefined; - } finally { - window.URL.revokeObjectURL(imgUrl); - } -} - -export type ImageMetadataFactory = typeof getImageMetadata; - -export function getBlurhashSrc({ - width, - height, - hash, -}: { - width: number; - height: number; - hash: string; -}): string { - const smallSizeImageCanvas = document.createElement('canvas'); - const { width: blurWidth, height: blurHeight } = fitToBox(width, height); - smallSizeImageCanvas.width = blurWidth; - smallSizeImageCanvas.height = blurHeight; - - const smallSizeImageCtx = smallSizeImageCanvas.getContext('2d')!; - const imageData = smallSizeImageCtx.createImageData(blurWidth, blurHeight); - imageData.data.set(bh.decode(hash, blurWidth, blurHeight)); - smallSizeImageCtx.putImageData(imageData, 0, 0); - - // scale back the blurred image to the size of the original image, - // so it is sized and positioned the same as the original image when used with an `` tag - const originalSizeImageCanvas = document.createElement('canvas'); - originalSizeImageCanvas.width = width; - originalSizeImageCanvas.height = height; - const originalSizeImageCtx = originalSizeImageCanvas.getContext('2d')!; - originalSizeImageCtx.drawImage(smallSizeImageCanvas, 0, 0, width, height); - return originalSizeImageCanvas.toDataURL(); -} diff --git a/src/plugins/files/public/components/util/index.ts b/src/plugins/files/public/components/util/index.ts deleted file mode 100644 index 9e0dccc6c3365..0000000000000 --- a/src/plugins/files/public/components/util/index.ts +++ /dev/null @@ -1,10 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -export { getImageMetadata, isImage, fitToBox, getBlurhashSrc } from './image_metadata'; -export type { ImageMetadataFactory } from './image_metadata'; diff --git a/src/plugins/files/public/files_client/files_client.ts b/src/plugins/files/public/files_client/files_client.ts index a842c702c2423..9044ab58cbf7e 100644 --- a/src/plugins/files/public/files_client/files_client.ts +++ b/src/plugins/files/public/files_client/files_client.ts @@ -8,6 +8,7 @@ import type { HttpStart } from '@kbn/core/public'; import type { ScopedFilesClient, FilesClient } from '../types'; +import { getFileKindsRegistry } from '../../common/file_kinds_registry'; import { API_BASE_PATH, FILES_API_BASE_PATH, @@ -170,6 +171,9 @@ export function createFilesClient({ publicDownload: ({ token, fileName }) => { return http.get(apiRoutes.getPublicDownloadRoute(fileName), { query: { token } }); }, + getFileKind(id: string) { + return getFileKindsRegistry().get(id); + }, }; return api; } diff --git a/src/plugins/files/public/index.ts b/src/plugins/files/public/index.ts index dc36b4d29d25a..168780bd92380 100644 --- a/src/plugins/files/public/index.ts +++ b/src/plugins/files/public/index.ts @@ -15,13 +15,6 @@ export type { FilesClientFactory, FilesClientResponses, } from './types'; -export { - FilesContext, - UploadFile, - type UploadFileProps, - FilePicker, - type FilePickerProps, -} from './components'; export function plugin() { return new FilesPlugin(); diff --git a/src/plugins/files/public/mocks.ts b/src/plugins/files/public/mocks.ts index 909f1c02c54c2..c22d9bda24608 100644 --- a/src/plugins/files/public/mocks.ts +++ b/src/plugins/files/public/mocks.ts @@ -6,25 +6,12 @@ * Side Public License, v 1. */ +import { createMockFilesClient as createBaseMocksFilesClient } from '@kbn/shared-ux-file-mocks'; import type { DeeplyMockedKeys } from '@kbn/utility-types-jest'; import type { FilesClient } from './types'; -// TODO: Remove this once we have access to the shared file client mock export const createMockFilesClient = (): DeeplyMockedKeys => ({ - create: jest.fn(), - bulkDelete: jest.fn(), - delete: jest.fn(), - download: jest.fn(), - find: jest.fn(), - getById: jest.fn(), - getDownloadHref: jest.fn(), + ...createBaseMocksFilesClient(), getMetrics: jest.fn(), - getShare: jest.fn(), - list: jest.fn(), - listShares: jest.fn(), publicDownload: jest.fn(), - share: jest.fn(), - unshare: jest.fn(), - update: jest.fn(), - upload: jest.fn(), }); diff --git a/src/plugins/files/public/types.ts b/src/plugins/files/public/types.ts index 9504468105852..ee4f622a8ddbb 100644 --- a/src/plugins/files/public/types.ts +++ b/src/plugins/files/public/types.ts @@ -6,189 +6,9 @@ * Side Public License, v 1. */ -import { FileJSON } from '../common'; -import type { - FindFilesHttpEndpoint, - FileShareHttpEndpoint, - BulkDeleteHttpEndpoint, - FileUnshareHttpEndpoint, - FileGetShareHttpEndpoint, - FilesMetricsHttpEndpoint, - ListFileKindHttpEndpoint, - CreateFileKindHttpEndpoint, - FileListSharesHttpEndpoint, - UpdateFileKindHttpEndpoint, - UploadFileKindHttpEndpoint, - DeleteFileKindHttpEndpoint, - GetByIdFileKindHttpEndpoint, - DownloadFileKindHttpEndpoint, - FilePublicDownloadHttpEndpoint, - HttpApiInterfaceEntryDefinition, -} from '../common/api_routes'; - -type UnscopedClientMethodFrom = ( - args: E['inputs']['body'] & - E['inputs']['params'] & - E['inputs']['query'] & { abortSignal?: AbortSignal } -) => Promise; - -/** - * @param args - Input to the endpoint which includes body, params and query of the RESTful endpoint. - */ -type ClientMethodFrom = ( - args: Parameters>[0] & { kind: string } & ExtraArgs -) => Promise; - -interface GlobalEndpoints { - /** - * Get metrics of file system, like storage usage. - * - * @param args - Get metrics arguments - */ - getMetrics: () => Promise; - /** - * Download a file, bypassing regular security by way of a - * secret share token. - * - * @param args - Get public download arguments. - */ - publicDownload: UnscopedClientMethodFrom; - /** - * Find a set of files given some filters. - * - * @param args - File filters - */ - find: UnscopedClientMethodFrom; - /** - * Bulk a delete a set of files given their IDs. - * - * @param args - Bulk delete args - */ - bulkDelete: UnscopedClientMethodFrom; -} - -/** - * A client that can be used to manage a specific {@link FileKind}. - */ -export interface FilesClient extends GlobalEndpoints { - /** - * Create a new file object with the provided metadata. - * - * @param args - create file args - */ - create: ClientMethodFrom>; - /** - * Delete a file object and all associated share and content objects. - * - * @param args - delete file args - */ - delete: ClientMethodFrom; - /** - * Get a file object by ID. - * - * @param args - get file by ID args - */ - getById: ClientMethodFrom>; - /** - * List all file objects, of a given {@link FileKind}. - * - * @param args - list files args - */ - list: ClientMethodFrom>; - /** - * Update a set of of metadata values of the file object. - * - * @param args - update file args - */ - update: ClientMethodFrom>; - /** - * Stream the contents of the file to Kibana server for storage. - * - * @param args - upload file args - */ - upload: ( - args: UploadFileKindHttpEndpoint['inputs']['params'] & - UploadFileKindHttpEndpoint['inputs']['query'] & { - /** - * Should be blob or ReadableStream of some kind. - */ - body: unknown; - kind: string; - abortSignal?: AbortSignal; - contentType?: string; - } - ) => Promise; - /** - * Stream a download of the file object's content. - * - * @param args - download file args - */ - download: ClientMethodFrom; - /** - * Get a string for downloading a file that can be passed to a button element's - * href for download. - * - * @param args - get download URL args - */ - getDownloadHref: (args: Pick) => string; - /** - * Share a file by creating a new file share instance. - * - * @note This returns the secret token that can be used - * to access a file via the public download enpoint. - * - * @param args - File share arguments - */ - share: ClientMethodFrom; - /** - * Delete a file share instance. - * - * @param args - File unshare arguments - */ - unshare: ClientMethodFrom; - /** - * Get a file share instance. - * - * @param args - Get file share arguments - */ - getShare: ClientMethodFrom; - /** - * List all file shares. Optionally scoping to a specific - * file. - * - * @param args - Get file share arguments - */ - listShares: ClientMethodFrom; -} - -export type FilesClientResponses = { - [K in keyof FilesClient]: Awaited[K]>>; -}; - -/** - * A files client that is scoped to a specific {@link FileKind}. - * - * More convenient if you want to re-use the same client for the same file kind - * and not specify the kind every time. - */ -export type ScopedFilesClient = { - [K in keyof FilesClient]: K extends 'list' - ? (arg?: Omit[K]>[0], 'kind'>) => ReturnType[K]> - : (arg: Omit[K]>[0], 'kind'>) => ReturnType[K]>; -}; - -/** - * A factory for creating a {@link ScopedFilesClient} - */ -export interface FilesClientFactory { - /** - * Create a files client. - */ - asUnscoped(): FilesClient; - /** - * Create a {@link ScopedFileClient} for a given {@link FileKind}. - * - * @param fileKind - The {@link FileKind} to create a client for. - */ - asScoped(fileKind: string): ScopedFilesClient; -} +export type { + FilesClient, + ScopedFilesClient, + FilesClientFactory, + FilesClientResponses, +} from '../common/files_client'; diff --git a/src/plugins/files/server/routes/bulk_delete.ts b/src/plugins/files/server/routes/bulk_delete.ts index efb5161dec595..280855bd2b9fb 100644 --- a/src/plugins/files/server/routes/bulk_delete.ts +++ b/src/plugins/files/server/routes/bulk_delete.ts @@ -7,8 +7,9 @@ */ import { schema } from '@kbn/config-schema'; -import type { CreateHandler, FilesRouter } from './types'; import { FILES_MANAGE_PRIVILEGE } from '../../common/constants'; +import { FilesClient } from '../../common/files_client'; +import type { CreateHandler, FilesRouter } from './types'; import { FILES_API_ROUTES, CreateRouteDefinition } from './api_routes'; const method = 'delete' as const; @@ -19,18 +20,20 @@ const rt = { }), }; -interface Result { - /** - * The files that were deleted - */ - succeeded: string[]; - /** - * Any failed deletions. Only included in the response if there were failures. - */ - failed?: Array<[id: string, reason: string]>; -} - -export type Endpoint = CreateRouteDefinition; +export type Endpoint = CreateRouteDefinition< + typeof rt, + { + /** + * The files that were deleted + */ + succeeded: string[]; + /** + * Any failed deletions. Only included in the response if there were failures. + */ + failed?: Array<[id: string, reason: string]>; + }, + FilesClient['bulkDelete'] +>; const handler: CreateHandler = async ({ files }, req, res) => { const fileService = (await files).fileService.asCurrentUser(); @@ -38,8 +41,8 @@ const handler: CreateHandler = async ({ files }, req, res) => { body: { ids }, } = req; - const succeeded: Result['succeeded'] = []; - const failed: Result['failed'] = []; + const succeeded: string[] = []; + const failed: Array<[string, string]> = []; for (const id of ids) { try { await fileService.delete({ id }); diff --git a/src/plugins/files/server/routes/common_schemas.ts b/src/plugins/files/server/routes/common_schemas.ts index 3f99f1cca8059..983caf931b9e1 100644 --- a/src/plugins/files/server/routes/common_schemas.ts +++ b/src/plugins/files/server/routes/common_schemas.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { schema } from '@kbn/config-schema'; +import { schema, Type } from '@kbn/config-schema'; const ALPHA_NUMERIC_WITH_SPACES_REGEX = /^[a-z0-9\s_]+$/i; const ALPHA_NUMERIC_WITH_SPACES_EXT_REGEX = /^[a-z0-9\s\._]+$/i; @@ -46,4 +46,6 @@ export const fileAlt = schema.maybe( export const page = schema.number({ min: 1, defaultValue: 1 }); export const pageSize = schema.number({ min: 1, defaultValue: 100 }); -export const fileMeta = schema.maybe(schema.object({}, { unknowns: 'allow' })); +export const fileMeta = schema.maybe( + schema.object({}, { unknowns: 'allow' }) +) as unknown as Type; diff --git a/src/plugins/files/server/routes/file_kind/create.ts b/src/plugins/files/server/routes/file_kind/create.ts index d654b2c8c1c44..9121e27df0ac1 100644 --- a/src/plugins/files/server/routes/file_kind/create.ts +++ b/src/plugins/files/server/routes/file_kind/create.ts @@ -7,6 +7,7 @@ */ import { schema } from '@kbn/config-schema'; +import { FilesClient } from '../../../common/files_client'; import type { FileJSON, FileKind } from '../../../common/types'; import { CreateRouteDefinition, FILES_API_ROUTES } from '../api_routes'; import type { FileKindRouter } from './types'; @@ -15,7 +16,7 @@ import { CreateHandler } from './types'; export const method = 'post' as const; -const rt = { +export const rt = { body: schema.object({ name: commonSchemas.fileName, alt: commonSchemas.fileAlt, @@ -24,7 +25,11 @@ const rt = { }), }; -export type Endpoint = CreateRouteDefinition }>; +export type Endpoint = CreateRouteDefinition< + typeof rt, + { file: FileJSON }, + FilesClient['create'] +>; export const handler: CreateHandler = async ({ fileKind, files }, req, res) => { const { fileService, security } = await files; diff --git a/src/plugins/files/server/routes/file_kind/delete.ts b/src/plugins/files/server/routes/file_kind/delete.ts index da7bb7bade214..ab1cc74cbae81 100644 --- a/src/plugins/files/server/routes/file_kind/delete.ts +++ b/src/plugins/files/server/routes/file_kind/delete.ts @@ -8,6 +8,7 @@ import { schema } from '@kbn/config-schema'; import type { FileKind } from '../../../common/types'; +import { FilesClient } from '../../../common/files_client'; import { fileErrors } from '../../file'; import { CreateRouteDefinition, FILES_API_ROUTES } from '../api_routes'; import type { CreateHandler, FileKindRouter } from './types'; @@ -22,7 +23,7 @@ const rt = { }), }; -export type Endpoint = CreateRouteDefinition; +export type Endpoint = CreateRouteDefinition; export const handler: CreateHandler = async ({ files, fileKind }, req, res) => { const { diff --git a/src/plugins/files/server/routes/file_kind/download.ts b/src/plugins/files/server/routes/file_kind/download.ts index 4776d37485c93..854a520a69427 100644 --- a/src/plugins/files/server/routes/file_kind/download.ts +++ b/src/plugins/files/server/routes/file_kind/download.ts @@ -8,7 +8,7 @@ import { schema } from '@kbn/config-schema'; import { Readable } from 'stream'; - +import type { FilesClient } from '../../../common/files_client'; import type { FileKind } from '../../../common/types'; import { fileNameWithExt } from '../common_schemas'; import { fileErrors } from '../../file'; @@ -26,7 +26,7 @@ const rt = { }), }; -export type Endpoint = CreateRouteDefinition; +export type Endpoint = CreateRouteDefinition; type Response = Readable; diff --git a/src/plugins/files/server/routes/file_kind/get_by_id.ts b/src/plugins/files/server/routes/file_kind/get_by_id.ts index 914d1e70b77d4..f8fa0ef2bd1d4 100644 --- a/src/plugins/files/server/routes/file_kind/get_by_id.ts +++ b/src/plugins/files/server/routes/file_kind/get_by_id.ts @@ -8,6 +8,7 @@ import { schema } from '@kbn/config-schema'; import type { FileJSON, FileKind } from '../../../common/types'; +import type { FilesClient } from '../../../common/files_client'; import { CreateRouteDefinition, FILES_API_ROUTES } from '../api_routes'; import { getById } from './helpers'; import type { CreateHandler, FileKindRouter } from './types'; @@ -20,7 +21,11 @@ const rt = { }), }; -export type Endpoint = CreateRouteDefinition }>; +export type Endpoint = CreateRouteDefinition< + typeof rt, + { file: FileJSON }, + FilesClient['getById'] +>; export const handler: CreateHandler = async ({ files, fileKind }, req, res) => { const { fileService } = await files; diff --git a/src/plugins/files/server/routes/file_kind/list.ts b/src/plugins/files/server/routes/file_kind/list.ts index 54d8e98fc24f6..d8f571e595d1c 100644 --- a/src/plugins/files/server/routes/file_kind/list.ts +++ b/src/plugins/files/server/routes/file_kind/list.ts @@ -8,6 +8,8 @@ import { schema } from '@kbn/config-schema'; import type { FileJSON, FileKind } from '../../../common/types'; +import type { FilesClient } from '../../../common/files_client'; +import * as commonSchemas from '../common_schemas'; import { CreateRouteDefinition, FILES_API_ROUTES } from '../api_routes'; import * as cs from '../common_schemas'; import type { CreateHandler, FileKindRouter } from './types'; @@ -24,7 +26,7 @@ const rt = { status: schema.maybe(stringOrArrayOfStrings), extension: schema.maybe(stringOrArrayOfStrings), name: schema.maybe(nameStringOrArrayOfNameStrings), - meta: schema.maybe(schema.object({}, { unknowns: 'allow' })), + meta: commonSchemas.fileMeta, }), query: schema.object({ page: schema.maybe(cs.page), @@ -34,7 +36,8 @@ const rt = { export type Endpoint = CreateRouteDefinition< typeof rt, - { files: Array>; total: number } + { files: Array>; total: number }, + FilesClient['find'] >; export const handler: CreateHandler = async ({ files, fileKind }, req, res) => { @@ -50,7 +53,7 @@ export const handler: CreateHandler = async ({ files, fileKind }, req, extension: toArrayOrUndefined(extension), page, perPage, - meta, + meta: meta as Record, }); return res.ok({ body }); }; diff --git a/src/plugins/files/server/routes/file_kind/share/get.ts b/src/plugins/files/server/routes/file_kind/share/get.ts index 07e00d4ad800b..0394bed5a791d 100644 --- a/src/plugins/files/server/routes/file_kind/share/get.ts +++ b/src/plugins/files/server/routes/file_kind/share/get.ts @@ -8,6 +8,7 @@ import { schema } from '@kbn/config-schema'; +import type { FilesClient } from '../../../../common/files_client'; import { FileShareNotFoundError } from '../../../file_share_service/errors'; import { CreateRouteDefinition, FILES_API_ROUTES } from '../../api_routes'; import type { FileKind, FileShareJSON } from '../../../../common/types'; @@ -22,7 +23,11 @@ const rt = { }), }; -export type Endpoint = CreateRouteDefinition; +export type Endpoint = CreateRouteDefinition< + typeof rt, + { share: FileShareJSON }, + FilesClient['getShare'] +>; export const handler: CreateHandler = async ({ files }, req, res) => { const { fileService } = await files; diff --git a/src/plugins/files/server/routes/file_kind/share/list.ts b/src/plugins/files/server/routes/file_kind/share/list.ts index 470102cb815f0..f8eb75b6451bb 100644 --- a/src/plugins/files/server/routes/file_kind/share/list.ts +++ b/src/plugins/files/server/routes/file_kind/share/list.ts @@ -8,6 +8,7 @@ import { schema } from '@kbn/config-schema'; +import type { FilesClient } from '../../../../common/files_client'; import { CreateRouteDefinition, FILES_API_ROUTES } from '../../api_routes'; import type { FileKind, FileShareJSON } from '../../../../common/types'; import { CreateHandler, FileKindRouter } from '../types'; @@ -23,7 +24,11 @@ const rt = { }), }; -export type Endpoint = CreateRouteDefinition; +export type Endpoint = CreateRouteDefinition< + typeof rt, + { shares: FileShareJSON[] }, + FilesClient['listShares'] +>; export const handler: CreateHandler = async ({ files }, req, res) => { const { fileService } = await files; diff --git a/src/plugins/files/server/routes/file_kind/share/share.ts b/src/plugins/files/server/routes/file_kind/share/share.ts index 3d3c5adb53e70..37f6a2e591a19 100644 --- a/src/plugins/files/server/routes/file_kind/share/share.ts +++ b/src/plugins/files/server/routes/file_kind/share/share.ts @@ -8,6 +8,7 @@ import { schema } from '@kbn/config-schema'; import { ExpiryDateInThePastError } from '../../../file_share_service/errors'; +import type { FilesClient } from '../../../../common/files_client'; import { CreateHandler, FileKindRouter } from '../types'; import { CreateRouteDefinition, FILES_API_ROUTES } from '../../api_routes'; @@ -34,7 +35,11 @@ const rt = { }), }; -export type Endpoint = CreateRouteDefinition; +export type Endpoint = CreateRouteDefinition< + typeof rt, + FileShareJSONWithToken, + FilesClient['share'] +>; export const handler: CreateHandler = async ({ files, fileKind }, req, res) => { const { fileService } = await files; diff --git a/src/plugins/files/server/routes/file_kind/share/unshare.ts b/src/plugins/files/server/routes/file_kind/share/unshare.ts index a41f5db8a5970..a09a5fb8fcc2c 100644 --- a/src/plugins/files/server/routes/file_kind/share/unshare.ts +++ b/src/plugins/files/server/routes/file_kind/share/unshare.ts @@ -8,6 +8,7 @@ import { schema } from '@kbn/config-schema'; +import type { FilesClient } from '../../../../common/files_client'; import { CreateRouteDefinition, FILES_API_ROUTES } from '../../api_routes'; import type { FileKind } from '../../../../common/types'; import { CreateHandler, FileKindRouter } from '../types'; @@ -21,7 +22,7 @@ const rt = { }), }; -export type Endpoint = CreateRouteDefinition; +export type Endpoint = CreateRouteDefinition; export const handler: CreateHandler = async ({ files }, req, res) => { const { fileService } = await files; diff --git a/src/plugins/files/server/routes/file_kind/update.ts b/src/plugins/files/server/routes/file_kind/update.ts index 048e846322c5b..0725fc0235f93 100644 --- a/src/plugins/files/server/routes/file_kind/update.ts +++ b/src/plugins/files/server/routes/file_kind/update.ts @@ -8,6 +8,7 @@ import { schema } from '@kbn/config-schema'; import type { FileJSON, FileKind } from '../../../common/types'; +import type { FilesClient } from '../../../common/files_client'; import type { CreateHandler, FileKindRouter } from './types'; import { CreateRouteDefinition, FILES_API_ROUTES } from '../api_routes'; import { getById } from './helpers'; @@ -27,7 +28,11 @@ const rt = { }), }; -export type Endpoint = CreateRouteDefinition }>; +export type Endpoint = CreateRouteDefinition< + typeof rt, + { file: FileJSON }, + FilesClient['update'] +>; export const handler: CreateHandler = async ({ files, fileKind }, req, res) => { const { fileService } = await files; diff --git a/src/plugins/files/server/routes/file_kind/upload.ts b/src/plugins/files/server/routes/file_kind/upload.ts index 88ef492ba11fb..66b9c57e0df67 100644 --- a/src/plugins/files/server/routes/file_kind/upload.ts +++ b/src/plugins/files/server/routes/file_kind/upload.ts @@ -6,9 +6,10 @@ * Side Public License, v 1. */ -import { schema } from '@kbn/config-schema'; +import { schema, Type } from '@kbn/config-schema'; import { ReplaySubject } from 'rxjs'; import { Readable } from 'stream'; +import type { FilesClient } from '../../../common/files_client'; import type { FileKind } from '../../../common/types'; import type { CreateRouteDefinition } from '../../../common/api_routes'; import { FILES_API_ROUTES } from '../api_routes'; @@ -23,7 +24,7 @@ const rt = { params: schema.object({ id: schema.string(), }), - body: schema.stream(), + body: schema.stream() as Type, query: schema.object({ selfDestructOnAbort: schema.maybe(schema.boolean()), }), @@ -34,7 +35,8 @@ export type Endpoint = CreateRouteDefinition< { ok: true; size: number; - } + }, + FilesClient['upload'] >; export const handler: CreateHandler = async ({ files, fileKind }, req, res) => { diff --git a/src/plugins/files/server/routes/find.ts b/src/plugins/files/server/routes/find.ts index 458adc26ec4ee..a81a9d2ea5220 100644 --- a/src/plugins/files/server/routes/find.ts +++ b/src/plugins/files/server/routes/find.ts @@ -7,11 +7,12 @@ */ import { schema } from '@kbn/config-schema'; -import type { CreateHandler, FilesRouter } from './types'; +import { FilesClient } from '../../common/files_client'; import { FileJSON } from '../../common'; import { FILES_MANAGE_PRIVILEGE } from '../../common/constants'; import { FILES_API_ROUTES, CreateRouteDefinition } from './api_routes'; -import { page, pageSize } from './common_schemas'; +import { page, pageSize, fileMeta } from './common_schemas'; +import type { CreateHandler, FilesRouter } from './types'; const method = 'post' as const; @@ -32,7 +33,7 @@ const rt = { status: schema.maybe(stringOrArrayOfStrings), extension: schema.maybe(stringOrArrayOfStrings), name: schema.maybe(nameStringOrArrayOfNameStrings), - meta: schema.maybe(schema.object({}, { unknowns: 'allow' })), + meta: fileMeta, }), query: schema.object({ page: schema.maybe(page), @@ -40,7 +41,11 @@ const rt = { }), }; -export type Endpoint = CreateRouteDefinition; +export type Endpoint = CreateRouteDefinition< + typeof rt, + { files: FileJSON[]; total: number }, + FilesClient['find'] +>; const handler: CreateHandler = async ({ files }, req, res) => { const { fileService } = await files; @@ -54,7 +59,7 @@ const handler: CreateHandler = async ({ files }, req, res) => { name: toArrayOrUndefined(name), status: toArrayOrUndefined(status), extension: toArrayOrUndefined(extension), - meta, + meta: meta as Record, ...query, }); diff --git a/src/plugins/files/server/routes/integration_tests/routes.test.ts b/src/plugins/files/server/routes/integration_tests/routes.test.ts index 133024a3e3130..deaffdd73a7c3 100644 --- a/src/plugins/files/server/routes/integration_tests/routes.test.ts +++ b/src/plugins/files/server/routes/integration_tests/routes.test.ts @@ -6,8 +6,9 @@ * Side Public License, v 1. */ +import { TypeOf } from '@kbn/config-schema'; import type { FileJSON } from '../../../common'; -import type { CreateFileKindHttpEndpoint } from '../../../common/api_routes'; +import type { rt } from '../file_kind/create'; import { setupIntegrationEnvironment, TestEnvironmentUtils } from '../../test_utils'; describe('File HTTP API', () => { @@ -28,7 +29,7 @@ describe('File HTTP API', () => { describe('find', () => { beforeEach(async () => { - const args: Array = [ + const args: Array> = [ { name: 'firstFile', alt: 'my first alt', @@ -55,7 +56,7 @@ describe('File HTTP API', () => { }, ]; - const files = await Promise.all(args.map((arg) => createFile(arg))); + const files = await Promise.all(args.map((arg) => createFile(arg as any))); for (const file of files.slice(0, 2)) { await request diff --git a/src/plugins/files/server/routes/metrics.ts b/src/plugins/files/server/routes/metrics.ts index 6e3a63b7b67c6..2e707ae1c8c1c 100644 --- a/src/plugins/files/server/routes/metrics.ts +++ b/src/plugins/files/server/routes/metrics.ts @@ -8,14 +8,14 @@ import { FILES_MANAGE_PRIVILEGE } from '../../common/constants'; import type { FilesRouter } from './types'; - import { FilesMetrics } from '../../common'; +import { FilesClient } from '../../common/files_client'; import { CreateRouteDefinition, FILES_API_ROUTES } from './api_routes'; import type { FilesRequestHandler } from './types'; const method = 'get' as const; -export type Endpoint = CreateRouteDefinition<{}, FilesMetrics>; +export type Endpoint = CreateRouteDefinition<{}, FilesMetrics, FilesClient['getMetrics']>; const handler: FilesRequestHandler = async ({ files }, req, res) => { const { fileService } = await files; diff --git a/src/plugins/files/server/routes/public_facing/download.ts b/src/plugins/files/server/routes/public_facing/download.ts index 1635f9a7d39fd..3155980095229 100644 --- a/src/plugins/files/server/routes/public_facing/download.ts +++ b/src/plugins/files/server/routes/public_facing/download.ts @@ -8,6 +8,7 @@ import { schema } from '@kbn/config-schema'; import { Readable } from 'stream'; +import type { FilesClient } from '../../../common/files_client'; import { NoDownloadAvailableError } from '../../file/errors'; import { FileNotFoundError } from '../../file_service/errors'; import { @@ -31,7 +32,7 @@ const rt = { }), }; -export type Endpoint = CreateRouteDefinition; +export type Endpoint = CreateRouteDefinition; const handler: CreateHandler = async ({ files }, req, res) => { const { fileService } = await files; diff --git a/src/plugins/guided_onboarding/.i18nrc.json b/src/plugins/guided_onboarding/.i18nrc.json index 4be5bba087b2a..f168a83139f97 100755 --- a/src/plugins/guided_onboarding/.i18nrc.json +++ b/src/plugins/guided_onboarding/.i18nrc.json @@ -3,5 +3,5 @@ "paths": { "guidedOnboarding": "." }, - "translations": ["translations/ja-JP.json"] + "translations": [] } diff --git a/src/plugins/guided_onboarding/common/index.ts b/src/plugins/guided_onboarding/common/index.ts new file mode 100644 index 0000000000000..fb3a9c6c23333 --- /dev/null +++ b/src/plugins/guided_onboarding/common/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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export { PLUGIN_ID, PLUGIN_NAME, API_BASE_PATH } from './constants'; +export { testGuideConfig } from './test_guide_config'; +export type { PluginStatus, PluginState, StepConfig, GuideConfig, GuidesConfig } from './types'; diff --git a/src/plugins/guided_onboarding/common/test_guide_config.ts b/src/plugins/guided_onboarding/common/test_guide_config.ts new file mode 100644 index 0000000000000..9dc252d83e8b0 --- /dev/null +++ b/src/plugins/guided_onboarding/common/test_guide_config.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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import type { GuideConfig } from './types'; + +export const testGuideConfig: GuideConfig = { + title: 'Test guide for development', + description: `This guide is used to test the guided onboarding UI while in development and to run automated tests for the API and UI components.`, + guideName: 'Testing example', + completedGuideRedirectLocation: { + appID: 'guidedOnboardingExample', + path: '/', + }, + docs: { + text: 'Testing example docs', + url: 'example.com', + }, + steps: [ + { + id: 'step1', + title: 'Step 1 (completed via an API request)', + descriptionList: [ + `This step is directly completed by clicking the button that uses the API function 'completeGuideStep`, + 'Navigate to /guidedOnboardingExample/stepOne to complete the step.', + ], + location: { + appID: 'guidedOnboardingExample', + path: 'stepOne', + }, + integration: 'testIntegration', + }, + { + id: 'step2', + title: 'Step 2 (manual completion after navigation)', + descriptionList: [ + 'This step is set to ready_to_complete on page navigation.', + 'After that click the popover on the guide button in the header and mark the step done', + ], + location: { + appID: 'guidedOnboardingExample', + path: 'stepTwo', + }, + manualCompletion: { + title: 'Manual completion step title', + description: + 'Mark the step complete by opening the panel and clicking the button "Mark done"', + readyToCompleteOnNavigation: true, + }, + }, + { + id: 'step3', + title: 'Step 3 (manual completion after click)', + description: + 'This step is completed by clicking a button on the page and then clicking the popover on the guide button in the header and marking the step done', + manualCompletion: { + title: 'Manual completion step title', + description: + 'Mark the step complete by opening the panel and clicking the button "Mark done"', + }, + location: { + appID: 'guidedOnboardingExample', + path: 'stepThree', + }, + }, + ], +}; diff --git a/src/plugins/guided_onboarding/common/types.ts b/src/plugins/guided_onboarding/common/types.ts index 74e0ed38f8142..667390aa342fb 100644 --- a/src/plugins/guided_onboarding/common/types.ts +++ b/src/plugins/guided_onboarding/common/types.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { GuideState } from '@kbn/guided-onboarding'; +import type { GuideId, GuideState, GuideStepIds, StepStatus } from '@kbn/guided-onboarding'; /** * Guided onboarding overall status: @@ -15,8 +15,15 @@ import { GuideState } from '@kbn/guided-onboarding'; * complete: at least one guide has been completed * quit: the user quit a guide before completion * skipped: the user skipped on the landing page + * error: unable to retrieve the plugin state from saved objects */ -export type PluginStatus = 'not_started' | 'in_progress' | 'complete' | 'quit' | 'skipped'; +export type PluginStatus = + | 'not_started' + | 'in_progress' + | 'complete' + | 'quit' + | 'skipped' + | 'error'; export interface PluginState { status: PluginStatus; @@ -24,3 +31,42 @@ export interface PluginState { isActivePeriod: boolean; activeGuide?: GuideState; } + +export interface StepConfig { + id: GuideStepIds; + title: string; + // description is displayed as a single paragraph, can be combined with description list + description?: string; + // description list is displayed as an unordered list, can be combined with description + descriptionList?: Array; + location?: { + appID: string; + path: string; + }; + status?: StepStatus; + integration?: string; + manualCompletion?: { + title: string; + description: string; + readyToCompleteOnNavigation?: boolean; + }; +} + +export interface GuideConfig { + title: string; + description: string; + guideName: string; + docs?: { + text: string; + url: string; + }; + completedGuideRedirectLocation?: { + appID: string; + path: string; + }; + steps: StepConfig[]; +} + +export type GuidesConfig = { + [key in GuideId]: GuideConfig; +}; diff --git a/src/plugins/guided_onboarding/public/components/guide_button.tsx b/src/plugins/guided_onboarding/public/components/guide_button.tsx index c8e17e749b125..7bff376c5af4b 100644 --- a/src/plugins/guided_onboarding/public/components/guide_button.tsx +++ b/src/plugins/guided_onboarding/public/components/guide_button.tsx @@ -11,12 +11,12 @@ import { EuiButton } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import type { GuideState } from '@kbn/guided-onboarding'; -import type { PluginState } from '../../common/types'; -import { getStepConfig } from '../services/helpers'; +import type { GuideConfig, PluginState } from '../../common'; import { GuideButtonPopover } from './guide_button_popover'; interface GuideButtonProps { pluginState: PluginState | undefined; + guideConfig: GuideConfig | undefined; toggleGuidePanel: () => void; isGuidePanelOpen: boolean; navigateToLandingPage: () => void; @@ -42,6 +42,7 @@ const getStepNumber = (state: GuideState): number | undefined => { export const GuideButton = ({ pluginState, + guideConfig, toggleGuidePanel, isGuidePanelOpen, navigateToLandingPage, @@ -101,7 +102,7 @@ export const GuideButton = ({ ); if (stepReadyToComplete) { - const stepConfig = getStepConfig(pluginState.activeGuide.guideId, stepReadyToComplete.id); + const stepConfig = guideConfig?.steps.find((step) => step.id === stepReadyToComplete.id); // check if the stepConfig has manualCompletion info if (stepConfig && stepConfig.manualCompletion) { return ( diff --git a/src/plugins/guided_onboarding/public/components/guide_button_popover.tsx b/src/plugins/guided_onboarding/public/components/guide_button_popover.tsx index 658d78f81ebbc..1decf16a42283 100644 --- a/src/plugins/guided_onboarding/public/components/guide_button_popover.tsx +++ b/src/plugins/guided_onboarding/public/components/guide_button_popover.tsx @@ -50,7 +50,9 @@ export const GuideButtonPopover = ({ )} - {description &&

    {description}

    }
    + + {description &&

    {description}

    } +
    ); }; diff --git a/src/plugins/guided_onboarding/public/components/guide_panel.styles.ts b/src/plugins/guided_onboarding/public/components/guide_panel.styles.ts index 7554ad73777dd..b571087b8650c 100644 --- a/src/plugins/guided_onboarding/public/components/guide_panel.styles.ts +++ b/src/plugins/guided_onboarding/public/components/guide_panel.styles.ts @@ -17,11 +17,14 @@ import { css } from '@emotion/react'; * See https://github.com/elastic/eui/issues/6241 for more details */ export const getGuidePanelStyles = (euiTheme: EuiThemeComputed) => ({ + setupButton: css` + margin-right: ${euiTheme.size.m}; + `, flyoutOverrides: { flyoutContainer: css` top: 55px !important; bottom: 25px !important; - right: 128px; + right: calc(${euiTheme.size.s} + 128px); // Accounting for margin on button border-radius: 6px; inline-size: 480px !important; height: auto; diff --git a/src/plugins/guided_onboarding/public/components/guide_panel.test.tsx b/src/plugins/guided_onboarding/public/components/guide_panel.test.tsx index 81c884d8ded0b..cff22fd433bdc 100644 --- a/src/plugins/guided_onboarding/public/components/guide_panel.test.tsx +++ b/src/plugins/guided_onboarding/public/components/guide_panel.test.tsx @@ -15,11 +15,12 @@ import { httpServiceMock } from '@kbn/core/public/mocks'; import type { HttpSetup } from '@kbn/core/public'; import { registerTestBed, TestBed } from '@kbn/test-jest-helpers'; -import type { PluginState } from '../../common/types'; -import { guidesConfig } from '../constants/guides_config'; +import type { PluginState } from '../../common'; +import { API_BASE_PATH, testGuideConfig } from '../../common'; import { apiService } from '../services/api'; import type { GuidedOnboardingApi } from '../types'; import { + testGuide, testGuideStep1ActiveState, testGuideStep1InProgressState, testGuideStep2InProgressState, @@ -33,13 +34,21 @@ import { GuidePanel } from './guide_panel'; const applicationMock = applicationServiceMock.createStartContract(); const notificationsMock = notificationServiceMock.createStartContract(); +const mockGetResponse = (path: string, pluginState: PluginState) => { + if (path === `${API_BASE_PATH}/configs/${testGuide}`) { + return Promise.resolve({ + config: testGuideConfig, + }); + } + return Promise.resolve({ pluginState }); +}; const setupComponentWithPluginStateMock = async ( httpClient: jest.Mocked, pluginState: PluginState ) => { - httpClient.get.mockResolvedValue({ - pluginState, - }); + httpClient.get.mockImplementation((path) => + mockGetResponse(path as unknown as string, pluginState) + ); apiService.setup(httpClient, true); return await setupGuidePanelComponent(apiService); }; @@ -232,68 +241,91 @@ describe('Guided setup', () => { expect(exists('guidePanel')).toBe(true); expect(exists('guideProgress')).toBe(false); - expect(find('guidePanelStep').length).toEqual(guidesConfig.testGuide.steps.length); + expect(find('guidePanelStep').length).toEqual(testGuideConfig.steps.length); }); - test('shows the progress bar if the first step has been completed', async () => { - const { exists, find, component } = await setupComponentWithPluginStateMock(httpClient, { - status: 'in_progress', - isActivePeriod: true, - activeGuide: testGuideStep2InProgressState, + describe('Guide completion', () => { + test('shows the progress bar if the first step has been completed', async () => { + const { exists, find, component } = await setupComponentWithPluginStateMock(httpClient, { + status: 'in_progress', + isActivePeriod: true, + activeGuide: testGuideStep2InProgressState, + }); + find('guideButton').simulate('click'); + component.update(); + + expect(exists('guidePanel')).toBe(true); + expect(exists('guideProgress')).toBe(true); }); - find('guideButton').simulate('click'); - component.update(); - expect(exists('guidePanel')).toBe(true); - expect(exists('guideProgress')).toBe(true); - }); + test('shows the completed state when all steps has been completed', async () => { + const { exists, find, component } = await setupComponentWithPluginStateMock(httpClient, { + status: 'in_progress', + isActivePeriod: true, + activeGuide: { ...readyToCompleteGuideState, status: 'ready_to_complete' }, + }); + find('guideButton').simulate('click'); + component.update(); - test('shows the completed state when all steps has been completed', async () => { - const { exists, find, component } = await setupComponentWithPluginStateMock(httpClient, { - status: 'in_progress', - isActivePeriod: true, - activeGuide: { ...readyToCompleteGuideState, status: 'ready_to_complete' }, + expect(find('guideTitle').text()).toContain('Well done'); + expect(find('guideDescription').text()).toContain( + `You've completed the Elastic Testing example guide` + ); + expect(exists('onboarding--completeGuideButton--testGuide')).toBe(true); }); - find('guideButton').simulate('click'); - component.update(); - expect(find('guideTitle').text()).toContain('Well done'); - expect(find('guideDescription').text()).toContain( - `You've completed the Elastic Testing example guide` - ); - expect(exists('onboarding--completeGuideButton--testGuide')).toBe(true); - }); + test(`doesn't show the completed state when the last step is not marked as complete`, async () => { + const { exists, find, component } = await setupComponentWithPluginStateMock(httpClient, { + status: 'in_progress', + isActivePeriod: true, + activeGuide: { + ...testGuideStep1ActiveState, + steps: [ + { + ...testGuideStep1ActiveState.steps[0], + status: 'complete', + }, + { + ...testGuideStep1ActiveState.steps[1], + status: 'complete', + }, + { + ...testGuideStep1ActiveState.steps[2], + status: 'ready_to_complete', + }, + ], + }, + }); + find('guideButton').simulate('click'); + component.update(); - test(`doesn't show the completed state when the last step is not marked as complete`, async () => { - const { exists, find, component } = await setupComponentWithPluginStateMock(httpClient, { - status: 'in_progress', - isActivePeriod: true, - activeGuide: { - ...testGuideStep1ActiveState, - steps: [ - { - ...testGuideStep1ActiveState.steps[0], - status: 'complete', - }, - { - ...testGuideStep1ActiveState.steps[1], - status: 'complete', - }, - { - ...testGuideStep1ActiveState.steps[2], - status: 'ready_to_complete', - }, - ], - }, + expect(find('guideTitle').text()).not.toContain('Well done'); + expect(find('guideDescription').text()).not.toContain( + `You've completed the Elastic Testing example guide` + ); + expect(exists('useElasticButton')).toBe(false); }); - find('guideButton').simulate('click'); - component.update(); - expect(find('guideTitle').text()).not.toContain('Well done'); - expect(find('guideDescription').text()).not.toContain( - `You've completed the Elastic Testing example guide` - ); - expect(exists('useElasticButton')).toBe(false); + test('panel works after a guide is completed', async () => { + const { exists, find, component } = await setupComponentWithPluginStateMock(httpClient, { + status: 'in_progress', + isActivePeriod: true, + activeGuide: { ...readyToCompleteGuideState, status: 'ready_to_complete' }, + }); + find('guideButton').simulate('click'); + component.update(); + + httpClient.put.mockResolvedValueOnce({ + pluginState: { status: 'complete', isActivePeriod: true }, + }); + await act(async () => { + find('onboarding--completeGuideButton--testGuide').simulate('click'); + }); + component.update(); + + expect(exists('guideButtonRedirect')).toBe(false); + expect(exists('guideButton')).toBe(false); + }); }); describe('Steps', () => { @@ -400,7 +432,7 @@ describe('Guided setup', () => { expect( find('guidePanelStepDescription') .last() - .containsMatchingElement(

    {guidesConfig.testGuide.steps[2].description}

    ) + .containsMatchingElement(

    {testGuideConfig.steps[2].description}

    ) ).toBe(true); }); @@ -418,7 +450,7 @@ describe('Guided setup', () => { .first() .containsMatchingElement(
      - {guidesConfig.testGuide.steps[0].descriptionList?.map((description, i) => ( + {testGuideConfig.steps[0].descriptionList?.map((description, i) => (
    • {description}
    • ))}
    diff --git a/src/plugins/guided_onboarding/public/components/guide_panel.tsx b/src/plugins/guided_onboarding/public/components/guide_panel.tsx index e8c60786c9870..b3afe28e2becc 100644 --- a/src/plugins/guided_onboarding/public/components/guide_panel.tsx +++ b/src/plugins/guided_onboarding/public/components/guide_panel.tsx @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect, useCallback } from 'react'; import { EuiFlyout, EuiFlyoutBody, @@ -32,10 +32,9 @@ import { ApplicationStart, NotificationsStart } from '@kbn/core/public'; import type { GuideState, GuideStep as GuideStepStatus } from '@kbn/guided-onboarding'; import { GuideId } from '@kbn/guided-onboarding'; -import type { GuideConfig, GuidedOnboardingApi, StepConfig } from '../types'; +import type { GuidedOnboardingApi } from '../types'; -import type { PluginState } from '../../common/types'; -import { getGuideConfig } from '../services/helpers'; +import type { GuideConfig, PluginState, StepConfig } from '../../common'; import { GuideStep } from './guide_panel_step'; import { QuitGuideModal } from './quit_guide_modal'; @@ -79,6 +78,7 @@ export const GuidePanel = ({ api, application, notifications }: GuidePanelProps) const [isGuideOpen, setIsGuideOpen] = useState(false); const [isQuitGuideModalOpen, setIsQuitGuideModalOpen] = useState(false); const [pluginState, setPluginState] = useState(undefined); + const [guideConfig, setGuideConfig] = useState(undefined); const styles = getGuidePanelStyles(euiTheme); @@ -170,7 +170,16 @@ export const GuidePanel = ({ api, application, notifications }: GuidePanelProps) return () => subscription.unsubscribe(); }, [api]); - const guideConfig = getGuideConfig(pluginState?.activeGuide?.guideId)!; + const fetchGuideConfig = useCallback(async () => { + if (pluginState?.activeGuide?.guideId) { + const config = await api.getGuideConfig(pluginState.activeGuide.guideId); + if (config) setGuideConfig(config); + } + }, [api, pluginState]); + + useEffect(() => { + fetchGuideConfig(); + }, [fetchGuideConfig]); // TODO handle loading state // https://github.com/elastic/kibana/issues/139799 @@ -181,14 +190,17 @@ export const GuidePanel = ({ api, application, notifications }: GuidePanelProps) return ( <> - - - {isGuideOpen && ( +
    + +
    + + {isGuideOpen && guideConfig && ( = { completeGuidedOnboardingForIntegration: jest.fn(), skipGuidedOnboarding: jest.fn(), isGuidePanelOpen$: new BehaviorSubject(false), + getGuideConfig: jest.fn(), }, }; diff --git a/src/plugins/guided_onboarding/public/services/api.mocks.ts b/src/plugins/guided_onboarding/public/services/api.mocks.ts index 19cb489a3fbdb..ed87a28c51c33 100644 --- a/src/plugins/guided_onboarding/public/services/api.mocks.ts +++ b/src/plugins/guided_onboarding/public/services/api.mocks.ts @@ -8,7 +8,7 @@ import type { GuideState, GuideId, GuideStepIds } from '@kbn/guided-onboarding'; -import { PluginState } from '../../common/types'; +import { PluginState } from '../../common'; export const testGuide: GuideId = 'testGuide'; export const testGuideFirstStep: GuideStepIds = 'step1'; @@ -87,7 +87,7 @@ export const testGuideStep2ReadyToCompleteState: GuideState = { status: 'complete', }, { - id: testGuideStep1ActiveState.steps[1].id, + ...testGuideStep1ActiveState.steps[1], status: 'ready_to_complete', }, testGuideStep1ActiveState.steps[2], diff --git a/src/plugins/guided_onboarding/public/services/api.test.ts b/src/plugins/guided_onboarding/public/services/api.test.ts index 2b39ca889ac95..0f308fc66a250 100644 --- a/src/plugins/guided_onboarding/public/services/api.test.ts +++ b/src/plugins/guided_onboarding/public/services/api.test.ts @@ -11,7 +11,7 @@ import { httpServiceMock } from '@kbn/core/public/mocks'; import type { GuideState } from '@kbn/guided-onboarding'; import { firstValueFrom, Subscription } from 'rxjs'; -import { API_BASE_PATH } from '../../common/constants'; +import { API_BASE_PATH, testGuideConfig } from '../../common'; import { ApiService } from './api'; import { testGuide, @@ -70,13 +70,13 @@ describe('GuidedOnboarding ApiService', () => { expect(httpClient.get).toHaveBeenCalledTimes(1); }); - it(`re-sends the request if the previous one failed`, async () => { + it(`doesn't send multiple requests if the request failed`, async () => { httpClient.get.mockRejectedValueOnce(new Error('request failed')); subscription = apiService.fetchPluginState$().subscribe(); // wait until the request fails await new Promise((resolve) => process.nextTick(resolve)); anotherSubscription = apiService.fetchPluginState$().subscribe(); - expect(httpClient.get).toHaveBeenCalledTimes(2); + expect(httpClient.get).toHaveBeenCalledTimes(1); }); it(`re-sends the request if the subscription was unsubscribed before the request completed`, async () => { @@ -129,9 +129,9 @@ describe('GuidedOnboarding ApiService', () => { describe('activateGuide', () => { it('activates a new guide', async () => { - // update the mock to no active guides - httpClient.get.mockResolvedValue({ - pluginState: mockPluginStateNotStarted, + // mock the get config request + httpClient.get.mockResolvedValueOnce({ + config: testGuideConfig, }); apiService.setup(httpClient, true); @@ -305,9 +305,12 @@ describe('GuidedOnboarding ApiService', () => { }); it(`marks the step as 'ready_to_complete' if it's configured for manual completion`, async () => { - httpClient.get.mockResolvedValue({ + httpClient.get.mockResolvedValueOnce({ pluginState: { ...mockPluginStateInProgress, activeGuide: testGuideStep2InProgressState }, }); + httpClient.get.mockResolvedValueOnce({ + config: testGuideConfig, + }); apiService.setup(httpClient, true); await apiService.completeGuideStep(testGuide, testGuideManualCompletionStep); @@ -329,7 +332,7 @@ describe('GuidedOnboarding ApiService', () => { }); it('marks the guide as "ready_to_complete" if the current step is the last step in the guide and configured for manual completion', async () => { - httpClient.get.mockResolvedValue({ + httpClient.get.mockResolvedValueOnce({ pluginState: { ...mockPluginStateInProgress, activeGuide: { @@ -341,6 +344,9 @@ describe('GuidedOnboarding ApiService', () => { }, }, }); + httpClient.get.mockResolvedValueOnce({ + config: testGuideConfig, + }); apiService.setup(httpClient, true); await apiService.completeGuideStep(testGuide, testGuideLastStep); @@ -402,9 +408,12 @@ describe('GuidedOnboarding ApiService', () => { describe('isGuidedOnboardingActiveForIntegration$', () => { it('returns true if the integration is part of the active step', (done) => { - httpClient.get.mockResolvedValue({ + httpClient.get.mockResolvedValueOnce({ pluginState: { ...mockPluginStateInProgress, activeGuide: testGuideStep1InProgressState }, }); + httpClient.get.mockResolvedValueOnce({ + config: testGuideConfig, + }); apiService.setup(httpClient, true); subscription = apiService .isGuidedOnboardingActiveForIntegration$(testIntegration) @@ -449,9 +458,12 @@ describe('GuidedOnboarding ApiService', () => { describe('completeGuidedOnboardingForIntegration', () => { it(`completes the step if it's active for the integration`, async () => { - httpClient.get.mockResolvedValue({ + httpClient.get.mockResolvedValueOnce({ pluginState: { ...mockPluginStateInProgress, activeGuide: testGuideStep1InProgressState }, }); + httpClient.get.mockResolvedValueOnce({ + config: testGuideConfig, + }); apiService.setup(httpClient, true); await apiService.completeGuidedOnboardingForIntegration(testIntegration); @@ -482,6 +494,26 @@ describe('GuidedOnboarding ApiService', () => { }); }); + describe('skipGuidedOnboarding', () => { + it(`sends a request to the put state API`, async () => { + await apiService.skipGuidedOnboarding(); + expect(httpClient.put).toHaveBeenCalledTimes(1); + // this assertion depends on the guides config + expect(httpClient.put).toHaveBeenCalledWith(`${API_BASE_PATH}/state`, { + body: JSON.stringify({ status: 'skipped' }), + }); + }); + }); + + describe('getGuideConfig', () => { + it('sends a request to the get config API', async () => { + apiService.setup(httpClient, true); + await apiService.getGuideConfig(testGuide); + expect(httpClient.get).toHaveBeenCalledTimes(1); + expect(httpClient.get).toHaveBeenCalledWith(`${API_BASE_PATH}/configs/${testGuide}`); + }); + }); + describe('no API requests are sent on self-managed deployments', () => { beforeEach(() => { apiService.setup(httpClient, false); @@ -501,5 +533,10 @@ describe('GuidedOnboarding ApiService', () => { await apiService.updatePluginState({}, false); expect(httpClient.put).not.toHaveBeenCalled(); }); + + it('getGuideConfig', async () => { + await apiService.getGuideConfig(testGuide); + expect(httpClient.get).not.toHaveBeenCalled(); + }); }); }); diff --git a/src/plugins/guided_onboarding/public/services/api.ts b/src/plugins/guided_onboarding/public/services/api.ts index 94fe675d9fb06..4bf9cd5b55cc6 100644 --- a/src/plugins/guided_onboarding/public/services/api.ts +++ b/src/plugins/guided_onboarding/public/services/api.ts @@ -7,23 +7,31 @@ */ import { HttpSetup } from '@kbn/core/public'; -import { BehaviorSubject, map, Observable, firstValueFrom, concat, of } from 'rxjs'; +import { + BehaviorSubject, + map, + Observable, + firstValueFrom, + concatMap, + of, + concat, + from, +} from 'rxjs'; import type { GuideState, GuideId, GuideStep, GuideStepIds } from '@kbn/guided-onboarding'; -import { API_BASE_PATH } from '../../common/constants'; -import { PluginState, PluginStatus } from '../../common/types'; +import { API_BASE_PATH } from '../../common'; +import type { PluginState, PluginStatus, GuideConfig } from '../../common'; import { GuidedOnboardingApi } from '../types'; import { - getGuideConfig, getInProgressStepId, - getStepConfig, - getUpdatedSteps, - getGuideStatusOnStepCompletion, - isIntegrationInGuideStep, + getCompletedSteps, isStepInProgress, isStepReadyToComplete, isGuideActive, + getStepConfig, + isLastStep, } from './helpers'; +import { ConfigService } from './config_service'; export class ApiService implements GuidedOnboardingApi { private isCloudEnabled: boolean | undefined; @@ -31,12 +39,14 @@ export class ApiService implements GuidedOnboardingApi { private pluginState$!: BehaviorSubject; private isPluginStateLoading: boolean | undefined; public isGuidePanelOpen$: BehaviorSubject = new BehaviorSubject(false); + private configService = new ConfigService(); public setup(httpClient: HttpSetup, isCloudEnabled: boolean) { this.isCloudEnabled = isCloudEnabled; this.client = httpClient; this.pluginState$ = new BehaviorSubject(undefined); this.isGuidePanelOpen$ = new BehaviorSubject(false); + this.configService.setup(httpClient); } private createGetPluginStateObservable(): Observable { @@ -55,7 +65,13 @@ export class ApiService implements GuidedOnboardingApi { }) .catch((error) => { this.isPluginStateLoading = false; - observer.error(error); + // if the request fails, we initialize the state with error + observer.next({ status: 'error', isActivePeriod: false }); + this.pluginState$.next({ + status: 'error', + isActivePeriod: false, + }); + observer.complete(); }); return () => { this.isPluginStateLoading = false; @@ -169,7 +185,7 @@ export class ApiService implements GuidedOnboardingApi { } // If this is the 1st-time attempt, we need to create the default state - const guideConfig = getGuideConfig(guideId); + const guideConfig = await this.configService.getGuideConfig(guideId); if (guideConfig) { const updatedSteps: GuideStep[] = guideConfig.steps.map((step, stepIndex) => { @@ -236,8 +252,7 @@ export class ApiService implements GuidedOnboardingApi { // All steps should be complete at this point // However, we do a final check here as a safeguard - const allStepsComplete = - Boolean(activeGuide!.steps.find((step) => step.status !== 'complete')) === false; + const allStepsComplete = Boolean(activeGuide!.steps.find((step) => step.status === 'complete')); if (allStepsComplete) { const updatedGuide: GuideState = { @@ -329,11 +344,13 @@ export class ApiService implements GuidedOnboardingApi { const isCurrentStepInProgress = isStepInProgress(activeGuide, guideId, stepId); const isCurrentStepReadyToComplete = isStepReadyToComplete(activeGuide, guideId, stepId); - const stepConfig = getStepConfig(activeGuide!.guideId, stepId); + const guideConfig = await this.configService.getGuideConfig(guideId); + const stepConfig = getStepConfig(guideConfig, activeGuide!.guideId, stepId); const isManualCompletion = stepConfig ? !!stepConfig.manualCompletion : false; + const isLastStepInGuide = isLastStep(guideConfig, guideId, stepId); if (isCurrentStepInProgress || isCurrentStepReadyToComplete) { - const updatedSteps = getUpdatedSteps( + const updatedSteps = getCompletedSteps( activeGuide!, stepId, // if current step is in progress and configured for manual completion, @@ -341,10 +358,15 @@ export class ApiService implements GuidedOnboardingApi { isManualCompletion && isCurrentStepInProgress ); + const status = await this.configService.getGuideStatusOnStepCompletion({ + isLastStepInGuide, + isManualCompletion, + isStepReadyToComplete: isCurrentStepReadyToComplete, + }); const currentGuide: GuideState = { guideId, isActive: true, - status: getGuideStatusOnStepCompletion(activeGuide, guideId, stepId), + status, steps: updatedSteps, }; @@ -371,7 +393,9 @@ export class ApiService implements GuidedOnboardingApi { */ public isGuidedOnboardingActiveForIntegration$(integration?: string): Observable { return this.fetchPluginState$().pipe( - map((state) => isIntegrationInGuideStep(state?.activeGuide, integration)) + concatMap((state) => + from(this.configService.isIntegrationInGuideStep(state?.activeGuide, integration)) + ) ); } @@ -390,7 +414,10 @@ export class ApiService implements GuidedOnboardingApi { const { activeGuide } = pluginState!; const inProgressStepId = getInProgressStepId(activeGuide!); if (!inProgressStepId) return undefined; - const isIntegrationStepActive = isIntegrationInGuideStep(activeGuide!, integration); + const isIntegrationStepActive = await this.configService.isIntegrationInGuideStep( + activeGuide!, + integration + ); if (isIntegrationStepActive) { return await this.completeGuideStep(activeGuide!.guideId, inProgressStepId); } @@ -404,6 +431,20 @@ export class ApiService implements GuidedOnboardingApi { public async skipGuidedOnboarding(): Promise<{ pluginState: PluginState } | undefined> { return await this.updatePluginState({ status: 'skipped' }, false); } + + /** + * Gets the config for the guide. + * @return {Promise} a promise with the guide config or undefined if the config is not found + */ + public async getGuideConfig(guideId: GuideId): Promise { + if (!this.isCloudEnabled) { + return undefined; + } + if (!this.client) { + throw new Error('ApiService has not be initialized.'); + } + return await this.configService.getGuideConfig(guideId); + } } export const apiService = new ApiService(); diff --git a/src/plugins/guided_onboarding/public/services/config_service.test.ts b/src/plugins/guided_onboarding/public/services/config_service.test.ts new file mode 100644 index 0000000000000..11132059af7fa --- /dev/null +++ b/src/plugins/guided_onboarding/public/services/config_service.test.ts @@ -0,0 +1,119 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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 { HttpSetup } from '@kbn/core-http-browser'; +import { httpServiceMock } from '@kbn/core-http-browser-mocks'; +import { API_BASE_PATH, testGuideConfig } from '../../common'; +import { + testGuide, + testGuideNotActiveState, + testGuideStep1InProgressState, + testGuideStep2InProgressState, + testIntegration, + wrongIntegration, +} from './api.mocks'; + +import { ConfigService } from './config_service'; + +describe('GuidedOnboarding ConfigService', () => { + let configService: ConfigService; + let httpClient: jest.Mocked; + + beforeEach(() => { + httpClient = httpServiceMock.createStartContract({ basePath: '/base/path' }); + httpClient.get.mockResolvedValue({ + config: testGuideConfig, + }); + configService = new ConfigService(); + configService.setup(httpClient); + }); + describe('getGuideConfig', () => { + it('sends only one request to the get configs API', async () => { + await configService.getGuideConfig(testGuide); + await configService.getGuideConfig(testGuide); + expect(httpClient.get).toHaveBeenCalledTimes(1); + expect(httpClient.get).toHaveBeenCalledWith(`${API_BASE_PATH}/configs/${testGuide}`); + }); + + it('returns undefined if the config is not found', async () => { + httpClient.get.mockRejectedValueOnce(new Error('Not found')); + configService.setup(httpClient); + const config = await configService.getGuideConfig(testGuide); + expect(config).toBeUndefined(); + }); + + it('returns the config for the guide', async () => { + const config = await configService.getGuideConfig(testGuide); + expect(config).toHaveProperty('title'); + }); + }); + + describe('getGuideStatusOnStepCompletion', () => { + it('returns in_progress when completing not the last step', async () => { + const status = await configService.getGuideStatusOnStepCompletion({ + isLastStepInGuide: false, + isManualCompletion: true, + isStepReadyToComplete: true, + }); + expect(status).toBe('in_progress'); + }); + + it('when completing the last step that is configured for manual completion, returns in_progress if the step is in progress', async () => { + const status = await configService.getGuideStatusOnStepCompletion({ + isLastStepInGuide: true, + isManualCompletion: true, + isStepReadyToComplete: false, + }); + expect(status).toBe('in_progress'); + }); + + it('when completing the last step that is configured for manual completion, returns ready_to_complete if the step is ready_to_complete', async () => { + const status = await configService.getGuideStatusOnStepCompletion({ + isLastStepInGuide: true, + isManualCompletion: true, + isStepReadyToComplete: true, + }); + expect(status).toBe('ready_to_complete'); + }); + }); + + describe('isIntegrationInGuideStep', () => { + it('return true if the integration is defined in the guide step config', async () => { + const result = await configService.isIntegrationInGuideStep( + testGuideStep1InProgressState, + testIntegration + ); + expect(result).toBe(true); + }); + it('returns false if a different integration is defined in the guide step', async () => { + const result = await configService.isIntegrationInGuideStep( + testGuideStep1InProgressState, + wrongIntegration + ); + expect(result).toBe(false); + }); + it('returns false if no integration is defined in the guide step', async () => { + const result = await configService.isIntegrationInGuideStep( + testGuideStep2InProgressState, + testIntegration + ); + expect(result).toBe(false); + }); + it('returns false if no guide is active', async () => { + const result = await configService.isIntegrationInGuideStep( + testGuideNotActiveState, + testIntegration + ); + expect(result).toBe(false); + }); + it('returns false if no integration passed', async () => { + const result = await configService.isIntegrationInGuideStep(testGuideStep1InProgressState); + expect(result).toBe(false); + }); + }); +}); diff --git a/src/plugins/guided_onboarding/public/services/config_service.ts b/src/plugins/guided_onboarding/public/services/config_service.ts new file mode 100644 index 0000000000000..b0bea7deb47bc --- /dev/null +++ b/src/plugins/guided_onboarding/public/services/config_service.ts @@ -0,0 +1,86 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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 { HttpSetup } from '@kbn/core-http-browser'; +import { GuideId, GuideState, GuideStatus } from '@kbn/guided-onboarding'; +import type { GuideConfig, GuidesConfig } from '../../common'; +import { API_BASE_PATH } from '../../common'; +import { findGuideConfigByGuideId, getInProgressStepConfig } from './helpers'; + +type ConfigInitialization = { + [key in GuideId]: boolean | undefined; +}; +export class ConfigService { + private client: HttpSetup | undefined; + private configs: GuidesConfig | undefined; + private isConfigInitialized: ConfigInitialization | undefined; + + setup(httpClient: HttpSetup) { + this.client = httpClient; + this.configs = {} as GuidesConfig; + this.isConfigInitialized = {} as ConfigInitialization; + } + + public async getGuideConfig(guideId: GuideId): Promise { + if (!this.client) { + throw new Error('ConfigService has not be initialized.'); + } + // if not initialized yet, get the config from the backend + if (!this.isConfigInitialized || !this.isConfigInitialized[guideId]) { + try { + const { config } = await this.client.get<{ config: GuideConfig }>( + `${API_BASE_PATH}/configs/${guideId}` + ); + if (!this.isConfigInitialized) this.isConfigInitialized = {} as ConfigInitialization; + this.isConfigInitialized[guideId] = true; + if (!this.configs) this.configs = {} as GuidesConfig; + this.configs[guideId] = config; + } catch (e) { + // if there is an error, set the isInitialized property to avoid multiple requests + if (!this.isConfigInitialized) this.isConfigInitialized = {} as ConfigInitialization; + this.isConfigInitialized[guideId] = true; + } + } + // get the config from the configs property + return findGuideConfigByGuideId(this.configs, guideId); + } + + public async getGuideStatusOnStepCompletion({ + isLastStepInGuide, + isManualCompletion, + isStepReadyToComplete, + }: { + isLastStepInGuide: boolean; + isManualCompletion: boolean; + isStepReadyToComplete: boolean; + }): Promise { + // We want to set the guide status to 'ready_to_complete' if the current step is the last step in the guide + // and the step is not configured for manual completion + // or if the current step is configured for manual completion and the last step is ready to complete + if ( + (isLastStepInGuide && !isManualCompletion) || + (isLastStepInGuide && isManualCompletion && isStepReadyToComplete) + ) { + return 'ready_to_complete'; + } + + // Otherwise the guide is still in progress + return 'in_progress'; + } + + public async isIntegrationInGuideStep( + guideState?: GuideState, + integration?: string + ): Promise { + if (!guideState || !guideState.isActive) return false; + + const guideConfig = await this.getGuideConfig(guideState.guideId); + const stepConfig = getInProgressStepConfig(guideConfig, guideState); + return stepConfig ? stepConfig.integration === integration : false; + } +} diff --git a/src/plugins/guided_onboarding/public/services/helpers.test.ts b/src/plugins/guided_onboarding/public/services/helpers.test.ts index 82720c4f9d223..f1cf0c7c0c561 100644 --- a/src/plugins/guided_onboarding/public/services/helpers.test.ts +++ b/src/plugins/guided_onboarding/public/services/helpers.test.ts @@ -6,51 +6,219 @@ * Side Public License, v 1. */ -import { isIntegrationInGuideStep, isLastStep } from './helpers'; +import { testGuideConfig } from '../../common'; +import type { GuidesConfig } from '../../common'; import { + findGuideConfigByGuideId, + getCompletedSteps, + getInProgressStepConfig, + getInProgressStepId, + getStepConfig, + isGuideActive, + isLastStep, + isStepInProgress, + isStepReadyToComplete, +} from './helpers'; +import { + mockPluginStateInProgress, + mockPluginStateNotStarted, testGuide, testGuideFirstStep, testGuideLastStep, - testGuideNotActiveState, + testGuideManualCompletionStep, + testGuideStep1ActiveState, testGuideStep1InProgressState, testGuideStep2InProgressState, - testIntegration, - wrongIntegration, + testGuideStep2ReadyToCompleteState, } from './api.mocks'; describe('GuidedOnboarding ApiService helpers', () => { - describe('isLastStepActive', () => { + describe('findGuideConfigByGuideId', () => { + it('returns undefined if the config is not found', () => { + const config = findGuideConfigByGuideId( + { testGuide: testGuideConfig } as GuidesConfig, + 'security' + ); + expect(config).toBeUndefined(); + }); + + it('returns the correct config guide', () => { + const config = findGuideConfigByGuideId( + { testGuide: testGuideConfig } as GuidesConfig, + testGuide + ); + expect(config).not.toBeUndefined(); + }); + }); + + describe('getStepConfig', () => { + it('returns undefined if the config is not found', async () => { + const config = getStepConfig(undefined, testGuide, testGuideFirstStep); + expect(config).toBeUndefined(); + }); + + it('returns the config for the step', async () => { + const config = getStepConfig(testGuideConfig, testGuide, testGuideFirstStep); + expect(config).toHaveProperty('title'); + }); + }); + + describe('isLastStep', () => { it('returns true if the passed params are for the last step', () => { - const result = isLastStep(testGuide, testGuideLastStep); + const result = isLastStep(testGuideConfig, testGuide, testGuideLastStep); expect(result).toBe(true); }); it('returns false if the passed params are not for the last step', () => { - const result = isLastStep(testGuide, testGuideFirstStep); + const result = isLastStep(testGuideConfig, testGuide, testGuideFirstStep); expect(result).toBe(false); }); }); - describe('isIntegrationInGuideStep', () => { - it('return true if the integration is defined in the guide step config', () => { - const result = isIntegrationInGuideStep(testGuideStep1InProgressState, testIntegration); - expect(result).toBe(true); + describe('getInProgressStepId', () => { + it('returns undefined if no step is in progress', () => { + const stepId = getInProgressStepId(testGuideStep1ActiveState); + expect(stepId).toBeUndefined(); }); - it('returns false if a different integration is defined in the guide step', () => { - const result = isIntegrationInGuideStep(testGuideStep1InProgressState, wrongIntegration); - expect(result).toBe(false); + it('returns the correct step if that is in progress', () => { + const stepId = getInProgressStepId(testGuideStep1InProgressState); + expect(stepId).toBe('step1'); }); - it('returns false if no integration is defined in the guide step', () => { - const result = isIntegrationInGuideStep(testGuideStep2InProgressState, testIntegration); - expect(result).toBe(false); + }); + + describe('getInProgressStepConfig', () => { + it('returns undefined if no guide config', () => { + const stepConfig = getInProgressStepConfig(undefined, testGuideStep1ActiveState); + expect(stepConfig).toBeUndefined(); }); - it('returns false if no guide is active', () => { - const result = isIntegrationInGuideStep(testGuideNotActiveState, testIntegration); - expect(result).toBe(false); + + it('returns undefined if no step is in progress', () => { + const stepConfig = getInProgressStepConfig(testGuideConfig, testGuideStep1ActiveState); + expect(stepConfig).toBeUndefined(); }); - it('returns false if no integration passed', () => { - const result = isIntegrationInGuideStep(testGuideStep1InProgressState); - expect(result).toBe(false); + + it('returns the correct step config for the step in progress', () => { + const stepConfig = getInProgressStepConfig(testGuideConfig, testGuideStep1InProgressState); + expect(stepConfig).toEqual(testGuideConfig.steps[0]); + }); + }); + + describe('isGuideActive', () => { + it('returns false if plugin state is undefined', () => { + const isActive = isGuideActive(undefined, testGuide); + expect(isActive).toBe(false); + }); + + it('returns true if guideId is undefined and the guide is active', () => { + const isActive = isGuideActive(mockPluginStateInProgress, undefined); + expect(isActive).toBe(true); + }); + + it('returns false if guideId is undefined and the guide is not active', () => { + const isActive = isGuideActive(mockPluginStateNotStarted, undefined); + expect(isActive).toBe(false); + }); + + it('returns false if guide is not in progress', () => { + const isActive = isGuideActive(mockPluginStateInProgress, 'security'); + expect(isActive).toBe(false); + }); + + it('returns true if guide is in progress', () => { + const isActive = isGuideActive(mockPluginStateInProgress, testGuide); + expect(isActive).toBe(true); + }); + }); + + describe('isStepInProgress', () => { + it('returns false if guide state is undefined', () => { + const isInProgress = isStepInProgress(undefined, testGuide, testGuideFirstStep); + expect(isInProgress).toBe(false); + }); + + it('returns false if guide is not active', () => { + const isInProgress = isStepInProgress( + testGuideStep1InProgressState, + 'security', + testGuideFirstStep + ); + expect(isInProgress).toBe(false); + }); + + it('returns false if step is not in progress', () => { + const isInProgress = isStepInProgress( + testGuideStep1InProgressState, + testGuide, + testGuideLastStep + ); + expect(isInProgress).toBe(false); + }); + + it('returns true if step is in progress', () => { + const isInProgress = isStepInProgress( + testGuideStep1InProgressState, + testGuide, + testGuideFirstStep + ); + expect(isInProgress).toBe(true); + }); + }); + + describe('isStepReadyToComplete', () => { + it('returns false if guide state is undefined', () => { + const isReadyToComplete = isStepReadyToComplete(undefined, testGuide, testGuideFirstStep); + expect(isReadyToComplete).toBe(false); + }); + + it('returns false if guide is not active', () => { + const isReadyToComplete = isStepReadyToComplete( + testGuideStep1InProgressState, + 'security', + testGuideFirstStep + ); + expect(isReadyToComplete).toBe(false); + }); + + it('returns false if step is not ready not complete', () => { + const isReadyToComplete = isStepReadyToComplete( + testGuideStep2ReadyToCompleteState, + testGuide, + testGuideLastStep + ); + expect(isReadyToComplete).toBe(false); + }); + + it('returns true if step is ready to complete', () => { + const isInProgress = isStepReadyToComplete( + testGuideStep2ReadyToCompleteState, + testGuide, + testGuideManualCompletionStep + ); + expect(isInProgress).toBe(true); + }); + }); + + describe('getCompletedSteps', () => { + it('completes the step if setToReadyToComplete is false', () => { + const completedSteps = getCompletedSteps( + testGuideStep1InProgressState, + testGuideFirstStep, + false + ); + expect(completedSteps[0].status).toBe('complete'); + // the next step is active + expect(completedSteps[1].status).toBe('active'); + }); + + it('sets the step to ready_to_complete if setToReadyToComplete is true', () => { + const completedSteps = getCompletedSteps( + testGuideStep2InProgressState, + testGuideManualCompletionStep, + true + ); + expect(completedSteps[1].status).toBe('ready_to_complete'); + // the next step is inactive + expect(completedSteps[2].status).toBe('inactive'); }); }); }); diff --git a/src/plugins/guided_onboarding/public/services/helpers.ts b/src/plugins/guided_onboarding/public/services/helpers.ts index 5dfba15f3e2d0..05e8861589da3 100644 --- a/src/plugins/guided_onboarding/public/services/helpers.ts +++ b/src/plugins/guided_onboarding/public/services/helpers.ts @@ -11,35 +11,45 @@ import type { GuideStepIds, GuideState, GuideStep, - GuideStatus, + StepStatus, } from '@kbn/guided-onboarding'; -import { guidesConfig } from '../constants/guides_config'; -import { GuideConfig, StepConfig } from '../types'; -import type { PluginState } from '../../common/types'; +import type { GuidesConfig, PluginState, GuideConfig, StepConfig } from '../../common'; -export const getGuideConfig = (guideId?: GuideId): GuideConfig | undefined => { - if (guideId && Object.keys(guidesConfig).includes(guideId)) { +export const findGuideConfigByGuideId = ( + guidesConfig?: GuidesConfig, + guideId?: GuideId +): GuideConfig | undefined => { + if (guidesConfig && guideId && Object.keys(guidesConfig).includes(guideId)) { return guidesConfig[guideId]; } }; -export const getStepConfig = (guideId: GuideId, stepId: GuideStepIds): StepConfig | undefined => { - const guideConfig = getGuideConfig(guideId); +export const getStepConfig = ( + guideConfig: GuideConfig | undefined, + guideId: GuideId, + stepId: GuideStepIds +): StepConfig | undefined => { return guideConfig?.steps.find((step) => step.id === stepId); }; -const getStepIndex = (guideId: GuideId, stepId: GuideStepIds): number => { - const guide = getGuideConfig(guideId); - if (guide) { - return guide.steps.findIndex((step: StepConfig) => step.id === stepId); +const getStepIndex = ( + guideConfig: GuideConfig | undefined, + guideId: GuideId, + stepId: GuideStepIds +): number => { + if (guideConfig) { + return guideConfig.steps.findIndex((step: StepConfig) => step.id === stepId); } return -1; }; -export const isLastStep = (guideId: GuideId, stepId: GuideStepIds): boolean => { - const guide = getGuideConfig(guideId); - const activeStepIndex = getStepIndex(guideId, stepId); - const stepsNumber = guide?.steps.length || 0; +export const isLastStep = ( + guideConfig: GuideConfig | undefined, + guideId: GuideId, + stepId: GuideStepIds +): boolean => { + const activeStepIndex = getStepIndex(guideConfig, guideId, stepId); + const stepsNumber = guideConfig?.steps.length || 0; if (stepsNumber > 0) { return activeStepIndex === stepsNumber - 1; } @@ -51,26 +61,18 @@ export const getInProgressStepId = (state: GuideState): GuideStepIds | undefined return inProgressStep ? inProgressStep.id : undefined; }; -const getInProgressStepConfig = (state: GuideState): StepConfig | undefined => { +export const getInProgressStepConfig = ( + guideConfig: GuideConfig | undefined, + state: GuideState +): StepConfig | undefined => { const inProgressStepId = getInProgressStepId(state); if (inProgressStepId) { - const config = getGuideConfig(state.guideId); - if (config) { - return config.steps.find((step) => step.id === inProgressStepId); + if (guideConfig) { + return guideConfig.steps.find((step) => step.id === inProgressStepId); } } }; -export const isIntegrationInGuideStep = ( - guideState?: GuideState, - integration?: string -): boolean => { - if (!guideState || !guideState.isActive) return false; - - const stepConfig = getInProgressStepConfig(guideState); - return stepConfig ? stepConfig.integration === integration : false; -}; - export const isGuideActive = (pluginState?: PluginState, guideId?: GuideId): boolean => { // false if pluginState is undefined or plugin state is not in progress // or active guide is undefined @@ -85,16 +87,24 @@ export const isGuideActive = (pluginState?: PluginState, guideId?: GuideId): boo return true; }; -export const isStepInProgress = ( +const isStepStatus = ( guideState: GuideState | undefined, + status: StepStatus, guideId: GuideId, stepId: GuideStepIds ): boolean => { - if (!guideState || !guideState.isActive) return false; + if (!guideState || !guideState.isActive || guideState.guideId !== guideId) return false; // false if the step is not 'in_progress' const selectedStep = guideState.steps.find((step) => step.id === stepId); - return selectedStep ? selectedStep.status === 'in_progress' : false; + return selectedStep ? selectedStep.status === status : false; +}; +export const isStepInProgress = ( + guideState: GuideState | undefined, + guideId: GuideId, + stepId: GuideStepIds +): boolean => { + return isStepStatus(guideState, 'in_progress', guideId, stepId); }; export const isStepReadyToComplete = ( @@ -102,13 +112,10 @@ export const isStepReadyToComplete = ( guideId: GuideId, stepId: GuideStepIds ): boolean => { - if (!guideState || !guideState.isActive) return false; - // false if the step is not 'ready_to_complete' - const selectedStep = guideState!.steps.find((step) => step.id === stepId); - return selectedStep ? selectedStep.status === 'ready_to_complete' : false; + return isStepStatus(guideState, 'ready_to_complete', guideId, stepId); }; -export const getUpdatedSteps = ( +export const getCompletedSteps = ( guideState: GuideState, stepId: GuideStepIds, setToReadyToComplete?: boolean @@ -141,27 +148,3 @@ export const getUpdatedSteps = ( return step; }); }; - -export const getGuideStatusOnStepCompletion = ( - guideState: GuideState | undefined, - guideId: GuideId, - stepId: GuideStepIds -): GuideStatus => { - const stepConfig = getStepConfig(guideId, stepId); - const isManualCompletion = stepConfig?.manualCompletion || false; - const isLastStepInGuide = isLastStep(guideId, stepId); - const isCurrentStepReadyToComplete = isStepReadyToComplete(guideState, guideId, stepId); - - // We want to set the guide status to 'ready_to_complete' if the current step is the last step in the guide - // and the step is not configured for manual completion - // or if the current step is configured for manual completion and the last step is ready to complete - if ( - (isLastStepInGuide && !isManualCompletion) || - (isLastStepInGuide && isManualCompletion && isCurrentStepReadyToComplete) - ) { - return 'ready_to_complete'; - } - - // Otherwise the guide is still in progress - return 'in_progress'; -}; diff --git a/src/plugins/guided_onboarding/public/types.ts b/src/plugins/guided_onboarding/public/types.ts index aeacb79d79a27..034c0337f3cb3 100755 --- a/src/plugins/guided_onboarding/public/types.ts +++ b/src/plugins/guided_onboarding/public/types.ts @@ -6,12 +6,11 @@ * Side Public License, v 1. */ -import React from 'react'; import { Observable } from 'rxjs'; import { HttpSetup } from '@kbn/core/public'; -import type { GuideState, GuideId, GuideStepIds, StepStatus } from '@kbn/guided-onboarding'; +import type { GuideState, GuideId, GuideStepIds } from '@kbn/guided-onboarding'; import type { CloudStart } from '@kbn/cloud-plugin/public'; -import type { PluginStatus, PluginState } from '../common/types'; +import type { PluginStatus, PluginState, GuideConfig } from '../common'; // eslint-disable-next-line @typescript-eslint/no-empty-interface export interface GuidedOnboardingPluginSetup {} @@ -53,42 +52,5 @@ export interface GuidedOnboardingApi { ) => Promise<{ pluginState: PluginState } | undefined>; skipGuidedOnboarding: () => Promise<{ pluginState: PluginState } | undefined>; isGuidePanelOpen$: Observable; + getGuideConfig: (guideId: GuideId) => Promise; } - -export interface StepConfig { - id: GuideStepIds; - title: string; - // description is displayed as a single paragraph, can be combined with description list - description?: string; - // description list is displayed as an unordered list, can be combined with description - descriptionList?: Array; - location?: { - appID: string; - path: string; - }; - status?: StepStatus; - integration?: string; - manualCompletion?: { - title: string; - description: string; - readyToCompleteOnNavigation?: boolean; - }; -} -export interface GuideConfig { - title: string; - description: string; - guideName: string; - docs?: { - text: string; - url: string; - }; - completedGuideRedirectLocation?: { - appID: string; - path: string; - }; - steps: StepConfig[]; -} - -export type GuidesConfig = { - [key in GuideId]: GuideConfig; -}; diff --git a/src/plugins/guided_onboarding/server/helpers/guides_config/index.ts b/src/plugins/guided_onboarding/server/helpers/guides_config/index.ts new file mode 100644 index 0000000000000..a21478ff4ae75 --- /dev/null +++ b/src/plugins/guided_onboarding/server/helpers/guides_config/index.ts @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import type { GuidesConfig } from '../../../common'; +import { testGuideConfig } from '../../../common'; +import { securityConfig } from './security'; +import { observabilityConfig } from './observability'; +import { searchConfig } from './search'; + +export const guidesConfig: GuidesConfig = { + security: securityConfig, + observability: observabilityConfig, + search: searchConfig, + testGuide: testGuideConfig, +}; diff --git a/src/plugins/guided_onboarding/public/constants/guides_config/observability.tsx b/src/plugins/guided_onboarding/server/helpers/guides_config/observability.tsx similarity index 83% rename from src/plugins/guided_onboarding/public/constants/guides_config/observability.tsx rename to src/plugins/guided_onboarding/server/helpers/guides_config/observability.tsx index 4dacb14d57c2d..b57d328345119 100644 --- a/src/plugins/guided_onboarding/public/constants/guides_config/observability.tsx +++ b/src/plugins/guided_onboarding/server/helpers/guides_config/observability.tsx @@ -6,13 +6,8 @@ * Side Public License, v 1. */ -import React from 'react'; - import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n-react'; - -import { EuiLink } from '@elastic/eui'; -import type { GuideConfig } from '../../types'; +import type { GuideConfig } from '../../../common'; export const observabilityConfig: GuideConfig = { title: i18n.translate('guidedOnboarding.observabilityGuide.title', { @@ -36,21 +31,10 @@ export const observabilityConfig: GuideConfig = { }), integration: 'kubernetes', descriptionList: [ - - kube-state-metrics - - ), - }} - />, + i18n.translate('guidedOnboarding.observabilityGuide.addDataStep.descriptionList.item1', { + // TODO add the link to the docs, when markdown support is implemented https://github.com/elastic/kibana/issues/146404 + defaultMessage: 'Deploy kube-state-metrics service to your Kubernetes.', + }), i18n.translate('guidedOnboarding.observabilityGuide.addDataStep.descriptionList.item2', { defaultMessage: 'Add the Elastic Kubernetes integration.', }), diff --git a/src/plugins/guided_onboarding/public/constants/guides_config/search.ts b/src/plugins/guided_onboarding/server/helpers/guides_config/search.ts similarity index 98% rename from src/plugins/guided_onboarding/public/constants/guides_config/search.ts rename to src/plugins/guided_onboarding/server/helpers/guides_config/search.ts index 7a28ad6e86925..fdfe9de877944 100644 --- a/src/plugins/guided_onboarding/public/constants/guides_config/search.ts +++ b/src/plugins/guided_onboarding/server/helpers/guides_config/search.ts @@ -7,7 +7,7 @@ */ import { i18n } from '@kbn/i18n'; -import type { GuideConfig } from '../../types'; +import type { GuideConfig } from '../../../common'; export const searchConfig: GuideConfig = { title: i18n.translate('guidedOnboarding.searchGuide.title', { diff --git a/src/plugins/guided_onboarding/public/constants/guides_config/security.ts b/src/plugins/guided_onboarding/server/helpers/guides_config/security.ts similarity index 84% rename from src/plugins/guided_onboarding/public/constants/guides_config/security.ts rename to src/plugins/guided_onboarding/server/helpers/guides_config/security.ts index 1fdb34ff0ca0e..37b10347eda38 100644 --- a/src/plugins/guided_onboarding/public/constants/guides_config/security.ts +++ b/src/plugins/guided_onboarding/server/helpers/guides_config/security.ts @@ -7,7 +7,7 @@ */ import { i18n } from '@kbn/i18n'; -import type { GuideConfig } from '../../types'; +import type { GuideConfig } from '../../../common'; export const securityConfig: GuideConfig = { title: i18n.translate('guidedOnboarding.securityGuide.title', { @@ -19,7 +19,7 @@ export const securityConfig: GuideConfig = { path: '/dashboards', }, description: i18n.translate('guidedOnboarding.securityGuide.description', { - defaultMessage: `We'll help you get set up quickly, using Elastic's out-of-the-box integrations.`, + defaultMessage: `We'll help you get set up quickly, using Elastic Defend.`, }), steps: [ { @@ -29,10 +29,10 @@ export const securityConfig: GuideConfig = { }), descriptionList: [ i18n.translate('guidedOnboarding.securityGuide.addDataStep.description1', { - defaultMessage: 'Select the Elastic Defend integration to add your data.', + defaultMessage: 'Use Elastic Defend to add your data.', }), i18n.translate('guidedOnboarding.securityGuide.addDataStep.description2', { - defaultMessage: 'Make sure your data looks good.', + defaultMessage: 'See data coming in to your SIEM.', }), ], integration: 'endpoint', @@ -48,10 +48,10 @@ export const securityConfig: GuideConfig = { }), descriptionList: [ i18n.translate('guidedOnboarding.securityGuide.rulesStep.description1', { - defaultMessage: 'Load the prebuilt rules.', + defaultMessage: 'Load the Elastic prebuilt rules.', }), i18n.translate('guidedOnboarding.securityGuide.rulesStep.description2', { - defaultMessage: 'Select the rules that you want.', + defaultMessage: 'Select and enable rules.', }), i18n.translate('guidedOnboarding.securityGuide.rulesStep.description3', { defaultMessage: 'Enable rules to generate alerts.', @@ -59,12 +59,12 @@ export const securityConfig: GuideConfig = { ], manualCompletion: { title: i18n.translate('guidedOnboarding.securityGuide.rulesStep.manualCompletion.title', { - defaultMessage: 'Continue with the tour', + defaultMessage: 'Continue with the guide', }), description: i18n.translate( 'guidedOnboarding.securityGuide.rulesStep.manualCompletion.description', { - defaultMessage: 'After you’ve enabled the rules you want, click here to continue.', + defaultMessage: 'After you’ve enabled the rules you need, continue.', } ), }, @@ -92,12 +92,12 @@ export const securityConfig: GuideConfig = { }, manualCompletion: { title: i18n.translate('guidedOnboarding.securityGuide.alertsStep.manualCompletion.title', { - defaultMessage: 'Continue with the tour', + defaultMessage: 'Continue the guide', }), description: i18n.translate( 'guidedOnboarding.securityGuide.alertsStep.manualCompletion.description', { - defaultMessage: `After you've explored the case you created, click here to continue.`, + defaultMessage: `After you've explored the case, continue.`, } ), }, diff --git a/src/plugins/guided_onboarding/server/helpers/plugin_state_utils.ts b/src/plugins/guided_onboarding/server/helpers/plugin_state_utils.ts index 06fc211f1864f..ca3d891b147be 100644 --- a/src/plugins/guided_onboarding/server/helpers/plugin_state_utils.ts +++ b/src/plugins/guided_onboarding/server/helpers/plugin_state_utils.ts @@ -8,7 +8,7 @@ import { SavedObjectsClient } from '@kbn/core/server'; import { findActiveGuide } from './guide_state_utils'; -import { PluginState, PluginStatus } from '../../common/types'; +import type { PluginState, PluginStatus } from '../../common'; import { pluginStateSavedObjectsId, pluginStateSavedObjectsType, @@ -40,14 +40,7 @@ export const getPluginState = async (savedObjectsClient: SavedObjectsClient) => return pluginState; } else { // create a SO to keep track of the correct creation date - try { - await updatePluginStatus(savedObjectsClient, 'not_started'); - // @yulia, we need to add a user permissions - // check here instead of swallowing this error - // see issue: https://github.com/elastic/kibana/issues/145434 - // eslint-disable-next-line no-empty - } catch (e) {} - + await updatePluginStatus(savedObjectsClient, 'not_started'); return { status: 'not_started', isActivePeriod: true, diff --git a/src/plugins/guided_onboarding/server/routes/config_routes.ts b/src/plugins/guided_onboarding/server/routes/config_routes.ts new file mode 100644 index 0000000000000..d9a82b2635e76 --- /dev/null +++ b/src/plugins/guided_onboarding/server/routes/config_routes.ts @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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 { IRouter } from '@kbn/core/server'; +import { schema } from '@kbn/config-schema'; +import type { GuideId } from '@kbn/guided-onboarding'; +import { API_BASE_PATH } from '../../common'; +import { guidesConfig } from '../helpers/guides_config'; + +export const registerGetConfigRoute = (router: IRouter) => { + // Fetch the config of the guide + router.get( + { + path: `${API_BASE_PATH}/configs/{guideId}`, + validate: { + params: schema.object({ + guideId: schema.string(), + }), + }, + }, + async (context, request, response) => { + const { guideId } = request.params; + if (guidesConfig && guideId && Object.keys(guidesConfig).includes(guideId)) { + return response.ok({ + body: { config: guidesConfig[guideId as GuideId] }, + }); + } + return response.notFound(); + } + ); +}; diff --git a/src/plugins/guided_onboarding/server/routes/guide_state_routes.ts b/src/plugins/guided_onboarding/server/routes/guide_state_routes.ts index daf4461773032..2d6a58d1e1684 100644 --- a/src/plugins/guided_onboarding/server/routes/guide_state_routes.ts +++ b/src/plugins/guided_onboarding/server/routes/guide_state_routes.ts @@ -7,7 +7,7 @@ */ import { IRouter, SavedObjectsClient } from '@kbn/core/server'; -import { API_BASE_PATH } from '../../common/constants'; +import { API_BASE_PATH } from '../../common'; import { findAllGuides } from '../helpers'; export const registerGetGuideStateRoute = (router: IRouter) => { diff --git a/src/plugins/guided_onboarding/server/routes/index.ts b/src/plugins/guided_onboarding/server/routes/index.ts index 361fd6ec797a0..a526d02ef94f6 100755 --- a/src/plugins/guided_onboarding/server/routes/index.ts +++ b/src/plugins/guided_onboarding/server/routes/index.ts @@ -9,10 +9,13 @@ import type { IRouter } from '@kbn/core/server'; import { registerGetGuideStateRoute } from './guide_state_routes'; import { registerGetPluginStateRoute, registerPutPluginStateRoute } from './plugin_state_routes'; +import { registerGetConfigRoute } from './config_routes'; export function defineRoutes(router: IRouter) { registerGetGuideStateRoute(router); registerGetPluginStateRoute(router); registerPutPluginStateRoute(router); + + registerGetConfigRoute(router); } diff --git a/src/plugins/guided_onboarding/server/routes/plugin_state_routes.ts b/src/plugins/guided_onboarding/server/routes/plugin_state_routes.ts index 169333f790912..33d52ea7ce255 100644 --- a/src/plugins/guided_onboarding/server/routes/plugin_state_routes.ts +++ b/src/plugins/guided_onboarding/server/routes/plugin_state_routes.ts @@ -10,7 +10,7 @@ import { IRouter, SavedObjectsClient } from '@kbn/core/server'; import { schema } from '@kbn/config-schema'; import { GuideState } from '@kbn/guided-onboarding'; import { getPluginState, updatePluginStatus } from '../helpers/plugin_state_utils'; -import { API_BASE_PATH } from '../../common/constants'; +import { API_BASE_PATH } from '../../common'; import { updateGuideState } from '../helpers'; export const registerGetPluginStateRoute = (router: IRouter) => { diff --git a/src/plugins/kibana_react/public/code_editor/code_editor.tsx b/src/plugins/kibana_react/public/code_editor/code_editor.tsx index 93a2c6333f9cc..bfae4b052f5d9 100644 --- a/src/plugins/kibana_react/public/code_editor/code_editor.tsx +++ b/src/plugins/kibana_react/public/code_editor/code_editor.tsx @@ -443,7 +443,7 @@ export const CodeEditor: React.FC = ({
    ) : null} diff --git a/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts b/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts index 150bc768601f4..0e77b705075f2 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts @@ -582,4 +582,8 @@ export const stackManagementSchema: MakeSchemaFrom = { type: 'boolean', _meta: { description: 'Non-default value of setting.' }, }, + 'enterpriseSearch:enableEnginesSection': { + type: 'boolean', + _meta: { description: 'Non-default value of setting.' }, + }, }; diff --git a/src/plugins/kibana_usage_collection/server/collectors/management/types.ts b/src/plugins/kibana_usage_collection/server/collectors/management/types.ts index 8a897721b6dc6..4365e2a1dd72e 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/management/types.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/management/types.ts @@ -155,4 +155,5 @@ export interface UsageStats { 'securitySolution:showRelatedIntegrations': boolean; 'visualization:visualize:legacyGaugeChartsLibrary': boolean; 'enterpriseSearch:enableBehavioralAnalyticsSection': boolean; + 'enterpriseSearch:enableEnginesSection': boolean; } diff --git a/src/plugins/kibana_utils/public/state_management/state_encoder/encode_decode_state.ts b/src/plugins/kibana_utils/public/state_management/state_encoder/encode_decode_state.ts index 3fa11164fad28..952463b2b7b37 100644 --- a/src/plugins/kibana_utils/public/state_management/state_encoder/encode_decode_state.ts +++ b/src/plugins/kibana_utils/public/state_management/state_encoder/encode_decode_state.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import rison, { RisonValue } from 'rison-node'; +import rison from '@kbn/rison'; import { isStateHash, retrieveState, persistState } from '../state_hash'; // should be: @@ -22,14 +22,14 @@ export function decodeState(expandedOrHashedState: string): State { } // should be: -// export function encodeState(expandedOrHashedState: string) -// but this leads to the chain of types mismatches up to BaseStateContainer interfaces, -// as in state containers we don't have any restrictions on state shape +// export function encodeState but this leads to the chain of +// types mismatches up to BaseStateContainer interfaces, as in state containers we don't +// have any restrictions on state shape export function encodeState(state: State, useHash: boolean): string { if (useHash) { return persistState(state); } else { - return rison.encode(state as unknown as RisonValue); + return rison.encodeUnknown(state) ?? ''; } } diff --git a/src/plugins/kibana_utils/public/state_management/state_hash/state_hash.test.ts b/src/plugins/kibana_utils/public/state_management/state_hash/state_hash.test.ts index b1b6bc3c8107d..cd850c25b71c3 100644 --- a/src/plugins/kibana_utils/public/state_management/state_hash/state_hash.test.ts +++ b/src/plugins/kibana_utils/public/state_management/state_hash/state_hash.test.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { encode as encodeRison } from 'rison-node'; +import { encode as encodeRison } from '@kbn/rison'; import { mockStorage } from '../../storage/hashed_item_store/mock'; import { createStateHash, isStateHash } from './state_hash'; diff --git a/src/plugins/presentation_util/public/components/expression_input/language.ts b/src/plugins/presentation_util/public/components/expression_input/language.ts index 8f50ae97fa83d..2246e9bb271ce 100644 --- a/src/plugins/presentation_util/public/components/expression_input/language.ts +++ b/src/plugins/presentation_util/public/components/expression_input/language.ts @@ -116,7 +116,7 @@ export function registerExpressionsLanguage(functions: ExpressionFunction[]) { expressionsLanguage.keywords = functions.map((fn) => fn.name); expressionsLanguage.deprecated = functions.filter((fn) => fn.deprecated).map((fn) => fn.name); monaco.languages.onLanguage(EXPRESSIONS_LANGUAGE_ID, () => { - monaco.languages.register({ id: EXPRESSIONS_LANGUAGE_ID }); monaco.languages.setMonarchTokensProvider(EXPRESSIONS_LANGUAGE_ID, expressionsLanguage); }); + monaco.languages.register({ id: EXPRESSIONS_LANGUAGE_ID }); } diff --git a/src/plugins/saved_search/public/services/saved_searches/get_saved_searches.test.ts b/src/plugins/saved_search/public/services/saved_searches/get_saved_searches.test.ts index f71763f6a25d3..f64d0ef659f00 100644 --- a/src/plugins/saved_search/public/services/saved_searches/get_saved_searches.test.ts +++ b/src/plugins/saved_search/public/services/saved_searches/get_saved_searches.test.ts @@ -100,6 +100,7 @@ describe('getSavedSearch', () => { expect(savedObjectsClient.resolve).toHaveBeenCalled(); expect(savedSearch).toMatchInlineSnapshot(` Object { + "breakdownField": undefined, "columns": Array [ "_source", ], @@ -198,6 +199,7 @@ describe('getSavedSearch', () => { expect(savedObjectsClient.resolve).toHaveBeenCalled(); expect(savedSearch).toMatchInlineSnapshot(` Object { + "breakdownField": undefined, "columns": Array [ "_source", ], diff --git a/src/plugins/saved_search/public/services/saved_searches/saved_searches_utils.test.ts b/src/plugins/saved_search/public/services/saved_searches/saved_searches_utils.test.ts index 9f227bc1afd04..feabac408c116 100644 --- a/src/plugins/saved_search/public/services/saved_searches/saved_searches_utils.test.ts +++ b/src/plugins/saved_search/public/services/saved_searches/saved_searches_utils.test.ts @@ -41,6 +41,7 @@ describe('saved_searches_utils', () => { ) ).toMatchInlineSnapshot(` Object { + "breakdownField": undefined, "columns": Array [ "a", "b", @@ -128,6 +129,7 @@ describe('saved_searches_utils', () => { expect(toSavedSearchAttributes(savedSearch, '{}')).toMatchInlineSnapshot(` Object { + "breakdownField": undefined, "columns": Array [ "c", "d", diff --git a/src/plugins/saved_search/public/services/saved_searches/saved_searches_utils.ts b/src/plugins/saved_search/public/services/saved_searches/saved_searches_utils.ts index 5d37355ea818d..83286f455d8c4 100644 --- a/src/plugins/saved_search/public/services/saved_searches/saved_searches_utils.ts +++ b/src/plugins/saved_search/public/services/saved_searches/saved_searches_utils.ts @@ -50,6 +50,7 @@ export const fromSavedSearchAttributes = ( timeRange: attributes.timeRange, refreshInterval: attributes.refreshInterval, rowsPerPage: attributes.rowsPerPage, + breakdownField: attributes.breakdownField, }); export const toSavedSearchAttributes = ( @@ -72,4 +73,5 @@ export const toSavedSearchAttributes = ( timeRange: savedSearch.timeRange, refreshInterval: savedSearch.refreshInterval, rowsPerPage: savedSearch.rowsPerPage, + breakdownField: savedSearch.breakdownField, }); diff --git a/src/plugins/saved_search/public/services/saved_searches/types.ts b/src/plugins/saved_search/public/services/saved_searches/types.ts index 81dbe19517798..f4e5ebecd559f 100644 --- a/src/plugins/saved_search/public/services/saved_searches/types.ts +++ b/src/plugins/saved_search/public/services/saved_searches/types.ts @@ -46,6 +46,7 @@ export interface SavedSearchAttributes { refreshInterval?: RefreshInterval; rowsPerPage?: number; + breakdownField?: string; } /** @internal **/ @@ -82,4 +83,5 @@ export interface SavedSearch { refreshInterval?: RefreshInterval; rowsPerPage?: number; + breakdownField?: string; } diff --git a/src/plugins/saved_search/server/saved_objects/search.ts b/src/plugins/saved_search/server/saved_objects/search.ts index 29130ac519334..5960cd8ebdbe9 100644 --- a/src/plugins/saved_search/server/saved_objects/search.ts +++ b/src/plugins/saved_search/server/saved_objects/search.ts @@ -68,6 +68,7 @@ export function getSavedSearchObjectType( }, }, rowsPerPage: { type: 'integer', index: false, doc_values: false }, + breakdownField: { type: 'text' }, }, }, migrations: () => getAllMigrations(getSearchSourceMigrations()), diff --git a/src/plugins/telemetry/schema/oss_plugins.json b/src/plugins/telemetry/schema/oss_plugins.json index b59177259ece3..c2b0971827d14 100644 --- a/src/plugins/telemetry/schema/oss_plugins.json +++ b/src/plugins/telemetry/schema/oss_plugins.json @@ -9125,6 +9125,12 @@ "_meta": { "description": "Non-default value of setting." } + }, + "enterpriseSearch:enableEnginesSection": { + "type": "boolean", + "_meta": { + "description": "Non-default value of setting." + } } } }, diff --git a/src/plugins/telemetry_collection_manager/server/plugin.test.ts b/src/plugins/telemetry_collection_manager/server/plugin.test.ts index 5ffe8235b3930..adfe2c64a4916 100644 --- a/src/plugins/telemetry_collection_manager/server/plugin.test.ts +++ b/src/plugins/telemetry_collection_manager/server/plugin.test.ts @@ -136,6 +136,15 @@ describe('Telemetry Collection Manager', () => { ).toBeInstanceOf(TelemetrySavedObjectsClient); }); + test('caches the promise calling `getStats` for concurrent requests', async () => { + collectionStrategy.clusterDetailsGetter.mockResolvedValue([ + { clusterUuid: 'clusterUuid' }, + ]); + collectionStrategy.statsGetter.mockResolvedValue([basicStats]); + await Promise.all([setupApi.getStats(config), setupApi.getStats(config)]); + expect(collectionStrategy.statsGetter).toHaveBeenCalledTimes(1); + }); + it('calls getStats with passed refreshCache config', async () => { const getStatsCollectionConfig: jest.SpyInstance< TelemetryCollectionManagerPlugin['getStatsCollectionConfig'] @@ -270,6 +279,15 @@ describe('Telemetry Collection Manager', () => { getStatsCollectionConfig.mockRestore(); }); + + test('does not cache the promise calling `getStats` for concurrent requests', async () => { + collectionStrategy.clusterDetailsGetter.mockResolvedValue([ + { clusterUuid: 'clusterUuid' }, + ]); + collectionStrategy.statsGetter.mockResolvedValue([basicStats]); + await Promise.all([setupApi.getStats(config), setupApi.getStats(config)]); + expect(collectionStrategy.statsGetter).toHaveBeenCalledTimes(2); + }); }); describe('getOptInStats', () => { diff --git a/src/plugins/telemetry_collection_manager/server/plugin.ts b/src/plugins/telemetry_collection_manager/server/plugin.ts index 4e7a96646b1ff..d3db80ed728de 100644 --- a/src/plugins/telemetry_collection_manager/server/plugin.ts +++ b/src/plugins/telemetry_collection_manager/server/plugin.ts @@ -340,20 +340,47 @@ export class TelemetryCollectionManagerPlugin } const cacheKey = this.createCacheKey(collectionSource, clustersDetails); - const cachedUsageStatsPayload = this.cacheManager.getFromCache(cacheKey); - if (cachedUsageStatsPayload) { - return this.updateFetchedAt(cachedUsageStatsPayload); + const cachedUsageStatsPromise = + this.cacheManager.getFromCache>(cacheKey); + if (cachedUsageStatsPromise) { + return this.updateFetchedAt(await cachedUsageStatsPromise); } + const statsFromCollectionPromise = this.getStatsFromCollection( + clustersDetails, + collection, + statsCollectionConfig + ); + this.cacheManager.setCache(cacheKey, statsFromCollectionPromise); + + try { + const stats = await statsFromCollectionPromise; + return this.updateFetchedAt(stats); + } catch (err) { + this.logger.debug( + `Failed to generate the telemetry report (${err.message}). Resetting the cache...` + ); + this.cacheManager.resetCache(); + throw err; + } + } + + private async getStatsFromCollection( + clustersDetails: ClusterDetails[], + collection: CollectionStrategy, + statsCollectionConfig: StatsCollectionConfig + ) { + const context: StatsCollectionContext = { + logger: this.logger.get(collection.title), + version: this.version, + }; + const { title: collectionSource } = collection; const now = new Date().toISOString(); const stats = await collection.statsGetter(clustersDetails, statsCollectionConfig, context); - const usageStatsPayload = stats.map((stat) => ({ + return stats.map((stat) => ({ collectionSource, cacheDetails: { updatedAt: now, fetchedAt: now }, ...stat, })); - this.cacheManager.setCache(cacheKey, usageStatsPayload); - - return this.updateFetchedAt(usageStatsPayload); } } diff --git a/src/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/handlebars.ts b/src/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/handlebars.ts index 9dbeb270a9d72..14e1832c48afb 100644 --- a/src/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/handlebars.ts +++ b/src/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/handlebars.ts @@ -7,7 +7,7 @@ */ import Handlebars from '@kbn/handlebars'; -import { encode, RisonValue } from 'rison-node'; +import { encode } from '@kbn/rison'; import dateMath from '@kbn/datemath'; import moment, { Moment } from 'moment'; import numeral from '@elastic/numeral'; @@ -44,7 +44,7 @@ function createSerializationHelper( handlebars.registerHelper('json', createSerializationHelper('json', JSON.stringify)); handlebars.registerHelper( 'rison', - createSerializationHelper('rison', (v) => encode(v as RisonValue)) + createSerializationHelper('rison', (v) => encode(v)) ); handlebars.registerHelper('date', (...args) => { diff --git a/src/plugins/unified_field_list/README.md b/src/plugins/unified_field_list/README.md index 23edffd5101dc..78a6e5084691e 100755 --- a/src/plugins/unified_field_list/README.md +++ b/src/plugins/unified_field_list/README.md @@ -81,18 +81,16 @@ const { refetchFieldsExistenceInfo, isProcessing } = useExistingFieldsFetcher({ ... }); const fieldsExistenceReader = useExistingFieldsReader() -const { fieldGroups } = useGroupedFields({ - dataViewId: currentDataViewId, - allFields, - fieldsExistenceReader, +const fieldListGroupedProps = useGroupedFields({ + dataViewId: currentDataViewId, // pass `null` here for text-based queries to skip fields existence check + allFields, // pass `null` to show loading indicators + fieldsExistenceReader, // pass `undefined` for text-based queries ... }); // and now we can render a field list diff --git a/src/plugins/unified_field_list/common/utils/field_existing_utils.ts b/src/plugins/unified_field_list/common/utils/field_existing_utils.ts index fcc08a141dae1..006568bf37f2e 100644 --- a/src/plugins/unified_field_list/common/utils/field_existing_utils.ts +++ b/src/plugins/unified_field_list/common/utils/field_existing_utils.ts @@ -132,7 +132,7 @@ export function buildFieldList(indexPattern: DataView, metaFields: string[]): Fi script: field.script, // id is a special case - it doesn't show up in the meta field list, // but as it's not part of source, it has to be handled separately. - isMeta: metaFields.includes(field.name) || field.name === '_id', + isMeta: metaFields?.includes(field.name) || field.name === '_id', runtimeField: !field.isMapped ? field.runtimeField : undefined, }; }); diff --git a/src/plugins/unified_field_list/public/components/field_list/field_list_grouped.test.tsx b/src/plugins/unified_field_list/public/components/field_list/field_list_grouped.test.tsx index 59cd7e56ff390..4d1cb45fe1936 100644 --- a/src/plugins/unified_field_list/public/components/field_list/field_list_grouped.test.tsx +++ b/src/plugins/unified_field_list/public/components/field_list/field_list_grouped.test.tsx @@ -24,7 +24,7 @@ describe('UnifiedFieldList + useGroupedFields()', () => { let defaultProps: FieldListGroupedProps; let mockedServices: GroupedFieldsParams['services']; const allFields = dataView.fields; - // 5 times more fields. Added fields will be treated as empty as they are not a part of the data view. + // 5 times more fields. Added fields will be treated as Unmapped as they are not a part of the data view. const manyFields = [...new Array(5)].flatMap((_, index) => allFields.map((field) => { return new DataViewField({ ...field.toSpec(), name: `${field.name}${index || ''}` }); @@ -44,6 +44,7 @@ describe('UnifiedFieldList + useGroupedFields()', () => { defaultProps = { fieldGroups: {}, fieldsExistenceStatus: ExistenceFetchStatus.succeeded, + scrollToTopResetCounter: 0, fieldsExistInIndex: true, screenReaderDescriptionForSearchInputId: 'testId', renderFieldItem: jest.fn(({ field, itemIndex, groupIndex }) => ( @@ -268,14 +269,14 @@ describe('UnifiedFieldList + useGroupedFields()', () => { expect( wrapper.find(`#${defaultProps.screenReaderDescriptionForSearchInputId}`).first().text() - ).toBe('25 available fields. 112 empty fields. 3 meta fields.'); + ).toBe('25 available fields. 112 unmapped fields. 0 empty fields. 3 meta fields.'); expect( wrapper.find(FieldsAccordion).map((accordion) => accordion.prop('paginatedFields').length) - ).toStrictEqual([25, 0, 0]); + ).toStrictEqual([25, 0, 0, 0]); await act(async () => { await wrapper - .find('[data-test-subj="fieldListGroupedEmptyFields"]') + .find('[data-test-subj="fieldListGroupedUnmappedFields"]') .find('button') .first() .simulate('click'); @@ -284,11 +285,11 @@ describe('UnifiedFieldList + useGroupedFields()', () => { expect( wrapper.find(FieldsAccordion).map((accordion) => accordion.prop('paginatedFields').length) - ).toStrictEqual([25, 50, 0]); + ).toStrictEqual([25, 50, 0, 0]); await act(async () => { await wrapper - .find('[data-test-subj="fieldListGroupedMetaFields"]') + .find('[data-test-subj="fieldListGroupedEmptyFields"]') .find('button') .first() .simulate('click'); @@ -297,7 +298,7 @@ describe('UnifiedFieldList + useGroupedFields()', () => { expect( wrapper.find(FieldsAccordion).map((accordion) => accordion.prop('paginatedFields').length) - ).toStrictEqual([25, 88, 0]); + ).toStrictEqual([25, 88, 0, 0]); }); it('renders correctly when filtered', async () => { @@ -315,7 +316,7 @@ describe('UnifiedFieldList + useGroupedFields()', () => { expect( wrapper.find(`#${defaultProps.screenReaderDescriptionForSearchInputId}`).first().text() - ).toBe('25 available fields. 112 empty fields. 3 meta fields.'); + ).toBe('25 available fields. 112 unmapped fields. 0 empty fields. 3 meta fields.'); await act(async () => { await wrapper.setProps({ @@ -329,7 +330,7 @@ describe('UnifiedFieldList + useGroupedFields()', () => { expect( wrapper.find(`#${defaultProps.screenReaderDescriptionForSearchInputId}`).first().text() - ).toBe('2 available fields. 8 empty fields. 0 meta fields.'); + ).toBe('2 available fields. 8 unmapped fields. 0 empty fields. 0 meta fields.'); await act(async () => { await wrapper.setProps({ @@ -343,7 +344,7 @@ describe('UnifiedFieldList + useGroupedFields()', () => { expect( wrapper.find(`#${defaultProps.screenReaderDescriptionForSearchInputId}`).first().text() - ).toBe('0 available fields. 12 empty fields. 3 meta fields.'); + ).toBe('0 available fields. 12 unmapped fields. 0 empty fields. 3 meta fields.'); }); it('renders correctly when non-supported fields are filtered out', async () => { @@ -361,7 +362,7 @@ describe('UnifiedFieldList + useGroupedFields()', () => { expect( wrapper.find(`#${defaultProps.screenReaderDescriptionForSearchInputId}`).first().text() - ).toBe('25 available fields. 112 empty fields. 3 meta fields.'); + ).toBe('25 available fields. 112 unmapped fields. 0 empty fields. 3 meta fields.'); await act(async () => { await wrapper.setProps({ @@ -375,7 +376,7 @@ describe('UnifiedFieldList + useGroupedFields()', () => { expect( wrapper.find(`#${defaultProps.screenReaderDescriptionForSearchInputId}`).first().text() - ).toBe('23 available fields. 104 empty fields. 3 meta fields.'); + ).toBe('23 available fields. 104 unmapped fields. 0 empty fields. 3 meta fields.'); }); it('renders correctly when selected fields are present', async () => { @@ -393,7 +394,7 @@ describe('UnifiedFieldList + useGroupedFields()', () => { expect( wrapper.find(`#${defaultProps.screenReaderDescriptionForSearchInputId}`).first().text() - ).toBe('25 available fields. 112 empty fields. 3 meta fields.'); + ).toBe('25 available fields. 112 unmapped fields. 0 empty fields. 3 meta fields.'); await act(async () => { await wrapper.setProps({ @@ -408,6 +409,30 @@ describe('UnifiedFieldList + useGroupedFields()', () => { expect( wrapper.find(`#${defaultProps.screenReaderDescriptionForSearchInputId}`).first().text() - ).toBe('2 selected fields. 25 available fields. 112 empty fields. 3 meta fields.'); + ).toBe( + '2 selected fields. 25 available fields. 112 unmapped fields. 0 empty fields. 3 meta fields.' + ); + }); + + it('renders correctly when popular fields limit and custom selected fields are present', async () => { + const hookParams = { + dataViewId: dataView.id!, + allFields: manyFields, + popularFieldsLimit: 10, + sortedSelectedFields: [manyFields[0], manyFields[1]], + }; + const wrapper = await mountGroupedList({ + listProps: { + ...defaultProps, + fieldsExistenceStatus: ExistenceFetchStatus.succeeded, + }, + hookParams, + }); + + expect( + wrapper.find(`#${defaultProps.screenReaderDescriptionForSearchInputId}`).first().text() + ).toBe( + '2 selected fields. 10 popular fields. 25 available fields. 112 unmapped fields. 0 empty fields. 3 meta fields.' + ); }); }); diff --git a/src/plugins/unified_field_list/public/components/field_list/field_list_grouped.tsx b/src/plugins/unified_field_list/public/components/field_list/field_list_grouped.tsx index 5510ddb2b1d43..94a76d9b6a6dc 100644 --- a/src/plugins/unified_field_list/public/components/field_list/field_list_grouped.tsx +++ b/src/plugins/unified_field_list/public/components/field_list/field_list_grouped.tsx @@ -7,14 +7,14 @@ */ import { partition, throttle } from 'lodash'; -import React, { useState, Fragment, useCallback, useMemo } from 'react'; +import React, { Fragment, useCallback, useEffect, useMemo, useState } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiScreenReaderOnly, EuiSpacer } from '@elastic/eui'; import { type DataViewField } from '@kbn/data-views-plugin/common'; import { NoFieldsCallout } from './no_fields_callout'; -import { FieldsAccordion, type FieldsAccordionProps } from './fields_accordion'; +import { FieldsAccordion, type FieldsAccordionProps, getFieldKey } from './fields_accordion'; import type { FieldListGroups, FieldListItem } from '../../types'; -import { ExistenceFetchStatus } from '../../types'; +import { ExistenceFetchStatus, FieldsGroup, FieldsGroupNames } from '../../types'; import './field_list_grouped.scss'; const PAGINATION_SIZE = 50; @@ -33,6 +33,7 @@ export interface FieldListGroupedProps { fieldsExistenceStatus: ExistenceFetchStatus; fieldsExistInIndex: boolean; renderFieldItem: FieldsAccordionProps['renderFieldItem']; + scrollToTopResetCounter: number; screenReaderDescriptionForSearchInputId?: string; 'data-test-subj'?: string; } @@ -42,6 +43,7 @@ function InnerFieldListGrouped({ fieldsExistenceStatus, fieldsExistInIndex, renderFieldItem, + scrollToTopResetCounter, screenReaderDescriptionForSearchInputId, 'data-test-subj': dataTestSubject = 'fieldListGrouped', }: FieldListGroupedProps) { @@ -60,6 +62,14 @@ function InnerFieldListGrouped({ ) ); + useEffect(() => { + // Reset the scroll if we have made material changes to the field list + if (scrollContainer && scrollToTopResetCounter) { + scrollContainer.scrollTop = 0; + setPageSize(PAGINATION_SIZE); + } + }, [scrollToTopResetCounter, scrollContainer]); + const lazyScroll = useCallback(() => { if (scrollContainer) { const nearBottom = @@ -93,9 +103,12 @@ function InnerFieldListGrouped({ ); }, [pageSize, fieldGroupsToShow, accordionState]); + const hasSpecialFields = Boolean(fieldGroupsToCollapse[0]?.[1]?.fields?.length); + return (
    { if (el && !el.dataset.dynamicScroll) { el.dataset.dynamicScroll = 'true'; @@ -114,9 +127,7 @@ function InnerFieldListGrouped({ > {hasSyncedExistingFields ? [ - fieldGroups.SelectedFields && - (!fieldGroups.SelectedFields?.hideIfEmpty || - fieldGroups.SelectedFields?.fields?.length > 0) && + shouldIncludeGroupDescriptionInAria(fieldGroups.SelectedFields) && i18n.translate( 'unifiedFieldList.fieldListGrouped.fieldSearchForSelectedFieldsLiveRegion', { @@ -127,6 +138,17 @@ function InnerFieldListGrouped({ }, } ), + shouldIncludeGroupDescriptionInAria(fieldGroups.PopularFields) && + i18n.translate( + 'unifiedFieldList.fieldListGrouped.fieldSearchForPopularFieldsLiveRegion', + { + defaultMessage: + '{popularFields} popular {popularFields, plural, one {field} other {fields}}.', + values: { + popularFields: fieldGroups.PopularFields?.fields?.length || 0, + }, + } + ), fieldGroups.AvailableFields?.fields && i18n.translate( 'unifiedFieldList.fieldListGrouped.fieldSearchForAvailableFieldsLiveRegion', @@ -138,9 +160,18 @@ function InnerFieldListGrouped({ }, } ), - fieldGroups.EmptyFields && - (!fieldGroups.EmptyFields?.hideIfEmpty || - fieldGroups.EmptyFields?.fields?.length > 0) && + shouldIncludeGroupDescriptionInAria(fieldGroups.UnmappedFields) && + i18n.translate( + 'unifiedFieldList.fieldListGrouped.fieldSearchForUnmappedFieldsLiveRegion', + { + defaultMessage: + '{unmappedFields} unmapped {unmappedFields, plural, one {field} other {fields}}.', + values: { + unmappedFields: fieldGroups.UnmappedFields?.fields?.length || 0, + }, + } + ), + shouldIncludeGroupDescriptionInAria(fieldGroups.EmptyFields) && i18n.translate( 'unifiedFieldList.fieldListGrouped.fieldSearchForEmptyFieldsLiveRegion', { @@ -151,9 +182,7 @@ function InnerFieldListGrouped({ }, } ), - fieldGroups.MetaFields && - (!fieldGroups.MetaFields?.hideIfEmpty || - fieldGroups.MetaFields?.fields?.length > 0) && + shouldIncludeGroupDescriptionInAria(fieldGroups.MetaFields) && i18n.translate( 'unifiedFieldList.fieldListGrouped.fieldSearchForMetaFieldsLiveRegion', { @@ -171,16 +200,26 @@ function InnerFieldListGrouped({
    )} -
      - {fieldGroupsToCollapse.flatMap(([, { fields }]) => - fields.map((field, index) => ( - - {renderFieldItem({ field, itemIndex: index, groupIndex: 0, hideDetails: true })} - - )) - )} -
    - + {hasSpecialFields && ( + <> +
      + {fieldGroupsToCollapse.flatMap(([key, { fields }]) => + fields.map((field, index) => ( + + {renderFieldItem({ + field, + itemIndex: index, + groupIndex: 0, + groupName: key as FieldsGroupNames, + hideDetails: true, + })} + + )) + )} +
    + + + )} {fieldGroupsToShow.map(([key, fieldGroup], index) => { const hidden = Boolean(fieldGroup.hideIfEmpty) && !fieldGroup.fields.length; if (hidden) { @@ -199,6 +238,7 @@ function InnerFieldListGrouped({ isFiltered={fieldGroup.fieldCount !== fieldGroup.fields.length} paginatedFields={paginatedFields[key]} groupIndex={index + 1} + groupName={key as FieldsGroupNames} onToggle={(open) => { setAccordionState((s) => ({ ...s, @@ -224,6 +264,7 @@ function InnerFieldListGrouped({ isAffectedByFieldFilter={fieldGroup.fieldCount !== fieldGroup.fields.length} fieldsExistInIndex={!!fieldsExistInIndex} defaultNoFieldsMessage={fieldGroup.defaultNoFieldsMessage} + data-test-subj={`${dataTestSubject}${key}NoFieldsCallout`} /> )} renderFieldItem={renderFieldItem} @@ -243,3 +284,13 @@ const FieldListGrouped = React.memo(InnerFieldListGrouped) as GenericFieldListGr // Necessary for React.lazy // eslint-disable-next-line import/no-default-export export default FieldListGrouped; + +function shouldIncludeGroupDescriptionInAria( + group: FieldsGroup | undefined +): boolean { + if (!group) { + return false; + } + // has some fields or an empty list should be still shown + return group.fields?.length > 0 || !group.hideIfEmpty; +} diff --git a/src/plugins/unified_field_list/public/components/field_list/fields_accordion.test.tsx b/src/plugins/unified_field_list/public/components/field_list/fields_accordion.test.tsx index 2804c1bbe5ee1..6c94f8a8e8335 100644 --- a/src/plugins/unified_field_list/public/components/field_list/fields_accordion.test.tsx +++ b/src/plugins/unified_field_list/public/components/field_list/fields_accordion.test.tsx @@ -11,7 +11,7 @@ import { stubLogstashDataView as dataView } from '@kbn/data-views-plugin/common/ import { EuiLoadingSpinner, EuiNotificationBadge, EuiText } from '@elastic/eui'; import { mountWithIntl } from '@kbn/test-jest-helpers'; import { FieldsAccordion, FieldsAccordionProps } from './fields_accordion'; -import { FieldListItem } from '../../types'; +import { FieldListItem, FieldsGroupNames } from '../../types'; describe('UnifiedFieldList ', () => { let defaultProps: FieldsAccordionProps; @@ -21,7 +21,8 @@ describe('UnifiedFieldList ', () => { defaultProps = { initialIsOpen: true, onToggle: jest.fn(), - groupIndex: 0, + groupIndex: 1, + groupName: FieldsGroupNames.AvailableFields, id: 'id', label: 'label-test', hasLoaded: true, diff --git a/src/plugins/unified_field_list/public/components/field_list/fields_accordion.tsx b/src/plugins/unified_field_list/public/components/field_list/fields_accordion.tsx index 5222cf1b0e678..8b7ca22bff676 100644 --- a/src/plugins/unified_field_list/public/components/field_list/fields_accordion.tsx +++ b/src/plugins/unified_field_list/public/components/field_list/fields_accordion.tsx @@ -18,7 +18,7 @@ import { } from '@elastic/eui'; import classNames from 'classnames'; import { type DataViewField } from '@kbn/data-views-plugin/common'; -import type { FieldListItem } from '../../types'; +import { type FieldListItem, FieldsGroupNames } from '../../types'; import './fields_accordion.scss'; export interface FieldsAccordionProps { @@ -32,12 +32,14 @@ export interface FieldsAccordionProps { hideDetails?: boolean; isFiltered: boolean; groupIndex: number; + groupName: FieldsGroupNames; paginatedFields: T[]; renderFieldItem: (params: { field: T; hideDetails?: boolean; itemIndex: number; groupIndex: number; + groupName: FieldsGroupNames; }) => JSX.Element; renderCallout: () => JSX.Element; showExistenceFetchError?: boolean; @@ -55,6 +57,7 @@ function InnerFieldsAccordion({ hideDetails, isFiltered, groupIndex, + groupName, paginatedFields, renderFieldItem, renderCallout, @@ -99,6 +102,9 @@ function InnerFieldsAccordion({ content={i18n.translate('unifiedFieldList.fieldsAccordion.existenceErrorLabel', { defaultMessage: "Field information can't be loaded", })} + iconProps={{ + 'data-test-subj': `${id}-fetchWarning`, + }} /> ); } @@ -128,7 +134,7 @@ function InnerFieldsAccordion({ ); } - return ; + return ; }, [showExistenceFetchError, showExistenceFetchTimeout, hasLoaded, isFiltered, id, fieldsCount]); return ( @@ -146,8 +152,8 @@ function InnerFieldsAccordion({
      {paginatedFields && paginatedFields.map((field, index) => ( - - {renderFieldItem({ field, itemIndex: index, groupIndex, hideDetails })} + + {renderFieldItem({ field, itemIndex: index, groupIndex, groupName, hideDetails })} ))}
    @@ -159,3 +165,6 @@ function InnerFieldsAccordion({ } export const FieldsAccordion = React.memo(InnerFieldsAccordion) as typeof InnerFieldsAccordion; + +export const getFieldKey = (field: FieldListItem): string => + `${field.name}-${field.displayName}-${field.type}`; diff --git a/src/plugins/unified_field_list/public/components/field_list/no_fields_callout.test.tsx b/src/plugins/unified_field_list/public/components/field_list/no_fields_callout.test.tsx index 03936a89877ba..5a18a261d136d 100644 --- a/src/plugins/unified_field_list/public/components/field_list/no_fields_callout.test.tsx +++ b/src/plugins/unified_field_list/public/components/field_list/no_fields_callout.test.tsx @@ -16,6 +16,7 @@ describe('UnifiedFieldList ', () => { expect(component).toMatchInlineSnapshot(` @@ -26,6 +27,7 @@ describe('UnifiedFieldList ', () => { expect(component).toMatchInlineSnapshot(` @@ -38,6 +40,7 @@ describe('UnifiedFieldList ', () => { expect(component).toMatchInlineSnapshot(` @@ -51,6 +54,7 @@ describe('UnifiedFieldList ', () => { expect(component).toMatchInlineSnapshot(` @@ -78,6 +82,7 @@ describe('UnifiedFieldList ', () => { expect(component).toMatchInlineSnapshot(` @@ -108,6 +113,7 @@ describe('UnifiedFieldList ', () => { expect(component).toMatchInlineSnapshot(` @@ -139,6 +145,7 @@ describe('UnifiedFieldList ', () => { expect(component).toMatchInlineSnapshot(` diff --git a/src/plugins/unified_field_list/public/components/field_list/no_fields_callout.tsx b/src/plugins/unified_field_list/public/components/field_list/no_fields_callout.tsx index 3d24b400da3cb..3eca7573d9110 100644 --- a/src/plugins/unified_field_list/public/components/field_list/no_fields_callout.tsx +++ b/src/plugins/unified_field_list/public/components/field_list/no_fields_callout.tsx @@ -23,12 +23,14 @@ export const NoFieldsCallout = ({ isAffectedByFieldFilter = false, isAffectedByTimerange = false, isAffectedByGlobalFilter = false, + 'data-test-subj': dataTestSubject = 'noFieldsCallout', }: { fieldsExistInIndex: boolean; isAffectedByFieldFilter?: boolean; defaultNoFieldsMessage?: string; isAffectedByTimerange?: boolean; isAffectedByGlobalFilter?: boolean; + 'data-test-subj'?: string; }) => { if (!fieldsExistInIndex) { return ( @@ -38,6 +40,7 @@ export const NoFieldsCallout = ({ title={i18n.translate('unifiedFieldList.fieldList.noFieldsCallout.noFieldsLabel', { defaultMessage: 'No fields exist in this data view.', })} + data-test-subj={`${dataTestSubject}-noFieldsExist`} /> ); } @@ -53,6 +56,7 @@ export const NoFieldsCallout = ({ }) : defaultNoFieldsMessage } + data-test-subj={`${dataTestSubject}-noFieldsMatch`} > {(isAffectedByTimerange || isAffectedByFieldFilter || isAffectedByGlobalFilter) && ( <> diff --git a/src/plugins/unified_field_list/public/components/field_stats/field_stats.test.tsx b/src/plugins/unified_field_list/public/components/field_stats/field_stats.test.tsx index 312abc2bb323f..317fe9082d28e 100644 --- a/src/plugins/unified_field_list/public/components/field_stats/field_stats.test.tsx +++ b/src/plugins/unified_field_list/public/components/field_stats/field_stats.test.tsx @@ -8,6 +8,7 @@ import React from 'react'; import { act } from 'react-dom/test-utils'; +import { ReactWrapper } from 'enzyme'; import { EuiLoadingSpinner, EuiProgress } from '@elastic/eui'; import { coreMock } from '@kbn/core/public/mocks'; import { mountWithIntl } from '@kbn/test-jest-helpers'; @@ -120,6 +121,18 @@ describe('UnifiedFieldList ', () => { }); }); + async function mountComponent(component: React.ReactElement): Promise { + let wrapper: ReactWrapper; + await act(async () => { + wrapper = await mountWithIntl(component); + // wait for lazy modules if any + await new Promise((resolve) => setTimeout(resolve, 0)); + await wrapper.update(); + }); + + return wrapper!; + } + beforeEach(() => { (loadFieldStats as jest.Mock).mockReset(); (loadFieldStats as jest.Mock).mockImplementation(() => Promise.resolve({})); @@ -134,7 +147,7 @@ describe('UnifiedFieldList ', () => { }); }); - const wrapper = mountWithIntl( + const wrapper = await mountComponent( ', () => { /> ); - await wrapper.update(); - expect(loadFieldStats).toHaveBeenCalledWith({ abortController: new AbortController(), services: { data: mockedServices.data }, @@ -260,33 +271,27 @@ describe('UnifiedFieldList ', () => { }); it('should not request field stats for range fields', async () => { - const wrapper = await mountWithIntl( + const wrapper = await mountComponent( f.name === 'ip_range')!} /> ); - await wrapper.update(); - expect(loadFieldStats).toHaveBeenCalled(); expect(wrapper.text()).toBe('Analysis is not available for this field.'); }); it('should not request field stats for geo fields', async () => { - const wrapper = await mountWithIntl( + const wrapper = await mountComponent( f.name === 'geo_shape')!} /> ); - await wrapper.update(); - expect(loadFieldStats).toHaveBeenCalled(); expect(wrapper.text()).toBe('Analysis is not available for this field.'); }); it('should render a message if no data is found', async () => { - const wrapper = await mountWithIntl(); - - await wrapper.update(); + const wrapper = await mountComponent(); expect(loadFieldStats).toHaveBeenCalled(); @@ -302,9 +307,7 @@ describe('UnifiedFieldList ', () => { }); }); - const wrapper = mountWithIntl(); - - await wrapper.update(); + const wrapper = await mountComponent(); await act(async () => { resolveFunction!({ @@ -330,7 +333,7 @@ describe('UnifiedFieldList ', () => { }); }); - const wrapper = mountWithIntl( + const wrapper = await mountComponent( ', () => { /> ); - await wrapper.update(); - expect(loadFieldStats).toHaveBeenCalledWith({ abortController: new AbortController(), services: { data: mockedServices.data }, @@ -433,7 +434,7 @@ describe('UnifiedFieldList ', () => { }); }); - const wrapper = mountWithIntl( + const wrapper = await mountComponent( ', () => { /> ); - await wrapper.update(); - expect(wrapper.find(EuiLoadingSpinner)).toHaveLength(1); await act(async () => { @@ -507,7 +506,7 @@ describe('UnifiedFieldList ', () => { }); }); - const wrapper = mountWithIntl( + const wrapper = await mountComponent( ', () => { /> ); - await wrapper.update(); - expect(loadFieldStats).toHaveBeenCalledWith({ abortController: new AbortController(), services: { data: mockedServices.data }, @@ -615,7 +612,7 @@ describe('UnifiedFieldList ', () => { const field = dataView.fields.find((f) => f.name === 'machine.ram')!; - const wrapper = mountWithIntl( + const wrapper = await mountComponent( ', () => { /> ); - await wrapper.update(); - expect(loadFieldStats).toHaveBeenCalledWith({ abortController: new AbortController(), services: { data: mockedServices.data }, diff --git a/src/plugins/unified_field_list/public/components/field_stats/field_stats.tsx b/src/plugins/unified_field_list/public/components/field_stats/field_stats.tsx index eafdcc0dab69a..99c31fc9f64e1 100755 --- a/src/plugins/unified_field_list/public/components/field_stats/field_stats.tsx +++ b/src/plugins/unified_field_list/public/components/field_stats/field_stats.tsx @@ -378,33 +378,36 @@ const FieldStatsComponent: React.FC = ({ if (histogram && histogram.buckets.length && topValues && topValues.buckets.length) { title = ( - { - setShowingHistogram(optionId === 'histogram'); - }} - idSelected={showingHistogram ? 'histogram' : 'topValues'} - /> + <> + { + setShowingHistogram(optionId === 'histogram'); + }} + idSelected={showingHistogram ? 'histogram' : 'topValues'} + /> + + ); } else if (field.type === 'date') { title = ( diff --git a/src/plugins/unified_field_list/public/hooks/__snapshots__/use_grouped_fields.test.tsx.snap b/src/plugins/unified_field_list/public/hooks/__snapshots__/use_grouped_fields.test.tsx.snap new file mode 100644 index 0000000000000..d3c95c363695d --- /dev/null +++ b/src/plugins/unified_field_list/public/hooks/__snapshots__/use_grouped_fields.test.tsx.snap @@ -0,0 +1,259 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`UnifiedFieldList useGroupedFields() should work correctly for no data 1`] = ` +Object { + "AvailableFields": Object { + "defaultNoFieldsMessage": "There are no available fields that contain data.", + "fieldCount": 0, + "fields": Array [], + "hideDetails": false, + "isAffectedByGlobalFilter": false, + "isAffectedByTimeFilter": true, + "isInitiallyOpen": true, + "showInAccordion": true, + "title": "Available fields", + }, + "EmptyFields": Object { + "defaultNoFieldsMessage": "There are no empty fields.", + "fieldCount": 0, + "fields": Array [], + "helpText": "Fields that don't have any values based on your filters.", + "hideDetails": false, + "hideIfEmpty": false, + "isAffectedByGlobalFilter": false, + "isAffectedByTimeFilter": false, + "isInitiallyOpen": false, + "showInAccordion": true, + "title": "Empty fields", + }, + "MetaFields": Object { + "defaultNoFieldsMessage": "There are no meta fields.", + "fieldCount": 0, + "fields": Array [], + "hideDetails": false, + "hideIfEmpty": false, + "isAffectedByGlobalFilter": false, + "isAffectedByTimeFilter": false, + "isInitiallyOpen": false, + "showInAccordion": true, + "title": "Meta fields", + }, + "PopularFields": Object { + "fieldCount": 0, + "fields": Array [], + "helpText": "Fields that your organization frequently uses, from most to least popular.", + "hideDetails": false, + "hideIfEmpty": true, + "isAffectedByGlobalFilter": false, + "isAffectedByTimeFilter": true, + "isInitiallyOpen": true, + "showInAccordion": true, + "title": "Popular fields", + }, + "SelectedFields": Object { + "fieldCount": 0, + "fields": Array [], + "hideDetails": false, + "hideIfEmpty": true, + "isAffectedByGlobalFilter": false, + "isAffectedByTimeFilter": true, + "isInitiallyOpen": true, + "showInAccordion": true, + "title": "Selected fields", + }, + "SpecialFields": Object { + "fieldCount": 0, + "fields": Array [], + "hideDetails": true, + "isAffectedByGlobalFilter": false, + "isAffectedByTimeFilter": false, + "isInitiallyOpen": false, + "showInAccordion": false, + "title": "", + }, + "UnmappedFields": Object { + "fieldCount": 0, + "fields": Array [], + "helpText": "Fields that aren't explicitly mapped to a field data type.", + "hideDetails": false, + "hideIfEmpty": true, + "isAffectedByGlobalFilter": false, + "isAffectedByTimeFilter": true, + "isInitiallyOpen": false, + "showInAccordion": true, + "title": "Unmapped fields", + }, +} +`; + +exports[`UnifiedFieldList useGroupedFields() should work correctly in loading state 1`] = ` +Object { + "AvailableFields": Object { + "defaultNoFieldsMessage": "There are no available fields that contain data.", + "fieldCount": 0, + "fields": Array [], + "hideDetails": false, + "isAffectedByGlobalFilter": false, + "isAffectedByTimeFilter": true, + "isInitiallyOpen": true, + "showInAccordion": true, + "title": "Available fields", + }, + "EmptyFields": Object { + "defaultNoFieldsMessage": "There are no empty fields.", + "fieldCount": 0, + "fields": Array [], + "helpText": "Fields that don't have any values based on your filters.", + "hideDetails": false, + "hideIfEmpty": false, + "isAffectedByGlobalFilter": false, + "isAffectedByTimeFilter": false, + "isInitiallyOpen": false, + "showInAccordion": true, + "title": "Empty fields", + }, + "MetaFields": Object { + "defaultNoFieldsMessage": "There are no meta fields.", + "fieldCount": 0, + "fields": Array [], + "hideDetails": false, + "hideIfEmpty": false, + "isAffectedByGlobalFilter": false, + "isAffectedByTimeFilter": false, + "isInitiallyOpen": false, + "showInAccordion": true, + "title": "Meta fields", + }, + "PopularFields": Object { + "fieldCount": 0, + "fields": Array [], + "helpText": "Fields that your organization frequently uses, from most to least popular.", + "hideDetails": false, + "hideIfEmpty": true, + "isAffectedByGlobalFilter": false, + "isAffectedByTimeFilter": true, + "isInitiallyOpen": true, + "showInAccordion": true, + "title": "Popular fields", + }, + "SelectedFields": Object { + "fieldCount": 0, + "fields": Array [], + "hideDetails": false, + "hideIfEmpty": true, + "isAffectedByGlobalFilter": false, + "isAffectedByTimeFilter": true, + "isInitiallyOpen": true, + "showInAccordion": true, + "title": "Selected fields", + }, + "SpecialFields": Object { + "fieldCount": 0, + "fields": Array [], + "hideDetails": true, + "isAffectedByGlobalFilter": false, + "isAffectedByTimeFilter": false, + "isInitiallyOpen": false, + "showInAccordion": false, + "title": "", + }, + "UnmappedFields": Object { + "fieldCount": 0, + "fields": Array [], + "helpText": "Fields that aren't explicitly mapped to a field data type.", + "hideDetails": false, + "hideIfEmpty": true, + "isAffectedByGlobalFilter": false, + "isAffectedByTimeFilter": true, + "isInitiallyOpen": false, + "showInAccordion": true, + "title": "Unmapped fields", + }, +} +`; + +exports[`UnifiedFieldList useGroupedFields() should work correctly when global filters are set 1`] = ` +Object { + "AvailableFields": Object { + "defaultNoFieldsMessage": "There are no available fields that contain data.", + "fieldCount": 0, + "fields": Array [], + "hideDetails": false, + "isAffectedByGlobalFilter": true, + "isAffectedByTimeFilter": true, + "isInitiallyOpen": true, + "showInAccordion": true, + "title": "Available fields", + }, + "EmptyFields": Object { + "defaultNoFieldsMessage": "There are no empty fields.", + "fieldCount": 0, + "fields": Array [], + "helpText": "Fields that don't have any values based on your filters.", + "hideDetails": false, + "hideIfEmpty": false, + "isAffectedByGlobalFilter": false, + "isAffectedByTimeFilter": false, + "isInitiallyOpen": false, + "showInAccordion": true, + "title": "Empty fields", + }, + "MetaFields": Object { + "defaultNoFieldsMessage": "There are no meta fields.", + "fieldCount": 0, + "fields": Array [], + "hideDetails": false, + "hideIfEmpty": false, + "isAffectedByGlobalFilter": false, + "isAffectedByTimeFilter": false, + "isInitiallyOpen": false, + "showInAccordion": true, + "title": "Meta fields", + }, + "PopularFields": Object { + "fieldCount": 0, + "fields": Array [], + "helpText": "Fields that your organization frequently uses, from most to least popular.", + "hideDetails": false, + "hideIfEmpty": true, + "isAffectedByGlobalFilter": true, + "isAffectedByTimeFilter": true, + "isInitiallyOpen": true, + "showInAccordion": true, + "title": "Popular fields", + }, + "SelectedFields": Object { + "fieldCount": 0, + "fields": Array [], + "hideDetails": false, + "hideIfEmpty": true, + "isAffectedByGlobalFilter": true, + "isAffectedByTimeFilter": true, + "isInitiallyOpen": true, + "showInAccordion": true, + "title": "Selected fields", + }, + "SpecialFields": Object { + "fieldCount": 0, + "fields": Array [], + "hideDetails": true, + "isAffectedByGlobalFilter": false, + "isAffectedByTimeFilter": false, + "isInitiallyOpen": false, + "showInAccordion": false, + "title": "", + }, + "UnmappedFields": Object { + "fieldCount": 0, + "fields": Array [], + "helpText": "Fields that aren't explicitly mapped to a field data type.", + "hideDetails": false, + "hideIfEmpty": true, + "isAffectedByGlobalFilter": true, + "isAffectedByTimeFilter": true, + "isInitiallyOpen": false, + "showInAccordion": true, + "title": "Unmapped fields", + }, +} +`; diff --git a/src/plugins/unified_field_list/public/hooks/use_existing_fields.ts b/src/plugins/unified_field_list/public/hooks/use_existing_fields.ts index ebf12d4609500..583ca32ce7508 100644 --- a/src/plugins/unified_field_list/public/hooks/use_existing_fields.ts +++ b/src/plugins/unified_field_list/public/hooks/use_existing_fields.ts @@ -15,7 +15,6 @@ import { DataPublicPluginStart, DataViewsContract, getEsQueryConfig, - UI_SETTINGS, } from '@kbn/data-plugin/public'; import { type DataView } from '@kbn/data-plugin/common'; import { loadFieldExisting } from '../services/field_existing'; @@ -32,11 +31,12 @@ export interface ExistingFieldsInfo { } export interface ExistingFieldsFetcherParams { + disableAutoFetching?: boolean; dataViews: DataView[]; - fromDate: string; - toDate: string; - query: Query | AggregateQuery; - filters: Filter[]; + fromDate: string | undefined; // fetching will be skipped if `undefined` + toDate: string | undefined; + query: Query | AggregateQuery | undefined; + filters: Filter[] | undefined; services: { core: Pick; data: DataPublicPluginStart; @@ -89,7 +89,7 @@ export const useExistingFieldsFetcher = ( dataViewId: string | undefined; fetchId: string; }): Promise => { - if (!dataViewId) { + if (!dataViewId || !query || !fromDate || !toDate) { return; } @@ -123,7 +123,7 @@ export const useExistingFieldsFetcher = ( dslQuery: await buildSafeEsQuery( dataView, query, - filters, + filters || [], getEsQueryConfig(core.uiSettings) ), fromDate, @@ -137,11 +137,11 @@ export const useExistingFieldsFetcher = ( const existingFieldNames = result?.existingFieldNames || []; - const metaFields = core.uiSettings.get(UI_SETTINGS.META_FIELDS) || []; if ( - !existingFieldNames.filter((fieldName) => !metaFields.includes?.(fieldName)).length && + onNoData && numberOfFetches === 1 && - onNoData + !existingFieldNames.filter((fieldName) => !dataView?.metaFields?.includes(fieldName)) + .length ) { onNoData(dataViewId); } @@ -173,12 +173,17 @@ export const useExistingFieldsFetcher = ( async (dataViewId?: string) => { const fetchId = generateId(); lastFetchId = fetchId; + + const options = { + fetchId, + dataViewId, + ...params, + }; // refetch only for the specified data view if (dataViewId) { await fetchFieldsExistenceInfo({ - fetchId, + ...options, dataViewId, - ...params, }); return; } @@ -186,9 +191,8 @@ export const useExistingFieldsFetcher = ( await Promise.all( params.dataViews.map((dataView) => fetchFieldsExistenceInfo({ - fetchId, + ...options, dataViewId: dataView.id, - ...params, }) ) ); @@ -205,8 +209,10 @@ export const useExistingFieldsFetcher = ( ); useEffect(() => { - refetchFieldsExistenceInfo(); - }, [refetchFieldsExistenceInfo]); + if (!params.disableAutoFetching) { + refetchFieldsExistenceInfo(); + } + }, [refetchFieldsExistenceInfo, params.disableAutoFetching]); useEffect(() => { return () => { diff --git a/src/plugins/unified_field_list/public/hooks/use_grouped_fields.test.tsx b/src/plugins/unified_field_list/public/hooks/use_grouped_fields.test.tsx index d4d6d3cdc906f..df4b3f684647f 100644 --- a/src/plugins/unified_field_list/public/hooks/use_grouped_fields.test.tsx +++ b/src/plugins/unified_field_list/public/hooks/use_grouped_fields.test.tsx @@ -20,6 +20,12 @@ import { ExistenceFetchStatus, FieldListGroups, FieldsGroupNames } from '../type describe('UnifiedFieldList useGroupedFields()', () => { let mockedServices: GroupedFieldsParams['services']; const allFields = dataView.fields; + // Added fields will be treated as Unmapped as they are not a part of the data view. + const allFieldsIncludingUnmapped = [...new Array(2)].flatMap((_, index) => + allFields.map((field) => { + return new DataViewField({ ...field.toSpec(), name: `${field.name}${index || ''}` }); + }) + ); const anotherDataView = createStubDataView({ spec: { id: 'another-data-view', @@ -39,14 +45,43 @@ describe('UnifiedFieldList useGroupedFields()', () => { }); }); + it('should work correctly in loading state', async () => { + const props: GroupedFieldsParams = { + dataViewId: dataView.id!, + allFields: null, + services: mockedServices, + }; + const { result, waitForNextUpdate, rerender } = renderHook(useGroupedFields, { + initialProps: props, + }); + + await waitForNextUpdate(); + + expect(result.current.fieldGroups).toMatchSnapshot(); + expect(result.current.fieldsExistenceStatus).toBe(ExistenceFetchStatus.unknown); + expect(result.current.fieldsExistInIndex).toBe(false); + expect(result.current.scrollToTopResetCounter).toBeTruthy(); + + rerender({ + ...props, + dataViewId: null, // for text-based queries + allFields: null, + }); + + expect(result.current.fieldsExistenceStatus).toBe(ExistenceFetchStatus.unknown); + expect(result.current.fieldsExistInIndex).toBe(true); + expect(result.current.scrollToTopResetCounter).toBeTruthy(); + }); + it('should work correctly for no data', async () => { - const { result, waitForNextUpdate } = renderHook(() => - useGroupedFields({ - dataViewId: dataView.id!, - allFields: [], - services: mockedServices, - }) - ); + const props: GroupedFieldsParams = { + dataViewId: dataView.id!, + allFields: [], + services: mockedServices, + }; + const { result, waitForNextUpdate, rerender } = renderHook(useGroupedFields, { + initialProps: props, + }); await waitForNextUpdate(); @@ -59,20 +94,36 @@ describe('UnifiedFieldList useGroupedFields()', () => { ).toStrictEqual([ 'SpecialFields-0', 'SelectedFields-0', + 'PopularFields-0', 'AvailableFields-0', + 'UnmappedFields-0', 'EmptyFields-0', 'MetaFields-0', ]); + + expect(fieldGroups).toMatchSnapshot(); + expect(result.current.fieldsExistenceStatus).toBe(ExistenceFetchStatus.succeeded); + expect(result.current.fieldsExistInIndex).toBe(false); + + rerender({ + ...props, + dataViewId: null, // for text-based queries + allFields: [], + }); + + expect(result.current.fieldsExistenceStatus).toBe(ExistenceFetchStatus.succeeded); + expect(result.current.fieldsExistInIndex).toBe(true); }); it('should work correctly with fields', async () => { - const { result, waitForNextUpdate } = renderHook(() => - useGroupedFields({ - dataViewId: dataView.id!, - allFields, - services: mockedServices, - }) - ); + const props: GroupedFieldsParams = { + dataViewId: dataView.id!, + allFields, + services: mockedServices, + }; + const { result, waitForNextUpdate, rerender } = renderHook(useGroupedFields, { + initialProps: props, + }); await waitForNextUpdate(); @@ -85,48 +136,116 @@ describe('UnifiedFieldList useGroupedFields()', () => { ).toStrictEqual([ 'SpecialFields-0', 'SelectedFields-0', + 'PopularFields-0', 'AvailableFields-25', + 'UnmappedFields-0', 'EmptyFields-0', 'MetaFields-3', ]); + + expect(result.current.fieldsExistenceStatus).toBe(ExistenceFetchStatus.succeeded); + expect(result.current.fieldsExistInIndex).toBe(true); + + rerender({ + ...props, + dataViewId: null, // for text-based queries + allFields, + }); + + expect(result.current.fieldsExistenceStatus).toBe(ExistenceFetchStatus.succeeded); + expect(result.current.fieldsExistInIndex).toBe(true); }); it('should work correctly when filtered', async () => { - const { result, waitForNextUpdate } = renderHook(() => - useGroupedFields({ - dataViewId: dataView.id!, - allFields, - services: mockedServices, - onFilterField: (field: DataViewField) => field.name.startsWith('@'), - }) - ); + const props: GroupedFieldsParams = { + dataViewId: dataView.id!, + allFields: allFieldsIncludingUnmapped, + services: mockedServices, + }; + const { result, waitForNextUpdate, rerender } = renderHook(useGroupedFields, { + initialProps: props, + }); await waitForNextUpdate(); - const fieldGroups = result.current.fieldGroups; + let fieldGroups = result.current.fieldGroups; + const scrollToTopResetCounter1 = result.current.scrollToTopResetCounter; expect( Object.keys(fieldGroups!).map( - (key) => `${key}-${fieldGroups![key as FieldsGroupNames]?.fields.length}` + (key) => + `${key}-${fieldGroups![key as FieldsGroupNames]?.fields.length}-${ + fieldGroups![key as FieldsGroupNames]?.fieldCount + }` ) ).toStrictEqual([ - 'SpecialFields-0', - 'SelectedFields-0', - 'AvailableFields-2', - 'EmptyFields-0', - 'MetaFields-0', + 'SpecialFields-0-0', + 'SelectedFields-0-0', + 'PopularFields-0-0', + 'AvailableFields-25-25', + 'UnmappedFields-28-28', + 'EmptyFields-0-0', + 'MetaFields-3-3', + ]); + + rerender({ + ...props, + onFilterField: (field: DataViewField) => field.name.startsWith('@'), + }); + + fieldGroups = result.current.fieldGroups; + + expect( + Object.keys(fieldGroups!).map( + (key) => + `${key}-${fieldGroups![key as FieldsGroupNames]?.fields.length}-${ + fieldGroups![key as FieldsGroupNames]?.fieldCount + }` + ) + ).toStrictEqual([ + 'SpecialFields-0-0', + 'SelectedFields-0-0', + 'PopularFields-0-0', + 'AvailableFields-2-25', + 'UnmappedFields-2-28', + 'EmptyFields-0-0', + 'MetaFields-0-3', ]); + + expect(result.current.scrollToTopResetCounter).not.toBe(scrollToTopResetCounter1); + }); + + it('should not change the scroll position if fields list is extended', async () => { + const props: GroupedFieldsParams = { + dataViewId: dataView.id!, + allFields, + services: mockedServices, + }; + const { result, waitForNextUpdate, rerender } = renderHook(useGroupedFields, { + initialProps: props, + }); + + await waitForNextUpdate(); + + const scrollToTopResetCounter1 = result.current.scrollToTopResetCounter; + + rerender({ + ...props, + allFields: allFieldsIncludingUnmapped, + }); + + expect(result.current.scrollToTopResetCounter).toBe(scrollToTopResetCounter1); }); it('should work correctly when custom unsupported fields are skipped', async () => { - const { result, waitForNextUpdate } = renderHook(() => - useGroupedFields({ + const { result, waitForNextUpdate } = renderHook(useGroupedFields, { + initialProps: { dataViewId: dataView.id!, allFields, services: mockedServices, onSupportedFieldFilter: (field: DataViewField) => field.aggregatable, - }) - ); + }, + }); await waitForNextUpdate(); @@ -139,22 +258,24 @@ describe('UnifiedFieldList useGroupedFields()', () => { ).toStrictEqual([ 'SpecialFields-0', 'SelectedFields-0', + 'PopularFields-0', 'AvailableFields-23', + 'UnmappedFields-0', 'EmptyFields-0', 'MetaFields-3', ]); }); it('should work correctly when selected fields are present', async () => { - const { result, waitForNextUpdate } = renderHook(() => - useGroupedFields({ + const { result, waitForNextUpdate } = renderHook(useGroupedFields, { + initialProps: { dataViewId: dataView.id!, allFields, services: mockedServices, onSelectedFieldFilter: (field: DataViewField) => ['bytes', 'extension', '_id', '@timestamp'].includes(field.name), - }) - ); + }, + }); await waitForNextUpdate(); @@ -167,20 +288,22 @@ describe('UnifiedFieldList useGroupedFields()', () => { ).toStrictEqual([ 'SpecialFields-0', 'SelectedFields-4', + 'PopularFields-0', 'AvailableFields-25', + 'UnmappedFields-0', 'EmptyFields-0', 'MetaFields-3', ]); }); it('should work correctly for text-based queries (no data view)', async () => { - const { result } = renderHook(() => - useGroupedFields({ + const { result } = renderHook(useGroupedFields, { + initialProps: { dataViewId: null, - allFields, + allFields: allFieldsIncludingUnmapped, services: mockedServices, - }) - ); + }, + }); const fieldGroups = result.current.fieldGroups; @@ -188,24 +311,36 @@ describe('UnifiedFieldList useGroupedFields()', () => { Object.keys(fieldGroups!).map( (key) => `${key}-${fieldGroups![key as FieldsGroupNames]?.fields.length}` ) - ).toStrictEqual(['SpecialFields-0', 'SelectedFields-0', 'AvailableFields-28', 'MetaFields-0']); + ).toStrictEqual([ + 'SpecialFields-0', + 'SelectedFields-0', + 'PopularFields-0', + 'AvailableFields-56', // even unmapped fields fall into Available + 'UnmappedFields-0', + 'MetaFields-0', + ]); + + expect(result.current.fieldsExistenceStatus).toBe(ExistenceFetchStatus.succeeded); + expect(result.current.fieldsExistInIndex).toBe(true); }); it('should work correctly when details are overwritten', async () => { - const { result, waitForNextUpdate } = renderHook(() => - useGroupedFields({ + const onOverrideFieldGroupDetails: GroupedFieldsParams['onOverrideFieldGroupDetails'] = + jest.fn((groupName) => { + if (groupName === FieldsGroupNames.SelectedFields) { + return { + helpText: 'test', + }; + } + }); + const { result, waitForNextUpdate } = renderHook(useGroupedFields, { + initialProps: { dataViewId: dataView.id!, allFields, services: mockedServices, - onOverrideFieldGroupDetails: (groupName) => { - if (groupName === FieldsGroupNames.SelectedFields) { - return { - helpText: 'test', - }; - } - }, - }) - ); + onOverrideFieldGroupDetails, + }, + }); await waitForNextUpdate(); @@ -213,6 +348,7 @@ describe('UnifiedFieldList useGroupedFields()', () => { expect(fieldGroups[FieldsGroupNames.SelectedFields]?.helpText).toBe('test'); expect(fieldGroups[FieldsGroupNames.AvailableFields]?.helpText).not.toBe('test'); + expect(onOverrideFieldGroupDetails).toHaveBeenCalled(); }); it('should work correctly when changing a data view and existence info is available only for one of them', async () => { @@ -248,11 +384,16 @@ describe('UnifiedFieldList useGroupedFields()', () => { ).toStrictEqual([ 'SpecialFields-0', 'SelectedFields-0', + 'PopularFields-0', 'AvailableFields-2', + 'UnmappedFields-0', 'EmptyFields-23', 'MetaFields-3', ]); + expect(result.current.fieldsExistenceStatus).toBe(ExistenceFetchStatus.succeeded); + expect(result.current.fieldsExistInIndex).toBe(true); + rerender({ ...props, dataViewId: anotherDataView.id!, @@ -267,6 +408,133 @@ describe('UnifiedFieldList useGroupedFields()', () => { Object.keys(fieldGroups!).map( (key) => `${key}-${fieldGroups![key as FieldsGroupNames]?.fields.length}` ) - ).toStrictEqual(['SpecialFields-0', 'SelectedFields-0', 'AvailableFields-8', 'MetaFields-0']); + ).toStrictEqual([ + 'SpecialFields-0', + 'SelectedFields-0', + 'PopularFields-0', + 'AvailableFields-8', + 'UnmappedFields-0', + 'MetaFields-0', + ]); + + expect(result.current.fieldsExistenceStatus).toBe(ExistenceFetchStatus.unknown); + expect(result.current.fieldsExistInIndex).toBe(true); + }); + + it('should work correctly when popular fields limit is present', async () => { + // `bytes` is popular, but we are skipping it here to test that it would not be shown under Popular and Available + const onSupportedFieldFilter = jest.fn((field) => field.name !== 'bytes'); + + const { result, waitForNextUpdate } = renderHook(useGroupedFields, { + initialProps: { + dataViewId: dataView.id!, + allFields, + popularFieldsLimit: 10, + services: mockedServices, + onSupportedFieldFilter, + }, + }); + + await waitForNextUpdate(); + + const fieldGroups = result.current.fieldGroups; + + expect( + Object.keys(fieldGroups!).map( + (key) => `${key}-${fieldGroups![key as FieldsGroupNames]?.fields.length}` + ) + ).toStrictEqual([ + 'SpecialFields-0', + 'SelectedFields-0', + 'PopularFields-3', + 'AvailableFields-24', + 'UnmappedFields-0', + 'EmptyFields-0', + 'MetaFields-3', + ]); + + expect(fieldGroups.PopularFields?.fields.map((field) => field.name).join(',')).toBe( + '@timestamp,time,ssl' + ); + }); + + it('should work correctly when global filters are set', async () => { + const { result, waitForNextUpdate } = renderHook(useGroupedFields, { + initialProps: { + dataViewId: dataView.id!, + allFields: [], + isAffectedByGlobalFilter: true, + services: mockedServices, + }, + }); + + await waitForNextUpdate(); + + const fieldGroups = result.current.fieldGroups; + expect(fieldGroups).toMatchSnapshot(); + }); + + it('should work correctly and show unmapped fields separately', async () => { + const { result, waitForNextUpdate } = renderHook(useGroupedFields, { + initialProps: { + dataViewId: dataView.id!, + allFields: allFieldsIncludingUnmapped, + services: mockedServices, + }, + }); + + await waitForNextUpdate(); + + const fieldGroups = result.current.fieldGroups; + + expect( + Object.keys(fieldGroups!).map( + (key) => `${key}-${fieldGroups![key as FieldsGroupNames]?.fields.length}` + ) + ).toStrictEqual([ + 'SpecialFields-0', + 'SelectedFields-0', + 'PopularFields-0', + 'AvailableFields-25', + 'UnmappedFields-28', + 'EmptyFields-0', + 'MetaFields-3', + ]); + }); + + it('should work correctly when custom selected fields are provided', async () => { + const customSortedFields = [ + allFieldsIncludingUnmapped[allFieldsIncludingUnmapped.length - 1], + allFields[2], + allFields[0], + ]; + const { result, waitForNextUpdate } = renderHook(useGroupedFields, { + initialProps: { + dataViewId: dataView.id!, + allFields, + sortedSelectedFields: customSortedFields, + services: mockedServices, + }, + }); + + await waitForNextUpdate(); + + const fieldGroups = result.current.fieldGroups; + + expect( + Object.keys(fieldGroups!).map( + (key) => `${key}-${fieldGroups![key as FieldsGroupNames]?.fields.length}` + ) + ).toStrictEqual([ + 'SpecialFields-0', + 'SelectedFields-3', + 'PopularFields-0', + 'AvailableFields-25', + 'UnmappedFields-0', + 'EmptyFields-0', + 'MetaFields-3', + ]); + + expect(fieldGroups.SelectedFields?.fields).toBe(customSortedFields); }); }); diff --git a/src/plugins/unified_field_list/public/hooks/use_grouped_fields.ts b/src/plugins/unified_field_list/public/hooks/use_grouped_fields.ts index cfa5407a238cc..39d1258ee62d8 100644 --- a/src/plugins/unified_field_list/public/hooks/use_grouped_fields.ts +++ b/src/plugins/unified_field_list/public/hooks/use_grouped_fields.ts @@ -17,16 +17,20 @@ import { type FieldsGroup, type FieldListItem, FieldsGroupNames, + ExistenceFetchStatus, } from '../types'; import { type ExistingFieldsReader } from './use_existing_fields'; export interface GroupedFieldsParams { dataViewId: string | null; // `null` is for text-based queries - allFields: T[]; + allFields: T[] | null; // `null` is for loading indicator services: { dataViews: DataViewsContract; }; - fieldsExistenceReader?: ExistingFieldsReader; + fieldsExistenceReader?: ExistingFieldsReader; // use `undefined` for text-based queries + isAffectedByGlobalFilter?: boolean; + popularFieldsLimit?: number; + sortedSelectedFields?: T[]; onOverrideFieldGroupDetails?: ( groupName: FieldsGroupNames ) => Partial | undefined | null; @@ -37,6 +41,9 @@ export interface GroupedFieldsParams { export interface GroupedFieldsResult { fieldGroups: FieldListGroups; + scrollToTopResetCounter: number; + fieldsExistenceStatus: ExistenceFetchStatus; + fieldsExistInIndex: boolean; } export function useGroupedFields({ @@ -44,12 +51,16 @@ export function useGroupedFields({ allFields, services, fieldsExistenceReader, + isAffectedByGlobalFilter = false, + popularFieldsLimit, + sortedSelectedFields, onOverrideFieldGroupDetails, onSupportedFieldFilter, onSelectedFieldFilter, onFilterField, }: GroupedFieldsParams): GroupedFieldsResult { const [dataView, setDataView] = useState(null); + const isAffectedByTimeFilter = Boolean(dataView?.timeFieldName); const fieldsExistenceInfoUnavailable: boolean = dataViewId ? fieldsExistenceReader?.isFieldsExistenceInfoUnavailable(dataViewId) ?? false : true; @@ -68,33 +79,59 @@ export function useGroupedFields({ // if field existence information changed, reload the data view too }, [dataViewId, services.dataViews, setDataView, hasFieldDataHandler]); + // important when switching from a known dataViewId to no data view (like in text-based queries) + useEffect(() => { + if (dataView && !dataViewId) { + setDataView(null); + } + }, [dataView, setDataView, dataViewId]); + + // eslint-disable-next-line react-hooks/exhaustive-deps + const scrollToTopResetCounter: number = useMemo(() => Date.now(), [dataViewId, onFilterField]); + const unfilteredFieldGroups: FieldListGroups = useMemo(() => { const containsData = (field: T) => { - if (!dataViewId || !dataView) { - return true; - } - const overallField = dataView.getFieldByName?.(field.name); - return Boolean(overallField && hasFieldDataHandler(dataViewId, overallField.name)); + return dataViewId ? hasFieldDataHandler(dataViewId, field.name) : true; }; - const fields = allFields || []; - const allSupportedTypesFields = onSupportedFieldFilter - ? fields.filter(onSupportedFieldFilter) - : fields; - const sortedFields = [...allSupportedTypesFields].sort(sortFields); + const selectedFields = sortedSelectedFields || []; + const sortedFields = [...(allFields || [])].sort(sortFields); const groupedFields = { ...getDefaultFieldGroups(), ...groupBy(sortedFields, (field) => { + if (!sortedSelectedFields && onSelectedFieldFilter && onSelectedFieldFilter(field)) { + selectedFields.push(field); + } + if (onSupportedFieldFilter && !onSupportedFieldFilter(field)) { + return 'skippedFields'; + } if (field.type === 'document') { return 'specialFields'; - } else if (dataView?.metaFields?.includes(field.name)) { + } + if (dataView?.metaFields?.includes(field.name)) { return 'metaFields'; - } else if (containsData(field)) { + } + if (dataView?.getFieldByName && !dataView.getFieldByName(field.name)) { + return 'unmappedFields'; + } + if (containsData(field) || fieldsExistenceInfoUnavailable) { return 'availableFields'; - } else return 'emptyFields'; + } + return 'emptyFields'; }), }; - const selectedFields = onSelectedFieldFilter ? sortedFields.filter(onSelectedFieldFilter) : []; + + const popularFields = popularFieldsLimit + ? sortedFields + .filter( + (field) => + field.count && + field.type !== '_source' && + (!onSupportedFieldFilter || onSupportedFieldFilter(field)) + ) + .sort((a: T, b: T) => (b.count || 0) - (a.count || 0)) // sort by popularity score + .slice(0, popularFieldsLimit) + : []; let fieldGroupDefinitions: FieldListGroups = { SpecialFields: { @@ -115,8 +152,25 @@ export function useGroupedFields({ title: i18n.translate('unifiedFieldList.useGroupedFields.selectedFieldsLabel', { defaultMessage: 'Selected fields', }), - isAffectedByGlobalFilter: false, - isAffectedByTimeFilter: true, + isAffectedByGlobalFilter, + isAffectedByTimeFilter, + hideDetails: false, + hideIfEmpty: true, + }, + PopularFields: { + fields: popularFields, + fieldCount: popularFields.length, + isInitiallyOpen: true, + showInAccordion: true, + title: i18n.translate('unifiedFieldList.useGroupedFields.popularFieldsLabel', { + defaultMessage: 'Popular fields', + }), + helpText: i18n.translate('unifiedFieldList.useGroupedFields.popularFieldsLabelHelp', { + defaultMessage: + 'Fields that your organization frequently uses, from most to least popular.', + }), + isAffectedByGlobalFilter, + isAffectedByTimeFilter, hideDetails: false, hideIfEmpty: true, }, @@ -133,8 +187,8 @@ export function useGroupedFields({ : i18n.translate('unifiedFieldList.useGroupedFields.availableFieldsLabel', { defaultMessage: 'Available fields', }), - isAffectedByGlobalFilter: false, - isAffectedByTimeFilter: true, + isAffectedByGlobalFilter, + isAffectedByTimeFilter, // Show details on timeout but not failure // hideDetails: fieldsExistenceInfoUnavailable && !existenceFetchTimeout, // TODO: is this check still necessary? hideDetails: fieldsExistenceInfoUnavailable, @@ -145,6 +199,22 @@ export function useGroupedFields({ } ), }, + UnmappedFields: { + fields: groupedFields.unmappedFields, + fieldCount: groupedFields.unmappedFields.length, + isAffectedByGlobalFilter, + isAffectedByTimeFilter, + isInitiallyOpen: false, + showInAccordion: true, + hideDetails: false, + hideIfEmpty: true, + title: i18n.translate('unifiedFieldList.useGroupedFields.unmappedFieldsLabel', { + defaultMessage: 'Unmapped fields', + }), + helpText: i18n.translate('unifiedFieldList.useGroupedFields.unmappedFieldsLabelHelp', { + defaultMessage: "Fields that aren't explicitly mapped to a field data type.", + }), + }, EmptyFields: { fields: groupedFields.emptyFields, fieldCount: groupedFields.emptyFields.length, @@ -157,15 +227,15 @@ export function useGroupedFields({ title: i18n.translate('unifiedFieldList.useGroupedFields.emptyFieldsLabel', { defaultMessage: 'Empty fields', }), + helpText: i18n.translate('unifiedFieldList.useGroupedFields.emptyFieldsLabelHelp', { + defaultMessage: "Fields that don't have any values based on your filters.", + }), defaultNoFieldsMessage: i18n.translate( 'unifiedFieldList.useGroupedFields.noEmptyDataLabel', { defaultMessage: `There are no empty fields.`, } ), - helpText: i18n.translate('unifiedFieldList.useGroupedFields.emptyFieldsLabelHelp', { - defaultMessage: 'Empty fields did not contain any values based on your filters.', - }), }, MetaFields: { fields: groupedFields.metaFields, @@ -220,6 +290,10 @@ export function useGroupedFields({ dataViewId, hasFieldDataHandler, fieldsExistenceInfoUnavailable, + isAffectedByGlobalFilter, + isAffectedByTimeFilter, + popularFieldsLimit, + sortedSelectedFields, ]); const fieldGroups: FieldListGroups = useMemo(() => { @@ -235,22 +309,39 @@ export function useGroupedFields({ ) as FieldListGroups; }, [unfilteredFieldGroups, onFilterField]); - return useMemo( - () => ({ + const hasDataLoaded = Boolean(allFields); + const allFieldsLength = allFields?.length; + + const fieldsExistInIndex = useMemo(() => { + return dataViewId ? Boolean(allFieldsLength) : true; + }, [dataViewId, allFieldsLength]); + + const fieldsExistenceStatus = useMemo(() => { + if (!hasDataLoaded) { + return ExistenceFetchStatus.unknown; // to show loading indicator in the list + } + if (!dataViewId || !fieldsExistenceReader) { + // ex. for text-based queries + return ExistenceFetchStatus.succeeded; + } + return fieldsExistenceReader.getFieldsExistenceStatus(dataViewId); + }, [dataViewId, hasDataLoaded, fieldsExistenceReader]); + + return useMemo(() => { + return { fieldGroups, - }), - [fieldGroups] - ); + scrollToTopResetCounter, + fieldsExistInIndex, + fieldsExistenceStatus, + }; + }, [fieldGroups, scrollToTopResetCounter, fieldsExistInIndex, fieldsExistenceStatus]); } +const collator = new Intl.Collator(undefined, { + sensitivity: 'base', +}); function sortFields(fieldA: T, fieldB: T) { - return (fieldA.displayName || fieldA.name).localeCompare( - fieldB.displayName || fieldB.name, - undefined, - { - sensitivity: 'base', - } - ); + return collator.compare(fieldA.displayName || fieldA.name, fieldB.displayName || fieldB.name); } function hasFieldDataByDefault(): boolean { @@ -263,5 +354,7 @@ function getDefaultFieldGroups() { availableFields: [], emptyFields: [], metaFields: [], + unmappedFields: [], + skippedFields: [], }; } diff --git a/src/plugins/unified_field_list/public/hooks/use_query_subscriber.ts b/src/plugins/unified_field_list/public/hooks/use_query_subscriber.ts index 9b42db6301f8f..44101d206a2de 100644 --- a/src/plugins/unified_field_list/public/hooks/use_query_subscriber.ts +++ b/src/plugins/unified_field_list/public/hooks/use_query_subscriber.ts @@ -9,6 +9,7 @@ import { useEffect, useState } from 'react'; import type { DataPublicPluginStart } from '@kbn/data-plugin/public'; import type { AggregateQuery, Query, Filter } from '@kbn/es-query'; +import { getResolvedDateRange } from '../utils/get_resolved_date_range'; /** * Hook params @@ -23,32 +24,68 @@ export interface QuerySubscriberParams { export interface QuerySubscriberResult { query: Query | AggregateQuery | undefined; filters: Filter[] | undefined; + fromDate: string | undefined; + toDate: string | undefined; } /** - * Memorizes current query and filters + * Memorizes current query, filters and absolute date range * @param data + * @public */ export const useQuerySubscriber = ({ data }: QuerySubscriberParams) => { + const timefilter = data.query.timefilter.timefilter; const [result, setResult] = useState(() => { const state = data.query.getState(); + const dateRange = getResolvedDateRange(timefilter); return { query: state?.query, filters: state?.filters, + fromDate: dateRange.fromDate, + toDate: dateRange.toDate, }; }); useEffect(() => { - const subscription = data.query.state$.subscribe(({ state }) => { + const subscription = data.search.session.state$.subscribe((sessionState) => { + const dateRange = getResolvedDateRange(timefilter); setResult((prevState) => ({ ...prevState, - query: state.query, - filters: state.filters, + fromDate: dateRange.fromDate, + toDate: dateRange.toDate, })); }); + return () => subscription.unsubscribe(); + }, [setResult, timefilter, data.search.session.state$]); + + useEffect(() => { + const subscription = data.query.state$.subscribe(({ state, changes }) => { + if (changes.query || changes.filters) { + setResult((prevState) => ({ + ...prevState, + query: state.query, + filters: state.filters, + })); + } + }); + return () => subscription.unsubscribe(); }, [setResult, data.query.state$]); return result; }; + +/** + * Checks if query result is ready to be used + * @param result + * @public + */ +export const hasQuerySubscriberData = ( + result: QuerySubscriberResult +): result is { + query: Query | AggregateQuery; + filters: Filter[]; + fromDate: string; + toDate: string; +} => Boolean(result.query && result.filters && result.fromDate && result.toDate); diff --git a/src/plugins/unified_field_list/public/index.ts b/src/plugins/unified_field_list/public/index.ts index e1a315401e0bc..68fddef0ffc16 100755 --- a/src/plugins/unified_field_list/public/index.ts +++ b/src/plugins/unified_field_list/public/index.ts @@ -76,6 +76,7 @@ export { export { useQuerySubscriber, + hasQuerySubscriberData, type QuerySubscriberResult, type QuerySubscriberParams, } from './hooks/use_query_subscriber'; diff --git a/src/plugins/unified_field_list/public/types.ts b/src/plugins/unified_field_list/public/types.ts index d2c80286f8dea..c28452ebc6f25 100755 --- a/src/plugins/unified_field_list/public/types.ts +++ b/src/plugins/unified_field_list/public/types.ts @@ -29,14 +29,17 @@ export interface FieldListItem { name: DataViewField['name']; type?: DataViewField['type']; displayName?: DataViewField['displayName']; + count?: DataViewField['count']; } export enum FieldsGroupNames { SpecialFields = 'SpecialFields', SelectedFields = 'SelectedFields', + PopularFields = 'PopularFields', AvailableFields = 'AvailableFields', EmptyFields = 'EmptyFields', MetaFields = 'MetaFields', + UnmappedFields = 'UnmappedFields', } export interface FieldsGroupDetails { diff --git a/src/plugins/unified_field_list/public/utils/get_resolved_date_range.ts b/src/plugins/unified_field_list/public/utils/get_resolved_date_range.ts new file mode 100644 index 0000000000000..3939c49d7f514 --- /dev/null +++ b/src/plugins/unified_field_list/public/utils/get_resolved_date_range.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { type TimefilterContract } from '@kbn/data-plugin/public'; + +/** + * Get resolved time range by using now provider + * @param timefilter + */ +export const getResolvedDateRange = (timefilter: TimefilterContract) => { + const { from, to } = timefilter.getTime(); + const { min, max } = timefilter.calculateBounds({ + from, + to, + }); + return { fromDate: min?.toISOString() || from, toDate: max?.toISOString() || to }; +}; diff --git a/src/plugins/unified_histogram/kibana.json b/src/plugins/unified_histogram/kibana.json index f89f9c9d4c714..5be043637ed33 100755 --- a/src/plugins/unified_histogram/kibana.json +++ b/src/plugins/unified_histogram/kibana.json @@ -11,5 +11,5 @@ "ui": true, "requiredPlugins": [], "optionalPlugins": [], - "requiredBundles": ["charts", "data"] + "requiredBundles": ["data", "dataViews", "embeddable", "kibanaUtils", "inspector"] } diff --git a/src/plugins/unified_histogram/public/__mocks__/data_view_with_timefield.ts b/src/plugins/unified_histogram/public/__mocks__/data_view_with_timefield.ts index 158d697d67c71..b0ec2fcf84ebb 100644 --- a/src/plugins/unified_histogram/public/__mocks__/data_view_with_timefield.ts +++ b/src/plugins/unified_histogram/public/__mocks__/data_view_with_timefield.ts @@ -12,12 +12,14 @@ import { buildDataViewMock } from './data_view'; const fields = [ { name: '_index', + displayName: '_index', type: 'string', scripted: false, filterable: true, }, { name: 'timestamp', + displayName: 'timestamp', type: 'date', scripted: false, filterable: true, @@ -26,12 +28,14 @@ const fields = [ }, { name: 'message', + displayName: 'message', type: 'string', scripted: false, filterable: false, }, { name: 'extension', + displayName: 'extension', type: 'string', scripted: false, filterable: true, @@ -39,6 +43,7 @@ const fields = [ }, { name: 'bytes', + displayName: 'bytes', type: 'number', scripted: false, filterable: true, @@ -46,6 +51,7 @@ const fields = [ }, { name: 'scripted', + displayName: 'scripted', type: 'number', scripted: true, filterable: false, diff --git a/src/plugins/unified_histogram/public/__mocks__/services.ts b/src/plugins/unified_histogram/public/__mocks__/services.ts index e827596d88feb..1ce16ad8fae85 100644 --- a/src/plugins/unified_histogram/public/__mocks__/services.ts +++ b/src/plugins/unified_histogram/public/__mocks__/services.ts @@ -25,4 +25,5 @@ export const unifiedHistogramServicesMock = { useChartsTheme: jest.fn(() => EUI_CHARTS_THEME_LIGHT.theme), useChartsBaseTheme: jest.fn(() => EUI_CHARTS_THEME_LIGHT.theme), }, + lens: { EmbeddableComponent: jest.fn(() => null) }, } as unknown as UnifiedHistogramServices; diff --git a/src/plugins/unified_histogram/public/chart/breakdown_field_selector.test.tsx b/src/plugins/unified_histogram/public/chart/breakdown_field_selector.test.tsx new file mode 100644 index 0000000000000..2b6b8bd7c537f --- /dev/null +++ b/src/plugins/unified_histogram/public/chart/breakdown_field_selector.test.tsx @@ -0,0 +1,73 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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 { EuiComboBox } from '@elastic/eui'; +import { mountWithIntl } from '@kbn/test-jest-helpers'; +import React from 'react'; +import { UnifiedHistogramBreakdownContext } from '..'; +import { dataViewWithTimefieldMock } from '../__mocks__/data_view_with_timefield'; +import { BreakdownFieldSelector } from './breakdown_field_selector'; +import { fieldSupportsBreakdown } from './field_supports_breakdown'; + +describe('BreakdownFieldSelector', () => { + it('should pass fields that support breakdown as options to the EuiComboBox', () => { + const onBreakdownFieldChange = jest.fn(); + const breakdown: UnifiedHistogramBreakdownContext = { + field: undefined, + }; + const wrapper = mountWithIntl( + + ); + const comboBox = wrapper.find(EuiComboBox); + expect(comboBox.prop('options')).toEqual( + dataViewWithTimefieldMock.fields + .filter(fieldSupportsBreakdown) + .map((field) => ({ label: field.displayName, value: field.name })) + .sort((a, b) => a.label.toLowerCase().localeCompare(b.label.toLowerCase())) + ); + }); + + it('should pass selectedOptions to the EuiComboBox if breakdown.field is defined', () => { + const onBreakdownFieldChange = jest.fn(); + const field = dataViewWithTimefieldMock.fields.find((f) => f.name === 'extension')!; + const breakdown: UnifiedHistogramBreakdownContext = { field }; + const wrapper = mountWithIntl( + + ); + const comboBox = wrapper.find(EuiComboBox); + expect(comboBox.prop('selectedOptions')).toEqual([ + { label: field.displayName, value: field.name }, + ]); + }); + + it('should call onBreakdownFieldChange with the selected field when the user selects a field', () => { + const onBreakdownFieldChange = jest.fn(); + const breakdown: UnifiedHistogramBreakdownContext = { + field: undefined, + }; + const wrapper = mountWithIntl( + + ); + const comboBox = wrapper.find(EuiComboBox); + const selectedField = dataViewWithTimefieldMock.fields.find((f) => f.name === 'extension')!; + comboBox.prop('onChange')!([{ label: selectedField.displayName, value: selectedField.name }]); + expect(onBreakdownFieldChange).toHaveBeenCalledWith(selectedField); + }); +}); diff --git a/src/plugins/unified_histogram/public/chart/breakdown_field_selector.tsx b/src/plugins/unified_histogram/public/chart/breakdown_field_selector.tsx new file mode 100644 index 0000000000000..ef2a14e4423b6 --- /dev/null +++ b/src/plugins/unified_histogram/public/chart/breakdown_field_selector.tsx @@ -0,0 +1,89 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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 { EuiComboBox, EuiComboBoxOptionOption, EuiToolTip, useEuiTheme } from '@elastic/eui'; +import { css } from '@emotion/react'; +import { DataView, DataViewField } from '@kbn/data-views-plugin/common'; +import { i18n } from '@kbn/i18n'; +import React, { useCallback, useState } from 'react'; +import { UnifiedHistogramBreakdownContext } from '../types'; +import { fieldSupportsBreakdown } from './field_supports_breakdown'; + +export interface BreakdownFieldSelectorProps { + dataView: DataView; + breakdown: UnifiedHistogramBreakdownContext; + onBreakdownFieldChange?: (breakdownField: DataViewField | undefined) => void; +} + +export const BreakdownFieldSelector = ({ + dataView, + breakdown, + onBreakdownFieldChange, +}: BreakdownFieldSelectorProps) => { + const fieldOptions = dataView.fields + .filter(fieldSupportsBreakdown) + .map((field) => ({ label: field.displayName, value: field.name })) + .sort((a, b) => a.label.toLowerCase().localeCompare(b.label.toLowerCase())); + + const selectedFields = breakdown.field + ? [{ label: breakdown.field.displayName, value: breakdown.field.name }] + : []; + + const onFieldChange = useCallback( + (newOptions: EuiComboBoxOptionOption[]) => { + const field = newOptions.length + ? dataView.fields.find((currentField) => currentField.name === newOptions[0].value) + : undefined; + + onBreakdownFieldChange?.(field); + }, + [dataView.fields, onBreakdownFieldChange] + ); + + const [fieldPopoverDisabled, setFieldPopoverDisabled] = useState(false); + const disableFieldPopover = useCallback(() => setFieldPopoverDisabled(true), []); + const enableFieldPopover = useCallback( + () => setTimeout(() => setFieldPopoverDisabled(false)), + [] + ); + + const { euiTheme } = useEuiTheme(); + const breakdownCss = css` + width: 100%; + max-width: ${euiTheme.base * 22}px; + `; + + return ( + + + + ); +}; diff --git a/src/plugins/unified_histogram/public/chart/build_bucket_interval.test.ts b/src/plugins/unified_histogram/public/chart/build_bucket_interval.test.ts new file mode 100644 index 0000000000000..072f7a811babe --- /dev/null +++ b/src/plugins/unified_histogram/public/chart/build_bucket_interval.test.ts @@ -0,0 +1,103 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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 { dataPluginMock } from '@kbn/data-plugin/public/mocks'; +import { dataViewWithTimefieldMock } from '../__mocks__/data_view_with_timefield'; +import { calculateBounds } from '@kbn/data-plugin/public'; +import { buildBucketInterval } from './build_bucket_interval'; + +describe('buildBucketInterval', () => { + const getOptions = () => { + const response = { + took: 0, + timed_out: false, + _shards: { + total: 1, + successful: 1, + skipped: 0, + failed: 0, + }, + hits: { + total: 29, + max_score: null, + hits: [], + }, + aggregations: { + '2': { + buckets: [ + { + key_as_string: '2022-10-05T16:00:00.000-03:00', + key: 1664996400000, + doc_count: 6, + }, + { + key_as_string: '2022-10-05T16:30:00.000-03:00', + key: 1664998200000, + doc_count: 2, + }, + { + key_as_string: '2022-10-05T17:00:00.000-03:00', + key: 1665000000000, + doc_count: 3, + }, + { + key_as_string: '2022-10-05T17:30:00.000-03:00', + key: 1665001800000, + doc_count: 8, + }, + { + key_as_string: '2022-10-05T18:00:00.000-03:00', + key: 1665003600000, + doc_count: 10, + }, + ], + }, + }, + }; + const dataView = dataViewWithTimefieldMock; + const dataMock = dataPluginMock.createStartContract(); + dataMock.query.timefilter.timefilter.getTime = () => { + return { from: '1991-03-29T08:04:00.694Z', to: '2021-03-29T07:04:00.695Z' }; + }; + dataMock.query.timefilter.timefilter.calculateBounds = (timeRange) => { + return calculateBounds(timeRange); + }; + return { + data: dataMock, + dataView, + timeInterval: 'auto', + response, + timeRange: { + from: '1991-03-29T08:04:00.694Z', + to: '2021-03-29T07:04:00.695Z', + }, + }; + }; + + it('should return an empty object if response or timeInterval is undefined', () => { + expect( + buildBucketInterval({ + ...getOptions(), + response: undefined, + timeInterval: undefined, + }) + ).toEqual({}); + expect( + buildBucketInterval({ + ...getOptions(), + response: undefined, + }) + ).toEqual({}); + expect( + buildBucketInterval({ + ...getOptions(), + timeInterval: undefined, + }) + ).toEqual({}); + }); +}); diff --git a/src/plugins/unified_histogram/public/chart/build_bucket_interval.ts b/src/plugins/unified_histogram/public/chart/build_bucket_interval.ts new file mode 100644 index 0000000000000..b3c9671662dbc --- /dev/null +++ b/src/plugins/unified_histogram/public/chart/build_bucket_interval.ts @@ -0,0 +1,46 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import type { SearchResponse } from '@elastic/elasticsearch/lib/api/types'; +import { DataPublicPluginStart, search, tabifyAggResponse } from '@kbn/data-plugin/public'; +import type { DataView } from '@kbn/data-views-plugin/common'; +import type { TimeRange } from '@kbn/es-query'; +import type { UnifiedHistogramBucketInterval } from '../types'; +import { getChartAggConfigs } from './get_chart_agg_configs'; + +/** + * Convert the response from the chart request into a format that can be used + * by the unified histogram chart. The returned object should be used to update + * time range interval of histogram. + */ +export const buildBucketInterval = ({ + data, + dataView, + timeInterval, + timeRange, + response, +}: { + data: DataPublicPluginStart; + dataView: DataView; + timeInterval?: string; + timeRange: TimeRange; + response?: SearchResponse; +}) => { + if (!timeInterval || !response) { + return {}; + } + + const chartAggConfigs = getChartAggConfigs({ dataView, timeInterval, timeRange, data }); + const bucketAggConfig = chartAggConfigs.aggs[1]; + + tabifyAggResponse(chartAggConfigs, response); + + return search.aggs.isDateHistogramBucketAggConfig(bucketAggConfig) + ? (bucketAggConfig?.buckets?.getInterval() as UnifiedHistogramBucketInterval) + : undefined; +}; diff --git a/src/plugins/unified_histogram/public/chart/build_chart_data.test.ts b/src/plugins/unified_histogram/public/chart/build_chart_data.test.ts deleted file mode 100644 index 6c920a4a1a5ab..0000000000000 --- a/src/plugins/unified_histogram/public/chart/build_chart_data.test.ts +++ /dev/null @@ -1,128 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { dataPluginMock } from '@kbn/data-plugin/public/mocks'; -import { dataViewWithTimefieldMock } from '../__mocks__/data_view_with_timefield'; -import { calculateBounds } from '@kbn/data-plugin/public'; -import { buildChartData } from './build_chart_data'; - -describe('buildChartData', () => { - const getOptions = () => { - const response = { - took: 0, - timed_out: false, - _shards: { - total: 1, - successful: 1, - skipped: 0, - failed: 0, - }, - hits: { - total: 29, - max_score: null, - hits: [], - }, - aggregations: { - '2': { - buckets: [ - { - key_as_string: '2022-10-05T16:00:00.000-03:00', - key: 1664996400000, - doc_count: 6, - }, - { - key_as_string: '2022-10-05T16:30:00.000-03:00', - key: 1664998200000, - doc_count: 2, - }, - { - key_as_string: '2022-10-05T17:00:00.000-03:00', - key: 1665000000000, - doc_count: 3, - }, - { - key_as_string: '2022-10-05T17:30:00.000-03:00', - key: 1665001800000, - doc_count: 8, - }, - { - key_as_string: '2022-10-05T18:00:00.000-03:00', - key: 1665003600000, - doc_count: 10, - }, - ], - }, - }, - }; - const dataView = dataViewWithTimefieldMock; - const dataMock = dataPluginMock.createStartContract(); - dataMock.query.timefilter.timefilter.getTime = () => { - return { from: '1991-03-29T08:04:00.694Z', to: '2021-03-29T07:04:00.695Z' }; - }; - dataMock.query.timefilter.timefilter.calculateBounds = (timeRange) => { - return calculateBounds(timeRange); - }; - return { - data: dataMock, - dataView, - timeInterval: 'auto', - response, - }; - }; - - const expectedChartData = { - xAxisOrderedValues: [1664996400000, 1664998200000, 1665000000000, 1665001800000, 1665003600000], - xAxisFormat: { id: 'date', params: { pattern: 'HH:mm:ss.SSS' } }, - xAxisLabel: 'timestamp per 0 milliseconds', - yAxisFormat: { id: 'number' }, - ordered: { - date: true, - interval: 'P0D', - intervalESUnit: 'ms', - intervalESValue: 0, - min: '1991-03-29T08:04:00.694Z', - max: '2021-03-29T07:04:00.695Z', - }, - yAxisLabel: 'Count', - values: [ - { x: 1664996400000, y: 6 }, - { x: 1664998200000, y: 2 }, - { x: 1665000000000, y: 3 }, - { x: 1665001800000, y: 8 }, - { x: 1665003600000, y: 10 }, - ], - }; - - it('should return the correct data', () => { - const { bucketInterval, chartData } = buildChartData(getOptions()); - expect(bucketInterval!.toString()).toEqual('P0D'); - expect(JSON.stringify(chartData)).toEqual(JSON.stringify(expectedChartData)); - }); - - it('should return an empty object if response or timeInterval is undefined', () => { - expect( - buildChartData({ - ...getOptions(), - response: undefined, - timeInterval: undefined, - }) - ).toEqual({}); - expect( - buildChartData({ - ...getOptions(), - response: undefined, - }) - ).toEqual({}); - expect( - buildChartData({ - ...getOptions(), - timeInterval: undefined, - }) - ).toEqual({}); - }); -}); diff --git a/src/plugins/unified_histogram/public/chart/build_chart_data.ts b/src/plugins/unified_histogram/public/chart/build_chart_data.ts deleted file mode 100644 index 03b208802ac4d..0000000000000 --- a/src/plugins/unified_histogram/public/chart/build_chart_data.ts +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import type { SearchResponse } from '@elastic/elasticsearch/lib/api/types'; -import { DataPublicPluginStart, search, tabifyAggResponse } from '@kbn/data-plugin/public'; -import type { DataView } from '@kbn/data-views-plugin/common'; -import type { UnifiedHistogramBucketInterval } from '../types'; -import { buildPointSeriesData } from './build_point_series_data'; -import { getChartAggConfigs } from './get_chart_agg_configs'; -import { getDimensions } from './get_dimensions'; - -/** - * Convert the response from the chart request into a format that can be used - * by the unified histogram chart. The returned object should be used to update - * {@link UnifiedHistogramChartContext.bucketInterval} and {@link UnifiedHistogramChartContext.data}. - */ -export const buildChartData = ({ - data, - dataView, - timeInterval, - response, -}: { - data: DataPublicPluginStart; - dataView: DataView; - timeInterval?: string; - response?: SearchResponse; -}) => { - if (!timeInterval || !response) { - return {}; - } - - const chartAggConfigs = getChartAggConfigs({ dataView, timeInterval, data }); - const bucketAggConfig = chartAggConfigs.aggs[1]; - const tabifiedData = tabifyAggResponse(chartAggConfigs, response); - const dimensions = getDimensions(chartAggConfigs, data); - const bucketInterval = search.aggs.isDateHistogramBucketAggConfig(bucketAggConfig) - ? (bucketAggConfig?.buckets?.getInterval() as UnifiedHistogramBucketInterval) - : undefined; - const chartData = buildPointSeriesData(tabifiedData, dimensions!); - - return { - bucketInterval, - chartData, - }; -}; diff --git a/src/plugins/unified_histogram/public/chart/build_point_series_data.test.ts b/src/plugins/unified_histogram/public/chart/build_point_series_data.test.ts deleted file mode 100644 index 3a7f81aa4cd40..0000000000000 --- a/src/plugins/unified_histogram/public/chart/build_point_series_data.test.ts +++ /dev/null @@ -1,120 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 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 { buildPointSeriesData } from './build_point_series_data'; -import moment from 'moment'; -import type { Unit } from '@kbn/datemath'; - -describe('buildPointSeriesData', () => { - test('with valid data', () => { - const table = { - type: 'datatable', - columns: [ - { - id: 'col-0-2', - name: 'order_date per 30 days', - meta: { - type: 'date', - field: 'order_date', - index: 'kibana_sample_data_ecommerce', - params: { id: 'date', params: { pattern: 'YYYY-MM-DD' } }, - source: 'esaggs', - sourceParams: { - dataViewId: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f', - id: '2', - enabled: true, - type: 'date_histogram', - params: { - field: 'order_date', - timeRange: { from: 'now-15y', to: 'now' }, - useNormalizedEsInterval: true, - scaleMetricValues: false, - interval: 'auto', - used_interval: '30d', - drop_partials: false, - min_doc_count: 1, - extended_bounds: {}, - }, - schema: 'segment', - }, - }, - }, - { - id: 'col-1-1', - name: 'Count', - meta: { - type: 'number', - index: 'kibana_sample_data_ecommerce', - params: { id: 'number' }, - source: 'esaggs', - sourceParams: { - dataViewId: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f', - id: '1', - enabled: true, - type: 'count', - params: {}, - schema: 'metric', - }, - }, - }, - ], - rows: [{ 'col-0-2': 1625176800000, 'col-1-1': 2139 }], - }; - const dimensions = { - x: { - accessor: 0, - label: 'order_date per 30 days', - format: { id: 'date', params: { pattern: 'YYYY-MM-DD' } }, - params: { - date: true, - interval: moment.duration(30, 'd'), - intervalESValue: 30, - intervalESUnit: 'd' as Unit, - format: 'YYYY-MM-DD', - bounds: { - min: moment('2006-07-29T11:08:13.078Z'), - max: moment('2021-07-29T11:08:13.078Z'), - }, - }, - }, - y: { accessor: 1, format: { id: 'number' }, label: 'Count' }, - } as const; - expect(buildPointSeriesData(table, dimensions)).toMatchInlineSnapshot(` - Object { - "ordered": Object { - "date": true, - "interval": "P30D", - "intervalESUnit": "d", - "intervalESValue": 30, - "max": "2021-07-29T11:08:13.078Z", - "min": "2006-07-29T11:08:13.078Z", - }, - "values": Array [ - Object { - "x": 1625176800000, - "y": 2139, - }, - ], - "xAxisFormat": Object { - "id": "date", - "params": Object { - "pattern": "YYYY-MM-DD", - }, - }, - "xAxisLabel": "order_date per 30 days", - "xAxisOrderedValues": Array [ - 1625176800000, - ], - "yAxisFormat": Object { - "id": "number", - }, - "yAxisLabel": "Count", - } - `); - }); -}); diff --git a/src/plugins/unified_histogram/public/chart/build_point_series_data.ts b/src/plugins/unified_histogram/public/chart/build_point_series_data.ts deleted file mode 100644 index dc9d97fd0708f..0000000000000 --- a/src/plugins/unified_histogram/public/chart/build_point_series_data.ts +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 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 { uniq } from 'lodash'; -import type { UnifiedHistogramChartData, Dimensions, Table } from '../types'; - -export const buildPointSeriesData = ( - table: Table, - dimensions: Dimensions -): UnifiedHistogramChartData => { - const { x, y } = dimensions; - const xAccessor = table.columns[x.accessor].id; - const yAccessor = table.columns[y.accessor].id; - const chart = {} as UnifiedHistogramChartData; - - chart.xAxisOrderedValues = uniq(table.rows.map((r) => r[xAccessor] as number)); - chart.xAxisFormat = x.format; - chart.xAxisLabel = table.columns[x.accessor].name; - chart.yAxisFormat = y.format; - const { intervalESUnit, intervalESValue, interval, bounds } = x.params; - chart.ordered = { - date: true, - interval, - intervalESUnit, - intervalESValue, - min: bounds.min, - max: bounds.max, - }; - - chart.yAxisLabel = table.columns[y.accessor].name; - - chart.values = table.rows - .filter((row) => row && row[yAccessor] !== 'NaN') - .map((row) => ({ - x: row[xAccessor] as number, - y: row[yAccessor] as number, - })); - - return chart; -}; diff --git a/src/plugins/unified_histogram/public/chart/chart.test.tsx b/src/plugins/unified_histogram/public/chart/chart.test.tsx index 41de0687acfa6..21682cb919d3c 100644 --- a/src/plugins/unified_histogram/public/chart/chart.test.tsx +++ b/src/plugins/unified_histogram/public/chart/chart.test.tsx @@ -9,70 +9,50 @@ import React, { ReactElement } from 'react'; import { act } from 'react-dom/test-utils'; import { mountWithIntl } from '@kbn/test-jest-helpers'; -import type { UnifiedHistogramChartData, UnifiedHistogramFetchStatus } from '../types'; +import type { DataView } from '@kbn/data-views-plugin/public'; +import type { UnifiedHistogramFetchStatus } from '../types'; import { Chart } from './chart'; import type { ReactWrapper } from 'enzyme'; import { unifiedHistogramServicesMock } from '../__mocks__/services'; +import { searchSourceInstanceMock } from '@kbn/data-plugin/common/search/search_source/mocks'; +import { of } from 'rxjs'; import { HitsCounter } from '../hits_counter'; +import { dataViewWithTimefieldMock } from '../__mocks__/data_view_with_timefield'; +import { dataViewMock } from '../__mocks__/data_view'; +import { BreakdownFieldSelector } from './breakdown_field_selector'; +import { Histogram } from './histogram'; async function mountComponent({ noChart, noHits, + noBreakdown, chartHidden = false, appendHistogram, onEditVisualization = jest.fn(), + dataView = dataViewWithTimefieldMock, }: { noChart?: boolean; noHits?: boolean; + noBreakdown?: boolean; chartHidden?: boolean; appendHistogram?: ReactElement; + dataView?: DataView; onEditVisualization?: null | (() => void); } = {}) { const services = unifiedHistogramServicesMock; services.data.query.timefilter.timefilter.getAbsoluteTime = () => { return { from: '2020-05-14T11:05:13.590', to: '2020-05-14T11:20:13.590' }; }; - - const chartData = { - xAxisOrderedValues: [ - 1623880800000, 1623967200000, 1624053600000, 1624140000000, 1624226400000, 1624312800000, - 1624399200000, 1624485600000, 1624572000000, 1624658400000, 1624744800000, 1624831200000, - 1624917600000, 1625004000000, 1625090400000, - ], - xAxisFormat: { id: 'date', params: { pattern: 'YYYY-MM-DD' } }, - xAxisLabel: 'order_date per day', - yAxisFormat: { id: 'number' }, - ordered: { - date: true, - interval: { - asMilliseconds: jest.fn(), - }, - intervalESUnit: 'd', - intervalESValue: 1, - min: '2021-03-18T08:28:56.411Z', - max: '2021-07-01T07:28:56.411Z', - }, - yAxisLabel: 'Count', - values: [ - { x: 1623880800000, y: 134 }, - { x: 1623967200000, y: 152 }, - { x: 1624053600000, y: 141 }, - { x: 1624140000000, y: 138 }, - { x: 1624226400000, y: 142 }, - { x: 1624312800000, y: 157 }, - { x: 1624399200000, y: 149 }, - { x: 1624485600000, y: 146 }, - { x: 1624572000000, y: 170 }, - { x: 1624658400000, y: 137 }, - { x: 1624744800000, y: 150 }, - { x: 1624831200000, y: 144 }, - { x: 1624917600000, y: 147 }, - { x: 1625004000000, y: 137 }, - { x: 1625090400000, y: 66 }, - ], - } as unknown as UnifiedHistogramChartData; + (services.data.query.queryString.getDefaultQuery as jest.Mock).mockReturnValue({ + language: 'kuery', + query: '', + }); + (searchSourceInstanceMock.fetch$ as jest.Mock).mockImplementation( + jest.fn().mockReturnValue(of({ rawResponse: { hits: { total: noHits ? 0 : 2 } } })) + ); const props = { + dataView, services: unifiedHistogramServicesMock, hits: noHits ? undefined @@ -91,8 +71,8 @@ async function mountComponent({ description: 'test', scale: 2, }, - data: chartData, }, + breakdown: noBreakdown ? undefined : { field: undefined }, appendHistogram, onEditVisualization: onEditVisualization || undefined, onResetChartHeight: jest.fn(), @@ -105,7 +85,7 @@ async function mountComponent({ instance = mountWithIntl(); // wait for initial async loading to complete await new Promise((r) => setTimeout(r, 0)); - await instance.update(); + instance.update(); }); return instance; } @@ -158,13 +138,13 @@ describe('Chart', () => { const fn = jest.fn(); const component = await mountComponent({ onEditVisualization: fn }); await act(async () => { - await component + component .find('[data-test-subj="unifiedHistogramEditVisualization"]') .first() .simulate('click'); }); - - expect(fn).toHaveBeenCalled(); + const lensAttributes = component.find(Histogram).prop('lensAttributes'); + expect(fn).toHaveBeenCalledWith(lensAttributes); }); it('should render HitsCounter when hits is defined', async () => { @@ -182,4 +162,29 @@ describe('Chart', () => { const component = await mountComponent({ appendHistogram }); expect(component.find('[data-test-subj="appendHistogram"]').exists()).toBeTruthy(); }); + + it('should not render chart if data view is not time based', async () => { + const component = await mountComponent({ dataView: dataViewMock }); + expect(component.find('[data-test-subj="unifiedHistogramChart"]').exists()).toBeFalsy(); + }); + + it('should render chart if data view is time based', async () => { + const component = await mountComponent(); + expect(component.find('[data-test-subj="unifiedHistogramChart"]').exists()).toBeTruthy(); + }); + + it('should render BreakdownFieldSelector when chart is visible and breakdown is defined', async () => { + const component = await mountComponent(); + expect(component.find(BreakdownFieldSelector).exists()).toBeTruthy(); + }); + + it('should not render BreakdownFieldSelector when chart is hidden', async () => { + const component = await mountComponent({ chartHidden: true }); + expect(component.find(BreakdownFieldSelector).exists()).toBeFalsy(); + }); + + it('should not render BreakdownFieldSelector when chart is visible and breakdown is undefined', async () => { + const component = await mountComponent({ noBreakdown: true }); + expect(component.find(BreakdownFieldSelector).exists()).toBeFalsy(); + }); }); diff --git a/src/plugins/unified_histogram/public/chart/chart.tsx b/src/plugins/unified_histogram/public/chart/chart.tsx index 0f6c47f8a532e..b1970cd26e365 100644 --- a/src/plugins/unified_histogram/public/chart/chart.tsx +++ b/src/plugins/unified_histogram/public/chart/chart.tsx @@ -6,9 +6,8 @@ * Side Public License, v 1. */ -import type { ReactElement } from 'react'; -import React, { memo, useCallback, useEffect, useRef, useState } from 'react'; -import moment from 'moment'; +import { ReactElement, useMemo } from 'react'; +import React, { memo } from 'react'; import { EuiButtonIcon, EuiContextMenu, @@ -16,31 +15,48 @@ import { EuiFlexItem, EuiPopover, EuiToolTip, - useEuiBreakpoint, - useEuiTheme, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { css } from '@emotion/react'; +import { DataView, DataViewField, DataViewType } from '@kbn/data-views-plugin/public'; +import type { TypedLensByValueInput } from '@kbn/lens-plugin/public'; import { HitsCounter } from '../hits_counter'; import { Histogram } from './histogram'; import { useChartPanels } from './use_chart_panels'; import type { + UnifiedHistogramBreakdownContext, UnifiedHistogramChartContext, + UnifiedHistogramFetchStatus, UnifiedHistogramHitsContext, + UnifiedHistogramChartLoadEvent, + UnifiedHistogramRequestContext, UnifiedHistogramServices, } from '../types'; +import { BreakdownFieldSelector } from './breakdown_field_selector'; +import { useTotalHits } from './use_total_hits'; +import { useRequestParams } from './use_request_params'; +import { useChartStyles } from './use_chart_styles'; +import { useChartActions } from './use_chart_actions'; +import { useRefetchId } from './use_refetch_id'; +import { getLensAttributes } from './get_lens_attributes'; export interface ChartProps { className?: string; services: UnifiedHistogramServices; + dataView: DataView; + lastReloadRequestTime?: number; + request?: UnifiedHistogramRequestContext; hits?: UnifiedHistogramHitsContext; chart?: UnifiedHistogramChartContext; + breakdown?: UnifiedHistogramBreakdownContext; appendHitsCounter?: ReactElement; appendHistogram?: ReactElement; - onEditVisualization?: () => void; + onEditVisualization?: (lensAttributes: TypedLensByValueInput['attributes']) => void; onResetChartHeight?: () => void; onChartHiddenChange?: (chartHidden: boolean) => void; onTimeIntervalChange?: (timeInterval: string) => void; + onBreakdownFieldChange?: (breakdownField: DataViewField | undefined) => void; + onTotalHitsChange?: (status: UnifiedHistogramFetchStatus, result?: number | Error) => void; + onChartLoad?: (event: UnifiedHistogramChartLoadEvent) => void; } const HistogramMemoized = memo(Histogram); @@ -48,89 +64,124 @@ const HistogramMemoized = memo(Histogram); export function Chart({ className, services, + dataView, + lastReloadRequestTime, + request, hits, chart, + breakdown, appendHitsCounter, appendHistogram, - onEditVisualization, + onEditVisualization: originalOnEditVisualization, onResetChartHeight, onChartHiddenChange, onTimeIntervalChange, + onBreakdownFieldChange, + onTotalHitsChange, + onChartLoad, }: ChartProps) { - const { data } = services; - const [showChartOptionsPopover, setShowChartOptionsPopover] = useState(false); - - const chartRef = useRef<{ element: HTMLElement | null; moveFocus: boolean }>({ - element: null, - moveFocus: false, + const { + showChartOptionsPopover, + chartRef, + toggleChartOptions, + closeChartOptions, + toggleHideChart, + } = useChartActions({ + chart, + onChartHiddenChange, }); - const onShowChartOptions = useCallback(() => { - setShowChartOptionsPopover(!showChartOptionsPopover); - }, [showChartOptionsPopover]); + const panels = useChartPanels({ + chart, + toggleHideChart, + onTimeIntervalChange, + closePopover: closeChartOptions, + onResetChartHeight, + }); - const closeChartOptions = useCallback(() => { - setShowChartOptionsPopover(false); - }, [setShowChartOptionsPopover]); + const chartVisible = !!( + chart && + !chart.hidden && + dataView.id && + dataView.type !== DataViewType.ROLLUP && + dataView.isTimeBased() + ); - useEffect(() => { - if (chartRef.current.moveFocus && chartRef.current.element) { - chartRef.current.element.focus(); - } - }, [chart?.hidden]); + const { filters, query, relativeTimeRange } = useRequestParams({ + services, + lastReloadRequestTime, + request, + }); - const toggleHideChart = useCallback(() => { - const chartHidden = !chart?.hidden; - chartRef.current.moveFocus = !chartHidden; - onChartHiddenChange?.(chartHidden); - }, [chart?.hidden, onChartHiddenChange]); + const refetchId = useRefetchId({ + dataView, + lastReloadRequestTime, + request, + hits, + chart, + chartVisible, + breakdown, + filters, + query, + relativeTimeRange, + }); - const timefilterUpdateHandler = useCallback( - (ranges: { from: number; to: number }) => { - data.query.timefilter.timefilter.setTime({ - from: moment(ranges.from).toISOString(), - to: moment(ranges.to).toISOString(), - mode: 'absolute', - }); - }, - [data] + // We need to update the absolute time range whenever the refetchId changes + const timeRange = useMemo( + () => services.data.query.timefilter.timefilter.getAbsoluteTime(), + // eslint-disable-next-line react-hooks/exhaustive-deps + [services.data.query.timefilter.timefilter, refetchId] ); - const panels = useChartPanels({ + useTotalHits({ + services, + dataView, + lastReloadRequestTime, + request, + hits, chart, - toggleHideChart, - onTimeIntervalChange: (timeInterval) => onTimeIntervalChange?.(timeInterval), - closePopover: () => setShowChartOptionsPopover(false), - onResetChartHeight, + chartVisible, + breakdown, + filters, + query, + timeRange, + refetchId, + onTotalHitsChange, }); - const { euiTheme } = useEuiTheme(); - const resultCountCss = css` - padding: ${euiTheme.size.s}; - min-height: ${euiTheme.base * 3}px; - `; - const resultCountTitleCss = css` - ${useEuiBreakpoint(['xs', 's'])} { - margin-bottom: 0 !important; - } - `; - const resultCountToggleCss = css` - ${useEuiBreakpoint(['xs', 's'])} { - align-items: flex-end; - } - `; - const timechartCss = css` - flex-grow: 1; - display: flex; - flex-direction: column; - position: relative; + const { + resultCountCss, + resultCountInnerCss, + resultCountTitleCss, + resultCountToggleCss, + histogramCss, + breakdownFieldSelectorGroupCss, + breakdownFieldSelectorItemCss, + chartToolButtonCss, + } = useChartStyles(chartVisible); - // SASSTODO: the visualizing component should have an option or a modifier - .series > rect { - fill-opacity: 0.5; - stroke-width: 1; - } - `; + const lensAttributes = useMemo( + () => + getLensAttributes({ + title: chart?.title, + filters, + query, + dataView, + timeInterval: chart?.timeInterval, + breakdownField: breakdown?.field, + }), + [breakdown?.field, chart?.timeInterval, chart?.title, dataView, filters, query] + ); + + const onEditVisualization = useMemo( + () => + originalOnEditVisualization + ? () => { + originalOnEditVisualization(lensAttributes); + } + : undefined, + [lensAttributes, originalOnEditVisualization] + ); return ( - + } {chart && ( - - + + + {chartVisible && breakdown && ( + + + + )} {onEditVisualization && ( - + )} - + - {chart && !chart.hidden && ( + {chartVisible && (
    (chartRef.current.element = element)} @@ -212,12 +283,19 @@ export function Chart({ aria-label={i18n.translate('unifiedHistogram.histogramOfFoundDocumentsAriaLabel', { defaultMessage: 'Histogram of found documents', })} - css={timechartCss} + css={histogramCss} >
    {appendHistogram} diff --git a/src/plugins/unified_histogram/public/chart/consts.ts b/src/plugins/unified_histogram/public/chart/consts.ts new file mode 100644 index 0000000000000..d2af2ed4ee33a --- /dev/null +++ b/src/plugins/unified_histogram/public/chart/consts.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export const REQUEST_DEBOUNCE_MS = 100; diff --git a/src/plugins/unified_histogram/public/chart/field_supports_breakdown.test.ts b/src/plugins/unified_histogram/public/chart/field_supports_breakdown.test.ts new file mode 100644 index 0000000000000..b38b42cf2a249 --- /dev/null +++ b/src/plugins/unified_histogram/public/chart/field_supports_breakdown.test.ts @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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 { fieldSupportsBreakdown } from './field_supports_breakdown'; + +describe('fieldSupportsBreakdown', () => { + it('should return false if field is not aggregatable', () => { + expect( + fieldSupportsBreakdown({ aggregatable: false, scripted: false, type: 'string' } as any) + ).toBe(false); + }); + + it('should return false if field is scripted', () => { + expect( + fieldSupportsBreakdown({ aggregatable: true, scripted: true, type: 'string' } as any) + ).toBe(false); + }); + + it('should return false if field type is not supported', () => { + expect( + fieldSupportsBreakdown({ aggregatable: true, scripted: false, type: 'unsupported' } as any) + ).toBe(false); + }); + + it('should return true if field is aggregatable and type is supported', () => { + expect( + fieldSupportsBreakdown({ aggregatable: true, scripted: false, type: 'string' } as any) + ).toBe(true); + }); +}); diff --git a/src/plugins/unified_histogram/public/chart/field_supports_breakdown.ts b/src/plugins/unified_histogram/public/chart/field_supports_breakdown.ts new file mode 100644 index 0000000000000..302a5950fefcb --- /dev/null +++ b/src/plugins/unified_histogram/public/chart/field_supports_breakdown.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { DataViewField } from '@kbn/data-views-plugin/public'; + +const supportedTypes = new Set(['string', 'boolean', 'number', 'ip']); + +export const fieldSupportsBreakdown = (field: DataViewField) => + supportedTypes.has(field.type) && field.aggregatable && !field.scripted; diff --git a/src/plugins/unified_histogram/public/chart/get_chart_agg_config.test.ts b/src/plugins/unified_histogram/public/chart/get_chart_agg_config.test.ts index 3b4f470ba6119..ef5ce1b677153 100644 --- a/src/plugins/unified_histogram/public/chart/get_chart_agg_config.test.ts +++ b/src/plugins/unified_histogram/public/chart/get_chart_agg_config.test.ts @@ -14,7 +14,15 @@ describe('getChartAggConfigs', () => { test('is working', () => { const dataView = dataViewWithTimefieldMock; const dataMock = dataPluginMock.createStartContract(); - const aggsConfig = getChartAggConfigs({ dataView, timeInterval: 'auto', data: dataMock }); + const aggsConfig = getChartAggConfigs({ + dataView, + timeInterval: 'auto', + data: dataMock, + timeRange: { + from: '2022-10-05T16:00:00.000-03:00', + to: '2022-10-05T18:00:00.000-03:00', + }, + }); expect(aggsConfig!.aggs).toMatchInlineSnapshot(` Array [ @@ -38,6 +46,10 @@ describe('getChartAggConfigs', () => { "interval": "auto", "min_doc_count": 1, "scaleMetricValues": false, + "timeRange": Object { + "from": "2022-10-05T16:00:00.000-03:00", + "to": "2022-10-05T18:00:00.000-03:00", + }, "useNormalizedEsInterval": true, "used_interval": "0ms", }, diff --git a/src/plugins/unified_histogram/public/chart/get_chart_agg_configs.ts b/src/plugins/unified_histogram/public/chart/get_chart_agg_configs.ts index 93ef7f3dd9188..d68330a22a45d 100644 --- a/src/plugins/unified_histogram/public/chart/get_chart_agg_configs.ts +++ b/src/plugins/unified_histogram/public/chart/get_chart_agg_configs.ts @@ -8,6 +8,7 @@ import type { DataPublicPluginStart } from '@kbn/data-plugin/public'; import type { DataView } from '@kbn/data-views-plugin/common'; +import type { TimeRange } from '@kbn/es-query'; /** * Helper function to get the agg configs required for the unified histogram chart request @@ -15,10 +16,12 @@ import type { DataView } from '@kbn/data-views-plugin/common'; export function getChartAggConfigs({ dataView, timeInterval, + timeRange, data, }: { dataView: DataView; timeInterval: string; + timeRange: TimeRange; data: DataPublicPluginStart; }) { const visStateAggs = [ @@ -32,7 +35,7 @@ export function getChartAggConfigs({ params: { field: dataView.timeFieldName!, interval: timeInterval, - timeRange: data.query.timefilter.timefilter.getTime(), + timeRange, }, }, ]; diff --git a/src/plugins/unified_histogram/public/chart/get_dimensions.test.ts b/src/plugins/unified_histogram/public/chart/get_dimensions.test.ts deleted file mode 100644 index fd26fa20ce793..0000000000000 --- a/src/plugins/unified_histogram/public/chart/get_dimensions.test.ts +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 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 { dataPluginMock } from '@kbn/data-plugin/public/mocks'; -import { getDimensions } from './get_dimensions'; -import { dataViewWithTimefieldMock } from '../__mocks__/data_view_with_timefield'; -import { calculateBounds } from '@kbn/data-plugin/public'; -import { getChartAggConfigs } from './get_chart_agg_configs'; - -test('getDimensions', () => { - const dataView = dataViewWithTimefieldMock; - const dataMock = dataPluginMock.createStartContract(); - dataMock.query.timefilter.timefilter.getTime = () => { - return { from: '1991-03-29T08:04:00.694Z', to: '2021-03-29T07:04:00.695Z' }; - }; - dataMock.query.timefilter.timefilter.calculateBounds = (timeRange) => { - return calculateBounds(timeRange); - }; - const aggsConfig = getChartAggConfigs({ dataView, timeInterval: 'auto', data: dataMock }); - const actual = getDimensions(aggsConfig!, dataMock); - expect(actual).toMatchInlineSnapshot(` - Object { - "x": Object { - "accessor": 0, - "format": Object { - "id": "date", - "params": Object { - "pattern": "HH:mm:ss.SSS", - }, - }, - "label": "timestamp per 0 milliseconds", - "params": Object { - "bounds": Object { - "max": "2021-03-29T07:04:00.695Z", - "min": "1991-03-29T08:04:00.694Z", - }, - "date": true, - "format": "HH:mm:ss.SSS", - "interval": "P0D", - "intervalESUnit": "ms", - "intervalESValue": 0, - }, - }, - "y": Object { - "accessor": 1, - "format": Object { - "id": "number", - }, - "label": "Count", - }, - } - `); -}); diff --git a/src/plugins/unified_histogram/public/chart/get_dimensions.ts b/src/plugins/unified_histogram/public/chart/get_dimensions.ts deleted file mode 100644 index 94ed3d4540d21..0000000000000 --- a/src/plugins/unified_histogram/public/chart/get_dimensions.ts +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 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 moment from 'moment'; -import dateMath from '@kbn/datemath'; -import { DataPublicPluginStart, search, IAggConfigs } from '@kbn/data-plugin/public'; -import type { Dimensions, HistogramParamsBounds } from '../types'; - -export function getDimensions( - aggs: IAggConfigs, - data: DataPublicPluginStart -): Dimensions | undefined { - const [metric, agg] = aggs.aggs; - const { from, to } = data.query.timefilter.timefilter.getTime(); - agg.params.timeRange = { - from: dateMath.parse(from), - to: dateMath.parse(to, { roundUp: true }), - }; - const bounds = agg.params.timeRange - ? (data.query.timefilter.timefilter.calculateBounds( - agg.params.timeRange - ) as HistogramParamsBounds) - : null; - const buckets = search.aggs.isDateHistogramBucketAggConfig(agg) ? agg.buckets : undefined; - - if (!buckets || !bounds) { - return; - } - - const { esUnit, esValue } = buckets.getInterval(); - return { - x: { - accessor: 0, - label: agg.makeLabel(), - format: agg.toSerializedFieldFormat(), - params: { - date: true, - interval: moment.duration(esValue, esUnit), - intervalESValue: esValue, - intervalESUnit: esUnit, - format: buckets.getScaledDateFormat(), - bounds, - }, - }, - y: { - accessor: 1, - format: metric.toSerializedFieldFormat(), - label: metric.makeLabel(), - }, - }; -} diff --git a/src/plugins/unified_histogram/public/chart/get_lens_attributes.test.ts b/src/plugins/unified_histogram/public/chart/get_lens_attributes.test.ts new file mode 100644 index 0000000000000..3e0ac936a6573 --- /dev/null +++ b/src/plugins/unified_histogram/public/chart/get_lens_attributes.test.ts @@ -0,0 +1,480 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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 { getLensAttributes } from './get_lens_attributes'; +import { AggregateQuery, Filter, FilterStateStore, Query } from '@kbn/es-query'; +import type { DataView, DataViewField } from '@kbn/data-views-plugin/public'; +import { dataViewWithTimefieldMock } from '../__mocks__/data_view_with_timefield'; + +describe('getLensAttributes', () => { + const dataView: DataView = dataViewWithTimefieldMock; + const filters: Filter[] = [ + { + meta: { + index: dataView.id, + negate: false, + disabled: false, + alias: null, + type: 'phrase', + key: 'extension', + params: { + query: 'js', + }, + }, + query: { + match: { + extension: { + query: 'js', + type: 'phrase', + }, + }, + }, + $state: { + store: FilterStateStore.APP_STATE, + }, + }, + ]; + const query: Query | AggregateQuery = { language: 'kuery', query: 'extension : css' }; + const timeInterval = 'auto'; + + it('should return correct attributes', () => { + const breakdownField: DataViewField | undefined = undefined; + expect( + getLensAttributes({ title: 'test', filters, query, dataView, timeInterval, breakdownField }) + ).toMatchInlineSnapshot(` + Object { + "references": Array [ + Object { + "id": "index-pattern-with-timefield-id", + "name": "indexpattern-datasource-current-indexpattern", + "type": "index-pattern", + }, + Object { + "id": "index-pattern-with-timefield-id", + "name": "indexpattern-datasource-layer-unifiedHistogram", + "type": "index-pattern", + }, + ], + "state": Object { + "datasourceStates": Object { + "formBased": Object { + "layers": Object { + "unifiedHistogram": Object { + "columnOrder": Array [ + "date_column", + "count_column", + ], + "columns": Object { + "count_column": Object { + "dataType": "number", + "isBucketed": false, + "label": "Count of records", + "operationType": "count", + "params": Object { + "format": Object { + "id": "number", + "params": Object { + "decimals": 0, + }, + }, + }, + "scale": "ratio", + "sourceField": "___records___", + }, + "date_column": Object { + "dataType": "date", + "isBucketed": true, + "label": "timestamp", + "operationType": "date_histogram", + "params": Object { + "interval": "auto", + }, + "scale": "interval", + "sourceField": "timestamp", + }, + }, + }, + }, + }, + }, + "filters": Array [ + Object { + "$state": Object { + "store": "appState", + }, + "meta": Object { + "alias": null, + "disabled": false, + "index": "index-pattern-with-timefield-id", + "key": "extension", + "negate": false, + "params": Object { + "query": "js", + }, + "type": "phrase", + }, + "query": Object { + "match": Object { + "extension": Object { + "query": "js", + "type": "phrase", + }, + }, + }, + }, + ], + "query": Object { + "language": "kuery", + "query": "extension : css", + }, + "visualization": Object { + "axisTitlesVisibilitySettings": Object { + "x": false, + "yLeft": false, + "yRight": false, + }, + "fittingFunction": "None", + "gridlinesVisibilitySettings": Object { + "x": true, + "yLeft": true, + "yRight": false, + }, + "layers": Array [ + Object { + "accessors": Array [ + "count_column", + ], + "layerId": "unifiedHistogram", + "layerType": "data", + "seriesType": "bar_stacked", + "xAccessor": "date_column", + "yConfig": Array [ + Object { + "forAccessor": "count_column", + }, + ], + }, + ], + "legend": Object { + "isVisible": true, + "position": "right", + }, + "preferredSeriesType": "bar_stacked", + "showCurrentTimeMarker": true, + "tickLabelsVisibilitySettings": Object { + "x": true, + "yLeft": true, + "yRight": false, + }, + "valueLabels": "hide", + }, + }, + "title": "test", + "visualizationType": "lnsXY", + } + `); + }); + + it('should return correct attributes with breakdown field', () => { + const breakdownField: DataViewField | undefined = dataView.fields.find( + (f) => f.name === 'extension' + ); + expect( + getLensAttributes({ title: 'test', filters, query, dataView, timeInterval, breakdownField }) + ).toMatchInlineSnapshot(` + Object { + "references": Array [ + Object { + "id": "index-pattern-with-timefield-id", + "name": "indexpattern-datasource-current-indexpattern", + "type": "index-pattern", + }, + Object { + "id": "index-pattern-with-timefield-id", + "name": "indexpattern-datasource-layer-unifiedHistogram", + "type": "index-pattern", + }, + ], + "state": Object { + "datasourceStates": Object { + "formBased": Object { + "layers": Object { + "unifiedHistogram": Object { + "columnOrder": Array [ + "breakdown_column", + "date_column", + "count_column", + ], + "columns": Object { + "breakdown_column": Object { + "dataType": "string", + "isBucketed": true, + "label": "Top 3 values of extension", + "operationType": "terms", + "params": Object { + "missingBucket": false, + "orderBy": Object { + "columnId": "count_column", + "type": "column", + }, + "orderDirection": "desc", + "otherBucket": true, + "parentFormat": Object { + "id": "terms", + }, + "size": 3, + }, + "scale": "ordinal", + "sourceField": "extension", + }, + "count_column": Object { + "dataType": "number", + "isBucketed": false, + "label": "Count of records", + "operationType": "count", + "params": Object { + "format": Object { + "id": "number", + "params": Object { + "decimals": 0, + }, + }, + }, + "scale": "ratio", + "sourceField": "___records___", + }, + "date_column": Object { + "dataType": "date", + "isBucketed": true, + "label": "timestamp", + "operationType": "date_histogram", + "params": Object { + "interval": "auto", + }, + "scale": "interval", + "sourceField": "timestamp", + }, + }, + }, + }, + }, + }, + "filters": Array [ + Object { + "$state": Object { + "store": "appState", + }, + "meta": Object { + "alias": null, + "disabled": false, + "index": "index-pattern-with-timefield-id", + "key": "extension", + "negate": false, + "params": Object { + "query": "js", + }, + "type": "phrase", + }, + "query": Object { + "match": Object { + "extension": Object { + "query": "js", + "type": "phrase", + }, + }, + }, + }, + ], + "query": Object { + "language": "kuery", + "query": "extension : css", + }, + "visualization": Object { + "axisTitlesVisibilitySettings": Object { + "x": false, + "yLeft": false, + "yRight": false, + }, + "fittingFunction": "None", + "gridlinesVisibilitySettings": Object { + "x": true, + "yLeft": true, + "yRight": false, + }, + "layers": Array [ + Object { + "accessors": Array [ + "count_column", + ], + "layerId": "unifiedHistogram", + "layerType": "data", + "seriesType": "bar_stacked", + "splitAccessor": "breakdown_column", + "xAccessor": "date_column", + }, + ], + "legend": Object { + "isVisible": true, + "position": "right", + }, + "preferredSeriesType": "bar_stacked", + "showCurrentTimeMarker": true, + "tickLabelsVisibilitySettings": Object { + "x": true, + "yLeft": true, + "yRight": false, + }, + "valueLabels": "hide", + }, + }, + "title": "test", + "visualizationType": "lnsXY", + } + `); + }); + + it('should return correct attributes with unsupported breakdown field', () => { + const breakdownField: DataViewField | undefined = dataView.fields.find( + (f) => f.name === 'scripted' + ); + expect( + getLensAttributes({ title: 'test', filters, query, dataView, timeInterval, breakdownField }) + ).toMatchInlineSnapshot(` + Object { + "references": Array [ + Object { + "id": "index-pattern-with-timefield-id", + "name": "indexpattern-datasource-current-indexpattern", + "type": "index-pattern", + }, + Object { + "id": "index-pattern-with-timefield-id", + "name": "indexpattern-datasource-layer-unifiedHistogram", + "type": "index-pattern", + }, + ], + "state": Object { + "datasourceStates": Object { + "formBased": Object { + "layers": Object { + "unifiedHistogram": Object { + "columnOrder": Array [ + "date_column", + "count_column", + ], + "columns": Object { + "count_column": Object { + "dataType": "number", + "isBucketed": false, + "label": "Count of records", + "operationType": "count", + "params": Object { + "format": Object { + "id": "number", + "params": Object { + "decimals": 0, + }, + }, + }, + "scale": "ratio", + "sourceField": "___records___", + }, + "date_column": Object { + "dataType": "date", + "isBucketed": true, + "label": "timestamp", + "operationType": "date_histogram", + "params": Object { + "interval": "auto", + }, + "scale": "interval", + "sourceField": "timestamp", + }, + }, + }, + }, + }, + }, + "filters": Array [ + Object { + "$state": Object { + "store": "appState", + }, + "meta": Object { + "alias": null, + "disabled": false, + "index": "index-pattern-with-timefield-id", + "key": "extension", + "negate": false, + "params": Object { + "query": "js", + }, + "type": "phrase", + }, + "query": Object { + "match": Object { + "extension": Object { + "query": "js", + "type": "phrase", + }, + }, + }, + }, + ], + "query": Object { + "language": "kuery", + "query": "extension : css", + }, + "visualization": Object { + "axisTitlesVisibilitySettings": Object { + "x": false, + "yLeft": false, + "yRight": false, + }, + "fittingFunction": "None", + "gridlinesVisibilitySettings": Object { + "x": true, + "yLeft": true, + "yRight": false, + }, + "layers": Array [ + Object { + "accessors": Array [ + "count_column", + ], + "layerId": "unifiedHistogram", + "layerType": "data", + "seriesType": "bar_stacked", + "xAccessor": "date_column", + "yConfig": Array [ + Object { + "forAccessor": "count_column", + }, + ], + }, + ], + "legend": Object { + "isVisible": true, + "position": "right", + }, + "preferredSeriesType": "bar_stacked", + "showCurrentTimeMarker": true, + "tickLabelsVisibilitySettings": Object { + "x": true, + "yLeft": true, + "yRight": false, + }, + "valueLabels": "hide", + }, + }, + "title": "test", + "visualizationType": "lnsXY", + } + `); + }); +}); diff --git a/src/plugins/unified_histogram/public/chart/get_lens_attributes.ts b/src/plugins/unified_histogram/public/chart/get_lens_attributes.ts new file mode 100644 index 0000000000000..dc6f9216b4e56 --- /dev/null +++ b/src/plugins/unified_histogram/public/chart/get_lens_attributes.ts @@ -0,0 +1,180 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import type { DataView, DataViewField } from '@kbn/data-views-plugin/public'; +import type { AggregateQuery, Filter, Query } from '@kbn/es-query'; +import { i18n } from '@kbn/i18n'; +import type { + CountIndexPatternColumn, + DateHistogramIndexPatternColumn, + GenericIndexPatternColumn, + TermsIndexPatternColumn, + TypedLensByValueInput, +} from '@kbn/lens-plugin/public'; +import { fieldSupportsBreakdown } from './field_supports_breakdown'; + +export const getLensAttributes = ({ + title, + filters, + query, + dataView, + timeInterval, + breakdownField, +}: { + title?: string; + filters: Filter[]; + query: Query | AggregateQuery; + dataView: DataView; + timeInterval: string | undefined; + breakdownField: DataViewField | undefined; +}) => { + const showBreakdown = breakdownField && fieldSupportsBreakdown(breakdownField); + + let columnOrder = ['date_column', 'count_column']; + + if (showBreakdown) { + columnOrder = ['breakdown_column', ...columnOrder]; + } + + let columns: Record = { + date_column: { + dataType: 'date', + isBucketed: true, + label: dataView.timeFieldName ?? '', + operationType: 'date_histogram', + scale: 'interval', + sourceField: dataView.timeFieldName, + params: { + interval: timeInterval ?? 'auto', + }, + } as DateHistogramIndexPatternColumn, + count_column: { + dataType: 'number', + isBucketed: false, + label: i18n.translate('unifiedHistogram.countColumnLabel', { + defaultMessage: 'Count of records', + }), + operationType: 'count', + scale: 'ratio', + sourceField: '___records___', + params: { + format: { + id: 'number', + params: { + decimals: 0, + }, + }, + }, + } as CountIndexPatternColumn, + }; + + if (showBreakdown) { + columns = { + ...columns, + breakdown_column: { + dataType: 'string', + isBucketed: true, + label: i18n.translate('unifiedHistogram.breakdownColumnLabel', { + defaultMessage: 'Top 3 values of {fieldName}', + values: { fieldName: breakdownField?.displayName }, + }), + operationType: 'terms', + scale: 'ordinal', + sourceField: breakdownField.name, + params: { + size: 3, + orderBy: { + type: 'column', + columnId: 'count_column', + }, + orderDirection: 'desc', + otherBucket: true, + missingBucket: false, + parentFormat: { + id: 'terms', + }, + }, + } as TermsIndexPatternColumn, + }; + } + + return { + title: + title ?? + i18n.translate('unifiedHistogram.lensTitle', { + defaultMessage: 'Edit visualization', + }), + references: [ + { + id: dataView.id ?? '', + name: 'indexpattern-datasource-current-indexpattern', + type: 'index-pattern', + }, + { + id: dataView.id ?? '', + name: 'indexpattern-datasource-layer-unifiedHistogram', + type: 'index-pattern', + }, + ], + state: { + datasourceStates: { + formBased: { + layers: { + unifiedHistogram: { columnOrder, columns }, + }, + }, + }, + filters, + query: 'language' in query ? query : { language: 'kuery', query: '' }, + visualization: { + layers: [ + { + accessors: ['count_column'], + layerId: 'unifiedHistogram', + layerType: 'data', + seriesType: 'bar_stacked', + xAccessor: 'date_column', + ...(showBreakdown + ? { splitAccessor: 'breakdown_column' } + : { + yConfig: [ + { + forAccessor: 'count_column', + }, + ], + }), + }, + ], + legend: { + isVisible: true, + position: 'right', + }, + preferredSeriesType: 'bar_stacked', + valueLabels: 'hide', + fittingFunction: 'None', + showCurrentTimeMarker: true, + axisTitlesVisibilitySettings: { + x: false, + yLeft: false, + yRight: false, + }, + gridlinesVisibilitySettings: { + x: true, + yLeft: true, + yRight: false, + }, + tickLabelsVisibilitySettings: { + x: true, + yLeft: true, + yRight: false, + }, + }, + }, + visualizationType: 'lnsXY', + } as TypedLensByValueInput['attributes']; +}; diff --git a/src/plugins/unified_histogram/public/chart/histogram.test.tsx b/src/plugins/unified_histogram/public/chart/histogram.test.tsx index 3e1213978e385..03652a63f5456 100644 --- a/src/plugins/unified_histogram/public/chart/histogram.test.tsx +++ b/src/plugins/unified_histogram/public/chart/histogram.test.tsx @@ -6,55 +6,37 @@ * Side Public License, v 1. */ import { mountWithIntl } from '@kbn/test-jest-helpers'; -import type { UnifiedHistogramChartData, UnifiedHistogramFetchStatus } from '../types'; -import { Histogram } from './histogram'; +import { getLensProps, Histogram } from './histogram'; import React from 'react'; import { unifiedHistogramServicesMock } from '../__mocks__/services'; +import { dataViewWithTimefieldMock } from '../__mocks__/data_view_with_timefield'; +import { createDefaultInspectorAdapters } from '@kbn/expressions-plugin/common'; +import { UnifiedHistogramFetchStatus } from '../types'; +import { getLensAttributes } from './get_lens_attributes'; +import { REQUEST_DEBOUNCE_MS } from './consts'; +import { act } from 'react-dom/test-utils'; +import * as buildBucketInterval from './build_bucket_interval'; +import * as useTimeRange from './use_time_range'; +import { RequestStatus } from '@kbn/inspector-plugin/public'; -const chartData = { - xAxisOrderedValues: [ - 1623880800000, 1623967200000, 1624053600000, 1624140000000, 1624226400000, 1624312800000, - 1624399200000, 1624485600000, 1624572000000, 1624658400000, 1624744800000, 1624831200000, - 1624917600000, 1625004000000, 1625090400000, - ], - xAxisFormat: { id: 'date', params: { pattern: 'YYYY-MM-DD' } }, - xAxisLabel: 'order_date per day', - yAxisFormat: { id: 'number' }, - ordered: { - date: true, - interval: { - asMilliseconds: jest.fn(), +const mockBucketInterval = { description: '1 minute', scale: undefined, scaled: false }; +jest.spyOn(buildBucketInterval, 'buildBucketInterval').mockReturnValue(mockBucketInterval); +jest.spyOn(useTimeRange, 'useTimeRange'); + +const getMockLensAttributes = () => + getLensAttributes({ + title: 'test', + filters: [], + query: { + language: 'kuery', + query: '', }, - intervalESUnit: 'd', - intervalESValue: 1, - min: '2021-03-18T08:28:56.411Z', - max: '2021-07-01T07:28:56.411Z', - }, - yAxisLabel: 'Count', - values: [ - { x: 1623880800000, y: 134 }, - { x: 1623967200000, y: 152 }, - { x: 1624053600000, y: 141 }, - { x: 1624140000000, y: 138 }, - { x: 1624226400000, y: 142 }, - { x: 1624312800000, y: 157 }, - { x: 1624399200000, y: 149 }, - { x: 1624485600000, y: 146 }, - { x: 1624572000000, y: 170 }, - { x: 1624658400000, y: 137 }, - { x: 1624744800000, y: 150 }, - { x: 1624831200000, y: 144 }, - { x: 1624917600000, y: 147 }, - { x: 1625004000000, y: 137 }, - { x: 1625090400000, y: 66 }, - ], -} as unknown as UnifiedHistogramChartData; + dataView: dataViewWithTimefieldMock, + timeInterval: 'auto', + breakdownField: dataViewWithTimefieldMock.getFieldByName('extension'), + }); -function mountComponent( - status: UnifiedHistogramFetchStatus, - data: UnifiedHistogramChartData | null = chartData, - error?: Error -) { +function mountComponent() { const services = unifiedHistogramServicesMock; services.data.query.timefilter.timefilter.getAbsoluteTime = () => { return { from: '2020-05-14T11:05:13.590', to: '2020-05-14T11:20:13.590' }; @@ -64,44 +46,212 @@ function mountComponent( const props = { services: unifiedHistogramServicesMock, + request: { + searchSessionId: '123', + }, + hits: { + status: UnifiedHistogramFetchStatus.loading, + total: undefined, + }, chart: { - status, hidden: false, timeInterval: 'auto', - bucketInterval: { - scaled: true, - description: 'test', - scale: 2, - }, - data: data ?? undefined, - error, }, timefilterUpdateHandler, + dataView: dataViewWithTimefieldMock, + timeRange: { + from: '2020-05-14T11:05:13.590', + to: '2020-05-14T11:20:13.590', + }, + lastReloadRequestTime: 42, + lensAttributes: getMockLensAttributes(), + onTotalHitsChange: jest.fn(), + onChartLoad: jest.fn(), }; - return mountWithIntl(); + return { + props, + component: mountWithIntl(), + }; } describe('Histogram', () => { it('renders correctly', () => { - const component = mountComponent('complete'); + const { component } = mountComponent(); expect(component.find('[data-test-subj="unifiedHistogramChart"]').exists()).toBe(true); }); - it('renders error correctly', () => { - const component = mountComponent('error', null, new Error('Loading error')); - expect(component.find('[data-test-subj="unifiedHistogramChart"]').exists()).toBe(false); - expect(component.find('[data-test-subj="unifiedHistogramErrorChartContainer"]').exists()).toBe( - true + it('should render lens.EmbeddableComponent with debounced props', async () => { + const { component, props } = mountComponent(); + const embeddable = unifiedHistogramServicesMock.lens.EmbeddableComponent; + expect(component.find(embeddable).exists()).toBe(true); + let lensProps = component.find(embeddable).props(); + const originalProps = getLensProps({ + timeRange: props.timeRange, + attributes: getMockLensAttributes(), + request: props.request, + lastReloadRequestTime: props.lastReloadRequestTime, + onLoad: lensProps.onLoad, + }); + expect(lensProps).toEqual(originalProps); + component.setProps({ lastReloadRequestTime: 43 }).update(); + lensProps = component.find(embeddable).props(); + expect(lensProps).toEqual(originalProps); + await act(async () => { + await new Promise((resolve) => setTimeout(resolve, REQUEST_DEBOUNCE_MS)); + }); + component.update(); + lensProps = component.find(embeddable).props(); + expect(lensProps).toEqual({ ...originalProps, lastReloadRequestTime: 43 }); + }); + + it('should execute onLoad correctly', async () => { + const { component, props } = mountComponent(); + const embeddable = unifiedHistogramServicesMock.lens.EmbeddableComponent; + const onLoad = component.find(embeddable).props().onLoad; + const adapters = createDefaultInspectorAdapters(); + adapters.tables.tables.unifiedHistogram = { meta: { statistics: { totalCount: 100 } } } as any; + const rawResponse = { + took: 0, + timed_out: false, + _shards: { + total: 1, + successful: 1, + skipped: 0, + failed: 0, + }, + hits: { + total: 100, + max_score: null, + hits: [], + }, + aggregations: { + '2': { + buckets: [ + { + key_as_string: '2022-10-05T16:00:00.000-03:00', + key: 1664996400000, + doc_count: 20, + }, + { + key_as_string: '2022-10-05T16:30:00.000-03:00', + key: 1664998200000, + doc_count: 20, + }, + { + key_as_string: '2022-10-05T17:00:00.000-03:00', + key: 1665000000000, + doc_count: 20, + }, + { + key_as_string: '2022-10-05T17:30:00.000-03:00', + key: 1665001800000, + doc_count: 20, + }, + { + key_as_string: '2022-10-05T18:00:00.000-03:00', + key: 1665003600000, + doc_count: 20, + }, + ], + }, + }, + }; + jest + .spyOn(adapters.requests, 'getRequests') + .mockReturnValue([{ response: { json: { rawResponse } } } as any]); + onLoad(true, undefined); + expect(props.onTotalHitsChange).toHaveBeenLastCalledWith( + UnifiedHistogramFetchStatus.loading, + undefined + ); + expect(props.onChartLoad).toHaveBeenLastCalledWith({ complete: false, adapters: {} }); + expect(buildBucketInterval.buildBucketInterval).not.toHaveBeenCalled(); + expect(useTimeRange.useTimeRange).toHaveBeenLastCalledWith( + expect.objectContaining({ bucketInterval: undefined }) + ); + act(() => { + onLoad(false, adapters); + }); + expect(props.onTotalHitsChange).toHaveBeenLastCalledWith( + UnifiedHistogramFetchStatus.complete, + 100 + ); + expect(props.onChartLoad).toHaveBeenLastCalledWith({ complete: true, adapters }); + expect(buildBucketInterval.buildBucketInterval).toHaveBeenCalled(); + expect(useTimeRange.useTimeRange).toHaveBeenLastCalledWith( + expect.objectContaining({ bucketInterval: mockBucketInterval }) ); - expect( - component.find('[data-test-subj="unifiedHistogramErrorChartText"]').get(1).props.children - ).toBe('Loading error'); }); - it('renders loading state correctly', () => { - const component = mountComponent('loading', null); - expect(component.find('[data-test-subj="unifiedHistogramChart"]').exists()).toBe(true); - expect(component.find('[data-test-subj="unifiedHistogramChartLoading"]').exists()).toBe(true); + it('should execute onLoad correctly when the request has a failure status', async () => { + const { component, props } = mountComponent(); + const embeddable = unifiedHistogramServicesMock.lens.EmbeddableComponent; + const onLoad = component.find(embeddable).props().onLoad; + const adapters = createDefaultInspectorAdapters(); + jest + .spyOn(adapters.requests, 'getRequests') + .mockReturnValue([{ status: RequestStatus.ERROR } as any]); + onLoad(false, adapters); + expect(props.onTotalHitsChange).toHaveBeenLastCalledWith( + UnifiedHistogramFetchStatus.error, + undefined + ); + expect(props.onChartLoad).toHaveBeenLastCalledWith({ complete: false, adapters }); + }); + + it('should execute onLoad correctly when the response has shard failures', async () => { + const { component, props } = mountComponent(); + const embeddable = unifiedHistogramServicesMock.lens.EmbeddableComponent; + const onLoad = component.find(embeddable).props().onLoad; + const adapters = createDefaultInspectorAdapters(); + const rawResponse = { + _shards: { + total: 1, + successful: 0, + skipped: 0, + failed: 1, + failures: [], + }, + }; + jest + .spyOn(adapters.requests, 'getRequests') + .mockReturnValue([{ response: { json: { rawResponse } } } as any]); + onLoad(false, adapters); + expect(props.onTotalHitsChange).toHaveBeenLastCalledWith( + UnifiedHistogramFetchStatus.error, + undefined + ); + expect(props.onChartLoad).toHaveBeenLastCalledWith({ complete: false, adapters }); + }); + + it('should not recreate onLoad in debounced lens props when hits.total changes', async () => { + const { component, props } = mountComponent(); + const embeddable = unifiedHistogramServicesMock.lens.EmbeddableComponent; + const onLoad = component.find(embeddable).props().onLoad; + onLoad(true, undefined); + expect(props.onTotalHitsChange).toHaveBeenLastCalledWith( + UnifiedHistogramFetchStatus.loading, + undefined + ); + component + .setProps({ + hits: { + status: UnifiedHistogramFetchStatus.complete, + total: 100, + }, + }) + .update(); + expect(component.find(embeddable).props().onLoad).toBe(onLoad); + await act(async () => { + await new Promise((resolve) => setTimeout(resolve, REQUEST_DEBOUNCE_MS)); + }); + component.update(); + expect(component.find(embeddable).props().onLoad).toBe(onLoad); + onLoad(true, undefined); + expect(props.onTotalHitsChange).toHaveBeenLastCalledWith( + UnifiedHistogramFetchStatus.loading, + 100 + ); }); }); diff --git a/src/plugins/unified_histogram/public/chart/histogram.tsx b/src/plugins/unified_histogram/public/chart/histogram.tsx index a201258e49bf2..c30c6a1410985 100644 --- a/src/plugins/unified_histogram/public/chart/histogram.tsx +++ b/src/plugins/unified_histogram/public/chart/histogram.tsx @@ -6,351 +6,187 @@ * Side Public License, v 1. */ -import moment, { unitOfTime } from 'moment-timezone'; -import React, { useCallback, useMemo } from 'react'; -import { - EuiFlexGroup, - EuiFlexItem, - EuiIcon, - EuiIconTip, - EuiLoadingChart, - EuiSpacer, - EuiText, - useEuiTheme, -} from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n-react'; -import dateMath from '@kbn/datemath'; -import type { - BrushEndListener, - ElementClickListener, - XYBrushEvent, - XYChartElementEvent, -} from '@elastic/charts'; -import { - Axis, - Chart, - HistogramBarSeries, - Position, - ScaleType, - Settings, - TooltipType, -} from '@elastic/charts'; -import type { IUiSettingsClient } from '@kbn/core/public'; -import { i18n } from '@kbn/i18n'; -import { - CurrentTime, - Endzones, - getAdjustedInterval, - renderEndzoneTooltip, -} from '@kbn/charts-plugin/public'; -import { LEGACY_TIME_AXIS, MULTILAYER_TIME_AXIS_STYLE } from '@kbn/charts-plugin/common'; +import { useEuiTheme } from '@elastic/eui'; import { css } from '@emotion/react'; -import type { UnifiedHistogramChartContext, UnifiedHistogramServices } from '../types'; +import React, { useCallback, useEffect, useRef, useState } from 'react'; +import type { DataView } from '@kbn/data-views-plugin/public'; +import type { DefaultInspectorAdapters } from '@kbn/expressions-plugin/common'; +import type { IKibanaSearchResponse } from '@kbn/data-plugin/public'; +import type { estypes } from '@elastic/elasticsearch'; +import type { TimeRange } from '@kbn/es-query'; +import useDebounce from 'react-use/lib/useDebounce'; +import { ViewMode } from '@kbn/embeddable-plugin/public'; +import type { TypedLensByValueInput } from '@kbn/lens-plugin/public'; +import { RequestStatus } from '@kbn/inspector-plugin/public'; +import { + UnifiedHistogramBucketInterval, + UnifiedHistogramChartContext, + UnifiedHistogramFetchStatus, + UnifiedHistogramHitsContext, + UnifiedHistogramChartLoadEvent, + UnifiedHistogramRequestContext, + UnifiedHistogramServices, +} from '../types'; +import { buildBucketInterval } from './build_bucket_interval'; +import { useTimeRange } from './use_time_range'; +import { REQUEST_DEBOUNCE_MS } from './consts'; export interface HistogramProps { services: UnifiedHistogramServices; + dataView: DataView; + lastReloadRequestTime: number | undefined; + request?: UnifiedHistogramRequestContext; + hits?: UnifiedHistogramHitsContext; chart: UnifiedHistogramChartContext; - timefilterUpdateHandler: (ranges: { from: number; to: number }) => void; -} - -function getTimezone(uiSettings: IUiSettingsClient) { - if (uiSettings.isDefault('dateFormat:tz')) { - const detectedTimezone = moment.tz.guess(); - if (detectedTimezone) return detectedTimezone; - else return moment().format('Z'); - } else { - return uiSettings.get('dateFormat:tz', 'Browser'); - } + timeRange: TimeRange; + lensAttributes: TypedLensByValueInput['attributes']; + onTotalHitsChange?: (status: UnifiedHistogramFetchStatus, result?: number | Error) => void; + onChartLoad?: (event: UnifiedHistogramChartLoadEvent) => void; } export function Histogram({ - services: { data, theme, uiSettings, fieldFormats }, - chart: { status, timeInterval, bucketInterval, data: chartData, error }, - timefilterUpdateHandler, + services: { data, lens, uiSettings }, + dataView, + lastReloadRequestTime, + request, + hits, + chart: { timeInterval }, + timeRange, + lensAttributes: attributes, + onTotalHitsChange, + onChartLoad, }: HistogramProps) { - const chartTheme = theme.useChartsTheme(); - const chartBaseTheme = theme.useChartsBaseTheme(); - const timeZone = getTimezone(uiSettings); + const [bucketInterval, setBucketInterval] = useState(); + const { timeRangeText, timeRangeDisplay } = useTimeRange({ + uiSettings, + bucketInterval, + timeRange, + timeInterval, + }); - const onBrushEnd = useCallback( - ({ x }: XYBrushEvent) => { - if (!x) { + // Keep track of previous hits in a ref to avoid recreating the + // onLoad callback when the hits change, which triggers a Lens reload + const previousHits = useRef(hits?.total); + + useEffect(() => { + previousHits.current = hits?.total; + }, [hits?.total]); + + const onLoad = useCallback( + (isLoading: boolean, adapters: Partial | undefined) => { + const lensRequest = adapters?.requests?.getRequests()[0]; + const requestFailed = lensRequest?.status === RequestStatus.ERROR; + const json = lensRequest?.response?.json as + | IKibanaSearchResponse + | undefined; + const response = json?.rawResponse; + + // Lens will swallow shard failures and return `isLoading: false` because it displays + // its own errors, but this causes us to emit onTotalHitsChange(UnifiedHistogramFetchStatus.complete, 0). + // This is incorrect, so we check for request failures and shard failures here, and emit an error instead. + if (requestFailed || response?._shards.failed) { + onTotalHitsChange?.(UnifiedHistogramFetchStatus.error, undefined); + onChartLoad?.({ complete: false, adapters: adapters ?? {} }); return; } - const [from, to] = x; - timefilterUpdateHandler({ from, to }); - }, - [timefilterUpdateHandler] - ); - const onElementClick = useCallback( - (xInterval: number): ElementClickListener => - ([elementData]) => { - const startRange = (elementData as XYChartElementEvent)[0].x; + const totalHits = adapters?.tables?.tables?.unifiedHistogram?.meta?.statistics?.totalCount; - const range = { - from: startRange, - to: startRange + xInterval, - }; + onTotalHitsChange?.( + isLoading ? UnifiedHistogramFetchStatus.loading : UnifiedHistogramFetchStatus.complete, + totalHits ?? previousHits.current + ); - timefilterUpdateHandler(range); - }, - [timefilterUpdateHandler] - ); + if (response) { + const newBucketInterval = buildBucketInterval({ + data, + dataView, + timeInterval, + timeRange, + response, + }); - const { timefilter } = data.query.timefilter; - const { from, to } = timefilter.getAbsoluteTime(); - const dateFormat = useMemo(() => uiSettings.get('dateFormat'), [uiSettings]); - - const toMoment = useCallback( - (datetime: moment.Moment | undefined) => { - if (!datetime) { - return ''; - } - if (!dateFormat) { - return String(datetime); + setBucketInterval(newBucketInterval); } - return datetime.format(dateFormat); + + onChartLoad?.({ complete: !isLoading, adapters: adapters ?? {} }); }, - [dateFormat] + [data, dataView, onChartLoad, onTotalHitsChange, timeInterval, timeRange] ); - const timeRangeText = useMemo(() => { - const timeRange = { - from: dateMath.parse(from), - to: dateMath.parse(to, { roundUp: true }), - }; - const intervalText = i18n.translate('unifiedHistogram.histogramTimeRangeIntervalDescription', { - defaultMessage: '(interval: {value})', - values: { - value: `${ - timeInterval === 'auto' - ? `${i18n.translate('unifiedHistogram.histogramTimeRangeIntervalAuto', { - defaultMessage: 'Auto', - })} - ` - : '' - }${bucketInterval?.description}`, - }, - }); - return `${toMoment(timeRange.from)} - ${toMoment(timeRange.to)} ${intervalText}`; - }, [from, to, timeInterval, bucketInterval?.description, toMoment]); - const { euiTheme } = useEuiTheme(); const chartCss = css` + position: relative; flex-grow: 1; - padding: 0 ${euiTheme.size.s} ${euiTheme.size.s} ${euiTheme.size.s}; - `; - if (!chartData && status === 'loading') { - const chartLoadingCss = css` - display: flex; - flex-direction: column; - justify-content: center; - flex: 1 0 100%; - text-align: center; + & > div { height: 100%; - width: 100%; - `; - - return ( -
    -
    - - - - - -
    -
    - ); - } - - if (status === 'error' && error) { - const chartErrorContainerCss = css` - padding: 0 ${euiTheme.size.s} 0 ${euiTheme.size.s}; - `; - const chartErrorIconCss = css` - padding-top: 0.5 * ${euiTheme.size.xs}; - `; - const chartErrorCss = css` - margin-left: ${euiTheme.size.xs} !important; - `; - const chartErrorTextCss = css` - margin-top: ${euiTheme.size.s}; - `; - - return ( -
    - - - - - - - - - - - - {error.message} - -
    - ); - } - - if (!chartData) { - return null; - } - - const formatXValue = (val: string) => { - const xAxisFormat = chartData.xAxisFormat.params!.pattern; - return moment(val).format(xAxisFormat); - }; - - const isDarkMode = uiSettings.get('theme:darkMode'); - - /* - * Deprecation: [interval] on [date_histogram] is deprecated, use [fixed_interval] or [calendar_interval]. - * see https://github.com/elastic/kibana/issues/27410 - * TODO: Once the Discover query has been update, we should change the below to use the new field - */ - const { intervalESValue, intervalESUnit, interval } = chartData.ordered; - const xInterval = interval.asMilliseconds(); - - const xValues = chartData.xAxisOrderedValues; - const lastXValue = xValues[xValues.length - 1]; - - const domain = chartData.ordered; - const domainStart = domain.min.valueOf(); - const domainEnd = domain.max.valueOf(); - - const domainMin = Math.min(chartData.values[0]?.x, domainStart); - const domainMax = Math.max(domainEnd - xInterval, lastXValue); - - const xDomain = { - min: domainMin, - max: domainMax, - minInterval: getAdjustedInterval( - xValues, - intervalESValue, - intervalESUnit as unitOfTime.Base, - timeZone - ), - }; - const tooltipProps = { - headerFormatter: renderEndzoneTooltip(xInterval, domainStart, domainEnd, formatXValue), - type: TooltipType.VerticalCursor, - }; - - const xAxisFormatter = fieldFormats.deserialize(chartData.yAxisFormat); - - const useLegacyTimeAxis = uiSettings.get(LEGACY_TIME_AXIS, false); + } + + & .echLegend .echLegendList { + padding-right: ${euiTheme.size.s}; + } + + & > .euiLoadingChart { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + } + `; - const toolTipTitle = i18n.translate('unifiedHistogram.timeIntervalWithValueWarning', { - defaultMessage: 'Warning', - }); + const [debouncedProps, setDebouncedProps] = useState( + getLensProps({ + timeRange, + attributes, + request, + lastReloadRequestTime, + onLoad, + }) + ); - const toolTipContent = i18n.translate('unifiedHistogram.bucketIntervalTooltip', { - defaultMessage: - 'This interval creates {bucketsDescription} to show in the selected time range, so it has been scaled to {bucketIntervalDescription}.', - values: { - bucketsDescription: - bucketInterval!.scale && bucketInterval!.scale > 1 - ? i18n.translate('unifiedHistogram.bucketIntervalTooltip.tooLargeBucketsText', { - defaultMessage: 'buckets that are too large', - }) - : i18n.translate('unifiedHistogram.bucketIntervalTooltip.tooManyBucketsText', { - defaultMessage: 'too many buckets', - }), - bucketIntervalDescription: bucketInterval?.description, + useDebounce( + () => { + setDebouncedProps( + getLensProps({ timeRange, attributes, request, lastReloadRequestTime, onLoad }) + ); }, - }); - - const timeRangeCss = css` - padding: 0 ${euiTheme.size.s} 0 ${euiTheme.size.s}; - `; - let timeRange = ( - - {timeRangeText} - + REQUEST_DEBOUNCE_MS, + [attributes, lastReloadRequestTime, onLoad, request, timeRange] ); - if (bucketInterval?.scaled) { - const timeRangeWrapperCss = css` - flex-grow: 0; - `; - timeRange = ( - - {timeRange} - - - - - ); - } return ( - + <>
    - - - xAxisFormatter.convert(value)} - /> - - - - - +
    - {timeRange} -
    + {timeRangeDisplay} + ); } + +export const getLensProps = ({ + timeRange, + attributes, + request, + lastReloadRequestTime, + onLoad, +}: { + timeRange: TimeRange; + attributes: TypedLensByValueInput['attributes']; + request: UnifiedHistogramRequestContext | undefined; + lastReloadRequestTime: number | undefined; + onLoad: (isLoading: boolean, adapters: Partial | undefined) => void; +}) => ({ + id: 'unifiedHistogramLensComponent', + viewMode: ViewMode.VIEW, + timeRange, + attributes, + noPadding: true, + searchSessionId: request?.searchSessionId, + executionContext: { + description: 'fetch chart data and total hits', + }, + lastReloadRequestTime, + onLoad, +}); diff --git a/src/plugins/unified_histogram/public/chart/index.ts b/src/plugins/unified_histogram/public/chart/index.ts index e50532f3bfec2..6a6d2d65f6f92 100644 --- a/src/plugins/unified_histogram/public/chart/index.ts +++ b/src/plugins/unified_histogram/public/chart/index.ts @@ -7,5 +7,3 @@ */ export { Chart } from './chart'; -export { getChartAggConfigs } from './get_chart_agg_configs'; -export { buildChartData } from './build_chart_data'; diff --git a/src/plugins/unified_histogram/public/chart/use_chart_actions.test.ts b/src/plugins/unified_histogram/public/chart/use_chart_actions.test.ts new file mode 100644 index 0000000000000..5967f01fd543b --- /dev/null +++ b/src/plugins/unified_histogram/public/chart/use_chart_actions.test.ts @@ -0,0 +1,82 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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 { renderHook } from '@testing-library/react-hooks'; +import { act } from 'react-test-renderer'; +import { UnifiedHistogramChartContext } from '..'; +import { useChartActions } from './use_chart_actions'; + +describe('useChartActions', () => { + const render = () => { + const chart: UnifiedHistogramChartContext = { + hidden: false, + timeInterval: 'auto', + }; + const onChartHiddenChange = jest.fn((hidden: boolean) => { + chart.hidden = hidden; + }); + return { + chart, + onChartHiddenChange, + hook: renderHook(() => useChartActions({ chart, onChartHiddenChange })), + }; + }; + + it('should toggle chart options', () => { + const { hook } = render(); + expect(hook.result.current.showChartOptionsPopover).toBe(false); + act(() => { + hook.result.current.toggleChartOptions(); + }); + expect(hook.result.current.showChartOptionsPopover).toBe(true); + act(() => { + hook.result.current.toggleChartOptions(); + }); + expect(hook.result.current.showChartOptionsPopover).toBe(false); + }); + + it('should close chart options', () => { + const { hook } = render(); + act(() => { + hook.result.current.toggleChartOptions(); + }); + expect(hook.result.current.showChartOptionsPopover).toBe(true); + act(() => { + hook.result.current.closeChartOptions(); + }); + expect(hook.result.current.showChartOptionsPopover).toBe(false); + }); + + it('should toggle hide chart', () => { + const { chart, onChartHiddenChange, hook } = render(); + act(() => { + hook.result.current.toggleHideChart(); + }); + expect(chart.hidden).toBe(true); + expect(onChartHiddenChange).toBeCalledWith(true); + act(() => { + hook.result.current.toggleHideChart(); + }); + expect(chart.hidden).toBe(false); + expect(onChartHiddenChange).toBeCalledWith(false); + }); + + it('should focus chart element', () => { + const { chart, hook } = render(); + hook.result.current.chartRef.current.element = document.createElement('div'); + hook.result.current.chartRef.current.element.focus = jest.fn(); + chart.hidden = true; + hook.rerender(); + act(() => { + hook.result.current.toggleHideChart(); + }); + hook.rerender(); + expect(hook.result.current.chartRef.current.moveFocus).toBe(true); + expect(hook.result.current.chartRef.current.element.focus).toBeCalled(); + }); +}); diff --git a/src/plugins/unified_histogram/public/chart/use_chart_actions.ts b/src/plugins/unified_histogram/public/chart/use_chart_actions.ts new file mode 100644 index 0000000000000..85b876e0862c1 --- /dev/null +++ b/src/plugins/unified_histogram/public/chart/use_chart_actions.ts @@ -0,0 +1,53 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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 { useCallback, useEffect, useRef, useState } from 'react'; +import type { UnifiedHistogramChartContext } from '../types'; + +export const useChartActions = ({ + chart, + onChartHiddenChange, +}: { + chart: UnifiedHistogramChartContext | undefined; + onChartHiddenChange?: (chartHidden: boolean) => void; +}) => { + const [showChartOptionsPopover, setShowChartOptionsPopover] = useState(false); + + const toggleChartOptions = useCallback(() => { + setShowChartOptionsPopover(!showChartOptionsPopover); + }, [showChartOptionsPopover]); + + const closeChartOptions = useCallback(() => { + setShowChartOptionsPopover(false); + }, [setShowChartOptionsPopover]); + + const chartRef = useRef<{ element: HTMLElement | null; moveFocus: boolean }>({ + element: null, + moveFocus: false, + }); + + useEffect(() => { + if (chartRef.current.moveFocus && chartRef.current.element) { + chartRef.current.element.focus(); + } + }, [chart?.hidden]); + + const toggleHideChart = useCallback(() => { + const chartHidden = !chart?.hidden; + chartRef.current.moveFocus = !chartHidden; + onChartHiddenChange?.(chartHidden); + }, [chart?.hidden, onChartHiddenChange]); + + return { + showChartOptionsPopover, + chartRef, + toggleChartOptions, + closeChartOptions, + toggleHideChart, + }; +}; diff --git a/src/plugins/unified_histogram/public/chart/use_chart_panels.test.ts b/src/plugins/unified_histogram/public/chart/use_chart_panels.test.ts index 71e2d3e4a705a..aec3f1a8e291f 100644 --- a/src/plugins/unified_histogram/public/chart/use_chart_panels.test.ts +++ b/src/plugins/unified_histogram/public/chart/use_chart_panels.test.ts @@ -19,7 +19,6 @@ describe('test useChartPanels', () => { closePopover: jest.fn(), onResetChartHeight: jest.fn(), chart: { - status: 'complete', hidden: true, timeInterval: 'auto', }, @@ -39,7 +38,6 @@ describe('test useChartPanels', () => { closePopover: jest.fn(), onResetChartHeight: jest.fn(), chart: { - status: 'complete', hidden: false, timeInterval: 'auto', }, @@ -59,7 +57,6 @@ describe('test useChartPanels', () => { onTimeIntervalChange: jest.fn(), closePopover: jest.fn(), chart: { - status: 'complete', hidden: false, timeInterval: 'auto', }, @@ -78,7 +75,6 @@ describe('test useChartPanels', () => { closePopover: jest.fn(), onResetChartHeight, chart: { - status: 'complete', hidden: false, timeInterval: 'auto', }, diff --git a/src/plugins/unified_histogram/public/chart/use_chart_panels.ts b/src/plugins/unified_histogram/public/chart/use_chart_panels.ts index dd6f162b352f6..8f2874baa624e 100644 --- a/src/plugins/unified_histogram/public/chart/use_chart_panels.ts +++ b/src/plugins/unified_histogram/public/chart/use_chart_panels.ts @@ -23,7 +23,7 @@ export function useChartPanels({ }: { chart?: UnifiedHistogramChartContext; toggleHideChart: () => void; - onTimeIntervalChange: (timeInterval: string) => void; + onTimeIntervalChange?: (timeInterval: string) => void; closePopover: () => void; onResetChartHeight?: () => void; }) { @@ -107,7 +107,7 @@ export function useChartPanels({ label: display, icon: val === chart.timeInterval ? 'check' : 'empty', onClick: () => { - onTimeIntervalChange(val); + onTimeIntervalChange?.(val); closePopover(); }, 'data-test-subj': `unifiedHistogramTimeInterval-${display}`, diff --git a/src/plugins/unified_histogram/public/chart/use_chart_styles.tsx b/src/plugins/unified_histogram/public/chart/use_chart_styles.tsx new file mode 100644 index 0000000000000..c019c7cef981e --- /dev/null +++ b/src/plugins/unified_histogram/public/chart/use_chart_styles.tsx @@ -0,0 +1,75 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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 { useEuiBreakpoint, useEuiTheme } from '@elastic/eui'; +import { css } from '@emotion/react'; + +export const useChartStyles = (chartVisible: boolean) => { + const { euiTheme } = useEuiTheme(); + const resultCountCss = css` + padding: ${euiTheme.size.s} ${euiTheme.size.s} ${chartVisible ? 0 : euiTheme.size.s} + ${euiTheme.size.s}; + min-height: ${euiTheme.base * 2.5}px; + `; + const resultCountInnerCss = css` + ${useEuiBreakpoint(['xs', 's'])} { + align-items: center; + } + `; + const resultCountTitleCss = css` + flex-basis: auto; + + ${useEuiBreakpoint(['xs', 's'])} { + margin-bottom: 0 !important; + } + `; + const resultCountToggleCss = css` + flex-basis: auto; + min-width: 0; + + ${useEuiBreakpoint(['xs', 's'])} { + align-items: flex-end; + } + `; + const histogramCss = css` + flex-grow: 1; + display: flex; + flex-direction: column; + position: relative; + + // SASSTODO: the visualizing component should have an option or a modifier + .series > rect { + fill-opacity: 0.5; + stroke-width: 1; + } + `; + const breakdownFieldSelectorGroupCss = css` + width: 100%; + `; + const breakdownFieldSelectorItemCss = css` + min-width: 0; + align-items: flex-end; + padding-left: ${euiTheme.size.s}; + `; + const chartToolButtonCss = css` + display: flex; + justify-content: center; + padding-left: ${euiTheme.size.s}; + `; + + return { + resultCountCss, + resultCountInnerCss, + resultCountTitleCss, + resultCountToggleCss, + histogramCss, + breakdownFieldSelectorGroupCss, + breakdownFieldSelectorItemCss, + chartToolButtonCss, + }; +}; diff --git a/src/plugins/unified_histogram/public/chart/use_refetch_id.test.ts b/src/plugins/unified_histogram/public/chart/use_refetch_id.test.ts new file mode 100644 index 0000000000000..8835173df2599 --- /dev/null +++ b/src/plugins/unified_histogram/public/chart/use_refetch_id.test.ts @@ -0,0 +1,68 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { DataView } from '@kbn/data-views-plugin/common'; +import { AggregateQuery, Filter, Query, TimeRange } from '@kbn/es-query'; +import { renderHook } from '@testing-library/react-hooks'; +import { + UnifiedHistogramBreakdownContext, + UnifiedHistogramChartContext, + UnifiedHistogramHitsContext, + UnifiedHistogramRequestContext, +} from '../types'; +import { dataViewWithTimefieldMock } from '../__mocks__/data_view_with_timefield'; +import { useRefetchId } from './use_refetch_id'; + +describe('useRefetchId', () => { + const getDeps: () => { + dataView: DataView; + lastReloadRequestTime: number | undefined; + request: UnifiedHistogramRequestContext | undefined; + hits: UnifiedHistogramHitsContext | undefined; + chart: UnifiedHistogramChartContext | undefined; + chartVisible: boolean; + breakdown: UnifiedHistogramBreakdownContext | undefined; + filters: Filter[]; + query: Query | AggregateQuery; + relativeTimeRange: TimeRange; + } = () => ({ + dataView: dataViewWithTimefieldMock, + lastReloadRequestTime: 0, + request: undefined, + hits: undefined, + chart: undefined, + chartVisible: true, + breakdown: undefined, + filters: [], + query: { language: 'kuery', query: '' }, + relativeTimeRange: { from: 'now-15m', to: 'now' }, + }); + + it('should increment the refetchId when any of the arguments change', () => { + const hook = renderHook((props) => useRefetchId(props), { initialProps: getDeps() }); + expect(hook.result.current).toBe(0); + hook.rerender(getDeps()); + expect(hook.result.current).toBe(0); + hook.rerender({ + ...getDeps(), + lastReloadRequestTime: 1, + }); + expect(hook.result.current).toBe(1); + hook.rerender({ + ...getDeps(), + lastReloadRequestTime: 1, + }); + expect(hook.result.current).toBe(1); + hook.rerender({ + ...getDeps(), + lastReloadRequestTime: 1, + query: { language: 'kuery', query: 'foo' }, + }); + expect(hook.result.current).toBe(2); + }); +}); diff --git a/src/plugins/unified_histogram/public/chart/use_refetch_id.ts b/src/plugins/unified_histogram/public/chart/use_refetch_id.ts new file mode 100644 index 0000000000000..4415be9ccd8b6 --- /dev/null +++ b/src/plugins/unified_histogram/public/chart/use_refetch_id.ts @@ -0,0 +1,121 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import type { DataView } from '@kbn/data-views-plugin/common'; +import type { AggregateQuery, Filter, Query, TimeRange } from '@kbn/es-query'; +import { cloneDeep, isEqual } from 'lodash'; +import { useEffect, useRef, useState } from 'react'; +import type { + UnifiedHistogramBreakdownContext, + UnifiedHistogramChartContext, + UnifiedHistogramHitsContext, + UnifiedHistogramRequestContext, +} from '../types'; + +export const useRefetchId = ({ + dataView, + lastReloadRequestTime, + request, + hits, + chart, + chartVisible, + breakdown, + filters, + query, + relativeTimeRange: relativeTimeRange, +}: { + dataView: DataView; + lastReloadRequestTime: number | undefined; + request: UnifiedHistogramRequestContext | undefined; + hits: UnifiedHistogramHitsContext | undefined; + chart: UnifiedHistogramChartContext | undefined; + chartVisible: boolean; + breakdown: UnifiedHistogramBreakdownContext | undefined; + filters: Filter[]; + query: Query | AggregateQuery; + relativeTimeRange: TimeRange; +}) => { + const refetchDeps = useRef>(); + const [refetchId, setRefetchId] = useState(0); + + // When the unified histogram props change, we must compare the current subset + // that should trigger a histogram refetch against the previous subset. If they + // are different, we must refetch the histogram to ensure it's up to date. + useEffect(() => { + const newRefetchDeps = getRefetchDeps({ + dataView, + lastReloadRequestTime, + request, + hits, + chart, + chartVisible, + breakdown, + filters, + query, + relativeTimeRange, + }); + + if (!isEqual(refetchDeps.current, newRefetchDeps)) { + if (refetchDeps.current) { + setRefetchId((id) => id + 1); + } + + refetchDeps.current = newRefetchDeps; + } + }, [ + breakdown, + chart, + chartVisible, + dataView, + filters, + hits, + lastReloadRequestTime, + query, + request, + relativeTimeRange, + ]); + + return refetchId; +}; + +const getRefetchDeps = ({ + dataView, + lastReloadRequestTime, + request, + hits, + chart, + chartVisible, + breakdown, + filters, + query, + relativeTimeRange, +}: { + dataView: DataView; + lastReloadRequestTime: number | undefined; + request: UnifiedHistogramRequestContext | undefined; + hits: UnifiedHistogramHitsContext | undefined; + chart: UnifiedHistogramChartContext | undefined; + chartVisible: boolean; + breakdown: UnifiedHistogramBreakdownContext | undefined; + filters: Filter[]; + query: Query | AggregateQuery; + relativeTimeRange: TimeRange; +}) => + cloneDeep([ + dataView.id, + lastReloadRequestTime, + request?.searchSessionId, + Boolean(hits), + chartVisible, + chart?.timeInterval, + Boolean(breakdown), + breakdown?.field, + filters, + query, + relativeTimeRange, + ]); diff --git a/src/plugins/unified_histogram/public/chart/use_request_params.tsx b/src/plugins/unified_histogram/public/chart/use_request_params.tsx new file mode 100644 index 0000000000000..defa2bdd920d9 --- /dev/null +++ b/src/plugins/unified_histogram/public/chart/use_request_params.tsx @@ -0,0 +1,58 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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 { connectToQueryState, QueryState } from '@kbn/data-plugin/public'; +import { createStateContainer, useContainerState } from '@kbn/kibana-utils-plugin/public'; +import { useEffect, useMemo, useRef } from 'react'; +import type { UnifiedHistogramRequestContext, UnifiedHistogramServices } from '../types'; + +export const useRequestParams = ({ + services, + lastReloadRequestTime, + request, +}: { + services: UnifiedHistogramServices; + lastReloadRequestTime: number | undefined; + request?: UnifiedHistogramRequestContext; +}) => { + const { data } = services; + + const queryStateContainer = useRef( + createStateContainer({ + filters: data.query.filterManager.getFilters(), + query: data.query.queryString.getQuery(), + refreshInterval: data.query.timefilter.timefilter.getRefreshInterval(), + time: data.query.timefilter.timefilter.getTime(), + }) + ).current; + + const queryState = useContainerState(queryStateContainer); + + useEffect(() => { + return connectToQueryState(data.query, queryStateContainer, { + time: true, + query: true, + filters: true, + refreshInterval: true, + }); + }, [data.query, queryStateContainer]); + + const filters = useMemo(() => queryState.filters ?? [], [queryState.filters]); + + const query = useMemo( + () => queryState.query ?? data.query.queryString.getDefaultQuery(), + [data.query.queryString, queryState.query] + ); + + const relativeTimeRange = useMemo( + () => queryState.time ?? data.query.timefilter.timefilter.getTimeDefaults(), + [data.query.timefilter.timefilter, queryState.time] + ); + + return { filters, query, relativeTimeRange }; +}; diff --git a/src/plugins/unified_histogram/public/chart/use_time_range.test.tsx b/src/plugins/unified_histogram/public/chart/use_time_range.test.tsx new file mode 100644 index 0000000000000..26070db1c7e54 --- /dev/null +++ b/src/plugins/unified_histogram/public/chart/use_time_range.test.tsx @@ -0,0 +1,239 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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 { uiSettingsServiceMock } from '@kbn/core-ui-settings-browser-mocks'; +import { TimeRange } from '@kbn/data-plugin/common'; +import { renderHook } from '@testing-library/react-hooks'; +import { UnifiedHistogramBucketInterval } from '../types'; +import { useTimeRange } from './use_time_range'; + +jest.mock('@kbn/datemath', () => ({ + parse: jest.fn((datetime: string) => { + return { + format: jest.fn(() => { + return datetime; + }), + }; + }), +})); + +describe('useTimeRange', () => { + const uiSettings = uiSettingsServiceMock.createStartContract(); + uiSettings.get.mockReturnValue('dateFormat'); + const bucketInterval: UnifiedHistogramBucketInterval = { + description: '1 minute', + }; + const timeRange: TimeRange = { + from: '2022-11-17T00:00:00.000Z', + to: '2022-11-17T12:00:00.000Z', + }; + const timeInterval = 'auto'; + + it('should return time range text', () => { + const { result } = renderHook(() => + useTimeRange({ + uiSettings, + bucketInterval, + timeRange, + timeInterval, + }) + ); + expect(result.current.timeRangeText).toMatchInlineSnapshot( + `"2022-11-17T00:00:00.000Z - 2022-11-17T12:00:00.000Z (interval: Auto - 1 minute)"` + ); + }); + + it('should return time range text when timeInterval is not auto', () => { + const { result } = renderHook(() => + useTimeRange({ + uiSettings, + bucketInterval, + timeRange, + timeInterval: '1m', + }) + ); + expect(result.current.timeRangeText).toMatchInlineSnapshot( + `"2022-11-17T00:00:00.000Z - 2022-11-17T12:00:00.000Z (interval: 1 minute)"` + ); + }); + + it('should return time range text when bucketInterval is undefined', () => { + const { result } = renderHook(() => + useTimeRange({ + uiSettings, + timeRange, + timeInterval, + }) + ); + expect(result.current.timeRangeText).toMatchInlineSnapshot( + `"2022-11-17T00:00:00.000Z - 2022-11-17T12:00:00.000Z (interval: Auto - Loading)"` + ); + }); + + it('should render time range display', () => { + const { result } = renderHook(() => + useTimeRange({ + uiSettings, + bucketInterval, + timeRange, + timeInterval, + }) + ); + expect(result.current.timeRangeDisplay).toMatchInlineSnapshot(` + + 2022-11-17T00:00:00.000Z - 2022-11-17T12:00:00.000Z (interval: Auto - 1 minute) + + `); + }); + + it('should render time range display when buckets are too large', () => { + const { result } = renderHook(() => + useTimeRange({ + uiSettings, + bucketInterval: { + ...bucketInterval, + scaled: true, + scale: 2, + }, + timeRange, + timeInterval, + }) + ); + expect(result.current.timeRangeDisplay).toMatchInlineSnapshot(` + + + + 2022-11-17T00:00:00.000Z - 2022-11-17T12:00:00.000Z (interval: Auto - 1 minute) + + + + + + + `); + }); + + it('should render time range display when there are too many buckets', () => { + const { result } = renderHook(() => + useTimeRange({ + uiSettings, + bucketInterval: { + ...bucketInterval, + scaled: true, + scale: 0.5, + }, + timeRange, + timeInterval, + }) + ); + expect(result.current.timeRangeDisplay).toMatchInlineSnapshot(` + + + + 2022-11-17T00:00:00.000Z - 2022-11-17T12:00:00.000Z (interval: Auto - 1 minute) + + + + + + + `); + }); +}); diff --git a/src/plugins/unified_histogram/public/chart/use_time_range.tsx b/src/plugins/unified_histogram/public/chart/use_time_range.tsx new file mode 100644 index 0000000000000..539f32251b832 --- /dev/null +++ b/src/plugins/unified_histogram/public/chart/use_time_range.tsx @@ -0,0 +1,127 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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 { EuiFlexGroup, EuiFlexItem, EuiIconTip, EuiText, useEuiTheme } from '@elastic/eui'; +import { css } from '@emotion/react'; +import type { IUiSettingsClient } from '@kbn/core-ui-settings-browser'; +import { i18n } from '@kbn/i18n'; +import React, { useCallback, useMemo } from 'react'; +import dateMath from '@kbn/datemath'; +import type { TimeRange } from '@kbn/data-plugin/common'; +import type { UnifiedHistogramBucketInterval } from '../types'; + +export const useTimeRange = ({ + uiSettings, + bucketInterval, + timeRange: { from, to }, + timeInterval, +}: { + uiSettings: IUiSettingsClient; + bucketInterval?: UnifiedHistogramBucketInterval; + timeRange: TimeRange; + timeInterval?: string; +}) => { + const dateFormat = useMemo(() => uiSettings.get('dateFormat'), [uiSettings]); + + const toMoment = useCallback( + (datetime?: moment.Moment) => { + if (!datetime) { + return ''; + } + if (!dateFormat) { + return String(datetime); + } + return datetime.format(dateFormat); + }, + [dateFormat] + ); + + const timeRangeText = useMemo(() => { + const timeRange = { + from: dateMath.parse(from), + to: dateMath.parse(to, { roundUp: true }), + }; + + const intervalText = i18n.translate('unifiedHistogram.histogramTimeRangeIntervalDescription', { + defaultMessage: '(interval: {value})', + values: { + value: `${ + timeInterval === 'auto' + ? `${i18n.translate('unifiedHistogram.histogramTimeRangeIntervalAuto', { + defaultMessage: 'Auto', + })} - ` + : '' + }${ + bucketInterval?.description ?? + i18n.translate('unifiedHistogram.histogramTimeRangeIntervalLoading', { + defaultMessage: 'Loading', + }) + }`, + }, + }); + + return `${toMoment(timeRange.from)} - ${toMoment(timeRange.to)} ${intervalText}`; + }, [bucketInterval, from, timeInterval, to, toMoment]); + + const { euiTheme } = useEuiTheme(); + const timeRangeCss = css` + padding: 0 ${euiTheme.size.s} 0 ${euiTheme.size.s}; + `; + + let timeRangeDisplay = ( + + {timeRangeText} + + ); + + if (bucketInterval?.scaled) { + const toolTipTitle = i18n.translate('unifiedHistogram.timeIntervalWithValueWarning', { + defaultMessage: 'Warning', + }); + + const toolTipContent = i18n.translate('unifiedHistogram.bucketIntervalTooltip', { + defaultMessage: + 'This interval creates {bucketsDescription} to show in the selected time range, so it has been scaled to {bucketIntervalDescription}.', + values: { + bucketsDescription: + bucketInterval.scale && bucketInterval.scale > 1 + ? i18n.translate('unifiedHistogram.bucketIntervalTooltip.tooLargeBucketsText', { + defaultMessage: 'buckets that are too large', + }) + : i18n.translate('unifiedHistogram.bucketIntervalTooltip.tooManyBucketsText', { + defaultMessage: 'too many buckets', + }), + bucketIntervalDescription: bucketInterval.description, + }, + }); + + const timeRangeWrapperCss = css` + flex-grow: 0; + `; + + timeRangeDisplay = ( + + {timeRangeDisplay} + + + + + ); + } + + return { + timeRangeText, + timeRangeDisplay, + }; +}; diff --git a/src/plugins/unified_histogram/public/chart/use_total_hits.test.ts b/src/plugins/unified_histogram/public/chart/use_total_hits.test.ts new file mode 100644 index 0000000000000..4782df3683fcb --- /dev/null +++ b/src/plugins/unified_histogram/public/chart/use_total_hits.test.ts @@ -0,0 +1,212 @@ +/* + * Copyright 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 { Filter } from '@kbn/es-query'; +import { UnifiedHistogramFetchStatus } from '../types'; +import { dataViewWithTimefieldMock } from '../__mocks__/data_view_with_timefield'; +import { useTotalHits } from './use_total_hits'; +import { useEffect as mockUseEffect } from 'react'; +import { renderHook } from '@testing-library/react-hooks'; +import { dataPluginMock } from '@kbn/data-plugin/public/mocks'; +import { searchSourceInstanceMock } from '@kbn/data-plugin/common/search/search_source/mocks'; +import { of, throwError } from 'rxjs'; +import { waitFor } from '@testing-library/dom'; +import { RequestAdapter } from '@kbn/inspector-plugin/common'; +import { DataViewType, SearchSourceSearchOptions } from '@kbn/data-plugin/common'; + +jest.mock('react-use/lib/useDebounce', () => { + return jest.fn((...args) => { + mockUseEffect(args[0], args[2]); + }); +}); + +describe('useTotalHits', () => { + const getDeps = () => ({ + services: { data: dataPluginMock.createStartContract() } as any, + dataView: dataViewWithTimefieldMock, + lastReloadRequestTime: undefined, + request: undefined, + hits: { + status: UnifiedHistogramFetchStatus.uninitialized, + total: undefined, + }, + chart: { + hidden: true, + timeInterval: 'auto', + }, + chartVisible: false, + breakdown: undefined, + filters: [], + query: { query: '', language: 'kuery' }, + timeRange: { from: 'now-15m', to: 'now' }, + refetchId: 0, + onTotalHitsChange: jest.fn(), + }); + + it('should fetch total hits on first execution', async () => { + const onTotalHitsChange = jest.fn(); + let fetchOptions: SearchSourceSearchOptions | undefined; + const fetchSpy = jest + .spyOn(searchSourceInstanceMock, 'fetch$') + .mockClear() + .mockImplementation((options) => { + fetchOptions = options; + return of({ + isRunning: false, + isPartial: false, + rawResponse: { + hits: { + total: 42, + }, + }, + }) as any; + }); + const setFieldSpy = jest.spyOn(searchSourceInstanceMock, 'setField').mockClear(); + const data = dataPluginMock.createStartContract(); + const timeRange = { from: 'now-15m', to: 'now' }; + jest + .spyOn(data.query.timefilter.timefilter, 'createFilter') + .mockClear() + .mockReturnValue(timeRange as any); + const query = { query: 'test query', language: 'kuery' }; + const filters: Filter[] = [{ meta: { index: 'test' }, query: { match_all: {} } }]; + const adapter = new RequestAdapter(); + renderHook(() => + useTotalHits({ + ...getDeps(), + services: { data } as any, + request: { + searchSessionId: '123', + adapter, + }, + query, + filters, + timeRange, + onTotalHitsChange, + }) + ); + expect(onTotalHitsChange).toBeCalledTimes(1); + expect(onTotalHitsChange).toBeCalledWith(UnifiedHistogramFetchStatus.loading, undefined); + expect(setFieldSpy).toHaveBeenCalledWith('index', dataViewWithTimefieldMock); + expect(setFieldSpy).toHaveBeenCalledWith('query', query); + expect(setFieldSpy).toHaveBeenCalledWith('size', 0); + expect(setFieldSpy).toHaveBeenCalledWith('trackTotalHits', true); + expect(setFieldSpy).toHaveBeenCalledWith('filter', [...filters, timeRange]); + expect(fetchSpy).toHaveBeenCalled(); + expect(fetchOptions?.inspector?.adapter).toBe(adapter); + expect(fetchOptions?.sessionId).toBe('123'); + expect(fetchOptions?.abortSignal).toBeInstanceOf(AbortSignal); + expect(fetchOptions?.executionContext?.description).toBe('fetch total hits'); + await waitFor(() => { + expect(onTotalHitsChange).toBeCalledTimes(2); + expect(onTotalHitsChange).toBeCalledWith(UnifiedHistogramFetchStatus.complete, 42); + }); + }); + + it('should not fetch total hits if chartVisible is true', async () => { + const onTotalHitsChange = jest.fn(); + const fetchSpy = jest.spyOn(searchSourceInstanceMock, 'fetch$').mockClear(); + const setFieldSpy = jest.spyOn(searchSourceInstanceMock, 'setField').mockClear(); + renderHook(() => useTotalHits({ ...getDeps(), chartVisible: true, onTotalHitsChange })); + expect(onTotalHitsChange).toBeCalledTimes(0); + expect(setFieldSpy).not.toHaveBeenCalled(); + expect(fetchSpy).not.toHaveBeenCalled(); + }); + + it('should not fetch total hits if hits is undefined', async () => { + const onTotalHitsChange = jest.fn(); + const fetchSpy = jest.spyOn(searchSourceInstanceMock, 'fetch$').mockClear(); + const setFieldSpy = jest.spyOn(searchSourceInstanceMock, 'setField').mockClear(); + renderHook(() => useTotalHits({ ...getDeps(), hits: undefined, onTotalHitsChange })); + expect(onTotalHitsChange).toBeCalledTimes(0); + expect(setFieldSpy).not.toHaveBeenCalled(); + expect(fetchSpy).not.toHaveBeenCalled(); + }); + + it('should not fetch a second time if fetchId is the same', async () => { + const onTotalHitsChange = jest.fn(); + const fetchSpy = jest.spyOn(searchSourceInstanceMock, 'fetch$').mockClear(); + const setFieldSpy = jest.spyOn(searchSourceInstanceMock, 'setField').mockClear(); + const options = { ...getDeps(), refetchId: 0, onTotalHitsChange }; + const { rerender } = renderHook(() => useTotalHits(options)); + expect(onTotalHitsChange).toBeCalledTimes(1); + expect(setFieldSpy).toHaveBeenCalled(); + expect(fetchSpy).toHaveBeenCalled(); + await waitFor(() => { + expect(onTotalHitsChange).toBeCalledTimes(2); + }); + rerender(); + expect(onTotalHitsChange).toBeCalledTimes(2); + expect(setFieldSpy).toHaveBeenCalledTimes(5); + expect(fetchSpy).toHaveBeenCalledTimes(1); + }); + + it('should fetch a second time if fetchId is different', async () => { + const abortSpy = jest.spyOn(AbortController.prototype, 'abort').mockClear(); + const onTotalHitsChange = jest.fn(); + const fetchSpy = jest.spyOn(searchSourceInstanceMock, 'fetch$').mockClear(); + const setFieldSpy = jest.spyOn(searchSourceInstanceMock, 'setField').mockClear(); + const options = { ...getDeps(), refetchId: 0, onTotalHitsChange }; + const { rerender } = renderHook(() => useTotalHits(options)); + expect(onTotalHitsChange).toBeCalledTimes(1); + expect(setFieldSpy).toHaveBeenCalled(); + expect(fetchSpy).toHaveBeenCalled(); + await waitFor(() => { + expect(onTotalHitsChange).toBeCalledTimes(2); + }); + options.refetchId = 1; + rerender(); + expect(abortSpy).toHaveBeenCalled(); + expect(onTotalHitsChange).toBeCalledTimes(3); + expect(setFieldSpy).toHaveBeenCalledTimes(10); + expect(fetchSpy).toHaveBeenCalledTimes(2); + await waitFor(() => { + expect(onTotalHitsChange).toBeCalledTimes(4); + }); + }); + + it('should call onTotalHitsChange with an error status if fetch fails', async () => { + const onTotalHitsChange = jest.fn(); + const error = new Error('test error'); + jest + .spyOn(searchSourceInstanceMock, 'fetch$') + .mockClear() + .mockReturnValue(throwError(() => error)); + renderHook(() => useTotalHits({ ...getDeps(), onTotalHitsChange })); + await waitFor(() => { + expect(onTotalHitsChange).toBeCalledTimes(2); + expect(onTotalHitsChange).toBeCalledWith(UnifiedHistogramFetchStatus.error, error); + }); + }); + + it('should call searchSource.setOverwriteDataViewType if dataView is a rollup', async () => { + const setOverwriteDataViewTypeSpy = jest + .spyOn(searchSourceInstanceMock, 'setOverwriteDataViewType') + .mockClear(); + const setFieldSpy = jest.spyOn(searchSourceInstanceMock, 'setField').mockClear(); + const data = dataPluginMock.createStartContract(); + const timeRange = { from: 'now-15m', to: 'now' }; + jest + .spyOn(data.query.timefilter.timefilter, 'createFilter') + .mockClear() + .mockReturnValue(timeRange as any); + const filters: Filter[] = [{ meta: { index: 'test' }, query: { match_all: {} } }]; + renderHook(() => + useTotalHits({ + ...getDeps(), + dataView: { + ...dataViewWithTimefieldMock, + type: DataViewType.ROLLUP, + } as any, + filters, + }) + ); + expect(setOverwriteDataViewTypeSpy).toHaveBeenCalledWith(undefined); + expect(setFieldSpy).toHaveBeenCalledWith('filter', filters); + }); +}); diff --git a/src/plugins/unified_histogram/public/chart/use_total_hits.ts b/src/plugins/unified_histogram/public/chart/use_total_hits.ts new file mode 100644 index 0000000000000..3f24b642c81bf --- /dev/null +++ b/src/plugins/unified_histogram/public/chart/use_total_hits.ts @@ -0,0 +1,177 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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 { isCompleteResponse } from '@kbn/data-plugin/public'; +import { DataView, DataViewType } from '@kbn/data-views-plugin/public'; +import type { AggregateQuery, Filter, Query, TimeRange } from '@kbn/es-query'; +import { i18n } from '@kbn/i18n'; +import { MutableRefObject, useRef } from 'react'; +import useDebounce from 'react-use/lib/useDebounce'; +import { catchError, filter, lastValueFrom, map, of } from 'rxjs'; +import { + UnifiedHistogramBreakdownContext, + UnifiedHistogramChartContext, + UnifiedHistogramFetchStatus, + UnifiedHistogramHitsContext, + UnifiedHistogramRequestContext, + UnifiedHistogramServices, +} from '../types'; +import { REQUEST_DEBOUNCE_MS } from './consts'; + +export const useTotalHits = ({ + services, + dataView, + lastReloadRequestTime, + request, + hits, + chart, + chartVisible, + breakdown, + filters, + query, + timeRange, + refetchId, + onTotalHitsChange, +}: { + services: UnifiedHistogramServices; + dataView: DataView; + lastReloadRequestTime: number | undefined; + request: UnifiedHistogramRequestContext | undefined; + hits: UnifiedHistogramHitsContext | undefined; + chart: UnifiedHistogramChartContext | undefined; + chartVisible: boolean; + breakdown: UnifiedHistogramBreakdownContext | undefined; + filters: Filter[]; + query: Query | AggregateQuery; + timeRange: TimeRange; + refetchId: number; + onTotalHitsChange?: (status: UnifiedHistogramFetchStatus, result?: number | Error) => void; +}) => { + const abortController = useRef(); + + useDebounce( + () => { + fetchTotalHits({ + services, + abortController, + dataView, + request, + hits, + chartVisible, + filters, + query, + timeRange, + onTotalHitsChange, + }); + }, + REQUEST_DEBOUNCE_MS, + [onTotalHitsChange, refetchId, services] + ); +}; + +const fetchTotalHits = async ({ + services: { data }, + abortController, + dataView, + request, + hits, + chartVisible, + filters: originalFilters, + query, + timeRange, + onTotalHitsChange, +}: { + services: UnifiedHistogramServices; + abortController: MutableRefObject; + dataView: DataView; + request: UnifiedHistogramRequestContext | undefined; + hits: UnifiedHistogramHitsContext | undefined; + chartVisible: boolean; + filters: Filter[]; + query: Query | AggregateQuery; + timeRange: TimeRange; + onTotalHitsChange?: (status: UnifiedHistogramFetchStatus, result?: number | Error) => void; +}) => { + abortController.current?.abort(); + abortController.current = undefined; + + // Either the chart is visible, in which case Lens will make the request, + // or there is no hits context, which means the total hits should be hidden + if (chartVisible || !hits) { + return; + } + + onTotalHitsChange?.(UnifiedHistogramFetchStatus.loading, hits.total); + + const searchSource = data.search.searchSource.createEmpty(); + + searchSource + .setField('index', dataView) + .setField('query', query) + .setField('size', 0) + .setField('trackTotalHits', true); + + let filters = originalFilters; + + if (dataView.type === DataViewType.ROLLUP) { + // We treat that data view as "normal" even if it was a rollup data view, + // since the rollup endpoint does not support querying individual documents, but we + // can get them from the regular _search API that will be used if the data view + // not a rollup data view. + searchSource.setOverwriteDataViewType(undefined); + } else { + // Set the date range filter fields from timeFilter using the absolute format. + // Search sessions requires that it be converted from a relative range + const timeFilter = data.query.timefilter.timefilter.createFilter(dataView, timeRange); + + if (timeFilter) { + filters = [...filters, timeFilter]; + } + } + + searchSource.setField('filter', filters); + + abortController.current = new AbortController(); + + // Let the consumer inspect the request if they want to track it + const inspector = request?.adapter + ? { + adapter: request.adapter, + title: i18n.translate('unifiedHistogram.inspectorRequestDataTitleTotalHits', { + defaultMessage: 'Total hits', + }), + description: i18n.translate('unifiedHistogram.inspectorRequestDescriptionTotalHits', { + defaultMessage: 'This request queries Elasticsearch to fetch the total hits.', + }), + } + : undefined; + + const fetch$ = searchSource + .fetch$({ + inspector, + sessionId: request?.searchSessionId, + abortSignal: abortController.current.signal, + executionContext: { + description: 'fetch total hits', + }, + }) + .pipe( + filter((res) => isCompleteResponse(res)), + map((res) => res.rawResponse.hits.total as number), + catchError((error: Error) => of(error)) + ); + + const result = await lastValueFrom(fetch$); + + const resultStatus = + result instanceof Error + ? UnifiedHistogramFetchStatus.error + : UnifiedHistogramFetchStatus.complete; + + onTotalHitsChange?.(resultStatus, result); +}; diff --git a/src/plugins/unified_histogram/public/hits_counter/hits_counter.test.tsx b/src/plugins/unified_histogram/public/hits_counter/hits_counter.test.tsx index d094fef953af8..03b350448e9c2 100644 --- a/src/plugins/unified_histogram/public/hits_counter/hits_counter.test.tsx +++ b/src/plugins/unified_histogram/public/hits_counter/hits_counter.test.tsx @@ -13,6 +13,7 @@ import type { HitsCounterProps } from './hits_counter'; import { HitsCounter } from './hits_counter'; import { findTestSubject } from '@elastic/eui/lib/test'; import { EuiLoadingSpinner } from '@elastic/eui'; +import { UnifiedHistogramFetchStatus } from '../types'; describe('hits counter', function () { let props: HitsCounterProps; @@ -21,7 +22,7 @@ describe('hits counter', function () { beforeAll(() => { props = { hits: { - status: 'complete', + status: UnifiedHistogramFetchStatus.complete, total: 2, }, }; @@ -35,7 +36,10 @@ describe('hits counter', function () { it('expect to render 1,899 hits if 1899 hits given', function () { component = mountWithIntl( - + ); const hits = findTestSubject(component, 'unifiedHistogramQueryHits'); expect(hits.text()).toBe('1,899'); @@ -48,12 +52,16 @@ describe('hits counter', function () { }); it('should render a EuiLoadingSpinner when status is partial', () => { - component = mountWithIntl(); + component = mountWithIntl( + + ); expect(component.find(EuiLoadingSpinner).length).toBe(1); }); it('should render unifiedHistogramQueryHitsPartial when status is partial', () => { - component = mountWithIntl(); + component = mountWithIntl( + + ); expect(component.find('[data-test-subj="unifiedHistogramQueryHitsPartial"]').length).toBe(1); }); diff --git a/src/plugins/unified_histogram/public/hits_counter/hits_counter.tsx b/src/plugins/unified_histogram/public/hits_counter/hits_counter.tsx index 39df40650557c..b6f1212bfeaed 100644 --- a/src/plugins/unified_histogram/public/hits_counter/hits_counter.tsx +++ b/src/plugins/unified_histogram/public/hits_counter/hits_counter.tsx @@ -37,6 +37,9 @@ export function HitsCounter({ hits, append }: HitsCounterProps) { const hitsCounterCss = css` flex-grow: 0; `; + const hitsCounterTextCss = css` + overflow: hidden; + `; return ( - - + + {hits.status === 'partial' && ( new UnifiedHistogramPublicPlugin(); diff --git a/src/plugins/unified_histogram/public/layout/layout.test.tsx b/src/plugins/unified_histogram/public/layout/layout.test.tsx index 3cb3fb254ecf7..d77bbfa05be30 100644 --- a/src/plugins/unified_histogram/public/layout/layout.test.tsx +++ b/src/plugins/unified_histogram/public/layout/layout.test.tsx @@ -6,13 +6,20 @@ * Side Public License, v 1. */ +import { searchSourceInstanceMock } from '@kbn/data-plugin/common/search/search_source/mocks'; import { mountWithIntl } from '@kbn/test-jest-helpers'; import type { ReactWrapper } from 'enzyme'; import React from 'react'; import { act } from 'react-dom/test-utils'; +import { of } from 'rxjs'; import { Chart } from '../chart'; import { Panels, PANELS_MODE } from '../panels'; -import type { UnifiedHistogramChartContext, UnifiedHistogramHitsContext } from '../types'; +import { + UnifiedHistogramChartContext, + UnifiedHistogramFetchStatus, + UnifiedHistogramHitsContext, +} from '../types'; +import { dataViewWithTimefieldMock } from '../__mocks__/data_view_with_timefield'; import { unifiedHistogramServicesMock } from '../__mocks__/services'; import { UnifiedHistogramLayout, UnifiedHistogramLayoutProps } from './layout'; @@ -30,19 +37,13 @@ jest.mock('@elastic/eui', () => { describe('Layout', () => { const createHits = (): UnifiedHistogramHitsContext => ({ - status: 'complete', + status: UnifiedHistogramFetchStatus.complete, total: 10, }); const createChart = (): UnifiedHistogramChartContext => ({ - status: 'complete', hidden: false, timeInterval: 'auto', - bucketInterval: { - scaled: true, - description: 'test', - scale: 2, - }, }); const mountComponent = async ({ @@ -59,12 +60,21 @@ describe('Layout', () => { return { from: '2020-05-14T11:05:13.590', to: '2020-05-14T11:20:13.590' }; }; + (services.data.query.queryString.getDefaultQuery as jest.Mock).mockReturnValue({ + language: 'kuery', + query: '', + }); + (searchSourceInstanceMock.fetch$ as jest.Mock).mockImplementation( + jest.fn().mockReturnValue(of({ rawResponse: { hits: { total: 2 } } })) + ); + const component = mountWithIntl( ); diff --git a/src/plugins/unified_histogram/public/layout/layout.tsx b/src/plugins/unified_histogram/public/layout/layout.tsx index 229d8a922e465..87d4170a1035f 100644 --- a/src/plugins/unified_histogram/public/layout/layout.tsx +++ b/src/plugins/unified_histogram/public/layout/layout.tsx @@ -11,17 +11,41 @@ import type { PropsWithChildren, ReactElement, RefObject } from 'react'; import React, { useMemo } from 'react'; import { createHtmlPortalNode, InPortal, OutPortal } from 'react-reverse-portal'; import { css } from '@emotion/css'; +import type { DataView, DataViewField } from '@kbn/data-views-plugin/public'; +import type { TypedLensByValueInput } from '@kbn/lens-plugin/public'; import { Chart } from '../chart'; import { Panels, PANELS_MODE } from '../panels'; import type { UnifiedHistogramChartContext, UnifiedHistogramServices, UnifiedHistogramHitsContext, + UnifiedHistogramBreakdownContext, + UnifiedHistogramFetchStatus, + UnifiedHistogramRequestContext, + UnifiedHistogramChartLoadEvent, } from '../types'; export interface UnifiedHistogramLayoutProps extends PropsWithChildren { + /** + * Optional class name to add to the layout container + */ className?: string; + /** + * Required services + */ services: UnifiedHistogramServices; + /** + * The current data view + */ + dataView: DataView; + /** + * Can be updated to `Date.now()` to force a refresh + */ + lastReloadRequestTime?: number; + /** + * Context object for requests made by unified histogram components -- optional + */ + request?: UnifiedHistogramRequestContext; /** * Context object for the hits count -- leave undefined to hide the hits count */ @@ -30,6 +54,10 @@ export interface UnifiedHistogramLayoutProps extends PropsWithChildren * Context object for the chart -- leave undefined to hide the chart */ chart?: UnifiedHistogramChartContext; + /** + * Context object for the breakdown -- leave undefined to hide the breakdown + */ + breakdown?: UnifiedHistogramBreakdownContext; /** * Ref to the element wrapping the layout which will be used for resize calculations */ @@ -49,7 +77,7 @@ export interface UnifiedHistogramLayoutProps extends PropsWithChildren /** * Callback to invoke when the user clicks the edit visualization button -- leave undefined to hide the button */ - onEditVisualization?: () => void; + onEditVisualization?: (lensAttributes: TypedLensByValueInput['attributes']) => void; /** * Callback to hide or show the chart -- should set {@link UnifiedHistogramChartContext.hidden} to chartHidden */ @@ -58,13 +86,30 @@ export interface UnifiedHistogramLayoutProps extends PropsWithChildren * Callback to update the time interval -- should set {@link UnifiedHistogramChartContext.timeInterval} to timeInterval */ onTimeIntervalChange?: (timeInterval: string) => void; + /** + * Callback to update the breakdown field -- should set {@link UnifiedHistogramBreakdownContext.field} to breakdownField + */ + onBreakdownFieldChange?: (breakdownField: DataViewField | undefined) => void; + /** + * Callback to update the total hits -- should set {@link UnifiedHistogramHitsContext.status} to status + * and {@link UnifiedHistogramHitsContext.total} to result + */ + onTotalHitsChange?: (status: UnifiedHistogramFetchStatus, result?: number | Error) => void; + /** + * Called when the histogram loading status changes + */ + onChartLoad?: (event: UnifiedHistogramChartLoadEvent) => void; } export const UnifiedHistogramLayout = ({ className, services, + dataView, + lastReloadRequestTime, + request, hits, chart, + breakdown, resizeRef, topPanelHeight, appendHitsCounter, @@ -72,6 +117,9 @@ export const UnifiedHistogramLayout = ({ onEditVisualization, onChartHiddenChange, onTimeIntervalChange, + onBreakdownFieldChange, + onTotalHitsChange, + onChartLoad, children, }: UnifiedHistogramLayoutProps) => { const topPanelNode = useMemo( @@ -88,7 +136,6 @@ export const UnifiedHistogramLayout = ({ const showFixedPanels = isMobile || !chart || chart.hidden; const { euiTheme } = useEuiTheme(); const defaultTopPanelHeight = euiTheme.base * 12; - const minTopPanelHeight = euiTheme.base * 8; const minMainPanelHeight = euiTheme.base * 10; const chartClassName = @@ -119,14 +166,21 @@ export const UnifiedHistogramLayout = ({ : } onEditVisualization={onEditVisualization} onResetChartHeight={onResetChartHeight} onChartHiddenChange={onChartHiddenChange} onTimeIntervalChange={onTimeIntervalChange} + onBreakdownFieldChange={onBreakdownFieldChange} + onTotalHitsChange={onTotalHitsChange} + onChartLoad={onChartLoad} /> {children} @@ -135,7 +189,7 @@ export const UnifiedHistogramLayout = ({ mode={panelsMode} resizeRef={resizeRef} topPanelHeight={currentTopPanelHeight} - minTopPanelHeight={minTopPanelHeight} + minTopPanelHeight={defaultTopPanelHeight} minMainPanelHeight={minMainPanelHeight} topPanel={} mainPanel={} diff --git a/src/plugins/unified_histogram/public/types.ts b/src/plugins/unified_histogram/public/types.ts index 53f81b0819900..a4b253274abde 100644 --- a/src/plugins/unified_histogram/public/types.ts +++ b/src/plugins/unified_histogram/public/types.ts @@ -10,19 +10,21 @@ import type { Theme } from '@kbn/charts-plugin/public/plugin'; import type { IUiSettingsClient } from '@kbn/core/public'; import type { DataPublicPluginStart } from '@kbn/data-plugin/public'; import type { FieldFormatsStart } from '@kbn/field-formats-plugin/public'; -import type { Duration, Moment } from 'moment'; -import type { Unit } from '@kbn/datemath'; -import type { SerializedFieldFormat } from '@kbn/field-formats-plugin/common'; +import type { LensPublicStart } from '@kbn/lens-plugin/public'; +import type { DataViewField } from '@kbn/data-views-plugin/public'; +import type { RequestAdapter } from '@kbn/inspector-plugin/public'; +import type { DefaultInspectorAdapters } from '@kbn/expressions-plugin/common'; /** * The fetch status of a unified histogram request */ -export type UnifiedHistogramFetchStatus = - | 'uninitialized' - | 'loading' - | 'partial' - | 'complete' - | 'error'; +export enum UnifiedHistogramFetchStatus { + uninitialized = 'uninitialized', + loading = 'loading', + partial = 'partial', + complete = 'complete', + error = 'error', +} /** * The services required by the unified histogram components @@ -32,81 +34,46 @@ export interface UnifiedHistogramServices { theme: Theme; uiSettings: IUiSettingsClient; fieldFormats: FieldFormatsStart; + lens: LensPublicStart; } -interface Column { - id: string; - name: string; -} - -interface Row { - [key: string]: number | 'NaN'; -} - -interface Dimension { - accessor: 0 | 1; - format: SerializedFieldFormat<{ pattern: string }>; - label: string; -} - -interface Ordered { - date: true; - interval: Duration; - intervalESUnit: string; - intervalESValue: number; - min: Moment; - max: Moment; -} - -interface HistogramParams { - date: true; - interval: Duration; - intervalESValue: number; - intervalESUnit: Unit; - format: string; - bounds: HistogramParamsBounds; -} - -export interface HistogramParamsBounds { - min: Moment; - max: Moment; -} - -export interface Table { - columns: Column[]; - rows: Row[]; +/** + * The bucketInterval object returned by {@link buildBucketInterval} + */ +export interface UnifiedHistogramBucketInterval { + scaled?: boolean; + description?: string; + scale?: number; } -export interface Dimensions { - x: Dimension & { params: HistogramParams }; - y: Dimension; -} +export type UnifiedHistogramAdapters = Partial; /** - * The chartData object returned by {@link buildChartData} that - * should be used to set {@link UnifiedHistogramChartContext.data} + * Emitted when the histogram loading status changes */ -export interface UnifiedHistogramChartData { - values: Array<{ - x: number; - y: number; - }>; - xAxisOrderedValues: number[]; - xAxisFormat: Dimension['format']; - yAxisFormat: Dimension['format']; - xAxisLabel: Column['name']; - yAxisLabel?: Column['name']; - ordered: Ordered; +export interface UnifiedHistogramChartLoadEvent { + /** + * True if loading is complete + */ + complete: boolean; + /** + * Inspector adapters for the request + */ + adapters: UnifiedHistogramAdapters; } /** - * The bucketInterval object returned by {@link buildChartData} that - * should be used to set {@link UnifiedHistogramChartContext.bucketInterval} + * Context object for requests made by unified histogram components */ -export interface UnifiedHistogramBucketInterval { - scaled?: boolean; - description?: string; - scale?: number; +export interface UnifiedHistogramRequestContext { + /** + * Current search session ID + */ + searchSessionId?: string; + /** + * The adapter to use for requests (does not apply to Lens requests) + */ + adapter?: RequestAdapter; } /** @@ -116,7 +83,7 @@ export interface UnifiedHistogramHitsContext { /** * The fetch status of the hits count request */ - status: UnifiedHistogramFetchStatus; + status?: UnifiedHistogramFetchStatus; /** * The total number of hits */ @@ -127,10 +94,6 @@ export interface UnifiedHistogramHitsContext { * Context object for the chart */ export interface UnifiedHistogramChartContext { - /** - * The fetch status of the chart request - */ - status: UnifiedHistogramFetchStatus; /** * Controls whether or not the chart is hidden */ @@ -140,15 +103,17 @@ export interface UnifiedHistogramChartContext { */ timeInterval?: string; /** - * The bucketInterval object returned by {@link buildChartData} - */ - bucketInterval?: UnifiedHistogramBucketInterval; - /** - * The chartData object returned by {@link buildChartData} + * The chart title -- sets the title property on the Lens chart input */ - data?: UnifiedHistogramChartData; + title?: string; +} + +/** + * Context object for the histogram breakdown + */ +export interface UnifiedHistogramBreakdownContext { /** - * Error from failed chart request + * The field used for the breakdown */ - error?: Error; + field?: DataViewField; } diff --git a/src/plugins/unified_histogram/tsconfig.json b/src/plugins/unified_histogram/tsconfig.json index a275fdc784dbc..9c6213783980c 100644 --- a/src/plugins/unified_histogram/tsconfig.json +++ b/src/plugins/unified_histogram/tsconfig.json @@ -5,12 +5,13 @@ "emitDeclarationOnly": true, "declaration": true, }, - "include": ["common/**/*", "public/**/*", "server/**/*"], + "include": [ "../../../typings/**/*", "common/**/*", "public/**/*", "server/**/*"], "kbn_references": [ { "path": "../../core/tsconfig.json" }, { "path": "../charts/tsconfig.json" }, { "path": "../data/tsconfig.json" }, { "path": "../data_views/tsconfig.json" }, - { "path": "../saved_search/tsconfig.json" } + { "path": "../saved_search/tsconfig.json" }, + { "path": "../../../x-pack/plugins/lens/tsconfig.json" } ] } diff --git a/src/plugins/unified_search/public/dataview_picker/explore_matching_button.tsx b/src/plugins/unified_search/public/dataview_picker/explore_matching_button.tsx index db98916b1a080..26007ca9be712 100644 --- a/src/plugins/unified_search/public/dataview_picker/explore_matching_button.tsx +++ b/src/plugins/unified_search/public/dataview_picker/explore_matching_button.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiSpacer, useEuiTheme } from '@elastic/eui'; -import { css } from '@emotion/css'; +import { css } from '@emotion/react'; import { i18n } from '@kbn/i18n'; interface ExploreMatchingButtonProps { diff --git a/src/plugins/unified_search/public/query_string_input/no_data_popover.test.tsx b/src/plugins/unified_search/public/query_string_input/no_data_popover.test.tsx index f80fc68494091..8d41fac7ad29e 100644 --- a/src/plugins/unified_search/public/query_string_input/no_data_popover.test.tsx +++ b/src/plugins/unified_search/public/query_string_input/no_data_popover.test.tsx @@ -75,7 +75,7 @@ describe('NoDataPopover', () => { }; const instance = mount(); act(() => { - instance.find(EuiTourStep).prop('footerAction')!.props.onClick(); + instance.find('button[data-test-subj="noDataPopoverDismissButton"]').simulate('click'); }); instance.setProps({ ...props }); expect(props.storage.set).toHaveBeenCalledWith(expect.any(String), true); diff --git a/src/plugins/unified_search/public/query_string_input/query_string_input.tsx b/src/plugins/unified_search/public/query_string_input/query_string_input.tsx index cc4e934b1270e..4f6737cec44a0 100644 --- a/src/plugins/unified_search/public/query_string_input/query_string_input.tsx +++ b/src/plugins/unified_search/public/query_string_input/query_string_input.tsx @@ -31,11 +31,7 @@ import { compact, debounce, isEmpty, isEqual, isFunction, partition } from 'loda import { CoreStart, DocLinksStart, Toast } from '@kbn/core/public'; import type { Query } from '@kbn/es-query'; import { DataPublicPluginStart, getQueryLog } from '@kbn/data-plugin/public'; -import { - type DataView, - DataView as KibanaDataView, - DataViewsPublicPluginStart, -} from '@kbn/data-views-plugin/public'; +import { type DataView, DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public'; import type { PersistedLog } from '@kbn/data-plugin/public'; import { getFieldSubtypeNested, KIBANA_USER_QUERY_LANGUAGE_KEY } from '@kbn/data-plugin/common'; import { toMountPoint } from '@kbn/kibana-react-plugin/public'; @@ -188,7 +184,7 @@ export default class QueryStringInputUI extends PureComponent(this.props.indexPatterns || [], (indexPattern): indexPattern is DataView => { - return indexPattern instanceof KibanaDataView; + return indexPattern.hasOwnProperty('fields') && indexPattern.hasOwnProperty('title'); }); const idOrTitlePatterns = stringPatterns.map((sp) => typeof sp === 'string' ? { type: 'title', value: sp } : sp diff --git a/src/plugins/unified_search/public/query_string_input/text_based_languages_editor/index.tsx b/src/plugins/unified_search/public/query_string_input/text_based_languages_editor/index.tsx index 88140ef3d46b3..ab9e8b0374783 100644 --- a/src/plugins/unified_search/public/query_string_input/text_based_languages_editor/index.tsx +++ b/src/plugins/unified_search/public/query_string_input/text_based_languages_editor/index.tsx @@ -8,7 +8,7 @@ import React, { useRef, memo, useEffect, useState, useCallback } from 'react'; import classNames from 'classnames'; -import { EsqlLang, monaco } from '@kbn/monaco'; +import { SQLLang, monaco } from '@kbn/monaco'; import type { AggregateQuery } from '@kbn/es-query'; import { getAggregateQueryMode } from '@kbn/es-query'; import { @@ -72,7 +72,7 @@ const languageId = (language: string) => { switch (language) { case 'sql': default: { - return EsqlLang.ID; + return SQLLang.ID; } } }; diff --git a/src/plugins/unified_search/server/autocomplete/autocomplete_service.ts b/src/plugins/unified_search/server/autocomplete/autocomplete_service.ts index 8ab86f8a05d90..d1969c381a7ce 100644 --- a/src/plugins/unified_search/server/autocomplete/autocomplete_service.ts +++ b/src/plugins/unified_search/server/autocomplete/autocomplete_service.ts @@ -7,6 +7,7 @@ */ import moment from 'moment'; +import { clone } from 'lodash'; import { CoreSetup, Plugin, PluginInitializerContext } from '@kbn/core/server'; import { registerRoutes } from './routes'; import { ConfigSchema } from '../../config'; @@ -32,6 +33,7 @@ export class AutocompleteService implements Plugin { terminateAfter: moment.duration(terminateAfter).asMilliseconds(), timeout: moment.duration(timeout).asMilliseconds(), }), + getInitializerContextConfig: () => clone(this.initializerContext.config), }; } diff --git a/src/plugins/unified_search/server/mocks.ts b/src/plugins/unified_search/server/mocks.ts index 7f63abcdaae0c..48708e5f2955e 100644 --- a/src/plugins/unified_search/server/mocks.ts +++ b/src/plugins/unified_search/server/mocks.ts @@ -6,10 +6,32 @@ * Side Public License, v 1. */ +import moment from 'moment'; +import { Observable } from 'rxjs'; +import { ConfigSchema } from '../config'; import { AutocompleteSetup } from './autocomplete'; const autocompleteSetupMock: jest.Mocked = { getAutocompleteSettings: jest.fn(), + // @ts-ignore as it is partially defined because not all fields are needed + getInitializerContextConfig: jest.fn(() => ({ + create: jest.fn( + () => + new Observable((subscribe) => + subscribe.next({ + autocomplete: { + querySuggestions: { enabled: true }, + valueSuggestions: { + enabled: true, + tiers: [], + terminateAfter: moment.duration(), + timeout: moment.duration(), + }, + }, + }) + ) + ), + })), }; function createSetupContract() { diff --git a/src/plugins/vis_types/heatmap/public/convert_to_lens/configurations/index.test.ts b/src/plugins/vis_types/heatmap/public/convert_to_lens/configurations/index.test.ts index 3f60b6fde0a94..523ed9ddba21c 100644 --- a/src/plugins/vis_types/heatmap/public/convert_to_lens/configurations/index.test.ts +++ b/src/plugins/vis_types/heatmap/public/convert_to_lens/configurations/index.test.ts @@ -90,7 +90,7 @@ describe('getConfiguration', () => { progression: 'fixed', rangeMax: 100, rangeMin: 0, - rangeType: 'number', + rangeType: 'percent', reverse: false, stops: [ { color: '#F7FBFF', stop: 12.5 }, diff --git a/src/plugins/vis_types/heatmap/public/convert_to_lens/configurations/palette.ts b/src/plugins/vis_types/heatmap/public/convert_to_lens/configurations/palette.ts index 32187e184d4ef..c92b3780796e9 100644 --- a/src/plugins/vis_types/heatmap/public/convert_to_lens/configurations/palette.ts +++ b/src/plugins/vis_types/heatmap/public/convert_to_lens/configurations/palette.ts @@ -37,13 +37,14 @@ export const getPaletteForHeatmap = async (params: HeatmapVisParams) => { true ); const colorsRange: Range[] = [{ from: stop[0], to: stop[stop.length - 1], type: 'range' }]; - const { colorSchema, invertColors, percentageMode } = params; + const { colorSchema, invertColors } = params; + // palette is type of percent, if user wants dynamic calulated ranges const percentageModeConfig = getPercentageModeConfig( { colorsRange, colorSchema, invertColors, - percentageMode, + percentageMode: true, }, false ); diff --git a/src/plugins/vis_types/timeseries/public/application/components/lib/replace_vars.ts b/src/plugins/vis_types/timeseries/public/application/components/lib/replace_vars.ts index d3a63f9f2421d..77ac817d53d28 100644 --- a/src/plugins/vis_types/timeseries/public/application/components/lib/replace_vars.ts +++ b/src/plugins/vis_types/timeseries/public/application/components/lib/replace_vars.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { encode, RisonValue } from 'rison-node'; +import { encode } from '@kbn/rison'; import Handlebars, { ExtendedCompileOptions, compileFnName } from '@kbn/handlebars'; import { i18n } from '@kbn/i18n'; import { emptyLabel } from '../../../../common/empty_label'; @@ -41,7 +41,7 @@ function createSerializationHelper( handlebars.registerHelper( 'rison', - createSerializationHelper('rison', (v) => encode(v as RisonValue)) + createSerializationHelper('rison', (v) => encode(v)) ); handlebars.registerHelper('encodeURIComponent', (component: unknown) => { diff --git a/src/plugins/visualizations/common/locator_location.ts b/src/plugins/visualizations/common/locator_location.ts index c4c86007fd124..e43fc9fcbd5e5 100644 --- a/src/plugins/visualizations/common/locator_location.ts +++ b/src/plugins/visualizations/common/locator_location.ts @@ -10,7 +10,7 @@ import type { Serializable } from '@kbn/utility-types'; import { omitBy } from 'lodash'; import type { ParsedQuery } from 'query-string'; import { stringify } from 'query-string'; -import rison from 'rison-node'; +import rison from '@kbn/rison'; import { isFilterPinned } from '@kbn/es-query'; import { url } from '@kbn/kibana-utils-plugin/common'; import { GLOBAL_STATE_STORAGE_KEY, STATE_STORAGE_KEY, VisualizeConstants } from './constants'; diff --git a/src/plugins/visualizations/public/wizard/agg_based_selection/agg_based_selection.tsx b/src/plugins/visualizations/public/wizard/agg_based_selection/agg_based_selection.tsx index d4d2c1505bf8a..8ee63361321ef 100644 --- a/src/plugins/visualizations/public/wizard/agg_based_selection/agg_based_selection.tsx +++ b/src/plugins/visualizations/public/wizard/agg_based_selection/agg_based_selection.tsx @@ -57,10 +57,12 @@ class AggBasedSelection extends React.Component - +

    + +

    diff --git a/src/plugins/visualizations/public/wizard/group_selection/group_selection.tsx b/src/plugins/visualizations/public/wizard/group_selection/group_selection.tsx index 6bc982a824de1..908c1a621ebc8 100644 --- a/src/plugins/visualizations/public/wizard/group_selection/group_selection.tsx +++ b/src/plugins/visualizations/public/wizard/group_selection/group_selection.tsx @@ -67,10 +67,12 @@ function GroupSelection(props: GroupSelectionProps) { <> - +

    + +

    diff --git a/src/plugins/visualizations/public/wizard/search_selection/search_selection.tsx b/src/plugins/visualizations/public/wizard/search_selection/search_selection.tsx index 5771747794273..89814c0a474a1 100644 --- a/src/plugins/visualizations/public/wizard/search_selection/search_selection.tsx +++ b/src/plugins/visualizations/public/wizard/search_selection/search_selection.tsx @@ -33,16 +33,18 @@ export class SearchSelection extends React.Component { - {' '} - /{' '} - +

    + {' '} + /{' '} + +

    diff --git a/test/api_integration/apis/data_views/fields_for_wildcard_route/params.js b/test/api_integration/apis/data_views/fields_for_wildcard_route/params.js index 5b82473721be8..b91c307cc1743 100644 --- a/test/api_integration/apis/data_views/fields_for_wildcard_route/params.js +++ b/test/api_integration/apis/data_views/fields_for_wildcard_route/params.js @@ -31,6 +31,15 @@ export default function ({ getService }) { }) .expect(200)); + it('accepts include_unmapped param', () => + supertest + .get('/api/index_patterns/_fields_for_wildcard') + .query({ + pattern: '*', + include_unmapped: true, + }) + .expect(200)); + it('accepts meta_fields query param in string array', () => supertest .get('/api/index_patterns/_fields_for_wildcard') diff --git a/test/api_integration/apis/guided_onboarding/get_config.ts b/test/api_integration/apis/guided_onboarding/get_config.ts new file mode 100644 index 0000000000000..fc96cb81c3816 --- /dev/null +++ b/test/api_integration/apis/guided_onboarding/get_config.ts @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import expect from '@kbn/expect'; +import type { FtrProviderContext } from '../../ftr_provider_context'; + +const getConfigsPath = '/api/guided_onboarding/configs'; +export default function testGetGuidesState({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + + describe('GET /api/guided_onboarding/configs', () => { + // check that all guides are present + ['testGuide', 'security', 'search', 'observability'].map((guideId) => { + it(`returns config for ${guideId}`, async () => { + const response = await supertest.get(`${getConfigsPath}/${guideId}`).expect(200); + expect(response.body).not.to.be.empty(); + const { config } = response.body; + expect(config).to.not.be.empty(); + }); + }); + }); +} diff --git a/test/api_integration/apis/guided_onboarding/index.ts b/test/api_integration/apis/guided_onboarding/index.ts index c924eafe6bdb1..b2b3c23705763 100644 --- a/test/api_integration/apis/guided_onboarding/index.ts +++ b/test/api_integration/apis/guided_onboarding/index.ts @@ -13,5 +13,6 @@ export default function apiIntegrationTests({ loadTestFile }: FtrProviderContext loadTestFile(require.resolve('./get_state')); loadTestFile(require.resolve('./put_state')); loadTestFile(require.resolve('./get_guides')); + loadTestFile(require.resolve('./get_config')); }); } diff --git a/test/functional/apps/context/_context_navigation.ts b/test/functional/apps/context/_context_navigation.ts index 2451e351b6d9c..5c6304bc36cee 100644 --- a/test/functional/apps/context/_context_navigation.ts +++ b/test/functional/apps/context/_context_navigation.ts @@ -31,8 +31,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { expect(await filterBar.hasFilter(columnName, value, true)).to.eql(true); } expect(await PageObjects.timePicker.getTimeConfigAsAbsoluteTimes()).to.eql({ - start: 'Sep 18, 2015 @ 06:31:44.000', - end: 'Sep 23, 2015 @ 18:31:44.000', + start: PageObjects.timePicker.defaultStartTime, + end: PageObjects.timePicker.defaultEndTime, }); }; diff --git a/test/functional/apps/discover/group1/_discover.ts b/test/functional/apps/discover/group1/_discover.ts index 60f2a54dd01fd..1cba5aa4812d8 100644 --- a/test/functional/apps/discover/group1/_discover.ts +++ b/test/functional/apps/discover/group1/_discover.ts @@ -18,7 +18,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const kibanaServer = getService('kibanaServer'); const queryBar = getService('queryBar'); const inspector = getService('inspector'); - const elasticChart = getService('elasticChart'); const testSubjects = getService('testSubjects'); const PageObjects = getPageObjects(['common', 'discover', 'header', 'timePicker']); @@ -106,48 +105,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { ); }); - it('should modify the time range when the histogram is brushed', async function () { - // this is the number of renderings of the histogram needed when new data is fetched - // this needs to be improved - const renderingCountInc = 2; - const prevRenderingCount = await elasticChart.getVisualizationRenderingCount(); - await PageObjects.timePicker.setDefaultAbsoluteRange(); - await PageObjects.discover.waitUntilSearchingHasFinished(); - await retry.waitFor('chart rendering complete', async () => { - const actualCount = await elasticChart.getVisualizationRenderingCount(); - const expectedCount = prevRenderingCount + renderingCountInc; - log.debug( - `renderings before brushing - actual: ${actualCount} expected: ${expectedCount}` - ); - return actualCount === expectedCount; - }); - let prevRowData = ''; - // to make sure the table is already rendered - await retry.try(async () => { - prevRowData = await PageObjects.discover.getDocTableField(1); - log.debug(`The first timestamp value in doc table before brushing: ${prevRowData}`); - }); - - await PageObjects.discover.brushHistogram(); - await PageObjects.discover.waitUntilSearchingHasFinished(); - await retry.waitFor('chart rendering complete after being brushed', async () => { - const actualCount = await elasticChart.getVisualizationRenderingCount(); - const expectedCount = prevRenderingCount + renderingCountInc * 2; - log.debug( - `renderings after brushing - actual: ${actualCount} expected: ${expectedCount}` - ); - return actualCount === expectedCount; - }); - const newDurationHours = await PageObjects.timePicker.getTimeDurationInHours(); - expect(Math.round(newDurationHours)).to.be(26); - - await retry.waitFor('doc table containing the documents of the brushed range', async () => { - const rowData = await PageObjects.discover.getDocTableField(1); - log.debug(`The first timestamp value in doc table after brushing: ${rowData}`); - return prevRowData !== rowData; - }); - }); - it('should show correct initial chart interval of Auto', async function () { await PageObjects.timePicker.setDefaultAbsoluteRange(); await PageObjects.discover.waitUntilSearchingHasFinished(); @@ -265,26 +222,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); }); - describe('empty query', function () { - it('should update the histogram timerange when the query is resubmitted', async function () { - await kibanaServer.uiSettings.update({ - 'timepicker:timeDefaults': '{ "from": "2015-09-18T19:37:13.000Z", "to": "now"}', - }); - await PageObjects.common.navigateToApp('discover'); - await PageObjects.header.awaitKibanaChrome(); - const initialTimeString = await PageObjects.discover.getChartTimespan(); - await queryBar.submitQuery(); - - await retry.waitFor('chart timespan to have changed', async () => { - const refreshedTimeString = await PageObjects.discover.getChartTimespan(); - log.debug( - `Timestamp before: ${initialTimeString}, Timestamp after: ${refreshedTimeString}` - ); - return refreshedTimeString !== initialTimeString; - }); - }); - }); - describe('managing fields', function () { it('should add a field, sort by it, remove it and also sorting by it', async function () { await PageObjects.timePicker.setDefaultAbsoluteRangeViaUiSettings(); diff --git a/test/functional/apps/discover/group1/_discover_histogram.ts b/test/functional/apps/discover/group1/_discover_histogram.ts index 12effb75cb7f3..70a1fba5afafc 100644 --- a/test/functional/apps/discover/group1/_discover_histogram.ts +++ b/test/functional/apps/discover/group1/_discover_histogram.ts @@ -23,6 +23,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const testSubjects = getService('testSubjects'); const browser = getService('browser'); const retry = getService('retry'); + const log = getService('log'); + const queryBar = getService('queryBar'); describe('discover histogram', function describeIndexTests() { before(async () => { @@ -52,6 +54,69 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { } } + it('should modify the time range when the histogram is brushed', async function () { + await PageObjects.common.navigateToApp('discover'); + await PageObjects.discover.waitUntilSearchingHasFinished(); + await PageObjects.timePicker.setDefaultAbsoluteRange(); + await PageObjects.discover.waitUntilSearchingHasFinished(); + // this is the number of renderings of the histogram needed when new data is fetched + let renderingCountInc = 1; + const prevRenderingCount = await elasticChart.getVisualizationRenderingCount(); + await PageObjects.timePicker.setDefaultAbsoluteRange(); + await PageObjects.discover.waitUntilSearchingHasFinished(); + await retry.waitFor('chart rendering complete', async () => { + const actualCount = await elasticChart.getVisualizationRenderingCount(); + const expectedCount = prevRenderingCount + renderingCountInc; + log.debug(`renderings before brushing - actual: ${actualCount} expected: ${expectedCount}`); + return actualCount === expectedCount; + }); + let prevRowData = ''; + // to make sure the table is already rendered + await retry.try(async () => { + prevRowData = await PageObjects.discover.getDocTableField(1); + log.debug(`The first timestamp value in doc table before brushing: ${prevRowData}`); + }); + + await PageObjects.discover.brushHistogram(); + await PageObjects.discover.waitUntilSearchingHasFinished(); + renderingCountInc = 2; + await retry.waitFor('chart rendering complete after being brushed', async () => { + const actualCount = await elasticChart.getVisualizationRenderingCount(); + const expectedCount = prevRenderingCount + renderingCountInc * 2; + log.debug(`renderings after brushing - actual: ${actualCount} expected: ${expectedCount}`); + return actualCount <= expectedCount; + }); + const newDurationHours = await PageObjects.timePicker.getTimeDurationInHours(); + expect(Math.round(newDurationHours)).to.be(26); + + await retry.waitFor('doc table containing the documents of the brushed range', async () => { + const rowData = await PageObjects.discover.getDocTableField(1); + log.debug(`The first timestamp value in doc table after brushing: ${rowData}`); + return prevRowData !== rowData; + }); + }); + + it('should update the histogram timerange when the query is resubmitted', async function () { + await kibanaServer.uiSettings.update({ + 'timepicker:timeDefaults': '{ "from": "2015-09-18T19:37:13.000Z", "to": "now"}', + }); + await PageObjects.common.navigateToApp('discover'); + await PageObjects.header.awaitKibanaChrome(); + const initialTimeString = await PageObjects.discover.getChartTimespan(); + await queryBar.clickQuerySubmitButton(); + await PageObjects.discover.waitUntilSearchingHasFinished(); + + await retry.waitFor('chart timespan to have changed', async () => { + const refreshedTimeString = await PageObjects.discover.getChartTimespan(); + await queryBar.clickQuerySubmitButton(); + await PageObjects.discover.waitUntilSearchingHasFinished(); + log.debug( + `Timestamp before: ${initialTimeString}, Timestamp after: ${refreshedTimeString}` + ); + return refreshedTimeString !== initialTimeString; + }); + }); + it('should visualize monthly data with different day intervals', async () => { const from = 'Nov 1, 2017 @ 00:00:00.000'; const to = 'Mar 21, 2018 @ 00:00:00.000'; @@ -83,8 +148,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { expect(canvasExists).to.be(true); await testSubjects.click('unifiedHistogramChartOptionsToggle'); await testSubjects.click('unifiedHistogramChartToggle'); - canvasExists = await elasticChart.canvasExists(); - expect(canvasExists).to.be(false); + await retry.try(async () => { + canvasExists = await elasticChart.canvasExists(); + expect(canvasExists).to.be(false); + }); // histogram is hidden, when reloading the page it should remain hidden await browser.refresh(); canvasExists = await elasticChart.canvasExists(); @@ -92,8 +159,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await testSubjects.click('unifiedHistogramChartOptionsToggle'); await testSubjects.click('unifiedHistogramChartToggle'); await PageObjects.header.waitUntilLoadingHasFinished(); - canvasExists = await elasticChart.canvasExists(); - expect(canvasExists).to.be(true); + await retry.try(async () => { + canvasExists = await elasticChart.canvasExists(); + expect(canvasExists).to.be(true); + }); }); it('should allow hiding the histogram, persisted in saved search', async () => { const from = 'Jan 1, 2010 @ 00:00:00.000'; @@ -104,8 +173,11 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { // close chart for saved search await testSubjects.click('unifiedHistogramChartOptionsToggle'); await testSubjects.click('unifiedHistogramChartToggle'); - let canvasExists = await elasticChart.canvasExists(); - expect(canvasExists).to.be(false); + let canvasExists: boolean; + await retry.try(async () => { + canvasExists = await elasticChart.canvasExists(); + expect(canvasExists).to.be(false); + }); // save search await PageObjects.discover.saveSearch(savedSearch); @@ -147,8 +219,11 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { // close chart await testSubjects.click('unifiedHistogramChartOptionsToggle'); await testSubjects.click('unifiedHistogramChartToggle'); - let canvasExists = await elasticChart.canvasExists(); - expect(canvasExists).to.be(false); + let canvasExists: boolean; + await retry.try(async () => { + canvasExists = await elasticChart.canvasExists(); + expect(canvasExists).to.be(false); + }); // save search await PageObjects.discover.saveSearch('persisted hidden histogram'); @@ -157,8 +232,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { // open chart await testSubjects.click('unifiedHistogramChartOptionsToggle'); await testSubjects.click('unifiedHistogramChartToggle'); - canvasExists = await elasticChart.canvasExists(); - expect(canvasExists).to.be(true); + await retry.try(async () => { + canvasExists = await elasticChart.canvasExists(); + expect(canvasExists).to.be(true); + }); // go to dashboard await PageObjects.common.navigateToApp('dashboard'); @@ -173,8 +250,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { // close chart await testSubjects.click('unifiedHistogramChartOptionsToggle'); await testSubjects.click('unifiedHistogramChartToggle'); - canvasExists = await elasticChart.canvasExists(); - expect(canvasExists).to.be(false); + await retry.try(async () => { + canvasExists = await elasticChart.canvasExists(); + expect(canvasExists).to.be(false); + }); }); }); } diff --git a/test/functional/apps/discover/group1/_discover_histogram_breakdown.ts b/test/functional/apps/discover/group1/_discover_histogram_breakdown.ts new file mode 100644 index 0000000000000..805d59aee937d --- /dev/null +++ b/test/functional/apps/discover/group1/_discover_histogram_breakdown.ts @@ -0,0 +1,61 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../ftr_provider_context'; + +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const esArchiver = getService('esArchiver'); + const kibanaServer = getService('kibanaServer'); + const filterBar = getService('filterBar'); + const PageObjects = getPageObjects(['common', 'discover', 'header', 'timePicker']); + + describe('discover unified histogram breakdown', function describeIndexTests() { + before(async () => { + await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/logstash_functional'); + await kibanaServer.importExport.load('test/functional/fixtures/kbn_archiver/discover'); + await kibanaServer.uiSettings.replace({ defaultIndex: 'logstash-*' }); + await PageObjects.common.navigateToApp('discover'); + await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.timePicker.setDefaultAbsoluteRange(); + await PageObjects.discover.waitUntilSearchingHasFinished(); + }); + + after(async () => { + await kibanaServer.importExport.unload('test/functional/fixtures/kbn_archiver/discover'); + }); + + it('should choose breakdown field', async () => { + await PageObjects.discover.chooseBreakdownField('extension.raw'); + await PageObjects.header.waitUntilLoadingHasFinished(); + const list = await PageObjects.discover.getHistogramLegendList(); + expect(list).to.eql(['Other', 'png', 'css', 'jpg']); + }); + + it('should add filter using histogram legend values', async () => { + await PageObjects.discover.clickLegendFilter('png', '+'); + await PageObjects.header.waitUntilLoadingHasFinished(); + expect(await filterBar.hasFilter('extension.raw', 'png')).to.be(true); + }); + + it('should save breakdown field in saved search', async () => { + await filterBar.removeFilter('extension.raw'); + await PageObjects.discover.saveSearch('with breakdown'); + + await PageObjects.discover.clickNewSearchButton(); + await PageObjects.header.waitUntilLoadingHasFinished(); + const prevList = await PageObjects.discover.getHistogramLegendList(); + expect(prevList).to.eql([]); + + await PageObjects.discover.loadSavedSearch('with breakdown'); + await PageObjects.header.waitUntilLoadingHasFinished(); + const list = await PageObjects.discover.getHistogramLegendList(); + expect(list).to.eql(['Other', 'png', 'css', 'jpg']); + }); + }); +} diff --git a/test/functional/apps/discover/group1/_inspector.ts b/test/functional/apps/discover/group1/_inspector.ts index 10451adc98e4f..851f992f42b20 100644 --- a/test/functional/apps/discover/group1/_inspector.ts +++ b/test/functional/apps/discover/group1/_inspector.ts @@ -38,10 +38,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await kibanaServer.importExport.load('test/functional/fixtures/kbn_archiver/discover.json'); await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/logstash_functional'); // delete .kibana index and update configDoc - await kibanaServer.uiSettings.replace({ - defaultIndex: 'logstash-*', - }); - + await kibanaServer.uiSettings.replace({ defaultIndex: 'logstash-*' }); + await PageObjects.timePicker.setDefaultAbsoluteRangeViaUiSettings(); await PageObjects.common.navigateToApp('discover'); }); @@ -51,9 +49,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('should display request stats with no results', async () => { await inspector.open(); - await testSubjects.click('inspectorRequestChooser'); let foundZero = false; - for (const subj of ['Documents', 'Chart_data']) { + for (const subj of ['Documents', 'Data']) { + await testSubjects.click('inspectorRequestChooser'); await testSubjects.click(`inspectorRequestChooser${subj}`); if (await testSubjects.exists('inspectorRequestDetailStatistics', { timeout: 500 })) { await testSubjects.click(`inspectorRequestDetailStatistics`); diff --git a/test/functional/apps/discover/group1/_shared_links.ts b/test/functional/apps/discover/group1/_shared_links.ts index 9235cd1160db7..edad2010db7ed 100644 --- a/test/functional/apps/discover/group1/_shared_links.ts +++ b/test/functional/apps/discover/group1/_shared_links.ts @@ -76,7 +76,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const expectedUrl = baseUrl + '/app/discover?_t=1453775307251#' + - '/?_g=(filters:!(),refreshInterval:(pause:!t,value:0),time' + + '/?_g=(filters:!(),refreshInterval:(pause:!t,value:60000),time' + ":(from:'2015-09-19T06:31:44.000Z',to:'2015-09" + "-23T18:31:44.000Z'))&_a=(columns:!(),filters:!(),index:'logstash-" + "*',interval:auto,query:(language:kuery,query:'')" + @@ -102,7 +102,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { baseUrl + '/app/discover#' + '/view/ab12e3c0-f231-11e6-9486-733b1ac9221a' + - '?_g=(filters%3A!()%2CrefreshInterval%3A(pause%3A!t%2Cvalue%3A0)' + + '?_g=(filters%3A!()%2CrefreshInterval%3A(pause%3A!t%2Cvalue%3A60000)' + "%2Ctime%3A(from%3A'2015-09-19T06%3A31%3A44.000Z'%2C" + "to%3A'2015-09-23T18%3A31%3A44.000Z'))"; await PageObjects.discover.loadSavedSearch('A Saved Search'); diff --git a/test/functional/apps/discover/group1/_sidebar.ts b/test/functional/apps/discover/group1/_sidebar.ts index 585aae36196e6..109e8aa37cd38 100644 --- a/test/functional/apps/discover/group1/_sidebar.ts +++ b/test/functional/apps/discover/group1/_sidebar.ts @@ -20,22 +20,31 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { 'unifiedSearch', ]); const testSubjects = getService('testSubjects'); + const find = getService('find'); const browser = getService('browser'); + const monacoEditor = getService('monacoEditor'); const filterBar = getService('filterBar'); + const fieldEditor = getService('fieldEditor'); describe('discover sidebar', function describeIndexTests() { before(async function () { await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/logstash_functional'); + }); + + beforeEach(async () => { await kibanaServer.importExport.load('test/functional/fixtures/kbn_archiver/discover'); await kibanaServer.uiSettings.replace({ defaultIndex: 'logstash-*', }); await PageObjects.timePicker.setDefaultAbsoluteRangeViaUiSettings(); await PageObjects.common.navigateToApp('discover'); + await PageObjects.discover.waitUntilSearchingHasFinished(); }); - after(async () => { + afterEach(async () => { await kibanaServer.importExport.unload('test/functional/fixtures/kbn_archiver/discover'); + await kibanaServer.savedObjects.cleanStandardList(); + await kibanaServer.uiSettings.replace({}); }); describe('field filtering', function () { @@ -107,5 +116,449 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await testSubjects.existOrFail('discover-sidebar'); }); }); + + describe('renders field groups', function () { + it('should show field list groups excluding subfields', async function () { + await PageObjects.discover.waitUntilSidebarHasLoaded(); + expect(await PageObjects.discover.doesSidebarShowFields()).to.be(true); + + // Initial Available fields + const expectedInitialAvailableFields = + '@message, @tags, @timestamp, agent, bytes, clientip, extension, geo.coordinates, geo.dest, geo.src, geo.srcdest, headings, host, id, index, ip, links, machine.os, machine.ram, machine.ram_range, memory, meta.char, meta.related, meta.user.firstname, meta.user.lastname, nestedField.child, phpmemory, referer, relatedContent.article:modified_time, relatedContent.article:published_time, relatedContent.article:section, relatedContent.article:tag, relatedContent.og:description, relatedContent.og:image, relatedContent.og:image:height, relatedContent.og:image:width, relatedContent.og:site_name, relatedContent.og:title, relatedContent.og:type, relatedContent.og:url, relatedContent.twitter:card, relatedContent.twitter:description, relatedContent.twitter:image, relatedContent.twitter:site, relatedContent.twitter:title, relatedContent.url, request, response, spaces, type'; + let availableFields = await PageObjects.discover.getSidebarSectionFieldNames('available'); + expect(availableFields.length).to.be(50); + expect(availableFields.join(', ')).to.be(expectedInitialAvailableFields); + + // Available fields after scrolling down + const emptySectionButton = await find.byCssSelector( + PageObjects.discover.getSidebarSectionSelector('empty', true) + ); + await emptySectionButton.scrollIntoViewIfNecessary(); + availableFields = await PageObjects.discover.getSidebarSectionFieldNames('available'); + expect(availableFields.length).to.be(53); + expect(availableFields.join(', ')).to.be( + `${expectedInitialAvailableFields}, url, utc_time, xss` + ); + + // Expand Empty section + await PageObjects.discover.toggleSidebarSection('empty'); + expect((await PageObjects.discover.getSidebarSectionFieldNames('empty')).join(', ')).to.be( + '' + ); + + // Expand Meta section + await PageObjects.discover.toggleSidebarSection('meta'); + expect((await PageObjects.discover.getSidebarSectionFieldNames('meta')).join(', ')).to.be( + '_id, _index, _score' + ); + + expect(await PageObjects.discover.getSidebarAriaDescription()).to.be( + '53 available fields. 0 empty fields. 3 meta fields.' + ); + }); + + it('should show field list groups excluding subfields when searched from source', async function () { + await kibanaServer.uiSettings.update({ 'discover:searchFieldsFromSource': true }); + await browser.refresh(); + + await PageObjects.discover.waitUntilSidebarHasLoaded(); + expect(await PageObjects.discover.doesSidebarShowFields()).to.be(true); + + // Initial Available fields + let availableFields = await PageObjects.discover.getSidebarSectionFieldNames('available'); + expect(availableFields.length).to.be(50); + expect( + availableFields + .join(', ') + .startsWith( + '@message, @tags, @timestamp, agent, bytes, clientip, extension, geo.coordinates' + ) + ).to.be(true); + + // Available fields after scrolling down + const emptySectionButton = await find.byCssSelector( + PageObjects.discover.getSidebarSectionSelector('empty', true) + ); + await emptySectionButton.scrollIntoViewIfNecessary(); + availableFields = await PageObjects.discover.getSidebarSectionFieldNames('available'); + expect(availableFields.length).to.be(53); + + // Expand Empty section + await PageObjects.discover.toggleSidebarSection('empty'); + expect((await PageObjects.discover.getSidebarSectionFieldNames('empty')).join(', ')).to.be( + '' + ); + + // Expand Meta section + await PageObjects.discover.toggleSidebarSection('meta'); + expect((await PageObjects.discover.getSidebarSectionFieldNames('meta')).join(', ')).to.be( + '_id, _index, _score' + ); + + // Expand Unmapped section + await PageObjects.discover.toggleSidebarSection('unmapped'); + expect( + (await PageObjects.discover.getSidebarSectionFieldNames('unmapped')).join(', ') + ).to.be('relatedContent'); + + expect(await PageObjects.discover.getSidebarAriaDescription()).to.be( + '53 available fields. 1 unmapped field. 0 empty fields. 3 meta fields.' + ); + }); + + it('should show selected and popular fields', async function () { + await PageObjects.discover.clickFieldListItemAdd('extension'); + await PageObjects.discover.waitUntilSearchingHasFinished(); + await PageObjects.discover.clickFieldListItemAdd('@message'); + await PageObjects.discover.waitUntilSearchingHasFinished(); + + expect( + (await PageObjects.discover.getSidebarSectionFieldNames('selected')).join(', ') + ).to.be('extension, @message'); + + const availableFields = await PageObjects.discover.getSidebarSectionFieldNames('available'); + expect(availableFields.includes('extension')).to.be(true); + expect(availableFields.includes('@message')).to.be(true); + + expect(await PageObjects.discover.getSidebarAriaDescription()).to.be( + '2 selected fields. 2 popular fields. 53 available fields. 0 empty fields. 3 meta fields.' + ); + + await PageObjects.discover.clickFieldListItemRemove('@message'); + await PageObjects.discover.waitUntilSearchingHasFinished(); + + await PageObjects.discover.clickFieldListItemAdd('_id'); + await PageObjects.discover.waitUntilSearchingHasFinished(); + await PageObjects.discover.clickFieldListItemAdd('@message'); + await PageObjects.discover.waitUntilSearchingHasFinished(); + + expect( + (await PageObjects.discover.getSidebarSectionFieldNames('selected')).join(', ') + ).to.be('extension, _id, @message'); + + expect( + (await PageObjects.discover.getSidebarSectionFieldNames('popular')).join(', ') + ).to.be('@message, _id, extension'); + + expect(await PageObjects.discover.getSidebarAriaDescription()).to.be( + '3 selected fields. 3 popular fields. 53 available fields. 0 empty fields. 3 meta fields.' + ); + }); + + it('should show selected and available fields in text-based mode', async function () { + await kibanaServer.uiSettings.update({ 'discover:enableSql': true }); + await browser.refresh(); + + await PageObjects.discover.waitUntilSidebarHasLoaded(); + + expect(await PageObjects.discover.getSidebarAriaDescription()).to.be( + '53 available fields. 0 empty fields. 3 meta fields.' + ); + + await PageObjects.discover.selectTextBaseLang('SQL'); + + expect(await PageObjects.discover.getSidebarAriaDescription()).to.be( + '50 selected fields. 51 available fields.' + ); + + await PageObjects.discover.clickFieldListItemRemove('extension'); + await PageObjects.discover.waitUntilSidebarHasLoaded(); + + expect(await PageObjects.discover.getSidebarAriaDescription()).to.be( + '49 selected fields. 51 available fields.' + ); + + const testQuery = `SELECT "@tags", geo.dest, count(*) occurred FROM "logstash-*" + GROUP BY "@tags", geo.dest + HAVING occurred > 20 + ORDER BY occurred DESC`; + + await monacoEditor.setCodeEditorValue(testQuery); + await testSubjects.click('querySubmitButton'); + await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.discover.waitUntilSidebarHasLoaded(); + + expect(await PageObjects.discover.getSidebarAriaDescription()).to.be( + '3 selected fields. 3 available fields.' + ); + expect( + (await PageObjects.discover.getSidebarSectionFieldNames('selected')).join(', ') + ).to.be('@tags, geo.dest, occurred'); + + await PageObjects.unifiedSearch.switchDataView( + 'discover-dataView-switch-link', + 'logstash-*', + true + ); + + await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.discover.waitUntilSidebarHasLoaded(); + + expect(await PageObjects.discover.getSidebarAriaDescription()).to.be( + '1 popular field. 53 available fields. 0 empty fields. 3 meta fields.' + ); + }); + + it('should work correctly for a data view for a missing index', async function () { + // but we are skipping importing the index itself + await kibanaServer.importExport.load( + 'test/functional/fixtures/kbn_archiver/index_pattern_without_timefield' + ); + await browser.refresh(); + await PageObjects.discover.waitUntilSidebarHasLoaded(); + + expect(await PageObjects.discover.getSidebarAriaDescription()).to.be( + '53 available fields. 0 empty fields. 3 meta fields.' + ); + + await PageObjects.discover.selectIndexPattern('with-timefield'); + + await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.discover.waitUntilSidebarHasLoaded(); + + expect(await PageObjects.discover.getSidebarAriaDescription()).to.be( + '0 available fields. 0 meta fields.' + ); + await testSubjects.existOrFail( + `${PageObjects.discover.getSidebarSectionSelector('available')}-fetchWarning` + ); + await testSubjects.existOrFail( + `${PageObjects.discover.getSidebarSectionSelector( + 'available' + )}NoFieldsCallout-noFieldsExist` + ); + + await PageObjects.discover.selectIndexPattern('logstash-*'); + + await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.discover.waitUntilSidebarHasLoaded(); + + expect(await PageObjects.discover.getSidebarAriaDescription()).to.be( + '53 available fields. 0 empty fields. 3 meta fields.' + ); + await kibanaServer.importExport.unload( + 'test/functional/fixtures/kbn_archiver/index_pattern_without_timefield' + ); + }); + + it('should work correctly when switching data views', async function () { + await esArchiver.loadIfNeeded( + 'test/functional/fixtures/es_archiver/index_pattern_without_timefield' + ); + await kibanaServer.importExport.load( + 'test/functional/fixtures/kbn_archiver/index_pattern_without_timefield' + ); + + await browser.refresh(); + await PageObjects.discover.waitUntilSidebarHasLoaded(); + + expect(await PageObjects.discover.getSidebarAriaDescription()).to.be( + '53 available fields. 0 empty fields. 3 meta fields.' + ); + + await PageObjects.discover.selectIndexPattern('without-timefield'); + + await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.discover.waitUntilSidebarHasLoaded(); + + expect(await PageObjects.discover.getSidebarAriaDescription()).to.be( + '6 available fields. 0 empty fields. 3 meta fields.' + ); + + await PageObjects.discover.selectIndexPattern('with-timefield'); + + await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.discover.waitUntilSidebarHasLoaded(); + + expect(await PageObjects.discover.getSidebarAriaDescription()).to.be( + '0 available fields. 7 empty fields. 3 meta fields.' + ); + await testSubjects.existOrFail( + `${PageObjects.discover.getSidebarSectionSelector( + 'available' + )}NoFieldsCallout-noFieldsMatch` + ); + + await PageObjects.discover.selectIndexPattern('logstash-*'); + + await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.discover.waitUntilSidebarHasLoaded(); + + expect(await PageObjects.discover.getSidebarAriaDescription()).to.be( + '53 available fields. 0 empty fields. 3 meta fields.' + ); + + await kibanaServer.importExport.unload( + 'test/functional/fixtures/kbn_archiver/index_pattern_without_timefield' + ); + + await esArchiver.unload( + 'test/functional/fixtures/es_archiver/index_pattern_without_timefield' + ); + }); + + it('should work when filters change', async () => { + await PageObjects.header.waitUntilLoadingHasFinished(); + + expect(await PageObjects.discover.getSidebarAriaDescription()).to.be( + '53 available fields. 0 empty fields. 3 meta fields.' + ); + + await PageObjects.discover.clickFieldListItem('extension'); + expect(await testSubjects.getVisibleText('dscFieldStats-topValues')).to.be( + 'jpg\n65.0%\ncss\n15.4%\npng\n9.8%\ngif\n6.6%\nphp\n3.2%' + ); + + await filterBar.addFilter('extension', 'is', 'jpg'); + await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.discover.waitUntilSidebarHasLoaded(); + + expect(await PageObjects.discover.getSidebarAriaDescription()).to.be( + '53 available fields. 0 empty fields. 3 meta fields.' + ); + + // check that the filter was passed down to the sidebar + await PageObjects.discover.clickFieldListItem('extension'); + expect(await testSubjects.getVisibleText('dscFieldStats-topValues')).to.be('jpg\n100%'); + }); + + it('should work for many fields', async () => { + await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/many_fields'); + await kibanaServer.importExport.load( + 'test/functional/fixtures/kbn_archiver/many_fields_data_view' + ); + + await browser.refresh(); + await PageObjects.discover.waitUntilSidebarHasLoaded(); + + expect(await PageObjects.discover.getSidebarAriaDescription()).to.be( + '53 available fields. 0 empty fields. 3 meta fields.' + ); + + await PageObjects.discover.selectIndexPattern('indices-stats*'); + + await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.discover.waitUntilSidebarHasLoaded(); + + expect(await PageObjects.discover.getSidebarAriaDescription()).to.be( + '6873 available fields. 0 empty fields. 3 meta fields.' + ); + + await PageObjects.discover.selectIndexPattern('logstash-*'); + + await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.discover.waitUntilSidebarHasLoaded(); + + expect(await PageObjects.discover.getSidebarAriaDescription()).to.be( + '53 available fields. 0 empty fields. 3 meta fields.' + ); + + await kibanaServer.importExport.unload( + 'test/functional/fixtures/kbn_archiver/many_fields_data_view' + ); + await esArchiver.unload('test/functional/fixtures/es_archiver/many_fields'); + }); + + it('should work with ad-hoc data views and runtime fields', async () => { + await PageObjects.discover.createAdHocDataView('logstash', true); + await PageObjects.header.waitUntilLoadingHasFinished(); + + expect(await PageObjects.discover.getSidebarAriaDescription()).to.be( + '53 available fields. 0 empty fields. 3 meta fields.' + ); + + await PageObjects.discover.addRuntimeField( + '_bytes-runtimefield', + `emit((doc["bytes"].value * 2).toString())` + ); + await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.discover.waitUntilSidebarHasLoaded(); + + expect(await PageObjects.discover.getSidebarAriaDescription()).to.be( + '54 available fields. 0 empty fields. 3 meta fields.' + ); + + let allFields = await PageObjects.discover.getAllFieldNames(); + expect(allFields.includes('_bytes-runtimefield')).to.be(true); + + await PageObjects.discover.editField('_bytes-runtimefield'); + await fieldEditor.enableCustomLabel(); + await fieldEditor.setCustomLabel('_bytes-runtimefield2'); + await fieldEditor.save(); + + await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.discover.waitUntilSidebarHasLoaded(); + + expect(await PageObjects.discover.getSidebarAriaDescription()).to.be( + '54 available fields. 0 empty fields. 3 meta fields.' + ); + + allFields = await PageObjects.discover.getAllFieldNames(); + expect(allFields.includes('_bytes-runtimefield2')).to.be(true); + expect(allFields.includes('_bytes-runtimefield')).to.be(false); + + await PageObjects.discover.removeField('_bytes-runtimefield'); + await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.discover.waitUntilSidebarHasLoaded(); + + expect(await PageObjects.discover.getSidebarAriaDescription()).to.be( + '53 available fields. 0 empty fields. 3 meta fields.' + ); + + allFields = await PageObjects.discover.getAllFieldNames(); + expect(allFields.includes('_bytes-runtimefield2')).to.be(false); + expect(allFields.includes('_bytes-runtimefield')).to.be(false); + }); + + it('should work correctly when time range is updated', async function () { + await esArchiver.loadIfNeeded( + 'test/functional/fixtures/es_archiver/index_pattern_without_timefield' + ); + await kibanaServer.importExport.load( + 'test/functional/fixtures/kbn_archiver/index_pattern_without_timefield' + ); + + await browser.refresh(); + await PageObjects.discover.waitUntilSidebarHasLoaded(); + + expect(await PageObjects.discover.getSidebarAriaDescription()).to.be( + '53 available fields. 0 empty fields. 3 meta fields.' + ); + + await PageObjects.discover.selectIndexPattern('with-timefield'); + + await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.discover.waitUntilSidebarHasLoaded(); + + expect(await PageObjects.discover.getSidebarAriaDescription()).to.be( + '0 available fields. 7 empty fields. 3 meta fields.' + ); + await testSubjects.existOrFail( + `${PageObjects.discover.getSidebarSectionSelector( + 'available' + )}NoFieldsCallout-noFieldsMatch` + ); + + await PageObjects.timePicker.setAbsoluteRange( + 'Sep 21, 2019 @ 00:00:00.000', + 'Sep 23, 2019 @ 00:00:00.000' + ); + + await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.discover.waitUntilSidebarHasLoaded(); + + expect(await PageObjects.discover.getSidebarAriaDescription()).to.be( + '7 available fields. 0 empty fields. 3 meta fields.' + ); + + await kibanaServer.importExport.unload( + 'test/functional/fixtures/kbn_archiver/index_pattern_without_timefield' + ); + + await esArchiver.unload( + 'test/functional/fixtures/es_archiver/index_pattern_without_timefield' + ); + }); + }); }); } diff --git a/test/functional/apps/discover/group1/index.ts b/test/functional/apps/discover/group1/index.ts index ab6798400b7a2..82fd341ccce04 100644 --- a/test/functional/apps/discover/group1/index.ts +++ b/test/functional/apps/discover/group1/index.ts @@ -23,6 +23,7 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./_no_data')); loadTestFile(require.resolve('./_discover')); loadTestFile(require.resolve('./_discover_accessibility')); + loadTestFile(require.resolve('./_discover_histogram_breakdown')); loadTestFile(require.resolve('./_discover_histogram')); loadTestFile(require.resolve('./_doc_accessibility')); loadTestFile(require.resolve('./_filter_editor')); diff --git a/test/functional/apps/discover/group2/_adhoc_data_views.ts b/test/functional/apps/discover/group2/_adhoc_data_views.ts index 773471994237f..50eb3be5f07d1 100644 --- a/test/functional/apps/discover/group2/_adhoc_data_views.ts +++ b/test/functional/apps/discover/group2/_adhoc_data_views.ts @@ -51,6 +51,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); after(async () => { + await kibanaServer.savedObjects.cleanStandardList(); await esArchiver.unload('x-pack/test/functional/es_archives/logstash_functional'); }); diff --git a/test/functional/apps/discover/group2/_huge_fields.ts b/test/functional/apps/discover/group2/_huge_fields.ts index 085788f1139d0..7ffcc891506e3 100644 --- a/test/functional/apps/discover/group2/_huge_fields.ts +++ b/test/functional/apps/discover/group2/_huge_fields.ts @@ -42,6 +42,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await security.testUser.restoreDefaults(); await esArchiver.unload('test/functional/fixtures/es_archiver/huge_fields'); await kibanaServer.uiSettings.unset('timepicker:timeDefaults'); + await kibanaServer.savedObjects.cleanStandardList(); }); }); } diff --git a/test/functional/apps/discover/group2/_indexpattern_with_unmapped_fields.ts b/test/functional/apps/discover/group2/_indexpattern_with_unmapped_fields.ts index d5d45d227d685..a17e6c0798a78 100644 --- a/test/functional/apps/discover/group2/_indexpattern_with_unmapped_fields.ts +++ b/test/functional/apps/discover/group2/_indexpattern_with_unmapped_fields.ts @@ -22,8 +22,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await kibanaServer.savedObjects.clean({ types: ['search', 'index-pattern'] }); await kibanaServer.importExport.load('test/functional/fixtures/kbn_archiver/unmapped_fields'); await security.testUser.setRoles(['kibana_admin', 'test-index-unmapped-fields']); - const fromTime = 'Jan 20, 2021 @ 00:00:00.000'; - const toTime = 'Jan 25, 2021 @ 00:00:00.000'; + const fromTime = '2021-01-20T00:00:00.000Z'; + const toTime = '2021-01-25T00:00:00.000Z'; await kibanaServer.uiSettings.replace({ defaultIndex: 'test-index-unmapped-fields', @@ -48,11 +48,18 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await retry.try(async function () { expect(await PageObjects.discover.getHitCount()).to.be(expectedHitCount); }); - const allFields = await PageObjects.discover.getAllFieldNames(); + let allFields = await PageObjects.discover.getAllFieldNames(); // message is a mapped field expect(allFields.includes('message')).to.be(true); // sender is not a mapped field - expect(allFields.includes('sender')).to.be(true); + expect(allFields.includes('sender')).to.be(false); + + await PageObjects.discover.toggleSidebarSection('unmapped'); + + allFields = await PageObjects.discover.getAllFieldNames(); + expect(allFields.includes('sender')).to.be(true); // now visible under Unmapped section + + await PageObjects.discover.toggleSidebarSection('unmapped'); }); it('unmapped fields exist on an existing saved search', async () => { @@ -61,10 +68,20 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await retry.try(async function () { expect(await PageObjects.discover.getHitCount()).to.be(expectedHitCount); }); - const allFields = await PageObjects.discover.getAllFieldNames(); + let allFields = await PageObjects.discover.getAllFieldNames(); expect(allFields.includes('message')).to.be(true); + expect(allFields.includes('sender')).to.be(false); + expect(allFields.includes('receiver')).to.be(false); + + await PageObjects.discover.toggleSidebarSection('unmapped'); + + allFields = await PageObjects.discover.getAllFieldNames(); + + // now visible under Unmapped section expect(allFields.includes('sender')).to.be(true); expect(allFields.includes('receiver')).to.be(true); + + await PageObjects.discover.toggleSidebarSection('unmapped'); }); }); } diff --git a/test/functional/apps/discover/group2/_indexpattern_without_timefield.ts b/test/functional/apps/discover/group2/_indexpattern_without_timefield.ts index 4ce16d24ef703..8fe192618f2ff 100644 --- a/test/functional/apps/discover/group2/_indexpattern_without_timefield.ts +++ b/test/functional/apps/discover/group2/_indexpattern_without_timefield.ts @@ -15,7 +15,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const kibanaServer = getService('kibanaServer'); const security = getService('security'); const testSubjects = getService('testSubjects'); - const PageObjects = getPageObjects(['common', 'timePicker', 'discover']); + const PageObjects = getPageObjects(['common', 'timePicker', 'discover', 'header']); describe('indexpattern without timefield', () => { before(async () => { @@ -114,5 +114,20 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { url = await browser.getCurrentUrl(); expect(url).to.contain(`refreshInterval:(pause:!t,value:${autoRefreshInterval * 1000})`); }); + + it('should allow switching from a saved search with a time field to a saved search without a time field', async () => { + await PageObjects.common.navigateToApp('discover'); + await PageObjects.discover.selectIndexPattern('with-timefield'); + await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.discover.saveSearch('with-timefield'); + await PageObjects.discover.selectIndexPattern('without-timefield'); + await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.discover.saveSearch('without-timefield', true); + await PageObjects.discover.loadSavedSearch('with-timefield'); + await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.discover.loadSavedSearch('without-timefield'); + await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.discover.assertHitCount('1'); + }); }); } diff --git a/test/functional/apps/discover/group2/_search_on_page_load.ts b/test/functional/apps/discover/group2/_search_on_page_load.ts index be738c3708854..cb2b21e3849db 100644 --- a/test/functional/apps/discover/group2/_search_on_page_load.ts +++ b/test/functional/apps/discover/group2/_search_on_page_load.ts @@ -25,6 +25,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { defaultIndex: 'logstash-*', }; + const savedSearchName = 'saved-search-with-on-page-load'; + const initSearchOnPageLoad = async (searchOnPageLoad: boolean) => { await kibanaServer.uiSettings.replace({ 'discover:searchOnPageLoad': searchOnPageLoad }); await PageObjects.common.navigateToApp('discover'); @@ -60,6 +62,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await esArchiver.unload('test/functional/fixtures/es_archiver/date_nested'); await esArchiver.unload('test/functional/fixtures/es_archiver/logstash_functional'); await kibanaServer.uiSettings.replace(defaultSettings); + await kibanaServer.savedObjects.cleanStandardList(); }); describe(`when it's false`, () => { @@ -68,6 +71,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('should not fetch data from ES initially', async function () { expect(await testSubjects.exists(refreshButtonSelector)).to.be(true); await retry.waitFor('number of fetches to be 0', waitForFetches(0)); + expect(await PageObjects.discover.doesSidebarShowFields()).to.be(false); }); it('should not fetch on indexPattern change', async function () { @@ -78,43 +82,77 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { expect(await testSubjects.exists(refreshButtonSelector)).to.be(true); await retry.waitFor('number of fetches to be 0', waitForFetches(0)); + expect(await PageObjects.discover.doesSidebarShowFields()).to.be(false); }); it('should fetch data from ES after refreshDataButton click', async function () { expect(await testSubjects.exists(refreshButtonSelector)).to.be(true); await retry.waitFor('number of fetches to be 0', waitForFetches(0)); + expect(await PageObjects.discover.doesSidebarShowFields()).to.be(false); await testSubjects.click(refreshButtonSelector); await testSubjects.missingOrFail(refreshButtonSelector); await retry.waitFor('number of fetches to be 1', waitForFetches(1)); + expect(await PageObjects.discover.doesSidebarShowFields()).to.be(true); }); it('should fetch data from ES after submit query', async function () { expect(await testSubjects.exists(refreshButtonSelector)).to.be(true); await retry.waitFor('number of fetches to be 0', waitForFetches(0)); + expect(await PageObjects.discover.doesSidebarShowFields()).to.be(false); await queryBar.submitQuery(); await testSubjects.missingOrFail(refreshButtonSelector); await retry.waitFor('number of fetches to be 1', waitForFetches(1)); + expect(await PageObjects.discover.doesSidebarShowFields()).to.be(true); }); it('should fetch data from ES after choosing commonly used time range', async function () { await PageObjects.discover.selectIndexPattern('logstash-*'); expect(await testSubjects.exists(refreshButtonSelector)).to.be(true); await retry.waitFor('number of fetches to be 0', waitForFetches(0)); + expect(await PageObjects.discover.doesSidebarShowFields()).to.be(false); await PageObjects.timePicker.setCommonlyUsedTime('This_week'); await testSubjects.missingOrFail(refreshButtonSelector); await retry.waitFor('number of fetches to be 1', waitForFetches(1)); + expect(await PageObjects.discover.doesSidebarShowFields()).to.be(true); + }); + + it('should fetch data when a search is saved', async function () { + await PageObjects.discover.selectIndexPattern('logstash-*'); + + await retry.waitFor('number of fetches to be 0', waitForFetches(0)); + expect(await PageObjects.discover.doesSidebarShowFields()).to.be(false); + + await PageObjects.discover.saveSearch(savedSearchName); + + await retry.waitFor('number of fetches to be 1', waitForFetches(1)); + expect(await PageObjects.discover.doesSidebarShowFields()).to.be(true); + }); + + it('should reset state after opening a saved search and pressing New', async function () { + await PageObjects.discover.loadSavedSearch(savedSearchName); + await PageObjects.header.waitUntilLoadingHasFinished(); + + await retry.waitFor('number of fetches to be 1', waitForFetches(1)); + expect(await PageObjects.discover.doesSidebarShowFields()).to.be(true); + + await testSubjects.click('discoverNewButton'); + await PageObjects.header.waitUntilLoadingHasFinished(); + + await retry.waitFor('number of fetches to be 1', waitForFetches(1)); + expect(await PageObjects.discover.doesSidebarShowFields()).to.be(false); }); }); it(`when it's true should fetch data from ES initially`, async function () { await initSearchOnPageLoad(true); await retry.waitFor('number of fetches to be 1', waitForFetches(1)); + expect(await PageObjects.discover.doesSidebarShowFields()).to.be(true); }); }); } diff --git a/test/functional/apps/management/_index_pattern_create_delete.ts b/test/functional/apps/management/_index_pattern_create_delete.ts index e9ceba4439a03..8447610d60aa8 100644 --- a/test/functional/apps/management/_index_pattern_create_delete.ts +++ b/test/functional/apps/management/_index_pattern_create_delete.ts @@ -46,6 +46,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('can resolve errors and submit', async function () { await PageObjects.settings.setIndexPatternField('log*'); + await new Promise((e) => setTimeout(e, 500)); await (await PageObjects.settings.getSaveDataViewButtonActive()).click(); await PageObjects.settings.removeIndexPattern(); }); diff --git a/test/functional/page_objects/context_page.ts b/test/functional/page_objects/context_page.ts index 05ea89cb65b3d..2bb3d7fb84a2a 100644 --- a/test/functional/page_objects/context_page.ts +++ b/test/functional/page_objects/context_page.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import rison from 'rison-node'; +import rison from '@kbn/rison'; import { getUrl } from '@kbn/test'; import { FtrService } from '../ftr_provider_context'; diff --git a/test/functional/page_objects/discover_page.ts b/test/functional/page_objects/discover_page.ts index 79a30dba288d3..d6c12c9bb9e43 100644 --- a/test/functional/page_objects/discover_page.ts +++ b/test/functional/page_objects/discover_page.ts @@ -8,6 +8,9 @@ import expect from '@kbn/expect'; import { FtrService } from '../ftr_provider_context'; +import { WebElementWrapper } from '../services/lib/web_element_wrapper'; + +type SidebarSectionName = 'meta' | 'empty' | 'available' | 'unmapped' | 'popular' | 'selected'; export class DiscoverPageObject extends FtrService { private readonly retry = this.ctx.getService('retry'); @@ -25,6 +28,7 @@ export class DiscoverPageObject extends FtrService { private readonly kibanaServer = this.ctx.getService('kibanaServer'); private readonly fieldEditor = this.ctx.getService('fieldEditor'); private readonly queryBar = this.ctx.getService('queryBar'); + private readonly comboBox = this.ctx.getService('comboBox'); private readonly defaultFindTimeout = this.config.get('timeouts.find'); @@ -202,6 +206,22 @@ export class DiscoverPageObject extends FtrService { ); } + public async chooseBreakdownField(field: string) { + await this.comboBox.set('unifiedHistogramBreakdownFieldSelector', field); + } + + public async getHistogramLegendList() { + const unifiedHistogram = await this.testSubjects.find('unifiedHistogramChart'); + const list = await unifiedHistogram.findAllByClassName('echLegendItem__label'); + return Promise.all(list.map((elem: WebElementWrapper) => elem.getVisibleText())); + } + + public async clickLegendFilter(field: string, type: '+' | '-') { + const filterType = type === '+' ? 'filterIn' : 'filterOut'; + await this.testSubjects.click(`legend-${field}`); + await this.testSubjects.click(`legend-${field}-${filterType}`); + } + public async getCurrentQueryName() { return await this.globalNav.getLastBreadcrumb(); } @@ -377,14 +397,14 @@ export class DiscoverPageObject extends FtrService { public async editField(field: string) { await this.retry.try(async () => { - await this.testSubjects.click(`field-${field}`); + await this.clickFieldListItem(field); await this.testSubjects.click(`discoverFieldListPanelEdit-${field}`); await this.find.byClassName('indexPatternFieldEditor__form'); }); } public async removeField(field: string) { - await this.testSubjects.click(`field-${field}`); + await this.clickFieldListItem(field); await this.testSubjects.click(`discoverFieldListPanelDelete-${field}`); await this.testSubjects.existOrFail('runtimeFieldDeleteConfirmModal'); await this.fieldEditor.confirmDelete(); @@ -437,8 +457,65 @@ export class DiscoverPageObject extends FtrService { return await this.testSubjects.exists('discoverNoResultsTimefilter'); } + public async getSidebarAriaDescription(): Promise { + return await ( + await this.testSubjects.find('fieldListGrouped__ariaDescription') + ).getAttribute('innerText'); + } + + public async waitUntilSidebarHasLoaded() { + await this.retry.waitFor('sidebar is loaded', async () => { + return (await this.getSidebarAriaDescription()).length > 0; + }); + } + + public async doesSidebarShowFields() { + return await this.testSubjects.exists('fieldListGroupedFieldGroups'); + } + + public getSidebarSectionSelector( + sectionName: SidebarSectionName, + asCSSSelector: boolean = false + ) { + const testSubj = `fieldListGrouped${sectionName[0].toUpperCase()}${sectionName.substring( + 1 + )}Fields`; + if (!asCSSSelector) { + return testSubj; + } + return `[data-test-subj="${testSubj}"]`; + } + + public async getSidebarSectionFieldNames(sectionName: SidebarSectionName): Promise { + const elements = await this.find.allByCssSelector( + `${this.getSidebarSectionSelector(sectionName, true)} li` + ); + + if (!elements?.length) { + return []; + } + + return Promise.all( + elements.map(async (element) => await element.getAttribute('data-attr-field')) + ); + } + + public async toggleSidebarSection(sectionName: SidebarSectionName) { + return await this.find.clickByCssSelector( + `${this.getSidebarSectionSelector(sectionName, true)} .euiAccordion__iconButton` + ); + } + + public async waitUntilFieldPopoverIsOpen() { + await this.retry.waitFor('popover is open', async () => { + return Boolean(await this.find.byCssSelector('[data-popover-open="true"]')); + }); + } + public async clickFieldListItem(field: string) { - return await this.testSubjects.click(`field-${field}`); + await this.testSubjects.click(`field-${field}`); + + await this.waitUntilFieldPopoverIsOpen(); } public async clickFieldSort(field: string, text = 'Sort New-Old') { @@ -455,11 +532,16 @@ export class DiscoverPageObject extends FtrService { } public async clickFieldListItemAdd(field: string) { + await this.waitUntilSidebarHasLoaded(); + // a filter check may make sense here, but it should be properly handled to make // it work with the _score and _source fields as well if (await this.isFieldSelected(field)) { return; } + if (['_score', '_id', '_index'].includes(field)) { + await this.toggleSidebarSection('meta'); // expand Meta section + } await this.clickFieldListItemToggle(field); const isLegacyDefault = await this.useLegacyTable(); if (isLegacyDefault) { @@ -474,16 +556,18 @@ export class DiscoverPageObject extends FtrService { } public async isFieldSelected(field: string) { - if (!(await this.testSubjects.exists('fieldList-selected'))) { + if (!(await this.testSubjects.exists('fieldListGroupedSelectedFields'))) { return false; } - const selectedList = await this.testSubjects.find('fieldList-selected'); + const selectedList = await this.testSubjects.find('fieldListGroupedSelectedFields'); return await this.testSubjects.descendantExists(`field-${field}`, selectedList); } public async clickFieldListItemRemove(field: string) { + await this.waitUntilSidebarHasLoaded(); + if ( - !(await this.testSubjects.exists('fieldList-selected')) || + !(await this.testSubjects.exists('fieldListGroupedSelectedFields')) || !(await this.isFieldSelected(field)) ) { return; @@ -493,6 +577,8 @@ export class DiscoverPageObject extends FtrService { } public async clickFieldListItemVisualize(fieldName: string) { + await this.waitUntilSidebarHasLoaded(); + const field = await this.testSubjects.find(`field-${fieldName}-showDetails`); const isActive = await field.elementHasClass('kbnFieldButton-isActive'); @@ -501,6 +587,7 @@ export class DiscoverPageObject extends FtrService { await field.click(); } + await this.waitUntilFieldPopoverIsOpen(); await this.testSubjects.click(`fieldVisualize-${fieldName}`); await this.header.waitUntilLoadingHasFinished(); } diff --git a/test/functional/page_objects/time_picker.ts b/test/functional/page_objects/time_picker.ts index 47cc89f10ce3c..44aec2e3fb187 100644 --- a/test/functional/page_objects/time_picker.ts +++ b/test/functional/page_objects/time_picker.ts @@ -40,7 +40,7 @@ export class TimePickerPageObject extends FtrService { public readonly defaultStartTime = 'Sep 19, 2015 @ 06:31:44.000'; public readonly defaultEndTime = 'Sep 23, 2015 @ 18:31:44.000'; - public readonly defaultStartTimeUTC = '2015-09-18T06:31:44.000Z'; + public readonly defaultStartTimeUTC = '2015-09-19T06:31:44.000Z'; public readonly defaultEndTimeUTC = '2015-09-23T18:31:44.000Z'; async setDefaultAbsoluteRange() { @@ -123,8 +123,21 @@ export class TimePickerPageObject extends FtrService { /** * @param {String} fromTime MMM D, YYYY @ HH:mm:ss.SSS * @param {String} toTime MMM D, YYYY @ HH:mm:ss.SSS + * @param {Boolean} force time picker force update, default is false */ - public async setAbsoluteRange(fromTime: string, toTime: string) { + public async setAbsoluteRange(fromTime: string, toTime: string, force = false) { + if (!force) { + const currentUrl = decodeURI(await this.browser.getCurrentUrl()); + const DEFAULT_DATE_FORMAT = 'MMM D, YYYY @ HH:mm:ss.SSS'; + const startMoment = moment.utc(fromTime, DEFAULT_DATE_FORMAT).toISOString(); + const endMoment = moment.utc(toTime, DEFAULT_DATE_FORMAT).toISOString(); + if (currentUrl.includes(`time:(from:'${startMoment}',to:'${endMoment}'`)) { + this.log.debug( + `We already have the desired start (${fromTime}) and end (${toTime}) in the URL, returning from setAbsoluteRange` + ); + return; + } + } this.log.debug(`Setting absolute range to ${fromTime} to ${toTime}`); await this.showStartEndTimes(); let panel!: WebElementWrapper; @@ -285,7 +298,14 @@ export class TimePickerPageObject extends FtrService { await this.testSubjects.click('superDatePickerToggleRefreshButton'); } - await this.inputValue('superDatePickerRefreshIntervalInput', intervalS.toString()); + await this.retry.waitFor('auto refresh to be set correctly', async () => { + await this.inputValue('superDatePickerRefreshIntervalInput', intervalS.toString()); + return ( + (await this.testSubjects.getAttribute('superDatePickerRefreshIntervalInput', 'value')) === + intervalS.toString() + ); + }); + await this.quickSelectTimeMenuToggle.close(); } diff --git a/tsconfig.base.json b/tsconfig.base.json index 3e3d5de4875b2..787992f6e2133 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -540,6 +540,8 @@ "@kbn/repo-source-classifier/*": ["packages/kbn-repo-source-classifier/*"], "@kbn/repo-source-classifier-cli": ["packages/kbn-repo-source-classifier-cli"], "@kbn/repo-source-classifier-cli/*": ["packages/kbn-repo-source-classifier-cli/*"], + "@kbn/rison": ["packages/kbn-rison"], + "@kbn/rison/*": ["packages/kbn-rison/*"], "@kbn/rule-data-utils": ["packages/kbn-rule-data-utils"], "@kbn/rule-data-utils/*": ["packages/kbn-rule-data-utils/*"], "@kbn/safer-lodash-set": ["packages/kbn-safer-lodash-set"], @@ -654,12 +656,20 @@ "@kbn/shared-ux-card-no-data-mocks/*": ["packages/shared-ux/card/no_data/mocks/*"], "@kbn/shared-ux-card-no-data-types": ["packages/shared-ux/card/no_data/types"], "@kbn/shared-ux-card-no-data-types/*": ["packages/shared-ux/card/no_data/types/*"], + "@kbn/shared-ux-file-context": ["packages/shared-ux/file/context"], + "@kbn/shared-ux-file-context/*": ["packages/shared-ux/file/context/*"], + "@kbn/shared-ux-file-picker": ["packages/shared-ux/file/file_picker/impl"], + "@kbn/shared-ux-file-picker/*": ["packages/shared-ux/file/file_picker/impl/*"], + "@kbn/shared-ux-file-upload": ["packages/shared-ux/file/file_upload/impl"], + "@kbn/shared-ux-file-upload/*": ["packages/shared-ux/file/file_upload/impl/*"], "@kbn/shared-ux-file-image": ["packages/shared-ux/file/image/impl"], "@kbn/shared-ux-file-image/*": ["packages/shared-ux/file/image/impl/*"], "@kbn/shared-ux-file-image-mocks": ["packages/shared-ux/file/image/mocks"], "@kbn/shared-ux-file-image-mocks/*": ["packages/shared-ux/file/image/mocks/*"], - "@kbn/shared-ux-link-redirect-app-types": ["packages/shared-ux/file/image/types"], - "@kbn/shared-ux-link-redirect-app-types/*": ["packages/shared-ux/file/image/types/*"], + "@kbn/shared-ux-file-mocks": ["packages/shared-ux/file/mocks"], + "@kbn/shared-ux-file-mocks/*": ["packages/shared-ux/file/mocks/*"], + "@kbn/shared-ux-file-types": ["packages/shared-ux/file/types"], + "@kbn/shared-ux-file-types/*": ["packages/shared-ux/file/types/*"], "@kbn/shared-ux-file-util": ["packages/shared-ux/file/util"], "@kbn/shared-ux-file-util/*": ["packages/shared-ux/file/util/*"], "@kbn/shared-ux-link-redirect-app": ["packages/shared-ux/link/redirect_app/impl"], @@ -712,6 +722,8 @@ "@kbn/shared-ux-prompt-no-data-views-mocks/*": ["packages/shared-ux/prompt/no_data_views/mocks/*"], "@kbn/shared-ux-prompt-no-data-views-types": ["packages/shared-ux/prompt/no_data_views/types"], "@kbn/shared-ux-prompt-no-data-views-types/*": ["packages/shared-ux/prompt/no_data_views/types/*"], + "@kbn/shared-ux-prompt-not-found": ["packages/shared-ux/prompt/not_found"], + "@kbn/shared-ux-prompt-not-found/*": ["packages/shared-ux/prompt/not_found/*"], "@kbn/shared-ux-router": ["packages/shared-ux/router/impl"], "@kbn/shared-ux-router/*": ["packages/shared-ux/router/impl/*"], "@kbn/shared-ux-router-mocks": ["packages/shared-ux/router/mocks"], diff --git a/typings/rison_node.d.ts b/typings/rison_node.d.ts deleted file mode 100644 index dacb2524907be..0000000000000 --- a/typings/rison_node.d.ts +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 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. - */ - -declare module 'rison-node' { - export type RisonValue = undefined | null | boolean | number | string | RisonObject | RisonArray; - - // eslint-disable-next-line @typescript-eslint/no-empty-interface - export interface RisonArray extends Array {} - - export interface RisonObject { - [key: string]: RisonValue; - } - - export const decode: (input: string) => RisonValue; - - // eslint-disable-next-line @typescript-eslint/naming-convention - export const decode_object: (input: string) => RisonObject; - - export const encode: (input: Input) => string; - - // eslint-disable-next-line @typescript-eslint/naming-convention - export const encode_object: (input: Input) => string; -} diff --git a/x-pack/plugins/actions/server/create_execute_function.test.ts b/x-pack/plugins/actions/server/create_execute_function.test.ts index a38a9e4a0e780..6e1c9aded6a3d 100644 --- a/x-pack/plugins/actions/server/create_execute_function.test.ts +++ b/x-pack/plugins/actions/server/create_execute_function.test.ts @@ -605,24 +605,27 @@ describe('bulkExecute()', () => { ] `); expect(savedObjectsClient.bulkGet).toHaveBeenCalledWith([{ id: '123', type: 'action' }]); - expect(savedObjectsClient.bulkCreate).toHaveBeenCalledWith([ - { - type: 'action_task_params', - attributes: { - actionId: '123', - params: { baz: false }, - executionId: '123abc', - apiKey: Buffer.from('123:abc').toString('base64'), - }, - references: [ - { - id: '123', - name: 'actionRef', - type: 'action', + expect(savedObjectsClient.bulkCreate).toHaveBeenCalledWith( + [ + { + type: 'action_task_params', + attributes: { + actionId: '123', + params: { baz: false }, + executionId: '123abc', + apiKey: Buffer.from('123:abc').toString('base64'), }, - ], - }, - ]); + references: [ + { + id: '123', + name: 'actionRef', + type: 'action', + }, + ], + }, + ], + { refresh: false } + ); expect(actionTypeRegistry.isActionExecutable).toHaveBeenCalledWith('123', 'mock-action', { notifyUsage: true, }); @@ -691,25 +694,28 @@ describe('bulkExecute()', () => { ] `); expect(savedObjectsClient.bulkGet).toHaveBeenCalledWith([{ id: '123', type: 'action' }]); - expect(savedObjectsClient.bulkCreate).toHaveBeenCalledWith([ - { - type: 'action_task_params', - attributes: { - actionId: '123', - params: { baz: false }, - executionId: '123abc', - consumer: 'test-consumer', - apiKey: Buffer.from('123:abc').toString('base64'), - }, - references: [ - { - id: '123', - name: 'actionRef', - type: 'action', + expect(savedObjectsClient.bulkCreate).toHaveBeenCalledWith( + [ + { + type: 'action_task_params', + attributes: { + actionId: '123', + params: { baz: false }, + executionId: '123abc', + consumer: 'test-consumer', + apiKey: Buffer.from('123:abc').toString('base64'), }, - ], - }, - ]); + references: [ + { + id: '123', + name: 'actionRef', + type: 'action', + }, + ], + }, + ], + { refresh: false } + ); expect(actionTypeRegistry.isActionExecutable).toHaveBeenCalledWith('123', 'mock-action', { notifyUsage: true, }); @@ -763,37 +769,40 @@ describe('bulkExecute()', () => { ], }, ]); - expect(savedObjectsClient.bulkCreate).toHaveBeenCalledWith([ - { - type: 'action_task_params', - attributes: { - actionId: '123', - params: { baz: false }, - apiKey: Buffer.from('123:abc').toString('base64'), - executionId: '123abc', - relatedSavedObjects: [ + expect(savedObjectsClient.bulkCreate).toHaveBeenCalledWith( + [ + { + type: 'action_task_params', + attributes: { + actionId: '123', + params: { baz: false }, + apiKey: Buffer.from('123:abc').toString('base64'), + executionId: '123abc', + relatedSavedObjects: [ + { + id: 'related_some-type_0', + namespace: 'some-namespace', + type: 'some-type', + typeId: 'some-typeId', + }, + ], + }, + references: [ { - id: 'related_some-type_0', - namespace: 'some-namespace', + id: '123', + name: 'actionRef', + type: 'action', + }, + { + id: 'some-id', + name: 'related_some-type_0', type: 'some-type', - typeId: 'some-typeId', }, ], }, - references: [ - { - id: '123', - name: 'actionRef', - type: 'action', - }, - { - id: 'some-id', - name: 'related_some-type_0', - type: 'some-type', - }, - ], - }, - ]); + ], + { refresh: false } + ); }); test('schedules the action with all given parameters with a preconfigured action', async () => { @@ -868,24 +877,27 @@ describe('bulkExecute()', () => { ] `); expect(savedObjectsClient.get).not.toHaveBeenCalled(); - expect(savedObjectsClient.bulkCreate).toHaveBeenCalledWith([ - { - type: 'action_task_params', - attributes: { - actionId: '123', - params: { baz: false }, - executionId: '123abc', - apiKey: Buffer.from('123:abc').toString('base64'), - }, - references: [ - { - id: source.id, - name: 'source', - type: source.type, + expect(savedObjectsClient.bulkCreate).toHaveBeenCalledWith( + [ + { + type: 'action_task_params', + attributes: { + actionId: '123', + params: { baz: false }, + executionId: '123abc', + apiKey: Buffer.from('123:abc').toString('base64'), }, - ], - }, - ]); + references: [ + { + id: source.id, + name: 'source', + type: source.type, + }, + ], + }, + ], + { refresh: false } + ); }); test('schedules the action with all given parameters with a preconfigured action and relatedSavedObjects', async () => { @@ -968,37 +980,40 @@ describe('bulkExecute()', () => { ] `); expect(savedObjectsClient.get).not.toHaveBeenCalled(); - expect(savedObjectsClient.bulkCreate).toHaveBeenCalledWith([ - { - type: 'action_task_params', - attributes: { - actionId: '123', - params: { baz: false }, - apiKey: Buffer.from('123:abc').toString('base64'), - executionId: '123abc', - relatedSavedObjects: [ + expect(savedObjectsClient.bulkCreate).toHaveBeenCalledWith( + [ + { + type: 'action_task_params', + attributes: { + actionId: '123', + params: { baz: false }, + apiKey: Buffer.from('123:abc').toString('base64'), + executionId: '123abc', + relatedSavedObjects: [ + { + id: 'related_some-type_0', + namespace: 'some-namespace', + type: 'some-type', + typeId: 'some-typeId', + }, + ], + }, + references: [ { - id: 'related_some-type_0', - namespace: 'some-namespace', + id: source.id, + name: 'source', + type: source.type, + }, + { + id: 'some-id', + name: 'related_some-type_0', type: 'some-type', - typeId: 'some-typeId', }, ], }, - references: [ - { - id: source.id, - name: 'source', - type: source.type, - }, - { - id: 'some-id', - name: 'related_some-type_0', - type: 'some-type', - }, - ], - }, - ]); + ], + { refresh: false } + ); }); test('throws when passing isESOCanEncrypt with false as a value', async () => { diff --git a/x-pack/plugins/actions/server/create_execute_function.ts b/x-pack/plugins/actions/server/create_execute_function.ts index 19447bf8e79e5..c050cf34c9fca 100644 --- a/x-pack/plugins/actions/server/create_execute_function.ts +++ b/x-pack/plugins/actions/server/create_execute_function.ts @@ -207,7 +207,7 @@ export function createBulkExecutionEnqueuerFunction({ }; }); const actionTaskParamsRecords: SavedObjectsBulkResponse = - await unsecuredSavedObjectsClient.bulkCreate(actions); + await unsecuredSavedObjectsClient.bulkCreate(actions, { refresh: false }); const taskInstances = actionTaskParamsRecords.saved_objects.map((so) => { const actionId = so.attributes.actionId; return { diff --git a/x-pack/plugins/aiops/common/index.ts b/x-pack/plugins/aiops/common/index.ts index 0726447532aa2..b4ec6c287b22d 100755 --- a/x-pack/plugins/aiops/common/index.ts +++ b/x-pack/plugins/aiops/common/index.ts @@ -21,4 +21,4 @@ export const PLUGIN_NAME = 'AIOps'; */ export const AIOPS_ENABLED = true; -export const CHANGE_POINT_DETECTION_ENABLED = false; +export const CHANGE_POINT_DETECTION_ENABLED = true; diff --git a/x-pack/plugins/aiops/public/components/log_categorization/use_discover_links.ts b/x-pack/plugins/aiops/public/components/log_categorization/use_discover_links.ts index 8a1c438199878..a56d5c0cdfc6e 100644 --- a/x-pack/plugins/aiops/public/components/log_categorization/use_discover_links.ts +++ b/x-pack/plugins/aiops/public/components/log_categorization/use_discover_links.ts @@ -5,7 +5,7 @@ * 2.0. */ -import rison from 'rison-node'; +import rison from '@kbn/rison'; import moment from 'moment'; import type { TimeRangeBounds } from '@kbn/data-plugin/common'; diff --git a/x-pack/plugins/aiops/public/components/spike_analysis_table/spike_analysis_table_groups.tsx b/x-pack/plugins/aiops/public/components/spike_analysis_table/spike_analysis_table_groups.tsx index 76dd42cf10790..7f628a7dfaa66 100644 --- a/x-pack/plugins/aiops/public/components/spike_analysis_table/spike_analysis_table_groups.tsx +++ b/x-pack/plugins/aiops/public/components/spike_analysis_table/spike_analysis_table_groups.tsx @@ -223,7 +223,7 @@ export const SpikeAnalysisGroupsTable: FC = ({ } ) } - iconType={itemIdToExpandedRowMap[item.id] ? 'arrowUp' : 'arrowDown'} + iconType={itemIdToExpandedRowMap[item.id] ? 'arrowDown' : 'arrowRight'} /> ), valign: 'top', diff --git a/x-pack/plugins/aiops/public/hooks/use_url_state.tsx b/x-pack/plugins/aiops/public/hooks/use_url_state.tsx index c4d84163f887e..94273a204f5cc 100644 --- a/x-pack/plugins/aiops/public/hooks/use_url_state.tsx +++ b/x-pack/plugins/aiops/public/hooks/use_url_state.tsx @@ -8,7 +8,7 @@ import React, { FC } from 'react'; import { parse, stringify } from 'query-string'; import { createContext, useCallback, useContext, useMemo } from 'react'; -import { decode, encode } from 'rison-node'; +import { decode, encode } from '@kbn/rison'; import { useHistory, useLocation } from 'react-router-dom'; import { isEqual } from 'lodash'; diff --git a/x-pack/plugins/alerting/common/alert_instance.ts b/x-pack/plugins/alerting/common/alert_instance.ts index e835e7b172ca1..115ff9319e416 100644 --- a/x-pack/plugins/alerting/common/alert_instance.ts +++ b/x-pack/plugins/alerting/common/alert_instance.ts @@ -18,6 +18,12 @@ const metaSchema = t.partial({ date: DateFromString, }), ]), + // an array used to track changes in alert state, the order is based on the rule executions (oldest to most recent) + // true - alert has changed from active/recovered + // false - the status has remained either active or recovered + flappingHistory: t.array(t.boolean), + // flapping flag that indicates whether the alert is flapping + flapping: t.boolean, }); export type AlertInstanceMeta = t.TypeOf; diff --git a/x-pack/plugins/alerting/common/rule_task_instance.ts b/x-pack/plugins/alerting/common/rule_task_instance.ts index 77537bafc4bb9..a732f6d9e2ec8 100644 --- a/x-pack/plugins/alerting/common/rule_task_instance.ts +++ b/x-pack/plugins/alerting/common/rule_task_instance.ts @@ -16,7 +16,10 @@ export enum ActionsCompletion { export const ruleStateSchema = t.partial({ alertTypeState: t.record(t.string, t.unknown), + // tracks the active alerts alertInstances: t.record(t.string, rawAlertInstance), + // tracks the recovered alerts for flapping purposes + alertRecoveredInstances: t.record(t.string, rawAlertInstance), previousStartedAt: t.union([t.null, DateFromString]), }); diff --git a/x-pack/plugins/alerting/server/alert/alert.test.ts b/x-pack/plugins/alerting/server/alert/alert.test.ts index d9e05e55a67fd..e74b103a99880 100644 --- a/x-pack/plugins/alerting/server/alert/alert.test.ts +++ b/x-pack/plugins/alerting/server/alert/alert.test.ts @@ -252,6 +252,7 @@ describe('updateLastScheduledActions()', () => { date: new Date().toISOString(), group: 'default', }, + flappingHistory: [], }, }); }); @@ -340,11 +341,13 @@ describe('toJSON', () => { date: new Date(), group: 'default', }, + flappingHistory: [false, true], + flapping: false, }, } ); expect(JSON.stringify(alertInstance)).toEqual( - '{"state":{"foo":true},"meta":{"lastScheduledActions":{"date":"1970-01-01T00:00:00.000Z","group":"default"}}}' + '{"state":{"foo":true},"meta":{"lastScheduledActions":{"date":"1970-01-01T00:00:00.000Z","group":"default"},"flappingHistory":[false,true],"flapping":false}}' ); }); }); @@ -358,6 +361,7 @@ describe('toRaw', () => { date: new Date(), group: 'default', }, + flappingHistory: [false, true, true], }, }; const alertInstance = new Alert( @@ -366,4 +370,91 @@ describe('toRaw', () => { ); expect(alertInstance.toRaw()).toEqual(raw); }); + + test('returns unserialised underlying partial meta if recovered is true', () => { + const raw = { + state: { foo: true }, + meta: { + lastScheduledActions: { + date: new Date(), + group: 'default', + }, + flappingHistory: [false, true, true], + flapping: false, + }, + }; + const alertInstance = new Alert( + '1', + raw + ); + expect(alertInstance.toRaw(true)).toEqual({ + meta: { + flappingHistory: [false, true, true], + flapping: false, + }, + }); + }); +}); + +describe('setFlappingHistory', () => { + test('sets flappingHistory', () => { + const alertInstance = new Alert( + '1', + { + meta: { flappingHistory: [false, true, true] }, + } + ); + alertInstance.setFlappingHistory([false]); + expect(alertInstance.getFlappingHistory()).toEqual([false]); + expect(alertInstance.toRaw()).toMatchInlineSnapshot(` + Object { + "meta": Object { + "flappingHistory": Array [ + false, + ], + }, + "state": Object {}, + } + `); + }); +}); + +describe('getFlappingHistory', () => { + test('correctly sets flappingHistory', () => { + const alert = new Alert('1', { + meta: { flappingHistory: [false, false] }, + }); + expect(alert.getFlappingHistory()).toEqual([false, false]); + }); +}); + +describe('setFlapping', () => { + test('sets flapping', () => { + const alertInstance = new Alert( + '1', + { + meta: { flapping: true }, + } + ); + alertInstance.setFlapping(false); + expect(alertInstance.getFlapping()).toEqual(false); + expect(alertInstance.toRaw()).toMatchInlineSnapshot(` + Object { + "meta": Object { + "flapping": false, + "flappingHistory": Array [], + }, + "state": Object {}, + } + `); + }); +}); + +describe('getFlapping', () => { + test('correctly sets flapping', () => { + const alert = new Alert('1', { + meta: { flapping: true }, + }); + expect(alert.getFlapping()).toEqual(true); + }); }); diff --git a/x-pack/plugins/alerting/server/alert/alert.ts b/x-pack/plugins/alerting/server/alert/alert.ts index e24b15de41db4..6c56abdf96624 100644 --- a/x-pack/plugins/alerting/server/alert/alert.ts +++ b/x-pack/plugins/alerting/server/alert/alert.ts @@ -52,6 +52,10 @@ export class Alert< this.state = (state || {}) as State; this.context = {} as Context; this.meta = meta; + + if (!this.meta.flappingHistory) { + this.meta.flappingHistory = []; + } } getId() { @@ -168,10 +172,35 @@ export class Alert< return rawAlertInstance.encode(this.toRaw()); } - toRaw(): RawAlertInstance { - return { - state: this.state, - meta: this.meta, - }; + toRaw(recovered: boolean = false): RawAlertInstance { + return recovered + ? { + // for a recovered alert, we only care to track the flappingHistory + // and the flapping flag + meta: { + flappingHistory: this.meta.flappingHistory, + flapping: this.meta.flapping, + }, + } + : { + state: this.state, + meta: this.meta, + }; + } + + setFlappingHistory(fh: boolean[] = []) { + this.meta.flappingHistory = fh; + } + + getFlappingHistory() { + return this.meta.flappingHistory; + } + + setFlapping(f: boolean) { + this.meta.flapping = f; + } + + getFlapping() { + return this.meta.flapping || false; } } diff --git a/x-pack/plugins/alerting/server/alert/create_alert_factory.test.ts b/x-pack/plugins/alerting/server/alert/create_alert_factory.test.ts index 30013e212c526..663a0b7401b2e 100644 --- a/x-pack/plugins/alerting/server/alert/create_alert_factory.test.ts +++ b/x-pack/plugins/alerting/server/alert/create_alert_factory.test.ts @@ -33,11 +33,13 @@ describe('createAlertFactory()', () => { }); const result = alertFactory.create('1'); expect(result).toMatchInlineSnapshot(` - Object { - "meta": Object {}, - "state": Object {}, - } - `); + Object { + "meta": Object { + "flappingHistory": Array [], + }, + "state": Object {}, + } + `); // @ts-expect-error expect(result.getId()).toEqual('1'); }); @@ -58,6 +60,7 @@ describe('createAlertFactory()', () => { expect(result).toMatchInlineSnapshot(` Object { "meta": Object { + "flappingHistory": Array [], "lastScheduledActions": Object { "date": "1970-01-01T00:00:00.000Z", "group": "default", @@ -79,13 +82,15 @@ describe('createAlertFactory()', () => { }); alertFactory.create('1'); expect(alerts).toMatchInlineSnapshot(` - Object { - "1": Object { - "meta": Object {}, - "state": Object {}, - }, - } - `); + Object { + "1": Object { + "meta": Object { + "flappingHistory": Array [], + }, + "state": Object {}, + }, + } + `); }); test('throws error and sets flag when more alerts are created than allowed', () => { @@ -115,7 +120,9 @@ describe('createAlertFactory()', () => { }); const result = alertFactory.create('1'); expect(result).toEqual({ - meta: {}, + meta: { + flappingHistory: [], + }, state: {}, context: {}, scheduledExecutionOptions: undefined, @@ -133,7 +140,7 @@ describe('createAlertFactory()', () => { test('returns recovered alerts when setsRecoveryContext is true', () => { (processAlerts as jest.Mock).mockReturnValueOnce({ - recoveredAlerts: { + currentRecoveredAlerts: { z: { id: 'z', state: { foo: true }, @@ -154,7 +161,9 @@ describe('createAlertFactory()', () => { }); const result = alertFactory.create('1'); expect(result).toEqual({ - meta: {}, + meta: { + flappingHistory: [], + }, state: {}, context: {}, scheduledExecutionOptions: undefined, @@ -178,7 +187,9 @@ describe('createAlertFactory()', () => { }); const result = alertFactory.create('1'); expect(result).toEqual({ - meta: {}, + meta: { + flappingHistory: [], + }, state: {}, context: {}, scheduledExecutionOptions: undefined, @@ -201,7 +212,9 @@ describe('createAlertFactory()', () => { }); const result = alertFactory.create('1'); expect(result).toEqual({ - meta: {}, + meta: { + flappingHistory: [], + }, state: {}, context: {}, scheduledExecutionOptions: undefined, @@ -223,7 +236,9 @@ describe('createAlertFactory()', () => { }); const result = alertFactory.create('1'); expect(result).toEqual({ - meta: {}, + meta: { + flappingHistory: [], + }, state: {}, context: {}, scheduledExecutionOptions: undefined, diff --git a/x-pack/plugins/alerting/server/alert/create_alert_factory.ts b/x-pack/plugins/alerting/server/alert/create_alert_factory.ts index e0d3ecc2690fc..ff8aacd52f5fe 100644 --- a/x-pack/plugins/alerting/server/alert/create_alert_factory.ts +++ b/x-pack/plugins/alerting/server/alert/create_alert_factory.ts @@ -129,16 +129,22 @@ export function createAlertFactory< return []; } - const { recoveredAlerts } = processAlerts( - { - alerts, - existingAlerts: originalAlerts, - hasReachedAlertLimit, - alertLimit: maxAlerts, - } - ); - return Object.keys(recoveredAlerts ?? {}).map( - (alertId: string) => recoveredAlerts[alertId] + const { currentRecoveredAlerts } = processAlerts< + State, + Context, + ActionGroupIds, + ActionGroupIds + >({ + alerts, + existingAlerts: originalAlerts, + previouslyRecoveredAlerts: {}, + hasReachedAlertLimit, + alertLimit: maxAlerts, + // setFlapping is false, as we only want to use this function to get the recovered alerts + setFlapping: false, + }); + return Object.keys(currentRecoveredAlerts ?? {}).map( + (alertId: string) => currentRecoveredAlerts[alertId] ); }, }; diff --git a/x-pack/plugins/alerting/server/lib/determine_alerts_to_return.test.ts b/x-pack/plugins/alerting/server/lib/determine_alerts_to_return.test.ts new file mode 100644 index 0000000000000..4fd80033fd341 --- /dev/null +++ b/x-pack/plugins/alerting/server/lib/determine_alerts_to_return.test.ts @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { keys, size } from 'lodash'; +import { Alert } from '../alert'; +import { determineAlertsToReturn } from './determine_alerts_to_return'; + +describe('determineAlertsToReturn', () => { + const flapping = new Array(16).fill(false).concat([true, true, true, true]); + const notFlapping = new Array(20).fill(false); + + describe('determineAlertsToReturn', () => { + test('should return all active alerts regardless of flapping', () => { + const activeAlerts = { + '1': new Alert('1', { meta: { flappingHistory: flapping } }), + '2': new Alert('2', { meta: { flappingHistory: [false, false] } }), + }; + const { alertsToReturn } = determineAlertsToReturn(activeAlerts, {}); + expect(size(alertsToReturn)).toEqual(2); + }); + + test('should return all flapping recovered alerts', () => { + const recoveredAlerts = { + '1': new Alert('1', { meta: { flappingHistory: flapping } }), + '2': new Alert('2', { meta: { flappingHistory: notFlapping } }), + }; + const { recoveredAlertsToReturn } = determineAlertsToReturn({}, recoveredAlerts); + expect(keys(recoveredAlertsToReturn)).toEqual(['1']); + }); + }); +}); diff --git a/x-pack/plugins/alerting/server/lib/determine_alerts_to_return.ts b/x-pack/plugins/alerting/server/lib/determine_alerts_to_return.ts new file mode 100644 index 0000000000000..5916bf91efcc0 --- /dev/null +++ b/x-pack/plugins/alerting/server/lib/determine_alerts_to_return.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 { keys } from 'lodash'; +import { Alert } from '../alert'; +import { AlertInstanceState, AlertInstanceContext, RawAlertInstance } from '../types'; + +// determines which alerts to return in the state +export function determineAlertsToReturn< + State extends AlertInstanceState, + Context extends AlertInstanceContext, + ActionGroupIds extends string, + RecoveryActionGroupId extends string +>( + activeAlerts: Record> = {}, + recoveredAlerts: Record> = {} +): { + alertsToReturn: Record; + recoveredAlertsToReturn: Record; +} { + const alertsToReturn: Record = {}; + const recoveredAlertsToReturn: Record = {}; + + // return all active alerts regardless of whether or not the alert is flapping + for (const id of keys(activeAlerts)) { + alertsToReturn[id] = activeAlerts[id].toRaw(); + } + + for (const id of keys(recoveredAlerts)) { + const alert = recoveredAlerts[id]; + // return recovered alerts if they are flapping or if the flapping array is not at capacity + // this is a space saving effort that will stop tracking a recovered alert if it wasn't flapping and doesn't have state changes + // in the last max capcity number of executions + const flapping = alert.getFlapping(); + const flappingHistory: boolean[] = alert.getFlappingHistory() || []; + const numStateChanges = flappingHistory.filter((f) => f).length; + if (flapping) { + recoveredAlertsToReturn[id] = alert.toRaw(true); + } else if (numStateChanges > 0) { + recoveredAlertsToReturn[id] = alert.toRaw(true); + } + } + return { alertsToReturn, recoveredAlertsToReturn }; +} diff --git a/x-pack/plugins/alerting/server/lib/flapping_utils.test.ts b/x-pack/plugins/alerting/server/lib/flapping_utils.test.ts new file mode 100644 index 0000000000000..ee5525634cf4f --- /dev/null +++ b/x-pack/plugins/alerting/server/lib/flapping_utils.test.ts @@ -0,0 +1,91 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { atCapacity, updateFlappingHistory, isFlapping } from './flapping_utils'; + +describe('flapping utils', () => { + describe('updateFlappingHistory function', () => { + test('correctly updates flappingHistory', () => { + const flappingHistory = updateFlappingHistory([false, false], true); + expect(flappingHistory).toEqual([false, false, true]); + }); + + test('correctly updates flappingHistory while maintaining a fixed size', () => { + const flappingHistory = new Array(20).fill(false); + const fh = updateFlappingHistory(flappingHistory, true); + expect(fh.length).toEqual(20); + const result = new Array(19).fill(false); + expect(fh).toEqual(result.concat(true)); + }); + + test('correctly updates flappingHistory while maintaining if array is larger than fixed size', () => { + const flappingHistory = new Array(23).fill(false); + const fh = updateFlappingHistory(flappingHistory, true); + expect(fh.length).toEqual(20); + const result = new Array(19).fill(false); + expect(fh).toEqual(result.concat(true)); + }); + }); + + describe('atCapacity and getCapacityDiff functions', () => { + test('returns true if flappingHistory == set capacity', () => { + const flappingHistory = new Array(20).fill(false); + expect(atCapacity(flappingHistory)).toEqual(true); + }); + + test('returns true if flappingHistory > set capacity', () => { + const flappingHistory = new Array(25).fill(false); + expect(atCapacity(flappingHistory)).toEqual(true); + }); + + test('returns false if flappingHistory < set capacity', () => { + const flappingHistory = new Array(15).fill(false); + expect(atCapacity(flappingHistory)).toEqual(false); + }); + }); + + describe('isFlapping', () => { + describe('not currently flapping', () => { + test('returns true if at capacity and flap count exceeds the threshold', () => { + const flappingHistory = [true, true, true, true].concat(new Array(16).fill(false)); + expect(isFlapping(flappingHistory)).toEqual(true); + }); + + test("returns false if at capacity and flap count doesn't exceed the threshold", () => { + const flappingHistory = [true, true].concat(new Array(20).fill(false)); + expect(isFlapping(flappingHistory)).toEqual(false); + }); + + test('returns true if not at capacity', () => { + const flappingHistory = new Array(5).fill(true); + expect(isFlapping(flappingHistory)).toEqual(true); + }); + }); + + describe('currently flapping', () => { + test('returns true if at capacity and the flap count exceeds the threshold', () => { + const flappingHistory = new Array(16).fill(false).concat([true, true, true, true]); + expect(isFlapping(flappingHistory, true)).toEqual(true); + }); + + test("returns true if not at capacity and the flap count doesn't exceed the threshold", () => { + const flappingHistory = new Array(16).fill(false); + expect(isFlapping(flappingHistory, true)).toEqual(true); + }); + + test('returns true if not at capacity and the flap count exceeds the threshold', () => { + const flappingHistory = new Array(10).fill(false).concat([true, true, true, true]); + expect(isFlapping(flappingHistory, true)).toEqual(true); + }); + + test("returns false if at capacity and the flap count doesn't exceed the threshold", () => { + const flappingHistory = new Array(20).fill(false); + expect(isFlapping(flappingHistory, true)).toEqual(false); + }); + }); + }); +}); diff --git a/x-pack/plugins/alerting/server/lib/flapping_utils.ts b/x-pack/plugins/alerting/server/lib/flapping_utils.ts new file mode 100644 index 0000000000000..8427e02880844 --- /dev/null +++ b/x-pack/plugins/alerting/server/lib/flapping_utils.ts @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +const MAX_CAPACITY = 20; +const MAX_FLAP_COUNT = 4; + +export function updateFlappingHistory(flappingHistory: boolean[], state: boolean) { + const updatedFlappingHistory = flappingHistory.concat(state).slice(MAX_CAPACITY * -1); + return updatedFlappingHistory; +} + +export function isFlapping( + flappingHistory: boolean[], + isCurrentlyFlapping: boolean = false +): boolean { + const numStateChanges = flappingHistory.filter((f) => f).length; + if (isCurrentlyFlapping) { + // if an alert is currently flapping, + // it will return false if the flappingHistory array is at capacity and there are 0 state changes + // else it will return true + return !(atCapacity(flappingHistory) && numStateChanges === 0); + } else { + // if an alert is not currently flapping, + // it will return true if the number of state changes in flappingHistory array >= the max flapping count + return numStateChanges >= MAX_FLAP_COUNT; + } +} + +export function atCapacity(flappingHistory: boolean[] = []): boolean { + return flappingHistory.length >= MAX_CAPACITY; +} diff --git a/x-pack/plugins/alerting/server/lib/index.ts b/x-pack/plugins/alerting/server/lib/index.ts index de4c89e06f33d..8b1416df007fa 100644 --- a/x-pack/plugins/alerting/server/lib/index.ts +++ b/x-pack/plugins/alerting/server/lib/index.ts @@ -38,3 +38,6 @@ export { isRuleSnoozed, getRuleSnoozeEndTime } from './is_rule_snoozed'; export { convertRuleIdsToKueryNode } from './convert_rule_ids_to_kuery_node'; export { convertEsSortToEventLogSort } from './convert_es_sort_to_event_log_sort'; export * from './snooze'; +export { setFlapping } from './set_flapping'; +export { determineAlertsToReturn } from './determine_alerts_to_return'; +export { updateFlappingHistory, isFlapping } from './flapping_utils'; diff --git a/x-pack/plugins/alerting/server/lib/process_alerts.test.ts b/x-pack/plugins/alerting/server/lib/process_alerts.test.ts index dcf13bdc4f7a1..3b09b072476e6 100644 --- a/x-pack/plugins/alerting/server/lib/process_alerts.test.ts +++ b/x-pack/plugins/alerting/server/lib/process_alerts.test.ts @@ -7,9 +7,9 @@ import sinon from 'sinon'; import { cloneDeep } from 'lodash'; -import { processAlerts } from './process_alerts'; +import { processAlerts, updateAlertFlappingHistory } from './process_alerts'; import { Alert } from '../alert'; -import { DefaultActionGroupId } from '../types'; +import { AlertInstanceState, AlertInstanceContext } from '../types'; describe('processAlerts', () => { let clock: sinon.SinonFakeTimers; @@ -25,42 +25,47 @@ describe('processAlerts', () => { afterAll(() => clock.restore()); describe('newAlerts', () => { - test('considers alert new if it has scheduled actions and its id is not in originalAlertIds list', () => { - const newAlert = new Alert<{}, {}, DefaultActionGroupId>('1'); - const existingAlert1 = new Alert<{}, {}, DefaultActionGroupId>('2'); - const existingAlert2 = new Alert<{}, {}, DefaultActionGroupId>('3'); + test('considers alert new if it has scheduled actions and its id is not in originalAlertIds or previouslyRecoveredAlertIds list', () => { + const newAlert = new Alert('1'); + const existingAlert1 = new Alert('2'); + const existingAlert2 = new Alert('3', {}); + const existingRecoveredAlert1 = new Alert('4'); const existingAlerts = { '2': existingAlert1, '3': existingAlert2, }; + const previouslyRecoveredAlerts = { + '4': existingRecoveredAlert1, + }; + const updatedAlerts = { ...cloneDeep(existingAlerts), '1': newAlert, }; - updatedAlerts['1'].scheduleActions('default', { foo: '1' }); - updatedAlerts['2'].scheduleActions('default', { foo: '1' }); - updatedAlerts['3'].scheduleActions('default', { foo: '2' }); + updatedAlerts['1'].scheduleActions('default' as never, { foo: '1' }); + updatedAlerts['2'].scheduleActions('default' as never, { foo: '1' }); + updatedAlerts['3'].scheduleActions('default' as never, { foo: '2' }); const { newAlerts } = processAlerts({ - // @ts-expect-error alerts: updatedAlerts, - // @ts-expect-error existingAlerts, + previouslyRecoveredAlerts, hasReachedAlertLimit: false, alertLimit: 10, + setFlapping: false, }); expect(newAlerts).toEqual({ '1': newAlert }); }); test('sets start time in new alert state', () => { - const newAlert1 = new Alert<{}, {}, DefaultActionGroupId>('1'); - const newAlert2 = new Alert<{}, {}, DefaultActionGroupId>('2'); - const existingAlert1 = new Alert<{}, {}, DefaultActionGroupId>('3'); - const existingAlert2 = new Alert<{}, {}, DefaultActionGroupId>('4'); + const newAlert1 = new Alert('1'); + const newAlert2 = new Alert('2'); + const existingAlert1 = new Alert('3'); + const existingAlert2 = new Alert('4'); const existingAlerts = { '3': existingAlert1, @@ -73,21 +78,21 @@ describe('processAlerts', () => { '2': newAlert2, }; - updatedAlerts['1'].scheduleActions('default', { foo: '1' }); - updatedAlerts['2'].scheduleActions('default', { foo: '1' }); - updatedAlerts['3'].scheduleActions('default', { foo: '1' }); - updatedAlerts['4'].scheduleActions('default', { foo: '2' }); + updatedAlerts['1'].scheduleActions('default' as never, { foo: '1' }); + updatedAlerts['2'].scheduleActions('default' as never, { foo: '1' }); + updatedAlerts['3'].scheduleActions('default' as never, { foo: '1' }); + updatedAlerts['4'].scheduleActions('default' as never, { foo: '2' }); expect(newAlert1.getState()).toStrictEqual({}); expect(newAlert2.getState()).toStrictEqual({}); const { newAlerts } = processAlerts({ - // @ts-expect-error alerts: updatedAlerts, - // @ts-expect-error existingAlerts, + previouslyRecoveredAlerts: {}, hasReachedAlertLimit: false, alertLimit: 10, + setFlapping: false, }); expect(newAlerts).toEqual({ '1': newAlert1, '2': newAlert2 }); @@ -95,28 +100,22 @@ describe('processAlerts', () => { const newAlert1State = newAlerts['1'].getState(); const newAlert2State = newAlerts['2'].getState(); - // @ts-expect-error expect(newAlert1State.start).toEqual('1970-01-01T00:00:00.000Z'); - // @ts-expect-error expect(newAlert2State.start).toEqual('1970-01-01T00:00:00.000Z'); - // @ts-expect-error expect(newAlert1State.duration).toEqual('0'); - // @ts-expect-error expect(newAlert2State.duration).toEqual('0'); - // @ts-expect-error expect(newAlert1State.end).not.toBeDefined(); - // @ts-expect-error expect(newAlert2State.end).not.toBeDefined(); }); }); describe('activeAlerts', () => { test('considers alert active if it has scheduled actions', () => { - const newAlert = new Alert<{}, {}, DefaultActionGroupId>('1'); - const existingAlert1 = new Alert<{}, {}, DefaultActionGroupId>('2'); - const existingAlert2 = new Alert<{}, {}, DefaultActionGroupId>('3'); + const newAlert = new Alert('1'); + const existingAlert1 = new Alert('2'); + const existingAlert2 = new Alert('3'); const existingAlerts = { '2': existingAlert1, @@ -128,17 +127,17 @@ describe('processAlerts', () => { '1': newAlert, }; - updatedAlerts['1'].scheduleActions('default', { foo: '1' }); - updatedAlerts['2'].scheduleActions('default', { foo: '1' }); - updatedAlerts['3'].scheduleActions('default', { foo: '2' }); + updatedAlerts['1'].scheduleActions('default' as never, { foo: '1' }); + updatedAlerts['2'].scheduleActions('default' as never, { foo: '1' }); + updatedAlerts['3'].scheduleActions('default' as never, { foo: '2' }); const { activeAlerts } = processAlerts({ - // @ts-expect-error alerts: updatedAlerts, - // @ts-expect-error existingAlerts, + previouslyRecoveredAlerts: {}, hasReachedAlertLimit: false, alertLimit: 10, + setFlapping: false, }); expect(activeAlerts).toEqual({ @@ -149,9 +148,9 @@ describe('processAlerts', () => { }); test('updates duration in active alerts if start is available', () => { - const newAlert = new Alert<{}, {}, DefaultActionGroupId>('1'); - const existingAlert1 = new Alert<{}, {}, DefaultActionGroupId>('2'); - const existingAlert2 = new Alert<{}, {}, DefaultActionGroupId>('3'); + const newAlert = new Alert('1'); + const existingAlert1 = new Alert('2'); + const existingAlert2 = new Alert('3'); const existingAlerts = { '2': existingAlert1, @@ -165,17 +164,17 @@ describe('processAlerts', () => { '1': newAlert, }; - updatedAlerts['1'].scheduleActions('default', { foo: '1' }); - updatedAlerts['2'].scheduleActions('default', { foo: '1' }); - updatedAlerts['3'].scheduleActions('default', { foo: '2' }); + updatedAlerts['1'].scheduleActions('default' as never, { foo: '1' }); + updatedAlerts['2'].scheduleActions('default' as never, { foo: '1' }); + updatedAlerts['3'].scheduleActions('default' as never, { foo: '2' }); const { activeAlerts } = processAlerts({ - // @ts-expect-error alerts: updatedAlerts, - // @ts-expect-error existingAlerts, + previouslyRecoveredAlerts: {}, hasReachedAlertLimit: false, alertLimit: 10, + setFlapping: false, }); expect(activeAlerts).toEqual({ @@ -187,26 +186,20 @@ describe('processAlerts', () => { const activeAlert1State = activeAlerts['2'].getState(); const activeAlert2State = activeAlerts['3'].getState(); - // @ts-expect-error expect(activeAlert1State.start).toEqual('1969-12-30T00:00:00.000Z'); - // @ts-expect-error expect(activeAlert2State.start).toEqual('1969-12-31T07:34:00.000Z'); - // @ts-expect-error expect(activeAlert1State.duration).toEqual('172800000000000'); - // @ts-expect-error expect(activeAlert2State.duration).toEqual('59160000000000'); - // @ts-expect-error expect(activeAlert1State.end).not.toBeDefined(); - // @ts-expect-error expect(activeAlert2State.end).not.toBeDefined(); }); test('does not update duration in active alerts if start is not available', () => { - const newAlert = new Alert<{}, {}, DefaultActionGroupId>('1'); - const existingAlert1 = new Alert<{}, {}, DefaultActionGroupId>('2'); - const existingAlert2 = new Alert<{}, {}, DefaultActionGroupId>('3'); + const newAlert = new Alert('1'); + const existingAlert1 = new Alert('2'); + const existingAlert2 = new Alert('3'); const existingAlerts = { '2': existingAlert1, @@ -218,17 +211,17 @@ describe('processAlerts', () => { '1': newAlert, }; - updatedAlerts['1'].scheduleActions('default', { foo: '1' }); - updatedAlerts['2'].scheduleActions('default', { foo: '1' }); - updatedAlerts['3'].scheduleActions('default', { foo: '2' }); + updatedAlerts['1'].scheduleActions('default' as never, { foo: '1' }); + updatedAlerts['2'].scheduleActions('default' as never, { foo: '1' }); + updatedAlerts['3'].scheduleActions('default' as never, { foo: '2' }); const { activeAlerts } = processAlerts({ - // @ts-expect-error alerts: updatedAlerts, - // @ts-expect-error existingAlerts, + previouslyRecoveredAlerts: {}, hasReachedAlertLimit: false, alertLimit: 10, + setFlapping: false, }); expect(activeAlerts).toEqual({ @@ -240,26 +233,20 @@ describe('processAlerts', () => { const activeAlert1State = activeAlerts['2'].getState(); const activeAlert2State = activeAlerts['3'].getState(); - // @ts-expect-error expect(activeAlert1State.start).not.toBeDefined(); - // @ts-expect-error expect(activeAlert2State.start).not.toBeDefined(); - // @ts-expect-error expect(activeAlert1State.duration).not.toBeDefined(); - // @ts-expect-error expect(activeAlert2State.duration).not.toBeDefined(); - // @ts-expect-error expect(activeAlert1State.end).not.toBeDefined(); - // @ts-expect-error expect(activeAlert2State.end).not.toBeDefined(); }); test('preserves other state fields', () => { - const newAlert = new Alert<{}, {}, DefaultActionGroupId>('1'); - const existingAlert1 = new Alert<{}, {}, DefaultActionGroupId>('2'); - const existingAlert2 = new Alert<{}, {}, DefaultActionGroupId>('3'); + const newAlert = new Alert('1'); + const existingAlert1 = new Alert('2'); + const existingAlert2 = new Alert('3'); const existingAlerts = { '2': existingAlert1, @@ -281,17 +268,17 @@ describe('processAlerts', () => { '1': newAlert, }; - updatedAlerts['1'].scheduleActions('default', { foo: '1' }); - updatedAlerts['2'].scheduleActions('default', { foo: '1' }); - updatedAlerts['3'].scheduleActions('default', { foo: '2' }); + updatedAlerts['1'].scheduleActions('default' as never, { foo: '1' }); + updatedAlerts['2'].scheduleActions('default' as never, { foo: '1' }); + updatedAlerts['3'].scheduleActions('default' as never, { foo: '2' }); const { activeAlerts } = processAlerts({ - // @ts-expect-error alerts: updatedAlerts, - // @ts-expect-error existingAlerts, + previouslyRecoveredAlerts: {}, hasReachedAlertLimit: false, alertLimit: 10, + setFlapping: false, }); expect(activeAlerts).toEqual({ @@ -303,32 +290,79 @@ describe('processAlerts', () => { const activeAlert1State = activeAlerts['2'].getState(); const activeAlert2State = activeAlerts['3'].getState(); - // @ts-expect-error expect(activeAlert1State.start).toEqual('1969-12-30T00:00:00.000Z'); - // @ts-expect-error expect(activeAlert2State.start).toEqual('1969-12-31T07:34:00.000Z'); - // @ts-expect-error expect(activeAlert1State.stateField1).toEqual('xyz'); - // @ts-expect-error expect(activeAlert2State.anotherState).toEqual(true); - // @ts-expect-error expect(activeAlert1State.duration).toEqual('172800000000000'); - // @ts-expect-error expect(activeAlert2State.duration).toEqual('59160000000000'); - // @ts-expect-error expect(activeAlert1State.end).not.toBeDefined(); - // @ts-expect-error expect(activeAlert2State.end).not.toBeDefined(); }); + + test('sets start time in active alert state if alert was previously recovered', () => { + const previouslyRecoveredAlert1 = new Alert('1'); + const previouslyRecoveredAlert2 = new Alert('2'); + const existingAlert1 = new Alert('3'); + const existingAlert2 = new Alert('4'); + + const existingAlerts = { + '3': existingAlert1, + '4': existingAlert2, + }; + + const previouslyRecoveredAlerts = { + '1': previouslyRecoveredAlert1, + '2': previouslyRecoveredAlert2, + }; + + const updatedAlerts = { + ...cloneDeep(existingAlerts), + ...cloneDeep(previouslyRecoveredAlerts), + }; + + updatedAlerts['1'].scheduleActions('default' as never, { foo: '1' }); + updatedAlerts['2'].scheduleActions('default' as never, { foo: '1' }); + updatedAlerts['3'].scheduleActions('default' as never, { foo: '1' }); + updatedAlerts['4'].scheduleActions('default' as never, { foo: '2' }); + + expect(updatedAlerts['1'].getState()).toStrictEqual({}); + expect(updatedAlerts['2'].getState()).toStrictEqual({}); + + const { activeAlerts } = processAlerts({ + alerts: updatedAlerts, + existingAlerts, + previouslyRecoveredAlerts, + hasReachedAlertLimit: false, + alertLimit: 10, + setFlapping: true, + }); + + expect( + Object.keys(activeAlerts).map((id) => ({ [id]: activeAlerts[id].getFlappingHistory() })) + ).toEqual([{ '1': [true] }, { '2': [true] }, { '3': [false] }, { '4': [false] }]); + + const previouslyRecoveredAlert1State = activeAlerts['1'].getState(); + const previouslyRecoveredAlert2State = activeAlerts['2'].getState(); + + expect(previouslyRecoveredAlert1State.start).toEqual('1970-01-01T00:00:00.000Z'); + expect(previouslyRecoveredAlert2State.start).toEqual('1970-01-01T00:00:00.000Z'); + + expect(previouslyRecoveredAlert1State.duration).toEqual('0'); + expect(previouslyRecoveredAlert2State.duration).toEqual('0'); + + expect(previouslyRecoveredAlert1State.end).not.toBeDefined(); + expect(previouslyRecoveredAlert2State.end).not.toBeDefined(); + }); }); describe('recoveredAlerts', () => { test('considers alert recovered if it has no scheduled actions', () => { - const activeAlert = new Alert<{}, {}, DefaultActionGroupId>('1'); - const recoveredAlert = new Alert<{}, {}, DefaultActionGroupId>('2'); + const activeAlert = new Alert('1'); + const recoveredAlert = new Alert('2'); const existingAlerts = { '1': activeAlert, @@ -337,24 +371,24 @@ describe('processAlerts', () => { const updatedAlerts = cloneDeep(existingAlerts); - updatedAlerts['1'].scheduleActions('default', { foo: '1' }); + updatedAlerts['1'].scheduleActions('default' as never, { foo: '1' }); updatedAlerts['2'].setContext({ foo: '2' }); const { recoveredAlerts } = processAlerts({ - // @ts-expect-error alerts: updatedAlerts, - // @ts-expect-error existingAlerts, + previouslyRecoveredAlerts: {}, hasReachedAlertLimit: false, alertLimit: 10, + setFlapping: false, }); expect(recoveredAlerts).toEqual({ '2': updatedAlerts['2'] }); }); test('does not consider alert recovered if it has no actions but was not in original alerts list', () => { - const activeAlert = new Alert<{}, {}, DefaultActionGroupId>('1'); - const notRecoveredAlert = new Alert<{}, {}, DefaultActionGroupId>('2'); + const activeAlert = new Alert('1'); + const notRecoveredAlert = new Alert('2'); const existingAlerts = { '1': activeAlert, @@ -365,24 +399,24 @@ describe('processAlerts', () => { '2': notRecoveredAlert, }; - updatedAlerts['1'].scheduleActions('default', { foo: '1' }); + updatedAlerts['1'].scheduleActions('default' as never, { foo: '1' }); const { recoveredAlerts } = processAlerts({ - // @ts-expect-error alerts: updatedAlerts, - // @ts-expect-error existingAlerts, + previouslyRecoveredAlerts: {}, hasReachedAlertLimit: false, alertLimit: 10, + setFlapping: false, }); expect(recoveredAlerts).toEqual({}); }); test('updates duration in recovered alerts if start is available and adds end time', () => { - const activeAlert = new Alert<{}, {}, DefaultActionGroupId>('1'); - const recoveredAlert1 = new Alert<{}, {}, DefaultActionGroupId>('2'); - const recoveredAlert2 = new Alert<{}, {}, DefaultActionGroupId>('3'); + const activeAlert = new Alert('1'); + const recoveredAlert1 = new Alert('2'); + const recoveredAlert2 = new Alert('3'); const existingAlerts = { '1': activeAlert, @@ -394,15 +428,15 @@ describe('processAlerts', () => { const updatedAlerts = cloneDeep(existingAlerts); - updatedAlerts['1'].scheduleActions('default', { foo: '1' }); + updatedAlerts['1'].scheduleActions('default' as never, { foo: '1' }); const { recoveredAlerts } = processAlerts({ - // @ts-expect-error alerts: updatedAlerts, - // @ts-expect-error existingAlerts, + previouslyRecoveredAlerts: {}, hasReachedAlertLimit: false, alertLimit: 10, + setFlapping: false, }); expect(recoveredAlerts).toEqual({ '2': updatedAlerts['2'], '3': updatedAlerts['3'] }); @@ -410,26 +444,20 @@ describe('processAlerts', () => { const recoveredAlert1State = recoveredAlerts['2'].getState(); const recoveredAlert2State = recoveredAlerts['3'].getState(); - // @ts-expect-error expect(recoveredAlert1State.start).toEqual('1969-12-30T00:00:00.000Z'); - // @ts-expect-error expect(recoveredAlert2State.start).toEqual('1969-12-31T07:34:00.000Z'); - // @ts-expect-error expect(recoveredAlert1State.duration).toEqual('172800000000000'); - // @ts-expect-error expect(recoveredAlert2State.duration).toEqual('59160000000000'); - // @ts-expect-error expect(recoveredAlert1State.end).toEqual('1970-01-01T00:00:00.000Z'); - // @ts-expect-error expect(recoveredAlert2State.end).toEqual('1970-01-01T00:00:00.000Z'); }); test('does not update duration or set end in recovered alerts if start is not available', () => { - const activeAlert = new Alert<{}, {}, DefaultActionGroupId>('1'); - const recoveredAlert1 = new Alert<{}, {}, DefaultActionGroupId>('2'); - const recoveredAlert2 = new Alert<{}, {}, DefaultActionGroupId>('3'); + const activeAlert = new Alert('1'); + const recoveredAlert1 = new Alert('2'); + const recoveredAlert2 = new Alert('3'); const existingAlerts = { '1': activeAlert, @@ -438,15 +466,15 @@ describe('processAlerts', () => { }; const updatedAlerts = cloneDeep(existingAlerts); - updatedAlerts['1'].scheduleActions('default', { foo: '1' }); + updatedAlerts['1'].scheduleActions('default' as never, { foo: '1' }); const { recoveredAlerts } = processAlerts({ - // @ts-expect-error alerts: updatedAlerts, - // @ts-expect-error existingAlerts, + previouslyRecoveredAlerts: {}, hasReachedAlertLimit: false, alertLimit: 10, + setFlapping: false, }); expect(recoveredAlerts).toEqual({ '2': updatedAlerts['2'], '3': updatedAlerts['3'] }); @@ -454,32 +482,52 @@ describe('processAlerts', () => { const recoveredAlert1State = recoveredAlerts['2'].getState(); const recoveredAlert2State = recoveredAlerts['3'].getState(); - // @ts-expect-error expect(recoveredAlert1State.start).not.toBeDefined(); - // @ts-expect-error expect(recoveredAlert2State.start).not.toBeDefined(); - // @ts-expect-error expect(recoveredAlert1State.duration).not.toBeDefined(); - // @ts-expect-error expect(recoveredAlert2State.duration).not.toBeDefined(); - // @ts-expect-error expect(recoveredAlert1State.end).not.toBeDefined(); - // @ts-expect-error expect(recoveredAlert2State.end).not.toBeDefined(); }); + + test('considers alert recovered if it was previously recovered and not active', () => { + const recoveredAlert1 = new Alert('1'); + const recoveredAlert2 = new Alert('2'); + + const previouslyRecoveredAlerts = { + '1': recoveredAlert1, + '2': recoveredAlert2, + }; + + const updatedAlerts = cloneDeep(previouslyRecoveredAlerts); + + updatedAlerts['1'].setFlappingHistory([false]); + updatedAlerts['2'].setFlappingHistory([false]); + + const { recoveredAlerts } = processAlerts({ + alerts: {}, + existingAlerts: {}, + previouslyRecoveredAlerts, + hasReachedAlertLimit: false, + alertLimit: 10, + setFlapping: true, + }); + + expect(recoveredAlerts).toEqual(updatedAlerts); + }); }); describe('when hasReachedAlertLimit is true', () => { test('does not calculate recovered alerts', () => { - const existingAlert1 = new Alert<{}, {}, DefaultActionGroupId>('1'); - const existingAlert2 = new Alert<{}, {}, DefaultActionGroupId>('2'); - const existingAlert3 = new Alert<{}, {}, DefaultActionGroupId>('3'); - const existingAlert4 = new Alert<{}, {}, DefaultActionGroupId>('4'); - const existingAlert5 = new Alert<{}, {}, DefaultActionGroupId>('5'); - const newAlert6 = new Alert<{}, {}, DefaultActionGroupId>('6'); - const newAlert7 = new Alert<{}, {}, DefaultActionGroupId>('7'); + const existingAlert1 = new Alert('1'); + const existingAlert2 = new Alert('2'); + const existingAlert3 = new Alert('3'); + const existingAlert4 = new Alert('4'); + const existingAlert5 = new Alert('5'); + const newAlert6 = new Alert('6'); + const newAlert7 = new Alert('7'); const existingAlerts = { '1': existingAlert1, @@ -495,32 +543,32 @@ describe('processAlerts', () => { '7': newAlert7, }; - updatedAlerts['1'].scheduleActions('default', { foo: '1' }); - updatedAlerts['2'].scheduleActions('default', { foo: '1' }); - updatedAlerts['3'].scheduleActions('default', { foo: '2' }); - updatedAlerts['4'].scheduleActions('default', { foo: '2' }); + updatedAlerts['1'].scheduleActions('default' as never, { foo: '1' }); + updatedAlerts['2'].scheduleActions('default' as never, { foo: '1' }); + updatedAlerts['3'].scheduleActions('default' as never, { foo: '2' }); + updatedAlerts['4'].scheduleActions('default' as never, { foo: '2' }); // intentionally not scheduling actions for alert "5" - updatedAlerts['6'].scheduleActions('default', { foo: '2' }); - updatedAlerts['7'].scheduleActions('default', { foo: '2' }); + updatedAlerts['6'].scheduleActions('default' as never, { foo: '2' }); + updatedAlerts['7'].scheduleActions('default' as never, { foo: '2' }); const { recoveredAlerts } = processAlerts({ - // @ts-expect-error alerts: updatedAlerts, - // @ts-expect-error existingAlerts, + previouslyRecoveredAlerts: {}, hasReachedAlertLimit: true, alertLimit: 7, + setFlapping: false, }); expect(recoveredAlerts).toEqual({}); }); test('persists existing alerts', () => { - const existingAlert1 = new Alert<{}, {}, DefaultActionGroupId>('1'); - const existingAlert2 = new Alert<{}, {}, DefaultActionGroupId>('2'); - const existingAlert3 = new Alert<{}, {}, DefaultActionGroupId>('3'); - const existingAlert4 = new Alert<{}, {}, DefaultActionGroupId>('4'); - const existingAlert5 = new Alert<{}, {}, DefaultActionGroupId>('5'); + const existingAlert1 = new Alert('1'); + const existingAlert2 = new Alert('2'); + const existingAlert3 = new Alert('3'); + const existingAlert4 = new Alert('4'); + const existingAlert5 = new Alert('5'); const existingAlerts = { '1': existingAlert1, @@ -532,19 +580,19 @@ describe('processAlerts', () => { const updatedAlerts = cloneDeep(existingAlerts); - updatedAlerts['1'].scheduleActions('default', { foo: '1' }); - updatedAlerts['2'].scheduleActions('default', { foo: '1' }); - updatedAlerts['3'].scheduleActions('default', { foo: '2' }); - updatedAlerts['4'].scheduleActions('default', { foo: '2' }); + updatedAlerts['1'].scheduleActions('default' as never, { foo: '1' }); + updatedAlerts['2'].scheduleActions('default' as never, { foo: '1' }); + updatedAlerts['3'].scheduleActions('default' as never, { foo: '2' }); + updatedAlerts['4'].scheduleActions('default' as never, { foo: '2' }); // intentionally not scheduling actions for alert "5" const { activeAlerts } = processAlerts({ - // @ts-expect-error alerts: updatedAlerts, - // @ts-expect-error existingAlerts, + previouslyRecoveredAlerts: {}, hasReachedAlertLimit: true, alertLimit: 7, + setFlapping: false, }); expect(activeAlerts).toEqual({ @@ -558,16 +606,16 @@ describe('processAlerts', () => { test('adds new alerts up to max allowed', () => { const MAX_ALERTS = 7; - const existingAlert1 = new Alert<{}, {}, DefaultActionGroupId>('1'); - const existingAlert2 = new Alert<{}, {}, DefaultActionGroupId>('2'); - const existingAlert3 = new Alert<{}, {}, DefaultActionGroupId>('3'); - const existingAlert4 = new Alert<{}, {}, DefaultActionGroupId>('4'); - const existingAlert5 = new Alert<{}, {}, DefaultActionGroupId>('5'); - const newAlert6 = new Alert<{}, {}, DefaultActionGroupId>('6'); - const newAlert7 = new Alert<{}, {}, DefaultActionGroupId>('7'); - const newAlert8 = new Alert<{}, {}, DefaultActionGroupId>('8'); - const newAlert9 = new Alert<{}, {}, DefaultActionGroupId>('9'); - const newAlert10 = new Alert<{}, {}, DefaultActionGroupId>('10'); + const existingAlert1 = new Alert('1'); + const existingAlert2 = new Alert('2'); + const existingAlert3 = new Alert('3'); + const existingAlert4 = new Alert('4'); + const existingAlert5 = new Alert('5'); + const newAlert6 = new Alert('6'); + const newAlert7 = new Alert('7'); + const newAlert8 = new Alert('8'); + const newAlert9 = new Alert('9'); + const newAlert10 = new Alert('10'); const existingAlerts = { '1': existingAlert1, @@ -586,24 +634,24 @@ describe('processAlerts', () => { '10': newAlert10, }; - updatedAlerts['1'].scheduleActions('default', { foo: '1' }); - updatedAlerts['2'].scheduleActions('default', { foo: '1' }); - updatedAlerts['3'].scheduleActions('default', { foo: '2' }); - updatedAlerts['4'].scheduleActions('default', { foo: '2' }); + updatedAlerts['1'].scheduleActions('default' as never, { foo: '1' }); + updatedAlerts['2'].scheduleActions('default' as never, { foo: '1' }); + updatedAlerts['3'].scheduleActions('default' as never, { foo: '2' }); + updatedAlerts['4'].scheduleActions('default' as never, { foo: '2' }); // intentionally not scheduling actions for alert "5" - updatedAlerts['6'].scheduleActions('default', { foo: '2' }); - updatedAlerts['7'].scheduleActions('default', { foo: '2' }); - updatedAlerts['8'].scheduleActions('default', { foo: '2' }); - updatedAlerts['9'].scheduleActions('default', { foo: '2' }); - updatedAlerts['10'].scheduleActions('default', { foo: '2' }); + updatedAlerts['6'].scheduleActions('default' as never, { foo: '2' }); + updatedAlerts['7'].scheduleActions('default' as never, { foo: '2' }); + updatedAlerts['8'].scheduleActions('default' as never, { foo: '2' }); + updatedAlerts['9'].scheduleActions('default' as never, { foo: '2' }); + updatedAlerts['10'].scheduleActions('default' as never, { foo: '2' }); const { activeAlerts, newAlerts } = processAlerts({ - // @ts-expect-error alerts: updatedAlerts, - // @ts-expect-error existingAlerts, + previouslyRecoveredAlerts: {}, hasReachedAlertLimit: true, alertLimit: MAX_ALERTS, + setFlapping: false, }); expect(Object.keys(activeAlerts).length).toEqual(MAX_ALERTS); @@ -622,4 +670,547 @@ describe('processAlerts', () => { }); }); }); + + describe('updating flappingHistory', () => { + test('if new alert, set flapping state to true', () => { + const activeAlert = new Alert('1'); + + const alerts = cloneDeep({ '1': activeAlert }); + alerts['1'].scheduleActions('default' as never, { foo: '1' }); + + const { activeAlerts, newAlerts, recoveredAlerts } = processAlerts({ + alerts, + existingAlerts: {}, + previouslyRecoveredAlerts: {}, + hasReachedAlertLimit: false, + alertLimit: 10, + setFlapping: true, + }); + + expect(activeAlerts).toMatchInlineSnapshot(` + Object { + "1": Object { + "meta": Object { + "flappingHistory": Array [ + true, + ], + }, + "state": Object { + "duration": "0", + "start": "1970-01-01T00:00:00.000Z", + }, + }, + } + `); + expect(newAlerts).toMatchInlineSnapshot(` + Object { + "1": Object { + "meta": Object { + "flappingHistory": Array [ + true, + ], + }, + "state": Object { + "duration": "0", + "start": "1970-01-01T00:00:00.000Z", + }, + }, + } + `); + expect(recoveredAlerts).toMatchInlineSnapshot(`Object {}`); + }); + + test('if alert is still active, set flapping state to false', () => { + const activeAlert = new Alert('1', { + meta: { flappingHistory: [false] }, + }); + + const alerts = cloneDeep({ '1': activeAlert }); + alerts['1'].scheduleActions('default' as never, { foo: '1' }); + + const { activeAlerts, newAlerts, recoveredAlerts } = processAlerts({ + alerts, + existingAlerts: alerts, + previouslyRecoveredAlerts: {}, + hasReachedAlertLimit: false, + alertLimit: 10, + setFlapping: true, + }); + + expect(activeAlerts).toMatchInlineSnapshot(` + Object { + "1": Object { + "meta": Object { + "flappingHistory": Array [ + false, + false, + ], + }, + "state": Object {}, + }, + } + `); + expect(newAlerts).toMatchInlineSnapshot(`Object {}`); + expect(recoveredAlerts).toMatchInlineSnapshot(`Object {}`); + }); + + test('if alert is active and previously recovered, set flapping state to true', () => { + const activeAlert = new Alert('1'); + const recoveredAlert = new Alert('1', { + meta: { flappingHistory: [false] }, + }); + + const alerts = cloneDeep({ '1': activeAlert }); + alerts['1'].scheduleActions('default' as never, { foo: '1' }); + alerts['1'].setFlappingHistory([false]); + + const { activeAlerts, newAlerts, recoveredAlerts } = processAlerts({ + alerts, + existingAlerts: {}, + previouslyRecoveredAlerts: { '1': recoveredAlert }, + hasReachedAlertLimit: false, + alertLimit: 10, + setFlapping: true, + }); + + expect(activeAlerts).toMatchInlineSnapshot(` + Object { + "1": Object { + "meta": Object { + "flappingHistory": Array [ + false, + true, + ], + }, + "state": Object { + "duration": "0", + "start": "1970-01-01T00:00:00.000Z", + }, + }, + } + `); + expect(newAlerts).toMatchInlineSnapshot(` + Object { + "1": Object { + "meta": Object { + "flappingHistory": Array [ + false, + true, + ], + }, + "state": Object { + "duration": "0", + "start": "1970-01-01T00:00:00.000Z", + }, + }, + } + `); + expect(recoveredAlerts).toMatchInlineSnapshot(`Object {}`); + }); + + test('if alert is recovered and previously active, set flapping state to true', () => { + const activeAlert = new Alert('1', { + meta: { flappingHistory: [false] }, + }); + activeAlert.scheduleActions('default' as never, { foo: '1' }); + const recoveredAlert = new Alert('1', { + meta: { flappingHistory: [false] }, + }); + + const alerts = cloneDeep({ '1': recoveredAlert }); + + const { activeAlerts, newAlerts, recoveredAlerts } = processAlerts({ + alerts, + existingAlerts: { '1': activeAlert }, + previouslyRecoveredAlerts: {}, + hasReachedAlertLimit: false, + alertLimit: 10, + setFlapping: true, + }); + + expect(activeAlerts).toMatchInlineSnapshot(`Object {}`); + expect(newAlerts).toMatchInlineSnapshot(`Object {}`); + expect(recoveredAlerts).toMatchInlineSnapshot(` + Object { + "1": Object { + "meta": Object { + "flappingHistory": Array [ + false, + true, + ], + }, + "state": Object {}, + }, + } + `); + }); + + test('if alert is still recovered, set flapping state to false', () => { + const recoveredAlert = new Alert('1', { + meta: { flappingHistory: [false] }, + }); + + const alerts = cloneDeep({ '1': recoveredAlert }); + + const { activeAlerts, newAlerts, recoveredAlerts } = processAlerts({ + alerts: {}, + existingAlerts: {}, + previouslyRecoveredAlerts: alerts, + hasReachedAlertLimit: false, + alertLimit: 10, + setFlapping: true, + }); + + expect(activeAlerts).toMatchInlineSnapshot(`Object {}`); + expect(newAlerts).toMatchInlineSnapshot(`Object {}`); + expect(recoveredAlerts).toMatchInlineSnapshot(` + Object { + "1": Object { + "meta": Object { + "flappingHistory": Array [ + false, + false, + ], + }, + "state": Object {}, + }, + } + `); + }); + + test('if setFlapping is false should not update flappingHistory', () => { + const activeAlert1 = new Alert('1'); + activeAlert1.scheduleActions('default' as never, { foo: '1' }); + const activeAlert2 = new Alert('2', { + meta: { flappingHistory: [false] }, + }); + activeAlert2.scheduleActions('default' as never, { foo: '1' }); + const recoveredAlert = new Alert('3', { + meta: { flappingHistory: [false] }, + }); + + const previouslyRecoveredAlerts = cloneDeep({ '3': recoveredAlert }); + const alerts = cloneDeep({ '1': activeAlert1, '2': activeAlert2 }); + const existingAlerts = cloneDeep({ '2': activeAlert2 }); + + const { activeAlerts, newAlerts, recoveredAlerts } = processAlerts({ + alerts, + existingAlerts, + previouslyRecoveredAlerts, + hasReachedAlertLimit: false, + alertLimit: 10, + setFlapping: false, + }); + + expect(activeAlerts).toMatchInlineSnapshot(` + Object { + "1": Object { + "meta": Object { + "flappingHistory": Array [], + }, + "state": Object { + "duration": "0", + "start": "1970-01-01T00:00:00.000Z", + }, + }, + "2": Object { + "meta": Object { + "flappingHistory": Array [ + false, + ], + }, + "state": Object {}, + }, + } + `); + expect(newAlerts).toMatchInlineSnapshot(` + Object { + "1": Object { + "meta": Object { + "flappingHistory": Array [], + }, + "state": Object { + "duration": "0", + "start": "1970-01-01T00:00:00.000Z", + }, + }, + } + `); + expect(recoveredAlerts).toMatchInlineSnapshot(` + Object { + "3": Object { + "meta": Object { + "flappingHistory": Array [ + false, + ], + }, + "state": Object {}, + }, + } + `); + }); + + describe('when hasReachedAlertLimit is true', () => { + test('if alert is still active, set flapping state to false', () => { + const activeAlert = new Alert('1', { + meta: { flappingHistory: [false] }, + }); + + const alerts = cloneDeep({ '1': activeAlert }); + alerts['1'].scheduleActions('default' as never, { foo: '1' }); + + const { activeAlerts, newAlerts, recoveredAlerts } = processAlerts({ + alerts, + existingAlerts: alerts, + previouslyRecoveredAlerts: {}, + hasReachedAlertLimit: true, + alertLimit: 10, + setFlapping: true, + }); + + expect(activeAlerts).toMatchInlineSnapshot(` + Object { + "1": Object { + "meta": Object { + "flappingHistory": Array [ + false, + false, + ], + }, + "state": Object {}, + }, + } + `); + expect(newAlerts).toMatchInlineSnapshot(`Object {}`); + expect(recoveredAlerts).toMatchInlineSnapshot(`Object {}`); + }); + + test('if new alert, set flapping state to true', () => { + const activeAlert1 = new Alert('1', { + meta: { flappingHistory: [false] }, + }); + activeAlert1.scheduleActions('default' as never, { foo: '1' }); + const activeAlert2 = new Alert('1'); + activeAlert2.scheduleActions('default' as never, { foo: '1' }); + + const alerts = cloneDeep({ '1': activeAlert1, '2': activeAlert2 }); + + const { activeAlerts, newAlerts, recoveredAlerts } = processAlerts({ + alerts, + existingAlerts: { '1': activeAlert1 }, + previouslyRecoveredAlerts: {}, + hasReachedAlertLimit: true, + alertLimit: 10, + setFlapping: true, + }); + + expect(activeAlerts).toMatchInlineSnapshot(` + Object { + "1": Object { + "meta": Object { + "flappingHistory": Array [ + false, + false, + ], + }, + "state": Object {}, + }, + "2": Object { + "meta": Object { + "flappingHistory": Array [ + true, + ], + }, + "state": Object { + "duration": "0", + "start": "1970-01-01T00:00:00.000Z", + }, + }, + } + `); + expect(newAlerts).toMatchInlineSnapshot(` + Object { + "2": Object { + "meta": Object { + "flappingHistory": Array [ + true, + ], + }, + "state": Object { + "duration": "0", + "start": "1970-01-01T00:00:00.000Z", + }, + }, + } + `); + expect(recoveredAlerts).toMatchInlineSnapshot(`Object {}`); + }); + + test('if alert is active and previously recovered, set flapping state to true', () => { + const activeAlert1 = new Alert('1', { + meta: { flappingHistory: [false] }, + }); + activeAlert1.scheduleActions('default' as never, { foo: '1' }); + const activeAlert2 = new Alert('1'); + activeAlert2.scheduleActions('default' as never, { foo: '1' }); + + const alerts = cloneDeep({ '1': activeAlert1, '2': activeAlert2 }); + + const { activeAlerts, newAlerts, recoveredAlerts } = processAlerts({ + alerts, + existingAlerts: {}, + previouslyRecoveredAlerts: { '1': activeAlert1 }, + hasReachedAlertLimit: true, + alertLimit: 10, + setFlapping: true, + }); + + expect(activeAlerts).toMatchInlineSnapshot(` + Object { + "1": Object { + "meta": Object { + "flappingHistory": Array [ + false, + true, + ], + }, + "state": Object { + "duration": "0", + "start": "1970-01-01T00:00:00.000Z", + }, + }, + "2": Object { + "meta": Object { + "flappingHistory": Array [ + true, + ], + }, + "state": Object { + "duration": "0", + "start": "1970-01-01T00:00:00.000Z", + }, + }, + } + `); + expect(newAlerts).toMatchInlineSnapshot(` + Object { + "1": Object { + "meta": Object { + "flappingHistory": Array [ + false, + true, + ], + }, + "state": Object { + "duration": "0", + "start": "1970-01-01T00:00:00.000Z", + }, + }, + "2": Object { + "meta": Object { + "flappingHistory": Array [ + true, + ], + }, + "state": Object { + "duration": "0", + "start": "1970-01-01T00:00:00.000Z", + }, + }, + } + `); + expect(recoveredAlerts).toMatchInlineSnapshot(`Object {}`); + }); + + test('if setFlapping is false should not update flappingHistory', () => { + const activeAlert1 = new Alert('1', { + meta: { flappingHistory: [false] }, + }); + activeAlert1.scheduleActions('default' as never, { foo: '1' }); + const activeAlert2 = new Alert('1'); + activeAlert2.scheduleActions('default' as never, { foo: '1' }); + + const alerts = cloneDeep({ '1': activeAlert1, '2': activeAlert2 }); + + const { activeAlerts, newAlerts, recoveredAlerts } = processAlerts({ + alerts, + existingAlerts: { '1': activeAlert1 }, + previouslyRecoveredAlerts: {}, + hasReachedAlertLimit: true, + alertLimit: 10, + setFlapping: false, + }); + + expect(activeAlerts).toMatchInlineSnapshot(` + Object { + "1": Object { + "meta": Object { + "flappingHistory": Array [ + false, + ], + }, + "state": Object {}, + }, + "2": Object { + "meta": Object { + "flappingHistory": Array [], + }, + "state": Object { + "duration": "0", + "start": "1970-01-01T00:00:00.000Z", + }, + }, + } + `); + expect(newAlerts).toMatchInlineSnapshot(` + Object { + "2": Object { + "meta": Object { + "flappingHistory": Array [], + }, + "state": Object { + "duration": "0", + "start": "1970-01-01T00:00:00.000Z", + }, + }, + } + `); + expect(recoveredAlerts).toMatchInlineSnapshot(`Object {}`); + }); + }); + }); + + describe('updateAlertFlappingHistory function', () => { + test('correctly updates flappingHistory', () => { + const alert = new Alert('1', { + meta: { flappingHistory: [false, false] }, + }); + updateAlertFlappingHistory(alert, true); + expect(alert.getFlappingHistory()).toEqual([false, false, true]); + }); + + test('correctly updates flappingHistory while maintaining a fixed size', () => { + const flappingHistory = new Array(20).fill(false); + const alert = new Alert('1', { + meta: { flappingHistory }, + }); + updateAlertFlappingHistory(alert, true); + const fh = alert.getFlappingHistory() || []; + expect(fh.length).toEqual(20); + const result = new Array(19).fill(false); + expect(fh).toEqual(result.concat(true)); + }); + + test('correctly updates flappingHistory while maintaining if array is larger than fixed size', () => { + const flappingHistory = new Array(23).fill(false); + const alert = new Alert('1', { + meta: { flappingHistory }, + }); + updateAlertFlappingHistory(alert, true); + const fh = alert.getFlappingHistory() || []; + expect(fh.length).toEqual(20); + const result = new Array(19).fill(false); + expect(fh).toEqual(result.concat(true)); + }); + }); }); diff --git a/x-pack/plugins/alerting/server/lib/process_alerts.ts b/x-pack/plugins/alerting/server/lib/process_alerts.ts index c0352a06f2eba..40c86dc461ab0 100644 --- a/x-pack/plugins/alerting/server/lib/process_alerts.ts +++ b/x-pack/plugins/alerting/server/lib/process_alerts.ts @@ -9,6 +9,7 @@ import { millisToNanos } from '@kbn/event-log-plugin/server'; import { cloneDeep } from 'lodash'; import { Alert } from '../alert'; import { AlertInstanceState, AlertInstanceContext } from '../types'; +import { updateFlappingHistory } from './flapping_utils'; interface ProcessAlertsOpts< State extends AlertInstanceState, @@ -16,8 +17,11 @@ interface ProcessAlertsOpts< > { alerts: Record>; existingAlerts: Record>; + previouslyRecoveredAlerts: Record>; hasReachedAlertLimit: boolean; alertLimit: number; + // flag used to determine whether or not we want to push the flapping state on to the flappingHistory array + setFlapping: boolean; } interface ProcessAlertsResult< State extends AlertInstanceState, @@ -27,6 +31,8 @@ interface ProcessAlertsResult< > { newAlerts: Record>; activeAlerts: Record>; + // recovered alerts in the current rule run that were previously active + currentRecoveredAlerts: Record>; recoveredAlerts: Record>; } @@ -38,8 +44,10 @@ export function processAlerts< >({ alerts, existingAlerts, + previouslyRecoveredAlerts, hasReachedAlertLimit, alertLimit, + setFlapping, }: ProcessAlertsOpts): ProcessAlertsResult< State, Context, @@ -47,8 +55,14 @@ export function processAlerts< RecoveryActionGroupId > { return hasReachedAlertLimit - ? processAlertsLimitReached(alerts, existingAlerts, alertLimit) - : processAlertsHelper(alerts, existingAlerts); + ? processAlertsLimitReached( + alerts, + existingAlerts, + previouslyRecoveredAlerts, + alertLimit, + setFlapping + ) + : processAlertsHelper(alerts, existingAlerts, previouslyRecoveredAlerts, setFlapping); } function processAlertsHelper< @@ -58,13 +72,17 @@ function processAlertsHelper< RecoveryActionGroupId extends string >( alerts: Record>, - existingAlerts: Record> + existingAlerts: Record>, + previouslyRecoveredAlerts: Record>, + setFlapping: boolean ): ProcessAlertsResult { const existingAlertIds = new Set(Object.keys(existingAlerts)); + const previouslyRecoveredAlertsIds = new Set(Object.keys(previouslyRecoveredAlerts)); const currentTime = new Date().toISOString(); const newAlerts: Record> = {}; const activeAlerts: Record> = {}; + const currentRecoveredAlerts: Record> = {}; const recoveredAlerts: Record> = {}; for (const id in alerts) { @@ -73,13 +91,20 @@ function processAlertsHelper< if (alerts[id].hasScheduledActions()) { activeAlerts[id] = alerts[id]; - // if this alert did not exist in previous run, it is considered "new" + // if this alert was not active in the previous run, we need to inject start time into the alert state if (!existingAlertIds.has(id)) { newAlerts[id] = alerts[id]; - - // Inject start time into alert state for new alerts const state = newAlerts[id].getState(); newAlerts[id].replaceState({ ...state, start: currentTime, duration: '0' }); + + if (setFlapping) { + if (previouslyRecoveredAlertsIds.has(id)) { + // this alert has flapped from recovered to active + newAlerts[id].setFlappingHistory(previouslyRecoveredAlerts[id].getFlappingHistory()); + previouslyRecoveredAlertsIds.delete(id); + } + updateAlertFlappingHistory(newAlerts[id], true); + } } else { // this alert did exist in previous run // calculate duration to date for active alerts @@ -92,9 +117,15 @@ function processAlertsHelper< ...(state.start ? { start: state.start } : {}), ...(duration !== undefined ? { duration } : {}), }); + + // this alert is still active + if (setFlapping) { + updateAlertFlappingHistory(activeAlerts[id], false); + } } } else if (existingAlertIds.has(id)) { recoveredAlerts[id] = alerts[id]; + currentRecoveredAlerts[id] = alerts[id]; // Inject end time into alert state of recovered alerts const state = recoveredAlerts[id].getState(); @@ -106,10 +137,23 @@ function processAlertsHelper< ...(duration ? { duration } : {}), ...(state.start ? { end: currentTime } : {}), }); + // this alert has flapped from active to recovered + if (setFlapping) { + updateAlertFlappingHistory(recoveredAlerts[id], true); + } } } } - return { recoveredAlerts, newAlerts, activeAlerts }; + + // alerts are still recovered + for (const id of previouslyRecoveredAlertsIds) { + recoveredAlerts[id] = previouslyRecoveredAlerts[id]; + if (setFlapping) { + updateAlertFlappingHistory(recoveredAlerts[id], false); + } + } + + return { recoveredAlerts, currentRecoveredAlerts, newAlerts, activeAlerts }; } function processAlertsLimitReached< @@ -120,9 +164,12 @@ function processAlertsLimitReached< >( alerts: Record>, existingAlerts: Record>, - alertLimit: number + previouslyRecoveredAlerts: Record>, + alertLimit: number, + setFlapping: boolean ): ProcessAlertsResult { const existingAlertIds = new Set(Object.keys(existingAlerts)); + const previouslyRecoveredAlertsIds = new Set(Object.keys(previouslyRecoveredAlerts)); // When the alert limit has been reached, // - skip determination of recovered alerts @@ -152,6 +199,11 @@ function processAlertsLimitReached< ...(state.start ? { start: state.start } : {}), ...(duration !== undefined ? { duration } : {}), }); + + // this alert is still active + if (setFlapping) { + updateAlertFlappingHistory(activeAlerts[id], false); + } } } @@ -161,7 +213,7 @@ function processAlertsLimitReached< // if we don't have capacity for new alerts, return if (!hasCapacityForNewAlerts()) { - return { recoveredAlerts: {}, newAlerts: {}, activeAlerts }; + return { recoveredAlerts: {}, currentRecoveredAlerts: {}, newAlerts: {}, activeAlerts }; } // look for new alerts and add until we hit capacity @@ -171,16 +223,33 @@ function processAlertsLimitReached< if (!existingAlertIds.has(id)) { activeAlerts[id] = alerts[id]; newAlerts[id] = alerts[id]; - - // Inject start time into alert state for new alerts + // if this alert was not active in the previous run, we need to inject start time into the alert state const state = newAlerts[id].getState(); newAlerts[id].replaceState({ ...state, start: currentTime, duration: '0' }); + if (setFlapping) { + if (previouslyRecoveredAlertsIds.has(id)) { + // this alert has flapped from recovered to active + newAlerts[id].setFlappingHistory(previouslyRecoveredAlerts[id].getFlappingHistory()); + } + updateAlertFlappingHistory(newAlerts[id], true); + } + if (!hasCapacityForNewAlerts()) { break; } } } } - return { recoveredAlerts: {}, newAlerts, activeAlerts }; + return { recoveredAlerts: {}, currentRecoveredAlerts: {}, newAlerts, activeAlerts }; +} + +export function updateAlertFlappingHistory< + State extends AlertInstanceState, + Context extends AlertInstanceContext, + ActionGroupIds extends string, + RecoveryActionGroupId extends string +>(alert: Alert, state: boolean) { + const updatedFlappingHistory = updateFlappingHistory(alert.getFlappingHistory() || [], state); + alert.setFlappingHistory(updatedFlappingHistory); } diff --git a/x-pack/plugins/alerting/server/lib/set_flapping.test.ts b/x-pack/plugins/alerting/server/lib/set_flapping.test.ts new file mode 100644 index 0000000000000..9900d3391861b --- /dev/null +++ b/x-pack/plugins/alerting/server/lib/set_flapping.test.ts @@ -0,0 +1,166 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { pick } from 'lodash'; +import { Alert } from '../alert'; +import { AlertInstanceState, AlertInstanceContext, DefaultActionGroupId } from '../../common'; +import { setFlapping, isAlertFlapping } from './set_flapping'; + +describe('setFlapping', () => { + const flapping = new Array(16).fill(false).concat([true, true, true, true]); + const notFlapping = new Array(20).fill(false); + + test('should set flapping on alerts', () => { + const activeAlerts = { + '1': new Alert('1', { meta: { flappingHistory: flapping } }), + '2': new Alert('2', { meta: { flappingHistory: [false, false] } }), + '3': new Alert('3', { meta: { flapping: true, flappingHistory: flapping } }), + '4': new Alert('4', { meta: { flapping: true, flappingHistory: [false, false] } }), + }; + + const recoveredAlerts = { + '1': new Alert('1', { meta: { flappingHistory: [true, true, true, true] } }), + '2': new Alert('2', { meta: { flappingHistory: notFlapping } }), + '3': new Alert('3', { meta: { flapping: true, flappingHistory: [true, true] } }), + '4': new Alert('4', { meta: { flapping: true, flappingHistory: notFlapping } }), + }; + + setFlapping(activeAlerts, recoveredAlerts); + const fields = ['1.meta.flapping', '2.meta.flapping', '3.meta.flapping', '4.meta.flapping']; + expect(pick(activeAlerts, fields)).toMatchInlineSnapshot(` + Object { + "1": Object { + "meta": Object { + "flapping": true, + }, + }, + "2": Object { + "meta": Object { + "flapping": false, + }, + }, + "3": Object { + "meta": Object { + "flapping": true, + }, + }, + "4": Object { + "meta": Object { + "flapping": true, + }, + }, + } + `); + expect(pick(recoveredAlerts, fields)).toMatchInlineSnapshot(` + Object { + "1": Object { + "meta": Object { + "flapping": true, + }, + }, + "2": Object { + "meta": Object { + "flapping": false, + }, + }, + "3": Object { + "meta": Object { + "flapping": true, + }, + }, + "4": Object { + "meta": Object { + "flapping": false, + }, + }, + } + `); + }); + + describe('isAlertFlapping', () => { + describe('not currently flapping', () => { + test('returns true if the flap count exceeds the threshold', () => { + const flappingHistory = [true, true, true, true].concat(new Array(16).fill(false)); + const alert = new Alert( + '1', + { + meta: { flappingHistory }, + } + ); + expect(isAlertFlapping(alert)).toEqual(true); + }); + + test("returns false the flap count doesn't exceed the threshold", () => { + const flappingHistory = [true, true].concat(new Array(20).fill(false)); + const alert = new Alert( + '1', + { + meta: { flappingHistory }, + } + ); + expect(isAlertFlapping(alert)).toEqual(false); + }); + + test('returns true if not at capacity and the flap count exceeds the threshold', () => { + const flappingHistory = new Array(5).fill(true); + const alert = new Alert( + '1', + { + meta: { flappingHistory }, + } + ); + expect(isAlertFlapping(alert)).toEqual(true); + }); + }); + + describe('currently flapping', () => { + test('returns true if at capacity and the flap count exceeds the threshold', () => { + const flappingHistory = new Array(16).fill(false).concat([true, true, true, true]); + const alert = new Alert( + '1', + { + meta: { flappingHistory, flapping: true }, + } + ); + expect(isAlertFlapping(alert)).toEqual(true); + }); + + test("returns true if not at capacity and the flap count doesn't exceed the threshold", () => { + const flappingHistory = new Array(16).fill(false); + const alert = new Alert( + '1', + { + meta: { flappingHistory, flapping: true }, + } + ); + expect(isAlertFlapping(alert)).toEqual(true); + }); + + test('returns true if not at capacity and the flap count exceeds the threshold', () => { + const flappingHistory = new Array(10).fill(false).concat([true, true, true, true]); + const alert = new Alert( + '1', + { + meta: { flappingHistory, flapping: true }, + } + ); + expect(isAlertFlapping(alert)).toEqual(true); + }); + + test("returns false if at capacity and the flap count doesn't exceed the threshold", () => { + const flappingHistory = new Array(20).fill(false); + const alert = new Alert( + '1', + { + meta: { flappingHistory, flapping: true }, + } + ); + expect(isAlertFlapping(alert)).toEqual(false); + }); + }); + }); +}); diff --git a/x-pack/plugins/alerting/server/lib/set_flapping.ts b/x-pack/plugins/alerting/server/lib/set_flapping.ts new file mode 100644 index 0000000000000..2e941cf06e07c --- /dev/null +++ b/x-pack/plugins/alerting/server/lib/set_flapping.ts @@ -0,0 +1,44 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { keys } from 'lodash'; +import { Alert } from '../alert'; +import { AlertInstanceState, AlertInstanceContext } from '../types'; +import { isFlapping } from './flapping_utils'; + +export function setFlapping< + State extends AlertInstanceState, + Context extends AlertInstanceContext, + ActionGroupIds extends string, + RecoveryActionGroupIds extends string +>( + activeAlerts: Record> = {}, + recoveredAlerts: Record> = {} +) { + for (const id of keys(activeAlerts)) { + const alert = activeAlerts[id]; + const flapping = isAlertFlapping(alert); + alert.setFlapping(flapping); + } + + for (const id of keys(recoveredAlerts)) { + const alert = recoveredAlerts[id]; + const flapping = isAlertFlapping(alert); + alert.setFlapping(flapping); + } +} + +export function isAlertFlapping< + State extends AlertInstanceState, + Context extends AlertInstanceContext, + ActionGroupIds extends string, + RecoveryActionGroupId extends string +>(alert: Alert): boolean { + const flappingHistory: boolean[] = alert.getFlappingHistory() || []; + const isCurrentlyFlapping = alert.getFlapping(); + return isFlapping(flappingHistory, isCurrentlyFlapping); +} diff --git a/x-pack/plugins/alerting/server/rules_client.mock.ts b/x-pack/plugins/alerting/server/rules_client.mock.ts index ba7654694da26..e9c3c3a0cee73 100644 --- a/x-pack/plugins/alerting/server/rules_client.mock.ts +++ b/x-pack/plugins/alerting/server/rules_client.mock.ts @@ -42,7 +42,6 @@ const createRulesClientMock = () => { bulkDisableRules: jest.fn(), snooze: jest.fn(), unsnooze: jest.fn(), - calculateIsSnoozedUntil: jest.fn(), clearExpiredSnoozes: jest.fn(), runSoon: jest.fn(), clone: jest.fn(), diff --git a/x-pack/plugins/alerting/server/rules_client/common/api_key_as_alert_attributes.ts b/x-pack/plugins/alerting/server/rules_client/common/api_key_as_alert_attributes.ts new file mode 100644 index 0000000000000..e98fb875b74a7 --- /dev/null +++ b/x-pack/plugins/alerting/server/rules_client/common/api_key_as_alert_attributes.ts @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { RawRule } from '../../types'; +import { CreateAPIKeyResult } from '../types'; + +export function apiKeyAsAlertAttributes( + apiKey: CreateAPIKeyResult | null, + username: string | null +): Pick { + return apiKey && apiKey.apiKeysEnabled + ? { + apiKeyOwner: username, + apiKey: Buffer.from(`${apiKey.result.id}:${apiKey.result.api_key}`).toString('base64'), + } + : { + apiKeyOwner: null, + apiKey: null, + }; +} diff --git a/x-pack/plugins/alerting/server/rules_client/lib/apply_bulk_edit_operation.test.ts b/x-pack/plugins/alerting/server/rules_client/common/apply_bulk_edit_operation.test.ts similarity index 100% rename from x-pack/plugins/alerting/server/rules_client/lib/apply_bulk_edit_operation.test.ts rename to x-pack/plugins/alerting/server/rules_client/common/apply_bulk_edit_operation.test.ts diff --git a/x-pack/plugins/alerting/server/rules_client/lib/apply_bulk_edit_operation.ts b/x-pack/plugins/alerting/server/rules_client/common/apply_bulk_edit_operation.ts similarity index 95% rename from x-pack/plugins/alerting/server/rules_client/lib/apply_bulk_edit_operation.ts rename to x-pack/plugins/alerting/server/rules_client/common/apply_bulk_edit_operation.ts index 360bd0d72a5fa..e40d8e6c8c854 100644 --- a/x-pack/plugins/alerting/server/rules_client/lib/apply_bulk_edit_operation.ts +++ b/x-pack/plugins/alerting/server/rules_client/common/apply_bulk_edit_operation.ts @@ -5,7 +5,7 @@ * 2.0. */ import { set, get } from 'lodash'; -import type { BulkEditOperation, BulkEditFields } from '../rules_client'; +import type { BulkEditOperation, BulkEditFields } from '../types'; // defining an union type that will passed directly to generic function as a workaround for the issue similar to // https://github.com/microsoft/TypeScript/issues/29479 diff --git a/x-pack/plugins/alerting/server/rules_client/audit_events.test.ts b/x-pack/plugins/alerting/server/rules_client/common/audit_events.test.ts similarity index 100% rename from x-pack/plugins/alerting/server/rules_client/audit_events.test.ts rename to x-pack/plugins/alerting/server/rules_client/common/audit_events.test.ts diff --git a/x-pack/plugins/alerting/server/rules_client/audit_events.ts b/x-pack/plugins/alerting/server/rules_client/common/audit_events.ts similarity index 100% rename from x-pack/plugins/alerting/server/rules_client/audit_events.ts rename to x-pack/plugins/alerting/server/rules_client/common/audit_events.ts diff --git a/x-pack/plugins/alerting/server/rules_client/lib/build_kuery_node_filter.test.ts b/x-pack/plugins/alerting/server/rules_client/common/build_kuery_node_filter.test.ts similarity index 100% rename from x-pack/plugins/alerting/server/rules_client/lib/build_kuery_node_filter.test.ts rename to x-pack/plugins/alerting/server/rules_client/common/build_kuery_node_filter.test.ts diff --git a/x-pack/plugins/alerting/server/rules_client/lib/build_kuery_node_filter.ts b/x-pack/plugins/alerting/server/rules_client/common/build_kuery_node_filter.ts similarity index 100% rename from x-pack/plugins/alerting/server/rules_client/lib/build_kuery_node_filter.ts rename to x-pack/plugins/alerting/server/rules_client/common/build_kuery_node_filter.ts diff --git a/x-pack/plugins/alerting/server/rules_client/common/calculate_is_snoozed_until.ts b/x-pack/plugins/alerting/server/rules_client/common/calculate_is_snoozed_until.ts new file mode 100644 index 0000000000000..d77a013ae3f6b --- /dev/null +++ b/x-pack/plugins/alerting/server/rules_client/common/calculate_is_snoozed_until.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { RuleSnooze } from '../../types'; +import { getRuleSnoozeEndTime } from '../../lib'; + +export function calculateIsSnoozedUntil(rule: { + muteAll: boolean; + snoozeSchedule?: RuleSnooze; +}): string | null { + const isSnoozedUntil = getRuleSnoozeEndTime(rule); + return isSnoozedUntil ? isSnoozedUntil.toISOString() : null; +} diff --git a/x-pack/plugins/alerting/server/rules_client/common/constants.ts b/x-pack/plugins/alerting/server/rules_client/common/constants.ts new file mode 100644 index 0000000000000..d72f80d41a91e --- /dev/null +++ b/x-pack/plugins/alerting/server/rules_client/common/constants.ts @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + AlertingAuthorizationFilterType, + AlertingAuthorizationFilterOpts, +} from '../../authorization'; + +// NOTE: Changing this prefix will require a migration to update the prefix in all existing `rule` saved objects +export const extractedSavedObjectParamReferenceNamePrefix = 'param:'; + +// NOTE: Changing this prefix will require a migration to update the prefix in all existing `rule` saved objects +export const preconfiguredConnectorActionRefPrefix = 'preconfigured:'; + +export const alertingAuthorizationFilterOpts: AlertingAuthorizationFilterOpts = { + type: AlertingAuthorizationFilterType.KQL, + fieldNames: { ruleTypeId: 'alert.attributes.alertTypeId', consumer: 'alert.attributes.consumer' }, +}; + +export const MAX_RULES_NUMBER_FOR_BULK_OPERATION = 10000; +export const API_KEY_GENERATE_CONCURRENCY = 50; +export const RULE_TYPE_CHECKS_CONCURRENCY = 50; diff --git a/x-pack/plugins/alerting/server/rules_client/common/generate_api_key_name.ts b/x-pack/plugins/alerting/server/rules_client/common/generate_api_key_name.ts new file mode 100644 index 0000000000000..bb5c8ae9bac23 --- /dev/null +++ b/x-pack/plugins/alerting/server/rules_client/common/generate_api_key_name.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { truncate, trim } from 'lodash'; + +export function generateAPIKeyName(alertTypeId: string, alertName: string) { + return truncate(`Alerting: ${alertTypeId}/${trim(alertName)}`, { length: 256 }); +} diff --git a/x-pack/plugins/alerting/server/rules_client/common/get_and_validate_common_bulk_options.ts b/x-pack/plugins/alerting/server/rules_client/common/get_and_validate_common_bulk_options.ts new file mode 100644 index 0000000000000..e4497cce1e30b --- /dev/null +++ b/x-pack/plugins/alerting/server/rules_client/common/get_and_validate_common_bulk_options.ts @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import Boom from '@hapi/boom'; +import { BulkOptions, BulkOptionsFilter, BulkOptionsIds } from '../types'; + +export const getAndValidateCommonBulkOptions = (options: BulkOptions) => { + const filter = (options as BulkOptionsFilter).filter; + const ids = (options as BulkOptionsIds).ids; + + if (!ids && !filter) { + throw Boom.badRequest( + "Either 'ids' or 'filter' property in method's arguments should be provided" + ); + } + + if (ids?.length === 0) { + throw Boom.badRequest("'ids' property should not be an empty array"); + } + + if (ids && filter) { + throw Boom.badRequest( + "Both 'filter' and 'ids' are supplied. Define either 'ids' or 'filter' properties in method's arguments" + ); + } + return { ids, filter }; +}; diff --git a/x-pack/plugins/alerting/server/rules_client/common/include_fields_required_for_authentication.ts b/x-pack/plugins/alerting/server/rules_client/common/include_fields_required_for_authentication.ts new file mode 100644 index 0000000000000..b89d56174c59b --- /dev/null +++ b/x-pack/plugins/alerting/server/rules_client/common/include_fields_required_for_authentication.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { uniq } from 'lodash'; + +export function includeFieldsRequiredForAuthentication(fields: string[]): string[] { + return uniq([...fields, 'alertTypeId', 'consumer']); +} diff --git a/x-pack/plugins/alerting/server/rules_client/common/index.ts b/x-pack/plugins/alerting/server/rules_client/common/index.ts new file mode 100644 index 0000000000000..6604c417cc639 --- /dev/null +++ b/x-pack/plugins/alerting/server/rules_client/common/index.ts @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { mapSortField } from './map_sort_field'; +export { validateOperationOnAttributes } from './validate_attributes'; +export { retryIfBulkEditConflicts } from './retry_if_bulk_edit_conflicts'; +export { retryIfBulkDeleteConflicts } from './retry_if_bulk_delete_conflicts'; +export { retryIfBulkDisableConflicts } from './retry_if_bulk_disable_conflicts'; +export { retryIfBulkOperationConflicts } from './retry_if_bulk_operation_conflicts'; +export { applyBulkEditOperation } from './apply_bulk_edit_operation'; +export { buildKueryNodeFilter } from './build_kuery_node_filter'; +export { generateAPIKeyName } from './generate_api_key_name'; +export * from './mapped_params_utils'; +export { apiKeyAsAlertAttributes } from './api_key_as_alert_attributes'; +export { calculateIsSnoozedUntil } from './calculate_is_snoozed_until'; +export * from './inject_references'; +export { parseDate } from './parse_date'; +export { includeFieldsRequiredForAuthentication } from './include_fields_required_for_authentication'; +export { getAndValidateCommonBulkOptions } from './get_and_validate_common_bulk_options'; +export * from './snooze_utils'; diff --git a/x-pack/plugins/alerting/server/rules_client/common/inject_references.ts b/x-pack/plugins/alerting/server/rules_client/common/inject_references.ts new file mode 100644 index 0000000000000..07565240ed5c4 --- /dev/null +++ b/x-pack/plugins/alerting/server/rules_client/common/inject_references.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 Boom from '@hapi/boom'; +import { omit } from 'lodash'; +import { SavedObjectReference, SavedObjectAttributes } from '@kbn/core/server'; +import { UntypedNormalizedRuleType } from '../../rule_type_registry'; +import { Rule, RawRule, RuleTypeParams } from '../../types'; +import { + preconfiguredConnectorActionRefPrefix, + extractedSavedObjectParamReferenceNamePrefix, +} from './constants'; + +export function injectReferencesIntoActions( + alertId: string, + actions: RawRule['actions'], + references: SavedObjectReference[] +) { + return actions.map((action) => { + if (action.actionRef.startsWith(preconfiguredConnectorActionRefPrefix)) { + return { + ...omit(action, 'actionRef'), + id: action.actionRef.replace(preconfiguredConnectorActionRefPrefix, ''), + }; + } + + const reference = references.find((ref) => ref.name === action.actionRef); + if (!reference) { + throw new Error(`Action reference "${action.actionRef}" not found in alert id: ${alertId}`); + } + return { + ...omit(action, 'actionRef'), + id: reference.id, + }; + }) as Rule['actions']; +} + +export function injectReferencesIntoParams< + Params extends RuleTypeParams, + ExtractedParams extends RuleTypeParams +>( + ruleId: string, + ruleType: UntypedNormalizedRuleType, + ruleParams: SavedObjectAttributes | undefined, + references: SavedObjectReference[] +): Params { + try { + const paramReferences = references + .filter((reference: SavedObjectReference) => + reference.name.startsWith(extractedSavedObjectParamReferenceNamePrefix) + ) + .map((reference: SavedObjectReference) => ({ + ...reference, + name: reference.name.replace(extractedSavedObjectParamReferenceNamePrefix, ''), + })); + return ruleParams && ruleType?.useSavedObjectReferences?.injectReferences + ? (ruleType.useSavedObjectReferences.injectReferences( + ruleParams as ExtractedParams, + paramReferences + ) as Params) + : (ruleParams as Params); + } catch (err) { + throw Boom.badRequest( + `Error injecting reference into rule params for rule id ${ruleId} - ${err.message}` + ); + } +} diff --git a/x-pack/plugins/alerting/server/rules_client/lib/map_sort_field.test.ts b/x-pack/plugins/alerting/server/rules_client/common/map_sort_field.test.ts similarity index 100% rename from x-pack/plugins/alerting/server/rules_client/lib/map_sort_field.test.ts rename to x-pack/plugins/alerting/server/rules_client/common/map_sort_field.test.ts diff --git a/x-pack/plugins/alerting/server/rules_client/lib/map_sort_field.ts b/x-pack/plugins/alerting/server/rules_client/common/map_sort_field.ts similarity index 100% rename from x-pack/plugins/alerting/server/rules_client/lib/map_sort_field.ts rename to x-pack/plugins/alerting/server/rules_client/common/map_sort_field.ts diff --git a/x-pack/plugins/alerting/server/rules_client/lib/mapped_params_utils.test.ts b/x-pack/plugins/alerting/server/rules_client/common/mapped_params_utils.test.ts similarity index 100% rename from x-pack/plugins/alerting/server/rules_client/lib/mapped_params_utils.test.ts rename to x-pack/plugins/alerting/server/rules_client/common/mapped_params_utils.test.ts diff --git a/x-pack/plugins/alerting/server/rules_client/lib/mapped_params_utils.ts b/x-pack/plugins/alerting/server/rules_client/common/mapped_params_utils.ts similarity index 100% rename from x-pack/plugins/alerting/server/rules_client/lib/mapped_params_utils.ts rename to x-pack/plugins/alerting/server/rules_client/common/mapped_params_utils.ts diff --git a/x-pack/plugins/alerting/server/rules_client/common/parse_date.ts b/x-pack/plugins/alerting/server/rules_client/common/parse_date.ts new file mode 100644 index 0000000000000..21c005605ea6f --- /dev/null +++ b/x-pack/plugins/alerting/server/rules_client/common/parse_date.ts @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import Boom from '@hapi/boom'; +import { i18n } from '@kbn/i18n'; +import { parseIsoOrRelativeDate } from '../../lib/iso_or_relative_date'; + +export function parseDate( + dateString: string | undefined, + propertyName: string, + defaultValue: Date +): Date { + if (dateString === undefined) { + return defaultValue; + } + + const parsedDate = parseIsoOrRelativeDate(dateString); + if (parsedDate === undefined) { + throw Boom.badRequest( + i18n.translate('xpack.alerting.rulesClient.invalidDate', { + defaultMessage: 'Invalid date for parameter {field}: "{dateValue}"', + values: { + field: propertyName, + dateValue: dateString, + }, + }) + ); + } + + return parsedDate; +} diff --git a/x-pack/plugins/alerting/server/rules_client/lib/retry_if_bulk_delete_conflicts.test.ts b/x-pack/plugins/alerting/server/rules_client/common/retry_if_bulk_delete_conflicts.test.ts similarity index 100% rename from x-pack/plugins/alerting/server/rules_client/lib/retry_if_bulk_delete_conflicts.test.ts rename to x-pack/plugins/alerting/server/rules_client/common/retry_if_bulk_delete_conflicts.test.ts diff --git a/x-pack/plugins/alerting/server/rules_client/lib/retry_if_bulk_delete_conflicts.ts b/x-pack/plugins/alerting/server/rules_client/common/retry_if_bulk_delete_conflicts.ts similarity index 98% rename from x-pack/plugins/alerting/server/rules_client/lib/retry_if_bulk_delete_conflicts.ts rename to x-pack/plugins/alerting/server/rules_client/common/retry_if_bulk_delete_conflicts.ts index 0c2bac9695c85..46a0932253276 100644 --- a/x-pack/plugins/alerting/server/rules_client/lib/retry_if_bulk_delete_conflicts.ts +++ b/x-pack/plugins/alerting/server/rules_client/common/retry_if_bulk_delete_conflicts.ts @@ -10,7 +10,7 @@ import { chunk } from 'lodash'; import { KueryNode } from '@kbn/es-query'; import { Logger } from '@kbn/core/server'; import { convertRuleIdsToKueryNode } from '../../lib'; -import { BulkOperationError } from '../rules_client'; +import { BulkOperationError } from '../types'; import { waitBeforeNextRetry, RETRY_IF_CONFLICTS_ATTEMPTS } from './wait_before_next_retry'; const MAX_RULES_IDS_IN_RETRY = 1000; diff --git a/x-pack/plugins/alerting/server/rules_client/lib/retry_if_bulk_disable_conflicts.ts b/x-pack/plugins/alerting/server/rules_client/common/retry_if_bulk_disable_conflicts.ts similarity index 98% rename from x-pack/plugins/alerting/server/rules_client/lib/retry_if_bulk_disable_conflicts.ts rename to x-pack/plugins/alerting/server/rules_client/common/retry_if_bulk_disable_conflicts.ts index 90ecf57c029a0..29fd62b9333f0 100644 --- a/x-pack/plugins/alerting/server/rules_client/lib/retry_if_bulk_disable_conflicts.ts +++ b/x-pack/plugins/alerting/server/rules_client/common/retry_if_bulk_disable_conflicts.ts @@ -10,7 +10,7 @@ import { chunk } from 'lodash'; import { KueryNode } from '@kbn/es-query'; import { Logger, SavedObjectsBulkUpdateObject } from '@kbn/core/server'; import { convertRuleIdsToKueryNode } from '../../lib'; -import { BulkOperationError } from '../rules_client'; +import { BulkOperationError } from '../types'; import { waitBeforeNextRetry, RETRY_IF_CONFLICTS_ATTEMPTS } from './wait_before_next_retry'; import { RawRule } from '../../types'; diff --git a/x-pack/plugins/alerting/server/rules_client/lib/retry_if_bulk_edit_conflicts.test.ts b/x-pack/plugins/alerting/server/rules_client/common/retry_if_bulk_edit_conflicts.test.ts similarity index 100% rename from x-pack/plugins/alerting/server/rules_client/lib/retry_if_bulk_edit_conflicts.test.ts rename to x-pack/plugins/alerting/server/rules_client/common/retry_if_bulk_edit_conflicts.test.ts diff --git a/x-pack/plugins/alerting/server/rules_client/lib/retry_if_bulk_edit_conflicts.ts b/x-pack/plugins/alerting/server/rules_client/common/retry_if_bulk_edit_conflicts.ts similarity index 98% rename from x-pack/plugins/alerting/server/rules_client/lib/retry_if_bulk_edit_conflicts.ts rename to x-pack/plugins/alerting/server/rules_client/common/retry_if_bulk_edit_conflicts.ts index d893f2e9b5df8..984ce17d149e0 100644 --- a/x-pack/plugins/alerting/server/rules_client/lib/retry_if_bulk_edit_conflicts.ts +++ b/x-pack/plugins/alerting/server/rules_client/common/retry_if_bulk_edit_conflicts.ts @@ -10,7 +10,7 @@ import { chunk } from 'lodash'; import { KueryNode } from '@kbn/es-query'; import { Logger, SavedObjectsBulkUpdateObject, SavedObjectsUpdateResponse } from '@kbn/core/server'; import { convertRuleIdsToKueryNode } from '../../lib'; -import { BulkOperationError } from '../rules_client'; +import { BulkOperationError } from '../types'; import { RawRule } from '../../types'; import { waitBeforeNextRetry, RETRY_IF_CONFLICTS_ATTEMPTS } from './wait_before_next_retry'; diff --git a/x-pack/plugins/alerting/server/rules_client/lib/retry_if_bulk_operation_conflicts.test.ts b/x-pack/plugins/alerting/server/rules_client/common/retry_if_bulk_operation_conflicts.test.ts similarity index 100% rename from x-pack/plugins/alerting/server/rules_client/lib/retry_if_bulk_operation_conflicts.test.ts rename to x-pack/plugins/alerting/server/rules_client/common/retry_if_bulk_operation_conflicts.test.ts diff --git a/x-pack/plugins/alerting/server/rules_client/lib/retry_if_bulk_operation_conflicts.ts b/x-pack/plugins/alerting/server/rules_client/common/retry_if_bulk_operation_conflicts.ts similarity index 98% rename from x-pack/plugins/alerting/server/rules_client/lib/retry_if_bulk_operation_conflicts.ts rename to x-pack/plugins/alerting/server/rules_client/common/retry_if_bulk_operation_conflicts.ts index 9b95335f3994c..4210de1207623 100644 --- a/x-pack/plugins/alerting/server/rules_client/lib/retry_if_bulk_operation_conflicts.ts +++ b/x-pack/plugins/alerting/server/rules_client/common/retry_if_bulk_operation_conflicts.ts @@ -10,7 +10,7 @@ import { chunk } from 'lodash'; import { KueryNode } from '@kbn/es-query'; import { Logger, SavedObjectsBulkUpdateObject } from '@kbn/core/server'; import { convertRuleIdsToKueryNode } from '../../lib'; -import { BulkOperationError } from '../rules_client'; +import { BulkOperationError } from '../types'; import { waitBeforeNextRetry, RETRY_IF_CONFLICTS_ATTEMPTS } from './wait_before_next_retry'; import { RawRule } from '../../types'; diff --git a/x-pack/plugins/alerting/server/rules_client/common/snooze_utils.ts b/x-pack/plugins/alerting/server/rules_client/common/snooze_utils.ts new file mode 100644 index 0000000000000..2a6d1b3b06e7a --- /dev/null +++ b/x-pack/plugins/alerting/server/rules_client/common/snooze_utils.ts @@ -0,0 +1,139 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; +import { RawRule, RuleSnoozeSchedule } from '../../types'; +import { getActiveScheduledSnoozes } from '../../lib/is_rule_snoozed'; + +export function getSnoozeAttributes(attributes: RawRule, snoozeSchedule: RuleSnoozeSchedule) { + // If duration is -1, instead mute all + const { id: snoozeId, duration } = snoozeSchedule; + + if (duration === -1) { + return { + muteAll: true, + snoozeSchedule: clearUnscheduledSnooze(attributes), + }; + } + return { + snoozeSchedule: (snoozeId + ? clearScheduledSnoozesById(attributes, [snoozeId]) + : clearUnscheduledSnooze(attributes) + ).concat(snoozeSchedule), + muteAll: false, + }; +} + +export function getBulkSnoozeAttributes(attributes: RawRule, snoozeSchedule: RuleSnoozeSchedule) { + // If duration is -1, instead mute all + const { id: snoozeId, duration } = snoozeSchedule; + + if (duration === -1) { + return { + muteAll: true, + snoozeSchedule: clearUnscheduledSnooze(attributes), + }; + } + + // Bulk adding snooze schedule, don't touch the existing snooze/indefinite snooze + if (snoozeId) { + const existingSnoozeSchedules = attributes.snoozeSchedule || []; + return { + muteAll: attributes.muteAll, + snoozeSchedule: [...existingSnoozeSchedules, snoozeSchedule], + }; + } + + // Bulk snoozing, don't touch the existing snooze schedules + return { + muteAll: false, + snoozeSchedule: [...clearUnscheduledSnooze(attributes), snoozeSchedule], + }; +} + +export function getUnsnoozeAttributes(attributes: RawRule, scheduleIds?: string[]) { + const snoozeSchedule = scheduleIds + ? clearScheduledSnoozesById(attributes, scheduleIds) + : clearCurrentActiveSnooze(attributes); + + return { + snoozeSchedule, + ...(!scheduleIds ? { muteAll: false } : {}), + }; +} + +export function getBulkUnsnoozeAttributes(attributes: RawRule, scheduleIds?: string[]) { + // Bulk removing snooze schedules, don't touch the current snooze/indefinite snooze + if (scheduleIds) { + const newSchedules = clearScheduledSnoozesById(attributes, scheduleIds); + // Unscheduled snooze is also known as snooze now + const unscheduledSnooze = + attributes.snoozeSchedule?.filter((s) => typeof s.id === 'undefined') || []; + + return { + snoozeSchedule: [...unscheduledSnooze, ...newSchedules], + muteAll: attributes.muteAll, + }; + } + + // Bulk unsnoozing, don't touch current snooze schedules that are NOT active + return { + snoozeSchedule: clearCurrentActiveSnooze(attributes), + muteAll: false, + }; +} + +export function clearUnscheduledSnooze(attributes: RawRule) { + // Clear any snoozes that have no ID property. These are "simple" snoozes created with the quick UI, e.g. snooze for 3 days starting now + return attributes.snoozeSchedule + ? attributes.snoozeSchedule.filter((s) => typeof s.id !== 'undefined') + : []; +} + +export function clearScheduledSnoozesById(attributes: RawRule, ids: string[]) { + return attributes.snoozeSchedule + ? attributes.snoozeSchedule.filter((s) => s.id && !ids.includes(s.id)) + : []; +} + +export function clearCurrentActiveSnooze(attributes: RawRule) { + // First attempt to cancel a simple (unscheduled) snooze + const clearedUnscheduledSnoozes = clearUnscheduledSnooze(attributes); + // Now clear any scheduled snoozes that are currently active and never recur + const activeSnoozes = getActiveScheduledSnoozes(attributes); + const activeSnoozeIds = activeSnoozes?.map((s) => s.id) ?? []; + const recurringSnoozesToSkip: string[] = []; + const clearedNonRecurringActiveSnoozes = clearedUnscheduledSnoozes.filter((s) => { + if (!activeSnoozeIds.includes(s.id!)) return true; + // Check if this is a recurring snooze, and return true if so + if (s.rRule.freq && s.rRule.count !== 1) { + recurringSnoozesToSkip.push(s.id!); + return true; + } + }); + const clearedSnoozesAndSkippedRecurringSnoozes = clearedNonRecurringActiveSnoozes.map((s) => { + if (s.id && !recurringSnoozesToSkip.includes(s.id)) return s; + const currentRecurrence = activeSnoozes?.find((a) => a.id === s.id)?.lastOccurrence; + if (!currentRecurrence) return s; + return { + ...s, + skipRecurrences: (s.skipRecurrences ?? []).concat(currentRecurrence.toISOString()), + }; + }); + return clearedSnoozesAndSkippedRecurringSnoozes; +} + +export function verifySnoozeScheduleLimit(attributes: Partial) { + const schedules = attributes.snoozeSchedule?.filter((snooze) => snooze.id); + if (schedules && schedules.length > 5) { + throw Error( + i18n.translate('xpack.alerting.rulesClient.snoozeSchedule.limitReached', { + defaultMessage: 'Rule cannot have more than 5 snooze schedules', + }) + ); + } +} diff --git a/x-pack/plugins/alerting/server/rules_client/lib/validate_attributes.test.ts b/x-pack/plugins/alerting/server/rules_client/common/validate_attributes.test.ts similarity index 100% rename from x-pack/plugins/alerting/server/rules_client/lib/validate_attributes.test.ts rename to x-pack/plugins/alerting/server/rules_client/common/validate_attributes.test.ts diff --git a/x-pack/plugins/alerting/server/rules_client/lib/validate_attributes.ts b/x-pack/plugins/alerting/server/rules_client/common/validate_attributes.ts similarity index 100% rename from x-pack/plugins/alerting/server/rules_client/lib/validate_attributes.ts rename to x-pack/plugins/alerting/server/rules_client/common/validate_attributes.ts diff --git a/x-pack/plugins/alerting/server/rules_client/lib/wait_before_next_retry.test.ts b/x-pack/plugins/alerting/server/rules_client/common/wait_before_next_retry.test.ts similarity index 100% rename from x-pack/plugins/alerting/server/rules_client/lib/wait_before_next_retry.test.ts rename to x-pack/plugins/alerting/server/rules_client/common/wait_before_next_retry.test.ts diff --git a/x-pack/plugins/alerting/server/rules_client/lib/wait_before_next_retry.ts b/x-pack/plugins/alerting/server/rules_client/common/wait_before_next_retry.ts similarity index 100% rename from x-pack/plugins/alerting/server/rules_client/lib/wait_before_next_retry.ts rename to x-pack/plugins/alerting/server/rules_client/common/wait_before_next_retry.ts diff --git a/x-pack/plugins/alerting/server/rules_client/index.ts b/x-pack/plugins/alerting/server/rules_client/index.ts index e5f2b18ee82d1..ed2b5a8558368 100644 --- a/x-pack/plugins/alerting/server/rules_client/index.ts +++ b/x-pack/plugins/alerting/server/rules_client/index.ts @@ -6,3 +6,4 @@ */ export * from './rules_client'; +export * from './types'; diff --git a/x-pack/plugins/alerting/server/rules_client/lib/check_authorization_and_get_total.ts b/x-pack/plugins/alerting/server/rules_client/lib/check_authorization_and_get_total.ts new file mode 100644 index 0000000000000..ecaa7fd172fa7 --- /dev/null +++ b/x-pack/plugins/alerting/server/rules_client/lib/check_authorization_and_get_total.ts @@ -0,0 +1,104 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import pMap from 'p-map'; +import Boom from '@hapi/boom'; +import { KueryNode } from '@kbn/es-query'; +import { RawRule } from '../../types'; +import { WriteOperations, ReadOperations, AlertingAuthorizationEntity } from '../../authorization'; +import { BulkAction, RuleBulkOperationAggregation } from '../types'; +import { + MAX_RULES_NUMBER_FOR_BULK_OPERATION, + RULE_TYPE_CHECKS_CONCURRENCY, +} from '../common/constants'; +import { RulesClientContext } from '../types'; +import { ruleAuditEvent, RuleAuditAction } from '../common/audit_events'; + +export const checkAuthorizationAndGetTotal = async ( + context: RulesClientContext, + { + filter, + action, + }: { + filter: KueryNode | null; + action: BulkAction; + } +) => { + const actionToConstantsMapping: Record< + BulkAction, + { WriteOperation: WriteOperations | ReadOperations; RuleAuditAction: RuleAuditAction } + > = { + DELETE: { + WriteOperation: WriteOperations.BulkDelete, + RuleAuditAction: RuleAuditAction.DELETE, + }, + ENABLE: { + WriteOperation: WriteOperations.BulkEnable, + RuleAuditAction: RuleAuditAction.ENABLE, + }, + DISABLE: { + WriteOperation: WriteOperations.BulkDisable, + RuleAuditAction: RuleAuditAction.DISABLE, + }, + }; + const { aggregations, total } = await context.unsecuredSavedObjectsClient.find< + RawRule, + RuleBulkOperationAggregation + >({ + filter, + page: 1, + perPage: 0, + type: 'alert', + aggs: { + alertTypeId: { + multi_terms: { + terms: [ + { field: 'alert.attributes.alertTypeId' }, + { field: 'alert.attributes.consumer' }, + ], + }, + }, + }, + }); + + if (total > MAX_RULES_NUMBER_FOR_BULK_OPERATION) { + throw Boom.badRequest( + `More than ${MAX_RULES_NUMBER_FOR_BULK_OPERATION} rules matched for bulk ${action.toLocaleLowerCase()}` + ); + } + + const buckets = aggregations?.alertTypeId.buckets; + + if (buckets === undefined || buckets?.length === 0) { + throw Boom.badRequest(`No rules found for bulk ${action.toLocaleLowerCase()}`); + } + + await pMap( + buckets, + async ({ key: [ruleType, consumer] }) => { + context.ruleTypeRegistry.ensureRuleTypeEnabled(ruleType); + try { + await context.authorization.ensureAuthorized({ + ruleTypeId: ruleType, + consumer, + operation: actionToConstantsMapping[action].WriteOperation, + entity: AlertingAuthorizationEntity.Rule, + }); + } catch (error) { + context.auditLogger?.log( + ruleAuditEvent({ + action: actionToConstantsMapping[action].RuleAuditAction, + error, + }) + ); + throw error; + } + }, + { concurrency: RULE_TYPE_CHECKS_CONCURRENCY } + ); + return { total }; +}; diff --git a/x-pack/plugins/alerting/server/rules_client/lib/create_new_api_key_set.ts b/x-pack/plugins/alerting/server/rules_client/lib/create_new_api_key_set.ts new file mode 100644 index 0000000000000..202ab4d23972d --- /dev/null +++ b/x-pack/plugins/alerting/server/rules_client/lib/create_new_api_key_set.ts @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import Boom from '@hapi/boom'; +import { RawRule } from '../../types'; +import { generateAPIKeyName, apiKeyAsAlertAttributes } from '../common'; +import { RulesClientContext } from '../types'; + +export async function createNewAPIKeySet( + context: RulesClientContext, + { + attributes, + username, + }: { + attributes: RawRule; + username: string | null; + } +): Promise> { + let createdAPIKey = null; + try { + createdAPIKey = await context.createAPIKey( + generateAPIKeyName(attributes.alertTypeId, attributes.name) + ); + } catch (error) { + throw Boom.badRequest(`Error creating API key for rule: ${error.message}`); + } + + return apiKeyAsAlertAttributes(createdAPIKey, username); +} diff --git a/x-pack/plugins/alerting/server/rules_client/lib/create_rule_saved_object.ts b/x-pack/plugins/alerting/server/rules_client/lib/create_rule_saved_object.ts new file mode 100644 index 0000000000000..45ade4086af4a --- /dev/null +++ b/x-pack/plugins/alerting/server/rules_client/lib/create_rule_saved_object.ts @@ -0,0 +1,110 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { SavedObjectReference, SavedObject } from '@kbn/core/server'; +import { RawRule, RuleTypeParams } from '../../types'; +import { bulkMarkApiKeysForInvalidation } from '../../invalidate_pending_api_keys/bulk_mark_api_keys_for_invalidation'; +import { ruleAuditEvent, RuleAuditAction } from '../common/audit_events'; +import { SavedObjectOptions } from '../types'; +import { RulesClientContext } from '../types'; +import { updateMeta } from './update_meta'; +import { scheduleTask } from './schedule_task'; +import { getAlertFromRaw } from './get_alert_from_raw'; + +export async function createRuleSavedObject( + context: RulesClientContext, + { + intervalInMs, + rawRule, + references, + ruleId, + options, + }: { + intervalInMs: number; + rawRule: RawRule; + references: SavedObjectReference[]; + ruleId: string; + options?: SavedObjectOptions; + } +) { + context.auditLogger?.log( + ruleAuditEvent({ + action: RuleAuditAction.CREATE, + outcome: 'unknown', + savedObject: { type: 'alert', id: ruleId }, + }) + ); + + let createdAlert: SavedObject; + try { + createdAlert = await context.unsecuredSavedObjectsClient.create( + 'alert', + updateMeta(context, rawRule), + { + ...options, + references, + id: ruleId, + } + ); + } catch (e) { + // Avoid unused API key + await bulkMarkApiKeysForInvalidation( + { apiKeys: rawRule.apiKey ? [rawRule.apiKey] : [] }, + context.logger, + context.unsecuredSavedObjectsClient + ); + + throw e; + } + if (rawRule.enabled) { + let scheduledTask; + try { + scheduledTask = await scheduleTask(context, { + id: createdAlert.id, + consumer: rawRule.consumer, + ruleTypeId: rawRule.alertTypeId, + schedule: rawRule.schedule, + throwOnConflict: true, + }); + } catch (e) { + // Cleanup data, something went wrong scheduling the task + try { + await context.unsecuredSavedObjectsClient.delete('alert', createdAlert.id); + } catch (err) { + // Skip the cleanup error and throw the task manager error to avoid confusion + context.logger.error( + `Failed to cleanup alert "${createdAlert.id}" after scheduling task failed. Error: ${err.message}` + ); + } + throw e; + } + await context.unsecuredSavedObjectsClient.update('alert', createdAlert.id, { + scheduledTaskId: scheduledTask.id, + }); + createdAlert.attributes.scheduledTaskId = scheduledTask.id; + } + + // Log warning if schedule interval is less than the minimum but we're not enforcing it + if ( + intervalInMs < context.minimumScheduleIntervalInMs && + !context.minimumScheduleInterval.enforce + ) { + context.logger.warn( + `Rule schedule interval (${rawRule.schedule.interval}) for "${createdAlert.attributes.alertTypeId}" rule type with ID "${createdAlert.id}" is less than the minimum value (${context.minimumScheduleInterval.value}). Running rules at this interval may impact alerting performance. Set "xpack.alerting.rules.minimumScheduleInterval.enforce" to true to prevent creation of these rules.` + ); + } + + return getAlertFromRaw( + context, + createdAlert.id, + createdAlert.attributes.alertTypeId, + createdAlert.attributes, + references, + false, + true + ); +} diff --git a/x-pack/plugins/alerting/server/rules_client/lib/denormalize_actions.ts b/x-pack/plugins/alerting/server/rules_client/lib/denormalize_actions.ts new file mode 100644 index 0000000000000..0f7a164d2a741 --- /dev/null +++ b/x-pack/plugins/alerting/server/rules_client/lib/denormalize_actions.ts @@ -0,0 +1,64 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { SavedObjectReference } from '@kbn/core/server'; +import { RawRule } from '../../types'; +import { preconfiguredConnectorActionRefPrefix } from '../common/constants'; +import { RulesClientContext } from '../types'; +import { NormalizedAlertAction } from '../types'; + +export async function denormalizeActions( + context: RulesClientContext, + alertActions: NormalizedAlertAction[] +): Promise<{ actions: RawRule['actions']; references: SavedObjectReference[] }> { + const references: SavedObjectReference[] = []; + const actions: RawRule['actions'] = []; + if (alertActions.length) { + const actionsClient = await context.getActionsClient(); + const actionIds = [...new Set(alertActions.map((alertAction) => alertAction.id))]; + const actionResults = await actionsClient.getBulk(actionIds); + const actionTypeIds = [...new Set(actionResults.map((action) => action.actionTypeId))]; + actionTypeIds.forEach((id) => { + // Notify action type usage via "isActionTypeEnabled" function + actionsClient.isActionTypeEnabled(id, { notifyUsage: true }); + }); + alertActions.forEach(({ id, ...alertAction }, i) => { + const actionResultValue = actionResults.find((action) => action.id === id); + if (actionResultValue) { + if (actionsClient.isPreconfigured(id)) { + actions.push({ + ...alertAction, + actionRef: `${preconfiguredConnectorActionRefPrefix}${id}`, + actionTypeId: actionResultValue.actionTypeId, + }); + } else { + const actionRef = `action_${i}`; + references.push({ + id, + name: actionRef, + type: 'action', + }); + actions.push({ + ...alertAction, + actionRef, + actionTypeId: actionResultValue.actionTypeId, + }); + } + } else { + actions.push({ + ...alertAction, + actionRef: '', + actionTypeId: '', + }); + } + }); + } + return { + actions, + references, + }; +} diff --git a/x-pack/plugins/alerting/server/rules_client/lib/extract_references.ts b/x-pack/plugins/alerting/server/rules_client/lib/extract_references.ts new file mode 100644 index 0000000000000..58f6f6ab20dbc --- /dev/null +++ b/x-pack/plugins/alerting/server/rules_client/lib/extract_references.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 { SavedObjectReference } from '@kbn/core/server'; +import { RawRule, RuleTypeParams } from '../../types'; +import { UntypedNormalizedRuleType } from '../../rule_type_registry'; +import { NormalizedAlertAction } from '../types'; +import { extractedSavedObjectParamReferenceNamePrefix } from '../common/constants'; +import { RulesClientContext } from '../types'; +import { denormalizeActions } from './denormalize_actions'; + +export async function extractReferences< + Params extends RuleTypeParams, + ExtractedParams extends RuleTypeParams +>( + context: RulesClientContext, + ruleType: UntypedNormalizedRuleType, + ruleActions: NormalizedAlertAction[], + ruleParams: Params +): Promise<{ + actions: RawRule['actions']; + params: ExtractedParams; + references: SavedObjectReference[]; +}> { + const { references: actionReferences, actions } = await denormalizeActions(context, ruleActions); + + // Extracts any references using configured reference extractor if available + const extractedRefsAndParams = ruleType?.useSavedObjectReferences?.extractReferences + ? ruleType.useSavedObjectReferences.extractReferences(ruleParams) + : null; + const extractedReferences = extractedRefsAndParams?.references ?? []; + const params = (extractedRefsAndParams?.params as ExtractedParams) ?? ruleParams; + + // Prefix extracted references in order to avoid clashes with framework level references + const paramReferences = extractedReferences.map((reference: SavedObjectReference) => ({ + ...reference, + name: `${extractedSavedObjectParamReferenceNamePrefix}${reference.name}`, + })); + + const references = [...actionReferences, ...paramReferences]; + + return { + actions, + params, + references, + }; +} diff --git a/x-pack/plugins/alerting/server/rules_client/lib/get_alert_from_raw.ts b/x-pack/plugins/alerting/server/rules_client/lib/get_alert_from_raw.ts new file mode 100644 index 0000000000000..72cd5c0ec4b1a --- /dev/null +++ b/x-pack/plugins/alerting/server/rules_client/lib/get_alert_from_raw.ts @@ -0,0 +1,137 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { omit, isEmpty } from 'lodash'; +import { SavedObjectReference } from '@kbn/core/server'; +import { + Rule, + PartialRule, + RawRule, + IntervalSchedule, + RuleTypeParams, + RuleWithLegacyId, + PartialRuleWithLegacyId, +} from '../../types'; +import { ruleExecutionStatusFromRaw, convertMonitoringFromRawAndVerify } from '../../lib'; +import { UntypedNormalizedRuleType } from '../../rule_type_registry'; +import { getActiveScheduledSnoozes } from '../../lib/is_rule_snoozed'; +import { + calculateIsSnoozedUntil, + injectReferencesIntoActions, + injectReferencesIntoParams, +} from '../common'; +import { RulesClientContext } from '../types'; + +export function getAlertFromRaw( + context: RulesClientContext, + id: string, + ruleTypeId: string, + rawRule: RawRule, + references: SavedObjectReference[] | undefined, + includeLegacyId: boolean = false, + excludeFromPublicApi: boolean = false, + includeSnoozeData: boolean = false +): Rule | RuleWithLegacyId { + const ruleType = context.ruleTypeRegistry.get(ruleTypeId); + // In order to support the partial update API of Saved Objects we have to support + // partial updates of an Alert, but when we receive an actual RawRule, it is safe + // to cast the result to an Alert + const res = getPartialRuleFromRaw( + context, + id, + ruleType, + rawRule, + references, + includeLegacyId, + excludeFromPublicApi, + includeSnoozeData + ); + // include to result because it is for internal rules client usage + if (includeLegacyId) { + return res as RuleWithLegacyId; + } + // exclude from result because it is an internal variable + return omit(res, ['legacyId']) as Rule; +} + +export function getPartialRuleFromRaw( + context: RulesClientContext, + id: string, + ruleType: UntypedNormalizedRuleType, + { + createdAt, + updatedAt, + meta, + notifyWhen, + legacyId, + scheduledTaskId, + params, + executionStatus, + monitoring, + nextRun, + schedule, + actions, + snoozeSchedule, + ...partialRawRule + }: Partial, + references: SavedObjectReference[] | undefined, + includeLegacyId: boolean = false, + excludeFromPublicApi: boolean = false, + includeSnoozeData: boolean = false +): PartialRule | PartialRuleWithLegacyId { + const snoozeScheduleDates = snoozeSchedule?.map((s) => ({ + ...s, + rRule: { + ...s.rRule, + dtstart: new Date(s.rRule.dtstart), + ...(s.rRule.until ? { until: new Date(s.rRule.until) } : {}), + }, + })); + const includeSnoozeSchedule = + snoozeSchedule !== undefined && !isEmpty(snoozeSchedule) && !excludeFromPublicApi; + const isSnoozedUntil = includeSnoozeSchedule + ? calculateIsSnoozedUntil({ + muteAll: partialRawRule.muteAll ?? false, + snoozeSchedule, + }) + : null; + const includeMonitoring = monitoring && !excludeFromPublicApi; + const rule = { + id, + notifyWhen, + ...omit(partialRawRule, excludeFromPublicApi ? [...context.fieldsToExcludeFromPublicApi] : ''), + // we currently only support the Interval Schedule type + // Once we support additional types, this type signature will likely change + schedule: schedule as IntervalSchedule, + actions: actions ? injectReferencesIntoActions(id, actions, references || []) : [], + params: injectReferencesIntoParams(id, ruleType, params, references || []) as Params, + ...(excludeFromPublicApi ? {} : { snoozeSchedule: snoozeScheduleDates ?? [] }), + ...(includeSnoozeData && !excludeFromPublicApi + ? { + activeSnoozes: getActiveScheduledSnoozes({ + snoozeSchedule, + muteAll: partialRawRule.muteAll ?? false, + })?.map((s) => s.id), + isSnoozedUntil, + } + : {}), + ...(updatedAt ? { updatedAt: new Date(updatedAt) } : {}), + ...(createdAt ? { createdAt: new Date(createdAt) } : {}), + ...(scheduledTaskId ? { scheduledTaskId } : {}), + ...(executionStatus + ? { executionStatus: ruleExecutionStatusFromRaw(context.logger, id, executionStatus) } + : {}), + ...(includeMonitoring + ? { monitoring: convertMonitoringFromRawAndVerify(context.logger, id, monitoring) } + : {}), + ...(nextRun ? { nextRun: new Date(nextRun) } : {}), + }; + + return includeLegacyId + ? ({ ...rule, legacyId } as PartialRuleWithLegacyId) + : (rule as PartialRule); +} diff --git a/x-pack/plugins/alerting/server/rules_client/lib/get_authorization_filter.ts b/x-pack/plugins/alerting/server/rules_client/lib/get_authorization_filter.ts new file mode 100644 index 0000000000000..28e42c6b12e42 --- /dev/null +++ b/x-pack/plugins/alerting/server/rules_client/lib/get_authorization_filter.ts @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { AlertingAuthorizationEntity } from '../../authorization'; +import { ruleAuditEvent, RuleAuditAction } from '../common/audit_events'; +import { RulesClientContext } from '../types'; +import { alertingAuthorizationFilterOpts } from '../common/constants'; +import { BulkAction } from '../types'; + +export const getAuthorizationFilter = async ( + context: RulesClientContext, + { action }: { action: BulkAction } +) => { + try { + const authorizationTuple = await context.authorization.getFindAuthorizationFilter( + AlertingAuthorizationEntity.Rule, + alertingAuthorizationFilterOpts + ); + return authorizationTuple.filter; + } catch (error) { + context.auditLogger?.log( + ruleAuditEvent({ + action: RuleAuditAction[action], + error, + }) + ); + throw error; + } +}; diff --git a/x-pack/plugins/alerting/server/rules_client/lib/index.ts b/x-pack/plugins/alerting/server/rules_client/lib/index.ts index f7e0620222ec6..1f9534a5c6da2 100644 --- a/x-pack/plugins/alerting/server/rules_client/lib/index.ts +++ b/x-pack/plugins/alerting/server/rules_client/lib/index.ts @@ -5,11 +5,13 @@ * 2.0. */ -export { mapSortField } from './map_sort_field'; -export { validateOperationOnAttributes } from './validate_attributes'; -export { retryIfBulkEditConflicts } from './retry_if_bulk_edit_conflicts'; -export { retryIfBulkDeleteConflicts } from './retry_if_bulk_delete_conflicts'; -export { retryIfBulkDisableConflicts } from './retry_if_bulk_disable_conflicts'; -export { retryIfBulkOperationConflicts } from './retry_if_bulk_operation_conflicts'; -export { applyBulkEditOperation } from './apply_bulk_edit_operation'; -export { buildKueryNodeFilter } from './build_kuery_node_filter'; +export { createRuleSavedObject } from './create_rule_saved_object'; +export { extractReferences } from './extract_references'; +export { validateActions } from './validate_actions'; +export { updateMeta } from './update_meta'; +export * from './get_alert_from_raw'; +export { getAuthorizationFilter } from './get_authorization_filter'; +export { checkAuthorizationAndGetTotal } from './check_authorization_and_get_total'; +export { scheduleTask } from './schedule_task'; +export { createNewAPIKeySet } from './create_new_api_key_set'; +export { recoverRuleAlerts } from './recover_rule_alerts'; diff --git a/x-pack/plugins/alerting/server/rules_client/lib/recover_rule_alerts.ts b/x-pack/plugins/alerting/server/rules_client/lib/recover_rule_alerts.ts new file mode 100644 index 0000000000000..aaa84a8b6950b --- /dev/null +++ b/x-pack/plugins/alerting/server/rules_client/lib/recover_rule_alerts.ts @@ -0,0 +1,69 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { mapValues } from 'lodash'; +import { SAVED_OBJECT_REL_PRIMARY } from '@kbn/event-log-plugin/server'; +import { RawRule, SanitizedRule, RawAlertInstance as RawAlert } from '../../types'; +import { taskInstanceToAlertTaskInstance } from '../../task_runner/alert_task_instance'; +import { Alert } from '../../alert'; +import { EVENT_LOG_ACTIONS } from '../../plugin'; +import { createAlertEventLogRecordObject } from '../../lib/create_alert_event_log_record_object'; +import { RulesClientContext } from '../types'; + +export const recoverRuleAlerts = async ( + context: RulesClientContext, + id: string, + attributes: RawRule +) => { + if (!context.eventLogger || !attributes.scheduledTaskId) return; + try { + const { state } = taskInstanceToAlertTaskInstance( + await context.taskManager.get(attributes.scheduledTaskId), + attributes as unknown as SanitizedRule + ); + + const recoveredAlerts = mapValues, Alert>( + state.alertInstances ?? {}, + (rawAlertInstance, alertId) => new Alert(alertId, rawAlertInstance) + ); + const recoveredAlertIds = Object.keys(recoveredAlerts); + + for (const alertId of recoveredAlertIds) { + const { group: actionGroup } = recoveredAlerts[alertId].getLastScheduledActions() ?? {}; + const instanceState = recoveredAlerts[alertId].getState(); + const message = `instance '${alertId}' has recovered due to the rule was disabled`; + + const event = createAlertEventLogRecordObject({ + ruleId: id, + ruleName: attributes.name, + ruleType: context.ruleTypeRegistry.get(attributes.alertTypeId), + consumer: attributes.consumer, + instanceId: alertId, + action: EVENT_LOG_ACTIONS.recoveredInstance, + message, + state: instanceState, + group: actionGroup, + namespace: context.namespace, + spaceId: context.spaceId, + savedObjects: [ + { + id, + type: 'alert', + typeId: attributes.alertTypeId, + relation: SAVED_OBJECT_REL_PRIMARY, + }, + ], + }); + context.eventLogger.logEvent(event); + } + } catch (error) { + // this should not block the rest of the disable process + context.logger.warn( + `rulesClient.disable('${id}') - Could not write recovery events - ${error.message}` + ); + } +}; diff --git a/x-pack/plugins/alerting/server/rules_client/lib/schedule_task.ts b/x-pack/plugins/alerting/server/rules_client/lib/schedule_task.ts new file mode 100644 index 0000000000000..eecdcf0314d02 --- /dev/null +++ b/x-pack/plugins/alerting/server/rules_client/lib/schedule_task.ts @@ -0,0 +1,38 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { RulesClientContext } from '../types'; +import { ScheduleTaskOptions } from '../types'; + +export async function scheduleTask(context: RulesClientContext, opts: ScheduleTaskOptions) { + const { id, consumer, ruleTypeId, schedule, throwOnConflict } = opts; + const taskInstance = { + id, // use the same ID for task document as the rule + taskType: `alerting:${ruleTypeId}`, + schedule, + params: { + alertId: id, + spaceId: context.spaceId, + consumer, + }, + state: { + previousStartedAt: null, + alertTypeState: {}, + alertInstances: {}, + }, + scope: ['alerting'], + enabled: true, + }; + try { + return await context.taskManager.schedule(taskInstance); + } catch (err) { + if (err.statusCode === 409 && !throwOnConflict) { + return taskInstance; + } + throw err; + } +} diff --git a/x-pack/plugins/alerting/server/rules_client/lib/update_meta.ts b/x-pack/plugins/alerting/server/rules_client/lib/update_meta.ts new file mode 100644 index 0000000000000..5fbe2b275f077 --- /dev/null +++ b/x-pack/plugins/alerting/server/rules_client/lib/update_meta.ts @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { RawRule } from '../../types'; +import { RulesClientContext } from '../types'; + +export function updateMeta>( + context: RulesClientContext, + alertAttributes: T +): T { + if (alertAttributes.hasOwnProperty('apiKey') || alertAttributes.hasOwnProperty('apiKeyOwner')) { + alertAttributes.meta = alertAttributes.meta ?? {}; + alertAttributes.meta.versionApiKeyLastmodified = context.kibanaVersion; + } + return alertAttributes; +} diff --git a/x-pack/plugins/alerting/server/rules_client/lib/validate_actions.ts b/x-pack/plugins/alerting/server/rules_client/lib/validate_actions.ts new file mode 100644 index 0000000000000..683b95c066343 --- /dev/null +++ b/x-pack/plugins/alerting/server/rules_client/lib/validate_actions.ts @@ -0,0 +1,106 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import Boom from '@hapi/boom'; +import { map } from 'lodash'; +import { i18n } from '@kbn/i18n'; +import { RawRule } from '../../types'; +import { UntypedNormalizedRuleType } from '../../rule_type_registry'; +import { NormalizedAlertAction } from '../types'; +import { RulesClientContext } from '../types'; + +export async function validateActions( + context: RulesClientContext, + alertType: UntypedNormalizedRuleType, + data: Pick & { actions: NormalizedAlertAction[] } +): Promise { + const { actions, notifyWhen, throttle } = data; + const hasNotifyWhen = typeof notifyWhen !== 'undefined'; + const hasThrottle = typeof throttle !== 'undefined'; + let usesRuleLevelFreqParams; + if (hasNotifyWhen && hasThrottle) usesRuleLevelFreqParams = true; + else if (!hasNotifyWhen && !hasThrottle) usesRuleLevelFreqParams = false; + else { + throw Boom.badRequest( + i18n.translate('xpack.alerting.rulesClient.usesValidGlobalFreqParams.oneUndefined', { + defaultMessage: + 'Rule-level notifyWhen and throttle must both be defined or both be undefined', + }) + ); + } + + if (actions.length === 0) { + return; + } + + // check for actions using connectors with missing secrets + const actionsClient = await context.getActionsClient(); + const actionIds = [...new Set(actions.map((action) => action.id))]; + const actionResults = (await actionsClient.getBulk(actionIds)) || []; + const actionsUsingConnectorsWithMissingSecrets = actionResults.filter( + (result) => result.isMissingSecrets + ); + + if (actionsUsingConnectorsWithMissingSecrets.length) { + throw Boom.badRequest( + i18n.translate('xpack.alerting.rulesClient.validateActions.misconfiguredConnector', { + defaultMessage: 'Invalid connectors: {groups}', + values: { + groups: actionsUsingConnectorsWithMissingSecrets + .map((connector) => connector.name) + .join(', '), + }, + }) + ); + } + + // check for actions with invalid action groups + const { actionGroups: alertTypeActionGroups } = alertType; + const usedAlertActionGroups = actions.map((action) => action.group); + const availableAlertTypeActionGroups = new Set(map(alertTypeActionGroups, 'id')); + const invalidActionGroups = usedAlertActionGroups.filter( + (group) => !availableAlertTypeActionGroups.has(group) + ); + if (invalidActionGroups.length) { + throw Boom.badRequest( + i18n.translate('xpack.alerting.rulesClient.validateActions.invalidGroups', { + defaultMessage: 'Invalid action groups: {groups}', + values: { + groups: invalidActionGroups.join(', '), + }, + }) + ); + } + + // check for actions using frequency params if the rule has rule-level frequency params defined + if (usesRuleLevelFreqParams) { + const actionsWithFrequency = actions.filter((action) => Boolean(action.frequency)); + if (actionsWithFrequency.length) { + throw Boom.badRequest( + i18n.translate('xpack.alerting.rulesClient.validateActions.mixAndMatchFreqParams', { + defaultMessage: + 'Cannot specify per-action frequency params when notify_when and throttle are defined at the rule level: {groups}', + values: { + groups: actionsWithFrequency.map((a) => a.group).join(', '), + }, + }) + ); + } + } else { + const actionsWithoutFrequency = actions.filter((action) => !action.frequency); + if (actionsWithoutFrequency.length) { + throw Boom.badRequest( + i18n.translate('xpack.alerting.rulesClient.validateActions.notAllActionsWithFreq', { + defaultMessage: 'Actions missing frequency parameters: {groups}', + values: { + groups: actionsWithoutFrequency.map((a) => a.group).join(', '), + }, + }) + ); + } + } +} diff --git a/x-pack/plugins/alerting/server/rules_client/methods/aggregate.ts b/x-pack/plugins/alerting/server/rules_client/methods/aggregate.ts new file mode 100644 index 0000000000000..79a07b3ebad49 --- /dev/null +++ b/x-pack/plugins/alerting/server/rules_client/methods/aggregate.ts @@ -0,0 +1,223 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor 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 { KueryNode, nodeBuilder } from '@kbn/es-query'; +import { RawRule, RuleExecutionStatusValues, RuleLastRunOutcomeValues } from '../../types'; +import { AlertingAuthorizationEntity } from '../../authorization'; +import { ruleAuditEvent, RuleAuditAction } from '../common/audit_events'; +import { buildKueryNodeFilter } from '../common'; +import { alertingAuthorizationFilterOpts } from '../common/constants'; +import { RulesClientContext } from '../types'; + +export interface AggregateOptions extends IndexType { + search?: string; + defaultSearchOperator?: 'AND' | 'OR'; + searchFields?: string[]; + hasReference?: { + type: string; + id: string; + }; + filter?: string | KueryNode; +} + +interface IndexType { + [key: string]: unknown; +} + +export interface AggregateResult { + alertExecutionStatus: { [status: string]: number }; + ruleLastRunOutcome: { [status: string]: number }; + ruleEnabledStatus?: { enabled: number; disabled: number }; + ruleMutedStatus?: { muted: number; unmuted: number }; + ruleSnoozedStatus?: { snoozed: number }; + ruleTags?: string[]; +} + +export interface RuleAggregation { + status: { + buckets: Array<{ + key: string; + doc_count: number; + }>; + }; + outcome: { + buckets: Array<{ + key: string; + doc_count: number; + }>; + }; + muted: { + buckets: Array<{ + key: number; + key_as_string: string; + doc_count: number; + }>; + }; + enabled: { + buckets: Array<{ + key: number; + key_as_string: string; + doc_count: number; + }>; + }; + snoozed: { + count: { + doc_count: number; + }; + }; + tags: { + buckets: Array<{ + key: string; + doc_count: number; + }>; + }; +} + +export async function aggregate( + context: RulesClientContext, + { options: { fields, filter, ...options } = {} }: { options?: AggregateOptions } = {} +): Promise { + let authorizationTuple; + try { + authorizationTuple = await context.authorization.getFindAuthorizationFilter( + AlertingAuthorizationEntity.Rule, + alertingAuthorizationFilterOpts + ); + } catch (error) { + context.auditLogger?.log( + ruleAuditEvent({ + action: RuleAuditAction.AGGREGATE, + error, + }) + ); + throw error; + } + + const { filter: authorizationFilter } = authorizationTuple; + const filterKueryNode = buildKueryNodeFilter(filter); + + const resp = await context.unsecuredSavedObjectsClient.find({ + ...options, + filter: + authorizationFilter && filterKueryNode + ? nodeBuilder.and([filterKueryNode, authorizationFilter as KueryNode]) + : authorizationFilter, + page: 1, + perPage: 0, + type: 'alert', + aggs: { + status: { + terms: { field: 'alert.attributes.executionStatus.status' }, + }, + outcome: { + terms: { field: 'alert.attributes.lastRun.outcome' }, + }, + enabled: { + terms: { field: 'alert.attributes.enabled' }, + }, + muted: { + terms: { field: 'alert.attributes.muteAll' }, + }, + tags: { + terms: { field: 'alert.attributes.tags', order: { _key: 'asc' }, size: 50 }, + }, + snoozed: { + nested: { + path: 'alert.attributes.snoozeSchedule', + }, + aggs: { + count: { + filter: { + exists: { + field: 'alert.attributes.snoozeSchedule.duration', + }, + }, + }, + }, + }, + }, + }); + + if (!resp.aggregations) { + // Return a placeholder with all zeroes + const placeholder: AggregateResult = { + alertExecutionStatus: {}, + ruleLastRunOutcome: {}, + ruleEnabledStatus: { + enabled: 0, + disabled: 0, + }, + ruleMutedStatus: { + muted: 0, + unmuted: 0, + }, + ruleSnoozedStatus: { snoozed: 0 }, + }; + + for (const key of RuleExecutionStatusValues) { + placeholder.alertExecutionStatus[key] = 0; + } + + return placeholder; + } + + const alertExecutionStatus = resp.aggregations.status.buckets.map( + ({ key, doc_count: docCount }) => ({ + [key]: docCount, + }) + ); + + const ruleLastRunOutcome = resp.aggregations.outcome.buckets.map( + ({ key, doc_count: docCount }) => ({ + [key]: docCount, + }) + ); + + const ret: AggregateResult = { + alertExecutionStatus: alertExecutionStatus.reduce( + (acc, curr: { [status: string]: number }) => Object.assign(acc, curr), + {} + ), + ruleLastRunOutcome: ruleLastRunOutcome.reduce( + (acc, curr: { [status: string]: number }) => Object.assign(acc, curr), + {} + ), + }; + + // Fill missing keys with zeroes + for (const key of RuleExecutionStatusValues) { + if (!ret.alertExecutionStatus.hasOwnProperty(key)) { + ret.alertExecutionStatus[key] = 0; + } + } + for (const key of RuleLastRunOutcomeValues) { + if (!ret.ruleLastRunOutcome.hasOwnProperty(key)) { + ret.ruleLastRunOutcome[key] = 0; + } + } + + const enabledBuckets = resp.aggregations.enabled.buckets; + ret.ruleEnabledStatus = { + enabled: enabledBuckets.find((bucket) => bucket.key === 1)?.doc_count ?? 0, + disabled: enabledBuckets.find((bucket) => bucket.key === 0)?.doc_count ?? 0, + }; + + const mutedBuckets = resp.aggregations.muted.buckets; + ret.ruleMutedStatus = { + muted: mutedBuckets.find((bucket) => bucket.key === 1)?.doc_count ?? 0, + unmuted: mutedBuckets.find((bucket) => bucket.key === 0)?.doc_count ?? 0, + }; + + ret.ruleSnoozedStatus = { + snoozed: resp.aggregations.snoozed?.count?.doc_count ?? 0, + }; + + const tagsBuckets = resp.aggregations.tags?.buckets || []; + ret.ruleTags = tagsBuckets.map((bucket) => bucket.key); + + return ret; +} diff --git a/x-pack/plugins/alerting/server/rules_client/methods/bulk_delete.ts b/x-pack/plugins/alerting/server/rules_client/methods/bulk_delete.ts new file mode 100644 index 0000000000000..66bbd86bf9155 --- /dev/null +++ b/x-pack/plugins/alerting/server/rules_client/methods/bulk_delete.ts @@ -0,0 +1,153 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { KueryNode, nodeBuilder } from '@kbn/es-query'; +import { SavedObjectsBulkDeleteObject } from '@kbn/core/server'; +import { RawRule } from '../../types'; +import { convertRuleIdsToKueryNode } from '../../lib'; +import { bulkMarkApiKeysForInvalidation } from '../../invalidate_pending_api_keys/bulk_mark_api_keys_for_invalidation'; +import { ruleAuditEvent, RuleAuditAction } from '../common/audit_events'; +import { getAuthorizationFilter, checkAuthorizationAndGetTotal } from '../lib'; +import { + retryIfBulkDeleteConflicts, + buildKueryNodeFilter, + getAndValidateCommonBulkOptions, +} from '../common'; +import { BulkOptions, BulkOperationError, RulesClientContext } from '../types'; + +export const bulkDeleteRules = async (context: RulesClientContext, options: BulkOptions) => { + const { ids, filter } = getAndValidateCommonBulkOptions(options); + + const kueryNodeFilter = ids ? convertRuleIdsToKueryNode(ids) : buildKueryNodeFilter(filter); + const authorizationFilter = await getAuthorizationFilter(context, { action: 'DELETE' }); + + const kueryNodeFilterWithAuth = + authorizationFilter && kueryNodeFilter + ? nodeBuilder.and([kueryNodeFilter, authorizationFilter as KueryNode]) + : kueryNodeFilter; + + const { total } = await checkAuthorizationAndGetTotal(context, { + filter: kueryNodeFilterWithAuth, + action: 'DELETE', + }); + + const { apiKeysToInvalidate, errors, taskIdsToDelete } = await retryIfBulkDeleteConflicts( + context.logger, + (filterKueryNode: KueryNode | null) => bulkDeleteWithOCC(context, { filter: filterKueryNode }), + kueryNodeFilterWithAuth + ); + + const taskIdsFailedToBeDeleted: string[] = []; + const taskIdsSuccessfullyDeleted: string[] = []; + if (taskIdsToDelete.length > 0) { + try { + const resultFromDeletingTasks = await context.taskManager.bulkRemoveIfExist(taskIdsToDelete); + resultFromDeletingTasks?.statuses.forEach((status) => { + if (status.success) { + taskIdsSuccessfullyDeleted.push(status.id); + } else { + taskIdsFailedToBeDeleted.push(status.id); + } + }); + if (taskIdsSuccessfullyDeleted.length) { + context.logger.debug( + `Successfully deleted schedules for underlying tasks: ${taskIdsSuccessfullyDeleted.join( + ', ' + )}` + ); + } + if (taskIdsFailedToBeDeleted.length) { + context.logger.error( + `Failure to delete schedules for underlying tasks: ${taskIdsFailedToBeDeleted.join(', ')}` + ); + } + } catch (error) { + context.logger.error( + `Failure to delete schedules for underlying tasks: ${taskIdsToDelete.join( + ', ' + )}. TaskManager bulkRemoveIfExist failed with Error: ${error.message}` + ); + } + } + + await bulkMarkApiKeysForInvalidation( + { apiKeys: apiKeysToInvalidate }, + context.logger, + context.unsecuredSavedObjectsClient + ); + + return { errors, total, taskIdsFailedToBeDeleted }; +}; + +const bulkDeleteWithOCC = async ( + context: RulesClientContext, + { filter }: { filter: KueryNode | null } +) => { + const rulesFinder = + await context.encryptedSavedObjectsClient.createPointInTimeFinderDecryptedAsInternalUser( + { + filter, + type: 'alert', + perPage: 100, + ...(context.namespace ? { namespaces: [context.namespace] } : undefined), + } + ); + + const rules: SavedObjectsBulkDeleteObject[] = []; + const apiKeysToInvalidate: string[] = []; + const taskIdsToDelete: string[] = []; + const errors: BulkOperationError[] = []; + const apiKeyToRuleIdMapping: Record = {}; + const taskIdToRuleIdMapping: Record = {}; + const ruleNameToRuleIdMapping: Record = {}; + + for await (const response of rulesFinder.find()) { + for (const rule of response.saved_objects) { + if (rule.attributes.apiKey) { + apiKeyToRuleIdMapping[rule.id] = rule.attributes.apiKey; + } + if (rule.attributes.name) { + ruleNameToRuleIdMapping[rule.id] = rule.attributes.name; + } + if (rule.attributes.scheduledTaskId) { + taskIdToRuleIdMapping[rule.id] = rule.attributes.scheduledTaskId; + } + rules.push(rule); + + context.auditLogger?.log( + ruleAuditEvent({ + action: RuleAuditAction.DELETE, + outcome: 'unknown', + savedObject: { type: 'alert', id: rule.id }, + }) + ); + } + } + + const result = await context.unsecuredSavedObjectsClient.bulkDelete(rules); + + result.statuses.forEach((status) => { + if (status.error === undefined) { + if (apiKeyToRuleIdMapping[status.id]) { + apiKeysToInvalidate.push(apiKeyToRuleIdMapping[status.id]); + } + if (taskIdToRuleIdMapping[status.id]) { + taskIdsToDelete.push(taskIdToRuleIdMapping[status.id]); + } + } else { + errors.push({ + message: status.error.message ?? 'n/a', + status: status.error.statusCode, + rule: { + id: status.id, + name: ruleNameToRuleIdMapping[status.id] ?? 'n/a', + }, + }); + } + }); + return { apiKeysToInvalidate, errors, taskIdsToDelete }; +}; diff --git a/x-pack/plugins/alerting/server/rules_client/methods/bulk_disable.ts b/x-pack/plugins/alerting/server/rules_client/methods/bulk_disable.ts new file mode 100644 index 0000000000000..1a3b12e618fd2 --- /dev/null +++ b/x-pack/plugins/alerting/server/rules_client/methods/bulk_disable.ts @@ -0,0 +1,227 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor 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 pMap from 'p-map'; +import { KueryNode, nodeBuilder } from '@kbn/es-query'; +import { SavedObjectsBulkUpdateObject } from '@kbn/core/server'; +import { RawRule } from '../../types'; +import { convertRuleIdsToKueryNode } from '../../lib'; +import { ruleAuditEvent, RuleAuditAction } from '../common/audit_events'; +import { + retryIfBulkDisableConflicts, + buildKueryNodeFilter, + getAndValidateCommonBulkOptions, +} from '../common'; +import { + getAuthorizationFilter, + checkAuthorizationAndGetTotal, + getAlertFromRaw, + recoverRuleAlerts, + updateMeta, +} from '../lib'; +import { BulkOptions, BulkOperationError, RulesClientContext } from '../types'; + +export const bulkDisableRules = async (context: RulesClientContext, options: BulkOptions) => { + const { ids, filter } = getAndValidateCommonBulkOptions(options); + + const kueryNodeFilter = ids ? convertRuleIdsToKueryNode(ids) : buildKueryNodeFilter(filter); + const authorizationFilter = await getAuthorizationFilter(context, { action: 'DISABLE' }); + + const kueryNodeFilterWithAuth = + authorizationFilter && kueryNodeFilter + ? nodeBuilder.and([kueryNodeFilter, authorizationFilter as KueryNode]) + : kueryNodeFilter; + + const { total } = await checkAuthorizationAndGetTotal(context, { + filter: kueryNodeFilterWithAuth, + action: 'DISABLE', + }); + + const { errors, rules, taskIdsToDisable, taskIdsToDelete } = await retryIfBulkDisableConflicts( + context.logger, + (filterKueryNode: KueryNode | null) => + bulkDisableRulesWithOCC(context, { filter: filterKueryNode }), + kueryNodeFilterWithAuth + ); + + if (taskIdsToDisable.length > 0) { + try { + const resultFromDisablingTasks = await context.taskManager.bulkDisable(taskIdsToDisable); + if (resultFromDisablingTasks.tasks.length) { + context.logger.debug( + `Successfully disabled schedules for underlying tasks: ${resultFromDisablingTasks.tasks + .map((task) => task.id) + .join(', ')}` + ); + } + if (resultFromDisablingTasks.errors.length) { + context.logger.error( + `Failure to disable schedules for underlying tasks: ${resultFromDisablingTasks.errors + .map((error) => error.task.id) + .join(', ')}` + ); + } + } catch (error) { + context.logger.error( + `Failure to disable schedules for underlying tasks: ${taskIdsToDisable.join( + ', ' + )}. TaskManager bulkDisable failed with Error: ${error.message}` + ); + } + } + + const taskIdsFailedToBeDeleted: string[] = []; + const taskIdsSuccessfullyDeleted: string[] = []; + + if (taskIdsToDelete.length > 0) { + try { + const resultFromDeletingTasks = await context.taskManager.bulkRemoveIfExist(taskIdsToDelete); + resultFromDeletingTasks?.statuses.forEach((status) => { + if (status.success) { + taskIdsSuccessfullyDeleted.push(status.id); + } else { + taskIdsFailedToBeDeleted.push(status.id); + } + }); + if (taskIdsSuccessfullyDeleted.length) { + context.logger.debug( + `Successfully deleted schedules for underlying tasks: ${taskIdsSuccessfullyDeleted.join( + ', ' + )}` + ); + } + if (taskIdsFailedToBeDeleted.length) { + context.logger.error( + `Failure to delete schedules for underlying tasks: ${taskIdsFailedToBeDeleted.join(', ')}` + ); + } + } catch (error) { + context.logger.error( + `Failure to delete schedules for underlying tasks: ${taskIdsToDelete.join( + ', ' + )}. TaskManager bulkRemoveIfExist failed with Error: ${error.message}` + ); + } + } + + const updatedRules = rules.map(({ id, attributes, references }) => { + return getAlertFromRaw( + context, + id, + attributes.alertTypeId as string, + attributes as RawRule, + references, + false + ); + }); + + return { errors, rules: updatedRules, total }; +}; + +const bulkDisableRulesWithOCC = async ( + context: RulesClientContext, + { filter }: { filter: KueryNode | null } +) => { + const rulesFinder = + await context.encryptedSavedObjectsClient.createPointInTimeFinderDecryptedAsInternalUser( + { + filter, + type: 'alert', + perPage: 100, + ...(context.namespace ? { namespaces: [context.namespace] } : undefined), + } + ); + + const rulesToDisable: Array> = []; + const errors: BulkOperationError[] = []; + const ruleNameToRuleIdMapping: Record = {}; + + for await (const response of rulesFinder.find()) { + await pMap(response.saved_objects, async (rule) => { + try { + if (rule.attributes.enabled === false) return; + + recoverRuleAlerts(context, rule.id, rule.attributes); + + if (rule.attributes.name) { + ruleNameToRuleIdMapping[rule.id] = rule.attributes.name; + } + + const username = await context.getUserName(); + const updatedAttributes = updateMeta(context, { + ...rule.attributes, + enabled: false, + scheduledTaskId: + rule.attributes.scheduledTaskId === rule.id ? rule.attributes.scheduledTaskId : null, + updatedBy: username, + updatedAt: new Date().toISOString(), + }); + + rulesToDisable.push({ + ...rule, + attributes: { + ...updatedAttributes, + }, + }); + + context.auditLogger?.log( + ruleAuditEvent({ + action: RuleAuditAction.DISABLE, + outcome: 'unknown', + savedObject: { type: 'alert', id: rule.id }, + }) + ); + } catch (error) { + errors.push({ + message: error.message, + rule: { + id: rule.id, + name: rule.attributes?.name, + }, + }); + context.auditLogger?.log( + ruleAuditEvent({ + action: RuleAuditAction.DISABLE, + error, + }) + ); + } + }); + } + + const result = await context.unsecuredSavedObjectsClient.bulkCreate(rulesToDisable, { + overwrite: true, + }); + + const taskIdsToDisable: string[] = []; + const taskIdsToDelete: string[] = []; + const disabledRules: Array> = []; + + result.saved_objects.forEach((rule) => { + if (rule.error === undefined) { + if (rule.attributes.scheduledTaskId) { + if (rule.attributes.scheduledTaskId !== rule.id) { + taskIdsToDelete.push(rule.attributes.scheduledTaskId); + } else { + taskIdsToDisable.push(rule.attributes.scheduledTaskId); + } + } + disabledRules.push(rule); + } else { + errors.push({ + message: rule.error.message ?? 'n/a', + status: rule.error.statusCode, + rule: { + id: rule.id, + name: ruleNameToRuleIdMapping[rule.id] ?? 'n/a', + }, + }); + } + }); + + return { errors, rules: disabledRules, taskIdsToDisable, taskIdsToDelete }; +}; diff --git a/x-pack/plugins/alerting/server/rules_client/methods/bulk_edit.ts b/x-pack/plugins/alerting/server/rules_client/methods/bulk_edit.ts new file mode 100644 index 0000000000000..0ac7c8d24d0fe --- /dev/null +++ b/x-pack/plugins/alerting/server/rules_client/methods/bulk_edit.ts @@ -0,0 +1,544 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor 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 pMap from 'p-map'; +import Boom from '@hapi/boom'; +import { cloneDeep } from 'lodash'; +import { AlertConsumers } from '@kbn/rule-data-utils'; +import { KueryNode, nodeBuilder } from '@kbn/es-query'; +import { SavedObjectsBulkUpdateObject, SavedObjectsUpdateResponse } from '@kbn/core/server'; +import { RawRule, SanitizedRule, RuleTypeParams, Rule, RuleSnoozeSchedule } from '../../types'; +import { + validateRuleTypeParams, + getRuleNotifyWhenType, + validateMutatedRuleTypeParams, + convertRuleIdsToKueryNode, +} from '../../lib'; +import { WriteOperations, AlertingAuthorizationEntity } from '../../authorization'; +import { parseDuration } from '../../../common/parse_duration'; +import { bulkMarkApiKeysForInvalidation } from '../../invalidate_pending_api_keys/bulk_mark_api_keys_for_invalidation'; +import { ruleAuditEvent, RuleAuditAction } from '../common/audit_events'; +import { + retryIfBulkEditConflicts, + applyBulkEditOperation, + buildKueryNodeFilter, + injectReferencesIntoActions, + generateAPIKeyName, + apiKeyAsAlertAttributes, + getBulkSnoozeAttributes, + getBulkUnsnoozeAttributes, + verifySnoozeScheduleLimit, +} from '../common'; +import { + alertingAuthorizationFilterOpts, + MAX_RULES_NUMBER_FOR_BULK_OPERATION, + RULE_TYPE_CHECKS_CONCURRENCY, + API_KEY_GENERATE_CONCURRENCY, +} from '../common/constants'; +import { getMappedParams } from '../common/mapped_params_utils'; +import { getAlertFromRaw, extractReferences, validateActions, updateMeta } from '../lib'; +import { + NormalizedAlertAction, + BulkOperationError, + RuleBulkOperationAggregation, + RulesClientContext, +} from '../types'; + +export type BulkEditFields = keyof Pick< + Rule, + 'actions' | 'tags' | 'schedule' | 'throttle' | 'notifyWhen' | 'snoozeSchedule' | 'apiKey' +>; + +export type BulkEditOperation = + | { + operation: 'add' | 'delete' | 'set'; + field: Extract; + value: string[]; + } + | { + operation: 'add' | 'set'; + field: Extract; + value: NormalizedAlertAction[]; + } + | { + operation: 'set'; + field: Extract; + value: Rule['schedule']; + } + | { + operation: 'set'; + field: Extract; + value: Rule['throttle']; + } + | { + operation: 'set'; + field: Extract; + value: Rule['notifyWhen']; + } + | { + operation: 'set'; + field: Extract; + value: RuleSnoozeSchedule; + } + | { + operation: 'delete'; + field: Extract; + value?: string[]; + } + | { + operation: 'set'; + field: Extract; + value?: undefined; + }; + +type RuleParamsModifier = (params: Params) => Promise; + +export interface BulkEditOptionsFilter { + filter?: string | KueryNode; + operations: BulkEditOperation[]; + paramsModifier?: RuleParamsModifier; +} + +export interface BulkEditOptionsIds { + ids: string[]; + operations: BulkEditOperation[]; + paramsModifier?: RuleParamsModifier; +} + +export type BulkEditOptions = + | BulkEditOptionsFilter + | BulkEditOptionsIds; + +export async function bulkEdit( + context: RulesClientContext, + options: BulkEditOptions +): Promise<{ + rules: Array>; + errors: BulkOperationError[]; + total: number; +}> { + const queryFilter = (options as BulkEditOptionsFilter).filter; + const ids = (options as BulkEditOptionsIds).ids; + + if (ids && queryFilter) { + throw Boom.badRequest( + "Both 'filter' and 'ids' are supplied. Define either 'ids' or 'filter' properties in method arguments" + ); + } + + const qNodeQueryFilter = buildKueryNodeFilter(queryFilter); + + const qNodeFilter = ids ? convertRuleIdsToKueryNode(ids) : qNodeQueryFilter; + let authorizationTuple; + try { + authorizationTuple = await context.authorization.getFindAuthorizationFilter( + AlertingAuthorizationEntity.Rule, + alertingAuthorizationFilterOpts + ); + } catch (error) { + context.auditLogger?.log( + ruleAuditEvent({ + action: RuleAuditAction.BULK_EDIT, + error, + }) + ); + throw error; + } + const { filter: authorizationFilter } = authorizationTuple; + const qNodeFilterWithAuth = + authorizationFilter && qNodeFilter + ? nodeBuilder.and([qNodeFilter, authorizationFilter as KueryNode]) + : qNodeFilter; + + const { aggregations, total } = await context.unsecuredSavedObjectsClient.find< + RawRule, + RuleBulkOperationAggregation + >({ + filter: qNodeFilterWithAuth, + page: 1, + perPage: 0, + type: 'alert', + aggs: { + alertTypeId: { + multi_terms: { + terms: [ + { field: 'alert.attributes.alertTypeId' }, + { field: 'alert.attributes.consumer' }, + ], + }, + }, + }, + }); + + if (total > MAX_RULES_NUMBER_FOR_BULK_OPERATION) { + throw Boom.badRequest( + `More than ${MAX_RULES_NUMBER_FOR_BULK_OPERATION} rules matched for bulk edit` + ); + } + const buckets = aggregations?.alertTypeId.buckets; + + if (buckets === undefined) { + throw Error('No rules found for bulk edit'); + } + + await pMap( + buckets, + async ({ key: [ruleType, consumer] }) => { + context.ruleTypeRegistry.ensureRuleTypeEnabled(ruleType); + + try { + await context.authorization.ensureAuthorized({ + ruleTypeId: ruleType, + consumer, + operation: WriteOperations.BulkEdit, + entity: AlertingAuthorizationEntity.Rule, + }); + } catch (error) { + context.auditLogger?.log( + ruleAuditEvent({ + action: RuleAuditAction.BULK_EDIT, + error, + }) + ); + throw error; + } + }, + { concurrency: RULE_TYPE_CHECKS_CONCURRENCY } + ); + + const { apiKeysToInvalidate, results, errors } = await retryIfBulkEditConflicts( + context.logger, + `rulesClient.update('operations=${JSON.stringify(options.operations)}, paramsModifier=${ + options.paramsModifier ? '[Function]' : undefined + }')`, + (filterKueryNode: KueryNode | null) => + bulkEditOcc(context, { + filter: filterKueryNode, + operations: options.operations, + paramsModifier: options.paramsModifier, + }), + qNodeFilterWithAuth + ); + + await bulkMarkApiKeysForInvalidation( + { apiKeys: apiKeysToInvalidate }, + context.logger, + context.unsecuredSavedObjectsClient + ); + + const updatedRules = results.map(({ id, attributes, references }) => { + return getAlertFromRaw( + context, + id, + attributes.alertTypeId as string, + attributes as RawRule, + references, + false + ); + }); + + // update schedules only if schedule operation is present + const scheduleOperation = options.operations.find( + ( + operation + ): operation is Extract }> => + operation.field === 'schedule' + ); + + if (scheduleOperation?.value) { + const taskIds = updatedRules.reduce((acc, rule) => { + if (rule.scheduledTaskId) { + acc.push(rule.scheduledTaskId); + } + return acc; + }, []); + + try { + await context.taskManager.bulkUpdateSchedules(taskIds, scheduleOperation.value); + context.logger.debug( + `Successfully updated schedules for underlying tasks: ${taskIds.join(', ')}` + ); + } catch (error) { + context.logger.error( + `Failure to update schedules for underlying tasks: ${taskIds.join( + ', ' + )}. TaskManager bulkUpdateSchedules failed with Error: ${error.message}` + ); + } + } + + return { rules: updatedRules, errors, total }; +} + +async function bulkEditOcc( + context: RulesClientContext, + { + filter, + operations, + paramsModifier, + }: { + filter: KueryNode | null; + operations: BulkEditOptions['operations']; + paramsModifier: BulkEditOptions['paramsModifier']; + } +): Promise<{ + apiKeysToInvalidate: string[]; + rules: Array>; + resultSavedObjects: Array>; + errors: BulkOperationError[]; +}> { + const rulesFinder = + await context.encryptedSavedObjectsClient.createPointInTimeFinderDecryptedAsInternalUser( + { + filter, + type: 'alert', + perPage: 100, + ...(context.namespace ? { namespaces: [context.namespace] } : undefined), + } + ); + + const rules: Array> = []; + const errors: BulkOperationError[] = []; + const apiKeysToInvalidate: string[] = []; + const apiKeysMap = new Map(); + const username = await context.getUserName(); + + for await (const response of rulesFinder.find()) { + await pMap( + response.saved_objects, + async (rule) => { + try { + if (rule.attributes.apiKey) { + apiKeysMap.set(rule.id, { oldApiKey: rule.attributes.apiKey }); + } + + const ruleType = context.ruleTypeRegistry.get(rule.attributes.alertTypeId); + + let attributes = cloneDeep(rule.attributes); + let ruleActions = { + actions: injectReferencesIntoActions( + rule.id, + rule.attributes.actions, + rule.references || [] + ), + }; + + for (const operation of operations) { + const { field } = operation; + if (field === 'snoozeSchedule' || field === 'apiKey') { + if (rule.attributes.actions.length) { + try { + await context.actionsAuthorization.ensureAuthorized('execute'); + } catch (error) { + throw Error(`Rule not authorized for bulk ${field} update - ${error.message}`); + } + } + } + } + + let hasUpdateApiKeyOperation = false; + + for (const operation of operations) { + switch (operation.field) { + case 'actions': + await validateActions(context, ruleType, { + ...attributes, + actions: operation.value, + }); + ruleActions = applyBulkEditOperation(operation, ruleActions); + break; + case 'snoozeSchedule': + // Silently skip adding snooze or snooze schedules on security + // rules until we implement snoozing of their rules + if (attributes.consumer === AlertConsumers.SIEM) { + break; + } + if (operation.operation === 'set') { + const snoozeAttributes = getBulkSnoozeAttributes(attributes, operation.value); + try { + verifySnoozeScheduleLimit(snoozeAttributes); + } catch (error) { + throw Error(`Error updating rule: could not add snooze - ${error.message}`); + } + attributes = { + ...attributes, + ...snoozeAttributes, + }; + } + if (operation.operation === 'delete') { + const idsToDelete = operation.value && [...operation.value]; + if (idsToDelete?.length === 0) { + attributes.snoozeSchedule?.forEach((schedule) => { + if (schedule.id) { + idsToDelete.push(schedule.id); + } + }); + } + attributes = { + ...attributes, + ...getBulkUnsnoozeAttributes(attributes, idsToDelete), + }; + } + break; + case 'apiKey': { + hasUpdateApiKeyOperation = true; + break; + } + default: + attributes = applyBulkEditOperation(operation, attributes); + } + } + + // validate schedule interval + if (attributes.schedule.interval) { + const isIntervalInvalid = + parseDuration(attributes.schedule.interval as string) < + context.minimumScheduleIntervalInMs; + if (isIntervalInvalid && context.minimumScheduleInterval.enforce) { + throw Error( + `Error updating rule: the interval is less than the allowed minimum interval of ${context.minimumScheduleInterval.value}` + ); + } else if (isIntervalInvalid && !context.minimumScheduleInterval.enforce) { + context.logger.warn( + `Rule schedule interval (${attributes.schedule.interval}) for "${ruleType.id}" rule type with ID "${attributes.id}" is less than the minimum value (${context.minimumScheduleInterval.value}). Running rules at this interval may impact alerting performance. Set "xpack.alerting.rules.minimumScheduleInterval.enforce" to true to prevent such changes.` + ); + } + } + + const ruleParams = paramsModifier + ? await paramsModifier(attributes.params as Params) + : attributes.params; + + // validate rule params + const validatedAlertTypeParams = validateRuleTypeParams( + ruleParams, + ruleType.validate?.params + ); + const validatedMutatedAlertTypeParams = validateMutatedRuleTypeParams( + validatedAlertTypeParams, + rule.attributes.params, + ruleType.validate?.params + ); + + const { + actions: rawAlertActions, + references, + params: updatedParams, + } = await extractReferences( + context, + ruleType, + ruleActions.actions, + validatedMutatedAlertTypeParams + ); + + const shouldUpdateApiKey = attributes.enabled || hasUpdateApiKeyOperation; + + // create API key + let createdAPIKey = null; + try { + createdAPIKey = shouldUpdateApiKey + ? await context.createAPIKey(generateAPIKeyName(ruleType.id, attributes.name)) + : null; + } catch (error) { + throw Error(`Error updating rule: could not create API key - ${error.message}`); + } + + const apiKeyAttributes = apiKeyAsAlertAttributes(createdAPIKey, username); + + // collect generated API keys + if (apiKeyAttributes.apiKey) { + apiKeysMap.set(rule.id, { + ...apiKeysMap.get(rule.id), + newApiKey: apiKeyAttributes.apiKey, + }); + } + + // get notifyWhen + const notifyWhen = getRuleNotifyWhenType( + attributes.notifyWhen ?? null, + attributes.throttle ?? null + ); + + const updatedAttributes = updateMeta(context, { + ...attributes, + ...apiKeyAttributes, + params: updatedParams as RawRule['params'], + actions: rawAlertActions, + notifyWhen, + updatedBy: username, + updatedAt: new Date().toISOString(), + }); + + // add mapped_params + const mappedParams = getMappedParams(updatedParams); + + if (Object.keys(mappedParams).length) { + updatedAttributes.mapped_params = mappedParams; + } + + rules.push({ + ...rule, + references, + attributes: updatedAttributes, + }); + } catch (error) { + errors.push({ + message: error.message, + rule: { + id: rule.id, + name: rule.attributes?.name, + }, + }); + context.auditLogger?.log( + ruleAuditEvent({ + action: RuleAuditAction.BULK_EDIT, + error, + }) + ); + } + }, + { concurrency: API_KEY_GENERATE_CONCURRENCY } + ); + } + + let result; + try { + result = await context.unsecuredSavedObjectsClient.bulkCreate(rules, { overwrite: true }); + } catch (e) { + // avoid unused newly generated API keys + if (apiKeysMap.size > 0) { + await bulkMarkApiKeysForInvalidation( + { + apiKeys: Array.from(apiKeysMap.values()).reduce((acc, value) => { + if (value.newApiKey) { + acc.push(value.newApiKey); + } + return acc; + }, []), + }, + context.logger, + context.unsecuredSavedObjectsClient + ); + } + throw e; + } + + result.saved_objects.map(({ id, error }) => { + const oldApiKey = apiKeysMap.get(id)?.oldApiKey; + const newApiKey = apiKeysMap.get(id)?.newApiKey; + + // if SO wasn't saved and has new API key it will be invalidated + if (error && newApiKey) { + apiKeysToInvalidate.push(newApiKey); + // if SO saved and has old Api Key it will be invalidate + } else if (!error && oldApiKey) { + apiKeysToInvalidate.push(oldApiKey); + } + }); + + return { apiKeysToInvalidate, resultSavedObjects: result.saved_objects, errors, rules }; +} diff --git a/x-pack/plugins/alerting/server/rules_client/methods/bulk_enable.ts b/x-pack/plugins/alerting/server/rules_client/methods/bulk_enable.ts new file mode 100644 index 0000000000000..394f7aad0dea7 --- /dev/null +++ b/x-pack/plugins/alerting/server/rules_client/methods/bulk_enable.ts @@ -0,0 +1,240 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import pMap from 'p-map'; +import { KueryNode, nodeBuilder } from '@kbn/es-query'; +import { SavedObjectsBulkUpdateObject } from '@kbn/core/server'; +import { RawRule, IntervalSchedule } from '../../types'; +import { convertRuleIdsToKueryNode } from '../../lib'; +import { ruleAuditEvent, RuleAuditAction } from '../common/audit_events'; +import { + retryIfBulkOperationConflicts, + buildKueryNodeFilter, + getAndValidateCommonBulkOptions, +} from '../common'; +import { + getAuthorizationFilter, + checkAuthorizationAndGetTotal, + getAlertFromRaw, + scheduleTask, + updateMeta, + createNewAPIKeySet, +} from '../lib'; +import { RulesClientContext, BulkOperationError, BulkOptions } from '../types'; + +const getShouldScheduleTask = async ( + context: RulesClientContext, + scheduledTaskId: string | null | undefined +) => { + if (!scheduledTaskId) return true; + try { + // make sure scheduledTaskId exist + await context.taskManager.get(scheduledTaskId); + return false; + } catch (err) { + return true; + } +}; + +export const bulkEnableRules = async (context: RulesClientContext, options: BulkOptions) => { + const { ids, filter } = getAndValidateCommonBulkOptions(options); + + const kueryNodeFilter = ids ? convertRuleIdsToKueryNode(ids) : buildKueryNodeFilter(filter); + const authorizationFilter = await getAuthorizationFilter(context, { action: 'ENABLE' }); + + const kueryNodeFilterWithAuth = + authorizationFilter && kueryNodeFilter + ? nodeBuilder.and([kueryNodeFilter, authorizationFilter as KueryNode]) + : kueryNodeFilter; + + const { total } = await checkAuthorizationAndGetTotal(context, { + filter: kueryNodeFilterWithAuth, + action: 'ENABLE', + }); + + const { errors, rules, accListSpecificForBulkOperation } = await retryIfBulkOperationConflicts({ + action: 'ENABLE', + logger: context.logger, + bulkOperation: (filterKueryNode: KueryNode | null) => + bulkEnableRulesWithOCC(context, { filter: filterKueryNode }), + filter: kueryNodeFilterWithAuth, + }); + + const [taskIdsToEnable] = accListSpecificForBulkOperation; + + const taskIdsFailedToBeEnabled: string[] = []; + if (taskIdsToEnable.length > 0) { + try { + const resultFromEnablingTasks = await context.taskManager.bulkEnable(taskIdsToEnable); + resultFromEnablingTasks?.errors?.forEach((error) => { + taskIdsFailedToBeEnabled.push(error.task.id); + }); + if (resultFromEnablingTasks.tasks.length) { + context.logger.debug( + `Successfully enabled schedules for underlying tasks: ${resultFromEnablingTasks.tasks + .map((task) => task.id) + .join(', ')}` + ); + } + if (resultFromEnablingTasks.errors.length) { + context.logger.error( + `Failure to enable schedules for underlying tasks: ${resultFromEnablingTasks.errors + .map((error) => error.task.id) + .join(', ')}` + ); + } + } catch (error) { + taskIdsFailedToBeEnabled.push(...taskIdsToEnable); + context.logger.error( + `Failure to enable schedules for underlying tasks: ${taskIdsToEnable.join( + ', ' + )}. TaskManager bulkEnable failed with Error: ${error.message}` + ); + } + } + + const updatedRules = rules.map(({ id, attributes, references }) => { + return getAlertFromRaw( + context, + id, + attributes.alertTypeId as string, + attributes as RawRule, + references, + false + ); + }); + + return { errors, rules: updatedRules, total, taskIdsFailedToBeEnabled }; +}; + +const bulkEnableRulesWithOCC = async ( + context: RulesClientContext, + { filter }: { filter: KueryNode | null } +) => { + const rulesFinder = + await context.encryptedSavedObjectsClient.createPointInTimeFinderDecryptedAsInternalUser( + { + filter, + type: 'alert', + perPage: 100, + ...(context.namespace ? { namespaces: [context.namespace] } : undefined), + } + ); + + const rulesToEnable: Array> = []; + const taskIdsToEnable: string[] = []; + const errors: BulkOperationError[] = []; + const ruleNameToRuleIdMapping: Record = {}; + + for await (const response of rulesFinder.find()) { + await pMap(response.saved_objects, async (rule) => { + try { + if (rule.attributes.actions.length) { + try { + await context.actionsAuthorization.ensureAuthorized('execute'); + } catch (error) { + throw Error(`Rule not authorized for bulk enable - ${error.message}`); + } + } + if (rule.attributes.enabled === true) return; + if (rule.attributes.name) { + ruleNameToRuleIdMapping[rule.id] = rule.attributes.name; + } + + const username = await context.getUserName(); + + const updatedAttributes = updateMeta(context, { + ...rule.attributes, + ...(!rule.attributes.apiKey && + (await createNewAPIKeySet(context, { attributes: rule.attributes, username }))), + enabled: true, + updatedBy: username, + updatedAt: new Date().toISOString(), + executionStatus: { + status: 'pending', + lastDuration: 0, + lastExecutionDate: new Date().toISOString(), + error: null, + warning: null, + }, + }); + + const shouldScheduleTask = await getShouldScheduleTask( + context, + rule.attributes.scheduledTaskId + ); + + let scheduledTaskId; + if (shouldScheduleTask) { + const scheduledTask = await scheduleTask(context, { + id: rule.id, + consumer: rule.attributes.consumer, + ruleTypeId: rule.attributes.alertTypeId, + schedule: rule.attributes.schedule as IntervalSchedule, + throwOnConflict: false, + }); + scheduledTaskId = scheduledTask.id; + } + + rulesToEnable.push({ + ...rule, + attributes: { + ...updatedAttributes, + ...(scheduledTaskId ? { scheduledTaskId } : undefined), + }, + }); + + context.auditLogger?.log( + ruleAuditEvent({ + action: RuleAuditAction.ENABLE, + outcome: 'unknown', + savedObject: { type: 'alert', id: rule.id }, + }) + ); + } catch (error) { + errors.push({ + message: error.message, + rule: { + id: rule.id, + name: rule.attributes?.name, + }, + }); + context.auditLogger?.log( + ruleAuditEvent({ + action: RuleAuditAction.ENABLE, + error, + }) + ); + } + }); + } + + const result = await context.unsecuredSavedObjectsClient.bulkCreate(rulesToEnable, { + overwrite: true, + }); + + const rules: Array> = []; + + result.saved_objects.forEach((rule) => { + if (rule.error === undefined) { + if (rule.attributes.scheduledTaskId) { + taskIdsToEnable.push(rule.attributes.scheduledTaskId); + } + rules.push(rule); + } else { + errors.push({ + message: rule.error.message ?? 'n/a', + status: rule.error.statusCode, + rule: { + id: rule.id, + name: ruleNameToRuleIdMapping[rule.id] ?? 'n/a', + }, + }); + } + }); + return { errors, rules, accListSpecificForBulkOperation: [taskIdsToEnable] }; +}; diff --git a/x-pack/plugins/alerting/server/rules_client/methods/clear_expired_snoozes.ts b/x-pack/plugins/alerting/server/rules_client/methods/clear_expired_snoozes.ts new file mode 100644 index 0000000000000..284e5c89e25f5 --- /dev/null +++ b/x-pack/plugins/alerting/server/rules_client/methods/clear_expired_snoozes.ts @@ -0,0 +1,49 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { RawRule } from '../../types'; +import { partiallyUpdateAlert } from '../../saved_objects'; +import { isSnoozeExpired } from '../../lib'; +import { RulesClientContext } from '../types'; +import { updateMeta } from '../lib'; + +export async function clearExpiredSnoozes( + context: RulesClientContext, + { id }: { id: string } +): Promise { + const { attributes, version } = await context.unsecuredSavedObjectsClient.get( + 'alert', + id + ); + + const snoozeSchedule = attributes.snoozeSchedule + ? attributes.snoozeSchedule.filter((s) => { + try { + return !isSnoozeExpired(s); + } catch (e) { + context.logger.error(`Error checking for expiration of snooze ${s.id}: ${e}`); + return true; + } + }) + : []; + + if (snoozeSchedule.length === attributes.snoozeSchedule?.length) return; + + const updateAttributes = updateMeta(context, { + snoozeSchedule, + updatedBy: await context.getUserName(), + updatedAt: new Date().toISOString(), + }); + const updateOptions = { version }; + + await partiallyUpdateAlert( + context.unsecuredSavedObjectsClient, + id, + updateAttributes, + updateOptions + ); +} diff --git a/x-pack/plugins/alerting/server/rules_client/methods/clone.ts b/x-pack/plugins/alerting/server/rules_client/methods/clone.ts new file mode 100644 index 0000000000000..b4ebe5891885c --- /dev/null +++ b/x-pack/plugins/alerting/server/rules_client/methods/clone.ts @@ -0,0 +1,131 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor 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 Semver from 'semver'; +import Boom from '@hapi/boom'; +import { AlertConsumers } from '@kbn/rule-data-utils'; +import { SavedObject, SavedObjectsUtils } from '@kbn/core/server'; +import { RawRule, SanitizedRule, RuleTypeParams } from '../../types'; +import { getDefaultMonitoring } from '../../lib'; +import { WriteOperations, AlertingAuthorizationEntity } from '../../authorization'; +import { parseDuration } from '../../../common/parse_duration'; +import { ruleAuditEvent, RuleAuditAction } from '../common/audit_events'; +import { getRuleExecutionStatusPending } from '../../lib/rule_execution_status'; +import { isDetectionEngineAADRuleType } from '../../saved_objects/migrations/utils'; +import { generateAPIKeyName, apiKeyAsAlertAttributes } from '../common'; +import { createRuleSavedObject } from '../lib'; +import { RulesClientContext } from '../types'; + +export type CloneArguments = [string, { newId?: string }]; + +export async function clone( + context: RulesClientContext, + id: string, + { newId }: { newId?: string } +): Promise> { + let ruleSavedObject: SavedObject; + + try { + ruleSavedObject = await context.encryptedSavedObjectsClient.getDecryptedAsInternalUser( + 'alert', + id, + { + namespace: context.namespace, + } + ); + } catch (e) { + // We'll skip invalidating the API key since we failed to load the decrypted saved object + context.logger.error( + `update(): Failed to load API key to invalidate on alert ${id}: ${e.message}` + ); + // Still attempt to load the object using SOC + ruleSavedObject = await context.unsecuredSavedObjectsClient.get('alert', id); + } + + /* + * As the time of the creation of this PR, security solution already have a clone/duplicate API + * with some specific business logic so to avoid weird bugs, I prefer to exclude them from this + * functionality until we resolve our difference + */ + if ( + isDetectionEngineAADRuleType(ruleSavedObject) || + ruleSavedObject.attributes.consumer === AlertConsumers.SIEM + ) { + throw Boom.badRequest( + 'The clone functionality is not enable for rule who belongs to security solution' + ); + } + const ruleName = + ruleSavedObject.attributes.name.indexOf('[Clone]') > 0 + ? ruleSavedObject.attributes.name + : `${ruleSavedObject.attributes.name} [Clone]`; + const ruleId = newId ?? SavedObjectsUtils.generateId(); + try { + await context.authorization.ensureAuthorized({ + ruleTypeId: ruleSavedObject.attributes.alertTypeId, + consumer: ruleSavedObject.attributes.consumer, + operation: WriteOperations.Create, + entity: AlertingAuthorizationEntity.Rule, + }); + } catch (error) { + context.auditLogger?.log( + ruleAuditEvent({ + action: RuleAuditAction.CREATE, + savedObject: { type: 'alert', id }, + error, + }) + ); + throw error; + } + + context.ruleTypeRegistry.ensureRuleTypeEnabled(ruleSavedObject.attributes.alertTypeId); + // Throws an error if alert type isn't registered + const ruleType = context.ruleTypeRegistry.get(ruleSavedObject.attributes.alertTypeId); + const username = await context.getUserName(); + const createTime = Date.now(); + const lastRunTimestamp = new Date(); + const legacyId = Semver.lt(context.kibanaVersion, '8.0.0') ? id : null; + let createdAPIKey = null; + try { + createdAPIKey = ruleSavedObject.attributes.enabled + ? await context.createAPIKey(generateAPIKeyName(ruleType.id, ruleName)) + : null; + } catch (error) { + throw Boom.badRequest(`Error creating rule: could not create API key - ${error.message}`); + } + const rawRule: RawRule = { + ...ruleSavedObject.attributes, + name: ruleName, + ...apiKeyAsAlertAttributes(createdAPIKey, username), + legacyId, + createdBy: username, + updatedBy: username, + createdAt: new Date(createTime).toISOString(), + updatedAt: new Date(createTime).toISOString(), + snoozeSchedule: [], + muteAll: false, + mutedInstanceIds: [], + executionStatus: getRuleExecutionStatusPending(lastRunTimestamp.toISOString()), + monitoring: getDefaultMonitoring(lastRunTimestamp.toISOString()), + scheduledTaskId: null, + }; + + context.auditLogger?.log( + ruleAuditEvent({ + action: RuleAuditAction.CREATE, + outcome: 'unknown', + savedObject: { type: 'alert', id }, + }) + ); + + return await createRuleSavedObject(context, { + intervalInMs: parseDuration(rawRule.schedule.interval), + rawRule, + references: ruleSavedObject.references, + ruleId, + }); +} diff --git a/x-pack/plugins/alerting/server/rules_client/methods/create.ts b/x-pack/plugins/alerting/server/rules_client/methods/create.ts new file mode 100644 index 0000000000000..31707726b4e24 --- /dev/null +++ b/x-pack/plugins/alerting/server/rules_client/methods/create.ts @@ -0,0 +1,147 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor 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 Semver from 'semver'; +import Boom from '@hapi/boom'; +import { SavedObjectsUtils } from '@kbn/core/server'; +import { parseDuration } from '../../../common/parse_duration'; +import { RawRule, SanitizedRule, RuleTypeParams, RuleAction, Rule } from '../../types'; +import { WriteOperations, AlertingAuthorizationEntity } from '../../authorization'; +import { validateRuleTypeParams, getRuleNotifyWhenType, getDefaultMonitoring } from '../../lib'; +import { getRuleExecutionStatusPending } from '../../lib/rule_execution_status'; +import { createRuleSavedObject, extractReferences, validateActions } from '../lib'; +import { generateAPIKeyName, getMappedParams, apiKeyAsAlertAttributes } from '../common'; +import { ruleAuditEvent, RuleAuditAction } from '../common/audit_events'; +import { RulesClientContext } from '../types'; + +type NormalizedAlertAction = Omit; +interface SavedObjectOptions { + id?: string; + migrationVersion?: Record; +} + +export interface CreateOptions { + data: Omit< + Rule, + | 'id' + | 'createdBy' + | 'updatedBy' + | 'createdAt' + | 'updatedAt' + | 'apiKey' + | 'apiKeyOwner' + | 'muteAll' + | 'mutedInstanceIds' + | 'actions' + | 'executionStatus' + | 'snoozeSchedule' + | 'isSnoozedUntil' + | 'lastRun' + | 'nextRun' + > & { actions: NormalizedAlertAction[] }; + options?: SavedObjectOptions; +} + +export async function create( + context: RulesClientContext, + { data, options }: CreateOptions +): Promise> { + const id = options?.id || SavedObjectsUtils.generateId(); + + try { + await context.authorization.ensureAuthorized({ + ruleTypeId: data.alertTypeId, + consumer: data.consumer, + operation: WriteOperations.Create, + entity: AlertingAuthorizationEntity.Rule, + }); + } catch (error) { + context.auditLogger?.log( + ruleAuditEvent({ + action: RuleAuditAction.CREATE, + savedObject: { type: 'alert', id }, + error, + }) + ); + throw error; + } + + context.ruleTypeRegistry.ensureRuleTypeEnabled(data.alertTypeId); + + // Throws an error if alert type isn't registered + const ruleType = context.ruleTypeRegistry.get(data.alertTypeId); + + const validatedAlertTypeParams = validateRuleTypeParams(data.params, ruleType.validate?.params); + const username = await context.getUserName(); + + let createdAPIKey = null; + try { + createdAPIKey = data.enabled + ? await context.createAPIKey(generateAPIKeyName(ruleType.id, data.name)) + : null; + } catch (error) { + throw Boom.badRequest(`Error creating rule: could not create API key - ${error.message}`); + } + + await validateActions(context, ruleType, data); + + // Throw error if schedule interval is less than the minimum and we are enforcing it + const intervalInMs = parseDuration(data.schedule.interval); + if ( + intervalInMs < context.minimumScheduleIntervalInMs && + context.minimumScheduleInterval.enforce + ) { + throw Boom.badRequest( + `Error creating rule: the interval is less than the allowed minimum interval of ${context.minimumScheduleInterval.value}` + ); + } + + // Extract saved object references for this rule + const { + references, + params: updatedParams, + actions, + } = await extractReferences(context, ruleType, data.actions, validatedAlertTypeParams); + + const createTime = Date.now(); + const lastRunTimestamp = new Date(); + const legacyId = Semver.lt(context.kibanaVersion, '8.0.0') ? id : null; + const notifyWhen = getRuleNotifyWhenType(data.notifyWhen ?? null, data.throttle ?? null); + const throttle = data.throttle ?? null; + + const rawRule: RawRule = { + ...data, + ...apiKeyAsAlertAttributes(createdAPIKey, username), + legacyId, + actions, + createdBy: username, + updatedBy: username, + createdAt: new Date(createTime).toISOString(), + updatedAt: new Date(createTime).toISOString(), + snoozeSchedule: [], + params: updatedParams as RawRule['params'], + muteAll: false, + mutedInstanceIds: [], + notifyWhen, + throttle, + executionStatus: getRuleExecutionStatusPending(lastRunTimestamp.toISOString()), + monitoring: getDefaultMonitoring(lastRunTimestamp.toISOString()), + }; + + const mappedParams = getMappedParams(updatedParams); + + if (Object.keys(mappedParams).length) { + rawRule.mapped_params = mappedParams; + } + + return await createRuleSavedObject(context, { + intervalInMs, + rawRule, + references, + ruleId: id, + options, + }); +} diff --git a/x-pack/plugins/alerting/server/rules_client/methods/delete.ts b/x-pack/plugins/alerting/server/rules_client/methods/delete.ts new file mode 100644 index 0000000000000..406184e8f013e --- /dev/null +++ b/x-pack/plugins/alerting/server/rules_client/methods/delete.ts @@ -0,0 +1,86 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { RawRule } from '../../types'; +import { WriteOperations, AlertingAuthorizationEntity } from '../../authorization'; +import { retryIfConflicts } from '../../lib/retry_if_conflicts'; +import { bulkMarkApiKeysForInvalidation } from '../../invalidate_pending_api_keys/bulk_mark_api_keys_for_invalidation'; +import { ruleAuditEvent, RuleAuditAction } from '../common/audit_events'; +import { RulesClientContext } from '../types'; + +export async function deleteRule(context: RulesClientContext, { id }: { id: string }) { + return await retryIfConflicts( + context.logger, + `rulesClient.delete('${id}')`, + async () => await deleteWithOCC(context, { id }) + ); +} + +async function deleteWithOCC(context: RulesClientContext, { id }: { id: string }) { + let taskIdToRemove: string | undefined | null; + let apiKeyToInvalidate: string | null = null; + let attributes: RawRule; + + try { + const decryptedAlert = + await context.encryptedSavedObjectsClient.getDecryptedAsInternalUser('alert', id, { + namespace: context.namespace, + }); + apiKeyToInvalidate = decryptedAlert.attributes.apiKey; + taskIdToRemove = decryptedAlert.attributes.scheduledTaskId; + attributes = decryptedAlert.attributes; + } catch (e) { + // We'll skip invalidating the API key since we failed to load the decrypted saved object + context.logger.error( + `delete(): Failed to load API key to invalidate on alert ${id}: ${e.message}` + ); + // Still attempt to load the scheduledTaskId using SOC + const alert = await context.unsecuredSavedObjectsClient.get('alert', id); + taskIdToRemove = alert.attributes.scheduledTaskId; + attributes = alert.attributes; + } + + try { + await context.authorization.ensureAuthorized({ + ruleTypeId: attributes.alertTypeId, + consumer: attributes.consumer, + operation: WriteOperations.Delete, + entity: AlertingAuthorizationEntity.Rule, + }); + } catch (error) { + context.auditLogger?.log( + ruleAuditEvent({ + action: RuleAuditAction.DELETE, + savedObject: { type: 'alert', id }, + error, + }) + ); + throw error; + } + + context.auditLogger?.log( + ruleAuditEvent({ + action: RuleAuditAction.DELETE, + outcome: 'unknown', + savedObject: { type: 'alert', id }, + }) + ); + const removeResult = await context.unsecuredSavedObjectsClient.delete('alert', id); + + await Promise.all([ + taskIdToRemove ? context.taskManager.removeIfExists(taskIdToRemove) : null, + apiKeyToInvalidate + ? bulkMarkApiKeysForInvalidation( + { apiKeys: [apiKeyToInvalidate] }, + context.logger, + context.unsecuredSavedObjectsClient + ) + : null, + ]); + + return removeResult; +} diff --git a/x-pack/plugins/alerting/server/rules_client/methods/disable.ts b/x-pack/plugins/alerting/server/rules_client/methods/disable.ts new file mode 100644 index 0000000000000..3eae1d2df7b5d --- /dev/null +++ b/x-pack/plugins/alerting/server/rules_client/methods/disable.ts @@ -0,0 +1,97 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { RawRule } from '../../types'; +import { WriteOperations, AlertingAuthorizationEntity } from '../../authorization'; +import { retryIfConflicts } from '../../lib/retry_if_conflicts'; +import { ruleAuditEvent, RuleAuditAction } from '../common/audit_events'; +import { RulesClientContext } from '../types'; +import { recoverRuleAlerts, updateMeta } from '../lib'; + +export async function disable(context: RulesClientContext, { id }: { id: string }): Promise { + return await retryIfConflicts( + context.logger, + `rulesClient.disable('${id}')`, + async () => await disableWithOCC(context, { id }) + ); +} + +async function disableWithOCC(context: RulesClientContext, { id }: { id: string }) { + let attributes: RawRule; + let version: string | undefined; + + try { + const decryptedAlert = + await context.encryptedSavedObjectsClient.getDecryptedAsInternalUser('alert', id, { + namespace: context.namespace, + }); + attributes = decryptedAlert.attributes; + version = decryptedAlert.version; + } catch (e) { + context.logger.error(`disable(): Failed to load API key of alert ${id}: ${e.message}`); + // Still attempt to load the attributes and version using SOC + const alert = await context.unsecuredSavedObjectsClient.get('alert', id); + attributes = alert.attributes; + version = alert.version; + } + + recoverRuleAlerts(context, id, attributes); + + try { + await context.authorization.ensureAuthorized({ + ruleTypeId: attributes.alertTypeId, + consumer: attributes.consumer, + operation: WriteOperations.Disable, + entity: AlertingAuthorizationEntity.Rule, + }); + } catch (error) { + context.auditLogger?.log( + ruleAuditEvent({ + action: RuleAuditAction.DISABLE, + savedObject: { type: 'alert', id }, + error, + }) + ); + throw error; + } + + context.auditLogger?.log( + ruleAuditEvent({ + action: RuleAuditAction.DISABLE, + outcome: 'unknown', + savedObject: { type: 'alert', id }, + }) + ); + + context.ruleTypeRegistry.ensureRuleTypeEnabled(attributes.alertTypeId); + + if (attributes.enabled === true) { + await context.unsecuredSavedObjectsClient.update( + 'alert', + id, + updateMeta(context, { + ...attributes, + enabled: false, + scheduledTaskId: attributes.scheduledTaskId === id ? attributes.scheduledTaskId : null, + updatedBy: await context.getUserName(), + updatedAt: new Date().toISOString(), + nextRun: null, + }), + { version } + ); + + // If the scheduledTaskId does not match the rule id, we should + // remove the task, otherwise mark the task as disabled + if (attributes.scheduledTaskId) { + if (attributes.scheduledTaskId !== id) { + await context.taskManager.removeIfExists(attributes.scheduledTaskId); + } else { + await context.taskManager.bulkDisable([attributes.scheduledTaskId]); + } + } + } +} diff --git a/x-pack/plugins/alerting/server/rules_client/methods/enable.ts b/x-pack/plugins/alerting/server/rules_client/methods/enable.ts new file mode 100644 index 0000000000000..5b26061120b0a --- /dev/null +++ b/x-pack/plugins/alerting/server/rules_client/methods/enable.ts @@ -0,0 +1,142 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { RawRule, IntervalSchedule } from '../../types'; +import { updateMonitoring, getNextRun } from '../../lib'; +import { WriteOperations, AlertingAuthorizationEntity } from '../../authorization'; +import { retryIfConflicts } from '../../lib/retry_if_conflicts'; +import { ruleAuditEvent, RuleAuditAction } from '../common/audit_events'; +import { RulesClientContext } from '../types'; +import { updateMeta, createNewAPIKeySet, scheduleTask } from '../lib'; + +export async function enable(context: RulesClientContext, { id }: { id: string }): Promise { + return await retryIfConflicts( + context.logger, + `rulesClient.enable('${id}')`, + async () => await enableWithOCC(context, { id }) + ); +} + +async function enableWithOCC(context: RulesClientContext, { id }: { id: string }) { + let existingApiKey: string | null = null; + let attributes: RawRule; + let version: string | undefined; + + try { + const decryptedAlert = + await context.encryptedSavedObjectsClient.getDecryptedAsInternalUser('alert', id, { + namespace: context.namespace, + }); + existingApiKey = decryptedAlert.attributes.apiKey; + attributes = decryptedAlert.attributes; + version = decryptedAlert.version; + } catch (e) { + context.logger.error(`enable(): Failed to load API key of alert ${id}: ${e.message}`); + // Still attempt to load the attributes and version using SOC + const alert = await context.unsecuredSavedObjectsClient.get('alert', id); + attributes = alert.attributes; + version = alert.version; + } + + try { + await context.authorization.ensureAuthorized({ + ruleTypeId: attributes.alertTypeId, + consumer: attributes.consumer, + operation: WriteOperations.Enable, + entity: AlertingAuthorizationEntity.Rule, + }); + + if (attributes.actions.length) { + await context.actionsAuthorization.ensureAuthorized('execute'); + } + } catch (error) { + context.auditLogger?.log( + ruleAuditEvent({ + action: RuleAuditAction.ENABLE, + savedObject: { type: 'alert', id }, + error, + }) + ); + throw error; + } + + context.auditLogger?.log( + ruleAuditEvent({ + action: RuleAuditAction.ENABLE, + outcome: 'unknown', + savedObject: { type: 'alert', id }, + }) + ); + + context.ruleTypeRegistry.ensureRuleTypeEnabled(attributes.alertTypeId); + + if (attributes.enabled === false) { + const username = await context.getUserName(); + const now = new Date(); + + const schedule = attributes.schedule as IntervalSchedule; + + const updateAttributes = updateMeta(context, { + ...attributes, + ...(!existingApiKey && (await createNewAPIKeySet(context, { attributes, username }))), + ...(attributes.monitoring && { + monitoring: updateMonitoring({ + monitoring: attributes.monitoring, + timestamp: now.toISOString(), + duration: 0, + }), + }), + nextRun: getNextRun({ interval: schedule.interval }), + enabled: true, + updatedBy: username, + updatedAt: now.toISOString(), + executionStatus: { + status: 'pending', + lastDuration: 0, + lastExecutionDate: now.toISOString(), + error: null, + warning: null, + }, + }); + + try { + await context.unsecuredSavedObjectsClient.update('alert', id, updateAttributes, { version }); + } catch (e) { + throw e; + } + } + + let scheduledTaskIdToCreate: string | null = null; + if (attributes.scheduledTaskId) { + // If scheduledTaskId defined in rule SO, make sure it exists + try { + await context.taskManager.get(attributes.scheduledTaskId); + } catch (err) { + scheduledTaskIdToCreate = id; + } + } else { + // If scheduledTaskId doesn't exist in rule SO, set it to rule ID + scheduledTaskIdToCreate = id; + } + + if (scheduledTaskIdToCreate) { + // Schedule the task if it doesn't exist + const scheduledTask = await scheduleTask(context, { + id, + consumer: attributes.consumer, + ruleTypeId: attributes.alertTypeId, + schedule: attributes.schedule as IntervalSchedule, + throwOnConflict: false, + }); + await context.unsecuredSavedObjectsClient.update('alert', id, { + scheduledTaskId: scheduledTask.id, + }); + } else { + // Task exists so set enabled to true + await context.taskManager.bulkEnable([attributes.scheduledTaskId!]); + } +} diff --git a/x-pack/plugins/alerting/server/rules_client/methods/find.ts b/x-pack/plugins/alerting/server/rules_client/methods/find.ts new file mode 100644 index 0000000000000..080c72c624cb3 --- /dev/null +++ b/x-pack/plugins/alerting/server/rules_client/methods/find.ts @@ -0,0 +1,179 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import Boom from '@hapi/boom'; +import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import { pick } from 'lodash'; +import { KueryNode, nodeBuilder } from '@kbn/es-query'; +import { RawRule, RuleTypeParams, SanitizedRule } from '../../types'; +import { AlertingAuthorizationEntity } from '../../authorization'; +import { ruleAuditEvent, RuleAuditAction } from '../common/audit_events'; +import { + mapSortField, + validateOperationOnAttributes, + buildKueryNodeFilter, + includeFieldsRequiredForAuthentication, +} from '../common'; +import { + getModifiedField, + getModifiedSearchFields, + getModifiedSearch, + modifyFilterKueryNode, +} from '../common/mapped_params_utils'; +import { alertingAuthorizationFilterOpts } from '../common/constants'; +import { getAlertFromRaw } from '../lib/get_alert_from_raw'; +import type { IndexType, RulesClientContext } from '../types'; + +export interface FindParams { + options?: FindOptions; + excludeFromPublicApi?: boolean; + includeSnoozeData?: boolean; +} + +export interface FindOptions extends IndexType { + perPage?: number; + page?: number; + search?: string; + defaultSearchOperator?: 'AND' | 'OR'; + searchFields?: string[]; + sortField?: string; + sortOrder?: estypes.SortOrder; + hasReference?: { + type: string; + id: string; + }; + fields?: string[]; + filter?: string | KueryNode; +} + +export interface FindResult { + page: number; + perPage: number; + total: number; + data: Array>; +} + +export async function find( + context: RulesClientContext, + { + options: { fields, ...options } = {}, + excludeFromPublicApi = false, + includeSnoozeData = false, + }: FindParams = {} +): Promise> { + let authorizationTuple; + try { + authorizationTuple = await context.authorization.getFindAuthorizationFilter( + AlertingAuthorizationEntity.Rule, + alertingAuthorizationFilterOpts + ); + } catch (error) { + context.auditLogger?.log( + ruleAuditEvent({ + action: RuleAuditAction.FIND, + error, + }) + ); + throw error; + } + + const { filter: authorizationFilter, ensureRuleTypeIsAuthorized } = authorizationTuple; + + const filterKueryNode = buildKueryNodeFilter(options.filter); + let sortField = mapSortField(options.sortField); + if (excludeFromPublicApi) { + try { + validateOperationOnAttributes( + filterKueryNode, + sortField, + options.searchFields, + context.fieldsToExcludeFromPublicApi + ); + } catch (error) { + throw Boom.badRequest(`Error find rules: ${error.message}`); + } + } + + sortField = mapSortField(getModifiedField(options.sortField)); + + // Generate new modified search and search fields, translating certain params properties + // to mapped_params. Thus, allowing for sort/search/filtering on params. + // We do the modifcation after the validate check to make sure the public API does not + // use the mapped_params in their queries. + options = { + ...options, + ...(options.searchFields && { searchFields: getModifiedSearchFields(options.searchFields) }), + ...(options.search && { search: getModifiedSearch(options.searchFields, options.search) }), + }; + + // Modifies kuery node AST to translate params filter and the filter value to mapped_params. + // This translation is done in place, and therefore is not a pure function. + if (filterKueryNode) { + modifyFilterKueryNode({ astFilter: filterKueryNode }); + } + + const { + page, + per_page: perPage, + total, + saved_objects: data, + } = await context.unsecuredSavedObjectsClient.find({ + ...options, + sortField, + filter: + (authorizationFilter && filterKueryNode + ? nodeBuilder.and([filterKueryNode, authorizationFilter as KueryNode]) + : authorizationFilter) ?? filterKueryNode, + fields: fields ? includeFieldsRequiredForAuthentication(fields) : fields, + type: 'alert', + }); + + const authorizedData = data.map(({ id, attributes, references }) => { + try { + ensureRuleTypeIsAuthorized( + attributes.alertTypeId, + attributes.consumer, + AlertingAuthorizationEntity.Rule + ); + } catch (error) { + context.auditLogger?.log( + ruleAuditEvent({ + action: RuleAuditAction.FIND, + savedObject: { type: 'alert', id }, + error, + }) + ); + throw error; + } + return getAlertFromRaw( + context, + id, + attributes.alertTypeId, + fields ? (pick(attributes, fields) as RawRule) : attributes, + references, + false, + excludeFromPublicApi, + includeSnoozeData + ); + }); + + authorizedData.forEach(({ id }) => + context.auditLogger?.log( + ruleAuditEvent({ + action: RuleAuditAction.FIND, + savedObject: { type: 'alert', id }, + }) + ) + ); + + return { + page, + perPage, + total, + data: authorizedData, + }; +} diff --git a/x-pack/plugins/alerting/server/rules_client/methods/get.ts b/x-pack/plugins/alerting/server/rules_client/methods/get.ts new file mode 100644 index 0000000000000..932772f06d209 --- /dev/null +++ b/x-pack/plugins/alerting/server/rules_client/methods/get.ts @@ -0,0 +1,64 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { RawRule, SanitizedRule, RuleTypeParams, SanitizedRuleWithLegacyId } from '../../types'; +import { ReadOperations, AlertingAuthorizationEntity } from '../../authorization'; +import { ruleAuditEvent, RuleAuditAction } from '../common/audit_events'; +import { getAlertFromRaw } from '../lib/get_alert_from_raw'; +import { RulesClientContext } from '../types'; + +export interface GetParams { + id: string; + includeLegacyId?: boolean; + includeSnoozeData?: boolean; + excludeFromPublicApi?: boolean; +} + +export async function get( + context: RulesClientContext, + { + id, + includeLegacyId = false, + includeSnoozeData = false, + excludeFromPublicApi = false, + }: GetParams +): Promise | SanitizedRuleWithLegacyId> { + const result = await context.unsecuredSavedObjectsClient.get('alert', id); + try { + await context.authorization.ensureAuthorized({ + ruleTypeId: result.attributes.alertTypeId, + consumer: result.attributes.consumer, + operation: ReadOperations.Get, + entity: AlertingAuthorizationEntity.Rule, + }); + } catch (error) { + context.auditLogger?.log( + ruleAuditEvent({ + action: RuleAuditAction.GET, + savedObject: { type: 'alert', id }, + error, + }) + ); + throw error; + } + context.auditLogger?.log( + ruleAuditEvent({ + action: RuleAuditAction.GET, + savedObject: { type: 'alert', id }, + }) + ); + return getAlertFromRaw( + context, + result.id, + result.attributes.alertTypeId, + result.attributes, + result.references, + includeLegacyId, + excludeFromPublicApi, + includeSnoozeData + ); +} diff --git a/x-pack/plugins/alerting/server/rules_client/methods/get_action_error_log.ts b/x-pack/plugins/alerting/server/rules_client/methods/get_action_error_log.ts new file mode 100644 index 0000000000000..ebd1862d6b3f6 --- /dev/null +++ b/x-pack/plugins/alerting/server/rules_client/methods/get_action_error_log.ts @@ -0,0 +1,168 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { KueryNode } from '@kbn/es-query'; +import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import { SanitizedRuleWithLegacyId } from '../../types'; +import { convertEsSortToEventLogSort } from '../../lib'; +import { + ReadOperations, + AlertingAuthorizationEntity, + AlertingAuthorizationFilterType, +} from '../../authorization'; +import { ruleAuditEvent, RuleAuditAction } from '../common/audit_events'; +import { IExecutionErrorsResult } from '../../../common'; +import { formatExecutionErrorsResult } from '../../lib/format_execution_log_errors'; +import { parseDate } from '../common'; +import { RulesClientContext } from '../types'; +import { get } from './get'; + +const actionErrorLogDefaultFilter = + 'event.provider:actions AND ((event.action:execute AND (event.outcome:failure OR kibana.alerting.status:warning)) OR (event.action:execute-timeout))'; + +export interface GetActionErrorLogByIdParams { + id: string; + dateStart: string; + dateEnd?: string; + filter?: string; + page: number; + perPage: number; + sort: estypes.Sort; + namespace?: string; +} + +export async function getActionErrorLog( + context: RulesClientContext, + { id, dateStart, dateEnd, filter, page, perPage, sort }: GetActionErrorLogByIdParams +): Promise { + context.logger.debug(`getActionErrorLog(): getting action error logs for rule ${id}`); + const rule = (await get(context, { id, includeLegacyId: true })) as SanitizedRuleWithLegacyId; + + try { + await context.authorization.ensureAuthorized({ + ruleTypeId: rule.alertTypeId, + consumer: rule.consumer, + operation: ReadOperations.GetActionErrorLog, + entity: AlertingAuthorizationEntity.Rule, + }); + } catch (error) { + context.auditLogger?.log( + ruleAuditEvent({ + action: RuleAuditAction.GET_ACTION_ERROR_LOG, + savedObject: { type: 'alert', id }, + error, + }) + ); + throw error; + } + + context.auditLogger?.log( + ruleAuditEvent({ + action: RuleAuditAction.GET_ACTION_ERROR_LOG, + savedObject: { type: 'alert', id }, + }) + ); + + // default duration of instance summary is 60 * rule interval + const dateNow = new Date(); + const parsedDateStart = parseDate(dateStart, 'dateStart', dateNow); + const parsedDateEnd = parseDate(dateEnd, 'dateEnd', dateNow); + + const eventLogClient = await context.getEventLogClient(); + + try { + const errorResult = await eventLogClient.findEventsBySavedObjectIds( + 'alert', + [id], + { + start: parsedDateStart.toISOString(), + end: parsedDateEnd.toISOString(), + page, + per_page: perPage, + filter: filter + ? `(${actionErrorLogDefaultFilter}) AND (${filter})` + : actionErrorLogDefaultFilter, + sort: convertEsSortToEventLogSort(sort), + }, + rule.legacyId !== null ? [rule.legacyId] : undefined + ); + return formatExecutionErrorsResult(errorResult); + } catch (err) { + context.logger.debug( + `rulesClient.getActionErrorLog(): error searching event log for rule ${id}: ${err.message}` + ); + throw err; + } +} + +export async function getActionErrorLogWithAuth( + context: RulesClientContext, + { id, dateStart, dateEnd, filter, page, perPage, sort, namespace }: GetActionErrorLogByIdParams +): Promise { + context.logger.debug(`getActionErrorLogWithAuth(): getting action error logs for rule ${id}`); + + let authorizationTuple; + try { + authorizationTuple = await context.authorization.getFindAuthorizationFilter( + AlertingAuthorizationEntity.Alert, + { + type: AlertingAuthorizationFilterType.KQL, + fieldNames: { + ruleTypeId: 'kibana.alert.rule.rule_type_id', + consumer: 'kibana.alert.rule.consumer', + }, + } + ); + } catch (error) { + context.auditLogger?.log( + ruleAuditEvent({ + action: RuleAuditAction.GET_ACTION_ERROR_LOG, + error, + }) + ); + throw error; + } + + context.auditLogger?.log( + ruleAuditEvent({ + action: RuleAuditAction.GET_ACTION_ERROR_LOG, + savedObject: { type: 'alert', id }, + }) + ); + + // default duration of instance summary is 60 * rule interval + const dateNow = new Date(); + const parsedDateStart = parseDate(dateStart, 'dateStart', dateNow); + const parsedDateEnd = parseDate(dateEnd, 'dateEnd', dateNow); + + const eventLogClient = await context.getEventLogClient(); + + try { + const errorResult = await eventLogClient.findEventsWithAuthFilter( + 'alert', + [id], + authorizationTuple.filter as KueryNode, + namespace, + { + start: parsedDateStart.toISOString(), + end: parsedDateEnd.toISOString(), + page, + per_page: perPage, + filter: filter + ? `(${actionErrorLogDefaultFilter}) AND (${filter})` + : actionErrorLogDefaultFilter, + sort: convertEsSortToEventLogSort(sort), + } + ); + return formatExecutionErrorsResult(errorResult); + } catch (err) { + context.logger.debug( + `rulesClient.getActionErrorLog(): error searching event log for rule ${id}: ${err.message}` + ); + throw err; + } +} diff --git a/x-pack/plugins/alerting/server/rules_client/methods/get_alert_state.ts b/x-pack/plugins/alerting/server/rules_client/methods/get_alert_state.ts new file mode 100644 index 0000000000000..6497428e1c2f2 --- /dev/null +++ b/x-pack/plugins/alerting/server/rules_client/methods/get_alert_state.ts @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { RuleTaskState } from '../../types'; +import { taskInstanceToAlertTaskInstance } from '../../task_runner/alert_task_instance'; +import { ReadOperations, AlertingAuthorizationEntity } from '../../authorization'; +import { RulesClientContext } from '../types'; +import { get } from './get'; + +export interface GetAlertStateParams { + id: string; +} +export async function getAlertState( + context: RulesClientContext, + { id }: GetAlertStateParams +): Promise { + const alert = await get(context, { id }); + await context.authorization.ensureAuthorized({ + ruleTypeId: alert.alertTypeId, + consumer: alert.consumer, + operation: ReadOperations.GetRuleState, + entity: AlertingAuthorizationEntity.Rule, + }); + if (alert.scheduledTaskId) { + const { state } = taskInstanceToAlertTaskInstance( + await context.taskManager.get(alert.scheduledTaskId), + alert + ); + return state; + } +} diff --git a/x-pack/plugins/alerting/server/rules_client/methods/get_alert_summary.ts b/x-pack/plugins/alerting/server/rules_client/methods/get_alert_summary.ts new file mode 100644 index 0000000000000..e841423ad1949 --- /dev/null +++ b/x-pack/plugins/alerting/server/rules_client/methods/get_alert_summary.ts @@ -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 { IEvent } from '@kbn/event-log-plugin/server'; +import { AlertSummary, SanitizedRuleWithLegacyId } from '../../types'; +import { ReadOperations, AlertingAuthorizationEntity } from '../../authorization'; +import { alertSummaryFromEventLog } from '../../lib/alert_summary_from_event_log'; +import { parseDuration } from '../../../common/parse_duration'; +import { parseDate } from '../common'; +import { RulesClientContext } from '../types'; +import { get } from './get'; + +export interface GetAlertSummaryParams { + id: string; + dateStart?: string; + numberOfExecutions?: number; +} + +export async function getAlertSummary( + context: RulesClientContext, + { id, dateStart, numberOfExecutions }: GetAlertSummaryParams +): Promise { + context.logger.debug(`getAlertSummary(): getting alert ${id}`); + const rule = (await get(context, { id, includeLegacyId: true })) as SanitizedRuleWithLegacyId; + + await context.authorization.ensureAuthorized({ + ruleTypeId: rule.alertTypeId, + consumer: rule.consumer, + operation: ReadOperations.GetAlertSummary, + entity: AlertingAuthorizationEntity.Rule, + }); + + const dateNow = new Date(); + const durationMillis = parseDuration(rule.schedule.interval) * (numberOfExecutions ?? 60); + const defaultDateStart = new Date(dateNow.valueOf() - durationMillis); + const parsedDateStart = parseDate(dateStart, 'dateStart', defaultDateStart); + + const eventLogClient = await context.getEventLogClient(); + + context.logger.debug(`getAlertSummary(): search the event log for rule ${id}`); + let events: IEvent[]; + let executionEvents: IEvent[]; + + try { + const [queryResults, executionResults] = await Promise.all([ + eventLogClient.findEventsBySavedObjectIds( + 'alert', + [id], + { + page: 1, + per_page: 10000, + start: parsedDateStart.toISOString(), + sort: [{ sort_field: '@timestamp', sort_order: 'desc' }], + end: dateNow.toISOString(), + }, + rule.legacyId !== null ? [rule.legacyId] : undefined + ), + eventLogClient.findEventsBySavedObjectIds( + 'alert', + [id], + { + page: 1, + per_page: numberOfExecutions ?? 60, + filter: 'event.provider: alerting AND event.action:execute', + sort: [{ sort_field: '@timestamp', sort_order: 'desc' }], + end: dateNow.toISOString(), + }, + rule.legacyId !== null ? [rule.legacyId] : undefined + ), + ]); + events = queryResults.data; + executionEvents = executionResults.data; + } catch (err) { + context.logger.debug( + `rulesClient.getAlertSummary(): error searching event log for rule ${id}: ${err.message}` + ); + events = []; + executionEvents = []; + } + + return alertSummaryFromEventLog({ + rule, + events, + executionEvents, + dateStart: parsedDateStart.toISOString(), + dateEnd: dateNow.toISOString(), + }); +} diff --git a/x-pack/plugins/alerting/server/rules_client/methods/get_execution_kpi.ts b/x-pack/plugins/alerting/server/rules_client/methods/get_execution_kpi.ts new file mode 100644 index 0000000000000..734df53c9cb29 --- /dev/null +++ b/x-pack/plugins/alerting/server/rules_client/methods/get_execution_kpi.ts @@ -0,0 +1,158 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { KueryNode } from '@kbn/es-query'; +import { SanitizedRuleWithLegacyId } from '../../types'; +import { + ReadOperations, + AlertingAuthorizationEntity, + AlertingAuthorizationFilterType, +} from '../../authorization'; +import { ruleAuditEvent, RuleAuditAction } from '../common/audit_events'; +import { + formatExecutionKPIResult, + getExecutionKPIAggregation, +} from '../../lib/get_execution_log_aggregation'; +import { RulesClientContext } from '../types'; +import { parseDate } from '../common'; +import { get } from './get'; + +export interface GetRuleExecutionKPIParams { + id: string; + dateStart: string; + dateEnd?: string; + filter?: string; +} + +export interface GetGlobalExecutionKPIParams { + dateStart: string; + dateEnd?: string; + filter?: string; + namespaces?: Array; +} + +export async function getRuleExecutionKPI( + context: RulesClientContext, + { id, dateStart, dateEnd, filter }: GetRuleExecutionKPIParams +) { + context.logger.debug(`getRuleExecutionKPI(): getting execution KPI for rule ${id}`); + const rule = (await get(context, { id, includeLegacyId: true })) as SanitizedRuleWithLegacyId; + + try { + // Make sure user has access to this rule + await context.authorization.ensureAuthorized({ + ruleTypeId: rule.alertTypeId, + consumer: rule.consumer, + operation: ReadOperations.GetRuleExecutionKPI, + entity: AlertingAuthorizationEntity.Rule, + }); + } catch (error) { + context.auditLogger?.log( + ruleAuditEvent({ + action: RuleAuditAction.GET_RULE_EXECUTION_KPI, + savedObject: { type: 'alert', id }, + error, + }) + ); + throw error; + } + + context.auditLogger?.log( + ruleAuditEvent({ + action: RuleAuditAction.GET_RULE_EXECUTION_KPI, + savedObject: { type: 'alert', id }, + }) + ); + + // default duration of instance summary is 60 * rule interval + const dateNow = new Date(); + const parsedDateStart = parseDate(dateStart, 'dateStart', dateNow); + const parsedDateEnd = parseDate(dateEnd, 'dateEnd', dateNow); + + const eventLogClient = await context.getEventLogClient(); + + try { + const aggResult = await eventLogClient.aggregateEventsBySavedObjectIds( + 'alert', + [id], + { + start: parsedDateStart.toISOString(), + end: parsedDateEnd.toISOString(), + aggs: getExecutionKPIAggregation(filter), + }, + rule.legacyId !== null ? [rule.legacyId] : undefined + ); + + return formatExecutionKPIResult(aggResult); + } catch (err) { + context.logger.debug( + `rulesClient.getRuleExecutionKPI(): error searching execution KPI for rule ${id}: ${err.message}` + ); + throw err; + } +} + +export async function getGlobalExecutionKpiWithAuth( + context: RulesClientContext, + { dateStart, dateEnd, filter, namespaces }: GetGlobalExecutionKPIParams +) { + context.logger.debug(`getGlobalExecutionLogWithAuth(): getting global execution log`); + + let authorizationTuple; + try { + authorizationTuple = await context.authorization.getFindAuthorizationFilter( + AlertingAuthorizationEntity.Alert, + { + type: AlertingAuthorizationFilterType.KQL, + fieldNames: { + ruleTypeId: 'kibana.alert.rule.rule_type_id', + consumer: 'kibana.alert.rule.consumer', + }, + } + ); + } catch (error) { + context.auditLogger?.log( + ruleAuditEvent({ + action: RuleAuditAction.GET_GLOBAL_EXECUTION_KPI, + error, + }) + ); + throw error; + } + + context.auditLogger?.log( + ruleAuditEvent({ + action: RuleAuditAction.GET_GLOBAL_EXECUTION_KPI, + }) + ); + + const dateNow = new Date(); + const parsedDateStart = parseDate(dateStart, 'dateStart', dateNow); + const parsedDateEnd = parseDate(dateEnd, 'dateEnd', dateNow); + + const eventLogClient = await context.getEventLogClient(); + + try { + const aggResult = await eventLogClient.aggregateEventsWithAuthFilter( + 'alert', + authorizationTuple.filter as KueryNode, + { + start: parsedDateStart.toISOString(), + end: parsedDateEnd.toISOString(), + aggs: getExecutionKPIAggregation(filter), + }, + namespaces + ); + + return formatExecutionKPIResult(aggResult); + } catch (err) { + context.logger.debug( + `rulesClient.getGlobalExecutionKpiWithAuth(): error searching global execution KPI: ${err.message}` + ); + throw err; + } +} diff --git a/x-pack/plugins/alerting/server/rules_client/methods/get_execution_log.ts b/x-pack/plugins/alerting/server/rules_client/methods/get_execution_log.ts new file mode 100644 index 0000000000000..006109d71b4b5 --- /dev/null +++ b/x-pack/plugins/alerting/server/rules_client/methods/get_execution_log.ts @@ -0,0 +1,176 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import { KueryNode } from '@kbn/es-query'; +import { SanitizedRuleWithLegacyId } from '../../types'; +import { + ReadOperations, + AlertingAuthorizationEntity, + AlertingAuthorizationFilterType, +} from '../../authorization'; +import { ruleAuditEvent, RuleAuditAction } from '../common/audit_events'; +import { + formatExecutionLogResult, + getExecutionLogAggregation, +} from '../../lib/get_execution_log_aggregation'; +import { IExecutionLogResult } from '../../../common'; +import { parseDate } from '../common'; +import { RulesClientContext } from '../types'; +import { get } from './get'; + +export interface GetExecutionLogByIdParams { + id: string; + dateStart: string; + dateEnd?: string; + filter?: string; + page: number; + perPage: number; + sort: estypes.Sort; +} + +export interface GetGlobalExecutionLogParams { + dateStart: string; + dateEnd?: string; + filter?: string; + page: number; + perPage: number; + sort: estypes.Sort; + namespaces?: Array; +} + +export async function getExecutionLogForRule( + context: RulesClientContext, + { id, dateStart, dateEnd, filter, page, perPage, sort }: GetExecutionLogByIdParams +): Promise { + context.logger.debug(`getExecutionLogForRule(): getting execution log for rule ${id}`); + const rule = (await get(context, { id, includeLegacyId: true })) as SanitizedRuleWithLegacyId; + + try { + // Make sure user has access to this rule + await context.authorization.ensureAuthorized({ + ruleTypeId: rule.alertTypeId, + consumer: rule.consumer, + operation: ReadOperations.GetExecutionLog, + entity: AlertingAuthorizationEntity.Rule, + }); + } catch (error) { + context.auditLogger?.log( + ruleAuditEvent({ + action: RuleAuditAction.GET_EXECUTION_LOG, + savedObject: { type: 'alert', id }, + error, + }) + ); + throw error; + } + + context.auditLogger?.log( + ruleAuditEvent({ + action: RuleAuditAction.GET_EXECUTION_LOG, + savedObject: { type: 'alert', id }, + }) + ); + + // default duration of instance summary is 60 * rule interval + const dateNow = new Date(); + const parsedDateStart = parseDate(dateStart, 'dateStart', dateNow); + const parsedDateEnd = parseDate(dateEnd, 'dateEnd', dateNow); + + const eventLogClient = await context.getEventLogClient(); + + try { + const aggResult = await eventLogClient.aggregateEventsBySavedObjectIds( + 'alert', + [id], + { + start: parsedDateStart.toISOString(), + end: parsedDateEnd.toISOString(), + aggs: getExecutionLogAggregation({ + filter, + page, + perPage, + sort, + }), + }, + rule.legacyId !== null ? [rule.legacyId] : undefined + ); + + return formatExecutionLogResult(aggResult); + } catch (err) { + context.logger.debug( + `rulesClient.getExecutionLogForRule(): error searching event log for rule ${id}: ${err.message}` + ); + throw err; + } +} + +export async function getGlobalExecutionLogWithAuth( + context: RulesClientContext, + { dateStart, dateEnd, filter, page, perPage, sort, namespaces }: GetGlobalExecutionLogParams +): Promise { + context.logger.debug(`getGlobalExecutionLogWithAuth(): getting global execution log`); + + let authorizationTuple; + try { + authorizationTuple = await context.authorization.getFindAuthorizationFilter( + AlertingAuthorizationEntity.Alert, + { + type: AlertingAuthorizationFilterType.KQL, + fieldNames: { + ruleTypeId: 'kibana.alert.rule.rule_type_id', + consumer: 'kibana.alert.rule.consumer', + }, + } + ); + } catch (error) { + context.auditLogger?.log( + ruleAuditEvent({ + action: RuleAuditAction.GET_GLOBAL_EXECUTION_LOG, + error, + }) + ); + throw error; + } + + context.auditLogger?.log( + ruleAuditEvent({ + action: RuleAuditAction.GET_GLOBAL_EXECUTION_LOG, + }) + ); + + const dateNow = new Date(); + const parsedDateStart = parseDate(dateStart, 'dateStart', dateNow); + const parsedDateEnd = parseDate(dateEnd, 'dateEnd', dateNow); + + const eventLogClient = await context.getEventLogClient(); + + try { + const aggResult = await eventLogClient.aggregateEventsWithAuthFilter( + 'alert', + authorizationTuple.filter as KueryNode, + { + start: parsedDateStart.toISOString(), + end: parsedDateEnd.toISOString(), + aggs: getExecutionLogAggregation({ + filter, + page, + perPage, + sort, + }), + }, + namespaces + ); + + return formatExecutionLogResult(aggResult); + } catch (err) { + context.logger.debug( + `rulesClient.getGlobalExecutionLogWithAuth(): error searching global event log: ${err.message}` + ); + throw err; + } +} diff --git a/x-pack/plugins/alerting/server/rules_client/methods/list_alert_types.ts b/x-pack/plugins/alerting/server/rules_client/methods/list_alert_types.ts new file mode 100644 index 0000000000000..eabe15834d6d6 --- /dev/null +++ b/x-pack/plugins/alerting/server/rules_client/methods/list_alert_types.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { WriteOperations, ReadOperations, AlertingAuthorizationEntity } from '../../authorization'; +import { RulesClientContext } from '../types'; + +export async function listAlertTypes(context: RulesClientContext) { + return await context.authorization.filterByRuleTypeAuthorization( + context.ruleTypeRegistry.list(), + [ReadOperations.Get, WriteOperations.Create], + AlertingAuthorizationEntity.Rule + ); +} diff --git a/x-pack/plugins/alerting/server/rules_client/methods/mute_all.ts b/x-pack/plugins/alerting/server/rules_client/methods/mute_all.ts new file mode 100644 index 0000000000000..4ac6ad207fdc7 --- /dev/null +++ b/x-pack/plugins/alerting/server/rules_client/methods/mute_all.ts @@ -0,0 +1,78 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { RawRule } from '../../types'; +import { WriteOperations, AlertingAuthorizationEntity } from '../../authorization'; +import { retryIfConflicts } from '../../lib/retry_if_conflicts'; +import { partiallyUpdateAlert } from '../../saved_objects'; +import { ruleAuditEvent, RuleAuditAction } from '../common/audit_events'; +import { RulesClientContext } from '../types'; +import { updateMeta } from '../lib'; +import { clearUnscheduledSnooze } from '../common'; + +export async function muteAll(context: RulesClientContext, { id }: { id: string }): Promise { + return await retryIfConflicts( + context.logger, + `rulesClient.muteAll('${id}')`, + async () => await muteAllWithOCC(context, { id }) + ); +} + +async function muteAllWithOCC(context: RulesClientContext, { id }: { id: string }) { + const { attributes, version } = await context.unsecuredSavedObjectsClient.get( + 'alert', + id + ); + + try { + await context.authorization.ensureAuthorized({ + ruleTypeId: attributes.alertTypeId, + consumer: attributes.consumer, + operation: WriteOperations.MuteAll, + entity: AlertingAuthorizationEntity.Rule, + }); + + if (attributes.actions.length) { + await context.actionsAuthorization.ensureAuthorized('execute'); + } + } catch (error) { + context.auditLogger?.log( + ruleAuditEvent({ + action: RuleAuditAction.MUTE, + savedObject: { type: 'alert', id }, + error, + }) + ); + throw error; + } + + context.auditLogger?.log( + ruleAuditEvent({ + action: RuleAuditAction.MUTE, + outcome: 'unknown', + savedObject: { type: 'alert', id }, + }) + ); + + context.ruleTypeRegistry.ensureRuleTypeEnabled(attributes.alertTypeId); + + const updateAttributes = updateMeta(context, { + muteAll: true, + mutedInstanceIds: [], + snoozeSchedule: clearUnscheduledSnooze(attributes), + updatedBy: await context.getUserName(), + updatedAt: new Date().toISOString(), + }); + const updateOptions = { version }; + + await partiallyUpdateAlert( + context.unsecuredSavedObjectsClient, + id, + updateAttributes, + updateOptions + ); +} diff --git a/x-pack/plugins/alerting/server/rules_client/methods/mute_instance.ts b/x-pack/plugins/alerting/server/rules_client/methods/mute_instance.ts new file mode 100644 index 0000000000000..67e78b9851945 --- /dev/null +++ b/x-pack/plugins/alerting/server/rules_client/methods/mute_instance.ts @@ -0,0 +1,82 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { Rule } from '../../types'; +import { WriteOperations, AlertingAuthorizationEntity } from '../../authorization'; +import { retryIfConflicts } from '../../lib/retry_if_conflicts'; +import { ruleAuditEvent, RuleAuditAction } from '../common/audit_events'; +import { MuteOptions } from '../types'; +import { RulesClientContext } from '../types'; +import { updateMeta } from '../lib'; + +export async function muteInstance( + context: RulesClientContext, + { alertId, alertInstanceId }: MuteOptions +): Promise { + return await retryIfConflicts( + context.logger, + `rulesClient.muteInstance('${alertId}')`, + async () => await muteInstanceWithOCC(context, { alertId, alertInstanceId }) + ); +} + +async function muteInstanceWithOCC( + context: RulesClientContext, + { alertId, alertInstanceId }: MuteOptions +) { + const { attributes, version } = await context.unsecuredSavedObjectsClient.get( + 'alert', + alertId + ); + + try { + await context.authorization.ensureAuthorized({ + ruleTypeId: attributes.alertTypeId, + consumer: attributes.consumer, + operation: WriteOperations.MuteAlert, + entity: AlertingAuthorizationEntity.Rule, + }); + + if (attributes.actions.length) { + await context.actionsAuthorization.ensureAuthorized('execute'); + } + } catch (error) { + context.auditLogger?.log( + ruleAuditEvent({ + action: RuleAuditAction.MUTE_ALERT, + savedObject: { type: 'alert', id: alertId }, + error, + }) + ); + throw error; + } + + context.auditLogger?.log( + ruleAuditEvent({ + action: RuleAuditAction.MUTE_ALERT, + outcome: 'unknown', + savedObject: { type: 'alert', id: alertId }, + }) + ); + + context.ruleTypeRegistry.ensureRuleTypeEnabled(attributes.alertTypeId); + + const mutedInstanceIds = attributes.mutedInstanceIds || []; + if (!attributes.muteAll && !mutedInstanceIds.includes(alertInstanceId)) { + mutedInstanceIds.push(alertInstanceId); + await context.unsecuredSavedObjectsClient.update( + 'alert', + alertId, + updateMeta(context, { + mutedInstanceIds, + updatedBy: await context.getUserName(), + updatedAt: new Date().toISOString(), + }), + { version } + ); + } +} diff --git a/x-pack/plugins/alerting/server/rules_client/methods/resolve.ts b/x-pack/plugins/alerting/server/rules_client/methods/resolve.ts new file mode 100644 index 0000000000000..539e4c089d36d --- /dev/null +++ b/x-pack/plugins/alerting/server/rules_client/methods/resolve.ts @@ -0,0 +1,65 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { RawRule, RuleTypeParams, ResolvedSanitizedRule } from '../../types'; +import { ReadOperations, AlertingAuthorizationEntity } from '../../authorization'; +import { ruleAuditEvent, RuleAuditAction } from '../common/audit_events'; +import { getAlertFromRaw } from '../lib/get_alert_from_raw'; +import { RulesClientContext } from '../types'; + +export interface ResolveParams { + id: string; + includeLegacyId?: boolean; + includeSnoozeData?: boolean; +} + +export async function resolve( + context: RulesClientContext, + { id, includeLegacyId, includeSnoozeData = false }: ResolveParams +): Promise> { + const { saved_object: result, ...resolveResponse } = + await context.unsecuredSavedObjectsClient.resolve('alert', id); + try { + await context.authorization.ensureAuthorized({ + ruleTypeId: result.attributes.alertTypeId, + consumer: result.attributes.consumer, + operation: ReadOperations.Get, + entity: AlertingAuthorizationEntity.Rule, + }); + } catch (error) { + context.auditLogger?.log( + ruleAuditEvent({ + action: RuleAuditAction.RESOLVE, + savedObject: { type: 'alert', id }, + error, + }) + ); + throw error; + } + context.auditLogger?.log( + ruleAuditEvent({ + action: RuleAuditAction.RESOLVE, + savedObject: { type: 'alert', id }, + }) + ); + + const rule = getAlertFromRaw( + context, + result.id, + result.attributes.alertTypeId, + result.attributes, + result.references, + includeLegacyId, + false, + includeSnoozeData + ); + + return { + ...rule, + ...resolveResponse, + }; +} diff --git a/x-pack/plugins/alerting/server/rules_client/methods/run_soon.ts b/x-pack/plugins/alerting/server/rules_client/methods/run_soon.ts new file mode 100644 index 0000000000000..d683b5fbafe4f --- /dev/null +++ b/x-pack/plugins/alerting/server/rules_client/methods/run_soon.ts @@ -0,0 +1,89 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; +import { ConcreteTaskInstance, TaskStatus } from '@kbn/task-manager-plugin/server'; +import { Rule } from '../../types'; +import { ReadOperations, AlertingAuthorizationEntity } from '../../authorization'; +import { ruleAuditEvent, RuleAuditAction } from '../common/audit_events'; +import { RulesClientContext } from '../types'; + +export async function runSoon(context: RulesClientContext, { id }: { id: string }) { + const { attributes } = await context.unsecuredSavedObjectsClient.get('alert', id); + try { + await context.authorization.ensureAuthorized({ + ruleTypeId: attributes.alertTypeId, + consumer: attributes.consumer, + operation: ReadOperations.RunSoon, + entity: AlertingAuthorizationEntity.Rule, + }); + + if (attributes.actions.length) { + await context.actionsAuthorization.ensureAuthorized('execute'); + } + } catch (error) { + context.auditLogger?.log( + ruleAuditEvent({ + action: RuleAuditAction.RUN_SOON, + savedObject: { type: 'alert', id }, + error, + }) + ); + throw error; + } + + context.auditLogger?.log( + ruleAuditEvent({ + action: RuleAuditAction.RUN_SOON, + outcome: 'unknown', + savedObject: { type: 'alert', id }, + }) + ); + + context.ruleTypeRegistry.ensureRuleTypeEnabled(attributes.alertTypeId); + + // Check that the rule is enabled + if (!attributes.enabled) { + return i18n.translate('xpack.alerting.rulesClient.runSoon.disabledRuleError', { + defaultMessage: 'Error running rule: rule is disabled', + }); + } + + let taskDoc: ConcreteTaskInstance | null = null; + try { + taskDoc = attributes.scheduledTaskId + ? await context.taskManager.get(attributes.scheduledTaskId) + : null; + } catch (err) { + return i18n.translate('xpack.alerting.rulesClient.runSoon.getTaskError', { + defaultMessage: 'Error running rule: {errMessage}', + values: { + errMessage: err.message, + }, + }); + } + + if ( + taskDoc && + (taskDoc.status === TaskStatus.Claiming || taskDoc.status === TaskStatus.Running) + ) { + return i18n.translate('xpack.alerting.rulesClient.runSoon.ruleIsRunning', { + defaultMessage: 'Rule is already running', + }); + } + + try { + await context.taskManager.runSoon(attributes.scheduledTaskId ? attributes.scheduledTaskId : id); + } catch (err) { + return i18n.translate('xpack.alerting.rulesClient.runSoon.runSoonError', { + defaultMessage: 'Error running rule: {errMessage}', + values: { + errMessage: err.message, + }, + }); + } +} diff --git a/x-pack/plugins/alerting/server/rules_client/methods/snooze.ts b/x-pack/plugins/alerting/server/rules_client/methods/snooze.ts new file mode 100644 index 0000000000000..04585bca002b0 --- /dev/null +++ b/x-pack/plugins/alerting/server/rules_client/methods/snooze.ts @@ -0,0 +1,109 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import Boom from '@hapi/boom'; +import { RawRule, RuleSnoozeSchedule } from '../../types'; +import { WriteOperations, AlertingAuthorizationEntity } from '../../authorization'; +import { retryIfConflicts } from '../../lib/retry_if_conflicts'; +import { partiallyUpdateAlert } from '../../saved_objects'; +import { ruleAuditEvent, RuleAuditAction } from '../common/audit_events'; +import { validateSnoozeStartDate } from '../../lib/validate_snooze_date'; +import { RuleMutedError } from '../../lib/errors/rule_muted'; +import { RulesClientContext } from '../types'; +import { getSnoozeAttributes, verifySnoozeScheduleLimit } from '../common'; +import { updateMeta } from '../lib'; + +export interface SnoozeParams { + id: string; + snoozeSchedule: RuleSnoozeSchedule; +} + +export async function snooze( + context: RulesClientContext, + { id, snoozeSchedule }: SnoozeParams +): Promise { + const snoozeDateValidationMsg = validateSnoozeStartDate(snoozeSchedule.rRule.dtstart); + if (snoozeDateValidationMsg) { + throw new RuleMutedError(snoozeDateValidationMsg); + } + + return await retryIfConflicts( + context.logger, + `rulesClient.snooze('${id}', ${JSON.stringify(snoozeSchedule, null, 4)})`, + async () => await snoozeWithOCC(context, { id, snoozeSchedule }) + ); +} + +async function snoozeWithOCC( + context: RulesClientContext, + { + id, + snoozeSchedule, + }: { + id: string; + snoozeSchedule: RuleSnoozeSchedule; + } +) { + const { attributes, version } = await context.unsecuredSavedObjectsClient.get( + 'alert', + id + ); + + try { + await context.authorization.ensureAuthorized({ + ruleTypeId: attributes.alertTypeId, + consumer: attributes.consumer, + operation: WriteOperations.Snooze, + entity: AlertingAuthorizationEntity.Rule, + }); + + if (attributes.actions.length) { + await context.actionsAuthorization.ensureAuthorized('execute'); + } + } catch (error) { + context.auditLogger?.log( + ruleAuditEvent({ + action: RuleAuditAction.SNOOZE, + savedObject: { type: 'alert', id }, + error, + }) + ); + throw error; + } + + context.auditLogger?.log( + ruleAuditEvent({ + action: RuleAuditAction.SNOOZE, + outcome: 'unknown', + savedObject: { type: 'alert', id }, + }) + ); + + context.ruleTypeRegistry.ensureRuleTypeEnabled(attributes.alertTypeId); + + const newAttrs = getSnoozeAttributes(attributes, snoozeSchedule); + + try { + verifySnoozeScheduleLimit(newAttrs); + } catch (error) { + throw Boom.badRequest(error.message); + } + + const updateAttributes = updateMeta(context, { + ...newAttrs, + updatedBy: await context.getUserName(), + updatedAt: new Date().toISOString(), + }); + const updateOptions = { version }; + + await partiallyUpdateAlert( + context.unsecuredSavedObjectsClient, + id, + updateAttributes, + updateOptions + ); +} diff --git a/x-pack/plugins/alerting/server/rules_client/methods/unmute_all.ts b/x-pack/plugins/alerting/server/rules_client/methods/unmute_all.ts new file mode 100644 index 0000000000000..80819de2b6cc2 --- /dev/null +++ b/x-pack/plugins/alerting/server/rules_client/methods/unmute_all.ts @@ -0,0 +1,81 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { RawRule } from '../../types'; +import { WriteOperations, AlertingAuthorizationEntity } from '../../authorization'; +import { retryIfConflicts } from '../../lib/retry_if_conflicts'; +import { partiallyUpdateAlert } from '../../saved_objects'; +import { ruleAuditEvent, RuleAuditAction } from '../common/audit_events'; +import { RulesClientContext } from '../types'; +import { updateMeta } from '../lib'; +import { clearUnscheduledSnooze } from '../common'; + +export async function unmuteAll( + context: RulesClientContext, + { id }: { id: string } +): Promise { + return await retryIfConflicts( + context.logger, + `rulesClient.unmuteAll('${id}')`, + async () => await unmuteAllWithOCC(context, { id }) + ); +} + +async function unmuteAllWithOCC(context: RulesClientContext, { id }: { id: string }) { + const { attributes, version } = await context.unsecuredSavedObjectsClient.get( + 'alert', + id + ); + + try { + await context.authorization.ensureAuthorized({ + ruleTypeId: attributes.alertTypeId, + consumer: attributes.consumer, + operation: WriteOperations.UnmuteAll, + entity: AlertingAuthorizationEntity.Rule, + }); + + if (attributes.actions.length) { + await context.actionsAuthorization.ensureAuthorized('execute'); + } + } catch (error) { + context.auditLogger?.log( + ruleAuditEvent({ + action: RuleAuditAction.UNMUTE, + savedObject: { type: 'alert', id }, + error, + }) + ); + throw error; + } + + context.auditLogger?.log( + ruleAuditEvent({ + action: RuleAuditAction.UNMUTE, + outcome: 'unknown', + savedObject: { type: 'alert', id }, + }) + ); + + context.ruleTypeRegistry.ensureRuleTypeEnabled(attributes.alertTypeId); + + const updateAttributes = updateMeta(context, { + muteAll: false, + mutedInstanceIds: [], + snoozeSchedule: clearUnscheduledSnooze(attributes), + updatedBy: await context.getUserName(), + updatedAt: new Date().toISOString(), + }); + const updateOptions = { version }; + + await partiallyUpdateAlert( + context.unsecuredSavedObjectsClient, + id, + updateAttributes, + updateOptions + ); +} diff --git a/x-pack/plugins/alerting/server/rules_client/methods/unmute_instance.ts b/x-pack/plugins/alerting/server/rules_client/methods/unmute_instance.ts new file mode 100644 index 0000000000000..714e5c0a4f8e4 --- /dev/null +++ b/x-pack/plugins/alerting/server/rules_client/methods/unmute_instance.ts @@ -0,0 +1,86 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { Rule, RawRule } from '../../types'; +import { WriteOperations, AlertingAuthorizationEntity } from '../../authorization'; +import { retryIfConflicts } from '../../lib/retry_if_conflicts'; +import { ruleAuditEvent, RuleAuditAction } from '../common/audit_events'; +import { MuteOptions } from '../types'; +import { RulesClientContext } from '../types'; +import { updateMeta } from '../lib'; + +export async function unmuteInstance( + context: RulesClientContext, + { alertId, alertInstanceId }: MuteOptions +): Promise { + return await retryIfConflicts( + context.logger, + `rulesClient.unmuteInstance('${alertId}')`, + async () => await unmuteInstanceWithOCC(context, { alertId, alertInstanceId }) + ); +} + +async function unmuteInstanceWithOCC( + context: RulesClientContext, + { + alertId, + alertInstanceId, + }: { + alertId: string; + alertInstanceId: string; + } +) { + const { attributes, version } = await context.unsecuredSavedObjectsClient.get( + 'alert', + alertId + ); + + try { + await context.authorization.ensureAuthorized({ + ruleTypeId: attributes.alertTypeId, + consumer: attributes.consumer, + operation: WriteOperations.UnmuteAlert, + entity: AlertingAuthorizationEntity.Rule, + }); + if (attributes.actions.length) { + await context.actionsAuthorization.ensureAuthorized('execute'); + } + } catch (error) { + context.auditLogger?.log( + ruleAuditEvent({ + action: RuleAuditAction.UNMUTE_ALERT, + savedObject: { type: 'alert', id: alertId }, + error, + }) + ); + throw error; + } + + context.auditLogger?.log( + ruleAuditEvent({ + action: RuleAuditAction.UNMUTE_ALERT, + outcome: 'unknown', + savedObject: { type: 'alert', id: alertId }, + }) + ); + + context.ruleTypeRegistry.ensureRuleTypeEnabled(attributes.alertTypeId); + + const mutedInstanceIds = attributes.mutedInstanceIds || []; + if (!attributes.muteAll && mutedInstanceIds.includes(alertInstanceId)) { + await context.unsecuredSavedObjectsClient.update( + 'alert', + alertId, + updateMeta(context, { + updatedBy: await context.getUserName(), + updatedAt: new Date().toISOString(), + mutedInstanceIds: mutedInstanceIds.filter((id: string) => id !== alertInstanceId), + }), + { version } + ); + } +} diff --git a/x-pack/plugins/alerting/server/rules_client/methods/unsnooze.ts b/x-pack/plugins/alerting/server/rules_client/methods/unsnooze.ts new file mode 100644 index 0000000000000..67e8d76e649b4 --- /dev/null +++ b/x-pack/plugins/alerting/server/rules_client/methods/unsnooze.ts @@ -0,0 +1,85 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { RawRule } from '../../types'; +import { WriteOperations, AlertingAuthorizationEntity } from '../../authorization'; +import { retryIfConflicts } from '../../lib/retry_if_conflicts'; +import { partiallyUpdateAlert } from '../../saved_objects'; +import { ruleAuditEvent, RuleAuditAction } from '../common/audit_events'; +import { RulesClientContext } from '../types'; +import { updateMeta } from '../lib'; +import { getUnsnoozeAttributes } from '../common'; + +export interface UnsnoozeParams { + id: string; + scheduleIds?: string[]; +} + +export async function unsnooze( + context: RulesClientContext, + { id, scheduleIds }: UnsnoozeParams +): Promise { + return await retryIfConflicts( + context.logger, + `rulesClient.unsnooze('${id}')`, + async () => await unsnoozeWithOCC(context, { id, scheduleIds }) + ); +} + +async function unsnoozeWithOCC(context: RulesClientContext, { id, scheduleIds }: UnsnoozeParams) { + const { attributes, version } = await context.unsecuredSavedObjectsClient.get( + 'alert', + id + ); + + try { + await context.authorization.ensureAuthorized({ + ruleTypeId: attributes.alertTypeId, + consumer: attributes.consumer, + operation: WriteOperations.Unsnooze, + entity: AlertingAuthorizationEntity.Rule, + }); + + if (attributes.actions.length) { + await context.actionsAuthorization.ensureAuthorized('execute'); + } + } catch (error) { + context.auditLogger?.log( + ruleAuditEvent({ + action: RuleAuditAction.UNSNOOZE, + savedObject: { type: 'alert', id }, + error, + }) + ); + throw error; + } + + context.auditLogger?.log( + ruleAuditEvent({ + action: RuleAuditAction.UNSNOOZE, + outcome: 'unknown', + savedObject: { type: 'alert', id }, + }) + ); + + context.ruleTypeRegistry.ensureRuleTypeEnabled(attributes.alertTypeId); + const newAttrs = getUnsnoozeAttributes(attributes, scheduleIds); + + const updateAttributes = updateMeta(context, { + ...newAttrs, + updatedBy: await context.getUserName(), + updatedAt: new Date().toISOString(), + }); + const updateOptions = { version }; + + await partiallyUpdateAlert( + context.unsecuredSavedObjectsClient, + id, + updateAttributes, + updateOptions + ); +} diff --git a/x-pack/plugins/alerting/server/rules_client/methods/update.ts b/x-pack/plugins/alerting/server/rules_client/methods/update.ts new file mode 100644 index 0000000000000..289f5fe007874 --- /dev/null +++ b/x-pack/plugins/alerting/server/rules_client/methods/update.ts @@ -0,0 +1,240 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import Boom from '@hapi/boom'; +import { isEqual } from 'lodash'; +import { SavedObject } from '@kbn/core/server'; +import { + PartialRule, + RawRule, + RuleTypeParams, + RuleNotifyWhenType, + IntervalSchedule, +} from '../../types'; +import { validateRuleTypeParams, getRuleNotifyWhenType } from '../../lib'; +import { WriteOperations, AlertingAuthorizationEntity } from '../../authorization'; +import { parseDuration } from '../../../common/parse_duration'; +import { retryIfConflicts } from '../../lib/retry_if_conflicts'; +import { bulkMarkApiKeysForInvalidation } from '../../invalidate_pending_api_keys/bulk_mark_api_keys_for_invalidation'; +import { ruleAuditEvent, RuleAuditAction } from '../common/audit_events'; +import { getMappedParams } from '../common/mapped_params_utils'; +import { NormalizedAlertAction, RulesClientContext } from '../types'; +import { validateActions, extractReferences, updateMeta, getPartialRuleFromRaw } from '../lib'; +import { generateAPIKeyName, apiKeyAsAlertAttributes } from '../common'; + +export interface UpdateOptions { + id: string; + data: { + name: string; + tags: string[]; + schedule: IntervalSchedule; + actions: NormalizedAlertAction[]; + params: Params; + throttle?: string | null; + notifyWhen?: RuleNotifyWhenType | null; + }; +} + +export async function update( + context: RulesClientContext, + { id, data }: UpdateOptions +): Promise> { + return await retryIfConflicts( + context.logger, + `rulesClient.update('${id}')`, + async () => await updateWithOCC(context, { id, data }) + ); +} + +async function updateWithOCC( + context: RulesClientContext, + { id, data }: UpdateOptions +): Promise> { + let alertSavedObject: SavedObject; + + try { + alertSavedObject = + await context.encryptedSavedObjectsClient.getDecryptedAsInternalUser('alert', id, { + namespace: context.namespace, + }); + } catch (e) { + // We'll skip invalidating the API key since we failed to load the decrypted saved object + context.logger.error( + `update(): Failed to load API key to invalidate on alert ${id}: ${e.message}` + ); + // Still attempt to load the object using SOC + alertSavedObject = await context.unsecuredSavedObjectsClient.get('alert', id); + } + + try { + await context.authorization.ensureAuthorized({ + ruleTypeId: alertSavedObject.attributes.alertTypeId, + consumer: alertSavedObject.attributes.consumer, + operation: WriteOperations.Update, + entity: AlertingAuthorizationEntity.Rule, + }); + } catch (error) { + context.auditLogger?.log( + ruleAuditEvent({ + action: RuleAuditAction.UPDATE, + savedObject: { type: 'alert', id }, + error, + }) + ); + throw error; + } + + context.auditLogger?.log( + ruleAuditEvent({ + action: RuleAuditAction.UPDATE, + outcome: 'unknown', + savedObject: { type: 'alert', id }, + }) + ); + + context.ruleTypeRegistry.ensureRuleTypeEnabled(alertSavedObject.attributes.alertTypeId); + + const updateResult = await updateAlert(context, { id, data }, alertSavedObject); + + await Promise.all([ + alertSavedObject.attributes.apiKey + ? bulkMarkApiKeysForInvalidation( + { apiKeys: [alertSavedObject.attributes.apiKey] }, + context.logger, + context.unsecuredSavedObjectsClient + ) + : null, + (async () => { + if ( + updateResult.scheduledTaskId && + updateResult.schedule && + !isEqual(alertSavedObject.attributes.schedule, updateResult.schedule) + ) { + try { + const { tasks } = await context.taskManager.bulkUpdateSchedules( + [updateResult.scheduledTaskId], + updateResult.schedule + ); + + context.logger.debug( + `Rule update has rescheduled the underlying task: ${updateResult.scheduledTaskId} to run at: ${tasks?.[0]?.runAt}` + ); + } catch (err) { + context.logger.error( + `Rule update failed to run its underlying task. TaskManager bulkUpdateSchedules failed with Error: ${err.message}` + ); + } + } + })(), + ]); + + return updateResult; +} + +async function updateAlert( + context: RulesClientContext, + { id, data }: UpdateOptions, + { attributes, version }: SavedObject +): Promise> { + const ruleType = context.ruleTypeRegistry.get(attributes.alertTypeId); + + // Validate + const validatedAlertTypeParams = validateRuleTypeParams(data.params, ruleType.validate?.params); + await validateActions(context, ruleType, data); + + // Throw error if schedule interval is less than the minimum and we are enforcing it + const intervalInMs = parseDuration(data.schedule.interval); + if ( + intervalInMs < context.minimumScheduleIntervalInMs && + context.minimumScheduleInterval.enforce + ) { + throw Boom.badRequest( + `Error updating rule: the interval is less than the allowed minimum interval of ${context.minimumScheduleInterval.value}` + ); + } + + // Extract saved object references for this rule + const { + references, + params: updatedParams, + actions, + } = await extractReferences(context, ruleType, data.actions, validatedAlertTypeParams); + + const username = await context.getUserName(); + + let createdAPIKey = null; + try { + createdAPIKey = attributes.enabled + ? await context.createAPIKey(generateAPIKeyName(ruleType.id, data.name)) + : null; + } catch (error) { + throw Boom.badRequest(`Error updating rule: could not create API key - ${error.message}`); + } + + const apiKeyAttributes = apiKeyAsAlertAttributes(createdAPIKey, username); + const notifyWhen = getRuleNotifyWhenType(data.notifyWhen ?? null, data.throttle ?? null); + + let updatedObject: SavedObject; + const createAttributes = updateMeta(context, { + ...attributes, + ...data, + ...apiKeyAttributes, + params: updatedParams as RawRule['params'], + actions, + notifyWhen, + updatedBy: username, + updatedAt: new Date().toISOString(), + }); + + const mappedParams = getMappedParams(updatedParams); + + if (Object.keys(mappedParams).length) { + createAttributes.mapped_params = mappedParams; + } + + try { + updatedObject = await context.unsecuredSavedObjectsClient.create( + 'alert', + createAttributes, + { + id, + overwrite: true, + version, + references, + } + ); + } catch (e) { + // Avoid unused API key + await bulkMarkApiKeysForInvalidation( + { apiKeys: createAttributes.apiKey ? [createAttributes.apiKey] : [] }, + context.logger, + context.unsecuredSavedObjectsClient + ); + + throw e; + } + + // Log warning if schedule interval is less than the minimum but we're not enforcing it + if ( + intervalInMs < context.minimumScheduleIntervalInMs && + !context.minimumScheduleInterval.enforce + ) { + context.logger.warn( + `Rule schedule interval (${data.schedule.interval}) for "${ruleType.id}" rule type with ID "${id}" is less than the minimum value (${context.minimumScheduleInterval.value}). Running rules at this interval may impact alerting performance. Set "xpack.alerting.rules.minimumScheduleInterval.enforce" to true to prevent such changes.` + ); + } + + return getPartialRuleFromRaw( + context, + id, + ruleType, + updatedObject.attributes, + updatedObject.references, + false, + true + ); +} diff --git a/x-pack/plugins/alerting/server/rules_client/methods/update_api_key.ts b/x-pack/plugins/alerting/server/rules_client/methods/update_api_key.ts new file mode 100644 index 0000000000000..abb7d32404d57 --- /dev/null +++ b/x-pack/plugins/alerting/server/rules_client/methods/update_api_key.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 Boom from '@hapi/boom'; +import { RawRule } from '../../types'; +import { WriteOperations, AlertingAuthorizationEntity } from '../../authorization'; +import { retryIfConflicts } from '../../lib/retry_if_conflicts'; +import { bulkMarkApiKeysForInvalidation } from '../../invalidate_pending_api_keys/bulk_mark_api_keys_for_invalidation'; +import { ruleAuditEvent, RuleAuditAction } from '../common/audit_events'; +import { generateAPIKeyName, apiKeyAsAlertAttributes } from '../common'; +import { updateMeta } from '../lib'; +import { RulesClientContext } from '../types'; + +export async function updateApiKey( + context: RulesClientContext, + { id }: { id: string } +): Promise { + return await retryIfConflicts( + context.logger, + `rulesClient.updateApiKey('${id}')`, + async () => await updateApiKeyWithOCC(context, { id }) + ); +} + +async function updateApiKeyWithOCC(context: RulesClientContext, { id }: { id: string }) { + let apiKeyToInvalidate: string | null = null; + let attributes: RawRule; + let version: string | undefined; + + try { + const decryptedAlert = + await context.encryptedSavedObjectsClient.getDecryptedAsInternalUser('alert', id, { + namespace: context.namespace, + }); + apiKeyToInvalidate = decryptedAlert.attributes.apiKey; + attributes = decryptedAlert.attributes; + version = decryptedAlert.version; + } catch (e) { + // We'll skip invalidating the API key since we failed to load the decrypted saved object + context.logger.error( + `updateApiKey(): Failed to load API key to invalidate on alert ${id}: ${e.message}` + ); + // Still attempt to load the attributes and version using SOC + const alert = await context.unsecuredSavedObjectsClient.get('alert', id); + attributes = alert.attributes; + version = alert.version; + } + + try { + await context.authorization.ensureAuthorized({ + ruleTypeId: attributes.alertTypeId, + consumer: attributes.consumer, + operation: WriteOperations.UpdateApiKey, + entity: AlertingAuthorizationEntity.Rule, + }); + if (attributes.actions.length) { + await context.actionsAuthorization.ensureAuthorized('execute'); + } + } catch (error) { + context.auditLogger?.log( + ruleAuditEvent({ + action: RuleAuditAction.UPDATE_API_KEY, + savedObject: { type: 'alert', id }, + error, + }) + ); + throw error; + } + + const username = await context.getUserName(); + + let createdAPIKey = null; + try { + createdAPIKey = await context.createAPIKey( + generateAPIKeyName(attributes.alertTypeId, attributes.name) + ); + } catch (error) { + throw Boom.badRequest( + `Error updating API key for rule: could not create API key - ${error.message}` + ); + } + + const updateAttributes = updateMeta(context, { + ...attributes, + ...apiKeyAsAlertAttributes(createdAPIKey, username), + updatedAt: new Date().toISOString(), + updatedBy: username, + }); + + context.auditLogger?.log( + ruleAuditEvent({ + action: RuleAuditAction.UPDATE_API_KEY, + outcome: 'unknown', + savedObject: { type: 'alert', id }, + }) + ); + + context.ruleTypeRegistry.ensureRuleTypeEnabled(attributes.alertTypeId); + + try { + await context.unsecuredSavedObjectsClient.update('alert', id, updateAttributes, { version }); + } catch (e) { + // Avoid unused API key + await bulkMarkApiKeysForInvalidation( + { apiKeys: updateAttributes.apiKey ? [updateAttributes.apiKey] : [] }, + context.logger, + context.unsecuredSavedObjectsClient + ); + throw e; + } + + if (apiKeyToInvalidate) { + await bulkMarkApiKeysForInvalidation( + { apiKeys: [apiKeyToInvalidate] }, + context.logger, + context.unsecuredSavedObjectsClient + ); + } +} diff --git a/x-pack/plugins/alerting/server/rules_client/rules_client.ts b/x-pack/plugins/alerting/server/rules_client/rules_client.ts index fa0bee7d9a338..d790ec3587d77 100644 --- a/x-pack/plugins/alerting/server/rules_client/rules_client.ts +++ b/x-pack/plugins/alerting/server/rules_client/rules_client.ts @@ -5,4511 +5,134 @@ * 2.0. */ -import Semver from 'semver'; -import pMap from 'p-map'; -import Boom from '@hapi/boom'; -import { - omit, - isEqual, - map, - uniq, - pick, - truncate, - trim, - mapValues, - cloneDeep, - isEmpty, -} from 'lodash'; -import { i18n } from '@kbn/i18n'; -import { AlertConsumers } from '@kbn/rule-data-utils'; -import { KueryNode, nodeBuilder } from '@kbn/es-query'; -import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; -import { - Logger, - SavedObjectsClientContract, - SavedObjectReference, - SavedObject, - PluginInitializerContext, - SavedObjectsUtils, - SavedObjectAttributes, - SavedObjectsBulkUpdateObject, - SavedObjectsBulkDeleteObject, - SavedObjectsUpdateResponse, -} from '@kbn/core/server'; -import { ActionsClient, ActionsAuthorization } from '@kbn/actions-plugin/server'; -import { - GrantAPIKeyResult as SecurityPluginGrantAPIKeyResult, - InvalidateAPIKeyResult as SecurityPluginInvalidateAPIKeyResult, -} from '@kbn/security-plugin/server'; -import { EncryptedSavedObjectsClient } from '@kbn/encrypted-saved-objects-plugin/server'; -import { - ConcreteTaskInstance, - TaskManagerStartContract, - TaskStatus, -} from '@kbn/task-manager-plugin/server'; -import { - IEvent, - IEventLogClient, - IEventLogger, - SAVED_OBJECT_REL_PRIMARY, -} from '@kbn/event-log-plugin/server'; -import { AuditLogger } from '@kbn/security-plugin/server'; -import { - Rule, - PartialRule, - RawRule, - RuleTypeRegistry, - RuleAction, - IntervalSchedule, - SanitizedRule, - RuleTaskState, - AlertSummary, - RuleExecutionStatusValues, - RuleLastRunOutcomeValues, - RuleNotifyWhenType, - RuleTypeParams, - ResolvedSanitizedRule, - RuleWithLegacyId, - SanitizedRuleWithLegacyId, - PartialRuleWithLegacyId, - RuleSnooze, - RuleSnoozeSchedule, - RawAlertInstance as RawAlert, -} from '../types'; -import { - validateRuleTypeParams, - ruleExecutionStatusFromRaw, - getRuleNotifyWhenType, - validateMutatedRuleTypeParams, - convertRuleIdsToKueryNode, - getRuleSnoozeEndTime, - convertEsSortToEventLogSort, - getDefaultMonitoring, - updateMonitoring, - convertMonitoringFromRawAndVerify, - getNextRun, -} from '../lib'; -import { taskInstanceToAlertTaskInstance } from '../task_runner/alert_task_instance'; -import { RegistryRuleType, UntypedNormalizedRuleType } from '../rule_type_registry'; -import { - AlertingAuthorization, - WriteOperations, - ReadOperations, - AlertingAuthorizationEntity, - AlertingAuthorizationFilterType, - AlertingAuthorizationFilterOpts, -} from '../authorization'; -import { parseIsoOrRelativeDate } from '../lib/iso_or_relative_date'; -import { alertSummaryFromEventLog } from '../lib/alert_summary_from_event_log'; +import { SanitizedRule, RuleTypeParams } from '../types'; import { parseDuration } from '../../common/parse_duration'; -import { retryIfConflicts } from '../lib/retry_if_conflicts'; -import { partiallyUpdateAlert } from '../saved_objects'; -import { bulkMarkApiKeysForInvalidation } from '../invalidate_pending_api_keys/bulk_mark_api_keys_for_invalidation'; -import { ruleAuditEvent, RuleAuditAction } from './audit_events'; +import { RulesClientContext, BulkOptions, MuteOptions } from './types'; + +import { clone, CloneArguments } from './methods/clone'; +import { create, CreateOptions } from './methods/create'; +import { get, GetParams } from './methods/get'; +import { resolve, ResolveParams } from './methods/resolve'; +import { getAlertState, GetAlertStateParams } from './methods/get_alert_state'; +import { getAlertSummary, GetAlertSummaryParams } from './methods/get_alert_summary'; import { - mapSortField, - validateOperationOnAttributes, - retryIfBulkEditConflicts, - retryIfBulkDeleteConflicts, - retryIfBulkDisableConflicts, - retryIfBulkOperationConflicts, - applyBulkEditOperation, - buildKueryNodeFilter, -} from './lib'; -import { getRuleExecutionStatusPending } from '../lib/rule_execution_status'; -import { Alert } from '../alert'; -import { EVENT_LOG_ACTIONS } from '../plugin'; -import { createAlertEventLogRecordObject } from '../lib/create_alert_event_log_record_object'; + GetExecutionLogByIdParams, + getExecutionLogForRule, + GetGlobalExecutionLogParams, + getGlobalExecutionLogWithAuth, +} from './methods/get_execution_log'; import { - getMappedParams, - getModifiedField, - getModifiedSearchFields, - getModifiedSearch, - modifyFilterKueryNode, -} from './lib/mapped_params_utils'; -import { AlertingRulesConfig } from '../config'; + getActionErrorLog, + GetActionErrorLogByIdParams, + getActionErrorLogWithAuth, +} from './methods/get_action_error_log'; import { - formatExecutionLogResult, - formatExecutionKPIResult, - getExecutionLogAggregation, - getExecutionKPIAggregation, -} from '../lib/get_execution_log_aggregation'; -import { IExecutionLogResult, IExecutionErrorsResult } from '../../common'; -import { validateSnoozeStartDate } from '../lib/validate_snooze_date'; -import { RuleMutedError } from '../lib/errors/rule_muted'; -import { formatExecutionErrorsResult } from '../lib/format_execution_log_errors'; -import { getActiveScheduledSnoozes } from '../lib/is_rule_snoozed'; -import { isSnoozeExpired } from '../lib'; -import { isDetectionEngineAADRuleType } from '../saved_objects/migrations/utils'; - -export interface RegistryAlertTypeWithAuth extends RegistryRuleType { - authorizedConsumers: string[]; -} -type NormalizedAlertAction = Omit; -export type CreateAPIKeyResult = - | { apiKeysEnabled: false } - | { apiKeysEnabled: true; result: SecurityPluginGrantAPIKeyResult }; -export type InvalidateAPIKeyResult = - | { apiKeysEnabled: false } - | { apiKeysEnabled: true; result: SecurityPluginInvalidateAPIKeyResult }; - -export interface RuleAggregation { - status: { - buckets: Array<{ - key: string; - doc_count: number; - }>; - }; - outcome: { - buckets: Array<{ - key: string; - doc_count: number; - }>; - }; - muted: { - buckets: Array<{ - key: number; - key_as_string: string; - doc_count: number; - }>; - }; - enabled: { - buckets: Array<{ - key: number; - key_as_string: string; - doc_count: number; - }>; - }; - snoozed: { - count: { - doc_count: number; - }; - }; - tags: { - buckets: Array<{ - key: string; - doc_count: number; - }>; - }; -} - -export interface RuleBulkOperationAggregation { - alertTypeId: { - buckets: Array<{ - key: string[]; - doc_count: number; - }>; - }; -} - -export interface ConstructorOptions { - logger: Logger; - taskManager: TaskManagerStartContract; - unsecuredSavedObjectsClient: SavedObjectsClientContract; - authorization: AlertingAuthorization; - actionsAuthorization: ActionsAuthorization; - ruleTypeRegistry: RuleTypeRegistry; - minimumScheduleInterval: AlertingRulesConfig['minimumScheduleInterval']; - encryptedSavedObjectsClient: EncryptedSavedObjectsClient; - spaceId: string; - namespace?: string; - getUserName: () => Promise; - createAPIKey: (name: string) => Promise; - getActionsClient: () => Promise; - getEventLogClient: () => Promise; - kibanaVersion: PluginInitializerContext['env']['packageInfo']['version']; - auditLogger?: AuditLogger; - eventLogger?: IEventLogger; -} - -export interface MuteOptions extends IndexType { - alertId: string; - alertInstanceId: string; -} - -export interface SnoozeOptions extends IndexType { - snoozeSchedule: RuleSnoozeSchedule; -} - -export interface FindOptions extends IndexType { - perPage?: number; - page?: number; - search?: string; - defaultSearchOperator?: 'AND' | 'OR'; - searchFields?: string[]; - sortField?: string; - sortOrder?: estypes.SortOrder; - hasReference?: { - type: string; - id: string; - }; - fields?: string[]; - filter?: string | KueryNode; -} - -export type BulkEditFields = keyof Pick< - Rule, - 'actions' | 'tags' | 'schedule' | 'throttle' | 'notifyWhen' | 'snoozeSchedule' | 'apiKey' + GetGlobalExecutionKPIParams, + getGlobalExecutionKpiWithAuth, + getRuleExecutionKPI, + GetRuleExecutionKPIParams, +} from './methods/get_execution_kpi'; +import { find, FindParams } from './methods/find'; +import { aggregate, AggregateOptions } from './methods/aggregate'; +import { deleteRule } from './methods/delete'; +import { update, UpdateOptions } from './methods/update'; +import { bulkDeleteRules } from './methods/bulk_delete'; +import { bulkEdit, BulkEditOptions } from './methods/bulk_edit'; +import { bulkEnableRules } from './methods/bulk_enable'; +import { bulkDisableRules } from './methods/bulk_disable'; +import { updateApiKey } from './methods/update_api_key'; +import { enable } from './methods/enable'; +import { disable } from './methods/disable'; +import { snooze, SnoozeParams } from './methods/snooze'; +import { unsnooze, UnsnoozeParams } from './methods/unsnooze'; +import { clearExpiredSnoozes } from './methods/clear_expired_snoozes'; +import { muteAll } from './methods/mute_all'; +import { unmuteAll } from './methods/unmute_all'; +import { muteInstance } from './methods/mute_instance'; +import { unmuteInstance } from './methods/unmute_instance'; +import { runSoon } from './methods/run_soon'; +import { listAlertTypes } from './methods/list_alert_types'; + +export type ConstructorOptions = Omit< + RulesClientContext, + 'fieldsToExcludeFromPublicApi' | 'minimumScheduleIntervalInMs' >; -export type BulkEditOperation = - | { - operation: 'add' | 'delete' | 'set'; - field: Extract; - value: string[]; - } - | { - operation: 'add' | 'set'; - field: Extract; - value: NormalizedAlertAction[]; - } - | { - operation: 'set'; - field: Extract; - value: Rule['schedule']; - } - | { - operation: 'set'; - field: Extract; - value: Rule['throttle']; - } - | { - operation: 'set'; - field: Extract; - value: Rule['notifyWhen']; - } - | { - operation: 'set'; - field: Extract; - value: RuleSnoozeSchedule; - } - | { - operation: 'delete'; - field: Extract; - value?: string[]; - } - | { - operation: 'set'; - field: Extract; - value?: undefined; - }; - -type RuleParamsModifier = (params: Params) => Promise; - -export interface BulkEditOptionsFilter { - filter?: string | KueryNode; - operations: BulkEditOperation[]; - paramsModifier?: RuleParamsModifier; -} - -export interface BulkEditOptionsIds { - ids: string[]; - operations: BulkEditOperation[]; - paramsModifier?: RuleParamsModifier; -} - -export type BulkEditOptions = - | BulkEditOptionsFilter - | BulkEditOptionsIds; - -interface BulkOptionsFilter { - filter?: string | KueryNode; -} - -interface BulkOptionsIds { - ids?: string[]; -} - -export type BulkOptions = BulkOptionsFilter | BulkOptionsIds; - -export interface BulkOperationError { - message: string; - status?: number; - rule: { - id: string; - name: string; - }; -} - -export interface AggregateOptions extends IndexType { - search?: string; - defaultSearchOperator?: 'AND' | 'OR'; - searchFields?: string[]; - hasReference?: { - type: string; - id: string; - }; - filter?: string | KueryNode; -} - -interface IndexType { - [key: string]: unknown; -} - -export interface AggregateResult { - alertExecutionStatus: { [status: string]: number }; - ruleLastRunOutcome: { [status: string]: number }; - ruleEnabledStatus?: { enabled: number; disabled: number }; - ruleMutedStatus?: { muted: number; unmuted: number }; - ruleSnoozedStatus?: { snoozed: number }; - ruleTags?: string[]; -} - -export interface FindResult { - page: number; - perPage: number; - total: number; - data: Array>; -} - -interface SavedObjectOptions { - id?: string; - migrationVersion?: Record; -} - -export interface CreateOptions { - data: Omit< - Rule, - | 'id' - | 'createdBy' - | 'updatedBy' - | 'createdAt' - | 'updatedAt' - | 'apiKey' - | 'apiKeyOwner' - | 'muteAll' - | 'mutedInstanceIds' - | 'actions' - | 'executionStatus' - | 'snoozeSchedule' - | 'isSnoozedUntil' - | 'lastRun' - | 'nextRun' - > & { actions: NormalizedAlertAction[] }; - options?: SavedObjectOptions; -} - -export interface UpdateOptions { - id: string; - data: { - name: string; - tags: string[]; - schedule: IntervalSchedule; - actions: NormalizedAlertAction[]; - params: Params; - throttle?: string | null; - notifyWhen?: RuleNotifyWhenType | null; - }; -} - -export interface GetAlertSummaryParams { - id: string; - dateStart?: string; - numberOfExecutions?: number; -} - -export interface GetExecutionLogByIdParams { - id: string; - dateStart: string; - dateEnd?: string; - filter?: string; - page: number; - perPage: number; - sort: estypes.Sort; -} - -export interface GetRuleExecutionKPIParams { - id: string; - dateStart: string; - dateEnd?: string; - filter?: string; -} - -export interface GetGlobalExecutionKPIParams { - dateStart: string; - dateEnd?: string; - filter?: string; - namespaces?: Array; -} - -export interface GetGlobalExecutionLogParams { - dateStart: string; - dateEnd?: string; - filter?: string; - page: number; - perPage: number; - sort: estypes.Sort; - namespaces?: Array; -} - -export interface GetActionErrorLogByIdParams { - id: string; - dateStart: string; - dateEnd?: string; - filter?: string; - page: number; - perPage: number; - sort: estypes.Sort; - namespace?: string; -} - -interface ScheduleTaskOptions { - id: string; - consumer: string; - ruleTypeId: string; - schedule: IntervalSchedule; - throwOnConflict: boolean; // whether to throw conflict errors or swallow them -} - -type BulkAction = 'DELETE' | 'ENABLE' | 'DISABLE'; - -// NOTE: Changing this prefix will require a migration to update the prefix in all existing `rule` saved objects -const extractedSavedObjectParamReferenceNamePrefix = 'param:'; +const fieldsToExcludeFromPublicApi: Array = [ + 'monitoring', + 'mapped_params', + 'snoozeSchedule', + 'activeSnoozes', +]; -// NOTE: Changing this prefix will require a migration to update the prefix in all existing `rule` saved objects -const preconfiguredConnectorActionRefPrefix = 'preconfigured:'; - -const MAX_RULES_NUMBER_FOR_BULK_OPERATION = 10000; -const API_KEY_GENERATE_CONCURRENCY = 50; -const RULE_TYPE_CHECKS_CONCURRENCY = 50; - -const actionErrorLogDefaultFilter = - 'event.provider:actions AND ((event.action:execute AND (event.outcome:failure OR kibana.alerting.status:warning)) OR (event.action:execute-timeout))'; - -const alertingAuthorizationFilterOpts: AlertingAuthorizationFilterOpts = { - type: AlertingAuthorizationFilterType.KQL, - fieldNames: { ruleTypeId: 'alert.attributes.alertTypeId', consumer: 'alert.attributes.consumer' }, -}; export class RulesClient { - private readonly logger: Logger; - private readonly getUserName: () => Promise; - private readonly spaceId: string; - private readonly namespace?: string; - private readonly taskManager: TaskManagerStartContract; - private readonly unsecuredSavedObjectsClient: SavedObjectsClientContract; - private readonly authorization: AlertingAuthorization; - private readonly ruleTypeRegistry: RuleTypeRegistry; - private readonly minimumScheduleInterval: AlertingRulesConfig['minimumScheduleInterval']; - private readonly minimumScheduleIntervalInMs: number; - private readonly createAPIKey: (name: string) => Promise; - private readonly getActionsClient: () => Promise; - private readonly actionsAuthorization: ActionsAuthorization; - private readonly getEventLogClient: () => Promise; - private readonly encryptedSavedObjectsClient: EncryptedSavedObjectsClient; - private readonly kibanaVersion!: PluginInitializerContext['env']['packageInfo']['version']; - private readonly auditLogger?: AuditLogger; - private readonly eventLogger?: IEventLogger; - private readonly fieldsToExcludeFromPublicApi: Array = [ - 'monitoring', - 'mapped_params', - 'snoozeSchedule', - 'activeSnoozes', - ]; - - constructor({ - ruleTypeRegistry, - minimumScheduleInterval, - unsecuredSavedObjectsClient, - authorization, - taskManager, - logger, - spaceId, - namespace, - getUserName, - createAPIKey, - encryptedSavedObjectsClient, - getActionsClient, - actionsAuthorization, - getEventLogClient, - kibanaVersion, - auditLogger, - eventLogger, - }: ConstructorOptions) { - this.logger = logger; - this.getUserName = getUserName; - this.spaceId = spaceId; - this.namespace = namespace; - this.taskManager = taskManager; - this.ruleTypeRegistry = ruleTypeRegistry; - this.minimumScheduleInterval = minimumScheduleInterval; - this.minimumScheduleIntervalInMs = parseDuration(minimumScheduleInterval.value); - this.unsecuredSavedObjectsClient = unsecuredSavedObjectsClient; - this.authorization = authorization; - this.createAPIKey = createAPIKey; - this.encryptedSavedObjectsClient = encryptedSavedObjectsClient; - this.getActionsClient = getActionsClient; - this.actionsAuthorization = actionsAuthorization; - this.getEventLogClient = getEventLogClient; - this.kibanaVersion = kibanaVersion; - this.auditLogger = auditLogger; - this.eventLogger = eventLogger; - } - - public async clone( - id: string, - { newId }: { newId?: string } - ): Promise> { - let ruleSavedObject: SavedObject; - - try { - ruleSavedObject = await this.encryptedSavedObjectsClient.getDecryptedAsInternalUser( - 'alert', - id, - { - namespace: this.namespace, - } - ); - } catch (e) { - // We'll skip invalidating the API key since we failed to load the decrypted saved object - this.logger.error( - `update(): Failed to load API key to invalidate on alert ${id}: ${e.message}` - ); - // Still attempt to load the object using SOC - ruleSavedObject = await this.unsecuredSavedObjectsClient.get('alert', id); - } - - /* - * As the time of the creation of this PR, security solution already have a clone/duplicate API - * with some specific business logic so to avoid weird bugs, I prefer to exclude them from this - * functionality until we resolve our difference - */ - if ( - isDetectionEngineAADRuleType(ruleSavedObject) || - ruleSavedObject.attributes.consumer === AlertConsumers.SIEM - ) { - throw Boom.badRequest( - 'The clone functionality is not enable for rule who belongs to security solution' - ); - } - const ruleName = - ruleSavedObject.attributes.name.indexOf('[Clone]') > 0 - ? ruleSavedObject.attributes.name - : `${ruleSavedObject.attributes.name} [Clone]`; - const ruleId = newId ?? SavedObjectsUtils.generateId(); - try { - await this.authorization.ensureAuthorized({ - ruleTypeId: ruleSavedObject.attributes.alertTypeId, - consumer: ruleSavedObject.attributes.consumer, - operation: WriteOperations.Create, - entity: AlertingAuthorizationEntity.Rule, - }); - } catch (error) { - this.auditLogger?.log( - ruleAuditEvent({ - action: RuleAuditAction.CREATE, - savedObject: { type: 'alert', id }, - error, - }) - ); - throw error; - } - - this.ruleTypeRegistry.ensureRuleTypeEnabled(ruleSavedObject.attributes.alertTypeId); - // Throws an error if alert type isn't registered - const ruleType = this.ruleTypeRegistry.get(ruleSavedObject.attributes.alertTypeId); - const username = await this.getUserName(); - const createTime = Date.now(); - const lastRunTimestamp = new Date(); - const legacyId = Semver.lt(this.kibanaVersion, '8.0.0') ? id : null; - let createdAPIKey = null; - try { - createdAPIKey = ruleSavedObject.attributes.enabled - ? await this.createAPIKey(this.generateAPIKeyName(ruleType.id, ruleName)) - : null; - } catch (error) { - throw Boom.badRequest(`Error creating rule: could not create API key - ${error.message}`); - } - const rawRule: RawRule = { - ...ruleSavedObject.attributes, - name: ruleName, - ...this.apiKeyAsAlertAttributes(createdAPIKey, username), - legacyId, - createdBy: username, - updatedBy: username, - createdAt: new Date(createTime).toISOString(), - updatedAt: new Date(createTime).toISOString(), - snoozeSchedule: [], - muteAll: false, - mutedInstanceIds: [], - executionStatus: getRuleExecutionStatusPending(lastRunTimestamp.toISOString()), - monitoring: getDefaultMonitoring(lastRunTimestamp.toISOString()), - }; - - this.auditLogger?.log( - ruleAuditEvent({ - action: RuleAuditAction.CREATE, - outcome: 'unknown', - savedObject: { type: 'alert', id }, - }) - ); - - return await this.createRuleSavedObject({ - intervalInMs: parseDuration(rawRule.schedule.interval), - rawRule, - references: ruleSavedObject.references, - ruleId, - }); - } - - public async create({ - data, - options, - }: CreateOptions): Promise> { - const id = options?.id || SavedObjectsUtils.generateId(); - - try { - await this.authorization.ensureAuthorized({ - ruleTypeId: data.alertTypeId, - consumer: data.consumer, - operation: WriteOperations.Create, - entity: AlertingAuthorizationEntity.Rule, - }); - } catch (error) { - this.auditLogger?.log( - ruleAuditEvent({ - action: RuleAuditAction.CREATE, - savedObject: { type: 'alert', id }, - error, - }) - ); - throw error; - } - - this.ruleTypeRegistry.ensureRuleTypeEnabled(data.alertTypeId); - - // Throws an error if alert type isn't registered - const ruleType = this.ruleTypeRegistry.get(data.alertTypeId); - - const validatedAlertTypeParams = validateRuleTypeParams(data.params, ruleType.validate?.params); - const username = await this.getUserName(); - - let createdAPIKey = null; - try { - createdAPIKey = data.enabled - ? await this.createAPIKey(this.generateAPIKeyName(ruleType.id, data.name)) - : null; - } catch (error) { - throw Boom.badRequest(`Error creating rule: could not create API key - ${error.message}`); - } - - await this.validateActions(ruleType, data); - - // Throw error if schedule interval is less than the minimum and we are enforcing it - const intervalInMs = parseDuration(data.schedule.interval); - if (intervalInMs < this.minimumScheduleIntervalInMs && this.minimumScheduleInterval.enforce) { - throw Boom.badRequest( - `Error creating rule: the interval is less than the allowed minimum interval of ${this.minimumScheduleInterval.value}` - ); - } - - // Extract saved object references for this rule - const { - references, - params: updatedParams, - actions, - } = await this.extractReferences(ruleType, data.actions, validatedAlertTypeParams); - - const createTime = Date.now(); - const lastRunTimestamp = new Date(); - const legacyId = Semver.lt(this.kibanaVersion, '8.0.0') ? id : null; - const notifyWhen = getRuleNotifyWhenType(data.notifyWhen ?? null, data.throttle ?? null); - const throttle = data.throttle ?? null; - - const rawRule: RawRule = { - ...data, - ...this.apiKeyAsAlertAttributes(createdAPIKey, username), - legacyId, - actions, - createdBy: username, - updatedBy: username, - createdAt: new Date(createTime).toISOString(), - updatedAt: new Date(createTime).toISOString(), - snoozeSchedule: [], - params: updatedParams as RawRule['params'], - muteAll: false, - mutedInstanceIds: [], - notifyWhen, - throttle, - executionStatus: getRuleExecutionStatusPending(lastRunTimestamp.toISOString()), - monitoring: getDefaultMonitoring(lastRunTimestamp.toISOString()), - }; - - const mappedParams = getMappedParams(updatedParams); - - if (Object.keys(mappedParams).length) { - rawRule.mapped_params = mappedParams; - } - - return await this.createRuleSavedObject({ - intervalInMs, - rawRule, - references, - ruleId: id, - options, - }); - } - - private async createRuleSavedObject({ - intervalInMs, - rawRule, - references, - ruleId, - options, - }: { - intervalInMs: number; - rawRule: RawRule; - references: SavedObjectReference[]; - ruleId: string; - options?: SavedObjectOptions; - }) { - this.auditLogger?.log( - ruleAuditEvent({ - action: RuleAuditAction.CREATE, - outcome: 'unknown', - savedObject: { type: 'alert', id: ruleId }, - }) - ); - - let createdAlert: SavedObject; - try { - createdAlert = await this.unsecuredSavedObjectsClient.create( - 'alert', - this.updateMeta(rawRule), - { - ...options, - references, - id: ruleId, - } - ); - } catch (e) { - // Avoid unused API key - await bulkMarkApiKeysForInvalidation( - { apiKeys: rawRule.apiKey ? [rawRule.apiKey] : [] }, - this.logger, - this.unsecuredSavedObjectsClient - ); - - throw e; - } - if (rawRule.enabled) { - let scheduledTask; - try { - scheduledTask = await this.scheduleTask({ - id: createdAlert.id, - consumer: rawRule.consumer, - ruleTypeId: rawRule.alertTypeId, - schedule: rawRule.schedule, - throwOnConflict: true, - }); - } catch (e) { - // Cleanup data, something went wrong scheduling the task - try { - await this.unsecuredSavedObjectsClient.delete('alert', createdAlert.id); - } catch (err) { - // Skip the cleanup error and throw the task manager error to avoid confusion - this.logger.error( - `Failed to cleanup alert "${createdAlert.id}" after scheduling task failed. Error: ${err.message}` - ); - } - throw e; - } - await this.unsecuredSavedObjectsClient.update('alert', createdAlert.id, { - scheduledTaskId: scheduledTask.id, - }); - createdAlert.attributes.scheduledTaskId = scheduledTask.id; - } - - // Log warning if schedule interval is less than the minimum but we're not enforcing it - if (intervalInMs < this.minimumScheduleIntervalInMs && !this.minimumScheduleInterval.enforce) { - this.logger.warn( - `Rule schedule interval (${rawRule.schedule.interval}) for "${createdAlert.attributes.alertTypeId}" rule type with ID "${createdAlert.id}" is less than the minimum value (${this.minimumScheduleInterval.value}). Running rules at this interval may impact alerting performance. Set "xpack.alerting.rules.minimumScheduleInterval.enforce" to true to prevent creation of these rules.` - ); - } - - return this.getAlertFromRaw( - createdAlert.id, - createdAlert.attributes.alertTypeId, - createdAlert.attributes, - references, - false, - true - ); - } - - public async get({ - id, - includeLegacyId = false, - includeSnoozeData = false, - excludeFromPublicApi = false, - }: { - id: string; - includeLegacyId?: boolean; - includeSnoozeData?: boolean; - excludeFromPublicApi?: boolean; - }): Promise | SanitizedRuleWithLegacyId> { - const result = await this.unsecuredSavedObjectsClient.get('alert', id); - try { - await this.authorization.ensureAuthorized({ - ruleTypeId: result.attributes.alertTypeId, - consumer: result.attributes.consumer, - operation: ReadOperations.Get, - entity: AlertingAuthorizationEntity.Rule, - }); - } catch (error) { - this.auditLogger?.log( - ruleAuditEvent({ - action: RuleAuditAction.GET, - savedObject: { type: 'alert', id }, - error, - }) - ); - throw error; - } - this.auditLogger?.log( - ruleAuditEvent({ - action: RuleAuditAction.GET, - savedObject: { type: 'alert', id }, - }) - ); - return this.getAlertFromRaw( - result.id, - result.attributes.alertTypeId, - result.attributes, - result.references, - includeLegacyId, - excludeFromPublicApi, - includeSnoozeData - ); - } - - public async resolve({ - id, - includeLegacyId, - includeSnoozeData = false, - }: { - id: string; - includeLegacyId?: boolean; - includeSnoozeData?: boolean; - }): Promise> { - const { saved_object: result, ...resolveResponse } = - await this.unsecuredSavedObjectsClient.resolve('alert', id); - try { - await this.authorization.ensureAuthorized({ - ruleTypeId: result.attributes.alertTypeId, - consumer: result.attributes.consumer, - operation: ReadOperations.Get, - entity: AlertingAuthorizationEntity.Rule, - }); - } catch (error) { - this.auditLogger?.log( - ruleAuditEvent({ - action: RuleAuditAction.RESOLVE, - savedObject: { type: 'alert', id }, - error, - }) - ); - throw error; - } - this.auditLogger?.log( - ruleAuditEvent({ - action: RuleAuditAction.RESOLVE, - savedObject: { type: 'alert', id }, - }) - ); - - const rule = this.getAlertFromRaw( - result.id, - result.attributes.alertTypeId, - result.attributes, - result.references, - includeLegacyId, - false, - includeSnoozeData - ); - - return { - ...rule, - ...resolveResponse, - }; - } - - public async getAlertState({ id }: { id: string }): Promise { - const alert = await this.get({ id }); - await this.authorization.ensureAuthorized({ - ruleTypeId: alert.alertTypeId, - consumer: alert.consumer, - operation: ReadOperations.GetRuleState, - entity: AlertingAuthorizationEntity.Rule, - }); - if (alert.scheduledTaskId) { - const { state } = taskInstanceToAlertTaskInstance( - await this.taskManager.get(alert.scheduledTaskId), - alert - ); - return state; - } - } - - public async getAlertSummary({ - id, - dateStart, - numberOfExecutions, - }: GetAlertSummaryParams): Promise { - this.logger.debug(`getAlertSummary(): getting alert ${id}`); - const rule = (await this.get({ id, includeLegacyId: true })) as SanitizedRuleWithLegacyId; - - await this.authorization.ensureAuthorized({ - ruleTypeId: rule.alertTypeId, - consumer: rule.consumer, - operation: ReadOperations.GetAlertSummary, - entity: AlertingAuthorizationEntity.Rule, - }); - - const dateNow = new Date(); - const durationMillis = parseDuration(rule.schedule.interval) * (numberOfExecutions ?? 60); - const defaultDateStart = new Date(dateNow.valueOf() - durationMillis); - const parsedDateStart = parseDate(dateStart, 'dateStart', defaultDateStart); - - const eventLogClient = await this.getEventLogClient(); - - this.logger.debug(`getAlertSummary(): search the event log for rule ${id}`); - let events: IEvent[]; - let executionEvents: IEvent[]; - - try { - const [queryResults, executionResults] = await Promise.all([ - eventLogClient.findEventsBySavedObjectIds( - 'alert', - [id], - { - page: 1, - per_page: 10000, - start: parsedDateStart.toISOString(), - sort: [{ sort_field: '@timestamp', sort_order: 'desc' }], - end: dateNow.toISOString(), - }, - rule.legacyId !== null ? [rule.legacyId] : undefined - ), - eventLogClient.findEventsBySavedObjectIds( - 'alert', - [id], - { - page: 1, - per_page: numberOfExecutions ?? 60, - filter: 'event.provider: alerting AND event.action:execute', - sort: [{ sort_field: '@timestamp', sort_order: 'desc' }], - end: dateNow.toISOString(), - }, - rule.legacyId !== null ? [rule.legacyId] : undefined - ), - ]); - events = queryResults.data; - executionEvents = executionResults.data; - } catch (err) { - this.logger.debug( - `rulesClient.getAlertSummary(): error searching event log for rule ${id}: ${err.message}` - ); - events = []; - executionEvents = []; - } - - return alertSummaryFromEventLog({ - rule, - events, - executionEvents, - dateStart: parsedDateStart.toISOString(), - dateEnd: dateNow.toISOString(), - }); - } - - public async getExecutionLogForRule({ - id, - dateStart, - dateEnd, - filter, - page, - perPage, - sort, - }: GetExecutionLogByIdParams): Promise { - this.logger.debug(`getExecutionLogForRule(): getting execution log for rule ${id}`); - const rule = (await this.get({ id, includeLegacyId: true })) as SanitizedRuleWithLegacyId; - - try { - // Make sure user has access to this rule - await this.authorization.ensureAuthorized({ - ruleTypeId: rule.alertTypeId, - consumer: rule.consumer, - operation: ReadOperations.GetExecutionLog, - entity: AlertingAuthorizationEntity.Rule, - }); - } catch (error) { - this.auditLogger?.log( - ruleAuditEvent({ - action: RuleAuditAction.GET_EXECUTION_LOG, - savedObject: { type: 'alert', id }, - error, - }) - ); - throw error; - } - - this.auditLogger?.log( - ruleAuditEvent({ - action: RuleAuditAction.GET_EXECUTION_LOG, - savedObject: { type: 'alert', id }, - }) - ); - - // default duration of instance summary is 60 * rule interval - const dateNow = new Date(); - const parsedDateStart = parseDate(dateStart, 'dateStart', dateNow); - const parsedDateEnd = parseDate(dateEnd, 'dateEnd', dateNow); - - const eventLogClient = await this.getEventLogClient(); - - try { - const aggResult = await eventLogClient.aggregateEventsBySavedObjectIds( - 'alert', - [id], - { - start: parsedDateStart.toISOString(), - end: parsedDateEnd.toISOString(), - aggs: getExecutionLogAggregation({ - filter, - page, - perPage, - sort, - }), - }, - rule.legacyId !== null ? [rule.legacyId] : undefined - ); - - return formatExecutionLogResult(aggResult); - } catch (err) { - this.logger.debug( - `rulesClient.getExecutionLogForRule(): error searching event log for rule ${id}: ${err.message}` - ); - throw err; - } - } - - public async getGlobalExecutionLogWithAuth({ - dateStart, - dateEnd, - filter, - page, - perPage, - sort, - namespaces, - }: GetGlobalExecutionLogParams): Promise { - this.logger.debug(`getGlobalExecutionLogWithAuth(): getting global execution log`); - - let authorizationTuple; - try { - authorizationTuple = await this.authorization.getFindAuthorizationFilter( - AlertingAuthorizationEntity.Alert, - { - type: AlertingAuthorizationFilterType.KQL, - fieldNames: { - ruleTypeId: 'kibana.alert.rule.rule_type_id', - consumer: 'kibana.alert.rule.consumer', - }, - } - ); - } catch (error) { - this.auditLogger?.log( - ruleAuditEvent({ - action: RuleAuditAction.GET_GLOBAL_EXECUTION_LOG, - error, - }) - ); - throw error; - } - - this.auditLogger?.log( - ruleAuditEvent({ - action: RuleAuditAction.GET_GLOBAL_EXECUTION_LOG, - }) - ); - - const dateNow = new Date(); - const parsedDateStart = parseDate(dateStart, 'dateStart', dateNow); - const parsedDateEnd = parseDate(dateEnd, 'dateEnd', dateNow); - - const eventLogClient = await this.getEventLogClient(); - - try { - const aggResult = await eventLogClient.aggregateEventsWithAuthFilter( - 'alert', - authorizationTuple.filter as KueryNode, - { - start: parsedDateStart.toISOString(), - end: parsedDateEnd.toISOString(), - aggs: getExecutionLogAggregation({ - filter, - page, - perPage, - sort, - }), - }, - namespaces - ); - - return formatExecutionLogResult(aggResult); - } catch (err) { - this.logger.debug( - `rulesClient.getGlobalExecutionLogWithAuth(): error searching global event log: ${err.message}` - ); - throw err; - } - } - - public async getActionErrorLog({ - id, - dateStart, - dateEnd, - filter, - page, - perPage, - sort, - }: GetActionErrorLogByIdParams): Promise { - this.logger.debug(`getActionErrorLog(): getting action error logs for rule ${id}`); - const rule = (await this.get({ id, includeLegacyId: true })) as SanitizedRuleWithLegacyId; - - try { - await this.authorization.ensureAuthorized({ - ruleTypeId: rule.alertTypeId, - consumer: rule.consumer, - operation: ReadOperations.GetActionErrorLog, - entity: AlertingAuthorizationEntity.Rule, - }); - } catch (error) { - this.auditLogger?.log( - ruleAuditEvent({ - action: RuleAuditAction.GET_ACTION_ERROR_LOG, - savedObject: { type: 'alert', id }, - error, - }) - ); - throw error; - } - - this.auditLogger?.log( - ruleAuditEvent({ - action: RuleAuditAction.GET_ACTION_ERROR_LOG, - savedObject: { type: 'alert', id }, - }) - ); - - // default duration of instance summary is 60 * rule interval - const dateNow = new Date(); - const parsedDateStart = parseDate(dateStart, 'dateStart', dateNow); - const parsedDateEnd = parseDate(dateEnd, 'dateEnd', dateNow); - - const eventLogClient = await this.getEventLogClient(); - - try { - const errorResult = await eventLogClient.findEventsBySavedObjectIds( - 'alert', - [id], - { - start: parsedDateStart.toISOString(), - end: parsedDateEnd.toISOString(), - page, - per_page: perPage, - filter: filter - ? `(${actionErrorLogDefaultFilter}) AND (${filter})` - : actionErrorLogDefaultFilter, - sort: convertEsSortToEventLogSort(sort), - }, - rule.legacyId !== null ? [rule.legacyId] : undefined - ); - return formatExecutionErrorsResult(errorResult); - } catch (err) { - this.logger.debug( - `rulesClient.getActionErrorLog(): error searching event log for rule ${id}: ${err.message}` - ); - throw err; - } - } - - public async getActionErrorLogWithAuth({ - id, - dateStart, - dateEnd, - filter, - page, - perPage, - sort, - namespace, - }: GetActionErrorLogByIdParams): Promise { - this.logger.debug(`getActionErrorLogWithAuth(): getting action error logs for rule ${id}`); - - let authorizationTuple; - try { - authorizationTuple = await this.authorization.getFindAuthorizationFilter( - AlertingAuthorizationEntity.Alert, - { - type: AlertingAuthorizationFilterType.KQL, - fieldNames: { - ruleTypeId: 'kibana.alert.rule.rule_type_id', - consumer: 'kibana.alert.rule.consumer', - }, - } - ); - } catch (error) { - this.auditLogger?.log( - ruleAuditEvent({ - action: RuleAuditAction.GET_ACTION_ERROR_LOG, - error, - }) - ); - throw error; - } - - this.auditLogger?.log( - ruleAuditEvent({ - action: RuleAuditAction.GET_ACTION_ERROR_LOG, - savedObject: { type: 'alert', id }, - }) - ); - - // default duration of instance summary is 60 * rule interval - const dateNow = new Date(); - const parsedDateStart = parseDate(dateStart, 'dateStart', dateNow); - const parsedDateEnd = parseDate(dateEnd, 'dateEnd', dateNow); - - const eventLogClient = await this.getEventLogClient(); - - try { - const errorResult = await eventLogClient.findEventsWithAuthFilter( - 'alert', - [id], - authorizationTuple.filter as KueryNode, - namespace, - { - start: parsedDateStart.toISOString(), - end: parsedDateEnd.toISOString(), - page, - per_page: perPage, - filter: filter - ? `(${actionErrorLogDefaultFilter}) AND (${filter})` - : actionErrorLogDefaultFilter, - sort: convertEsSortToEventLogSort(sort), - } - ); - return formatExecutionErrorsResult(errorResult); - } catch (err) { - this.logger.debug( - `rulesClient.getActionErrorLog(): error searching event log for rule ${id}: ${err.message}` - ); - throw err; - } - } - - public async getGlobalExecutionKpiWithAuth({ - dateStart, - dateEnd, - filter, - namespaces, - }: GetGlobalExecutionKPIParams) { - this.logger.debug(`getGlobalExecutionLogWithAuth(): getting global execution log`); - - let authorizationTuple; - try { - authorizationTuple = await this.authorization.getFindAuthorizationFilter( - AlertingAuthorizationEntity.Alert, - { - type: AlertingAuthorizationFilterType.KQL, - fieldNames: { - ruleTypeId: 'kibana.alert.rule.rule_type_id', - consumer: 'kibana.alert.rule.consumer', - }, - } - ); - } catch (error) { - this.auditLogger?.log( - ruleAuditEvent({ - action: RuleAuditAction.GET_GLOBAL_EXECUTION_KPI, - error, - }) - ); - throw error; - } - - this.auditLogger?.log( - ruleAuditEvent({ - action: RuleAuditAction.GET_GLOBAL_EXECUTION_KPI, - }) - ); - - const dateNow = new Date(); - const parsedDateStart = parseDate(dateStart, 'dateStart', dateNow); - const parsedDateEnd = parseDate(dateEnd, 'dateEnd', dateNow); - - const eventLogClient = await this.getEventLogClient(); - - try { - const aggResult = await eventLogClient.aggregateEventsWithAuthFilter( - 'alert', - authorizationTuple.filter as KueryNode, - { - start: parsedDateStart.toISOString(), - end: parsedDateEnd.toISOString(), - aggs: getExecutionKPIAggregation(filter), - }, - namespaces - ); - - return formatExecutionKPIResult(aggResult); - } catch (err) { - this.logger.debug( - `rulesClient.getGlobalExecutionKpiWithAuth(): error searching global execution KPI: ${err.message}` - ); - throw err; - } - } - - public async getRuleExecutionKPI({ id, dateStart, dateEnd, filter }: GetRuleExecutionKPIParams) { - this.logger.debug(`getRuleExecutionKPI(): getting execution KPI for rule ${id}`); - const rule = (await this.get({ id, includeLegacyId: true })) as SanitizedRuleWithLegacyId; - - try { - // Make sure user has access to this rule - await this.authorization.ensureAuthorized({ - ruleTypeId: rule.alertTypeId, - consumer: rule.consumer, - operation: ReadOperations.GetRuleExecutionKPI, - entity: AlertingAuthorizationEntity.Rule, - }); - } catch (error) { - this.auditLogger?.log( - ruleAuditEvent({ - action: RuleAuditAction.GET_RULE_EXECUTION_KPI, - savedObject: { type: 'alert', id }, - error, - }) - ); - throw error; - } - - this.auditLogger?.log( - ruleAuditEvent({ - action: RuleAuditAction.GET_RULE_EXECUTION_KPI, - savedObject: { type: 'alert', id }, - }) - ); - - // default duration of instance summary is 60 * rule interval - const dateNow = new Date(); - const parsedDateStart = parseDate(dateStart, 'dateStart', dateNow); - const parsedDateEnd = parseDate(dateEnd, 'dateEnd', dateNow); - - const eventLogClient = await this.getEventLogClient(); - - try { - const aggResult = await eventLogClient.aggregateEventsBySavedObjectIds( - 'alert', - [id], - { - start: parsedDateStart.toISOString(), - end: parsedDateEnd.toISOString(), - aggs: getExecutionKPIAggregation(filter), - }, - rule.legacyId !== null ? [rule.legacyId] : undefined - ); - - return formatExecutionKPIResult(aggResult); - } catch (err) { - this.logger.debug( - `rulesClient.getRuleExecutionKPI(): error searching execution KPI for rule ${id}: ${err.message}` - ); - throw err; - } - } - - public async find({ - options: { fields, ...options } = {}, - excludeFromPublicApi = false, - includeSnoozeData = false, - }: { - options?: FindOptions; - excludeFromPublicApi?: boolean; - includeSnoozeData?: boolean; - } = {}): Promise> { - let authorizationTuple; - try { - authorizationTuple = await this.authorization.getFindAuthorizationFilter( - AlertingAuthorizationEntity.Rule, - alertingAuthorizationFilterOpts - ); - } catch (error) { - this.auditLogger?.log( - ruleAuditEvent({ - action: RuleAuditAction.FIND, - error, - }) - ); - throw error; - } - - const { filter: authorizationFilter, ensureRuleTypeIsAuthorized } = authorizationTuple; - - const filterKueryNode = buildKueryNodeFilter(options.filter); - let sortField = mapSortField(options.sortField); - if (excludeFromPublicApi) { - try { - validateOperationOnAttributes( - filterKueryNode, - sortField, - options.searchFields, - this.fieldsToExcludeFromPublicApi - ); - } catch (error) { - throw Boom.badRequest(`Error find rules: ${error.message}`); - } - } - - sortField = mapSortField(getModifiedField(options.sortField)); - - // Generate new modified search and search fields, translating certain params properties - // to mapped_params. Thus, allowing for sort/search/filtering on params. - // We do the modifcation after the validate check to make sure the public API does not - // use the mapped_params in their queries. - options = { - ...options, - ...(options.searchFields && { searchFields: getModifiedSearchFields(options.searchFields) }), - ...(options.search && { search: getModifiedSearch(options.searchFields, options.search) }), - }; - - // Modifies kuery node AST to translate params filter and the filter value to mapped_params. - // This translation is done in place, and therefore is not a pure function. - if (filterKueryNode) { - modifyFilterKueryNode({ astFilter: filterKueryNode }); - } - - const { - page, - per_page: perPage, - total, - saved_objects: data, - } = await this.unsecuredSavedObjectsClient.find({ - ...options, - sortField, - filter: - (authorizationFilter && filterKueryNode - ? nodeBuilder.and([filterKueryNode, authorizationFilter as KueryNode]) - : authorizationFilter) ?? filterKueryNode, - fields: fields ? this.includeFieldsRequiredForAuthentication(fields) : fields, - type: 'alert', - }); - - const authorizedData = data.map(({ id, attributes, references }) => { - try { - ensureRuleTypeIsAuthorized( - attributes.alertTypeId, - attributes.consumer, - AlertingAuthorizationEntity.Rule - ); - } catch (error) { - this.auditLogger?.log( - ruleAuditEvent({ - action: RuleAuditAction.FIND, - savedObject: { type: 'alert', id }, - error, - }) - ); - throw error; - } - return this.getAlertFromRaw( - id, - attributes.alertTypeId, - fields ? (pick(attributes, fields) as RawRule) : attributes, - references, - false, - excludeFromPublicApi, - includeSnoozeData - ); - }); - - authorizedData.forEach(({ id }) => - this.auditLogger?.log( - ruleAuditEvent({ - action: RuleAuditAction.FIND, - savedObject: { type: 'alert', id }, - }) - ) - ); - - return { - page, - perPage, - total, - data: authorizedData, - }; - } - - public async aggregate({ - options: { fields, filter, ...options } = {}, - }: { options?: AggregateOptions } = {}): Promise { - let authorizationTuple; - try { - authorizationTuple = await this.authorization.getFindAuthorizationFilter( - AlertingAuthorizationEntity.Rule, - alertingAuthorizationFilterOpts - ); - } catch (error) { - this.auditLogger?.log( - ruleAuditEvent({ - action: RuleAuditAction.AGGREGATE, - error, - }) - ); - throw error; - } - - const { filter: authorizationFilter } = authorizationTuple; - const filterKueryNode = buildKueryNodeFilter(filter); - - const resp = await this.unsecuredSavedObjectsClient.find({ - ...options, - filter: - authorizationFilter && filterKueryNode - ? nodeBuilder.and([filterKueryNode, authorizationFilter as KueryNode]) - : authorizationFilter, - page: 1, - perPage: 0, - type: 'alert', - aggs: { - status: { - terms: { field: 'alert.attributes.executionStatus.status' }, - }, - outcome: { - terms: { field: 'alert.attributes.lastRun.outcome' }, - }, - enabled: { - terms: { field: 'alert.attributes.enabled' }, - }, - muted: { - terms: { field: 'alert.attributes.muteAll' }, - }, - tags: { - terms: { field: 'alert.attributes.tags', order: { _key: 'asc' }, size: 50 }, - }, - snoozed: { - nested: { - path: 'alert.attributes.snoozeSchedule', - }, - aggs: { - count: { - filter: { - exists: { - field: 'alert.attributes.snoozeSchedule.duration', - }, - }, - }, - }, - }, - }, - }); - - if (!resp.aggregations) { - // Return a placeholder with all zeroes - const placeholder: AggregateResult = { - alertExecutionStatus: {}, - ruleLastRunOutcome: {}, - ruleEnabledStatus: { - enabled: 0, - disabled: 0, - }, - ruleMutedStatus: { - muted: 0, - unmuted: 0, - }, - ruleSnoozedStatus: { snoozed: 0 }, - }; - - for (const key of RuleExecutionStatusValues) { - placeholder.alertExecutionStatus[key] = 0; - } - - return placeholder; - } - - const alertExecutionStatus = resp.aggregations.status.buckets.map( - ({ key, doc_count: docCount }) => ({ - [key]: docCount, - }) - ); - - const ruleLastRunOutcome = resp.aggregations.outcome.buckets.map( - ({ key, doc_count: docCount }) => ({ - [key]: docCount, - }) - ); - - const ret: AggregateResult = { - alertExecutionStatus: alertExecutionStatus.reduce( - (acc, curr: { [status: string]: number }) => Object.assign(acc, curr), - {} - ), - ruleLastRunOutcome: ruleLastRunOutcome.reduce( - (acc, curr: { [status: string]: number }) => Object.assign(acc, curr), - {} - ), - }; - - // Fill missing keys with zeroes - for (const key of RuleExecutionStatusValues) { - if (!ret.alertExecutionStatus.hasOwnProperty(key)) { - ret.alertExecutionStatus[key] = 0; - } - } - for (const key of RuleLastRunOutcomeValues) { - if (!ret.ruleLastRunOutcome.hasOwnProperty(key)) { - ret.ruleLastRunOutcome[key] = 0; - } - } - - const enabledBuckets = resp.aggregations.enabled.buckets; - ret.ruleEnabledStatus = { - enabled: enabledBuckets.find((bucket) => bucket.key === 1)?.doc_count ?? 0, - disabled: enabledBuckets.find((bucket) => bucket.key === 0)?.doc_count ?? 0, - }; - - const mutedBuckets = resp.aggregations.muted.buckets; - ret.ruleMutedStatus = { - muted: mutedBuckets.find((bucket) => bucket.key === 1)?.doc_count ?? 0, - unmuted: mutedBuckets.find((bucket) => bucket.key === 0)?.doc_count ?? 0, - }; - - ret.ruleSnoozedStatus = { - snoozed: resp.aggregations.snoozed?.count?.doc_count ?? 0, - }; - - const tagsBuckets = resp.aggregations.tags?.buckets || []; - ret.ruleTags = tagsBuckets.map((bucket) => bucket.key); - - return ret; - } - - public async delete({ id }: { id: string }) { - return await retryIfConflicts( - this.logger, - `rulesClient.delete('${id}')`, - async () => await this.deleteWithOCC({ id }) - ); - } - - private async deleteWithOCC({ id }: { id: string }) { - let taskIdToRemove: string | undefined | null; - let apiKeyToInvalidate: string | null = null; - let attributes: RawRule; - - try { - const decryptedAlert = - await this.encryptedSavedObjectsClient.getDecryptedAsInternalUser('alert', id, { - namespace: this.namespace, - }); - apiKeyToInvalidate = decryptedAlert.attributes.apiKey; - taskIdToRemove = decryptedAlert.attributes.scheduledTaskId; - attributes = decryptedAlert.attributes; - } catch (e) { - // We'll skip invalidating the API key since we failed to load the decrypted saved object - this.logger.error( - `delete(): Failed to load API key to invalidate on alert ${id}: ${e.message}` - ); - // Still attempt to load the scheduledTaskId using SOC - const alert = await this.unsecuredSavedObjectsClient.get('alert', id); - taskIdToRemove = alert.attributes.scheduledTaskId; - attributes = alert.attributes; - } - - try { - await this.authorization.ensureAuthorized({ - ruleTypeId: attributes.alertTypeId, - consumer: attributes.consumer, - operation: WriteOperations.Delete, - entity: AlertingAuthorizationEntity.Rule, - }); - } catch (error) { - this.auditLogger?.log( - ruleAuditEvent({ - action: RuleAuditAction.DELETE, - savedObject: { type: 'alert', id }, - error, - }) - ); - throw error; - } - - this.auditLogger?.log( - ruleAuditEvent({ - action: RuleAuditAction.DELETE, - outcome: 'unknown', - savedObject: { type: 'alert', id }, - }) - ); - const removeResult = await this.unsecuredSavedObjectsClient.delete('alert', id); - - await Promise.all([ - taskIdToRemove ? this.taskManager.removeIfExists(taskIdToRemove) : null, - apiKeyToInvalidate - ? bulkMarkApiKeysForInvalidation( - { apiKeys: [apiKeyToInvalidate] }, - this.logger, - this.unsecuredSavedObjectsClient - ) - : null, - ]); - - return removeResult; - } - - public async update({ - id, - data, - }: UpdateOptions): Promise> { - return await retryIfConflicts( - this.logger, - `rulesClient.update('${id}')`, - async () => await this.updateWithOCC({ id, data }) - ); - } - - private async updateWithOCC({ - id, - data, - }: UpdateOptions): Promise> { - let alertSavedObject: SavedObject; - - try { - alertSavedObject = await this.encryptedSavedObjectsClient.getDecryptedAsInternalUser( - 'alert', - id, - { - namespace: this.namespace, - } - ); - } catch (e) { - // We'll skip invalidating the API key since we failed to load the decrypted saved object - this.logger.error( - `update(): Failed to load API key to invalidate on alert ${id}: ${e.message}` - ); - // Still attempt to load the object using SOC - alertSavedObject = await this.unsecuredSavedObjectsClient.get('alert', id); - } - - try { - await this.authorization.ensureAuthorized({ - ruleTypeId: alertSavedObject.attributes.alertTypeId, - consumer: alertSavedObject.attributes.consumer, - operation: WriteOperations.Update, - entity: AlertingAuthorizationEntity.Rule, - }); - } catch (error) { - this.auditLogger?.log( - ruleAuditEvent({ - action: RuleAuditAction.UPDATE, - savedObject: { type: 'alert', id }, - error, - }) - ); - throw error; - } - - this.auditLogger?.log( - ruleAuditEvent({ - action: RuleAuditAction.UPDATE, - outcome: 'unknown', - savedObject: { type: 'alert', id }, - }) - ); - - this.ruleTypeRegistry.ensureRuleTypeEnabled(alertSavedObject.attributes.alertTypeId); - - const updateResult = await this.updateAlert({ id, data }, alertSavedObject); - - await Promise.all([ - alertSavedObject.attributes.apiKey - ? bulkMarkApiKeysForInvalidation( - { apiKeys: [alertSavedObject.attributes.apiKey] }, - this.logger, - this.unsecuredSavedObjectsClient - ) - : null, - (async () => { - if ( - updateResult.scheduledTaskId && - updateResult.schedule && - !isEqual(alertSavedObject.attributes.schedule, updateResult.schedule) - ) { - try { - const { tasks } = await this.taskManager.bulkUpdateSchedules( - [updateResult.scheduledTaskId], - updateResult.schedule - ); - - this.logger.debug( - `Rule update has rescheduled the underlying task: ${updateResult.scheduledTaskId} to run at: ${tasks?.[0]?.runAt}` - ); - } catch (err) { - this.logger.error( - `Rule update failed to run its underlying task. TaskManager bulkUpdateSchedules failed with Error: ${err.message}` - ); - } - } - })(), - ]); - - return updateResult; - } - - private async updateAlert( - { id, data }: UpdateOptions, - { attributes, version }: SavedObject - ): Promise> { - const ruleType = this.ruleTypeRegistry.get(attributes.alertTypeId); - - // Validate - const validatedAlertTypeParams = validateRuleTypeParams(data.params, ruleType.validate?.params); - await this.validateActions(ruleType, data); - - // Throw error if schedule interval is less than the minimum and we are enforcing it - const intervalInMs = parseDuration(data.schedule.interval); - if (intervalInMs < this.minimumScheduleIntervalInMs && this.minimumScheduleInterval.enforce) { - throw Boom.badRequest( - `Error updating rule: the interval is less than the allowed minimum interval of ${this.minimumScheduleInterval.value}` - ); - } - - // Extract saved object references for this rule - const { - references, - params: updatedParams, - actions, - } = await this.extractReferences(ruleType, data.actions, validatedAlertTypeParams); - - const username = await this.getUserName(); - - let createdAPIKey = null; - try { - createdAPIKey = attributes.enabled - ? await this.createAPIKey(this.generateAPIKeyName(ruleType.id, data.name)) - : null; - } catch (error) { - throw Boom.badRequest(`Error updating rule: could not create API key - ${error.message}`); - } - - const apiKeyAttributes = this.apiKeyAsAlertAttributes(createdAPIKey, username); - const notifyWhen = getRuleNotifyWhenType(data.notifyWhen ?? null, data.throttle ?? null); - - let updatedObject: SavedObject; - const createAttributes = this.updateMeta({ - ...attributes, - ...data, - ...apiKeyAttributes, - params: updatedParams as RawRule['params'], - actions, - notifyWhen, - updatedBy: username, - updatedAt: new Date().toISOString(), - }); - - const mappedParams = getMappedParams(updatedParams); - - if (Object.keys(mappedParams).length) { - createAttributes.mapped_params = mappedParams; - } - - try { - updatedObject = await this.unsecuredSavedObjectsClient.create( - 'alert', - createAttributes, - { - id, - overwrite: true, - version, - references, - } - ); - } catch (e) { - // Avoid unused API key - await bulkMarkApiKeysForInvalidation( - { apiKeys: createAttributes.apiKey ? [createAttributes.apiKey] : [] }, - this.logger, - this.unsecuredSavedObjectsClient - ); - - throw e; - } - - // Log warning if schedule interval is less than the minimum but we're not enforcing it - if (intervalInMs < this.minimumScheduleIntervalInMs && !this.minimumScheduleInterval.enforce) { - this.logger.warn( - `Rule schedule interval (${data.schedule.interval}) for "${ruleType.id}" rule type with ID "${id}" is less than the minimum value (${this.minimumScheduleInterval.value}). Running rules at this interval may impact alerting performance. Set "xpack.alerting.rules.minimumScheduleInterval.enforce" to true to prevent such changes.` - ); - } - - return this.getPartialRuleFromRaw( - id, - ruleType, - updatedObject.attributes, - updatedObject.references, - false, - true - ); - } - - private getAuthorizationFilter = async ({ action }: { action: BulkAction }) => { - try { - const authorizationTuple = await this.authorization.getFindAuthorizationFilter( - AlertingAuthorizationEntity.Rule, - alertingAuthorizationFilterOpts - ); - return authorizationTuple.filter; - } catch (error) { - this.auditLogger?.log( - ruleAuditEvent({ - action: RuleAuditAction[action], - error, - }) - ); - throw error; - } - }; - - private getAndValidateCommonBulkOptions = (options: BulkOptions) => { - const filter = (options as BulkOptionsFilter).filter; - const ids = (options as BulkOptionsIds).ids; - - if (!ids && !filter) { - throw Boom.badRequest( - "Either 'ids' or 'filter' property in method's arguments should be provided" - ); - } - - if (ids?.length === 0) { - throw Boom.badRequest("'ids' property should not be an empty array"); - } - - if (ids && filter) { - throw Boom.badRequest( - "Both 'filter' and 'ids' are supplied. Define either 'ids' or 'filter' properties in method's arguments" - ); - } - return { ids, filter }; - }; - - private checkAuthorizationAndGetTotal = async ({ - filter, - action, - }: { - filter: KueryNode | null; - action: BulkAction; - }) => { - const actionToConstantsMapping: Record< - BulkAction, - { WriteOperation: WriteOperations | ReadOperations; RuleAuditAction: RuleAuditAction } - > = { - DELETE: { - WriteOperation: WriteOperations.BulkDelete, - RuleAuditAction: RuleAuditAction.DELETE, - }, - ENABLE: { - WriteOperation: WriteOperations.BulkEnable, - RuleAuditAction: RuleAuditAction.ENABLE, - }, - DISABLE: { - WriteOperation: WriteOperations.BulkDisable, - RuleAuditAction: RuleAuditAction.DISABLE, - }, - }; - const { aggregations, total } = await this.unsecuredSavedObjectsClient.find< - RawRule, - RuleBulkOperationAggregation - >({ - filter, - page: 1, - perPage: 0, - type: 'alert', - aggs: { - alertTypeId: { - multi_terms: { - terms: [ - { field: 'alert.attributes.alertTypeId' }, - { field: 'alert.attributes.consumer' }, - ], - }, - }, - }, - }); - - if (total > MAX_RULES_NUMBER_FOR_BULK_OPERATION) { - throw Boom.badRequest( - `More than ${MAX_RULES_NUMBER_FOR_BULK_OPERATION} rules matched for bulk ${action.toLocaleLowerCase()}` - ); - } - - const buckets = aggregations?.alertTypeId.buckets; - - if (buckets === undefined || buckets?.length === 0) { - throw Boom.badRequest(`No rules found for bulk ${action.toLocaleLowerCase()}`); - } - - await pMap( - buckets, - async ({ key: [ruleType, consumer, actions] }) => { - this.ruleTypeRegistry.ensureRuleTypeEnabled(ruleType); - try { - await this.authorization.ensureAuthorized({ - ruleTypeId: ruleType, - consumer, - operation: actionToConstantsMapping[action].WriteOperation, - entity: AlertingAuthorizationEntity.Rule, - }); - } catch (error) { - this.auditLogger?.log( - ruleAuditEvent({ - action: actionToConstantsMapping[action].RuleAuditAction, - error, - }) - ); - throw error; - } - }, - { concurrency: RULE_TYPE_CHECKS_CONCURRENCY } - ); - return { total }; - }; - - public bulkDeleteRules = async (options: BulkOptions) => { - const { ids, filter } = this.getAndValidateCommonBulkOptions(options); - - const kueryNodeFilter = ids ? convertRuleIdsToKueryNode(ids) : buildKueryNodeFilter(filter); - const authorizationFilter = await this.getAuthorizationFilter({ action: 'DELETE' }); - - const kueryNodeFilterWithAuth = - authorizationFilter && kueryNodeFilter - ? nodeBuilder.and([kueryNodeFilter, authorizationFilter as KueryNode]) - : kueryNodeFilter; - - const { total } = await this.checkAuthorizationAndGetTotal({ - filter: kueryNodeFilterWithAuth, - action: 'DELETE', - }); - - const { apiKeysToInvalidate, errors, taskIdsToDelete } = await retryIfBulkDeleteConflicts( - this.logger, - (filterKueryNode: KueryNode | null) => this.bulkDeleteWithOCC({ filter: filterKueryNode }), - kueryNodeFilterWithAuth - ); - - const taskIdsFailedToBeDeleted: string[] = []; - const taskIdsSuccessfullyDeleted: string[] = []; - if (taskIdsToDelete.length > 0) { - try { - const resultFromDeletingTasks = await this.taskManager.bulkRemoveIfExist(taskIdsToDelete); - resultFromDeletingTasks?.statuses.forEach((status) => { - if (status.success) { - taskIdsSuccessfullyDeleted.push(status.id); - } else { - taskIdsFailedToBeDeleted.push(status.id); - } - }); - if (taskIdsSuccessfullyDeleted.length) { - this.logger.debug( - `Successfully deleted schedules for underlying tasks: ${taskIdsSuccessfullyDeleted.join( - ', ' - )}` - ); - } - if (taskIdsFailedToBeDeleted.length) { - this.logger.error( - `Failure to delete schedules for underlying tasks: ${taskIdsFailedToBeDeleted.join( - ', ' - )}` - ); - } - } catch (error) { - this.logger.error( - `Failure to delete schedules for underlying tasks: ${taskIdsToDelete.join( - ', ' - )}. TaskManager bulkRemoveIfExist failed with Error: ${error.message}` - ); - } - } - - await bulkMarkApiKeysForInvalidation( - { apiKeys: apiKeysToInvalidate }, - this.logger, - this.unsecuredSavedObjectsClient - ); - - return { errors, total, taskIdsFailedToBeDeleted }; - }; - - private bulkDeleteWithOCC = async ({ filter }: { filter: KueryNode | null }) => { - const rulesFinder = - await this.encryptedSavedObjectsClient.createPointInTimeFinderDecryptedAsInternalUser( - { - filter, - type: 'alert', - perPage: 100, - ...(this.namespace ? { namespaces: [this.namespace] } : undefined), - } - ); - - const rules: SavedObjectsBulkDeleteObject[] = []; - const apiKeysToInvalidate: string[] = []; - const taskIdsToDelete: string[] = []; - const errors: BulkOperationError[] = []; - const apiKeyToRuleIdMapping: Record = {}; - const taskIdToRuleIdMapping: Record = {}; - const ruleNameToRuleIdMapping: Record = {}; - - for await (const response of rulesFinder.find()) { - for (const rule of response.saved_objects) { - if (rule.attributes.apiKey) { - apiKeyToRuleIdMapping[rule.id] = rule.attributes.apiKey; - } - if (rule.attributes.name) { - ruleNameToRuleIdMapping[rule.id] = rule.attributes.name; - } - if (rule.attributes.scheduledTaskId) { - taskIdToRuleIdMapping[rule.id] = rule.attributes.scheduledTaskId; - } - rules.push(rule); - - this.auditLogger?.log( - ruleAuditEvent({ - action: RuleAuditAction.DELETE, - outcome: 'unknown', - savedObject: { type: 'alert', id: rule.id }, - }) - ); - } - } - - const result = await this.unsecuredSavedObjectsClient.bulkDelete(rules); - - result.statuses.forEach((status) => { - if (status.error === undefined) { - if (apiKeyToRuleIdMapping[status.id]) { - apiKeysToInvalidate.push(apiKeyToRuleIdMapping[status.id]); - } - if (taskIdToRuleIdMapping[status.id]) { - taskIdsToDelete.push(taskIdToRuleIdMapping[status.id]); - } - } else { - errors.push({ - message: status.error.message ?? 'n/a', - status: status.error.statusCode, - rule: { - id: status.id, - name: ruleNameToRuleIdMapping[status.id] ?? 'n/a', - }, - }); - } - }); - return { apiKeysToInvalidate, errors, taskIdsToDelete }; - }; - - public async bulkEdit( - options: BulkEditOptions - ): Promise<{ - rules: Array>; - errors: BulkOperationError[]; - total: number; - }> { - const queryFilter = (options as BulkEditOptionsFilter).filter; - const ids = (options as BulkEditOptionsIds).ids; - - if (ids && queryFilter) { - throw Boom.badRequest( - "Both 'filter' and 'ids' are supplied. Define either 'ids' or 'filter' properties in method arguments" - ); - } - - const qNodeQueryFilter = buildKueryNodeFilter(queryFilter); - - const qNodeFilter = ids ? convertRuleIdsToKueryNode(ids) : qNodeQueryFilter; - let authorizationTuple; - try { - authorizationTuple = await this.authorization.getFindAuthorizationFilter( - AlertingAuthorizationEntity.Rule, - alertingAuthorizationFilterOpts - ); - } catch (error) { - this.auditLogger?.log( - ruleAuditEvent({ - action: RuleAuditAction.BULK_EDIT, - error, - }) - ); - throw error; - } - const { filter: authorizationFilter } = authorizationTuple; - const qNodeFilterWithAuth = - authorizationFilter && qNodeFilter - ? nodeBuilder.and([qNodeFilter, authorizationFilter as KueryNode]) - : qNodeFilter; - - const { aggregations, total } = await this.unsecuredSavedObjectsClient.find< - RawRule, - RuleBulkOperationAggregation - >({ - filter: qNodeFilterWithAuth, - page: 1, - perPage: 0, - type: 'alert', - aggs: { - alertTypeId: { - multi_terms: { - terms: [ - { field: 'alert.attributes.alertTypeId' }, - { field: 'alert.attributes.consumer' }, - ], - }, - }, - }, - }); - - if (total > MAX_RULES_NUMBER_FOR_BULK_OPERATION) { - throw Boom.badRequest( - `More than ${MAX_RULES_NUMBER_FOR_BULK_OPERATION} rules matched for bulk edit` - ); - } - const buckets = aggregations?.alertTypeId.buckets; - - if (buckets === undefined) { - throw Error('No rules found for bulk edit'); - } - - await pMap( - buckets, - async ({ key: [ruleType, consumer] }) => { - this.ruleTypeRegistry.ensureRuleTypeEnabled(ruleType); - - try { - await this.authorization.ensureAuthorized({ - ruleTypeId: ruleType, - consumer, - operation: WriteOperations.BulkEdit, - entity: AlertingAuthorizationEntity.Rule, - }); - } catch (error) { - this.auditLogger?.log( - ruleAuditEvent({ - action: RuleAuditAction.BULK_EDIT, - error, - }) - ); - throw error; - } - }, - { concurrency: RULE_TYPE_CHECKS_CONCURRENCY } - ); - - const { apiKeysToInvalidate, results, errors } = await retryIfBulkEditConflicts( - this.logger, - `rulesClient.update('operations=${JSON.stringify(options.operations)}, paramsModifier=${ - options.paramsModifier ? '[Function]' : undefined - }')`, - (filterKueryNode: KueryNode | null) => - this.bulkEditOcc({ - filter: filterKueryNode, - operations: options.operations, - paramsModifier: options.paramsModifier, - }), - qNodeFilterWithAuth - ); - - await bulkMarkApiKeysForInvalidation( - { apiKeys: apiKeysToInvalidate }, - this.logger, - this.unsecuredSavedObjectsClient - ); - - const updatedRules = results.map(({ id, attributes, references }) => { - return this.getAlertFromRaw( - id, - attributes.alertTypeId as string, - attributes as RawRule, - references, - false - ); - }); - - // update schedules only if schedule operation is present - const scheduleOperation = options.operations.find( - ( - operation - ): operation is Extract }> => - operation.field === 'schedule' - ); - - if (scheduleOperation?.value) { - const taskIds = updatedRules.reduce((acc, rule) => { - if (rule.scheduledTaskId) { - acc.push(rule.scheduledTaskId); - } - return acc; - }, []); - - try { - await this.taskManager.bulkUpdateSchedules(taskIds, scheduleOperation.value); - this.logger.debug( - `Successfully updated schedules for underlying tasks: ${taskIds.join(', ')}` - ); - } catch (error) { - this.logger.error( - `Failure to update schedules for underlying tasks: ${taskIds.join( - ', ' - )}. TaskManager bulkUpdateSchedules failed with Error: ${error.message}` - ); - } - } - - return { rules: updatedRules, errors, total }; - } - - private async bulkEditOcc({ - filter, - operations, - paramsModifier, - }: { - filter: KueryNode | null; - operations: BulkEditOptions['operations']; - paramsModifier: BulkEditOptions['paramsModifier']; - }): Promise<{ - apiKeysToInvalidate: string[]; - rules: Array>; - resultSavedObjects: Array>; - errors: BulkOperationError[]; - }> { - const rulesFinder = - await this.encryptedSavedObjectsClient.createPointInTimeFinderDecryptedAsInternalUser( - { - filter, - type: 'alert', - perPage: 100, - ...(this.namespace ? { namespaces: [this.namespace] } : undefined), - } - ); - - const rules: Array> = []; - const errors: BulkOperationError[] = []; - const apiKeysToInvalidate: string[] = []; - const apiKeysMap = new Map(); - const username = await this.getUserName(); - - for await (const response of rulesFinder.find()) { - await pMap( - response.saved_objects, - async (rule) => { - try { - if (rule.attributes.apiKey) { - apiKeysMap.set(rule.id, { oldApiKey: rule.attributes.apiKey }); - } - - const ruleType = this.ruleTypeRegistry.get(rule.attributes.alertTypeId); - - let attributes = cloneDeep(rule.attributes); - let ruleActions = { - actions: this.injectReferencesIntoActions( - rule.id, - rule.attributes.actions, - rule.references || [] - ), - }; - - for (const operation of operations) { - const { field } = operation; - if (field === 'snoozeSchedule' || field === 'apiKey') { - if (rule.attributes.actions.length) { - try { - await this.actionsAuthorization.ensureAuthorized('execute'); - } catch (error) { - throw Error(`Rule not authorized for bulk ${field} update - ${error.message}`); - } - } - } - } - - let hasUpdateApiKeyOperation = false; - - for (const operation of operations) { - switch (operation.field) { - case 'actions': - await this.validateActions(ruleType, { ...attributes, actions: operation.value }); - ruleActions = applyBulkEditOperation(operation, ruleActions); - break; - case 'snoozeSchedule': - // Silently skip adding snooze or snooze schedules on security - // rules until we implement snoozing of their rules - if (attributes.consumer === AlertConsumers.SIEM) { - break; - } - if (operation.operation === 'set') { - const snoozeAttributes = getBulkSnoozeAttributes(attributes, operation.value); - try { - verifySnoozeScheduleLimit(snoozeAttributes); - } catch (error) { - throw Error(`Error updating rule: could not add snooze - ${error.message}`); - } - attributes = { - ...attributes, - ...snoozeAttributes, - }; - } - if (operation.operation === 'delete') { - const idsToDelete = operation.value && [...operation.value]; - if (idsToDelete?.length === 0) { - attributes.snoozeSchedule?.forEach((schedule) => { - if (schedule.id) { - idsToDelete.push(schedule.id); - } - }); - } - attributes = { - ...attributes, - ...getBulkUnsnoozeAttributes(attributes, idsToDelete), - }; - } - break; - case 'apiKey': { - hasUpdateApiKeyOperation = true; - break; - } - default: - attributes = applyBulkEditOperation(operation, attributes); - } - } - - // validate schedule interval - if (attributes.schedule.interval) { - const isIntervalInvalid = - parseDuration(attributes.schedule.interval as string) < - this.minimumScheduleIntervalInMs; - if (isIntervalInvalid && this.minimumScheduleInterval.enforce) { - throw Error( - `Error updating rule: the interval is less than the allowed minimum interval of ${this.minimumScheduleInterval.value}` - ); - } else if (isIntervalInvalid && !this.minimumScheduleInterval.enforce) { - this.logger.warn( - `Rule schedule interval (${attributes.schedule.interval}) for "${ruleType.id}" rule type with ID "${attributes.id}" is less than the minimum value (${this.minimumScheduleInterval.value}). Running rules at this interval may impact alerting performance. Set "xpack.alerting.rules.minimumScheduleInterval.enforce" to true to prevent such changes.` - ); - } - } - - const ruleParams = paramsModifier - ? await paramsModifier(attributes.params as Params) - : attributes.params; - - // validate rule params - const validatedAlertTypeParams = validateRuleTypeParams( - ruleParams, - ruleType.validate?.params - ); - const validatedMutatedAlertTypeParams = validateMutatedRuleTypeParams( - validatedAlertTypeParams, - rule.attributes.params, - ruleType.validate?.params - ); - - const { - actions: rawAlertActions, - references, - params: updatedParams, - } = await this.extractReferences( - ruleType, - ruleActions.actions, - validatedMutatedAlertTypeParams - ); - - const shouldUpdateApiKey = attributes.enabled || hasUpdateApiKeyOperation; - - // create API key - let createdAPIKey = null; - try { - createdAPIKey = shouldUpdateApiKey - ? await this.createAPIKey(this.generateAPIKeyName(ruleType.id, attributes.name)) - : null; - } catch (error) { - throw Error(`Error updating rule: could not create API key - ${error.message}`); - } - - const apiKeyAttributes = this.apiKeyAsAlertAttributes(createdAPIKey, username); - - // collect generated API keys - if (apiKeyAttributes.apiKey) { - apiKeysMap.set(rule.id, { - ...apiKeysMap.get(rule.id), - newApiKey: apiKeyAttributes.apiKey, - }); - } - - // get notifyWhen - const notifyWhen = getRuleNotifyWhenType( - attributes.notifyWhen ?? null, - attributes.throttle ?? null - ); - - const updatedAttributes = this.updateMeta({ - ...attributes, - ...apiKeyAttributes, - params: updatedParams as RawRule['params'], - actions: rawAlertActions, - notifyWhen, - updatedBy: username, - updatedAt: new Date().toISOString(), - }); - - // add mapped_params - const mappedParams = getMappedParams(updatedParams); - - if (Object.keys(mappedParams).length) { - updatedAttributes.mapped_params = mappedParams; - } - - rules.push({ - ...rule, - references, - attributes: updatedAttributes, - }); - } catch (error) { - errors.push({ - message: error.message, - rule: { - id: rule.id, - name: rule.attributes?.name, - }, - }); - this.auditLogger?.log( - ruleAuditEvent({ - action: RuleAuditAction.BULK_EDIT, - error, - }) - ); - } - }, - { concurrency: API_KEY_GENERATE_CONCURRENCY } - ); - } - - let result; - try { - result = await this.unsecuredSavedObjectsClient.bulkCreate(rules, { overwrite: true }); - } catch (e) { - // avoid unused newly generated API keys - if (apiKeysMap.size > 0) { - await bulkMarkApiKeysForInvalidation( - { - apiKeys: Array.from(apiKeysMap.values()).reduce((acc, value) => { - if (value.newApiKey) { - acc.push(value.newApiKey); - } - return acc; - }, []), - }, - this.logger, - this.unsecuredSavedObjectsClient - ); - } - throw e; - } - - result.saved_objects.map(({ id, error }) => { - const oldApiKey = apiKeysMap.get(id)?.oldApiKey; - const newApiKey = apiKeysMap.get(id)?.newApiKey; - - // if SO wasn't saved and has new API key it will be invalidated - if (error && newApiKey) { - apiKeysToInvalidate.push(newApiKey); - // if SO saved and has old Api Key it will be invalidate - } else if (!error && oldApiKey) { - apiKeysToInvalidate.push(oldApiKey); - } - }); - - return { apiKeysToInvalidate, resultSavedObjects: result.saved_objects, errors, rules }; - } - - private getShouldScheduleTask = async (scheduledTaskId: string | null | undefined) => { - if (!scheduledTaskId) return true; - try { - // make sure scheduledTaskId exist - await this.taskManager.get(scheduledTaskId); - return false; - } catch (err) { - return true; - } - }; - - public bulkEnableRules = async (options: BulkOptions) => { - const { ids, filter } = this.getAndValidateCommonBulkOptions(options); - - const kueryNodeFilter = ids ? convertRuleIdsToKueryNode(ids) : buildKueryNodeFilter(filter); - const authorizationFilter = await this.getAuthorizationFilter({ action: 'ENABLE' }); - - const kueryNodeFilterWithAuth = - authorizationFilter && kueryNodeFilter - ? nodeBuilder.and([kueryNodeFilter, authorizationFilter as KueryNode]) - : kueryNodeFilter; - - const { total } = await this.checkAuthorizationAndGetTotal({ - filter: kueryNodeFilterWithAuth, - action: 'ENABLE', - }); - - const { errors, rules, accListSpecificForBulkOperation } = await retryIfBulkOperationConflicts({ - action: 'ENABLE', - logger: this.logger, - bulkOperation: (filterKueryNode: KueryNode | null) => - this.bulkEnableRulesWithOCC({ filter: filterKueryNode }), - filter: kueryNodeFilterWithAuth, - }); - - const [taskIdsToEnable] = accListSpecificForBulkOperation; - - const taskIdsFailedToBeEnabled: string[] = []; - if (taskIdsToEnable.length > 0) { - try { - const resultFromEnablingTasks = await this.taskManager.bulkEnable(taskIdsToEnable); - resultFromEnablingTasks?.errors?.forEach((error) => { - taskIdsFailedToBeEnabled.push(error.task.id); - }); - if (resultFromEnablingTasks.tasks.length) { - this.logger.debug( - `Successfully enabled schedules for underlying tasks: ${resultFromEnablingTasks.tasks - .map((task) => task.id) - .join(', ')}` - ); - } - if (resultFromEnablingTasks.errors.length) { - this.logger.error( - `Failure to enable schedules for underlying tasks: ${resultFromEnablingTasks.errors - .map((error) => error.task.id) - .join(', ')}` - ); - } - } catch (error) { - taskIdsFailedToBeEnabled.push(...taskIdsToEnable); - this.logger.error( - `Failure to enable schedules for underlying tasks: ${taskIdsToEnable.join( - ', ' - )}. TaskManager bulkEnable failed with Error: ${error.message}` - ); - } - } - - const updatedRules = rules.map(({ id, attributes, references }) => { - return this.getAlertFromRaw( - id, - attributes.alertTypeId as string, - attributes as RawRule, - references, - false - ); - }); - - return { errors, rules: updatedRules, total, taskIdsFailedToBeEnabled }; - }; - - private bulkEnableRulesWithOCC = async ({ filter }: { filter: KueryNode | null }) => { - const rulesFinder = - await this.encryptedSavedObjectsClient.createPointInTimeFinderDecryptedAsInternalUser( - { - filter, - type: 'alert', - perPage: 100, - ...(this.namespace ? { namespaces: [this.namespace] } : undefined), - } - ); - - const rulesToEnable: Array> = []; - const taskIdsToEnable: string[] = []; - const errors: BulkOperationError[] = []; - const ruleNameToRuleIdMapping: Record = {}; - - for await (const response of rulesFinder.find()) { - await pMap(response.saved_objects, async (rule) => { - try { - if (rule.attributes.actions.length) { - try { - await this.actionsAuthorization.ensureAuthorized('execute'); - } catch (error) { - throw Error(`Rule not authorized for bulk enable - ${error.message}`); - } - } - if (rule.attributes.enabled === true) return; - if (rule.attributes.name) { - ruleNameToRuleIdMapping[rule.id] = rule.attributes.name; - } - - const username = await this.getUserName(); - - const updatedAttributes = this.updateMeta({ - ...rule.attributes, - ...(!rule.attributes.apiKey && - (await this.createNewAPIKeySet({ attributes: rule.attributes, username }))), - enabled: true, - updatedBy: username, - updatedAt: new Date().toISOString(), - executionStatus: { - status: 'pending', - lastDuration: 0, - lastExecutionDate: new Date().toISOString(), - error: null, - warning: null, - }, - }); - - const shouldScheduleTask = await this.getShouldScheduleTask( - rule.attributes.scheduledTaskId - ); - - let scheduledTaskId; - if (shouldScheduleTask) { - const scheduledTask = await this.scheduleTask({ - id: rule.id, - consumer: rule.attributes.consumer, - ruleTypeId: rule.attributes.alertTypeId, - schedule: rule.attributes.schedule as IntervalSchedule, - throwOnConflict: false, - }); - scheduledTaskId = scheduledTask.id; - } - - rulesToEnable.push({ - ...rule, - attributes: { - ...updatedAttributes, - ...(scheduledTaskId ? { scheduledTaskId } : undefined), - }, - }); - - this.auditLogger?.log( - ruleAuditEvent({ - action: RuleAuditAction.ENABLE, - outcome: 'unknown', - savedObject: { type: 'alert', id: rule.id }, - }) - ); - } catch (error) { - errors.push({ - message: error.message, - rule: { - id: rule.id, - name: rule.attributes?.name, - }, - }); - this.auditLogger?.log( - ruleAuditEvent({ - action: RuleAuditAction.ENABLE, - error, - }) - ); - } - }); - } - - const result = await this.unsecuredSavedObjectsClient.bulkCreate(rulesToEnable, { - overwrite: true, - }); - - const rules: Array> = []; - - result.saved_objects.forEach((rule) => { - if (rule.error === undefined) { - if (rule.attributes.scheduledTaskId) { - taskIdsToEnable.push(rule.attributes.scheduledTaskId); - } - rules.push(rule); - } else { - errors.push({ - message: rule.error.message ?? 'n/a', - status: rule.error.statusCode, - rule: { - id: rule.id, - name: ruleNameToRuleIdMapping[rule.id] ?? 'n/a', - }, - }); - } - }); - return { errors, rules, accListSpecificForBulkOperation: [taskIdsToEnable] }; - }; - - private recoverRuleAlerts = async (id: string, attributes: RawRule) => { - if (!this.eventLogger || !attributes.scheduledTaskId) return; - try { - const { state } = taskInstanceToAlertTaskInstance( - await this.taskManager.get(attributes.scheduledTaskId), - attributes as unknown as SanitizedRule - ); - - const recoveredAlerts = mapValues, Alert>( - state.alertInstances ?? {}, - (rawAlertInstance, alertId) => new Alert(alertId, rawAlertInstance) - ); - const recoveredAlertIds = Object.keys(recoveredAlerts); - - for (const alertId of recoveredAlertIds) { - const { group: actionGroup } = recoveredAlerts[alertId].getLastScheduledActions() ?? {}; - const instanceState = recoveredAlerts[alertId].getState(); - const message = `instance '${alertId}' has recovered due to the rule was disabled`; - - const event = createAlertEventLogRecordObject({ - ruleId: id, - ruleName: attributes.name, - ruleType: this.ruleTypeRegistry.get(attributes.alertTypeId), - consumer: attributes.consumer, - instanceId: alertId, - action: EVENT_LOG_ACTIONS.recoveredInstance, - message, - state: instanceState, - group: actionGroup, - namespace: this.namespace, - spaceId: this.spaceId, - savedObjects: [ - { - id, - type: 'alert', - typeId: attributes.alertTypeId, - relation: SAVED_OBJECT_REL_PRIMARY, - }, - ], - }); - this.eventLogger.logEvent(event); - } - } catch (error) { - // this should not block the rest of the disable process - this.logger.warn( - `rulesClient.disable('${id}') - Could not write recovery events - ${error.message}` - ); - } - }; - - public bulkDisableRules = async (options: BulkOptions) => { - const { ids, filter } = this.getAndValidateCommonBulkOptions(options); - - const kueryNodeFilter = ids ? convertRuleIdsToKueryNode(ids) : buildKueryNodeFilter(filter); - const authorizationFilter = await this.getAuthorizationFilter({ action: 'DISABLE' }); - - const kueryNodeFilterWithAuth = - authorizationFilter && kueryNodeFilter - ? nodeBuilder.and([kueryNodeFilter, authorizationFilter as KueryNode]) - : kueryNodeFilter; - - const { total } = await this.checkAuthorizationAndGetTotal({ - filter: kueryNodeFilterWithAuth, - action: 'DISABLE', - }); - - const { errors, rules, taskIdsToDisable, taskIdsToDelete } = await retryIfBulkDisableConflicts( - this.logger, - (filterKueryNode: KueryNode | null) => - this.bulkDisableRulesWithOCC({ filter: filterKueryNode }), - kueryNodeFilterWithAuth - ); - - if (taskIdsToDisable.length > 0) { - try { - const resultFromDisablingTasks = await this.taskManager.bulkDisable(taskIdsToDisable); - if (resultFromDisablingTasks.tasks.length) { - this.logger.debug( - `Successfully disabled schedules for underlying tasks: ${resultFromDisablingTasks.tasks - .map((task) => task.id) - .join(', ')}` - ); - } - if (resultFromDisablingTasks.errors.length) { - this.logger.error( - `Failure to disable schedules for underlying tasks: ${resultFromDisablingTasks.errors - .map((error) => error.task.id) - .join(', ')}` - ); - } - } catch (error) { - this.logger.error( - `Failure to disable schedules for underlying tasks: ${taskIdsToDisable.join( - ', ' - )}. TaskManager bulkDisable failed with Error: ${error.message}` - ); - } - } - - const taskIdsFailedToBeDeleted: string[] = []; - const taskIdsSuccessfullyDeleted: string[] = []; - - if (taskIdsToDelete.length > 0) { - try { - const resultFromDeletingTasks = await this.taskManager.bulkRemoveIfExist(taskIdsToDelete); - resultFromDeletingTasks?.statuses.forEach((status) => { - if (status.success) { - taskIdsSuccessfullyDeleted.push(status.id); - } else { - taskIdsFailedToBeDeleted.push(status.id); - } - }); - if (taskIdsSuccessfullyDeleted.length) { - this.logger.debug( - `Successfully deleted schedules for underlying tasks: ${taskIdsSuccessfullyDeleted.join( - ', ' - )}` - ); - } - if (taskIdsFailedToBeDeleted.length) { - this.logger.error( - `Failure to delete schedules for underlying tasks: ${taskIdsFailedToBeDeleted.join( - ', ' - )}` - ); - } - } catch (error) { - this.logger.error( - `Failure to delete schedules for underlying tasks: ${taskIdsToDelete.join( - ', ' - )}. TaskManager bulkRemoveIfExist failed with Error: ${error.message}` - ); - } - } - - const updatedRules = rules.map(({ id, attributes, references }) => { - return this.getAlertFromRaw( - id, - attributes.alertTypeId as string, - attributes as RawRule, - references, - false - ); - }); - - return { errors, rules: updatedRules, total }; - }; - - private bulkDisableRulesWithOCC = async ({ filter }: { filter: KueryNode | null }) => { - const rulesFinder = - await this.encryptedSavedObjectsClient.createPointInTimeFinderDecryptedAsInternalUser( - { - filter, - type: 'alert', - perPage: 100, - ...(this.namespace ? { namespaces: [this.namespace] } : undefined), - } - ); - - const rulesToDisable: Array> = []; - const errors: BulkOperationError[] = []; - const ruleNameToRuleIdMapping: Record = {}; - - for await (const response of rulesFinder.find()) { - await pMap(response.saved_objects, async (rule) => { - try { - if (rule.attributes.enabled === false) return; - - this.recoverRuleAlerts(rule.id, rule.attributes); - - if (rule.attributes.name) { - ruleNameToRuleIdMapping[rule.id] = rule.attributes.name; - } - - const username = await this.getUserName(); - const updatedAttributes = this.updateMeta({ - ...rule.attributes, - enabled: false, - scheduledTaskId: - rule.attributes.scheduledTaskId === rule.id ? rule.attributes.scheduledTaskId : null, - updatedBy: username, - updatedAt: new Date().toISOString(), - }); - - rulesToDisable.push({ - ...rule, - attributes: { - ...updatedAttributes, - }, - }); - - this.auditLogger?.log( - ruleAuditEvent({ - action: RuleAuditAction.DISABLE, - outcome: 'unknown', - savedObject: { type: 'alert', id: rule.id }, - }) - ); - } catch (error) { - errors.push({ - message: error.message, - rule: { - id: rule.id, - name: rule.attributes?.name, - }, - }); - this.auditLogger?.log( - ruleAuditEvent({ - action: RuleAuditAction.DISABLE, - error, - }) - ); - } - }); - } - - const result = await this.unsecuredSavedObjectsClient.bulkCreate(rulesToDisable, { - overwrite: true, - }); - - const taskIdsToDisable: string[] = []; - const taskIdsToDelete: string[] = []; - const disabledRules: Array> = []; - - result.saved_objects.forEach((rule) => { - if (rule.error === undefined) { - if (rule.attributes.scheduledTaskId) { - if (rule.attributes.scheduledTaskId !== rule.id) { - taskIdsToDelete.push(rule.attributes.scheduledTaskId); - } else { - taskIdsToDisable.push(rule.attributes.scheduledTaskId); - } - } - disabledRules.push(rule); - } else { - errors.push({ - message: rule.error.message ?? 'n/a', - status: rule.error.statusCode, - rule: { - id: rule.id, - name: ruleNameToRuleIdMapping[rule.id] ?? 'n/a', - }, - }); - } - }); - - return { errors, rules: disabledRules, taskIdsToDisable, taskIdsToDelete }; - }; - - private apiKeyAsAlertAttributes( - apiKey: CreateAPIKeyResult | null, - username: string | null - ): Pick { - return apiKey && apiKey.apiKeysEnabled - ? { - apiKeyOwner: username, - apiKey: Buffer.from(`${apiKey.result.id}:${apiKey.result.api_key}`).toString('base64'), - } - : { - apiKeyOwner: null, - apiKey: null, - }; - } - - public async updateApiKey({ id }: { id: string }): Promise { - return await retryIfConflicts( - this.logger, - `rulesClient.updateApiKey('${id}')`, - async () => await this.updateApiKeyWithOCC({ id }) - ); - } - - private async updateApiKeyWithOCC({ id }: { id: string }) { - let apiKeyToInvalidate: string | null = null; - let attributes: RawRule; - let version: string | undefined; - - try { - const decryptedAlert = - await this.encryptedSavedObjectsClient.getDecryptedAsInternalUser('alert', id, { - namespace: this.namespace, - }); - apiKeyToInvalidate = decryptedAlert.attributes.apiKey; - attributes = decryptedAlert.attributes; - version = decryptedAlert.version; - } catch (e) { - // We'll skip invalidating the API key since we failed to load the decrypted saved object - this.logger.error( - `updateApiKey(): Failed to load API key to invalidate on alert ${id}: ${e.message}` - ); - // Still attempt to load the attributes and version using SOC - const alert = await this.unsecuredSavedObjectsClient.get('alert', id); - attributes = alert.attributes; - version = alert.version; - } - - try { - await this.authorization.ensureAuthorized({ - ruleTypeId: attributes.alertTypeId, - consumer: attributes.consumer, - operation: WriteOperations.UpdateApiKey, - entity: AlertingAuthorizationEntity.Rule, - }); - if (attributes.actions.length) { - await this.actionsAuthorization.ensureAuthorized('execute'); - } - } catch (error) { - this.auditLogger?.log( - ruleAuditEvent({ - action: RuleAuditAction.UPDATE_API_KEY, - savedObject: { type: 'alert', id }, - error, - }) - ); - throw error; - } - - const username = await this.getUserName(); - - let createdAPIKey = null; - try { - createdAPIKey = await this.createAPIKey( - this.generateAPIKeyName(attributes.alertTypeId, attributes.name) - ); - } catch (error) { - throw Boom.badRequest( - `Error updating API key for rule: could not create API key - ${error.message}` - ); - } - - const updateAttributes = this.updateMeta({ - ...attributes, - ...this.apiKeyAsAlertAttributes(createdAPIKey, username), - updatedAt: new Date().toISOString(), - updatedBy: username, - }); - - this.auditLogger?.log( - ruleAuditEvent({ - action: RuleAuditAction.UPDATE_API_KEY, - outcome: 'unknown', - savedObject: { type: 'alert', id }, - }) - ); - - this.ruleTypeRegistry.ensureRuleTypeEnabled(attributes.alertTypeId); - - try { - await this.unsecuredSavedObjectsClient.update('alert', id, updateAttributes, { version }); - } catch (e) { - // Avoid unused API key - await bulkMarkApiKeysForInvalidation( - { apiKeys: updateAttributes.apiKey ? [updateAttributes.apiKey] : [] }, - this.logger, - this.unsecuredSavedObjectsClient - ); - throw e; - } - - if (apiKeyToInvalidate) { - await bulkMarkApiKeysForInvalidation( - { apiKeys: [apiKeyToInvalidate] }, - this.logger, - this.unsecuredSavedObjectsClient - ); - } - } - - public async enable({ id }: { id: string }): Promise { - return await retryIfConflicts( - this.logger, - `rulesClient.enable('${id}')`, - async () => await this.enableWithOCC({ id }) - ); - } - - private async enableWithOCC({ id }: { id: string }) { - let existingApiKey: string | null = null; - let attributes: RawRule; - let version: string | undefined; - - try { - const decryptedAlert = - await this.encryptedSavedObjectsClient.getDecryptedAsInternalUser('alert', id, { - namespace: this.namespace, - }); - existingApiKey = decryptedAlert.attributes.apiKey; - attributes = decryptedAlert.attributes; - version = decryptedAlert.version; - } catch (e) { - this.logger.error(`enable(): Failed to load API key of alert ${id}: ${e.message}`); - // Still attempt to load the attributes and version using SOC - const alert = await this.unsecuredSavedObjectsClient.get('alert', id); - attributes = alert.attributes; - version = alert.version; - } - - try { - await this.authorization.ensureAuthorized({ - ruleTypeId: attributes.alertTypeId, - consumer: attributes.consumer, - operation: WriteOperations.Enable, - entity: AlertingAuthorizationEntity.Rule, - }); - - if (attributes.actions.length) { - await this.actionsAuthorization.ensureAuthorized('execute'); - } - } catch (error) { - this.auditLogger?.log( - ruleAuditEvent({ - action: RuleAuditAction.ENABLE, - savedObject: { type: 'alert', id }, - error, - }) - ); - throw error; - } - - this.auditLogger?.log( - ruleAuditEvent({ - action: RuleAuditAction.ENABLE, - outcome: 'unknown', - savedObject: { type: 'alert', id }, - }) - ); - - this.ruleTypeRegistry.ensureRuleTypeEnabled(attributes.alertTypeId); - - if (attributes.enabled === false) { - const username = await this.getUserName(); - const now = new Date(); - - const schedule = attributes.schedule as IntervalSchedule; - - const updateAttributes = this.updateMeta({ - ...attributes, - ...(!existingApiKey && (await this.createNewAPIKeySet({ attributes, username }))), - ...(attributes.monitoring && { - monitoring: updateMonitoring({ - monitoring: attributes.monitoring, - timestamp: now.toISOString(), - duration: 0, - }), - }), - nextRun: getNextRun({ interval: schedule.interval }), - enabled: true, - updatedBy: username, - updatedAt: now.toISOString(), - executionStatus: { - status: 'pending', - lastDuration: 0, - lastExecutionDate: now.toISOString(), - error: null, - warning: null, - }, - }); - - try { - await this.unsecuredSavedObjectsClient.update('alert', id, updateAttributes, { version }); - } catch (e) { - throw e; - } - } - - let scheduledTaskIdToCreate: string | null = null; - if (attributes.scheduledTaskId) { - // If scheduledTaskId defined in rule SO, make sure it exists - try { - await this.taskManager.get(attributes.scheduledTaskId); - } catch (err) { - scheduledTaskIdToCreate = id; - } - } else { - // If scheduledTaskId doesn't exist in rule SO, set it to rule ID - scheduledTaskIdToCreate = id; - } - - if (scheduledTaskIdToCreate) { - // Schedule the task if it doesn't exist - const scheduledTask = await this.scheduleTask({ - id, - consumer: attributes.consumer, - ruleTypeId: attributes.alertTypeId, - schedule: attributes.schedule as IntervalSchedule, - throwOnConflict: false, - }); - await this.unsecuredSavedObjectsClient.update('alert', id, { - scheduledTaskId: scheduledTask.id, - }); - } else { - // Task exists so set enabled to true - await this.taskManager.bulkEnable([attributes.scheduledTaskId!]); - } - } - - private async createNewAPIKeySet({ - attributes, - username, - }: { - attributes: RawRule; - username: string | null; - }): Promise> { - let createdAPIKey = null; - try { - createdAPIKey = await this.createAPIKey( - this.generateAPIKeyName(attributes.alertTypeId, attributes.name) - ); - } catch (error) { - throw Boom.badRequest(`Error creating API key for rule: ${error.message}`); - } - - return this.apiKeyAsAlertAttributes(createdAPIKey, username); - } - - public async disable({ id }: { id: string }): Promise { - return await retryIfConflicts( - this.logger, - `rulesClient.disable('${id}')`, - async () => await this.disableWithOCC({ id }) - ); - } - - private async disableWithOCC({ id }: { id: string }) { - let attributes: RawRule; - let version: string | undefined; - - try { - const decryptedAlert = - await this.encryptedSavedObjectsClient.getDecryptedAsInternalUser('alert', id, { - namespace: this.namespace, - }); - attributes = decryptedAlert.attributes; - version = decryptedAlert.version; - } catch (e) { - this.logger.error(`disable(): Failed to load API key of alert ${id}: ${e.message}`); - // Still attempt to load the attributes and version using SOC - const alert = await this.unsecuredSavedObjectsClient.get('alert', id); - attributes = alert.attributes; - version = alert.version; - } - - this.recoverRuleAlerts(id, attributes); - - try { - await this.authorization.ensureAuthorized({ - ruleTypeId: attributes.alertTypeId, - consumer: attributes.consumer, - operation: WriteOperations.Disable, - entity: AlertingAuthorizationEntity.Rule, - }); - } catch (error) { - this.auditLogger?.log( - ruleAuditEvent({ - action: RuleAuditAction.DISABLE, - savedObject: { type: 'alert', id }, - error, - }) - ); - throw error; - } - - this.auditLogger?.log( - ruleAuditEvent({ - action: RuleAuditAction.DISABLE, - outcome: 'unknown', - savedObject: { type: 'alert', id }, - }) - ); - - this.ruleTypeRegistry.ensureRuleTypeEnabled(attributes.alertTypeId); - - if (attributes.enabled === true) { - await this.unsecuredSavedObjectsClient.update( - 'alert', - id, - this.updateMeta({ - ...attributes, - enabled: false, - scheduledTaskId: attributes.scheduledTaskId === id ? attributes.scheduledTaskId : null, - updatedBy: await this.getUserName(), - updatedAt: new Date().toISOString(), - nextRun: null, - }), - { version } - ); - - // If the scheduledTaskId does not match the rule id, we should - // remove the task, otherwise mark the task as disabled - if (attributes.scheduledTaskId) { - if (attributes.scheduledTaskId !== id) { - await this.taskManager.removeIfExists(attributes.scheduledTaskId); - } else { - await this.taskManager.bulkDisable([attributes.scheduledTaskId]); - } - } - } - } - - public async snooze({ - id, - snoozeSchedule, - }: { - id: string; - snoozeSchedule: RuleSnoozeSchedule; - }): Promise { - const snoozeDateValidationMsg = validateSnoozeStartDate(snoozeSchedule.rRule.dtstart); - if (snoozeDateValidationMsg) { - throw new RuleMutedError(snoozeDateValidationMsg); - } - - return await retryIfConflicts( - this.logger, - `rulesClient.snooze('${id}', ${JSON.stringify(snoozeSchedule, null, 4)})`, - async () => await this.snoozeWithOCC({ id, snoozeSchedule }) - ); - } - - private async snoozeWithOCC({ - id, - snoozeSchedule, - }: { - id: string; - snoozeSchedule: RuleSnoozeSchedule; - }) { - const { attributes, version } = await this.unsecuredSavedObjectsClient.get( - 'alert', - id - ); - - try { - await this.authorization.ensureAuthorized({ - ruleTypeId: attributes.alertTypeId, - consumer: attributes.consumer, - operation: WriteOperations.Snooze, - entity: AlertingAuthorizationEntity.Rule, - }); - - if (attributes.actions.length) { - await this.actionsAuthorization.ensureAuthorized('execute'); - } - } catch (error) { - this.auditLogger?.log( - ruleAuditEvent({ - action: RuleAuditAction.SNOOZE, - savedObject: { type: 'alert', id }, - error, - }) - ); - throw error; - } - - this.auditLogger?.log( - ruleAuditEvent({ - action: RuleAuditAction.SNOOZE, - outcome: 'unknown', - savedObject: { type: 'alert', id }, - }) - ); - - this.ruleTypeRegistry.ensureRuleTypeEnabled(attributes.alertTypeId); - - const newAttrs = getSnoozeAttributes(attributes, snoozeSchedule); - - try { - verifySnoozeScheduleLimit(newAttrs); - } catch (error) { - throw Boom.badRequest(error.message); - } - - const updateAttributes = this.updateMeta({ - ...newAttrs, - updatedBy: await this.getUserName(), - updatedAt: new Date().toISOString(), - }); - const updateOptions = { version }; - - await partiallyUpdateAlert( - this.unsecuredSavedObjectsClient, - id, - updateAttributes, - updateOptions - ); - } - - public async unsnooze({ - id, - scheduleIds, - }: { - id: string; - scheduleIds?: string[]; - }): Promise { - return await retryIfConflicts( - this.logger, - `rulesClient.unsnooze('${id}')`, - async () => await this.unsnoozeWithOCC({ id, scheduleIds }) - ); - } - - private async unsnoozeWithOCC({ id, scheduleIds }: { id: string; scheduleIds?: string[] }) { - const { attributes, version } = await this.unsecuredSavedObjectsClient.get( - 'alert', - id - ); - - try { - await this.authorization.ensureAuthorized({ - ruleTypeId: attributes.alertTypeId, - consumer: attributes.consumer, - operation: WriteOperations.Unsnooze, - entity: AlertingAuthorizationEntity.Rule, - }); - - if (attributes.actions.length) { - await this.actionsAuthorization.ensureAuthorized('execute'); - } - } catch (error) { - this.auditLogger?.log( - ruleAuditEvent({ - action: RuleAuditAction.UNSNOOZE, - savedObject: { type: 'alert', id }, - error, - }) - ); - throw error; - } - - this.auditLogger?.log( - ruleAuditEvent({ - action: RuleAuditAction.UNSNOOZE, - outcome: 'unknown', - savedObject: { type: 'alert', id }, - }) - ); - - this.ruleTypeRegistry.ensureRuleTypeEnabled(attributes.alertTypeId); - const newAttrs = getUnsnoozeAttributes(attributes, scheduleIds); - - const updateAttributes = this.updateMeta({ - ...newAttrs, - updatedBy: await this.getUserName(), - updatedAt: new Date().toISOString(), - }); - const updateOptions = { version }; - - await partiallyUpdateAlert( - this.unsecuredSavedObjectsClient, - id, - updateAttributes, - updateOptions - ); - } - - public calculateIsSnoozedUntil(rule: { - muteAll: boolean; - snoozeSchedule?: RuleSnooze; - }): string | null { - const isSnoozedUntil = getRuleSnoozeEndTime(rule); - return isSnoozedUntil ? isSnoozedUntil.toISOString() : null; - } - - public async clearExpiredSnoozes({ id }: { id: string }): Promise { - const { attributes, version } = await this.unsecuredSavedObjectsClient.get( - 'alert', - id - ); - - const snoozeSchedule = attributes.snoozeSchedule - ? attributes.snoozeSchedule.filter((s) => { - try { - return !isSnoozeExpired(s); - } catch (e) { - this.logger.error(`Error checking for expiration of snooze ${s.id}: ${e}`); - return true; - } - }) - : []; - - if (snoozeSchedule.length === attributes.snoozeSchedule?.length) return; - - const updateAttributes = this.updateMeta({ - snoozeSchedule, - updatedBy: await this.getUserName(), - updatedAt: new Date().toISOString(), - }); - const updateOptions = { version }; - - await partiallyUpdateAlert( - this.unsecuredSavedObjectsClient, - id, - updateAttributes, - updateOptions - ); - } - - public async muteAll({ id }: { id: string }): Promise { - return await retryIfConflicts( - this.logger, - `rulesClient.muteAll('${id}')`, - async () => await this.muteAllWithOCC({ id }) - ); - } - - private async muteAllWithOCC({ id }: { id: string }) { - const { attributes, version } = await this.unsecuredSavedObjectsClient.get( - 'alert', - id - ); - - try { - await this.authorization.ensureAuthorized({ - ruleTypeId: attributes.alertTypeId, - consumer: attributes.consumer, - operation: WriteOperations.MuteAll, - entity: AlertingAuthorizationEntity.Rule, - }); - - if (attributes.actions.length) { - await this.actionsAuthorization.ensureAuthorized('execute'); - } - } catch (error) { - this.auditLogger?.log( - ruleAuditEvent({ - action: RuleAuditAction.MUTE, - savedObject: { type: 'alert', id }, - error, - }) - ); - throw error; - } - - this.auditLogger?.log( - ruleAuditEvent({ - action: RuleAuditAction.MUTE, - outcome: 'unknown', - savedObject: { type: 'alert', id }, - }) - ); - - this.ruleTypeRegistry.ensureRuleTypeEnabled(attributes.alertTypeId); - - const updateAttributes = this.updateMeta({ - muteAll: true, - mutedInstanceIds: [], - snoozeSchedule: clearUnscheduledSnooze(attributes), - updatedBy: await this.getUserName(), - updatedAt: new Date().toISOString(), - }); - const updateOptions = { version }; - - await partiallyUpdateAlert( - this.unsecuredSavedObjectsClient, - id, - updateAttributes, - updateOptions - ); - } - - public async unmuteAll({ id }: { id: string }): Promise { - return await retryIfConflicts( - this.logger, - `rulesClient.unmuteAll('${id}')`, - async () => await this.unmuteAllWithOCC({ id }) - ); - } - - private async unmuteAllWithOCC({ id }: { id: string }) { - const { attributes, version } = await this.unsecuredSavedObjectsClient.get( - 'alert', - id - ); - - try { - await this.authorization.ensureAuthorized({ - ruleTypeId: attributes.alertTypeId, - consumer: attributes.consumer, - operation: WriteOperations.UnmuteAll, - entity: AlertingAuthorizationEntity.Rule, - }); - - if (attributes.actions.length) { - await this.actionsAuthorization.ensureAuthorized('execute'); - } - } catch (error) { - this.auditLogger?.log( - ruleAuditEvent({ - action: RuleAuditAction.UNMUTE, - savedObject: { type: 'alert', id }, - error, - }) - ); - throw error; - } - - this.auditLogger?.log( - ruleAuditEvent({ - action: RuleAuditAction.UNMUTE, - outcome: 'unknown', - savedObject: { type: 'alert', id }, - }) - ); - - this.ruleTypeRegistry.ensureRuleTypeEnabled(attributes.alertTypeId); - - const updateAttributes = this.updateMeta({ - muteAll: false, - mutedInstanceIds: [], - snoozeSchedule: clearUnscheduledSnooze(attributes), - updatedBy: await this.getUserName(), - updatedAt: new Date().toISOString(), - }); - const updateOptions = { version }; - - await partiallyUpdateAlert( - this.unsecuredSavedObjectsClient, - id, - updateAttributes, - updateOptions - ); - } - - public async muteInstance({ alertId, alertInstanceId }: MuteOptions): Promise { - return await retryIfConflicts( - this.logger, - `rulesClient.muteInstance('${alertId}')`, - async () => await this.muteInstanceWithOCC({ alertId, alertInstanceId }) - ); - } - - private async muteInstanceWithOCC({ alertId, alertInstanceId }: MuteOptions) { - const { attributes, version } = await this.unsecuredSavedObjectsClient.get( - 'alert', - alertId - ); - - try { - await this.authorization.ensureAuthorized({ - ruleTypeId: attributes.alertTypeId, - consumer: attributes.consumer, - operation: WriteOperations.MuteAlert, - entity: AlertingAuthorizationEntity.Rule, - }); - - if (attributes.actions.length) { - await this.actionsAuthorization.ensureAuthorized('execute'); - } - } catch (error) { - this.auditLogger?.log( - ruleAuditEvent({ - action: RuleAuditAction.MUTE_ALERT, - savedObject: { type: 'alert', id: alertId }, - error, - }) - ); - throw error; - } - - this.auditLogger?.log( - ruleAuditEvent({ - action: RuleAuditAction.MUTE_ALERT, - outcome: 'unknown', - savedObject: { type: 'alert', id: alertId }, - }) - ); - - this.ruleTypeRegistry.ensureRuleTypeEnabled(attributes.alertTypeId); - - const mutedInstanceIds = attributes.mutedInstanceIds || []; - if (!attributes.muteAll && !mutedInstanceIds.includes(alertInstanceId)) { - mutedInstanceIds.push(alertInstanceId); - await this.unsecuredSavedObjectsClient.update( - 'alert', - alertId, - this.updateMeta({ - mutedInstanceIds, - updatedBy: await this.getUserName(), - updatedAt: new Date().toISOString(), - }), - { version } - ); - } - } - - public async unmuteInstance({ alertId, alertInstanceId }: MuteOptions): Promise { - return await retryIfConflicts( - this.logger, - `rulesClient.unmuteInstance('${alertId}')`, - async () => await this.unmuteInstanceWithOCC({ alertId, alertInstanceId }) - ); - } - - private async unmuteInstanceWithOCC({ - alertId, - alertInstanceId, - }: { - alertId: string; - alertInstanceId: string; - }) { - const { attributes, version } = await this.unsecuredSavedObjectsClient.get( - 'alert', - alertId - ); - - try { - await this.authorization.ensureAuthorized({ - ruleTypeId: attributes.alertTypeId, - consumer: attributes.consumer, - operation: WriteOperations.UnmuteAlert, - entity: AlertingAuthorizationEntity.Rule, - }); - if (attributes.actions.length) { - await this.actionsAuthorization.ensureAuthorized('execute'); - } - } catch (error) { - this.auditLogger?.log( - ruleAuditEvent({ - action: RuleAuditAction.UNMUTE_ALERT, - savedObject: { type: 'alert', id: alertId }, - error, - }) - ); - throw error; - } - - this.auditLogger?.log( - ruleAuditEvent({ - action: RuleAuditAction.UNMUTE_ALERT, - outcome: 'unknown', - savedObject: { type: 'alert', id: alertId }, - }) - ); - - this.ruleTypeRegistry.ensureRuleTypeEnabled(attributes.alertTypeId); - - const mutedInstanceIds = attributes.mutedInstanceIds || []; - if (!attributes.muteAll && mutedInstanceIds.includes(alertInstanceId)) { - await this.unsecuredSavedObjectsClient.update( - 'alert', - alertId, - this.updateMeta({ - updatedBy: await this.getUserName(), - updatedAt: new Date().toISOString(), - mutedInstanceIds: mutedInstanceIds.filter((id: string) => id !== alertInstanceId), - }), - { version } - ); - } - } - - public async runSoon({ id }: { id: string }) { - const { attributes } = await this.unsecuredSavedObjectsClient.get('alert', id); - try { - await this.authorization.ensureAuthorized({ - ruleTypeId: attributes.alertTypeId, - consumer: attributes.consumer, - operation: ReadOperations.RunSoon, - entity: AlertingAuthorizationEntity.Rule, - }); - - if (attributes.actions.length) { - await this.actionsAuthorization.ensureAuthorized('execute'); - } - } catch (error) { - this.auditLogger?.log( - ruleAuditEvent({ - action: RuleAuditAction.RUN_SOON, - savedObject: { type: 'alert', id }, - error, - }) - ); - throw error; - } - - this.auditLogger?.log( - ruleAuditEvent({ - action: RuleAuditAction.RUN_SOON, - outcome: 'unknown', - savedObject: { type: 'alert', id }, - }) - ); - - this.ruleTypeRegistry.ensureRuleTypeEnabled(attributes.alertTypeId); - - // Check that the rule is enabled - if (!attributes.enabled) { - return i18n.translate('xpack.alerting.rulesClient.runSoon.disabledRuleError', { - defaultMessage: 'Error running rule: rule is disabled', - }); - } - - let taskDoc: ConcreteTaskInstance | null = null; - try { - taskDoc = attributes.scheduledTaskId - ? await this.taskManager.get(attributes.scheduledTaskId) - : null; - } catch (err) { - return i18n.translate('xpack.alerting.rulesClient.runSoon.getTaskError', { - defaultMessage: 'Error running rule: {errMessage}', - values: { - errMessage: err.message, - }, - }); - } - - if ( - taskDoc && - (taskDoc.status === TaskStatus.Claiming || taskDoc.status === TaskStatus.Running) - ) { - return i18n.translate('xpack.alerting.rulesClient.runSoon.ruleIsRunning', { - defaultMessage: 'Rule is already running', - }); - } - - try { - await this.taskManager.runSoon(attributes.scheduledTaskId ? attributes.scheduledTaskId : id); - } catch (err) { - return i18n.translate('xpack.alerting.rulesClient.runSoon.runSoonError', { - defaultMessage: 'Error running rule: {errMessage}', - values: { - errMessage: err.message, - }, - }); - } - } - - public async listAlertTypes() { - return await this.authorization.filterByRuleTypeAuthorization( - this.ruleTypeRegistry.list(), - [ReadOperations.Get, WriteOperations.Create], - AlertingAuthorizationEntity.Rule - ); - } + private readonly context: RulesClientContext; + + constructor(context: ConstructorOptions) { + this.context = { + ...context, + minimumScheduleIntervalInMs: parseDuration(context.minimumScheduleInterval.value), + fieldsToExcludeFromPublicApi, + }; + } + + public aggregate = (params?: { options?: AggregateOptions }) => aggregate(this.context, params); + public clone = (...args: CloneArguments) => + clone(this.context, ...args); + public create = (params: CreateOptions) => + create(this.context, params); + public delete = (params: { id: string }) => deleteRule(this.context, params); + public find = (params?: FindParams) => + find(this.context, params); + public get = (params: GetParams) => + get(this.context, params); + public resolve = (params: ResolveParams) => + resolve(this.context, params); + public update = (params: UpdateOptions) => + update(this.context, params); + + public getAlertState = (params: GetAlertStateParams) => getAlertState(this.context, params); + public getAlertSummary = (params: GetAlertSummaryParams) => getAlertSummary(this.context, params); + public getExecutionLogForRule = (params: GetExecutionLogByIdParams) => + getExecutionLogForRule(this.context, params); + public getGlobalExecutionLogWithAuth = (params: GetGlobalExecutionLogParams) => + getGlobalExecutionLogWithAuth(this.context, params); + public getRuleExecutionKPI = (params: GetRuleExecutionKPIParams) => + getRuleExecutionKPI(this.context, params); + public getGlobalExecutionKpiWithAuth = (params: GetGlobalExecutionKPIParams) => + getGlobalExecutionKpiWithAuth(this.context, params); + public getActionErrorLog = (params: GetActionErrorLogByIdParams) => + getActionErrorLog(this.context, params); + public getActionErrorLogWithAuth = (params: GetActionErrorLogByIdParams) => + getActionErrorLogWithAuth(this.context, params); + + public bulkDeleteRules = (options: BulkOptions) => bulkDeleteRules(this.context, options); + public bulkEdit = (options: BulkEditOptions) => + bulkEdit(this.context, options); + public bulkEnableRules = (options: BulkOptions) => bulkEnableRules(this.context, options); + public bulkDisableRules = (options: BulkOptions) => bulkDisableRules(this.context, options); + + public updateApiKey = (options: { id: string }) => updateApiKey(this.context, options); + + public enable = (options: { id: string }) => enable(this.context, options); + public disable = (options: { id: string }) => disable(this.context, options); + + public snooze = (options: SnoozeParams) => snooze(this.context, options); + public unsnooze = (options: UnsnoozeParams) => unsnooze(this.context, options); + + public clearExpiredSnoozes = (options: { id: string }) => + clearExpiredSnoozes(this.context, options); + + public muteAll = (options: { id: string }) => muteAll(this.context, options); + public unmuteAll = (options: { id: string }) => unmuteAll(this.context, options); + public muteInstance = (options: MuteOptions) => muteInstance(this.context, options); + public unmuteInstance = (options: MuteOptions) => unmuteInstance(this.context, options); + + public runSoon = (options: { id: string }) => runSoon(this.context, options); + + public listAlertTypes = () => listAlertTypes(this.context); public getSpaceId(): string | undefined { - return this.spaceId; - } - - private async scheduleTask(opts: ScheduleTaskOptions) { - const { id, consumer, ruleTypeId, schedule, throwOnConflict } = opts; - const taskInstance = { - id, // use the same ID for task document as the rule - taskType: `alerting:${ruleTypeId}`, - schedule, - params: { - alertId: id, - spaceId: this.spaceId, - consumer, - }, - state: { - previousStartedAt: null, - alertTypeState: {}, - alertInstances: {}, - }, - scope: ['alerting'], - enabled: true, - }; - try { - return await this.taskManager.schedule(taskInstance); - } catch (err) { - if (err.statusCode === 409 && !throwOnConflict) { - return taskInstance; - } - throw err; - } - } - - private injectReferencesIntoActions( - alertId: string, - actions: RawRule['actions'], - references: SavedObjectReference[] - ) { - return actions.map((action) => { - if (action.actionRef.startsWith(preconfiguredConnectorActionRefPrefix)) { - return { - ...omit(action, 'actionRef'), - id: action.actionRef.replace(preconfiguredConnectorActionRefPrefix, ''), - }; - } - - const reference = references.find((ref) => ref.name === action.actionRef); - if (!reference) { - throw new Error(`Action reference "${action.actionRef}" not found in alert id: ${alertId}`); - } - return { - ...omit(action, 'actionRef'), - id: reference.id, - }; - }) as Rule['actions']; - } - - private getAlertFromRaw( - id: string, - ruleTypeId: string, - rawRule: RawRule, - references: SavedObjectReference[] | undefined, - includeLegacyId: boolean = false, - excludeFromPublicApi: boolean = false, - includeSnoozeData: boolean = false - ): Rule | RuleWithLegacyId { - const ruleType = this.ruleTypeRegistry.get(ruleTypeId); - // In order to support the partial update API of Saved Objects we have to support - // partial updates of an Alert, but when we receive an actual RawRule, it is safe - // to cast the result to an Alert - const res = this.getPartialRuleFromRaw( - id, - ruleType, - rawRule, - references, - includeLegacyId, - excludeFromPublicApi, - includeSnoozeData - ); - // include to result because it is for internal rules client usage - if (includeLegacyId) { - return res as RuleWithLegacyId; - } - // exclude from result because it is an internal variable - return omit(res, ['legacyId']) as Rule; - } - - private getPartialRuleFromRaw( - id: string, - ruleType: UntypedNormalizedRuleType, - { - createdAt, - updatedAt, - meta, - notifyWhen, - legacyId, - scheduledTaskId, - params, - executionStatus, - monitoring, - nextRun, - schedule, - actions, - snoozeSchedule, - ...partialRawRule - }: Partial, - references: SavedObjectReference[] | undefined, - includeLegacyId: boolean = false, - excludeFromPublicApi: boolean = false, - includeSnoozeData: boolean = false - ): PartialRule | PartialRuleWithLegacyId { - const snoozeScheduleDates = snoozeSchedule?.map((s) => ({ - ...s, - rRule: { - ...s.rRule, - dtstart: new Date(s.rRule.dtstart), - ...(s.rRule.until ? { until: new Date(s.rRule.until) } : {}), - }, - })); - const includeSnoozeSchedule = - snoozeSchedule !== undefined && !isEmpty(snoozeSchedule) && !excludeFromPublicApi; - const isSnoozedUntil = includeSnoozeSchedule - ? this.calculateIsSnoozedUntil({ - muteAll: partialRawRule.muteAll ?? false, - snoozeSchedule, - }) - : null; - const includeMonitoring = monitoring && !excludeFromPublicApi; - const rule = { - id, - notifyWhen, - ...omit(partialRawRule, excludeFromPublicApi ? [...this.fieldsToExcludeFromPublicApi] : ''), - // we currently only support the Interval Schedule type - // Once we support additional types, this type signature will likely change - schedule: schedule as IntervalSchedule, - actions: actions ? this.injectReferencesIntoActions(id, actions, references || []) : [], - params: this.injectReferencesIntoParams(id, ruleType, params, references || []) as Params, - ...(excludeFromPublicApi ? {} : { snoozeSchedule: snoozeScheduleDates ?? [] }), - ...(includeSnoozeData && !excludeFromPublicApi - ? { - activeSnoozes: getActiveScheduledSnoozes({ - snoozeSchedule, - muteAll: partialRawRule.muteAll ?? false, - })?.map((s) => s.id), - isSnoozedUntil, - } - : {}), - ...(updatedAt ? { updatedAt: new Date(updatedAt) } : {}), - ...(createdAt ? { createdAt: new Date(createdAt) } : {}), - ...(scheduledTaskId ? { scheduledTaskId } : {}), - ...(executionStatus - ? { executionStatus: ruleExecutionStatusFromRaw(this.logger, id, executionStatus) } - : {}), - ...(includeMonitoring - ? { monitoring: convertMonitoringFromRawAndVerify(this.logger, id, monitoring) } - : {}), - ...(nextRun ? { nextRun: new Date(nextRun) } : {}), - }; - - return includeLegacyId - ? ({ ...rule, legacyId } as PartialRuleWithLegacyId) - : (rule as PartialRule); - } - - private async validateActions( - alertType: UntypedNormalizedRuleType, - data: Pick & { actions: NormalizedAlertAction[] } - ): Promise { - const { actions, notifyWhen, throttle } = data; - const hasNotifyWhen = typeof notifyWhen !== 'undefined'; - const hasThrottle = typeof throttle !== 'undefined'; - let usesRuleLevelFreqParams; - if (hasNotifyWhen && hasThrottle) usesRuleLevelFreqParams = true; - else if (!hasNotifyWhen && !hasThrottle) usesRuleLevelFreqParams = false; - else { - throw Boom.badRequest( - i18n.translate('xpack.alerting.rulesClient.usesValidGlobalFreqParams.oneUndefined', { - defaultMessage: - 'Rule-level notifyWhen and throttle must both be defined or both be undefined', - }) - ); - } - - if (actions.length === 0) { - return; - } - - // check for actions using connectors with missing secrets - const actionsClient = await this.getActionsClient(); - const actionIds = [...new Set(actions.map((action) => action.id))]; - const actionResults = (await actionsClient.getBulk(actionIds)) || []; - const actionsUsingConnectorsWithMissingSecrets = actionResults.filter( - (result) => result.isMissingSecrets - ); - - if (actionsUsingConnectorsWithMissingSecrets.length) { - throw Boom.badRequest( - i18n.translate('xpack.alerting.rulesClient.validateActions.misconfiguredConnector', { - defaultMessage: 'Invalid connectors: {groups}', - values: { - groups: actionsUsingConnectorsWithMissingSecrets - .map((connector) => connector.name) - .join(', '), - }, - }) - ); - } - - // check for actions with invalid action groups - const { actionGroups: alertTypeActionGroups } = alertType; - const usedAlertActionGroups = actions.map((action) => action.group); - const availableAlertTypeActionGroups = new Set(map(alertTypeActionGroups, 'id')); - const invalidActionGroups = usedAlertActionGroups.filter( - (group) => !availableAlertTypeActionGroups.has(group) - ); - if (invalidActionGroups.length) { - throw Boom.badRequest( - i18n.translate('xpack.alerting.rulesClient.validateActions.invalidGroups', { - defaultMessage: 'Invalid action groups: {groups}', - values: { - groups: invalidActionGroups.join(', '), - }, - }) - ); - } - - // check for actions using frequency params if the rule has rule-level frequency params defined - if (usesRuleLevelFreqParams) { - const actionsWithFrequency = actions.filter((action) => Boolean(action.frequency)); - if (actionsWithFrequency.length) { - throw Boom.badRequest( - i18n.translate('xpack.alerting.rulesClient.validateActions.mixAndMatchFreqParams', { - defaultMessage: - 'Cannot specify per-action frequency params when notify_when and throttle are defined at the rule level: {groups}', - values: { - groups: actionsWithFrequency.map((a) => a.group).join(', '), - }, - }) - ); - } - } else { - const actionsWithoutFrequency = actions.filter((action) => !action.frequency); - if (actionsWithoutFrequency.length) { - throw Boom.badRequest( - i18n.translate('xpack.alerting.rulesClient.validateActions.notAllActionsWithFreq', { - defaultMessage: 'Actions missing frequency parameters: {groups}', - values: { - groups: actionsWithoutFrequency.map((a) => a.group).join(', '), - }, - }) - ); - } - } - } - - private async extractReferences< - Params extends RuleTypeParams, - ExtractedParams extends RuleTypeParams - >( - ruleType: UntypedNormalizedRuleType, - ruleActions: NormalizedAlertAction[], - ruleParams: Params - ): Promise<{ - actions: RawRule['actions']; - params: ExtractedParams; - references: SavedObjectReference[]; - }> { - const { references: actionReferences, actions } = await this.denormalizeActions(ruleActions); - - // Extracts any references using configured reference extractor if available - const extractedRefsAndParams = ruleType?.useSavedObjectReferences?.extractReferences - ? ruleType.useSavedObjectReferences.extractReferences(ruleParams) - : null; - const extractedReferences = extractedRefsAndParams?.references ?? []; - const params = (extractedRefsAndParams?.params as ExtractedParams) ?? ruleParams; - - // Prefix extracted references in order to avoid clashes with framework level references - const paramReferences = extractedReferences.map((reference: SavedObjectReference) => ({ - ...reference, - name: `${extractedSavedObjectParamReferenceNamePrefix}${reference.name}`, - })); - - const references = [...actionReferences, ...paramReferences]; - - return { - actions, - params, - references, - }; - } - - private injectReferencesIntoParams< - Params extends RuleTypeParams, - ExtractedParams extends RuleTypeParams - >( - ruleId: string, - ruleType: UntypedNormalizedRuleType, - ruleParams: SavedObjectAttributes | undefined, - references: SavedObjectReference[] - ): Params { - try { - const paramReferences = references - .filter((reference: SavedObjectReference) => - reference.name.startsWith(extractedSavedObjectParamReferenceNamePrefix) - ) - .map((reference: SavedObjectReference) => ({ - ...reference, - name: reference.name.replace(extractedSavedObjectParamReferenceNamePrefix, ''), - })); - return ruleParams && ruleType?.useSavedObjectReferences?.injectReferences - ? (ruleType.useSavedObjectReferences.injectReferences( - ruleParams as ExtractedParams, - paramReferences - ) as Params) - : (ruleParams as Params); - } catch (err) { - throw Boom.badRequest( - `Error injecting reference into rule params for rule id ${ruleId} - ${err.message}` - ); - } - } - - private async denormalizeActions( - alertActions: NormalizedAlertAction[] - ): Promise<{ actions: RawRule['actions']; references: SavedObjectReference[] }> { - const references: SavedObjectReference[] = []; - const actions: RawRule['actions'] = []; - if (alertActions.length) { - const actionsClient = await this.getActionsClient(); - const actionIds = [...new Set(alertActions.map((alertAction) => alertAction.id))]; - const actionResults = await actionsClient.getBulk(actionIds); - const actionTypeIds = [...new Set(actionResults.map((action) => action.actionTypeId))]; - actionTypeIds.forEach((id) => { - // Notify action type usage via "isActionTypeEnabled" function - actionsClient.isActionTypeEnabled(id, { notifyUsage: true }); - }); - alertActions.forEach(({ id, ...alertAction }, i) => { - const actionResultValue = actionResults.find((action) => action.id === id); - if (actionResultValue) { - if (actionsClient.isPreconfigured(id)) { - actions.push({ - ...alertAction, - actionRef: `${preconfiguredConnectorActionRefPrefix}${id}`, - actionTypeId: actionResultValue.actionTypeId, - }); - } else { - const actionRef = `action_${i}`; - references.push({ - id, - name: actionRef, - type: 'action', - }); - actions.push({ - ...alertAction, - actionRef, - actionTypeId: actionResultValue.actionTypeId, - }); - } - } else { - actions.push({ - ...alertAction, - actionRef: '', - actionTypeId: '', - }); - } - }); - } - return { - actions, - references, - }; - } - - private includeFieldsRequiredForAuthentication(fields: string[]): string[] { - return uniq([...fields, 'alertTypeId', 'consumer']); - } - - private generateAPIKeyName(alertTypeId: string, alertName: string) { - return truncate(`Alerting: ${alertTypeId}/${trim(alertName)}`, { length: 256 }); - } - - private updateMeta>(alertAttributes: T): T { - if (alertAttributes.hasOwnProperty('apiKey') || alertAttributes.hasOwnProperty('apiKeyOwner')) { - alertAttributes.meta = alertAttributes.meta ?? {}; - alertAttributes.meta.versionApiKeyLastmodified = this.kibanaVersion; - } - return alertAttributes; - } -} - -function parseDate(dateString: string | undefined, propertyName: string, defaultValue: Date): Date { - if (dateString === undefined) { - return defaultValue; - } - - const parsedDate = parseIsoOrRelativeDate(dateString); - if (parsedDate === undefined) { - throw Boom.badRequest( - i18n.translate('xpack.alerting.rulesClient.invalidDate', { - defaultMessage: 'Invalid date for parameter {field}: "{dateValue}"', - values: { - field: propertyName, - dateValue: dateString, - }, - }) - ); - } - - return parsedDate; -} - -function getSnoozeAttributes(attributes: RawRule, snoozeSchedule: RuleSnoozeSchedule) { - // If duration is -1, instead mute all - const { id: snoozeId, duration } = snoozeSchedule; - - if (duration === -1) { - return { - muteAll: true, - snoozeSchedule: clearUnscheduledSnooze(attributes), - }; - } - return { - snoozeSchedule: (snoozeId - ? clearScheduledSnoozesById(attributes, [snoozeId]) - : clearUnscheduledSnooze(attributes) - ).concat(snoozeSchedule), - muteAll: false, - }; -} - -function getBulkSnoozeAttributes(attributes: RawRule, snoozeSchedule: RuleSnoozeSchedule) { - // If duration is -1, instead mute all - const { id: snoozeId, duration } = snoozeSchedule; - - if (duration === -1) { - return { - muteAll: true, - snoozeSchedule: clearUnscheduledSnooze(attributes), - }; - } - - // Bulk adding snooze schedule, don't touch the existing snooze/indefinite snooze - if (snoozeId) { - const existingSnoozeSchedules = attributes.snoozeSchedule || []; - return { - muteAll: attributes.muteAll, - snoozeSchedule: [...existingSnoozeSchedules, snoozeSchedule], - }; - } - - // Bulk snoozing, don't touch the existing snooze schedules - return { - muteAll: false, - snoozeSchedule: [...clearUnscheduledSnooze(attributes), snoozeSchedule], - }; -} - -function getUnsnoozeAttributes(attributes: RawRule, scheduleIds?: string[]) { - const snoozeSchedule = scheduleIds - ? clearScheduledSnoozesById(attributes, scheduleIds) - : clearCurrentActiveSnooze(attributes); - - return { - snoozeSchedule, - ...(!scheduleIds ? { muteAll: false } : {}), - }; -} - -function getBulkUnsnoozeAttributes(attributes: RawRule, scheduleIds?: string[]) { - // Bulk removing snooze schedules, don't touch the current snooze/indefinite snooze - if (scheduleIds) { - const newSchedules = clearScheduledSnoozesById(attributes, scheduleIds); - // Unscheduled snooze is also known as snooze now - const unscheduledSnooze = - attributes.snoozeSchedule?.filter((s) => typeof s.id === 'undefined') || []; - - return { - snoozeSchedule: [...unscheduledSnooze, ...newSchedules], - muteAll: attributes.muteAll, - }; - } - - // Bulk unsnoozing, don't touch current snooze schedules that are NOT active - return { - snoozeSchedule: clearCurrentActiveSnooze(attributes), - muteAll: false, - }; -} - -function clearUnscheduledSnooze(attributes: RawRule) { - // Clear any snoozes that have no ID property. These are "simple" snoozes created with the quick UI, e.g. snooze for 3 days starting now - return attributes.snoozeSchedule - ? attributes.snoozeSchedule.filter((s) => typeof s.id !== 'undefined') - : []; -} - -function clearScheduledSnoozesById(attributes: RawRule, ids: string[]) { - return attributes.snoozeSchedule - ? attributes.snoozeSchedule.filter((s) => s.id && !ids.includes(s.id)) - : []; -} - -function clearCurrentActiveSnooze(attributes: RawRule) { - // First attempt to cancel a simple (unscheduled) snooze - const clearedUnscheduledSnoozes = clearUnscheduledSnooze(attributes); - // Now clear any scheduled snoozes that are currently active and never recur - const activeSnoozes = getActiveScheduledSnoozes(attributes); - const activeSnoozeIds = activeSnoozes?.map((s) => s.id) ?? []; - const recurringSnoozesToSkip: string[] = []; - const clearedNonRecurringActiveSnoozes = clearedUnscheduledSnoozes.filter((s) => { - if (!activeSnoozeIds.includes(s.id!)) return true; - // Check if this is a recurring snooze, and return true if so - if (s.rRule.freq && s.rRule.count !== 1) { - recurringSnoozesToSkip.push(s.id!); - return true; - } - }); - const clearedSnoozesAndSkippedRecurringSnoozes = clearedNonRecurringActiveSnoozes.map((s) => { - if (s.id && !recurringSnoozesToSkip.includes(s.id)) return s; - const currentRecurrence = activeSnoozes?.find((a) => a.id === s.id)?.lastOccurrence; - if (!currentRecurrence) return s; - return { - ...s, - skipRecurrences: (s.skipRecurrences ?? []).concat(currentRecurrence.toISOString()), - }; - }); - return clearedSnoozesAndSkippedRecurringSnoozes; -} - -function verifySnoozeScheduleLimit(attributes: Partial) { - const schedules = attributes.snoozeSchedule?.filter((snooze) => snooze.id); - if (schedules && schedules.length > 5) { - throw Error( - i18n.translate('xpack.alerting.rulesClient.snoozeSchedule.limitReached', { - defaultMessage: 'Rule cannot have more than 5 snooze schedules', - }) - ); + return this.context.spaceId; } } diff --git a/x-pack/plugins/alerting/server/rules_client/tests/create.test.ts b/x-pack/plugins/alerting/server/rules_client/tests/create.test.ts index 4d9b84e53a2f4..6205b4bb4ba6a 100644 --- a/x-pack/plugins/alerting/server/rules_client/tests/create.test.ts +++ b/x-pack/plugins/alerting/server/rules_client/tests/create.test.ts @@ -6,7 +6,8 @@ */ import { schema } from '@kbn/config-schema'; -import { RulesClient, ConstructorOptions, CreateOptions } from '../rules_client'; +import { CreateOptions } from '../methods/create'; +import { RulesClient, ConstructorOptions } from '../rules_client'; import { savedObjectsClientMock, loggingSystemMock } from '@kbn/core/server/mocks'; import { taskManagerMock } from '@kbn/task-manager-plugin/server/mocks'; import { ruleTypeRegistryMock } from '../../rule_type_registry.mock'; diff --git a/x-pack/plugins/alerting/server/rules_client/tests/find.test.ts b/x-pack/plugins/alerting/server/rules_client/tests/find.test.ts index 4cfa13f69b84d..1b65d27c8e72e 100644 --- a/x-pack/plugins/alerting/server/rules_client/tests/find.test.ts +++ b/x-pack/plugins/alerting/server/rules_client/tests/find.test.ts @@ -54,7 +54,7 @@ beforeEach(() => { setGlobalDate(); -jest.mock('../lib/map_sort_field', () => ({ +jest.mock('../common/map_sort_field', () => ({ mapSortField: jest.fn(), })); @@ -288,7 +288,7 @@ describe('find()', () => { test('calls mapSortField', async () => { const rulesClient = new RulesClient(rulesClientParams); await rulesClient.find({ options: { sortField: 'name' } }); - expect(jest.requireMock('../lib/map_sort_field').mapSortField).toHaveBeenCalledWith('name'); + expect(jest.requireMock('../common/map_sort_field').mapSortField).toHaveBeenCalledWith('name'); }); test('should translate filter/sort/search on params to mapped_params', async () => { diff --git a/x-pack/plugins/alerting/server/rules_client/tests/get_action_error_log.test.ts b/x-pack/plugins/alerting/server/rules_client/tests/get_action_error_log.test.ts index 6b635abe5d7f0..672281469d18b 100644 --- a/x-pack/plugins/alerting/server/rules_client/tests/get_action_error_log.test.ts +++ b/x-pack/plugins/alerting/server/rules_client/tests/get_action_error_log.test.ts @@ -5,7 +5,8 @@ * 2.0. */ -import { RulesClient, ConstructorOptions, GetActionErrorLogByIdParams } from '../rules_client'; +import { RulesClient, ConstructorOptions } from '../rules_client'; +import { GetActionErrorLogByIdParams } from '../methods/get_action_error_log'; import { savedObjectsClientMock, loggingSystemMock } from '@kbn/core/server/mocks'; import { taskManagerMock } from '@kbn/task-manager-plugin/server/mocks'; import { fromKueryExpression } from '@kbn/es-query'; diff --git a/x-pack/plugins/alerting/server/rules_client/types.ts b/x-pack/plugins/alerting/server/rules_client/types.ts new file mode 100644 index 0000000000000..ff59b5527f902 --- /dev/null +++ b/x-pack/plugins/alerting/server/rules_client/types.ts @@ -0,0 +1,148 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { KueryNode } from '@kbn/es-query'; +import { Logger, SavedObjectsClientContract, PluginInitializerContext } from '@kbn/core/server'; +import { ActionsClient, ActionsAuthorization } from '@kbn/actions-plugin/server'; +import { + GrantAPIKeyResult as SecurityPluginGrantAPIKeyResult, + InvalidateAPIKeyResult as SecurityPluginInvalidateAPIKeyResult, +} from '@kbn/security-plugin/server'; +import { EncryptedSavedObjectsClient } from '@kbn/encrypted-saved-objects-plugin/server'; +import { TaskManagerStartContract } from '@kbn/task-manager-plugin/server'; +import { IEventLogClient, IEventLogger } from '@kbn/event-log-plugin/server'; +import { AuditLogger } from '@kbn/security-plugin/server'; +import { RegistryRuleType } from '../rule_type_registry'; +import { + RuleTypeRegistry, + RuleAction, + IntervalSchedule, + SanitizedRule, + RuleSnoozeSchedule, +} from '../types'; +import { AlertingAuthorization } from '../authorization'; +import { AlertingRulesConfig } from '../config'; + +export type { + BulkEditOperation, + BulkEditFields, + BulkEditOptions, + BulkEditOptionsFilter, + BulkEditOptionsIds, +} from './methods/bulk_edit'; +export type { CreateOptions } from './methods/create'; +export type { FindOptions, FindResult } from './methods/find'; +export type { UpdateOptions } from './methods/update'; +export type { AggregateOptions, AggregateResult } from './methods/aggregate'; +export type { GetAlertSummaryParams } from './methods/get_alert_summary'; +export type { + GetExecutionLogByIdParams, + GetGlobalExecutionLogParams, +} from './methods/get_execution_log'; +export type { + GetGlobalExecutionKPIParams, + GetRuleExecutionKPIParams, +} from './methods/get_execution_kpi'; +export type { GetActionErrorLogByIdParams } from './methods/get_action_error_log'; + +export interface RulesClientContext { + readonly logger: Logger; + readonly getUserName: () => Promise; + readonly spaceId: string; + readonly namespace?: string; + readonly taskManager: TaskManagerStartContract; + readonly unsecuredSavedObjectsClient: SavedObjectsClientContract; + readonly authorization: AlertingAuthorization; + readonly ruleTypeRegistry: RuleTypeRegistry; + readonly minimumScheduleInterval: AlertingRulesConfig['minimumScheduleInterval']; + readonly minimumScheduleIntervalInMs: number; + readonly createAPIKey: (name: string) => Promise; + readonly getActionsClient: () => Promise; + readonly actionsAuthorization: ActionsAuthorization; + readonly getEventLogClient: () => Promise; + readonly encryptedSavedObjectsClient: EncryptedSavedObjectsClient; + readonly kibanaVersion: PluginInitializerContext['env']['packageInfo']['version']; + readonly auditLogger?: AuditLogger; + readonly eventLogger?: IEventLogger; + readonly fieldsToExcludeFromPublicApi: Array; +} + +export type NormalizedAlertAction = Omit; + +export interface RegistryAlertTypeWithAuth extends RegistryRuleType { + authorizedConsumers: string[]; +} +export type CreateAPIKeyResult = + | { apiKeysEnabled: false } + | { apiKeysEnabled: true; result: SecurityPluginGrantAPIKeyResult }; +export type InvalidateAPIKeyResult = + | { apiKeysEnabled: false } + | { apiKeysEnabled: true; result: SecurityPluginInvalidateAPIKeyResult }; + +export interface RuleBulkOperationAggregation { + alertTypeId: { + buckets: Array<{ + key: string[]; + doc_count: number; + }>; + }; +} +export interface SavedObjectOptions { + id?: string; + migrationVersion?: Record; +} + +export interface ScheduleTaskOptions { + id: string; + consumer: string; + ruleTypeId: string; + schedule: IntervalSchedule; + throwOnConflict: boolean; // whether to throw conflict errors or swallow them +} + +export interface IndexType { + [key: string]: unknown; +} + +export interface MuteOptions extends IndexType { + alertId: string; + alertInstanceId: string; +} + +export interface SnoozeOptions extends IndexType { + snoozeSchedule: RuleSnoozeSchedule; +} + +export interface BulkOptionsFilter { + filter?: string | KueryNode; +} + +export interface BulkOptionsIds { + ids?: string[]; +} + +export type BulkOptions = BulkOptionsFilter | BulkOptionsIds; + +export interface BulkOperationError { + message: string; + status?: number; + rule: { + id: string; + name: string; + }; +} + +export type BulkAction = 'DELETE' | 'ENABLE' | 'DISABLE'; + +export interface RuleBulkOperationAggregation { + alertTypeId: { + buckets: Array<{ + key: string[]; + doc_count: number; + }>; + }; +} diff --git a/x-pack/plugins/alerting/server/saved_objects/migrations/8.2/index.ts b/x-pack/plugins/alerting/server/saved_objects/migrations/8.2/index.ts index 6de67875ba2eb..9a8967c9556ab 100644 --- a/x-pack/plugins/alerting/server/saved_objects/migrations/8.2/index.ts +++ b/x-pack/plugins/alerting/server/saved_objects/migrations/8.2/index.ts @@ -7,7 +7,7 @@ import { SavedObjectUnsanitizedDoc } from '@kbn/core-saved-objects-server'; import { EncryptedSavedObjectsPluginSetup } from '@kbn/encrypted-saved-objects-plugin/server'; -import { getMappedParams } from '../../../rules_client/lib/mapped_params_utils'; +import { getMappedParams } from '../../../rules_client/common'; import { RawRule } from '../../../types'; import { createEsoMigration, pipeMigrations } from '../utils'; diff --git a/x-pack/plugins/alerting/server/task_runner/fixtures.ts b/x-pack/plugins/alerting/server/task_runner/fixtures.ts index b0e98263591a9..0c6bc7a41af69 100644 --- a/x-pack/plugins/alerting/server/task_runner/fixtures.ts +++ b/x-pack/plugins/alerting/server/task_runner/fixtures.ts @@ -250,6 +250,7 @@ export const generateRunnerResult = ({ state = false, interval = '10s', alertInstances = {}, + alertRecoveredInstances = {}, }: GeneratorParams = {}) => { return { monitoring: { @@ -276,6 +277,7 @@ export const generateRunnerResult = ({ }, state: { ...(state && { alertInstances }), + ...(state && { alertRecoveredInstances }), ...(state && { alertTypeState: undefined }), ...(state && { previousStartedAt: new Date('1970-01-01T00:00:00.000Z') }), }, @@ -311,13 +313,17 @@ export const generateEnqueueFunctionInput = (isArray: boolean = false) => { return isArray ? [input] : input; }; -export const generateAlertInstance = ({ id, duration, start }: GeneratorParams = { id: 1 }) => ({ +export const generateAlertInstance = ( + { id, duration, start, flappingHistory }: GeneratorParams = { id: 1, flappingHistory: [false] } +) => ({ [String(id)]: { meta: { lastScheduledActions: { date: new Date(DATE_1970), group: 'default', }, + flappingHistory, + flapping: false, }, state: { bar: false, diff --git a/x-pack/plugins/alerting/server/task_runner/log_alerts.test.ts b/x-pack/plugins/alerting/server/task_runner/log_alerts.test.ts index 43f191fc0a3aa..163cadf1d084b 100644 --- a/x-pack/plugins/alerting/server/task_runner/log_alerts.test.ts +++ b/x-pack/plugins/alerting/server/task_runner/log_alerts.test.ts @@ -241,4 +241,86 @@ describe('logAlerts', () => { expect(alertingEventLogger.logAlert).not.toHaveBeenCalled(); }); + + test('should correctly set flapping values', () => { + logAlerts({ + logger, + alertingEventLogger, + newAlerts: { + '4': new Alert<{}, {}, DefaultActionGroupId>('4'), + }, + activeAlerts: { + '1': new Alert<{}, {}, DefaultActionGroupId>('1', { meta: { flapping: true } }), + '2': new Alert<{}, {}, DefaultActionGroupId>('2'), + '4': new Alert<{}, {}, DefaultActionGroupId>('4'), + }, + recoveredAlerts: { + '7': new Alert<{}, {}, DefaultActionGroupId>('7'), + '8': new Alert<{}, {}, DefaultActionGroupId>('8', { meta: { flapping: true } }), + '9': new Alert<{}, {}, DefaultActionGroupId>('9'), + '10': new Alert<{}, {}, DefaultActionGroupId>('10'), + }, + ruleLogPrefix: `test-rule-type-id:123: 'test rule'`, + ruleRunMetricsStore, + canSetRecoveryContext: false, + shouldPersistAlerts: true, + }); + + expect(alertingEventLogger.logAlert).toHaveBeenNthCalledWith(1, { + action: 'recovered-instance', + id: '7', + message: "test-rule-type-id:123: 'test rule' alert '7' has recovered", + state: {}, + flapping: false, + }); + expect(alertingEventLogger.logAlert).toHaveBeenNthCalledWith(2, { + action: 'recovered-instance', + id: '8', + message: "test-rule-type-id:123: 'test rule' alert '8' has recovered", + state: {}, + flapping: true, + }); + expect(alertingEventLogger.logAlert).toHaveBeenNthCalledWith(3, { + action: 'recovered-instance', + id: '9', + message: "test-rule-type-id:123: 'test rule' alert '9' has recovered", + state: {}, + flapping: false, + }); + expect(alertingEventLogger.logAlert).toHaveBeenNthCalledWith(4, { + action: 'recovered-instance', + id: '10', + message: "test-rule-type-id:123: 'test rule' alert '10' has recovered", + state: {}, + flapping: false, + }); + expect(alertingEventLogger.logAlert).toHaveBeenNthCalledWith(5, { + action: 'new-instance', + id: '4', + message: "test-rule-type-id:123: 'test rule' created new alert: '4'", + state: {}, + flapping: false, + }); + expect(alertingEventLogger.logAlert).toHaveBeenNthCalledWith(6, { + action: 'active-instance', + id: '1', + message: "test-rule-type-id:123: 'test rule' active alert: '1' in actionGroup: 'undefined'", + state: {}, + flapping: true, + }); + expect(alertingEventLogger.logAlert).toHaveBeenNthCalledWith(7, { + action: 'active-instance', + id: '2', + message: "test-rule-type-id:123: 'test rule' active alert: '2' in actionGroup: 'undefined'", + state: {}, + flapping: false, + }); + expect(alertingEventLogger.logAlert).toHaveBeenNthCalledWith(8, { + action: 'active-instance', + id: '4', + message: "test-rule-type-id:123: 'test rule' active alert: '4' in actionGroup: 'undefined'", + state: {}, + flapping: false, + }); + }); }); diff --git a/x-pack/plugins/alerting/server/task_runner/log_alerts.ts b/x-pack/plugins/alerting/server/task_runner/log_alerts.ts index b7abaf4c236be..7f8f5d93a5d9f 100644 --- a/x-pack/plugins/alerting/server/task_runner/log_alerts.ts +++ b/x-pack/plugins/alerting/server/task_runner/log_alerts.ts @@ -90,19 +90,17 @@ export function logAlerts< ruleRunMetricsStore.setNumberOfNewAlerts(newAlertIds.length); ruleRunMetricsStore.setNumberOfActiveAlerts(activeAlertIds.length); ruleRunMetricsStore.setNumberOfRecoveredAlerts(recoveredAlertIds.length); - for (const id of recoveredAlertIds) { const { group: actionGroup } = recoveredAlerts[id].getLastScheduledActions() ?? {}; const state = recoveredAlerts[id].getState(); const message = `${ruleLogPrefix} alert '${id}' has recovered`; - alertingEventLogger.logAlert({ action: EVENT_LOG_ACTIONS.recoveredInstance, id, group: actionGroup, message, state, - flapping: false, + flapping: recoveredAlerts[id].getFlapping(), }); } @@ -116,7 +114,7 @@ export function logAlerts< group: actionGroup, message, state, - flapping: false, + flapping: activeAlerts[id].getFlapping(), }); } @@ -130,7 +128,7 @@ export function logAlerts< group: actionGroup, message, state, - flapping: false, + flapping: activeAlerts[id].getFlapping(), }); } } diff --git a/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts b/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts index 69b648f099e44..35094f1d8af49 100644 --- a/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts +++ b/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts @@ -1023,7 +1023,12 @@ describe('Task Runner', () => { encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValue(SAVED_OBJECT); const runnerResult = await taskRunner.run(); expect(runnerResult.state.alertInstances).toEqual( - generateAlertInstance({ id: 1, duration: MOCK_DURATION, start: DATE_1969 }) + generateAlertInstance({ + id: 1, + duration: MOCK_DURATION, + start: DATE_1969, + flappingHistory: [false], + }) ); expect(logger.debug).toHaveBeenCalledTimes(7); @@ -1340,7 +1345,13 @@ describe('Task Runner', () => { encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValue(SAVED_OBJECT); const runnerResult = await taskRunner.run(); expect(runnerResult.state.alertInstances).toEqual( - generateAlertInstance({ id: 1, duration: MOCK_DURATION, start: DATE_1969 }) + generateAlertInstance({ + id: 1, + duration: MOCK_DURATION, + start: DATE_1969, + flappingHistory: [false], + flapping: false, + }) ); testAlertingEventLogCalls({ @@ -2409,6 +2420,8 @@ describe('Task Runner', () => { date: new Date(DATE_1970), group: 'default', }, + flappingHistory: [true], + flapping: false, }, state: { duration: '0', @@ -2571,6 +2584,8 @@ describe('Task Runner', () => { date: new Date(DATE_1970), group: 'default', }, + flappingHistory: [true], + flapping: false, }, state: { duration: '0', @@ -2583,6 +2598,8 @@ describe('Task Runner', () => { date: new Date(DATE_1970), group: 'default', }, + flappingHistory: [true], + flapping: false, }, state: { duration: '0', diff --git a/x-pack/plugins/alerting/server/task_runner/task_runner.ts b/x-pack/plugins/alerting/server/task_runner/task_runner.ts index 3fc70537f1bc9..323c885416bef 100644 --- a/x-pack/plugins/alerting/server/task_runner/task_runner.ts +++ b/x-pack/plugins/alerting/server/task_runner/task_runner.ts @@ -23,14 +23,15 @@ import { ruleExecutionStatusToRaw, isRuleSnoozed, processAlerts, + setFlapping, lastRunFromError, getNextRun, + determineAlertsToReturn, } from '../lib'; import { RuleExecutionStatus, RuleExecutionStatusErrorReasons, IntervalSchedule, - RawAlertInstance, RawRuleExecutionStatus, RawRuleMonitoring, RuleTaskState, @@ -233,6 +234,7 @@ export class TaskRunner< params: { alertId: ruleId, spaceId }, state: { alertInstances: alertRawInstances = {}, + alertRecoveredInstances: alertRecoveredRawInstances = {}, alertTypeState: ruleTypeState = {}, previousStartedAt, }, @@ -266,7 +268,7 @@ export class TaskRunner< searchSourceClient, }); - const { updatedRuleTypeState, hasReachedAlertLimit, originalAlerts } = + const { updatedRuleTypeState, hasReachedAlertLimit, originalAlerts, originalRecoveredAlerts } = await this.timer.runWithTimer(TaskRunnerTimerSpan.RuleTypeRun, async () => { for (const id in alertRawInstances) { if (alertRawInstances.hasOwnProperty(id)) { @@ -274,6 +276,13 @@ export class TaskRunner< } } + const recoveredAlerts: Record> = {}; + for (const id in alertRecoveredRawInstances) { + if (alertRecoveredRawInstances.hasOwnProperty(id)) { + recoveredAlerts[id] = new Alert(id, alertRecoveredRawInstances[id]); + } + } + const alertsCopy = cloneDeep(this.alerts); const alertFactory = createAlertFactory< @@ -386,31 +395,40 @@ export class TaskRunner< return { originalAlerts: alertsCopy, + originalRecoveredAlerts: recoveredAlerts, updatedRuleTypeState: updatedState || undefined, hasReachedAlertLimit: alertFactory.hasReachedAlertLimit(), }; }); - const { activeAlerts, recoveredAlerts } = await this.timer.runWithTimer( + const { activeAlerts, recoveredAlerts, currentRecoveredAlerts } = await this.timer.runWithTimer( TaskRunnerTimerSpan.ProcessAlerts, async () => { const { newAlerts: processedAlertsNew, activeAlerts: processedAlertsActive, + currentRecoveredAlerts: processedAlertsRecoveredCurrent, recoveredAlerts: processedAlertsRecovered, } = processAlerts({ alerts: this.alerts, existingAlerts: originalAlerts, + previouslyRecoveredAlerts: originalRecoveredAlerts, hasReachedAlertLimit, alertLimit: this.maxAlerts, + setFlapping: true, }); + setFlapping( + processedAlertsActive, + processedAlertsRecovered + ); + logAlerts({ logger: this.logger, alertingEventLogger: this.alertingEventLogger, newAlerts: processedAlertsNew, activeAlerts: processedAlertsActive, - recoveredAlerts: processedAlertsRecovered, + recoveredAlerts: processedAlertsRecoveredCurrent, ruleLogPrefix: ruleLabel, ruleRunMetricsStore, canSetRecoveryContext: ruleType.doesSetRecoveryContext ?? false, @@ -421,6 +439,7 @@ export class TaskRunner< newAlerts: processedAlertsNew, activeAlerts: processedAlertsActive, recoveredAlerts: processedAlertsRecovered, + currentRecoveredAlerts: processedAlertsRecoveredCurrent, }; } ); @@ -452,21 +471,22 @@ export class TaskRunner< this.countUsageOfActionExecutionAfterRuleCancellation(); } else { await executionHandler.run(activeAlerts); - await executionHandler.run(recoveredAlerts, true); + await executionHandler.run(currentRecoveredAlerts, true); } }); - const alertsToReturn: Record = {}; - for (const id in activeAlerts) { - if (activeAlerts.hasOwnProperty(id)) { - alertsToReturn[id] = activeAlerts[id].toRaw(); - } - } + const { alertsToReturn, recoveredAlertsToReturn } = determineAlertsToReturn< + State, + Context, + ActionGroupIds, + RecoveryActionGroupId + >(activeAlerts, recoveredAlerts); return { metrics: ruleRunMetricsStore.getMetrics(), alertTypeState: updatedRuleTypeState || undefined, alertInstances: alertsToReturn, + alertRecoveredInstances: recoveredAlertsToReturn, }; } diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/e2e/power_user/service_groups/generate_data.ts b/x-pack/plugins/apm/ftr_e2e/cypress/e2e/power_user/service_groups/generate_data.ts new file mode 100644 index 0000000000000..970c605dcf278 --- /dev/null +++ b/x-pack/plugins/apm/ftr_e2e/cypress/e2e/power_user/service_groups/generate_data.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 { apm, timerange } from '@kbn/apm-synthtrace'; + +export function generateData({ from, to }: { from: number; to: number }) { + const range = timerange(from, to); + const synthGo1 = apm + .service({ + name: 'synth-go-1', + environment: 'production', + agentName: 'go', + }) + .instance('my-instance'); + const synthGo2 = apm + .service({ name: 'synth-go-2', environment: 'production', agentName: 'go' }) + .instance('my-instance'); + const synthNode = apm + .service({ + name: 'synth-node-1', + environment: 'production', + agentName: 'nodejs', + }) + .instance('my-instance'); + + return range.interval('1m').generator((timestamp) => { + return [ + synthGo1 + .transaction({ transactionName: 'GET /apple 🍎' }) + .timestamp(timestamp) + .duration(1000) + .success(), + synthGo2 + .transaction({ transactionName: 'GET /banana 🍌' }) + .timestamp(timestamp) + .duration(1000) + .success(), + synthNode + .transaction({ transactionName: 'GET /apple 🍎' }) + .timestamp(timestamp) + .duration(1000) + .success(), + ]; + }); +} diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/e2e/power_user/service_groups/service_groups.cy.ts b/x-pack/plugins/apm/ftr_e2e/cypress/e2e/power_user/service_groups/service_groups.cy.ts new file mode 100644 index 0000000000000..a83766389a1f2 --- /dev/null +++ b/x-pack/plugins/apm/ftr_e2e/cypress/e2e/power_user/service_groups/service_groups.cy.ts @@ -0,0 +1,115 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import url from 'url'; +import { synthtrace } from '../../../../synthtrace'; +import { generateData } from './generate_data'; + +const start = Date.now() - 1000; +const end = Date.now(); + +const serviceGroupsHref = url.format({ + pathname: 'app/apm/service-groups', + query: { + rangeFrom: new Date(start).toISOString(), + rangeTo: new Date(end).toISOString(), + }, +}); + +const deleteAllServiceGroups = () => { + const kibanaUrl = Cypress.env('KIBANA_URL'); + cy.request({ + log: false, + method: 'GET', + url: `${kibanaUrl}/internal/apm/service-groups`, + body: {}, + headers: { + 'kbn-xsrf': 'e2e_test', + }, + auth: { user: 'editor', pass: 'changeme' }, + }).then((response) => { + const promises = response.body.serviceGroups.map((item: any) => { + if (item.id) { + return cy.request({ + log: false, + method: 'DELETE', + url: `${kibanaUrl}/internal/apm/service-group?serviceGroupId=${item.id}`, + headers: { + 'kbn-xsrf': 'e2e_test', + }, + auth: { user: 'editor', pass: 'changeme' }, + }); + } + }); + return Promise.all(promises); + }); +}; + +describe('Service groups', () => { + before(() => { + synthtrace.index( + generateData({ + from: new Date(start).getTime(), + to: new Date(end).getTime(), + }) + ); + }); + + after(() => { + synthtrace.clean(); + }); + + describe('When navigating to service groups', () => { + beforeEach(() => { + cy.loginAsEditorUser(); + cy.visitKibana(serviceGroupsHref); + }); + + after(() => { + deleteAllServiceGroups(); + }); + + describe('when there are not service groups', () => { + before(() => { + deleteAllServiceGroups(); + }); + it('shows no service groups', () => { + cy.contains('h2', 'No service groups'); + }); + + it('creates a service group', () => { + cy.getByTestSubj('apmCreateServiceGroupButton').click(); + cy.getByTestSubj('apmGroupNameInput').type('go services'); + cy.contains('Select services').click(); + cy.getByTestSubj('headerFilterKuerybar').type('agent.name:"go"{enter}'); + cy.contains('synth-go-1'); + cy.contains('synth-go-2'); + cy.contains('Save group').click(); + }); + }); + + describe('when there are service groups', () => { + it('shows service groups', () => { + cy.contains('1 group'); + cy.getByTestSubj('serviceGroupCard') + .should('contain', 'go services') + .should('contain', '2 services'); + }); + it('opens service list when click in service group card', () => { + cy.getByTestSubj('serviceGroupCard').click(); + cy.contains('go services'); + cy.contains('synth-go-1'); + cy.contains('synth-go-2'); + }); + it('deletes service group', () => { + cy.getByTestSubj('serviceGroupCard').click(); + cy.get('button:contains(Edit group)').click(); + cy.getByTestSubj('apmDeleteGroupButton').click(); + cy.contains('h2', 'No service groups'); + }); + }); + }); +}); diff --git a/x-pack/plugins/apm/public/components/alerting/rule_types/register_apm_rule_types.ts b/x-pack/plugins/apm/public/components/alerting/rule_types/register_apm_rule_types.ts index 4b4cc42e08089..ed9971307bf64 100644 --- a/x-pack/plugins/apm/public/components/alerting/rule_types/register_apm_rule_types.ts +++ b/x-pack/plugins/apm/public/components/alerting/rule_types/register_apm_rule_types.ts @@ -33,7 +33,8 @@ export function registerApmRuleTypes( return { reason: fields[ALERT_REASON]!, link: getAlertUrlErrorCount( - String(fields[SERVICE_NAME][0]!), + // TODO:fix SERVICE_NAME when we move it to initializeIndex + String(fields[SERVICE_NAME]![0]), fields[SERVICE_ENVIRONMENT] && String(fields[SERVICE_ENVIRONMENT][0]) ), }; @@ -46,6 +47,12 @@ export function registerApmRuleTypes( validate: () => ({ errors: [], }), + alertDetailsAppSection: lazy( + () => + import( + '../ui_components/alert_details_app_section/alert_details_app_section' + ) + ), requiresAppContext: false, defaultActionMessage: i18n.translate( 'xpack.apm.alertTypes.errorCount.defaultActionMessage', @@ -73,9 +80,10 @@ export function registerApmRuleTypes( return { reason: fields[ALERT_REASON]!, link: getAlertUrlTransaction( - String(fields[SERVICE_NAME][0]!), + // TODO:fix SERVICE_NAME when we move it to initializeIndex + String(fields[SERVICE_NAME]![0]), fields[SERVICE_ENVIRONMENT] && String(fields[SERVICE_ENVIRONMENT][0]), - String(fields[TRANSACTION_TYPE][0]!) + String(fields[TRANSACTION_TYPE]![0]) ), }; }, @@ -89,6 +97,12 @@ export function registerApmRuleTypes( validate: () => ({ errors: [], }), + alertDetailsAppSection: lazy( + () => + import( + '../ui_components/alert_details_app_section/alert_details_app_section' + ) + ), requiresAppContext: false, defaultActionMessage: i18n.translate( 'xpack.apm.alertTypes.transactionDuration.defaultActionMessage', @@ -116,9 +130,10 @@ export function registerApmRuleTypes( format: ({ fields, formatters: { asPercent } }) => ({ reason: fields[ALERT_REASON]!, link: getAlertUrlTransaction( - String(fields[SERVICE_NAME][0]!), + // TODO:fix SERVICE_NAME when we move it to initializeIndex + String(fields[SERVICE_NAME]![0]), fields[SERVICE_ENVIRONMENT] && String(fields[SERVICE_ENVIRONMENT][0]), - String(fields[TRANSACTION_TYPE][0]!) + String(fields[TRANSACTION_TYPE]![0]) ), }), iconClass: 'bell', @@ -131,6 +146,12 @@ export function registerApmRuleTypes( validate: () => ({ errors: [], }), + alertDetailsAppSection: lazy( + () => + import( + '../ui_components/alert_details_app_section/alert_details_app_section' + ) + ), requiresAppContext: false, defaultActionMessage: i18n.translate( 'xpack.apm.alertTypes.transactionErrorRate.defaultActionMessage', @@ -155,9 +176,10 @@ export function registerApmRuleTypes( format: ({ fields }) => ({ reason: fields[ALERT_REASON]!, link: getAlertUrlTransaction( - String(fields[SERVICE_NAME][0]!), + // TODO:fix SERVICE_NAME when we move it to initializeIndex + String(fields[SERVICE_NAME]![0]), fields[SERVICE_ENVIRONMENT] && String(fields[SERVICE_ENVIRONMENT][0]), - String(fields[TRANSACTION_TYPE][0]!) + String(fields[TRANSACTION_TYPE]![0]) ), }), iconClass: 'bell', @@ -170,6 +192,12 @@ export function registerApmRuleTypes( validate: () => ({ errors: [], }), + alertDetailsAppSection: lazy( + () => + import( + '../ui_components/alert_details_app_section/alert_details_app_section' + ) + ), requiresAppContext: false, defaultActionMessage: i18n.translate( 'xpack.apm.alertTypes.transactionDurationAnomaly.defaultActionMessage', diff --git a/x-pack/plugins/apm/public/components/alerting/ui_components/alert_details_app_section/alert_details_app_section.tsx b/x-pack/plugins/apm/public/components/alerting/ui_components/alert_details_app_section/alert_details_app_section.tsx new file mode 100644 index 0000000000000..92cd229333062 --- /dev/null +++ b/x-pack/plugins/apm/public/components/alerting/ui_components/alert_details_app_section/alert_details_app_section.tsx @@ -0,0 +1,419 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useMemo } from 'react'; +import { EuiFlexGroup } from '@elastic/eui'; +import { EuiFlexItem } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { EuiPanel } from '@elastic/eui'; +import { EuiTitle } from '@elastic/eui'; +import { EuiIconTip } from '@elastic/eui'; +import { ALERT_DURATION, ALERT_END } from '@kbn/rule-data-utils'; +import moment from 'moment'; +import { ENVIRONMENT_ALL } from '../../../../../common/environment_filter_values'; +import { getTransactionType } from '../../../../context/apm_service/apm_service_context'; +import { useServiceAgentFetcher } from '../../../../context/apm_service/use_service_agent_fetcher'; +import { useServiceTransactionTypesFetcher } from '../../../../context/apm_service/use_service_transaction_types_fetcher'; +import { asPercent } from '../../../../../common/utils/formatters'; +import { APIReturnType } from '../../../../services/rest/create_call_apm_api'; +import { getDurationFormatter } from '../../../../../common/utils/formatters/duration'; +import { useFetcher } from '../../../../hooks/use_fetcher'; +import { useTimeRange } from '../../../../hooks/use_time_range'; +import { isTimeComparison } from '../../../shared/time_comparison/get_comparison_options'; +import { getComparisonChartTheme } from '../../../shared/time_comparison/get_comparison_chart_theme'; +import { getLatencyChartSelector } from '../../../../selectors/latency_chart_selectors'; +import { TimeseriesChart } from '../../../shared/charts/timeseries_chart'; +import { + getMaxY, + getResponseTimeTickFormatter, +} from '../../../shared/charts/transaction_charts/helper'; +import { ChartPointerEventContextProvider } from '../../../../context/chart_pointer_event/chart_pointer_event_context'; +import { + ChartType, + getTimeSeriesColor, +} from '../../../shared/charts/helper/get_timeseries_color'; +import { + AlertDetailsAppSectionProps, + SERVICE_NAME, + TRANSACTION_TYPE, +} from './types'; +import { getAggsTypeFromRule } from './helpers'; +import { filterNil } from '../../../shared/charts/latency_chart'; +import { errorRateI18n } from '../../../shared/charts/failed_transaction_rate_chart'; + +export function AlertDetailsAppSection({ + rule, + alert, + timeZone, +}: AlertDetailsAppSectionProps) { + const params = rule.params; + const environment = String(params.environment) || ENVIRONMENT_ALL.value; + const latencyAggregationType = getAggsTypeFromRule( + params.aggregationType as string + ); + + // duration is us, convert it to MS + const alertDurationMS = alert.fields[ALERT_DURATION]! / 1000; + + const serviceName = String(alert.fields[SERVICE_NAME]); + + // Currently, we don't use comparisonEnabled nor offset. + // But providing them as they are required for the chart. + const comparisonEnabled = false; + const offset = '1d'; + const ruleWindowSizeMS = moment + .duration(rule.params.windowSize, rule.params.windowUnit) + .asMilliseconds(); + + const TWENTY_TIMES_RULE_WINDOW_MS = 20 * ruleWindowSizeMS; + /** + * This is part or the requirements (RFC). + * If the alert is less than 20 units of `FOR THE LAST ` then we should draw a time range of 20 units. + * IE. The user set "FOR THE LAST 5 minutes" at a minimum we should show 100 minutes. + */ + const rangeFrom = + alertDurationMS < TWENTY_TIMES_RULE_WINDOW_MS + ? moment(alert.start) + .subtract(TWENTY_TIMES_RULE_WINDOW_MS, 'millisecond') + .toISOString() + : moment(alert.start) + .subtract(ruleWindowSizeMS, 'millisecond') + .toISOString(); + + const rangeTo = alert.active + ? 'now' + : moment(alert.fields[ALERT_END]) + .add(ruleWindowSizeMS, 'millisecond') + .toISOString(); + + const { start, end } = useTimeRange({ rangeFrom, rangeTo }); + const { agentName } = useServiceAgentFetcher({ + serviceName, + start, + end, + }); + const transactionTypes = useServiceTransactionTypesFetcher({ + serviceName, + start, + end, + }); + + const transactionType = getTransactionType({ + transactionType: String(alert.fields[TRANSACTION_TYPE]), + transactionTypes, + agentName, + }); + + const comparisonChartTheme = getComparisonChartTheme(); + const INITIAL_STATE = { + currentPeriod: [], + previousPeriod: [], + }; + + /* Latency Chart */ + const { data, status } = useFetcher( + (callApmApi) => { + if ( + serviceName && + start && + end && + transactionType && + latencyAggregationType + ) { + return callApmApi( + `GET /internal/apm/services/{serviceName}/transactions/charts/latency`, + { + params: { + path: { serviceName }, + query: { + environment, + kuery: '', + start, + end, + transactionType, + transactionName: undefined, + latencyAggregationType, + }, + }, + } + ); + } + }, + [ + end, + environment, + latencyAggregationType, + serviceName, + start, + transactionType, + ] + ); + + const memoizedData = useMemo( + () => + getLatencyChartSelector({ + latencyChart: data, + latencyAggregationType, + previousPeriodLabel: '', + }), + // It should only update when the data has changed + // eslint-disable-next-line react-hooks/exhaustive-deps + [data] + ); + const { currentPeriod, previousPeriod } = memoizedData; + + const timeseriesLatency = [ + currentPeriod, + comparisonEnabled && isTimeComparison(offset) ? previousPeriod : undefined, + ].filter(filterNil); + + const latencyMaxY = getMaxY(timeseriesLatency); + const latencyFormatter = getDurationFormatter(latencyMaxY); + + /* Latency Chart */ + + /* Throughput Chart */ + const { data: dataThroughput = INITIAL_STATE, status: statusThroughput } = + useFetcher( + (callApmApi) => { + if (serviceName && transactionType && start && end) { + return callApmApi( + 'GET /internal/apm/services/{serviceName}/throughput', + { + params: { + path: { + serviceName, + }, + query: { + environment, + kuery: '', + start, + end, + transactionType, + transactionName: undefined, + }, + }, + } + ); + } + }, + [environment, serviceName, start, end, transactionType] + ); + const { currentPeriodColor, previousPeriodColor } = getTimeSeriesColor( + ChartType.THROUGHPUT + ); + const timeseriesThroughput = [ + { + data: dataThroughput.currentPeriod, + type: 'linemark', + color: currentPeriodColor, + title: i18n.translate('xpack.apm.serviceOverview.throughtputChartTitle', { + defaultMessage: 'Throughput', + }), + }, + ...(comparisonEnabled + ? [ + { + data: dataThroughput.previousPeriod, + type: 'area', + color: previousPeriodColor, + title: '', + }, + ] + : []), + ]; + + /* Throughput Chart */ + + /* Error Rate */ + type ErrorRate = + APIReturnType<'GET /internal/apm/services/{serviceName}/transactions/charts/error_rate'>; + + const INITIAL_STATE_ERROR_RATE: ErrorRate = { + currentPeriod: { + timeseries: [], + average: null, + }, + previousPeriod: { + timeseries: [], + average: null, + }, + }; + function yLabelFormat(y?: number | null) { + return asPercent(y || 0, 1); + } + + const { + data: dataErrorRate = INITIAL_STATE_ERROR_RATE, + status: statusErrorRate, + } = useFetcher( + (callApmApi) => { + if (transactionType && serviceName && start && end) { + return callApmApi( + 'GET /internal/apm/services/{serviceName}/transactions/charts/error_rate', + { + params: { + path: { + serviceName, + }, + query: { + environment, + kuery: '', + start, + end, + transactionType, + transactionName: undefined, + }, + }, + } + ); + } + }, + [environment, serviceName, start, end, transactionType] + ); + + const { currentPeriodColor: currentPeriodColorErrorRate } = + getTimeSeriesColor(ChartType.FAILED_TRANSACTION_RATE); + + const timeseriesErrorRate = [ + { + data: dataErrorRate.currentPeriod.timeseries, + type: 'linemark', + color: currentPeriodColorErrorRate, + title: i18n.translate('xpack.apm.errorRate.chart.errorRate', { + defaultMessage: 'Failed transaction rate (avg.)', + }), + }, + ]; + + /* Error Rate */ + + return ( + + + + + + + +

    + {i18n.translate( + 'xpack.apm.dependencyLatencyChart.chartTitle', + { + defaultMessage: 'Latency', + } + )} +

    +
    +
    +
    + +
    +
    + + + + + + + + +

    + {i18n.translate( + 'xpack.apm.serviceOverview.throughtputChartTitle', + { defaultMessage: 'Throughput' } + )} +

    +
    +
    + + + + +
    + + +
    +
    + + + + + +

    + {i18n.translate('xpack.apm.errorRate', { + defaultMessage: 'Failed transaction rate', + })} +

    +
    +
    + + + + +
    + + +
    +
    +
    +
    +
    +
    + ); +} + +// eslint-disable-next-line import/no-default-export +export default AlertDetailsAppSection; diff --git a/x-pack/plugins/apm/public/components/alerting/ui_components/alert_details_app_section/helpers.ts b/x-pack/plugins/apm/public/components/alerting/ui_components/alert_details_app_section/helpers.ts new file mode 100644 index 0000000000000..a095f8caa4574 --- /dev/null +++ b/x-pack/plugins/apm/public/components/alerting/ui_components/alert_details_app_section/helpers.ts @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { LatencyAggregationType } from '../../../../../common/latency_aggregation_types'; + +export const getAggsTypeFromRule = ( + ruleAggType: string +): LatencyAggregationType => { + if (ruleAggType === '95th') return LatencyAggregationType.p95; + if (ruleAggType === '99th') return LatencyAggregationType.p99; + return LatencyAggregationType.avg; +}; diff --git a/x-pack/plugins/apm/public/components/alerting/ui_components/alert_details_app_section/types.ts b/x-pack/plugins/apm/public/components/alerting/ui_components/alert_details_app_section/types.ts new file mode 100644 index 0000000000000..0094d9332009a --- /dev/null +++ b/x-pack/plugins/apm/public/components/alerting/ui_components/alert_details_app_section/types.ts @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { Rule } from '@kbn/alerting-plugin/common'; +import { TopAlert } from '@kbn/observability-plugin/public/pages/alerts'; +import { TIME_UNITS } from '@kbn/triggers-actions-ui-plugin/public'; +import { LatencyAggregationType } from '../../../../../common/latency_aggregation_types'; + +export const SERVICE_NAME = 'service.name' as const; +export const TRANSACTION_TYPE = 'transaction.type' as const; +export interface AlertDetailsAppSectionProps { + rule: Rule<{ + environment: string; + aggregationType: LatencyAggregationType; + windowSize: number; + windowUnit: TIME_UNITS; + }>; + alert: TopAlert<{ [SERVICE_NAME]: string; [TRANSACTION_TYPE]: string }>; + timeZone: string; +} diff --git a/x-pack/plugins/apm/public/components/app/dependency_operation_detail_view/dependency_operation_detail_trace_list.tsx b/x-pack/plugins/apm/public/components/app/dependency_operation_detail_view/dependency_operation_detail_trace_list.tsx deleted file mode 100644 index 4dde4af56ccf3..0000000000000 --- a/x-pack/plugins/apm/public/components/app/dependency_operation_detail_view/dependency_operation_detail_trace_list.tsx +++ /dev/null @@ -1,273 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ -import { - EuiBadge, - EuiFlexGroup, - EuiFlexItem, - EuiLink, - EuiRadio, - EuiText, - EuiTitle, - RIGHT_ALIGNMENT, -} from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import React from 'react'; -import { useHistory } from 'react-router-dom'; -import { ValuesType } from 'utility-types'; -import { EventOutcome } from '../../../../common/event_outcome'; -import { asMillisecondDuration } from '../../../../common/utils/formatters'; -import { useApmParams } from '../../../hooks/use_apm_params'; -import { useApmRouter } from '../../../hooks/use_apm_router'; -import { FetcherResult, FETCH_STATUS } from '../../../hooks/use_fetcher'; -import { useTheme } from '../../../hooks/use_theme'; -import { APIReturnType } from '../../../services/rest/create_call_apm_api'; -import { push } from '../../shared/links/url_helpers'; -import { - ITableColumn, - ManagedTable, - SortFunction, -} from '../../shared/managed_table'; -import { ServiceLink } from '../../shared/service_link'; -import { TimestampTooltip } from '../../shared/timestamp_tooltip'; - -type DependencySpan = ValuesType< - APIReturnType<'GET /internal/apm/dependencies/operations/spans'>['spans'] ->; - -export function DependencyOperationDetailTraceList({ - spanFetch, - sortFn, -}: { - spanFetch: FetcherResult< - APIReturnType<'GET /internal/apm/dependencies/operations/spans'> - >; - sortFn: SortFunction; -}) { - const router = useApmRouter(); - - const history = useHistory(); - - const theme = useTheme(); - - const { - query: { - comparisonEnabled, - environment, - offset, - rangeFrom, - rangeTo, - refreshInterval, - refreshPaused, - kuery, - sortField = '@timestamp', - sortDirection = 'desc', - pageSize = 10, - page = 1, - spanId, - }, - } = useApmParams('/dependencies/operation'); - - function getTraceLink({ - transactionName, - transactionType, - traceId, - transactionId, - serviceName, - }: { - serviceName: string; - transactionName?: string; - transactionType?: string; - traceId: string; - transactionId?: string; - }) { - const href = transactionName - ? router.link('/services/{serviceName}/transactions/view', { - path: { serviceName }, - query: { - comparisonEnabled, - environment, - kuery, - rangeFrom, - rangeTo, - serviceGroup: '', - transactionName, - refreshInterval, - refreshPaused, - offset, - traceId, - transactionId, - transactionType, - showCriticalPath: false, - }, - }) - : router.link('/link-to/trace/{traceId}', { - path: { - traceId, - }, - query: { - rangeFrom, - rangeTo, - }, - }); - - return href; - } - - const columns: Array> = [ - { - name: '', - field: 'spanId', - render: (_, { spanId: itemSpanId }) => { - return ( - { - push(history, { - query: { spanId: value ? itemSpanId : '' }, - }); - }} - checked={itemSpanId === spanId} - /> - ); - }, - }, - { - name: i18n.translate( - 'xpack.apm.dependencyOperationDetailTraceListOutcomeColumn', - { defaultMessage: 'Outcome' } - ), - field: 'outcome', - render: (_, { outcome }) => { - let color: string; - if (outcome === EventOutcome.success) { - color = theme.eui.euiColorSuccess; - } else if (outcome === EventOutcome.failure) { - color = theme.eui.euiColorDanger; - } else { - color = theme.eui.euiColorMediumShade; - } - - return {outcome}; - }, - }, - { - name: i18n.translate( - 'xpack.apm.dependencyOperationDetailTraceListServiceNameColumn', - { defaultMessage: 'Originating service' } - ), - field: 'serviceName', - truncateText: true, - render: (_, { serviceName, agentName }) => { - const serviceLinkQuery = { - comparisonEnabled, - environment, - kuery, - rangeFrom, - rangeTo, - serviceGroup: '', - refreshInterval, - refreshPaused, - offset, - }; - - return ( - - ); - }, - sortable: true, - }, - { - name: i18n.translate( - 'xpack.apm.dependencyOperationDetailTraceListTransactionNameColumn', - { defaultMessage: 'Transaction name' } - ), - field: 'transactionName', - truncateText: true, - width: '60%', - render: ( - _, - { - serviceName, - transactionName, - traceId, - transactionId, - transactionType, - } - ) => { - const href = getTraceLink({ - serviceName, - transactionName, - traceId, - transactionId, - transactionType, - }); - - return {transactionName || traceId}; - }, - sortable: true, - }, - { - name: i18n.translate( - 'xpack.apm.dependencyOperationDetailTraceListDurationColumn', - { defaultMessage: 'Duration' } - ), - field: 'duration', - render: (_, { duration }) => { - return asMillisecondDuration(duration); - }, - sortable: true, - align: RIGHT_ALIGNMENT, - }, - { - name: i18n.translate( - 'xpack.apm.dependencyOperationDetailTraceListTimestampColumn', - { defaultMessage: 'Timestamp' } - ), - field: '@timestamp', - truncateText: true, - render: (_, { '@timestamp': timestamp }) => { - return ; - }, - sortable: true, - align: RIGHT_ALIGNMENT, - }, - ]; - - return ( - - - - - {i18n.translate('xpack.apm.dependencyOperationDetailTraceList', { - defaultMessage: 'Traces', - })} - - - - - - - - ); -} diff --git a/x-pack/plugins/apm/public/components/app/dependency_operation_detail_view/index.tsx b/x-pack/plugins/apm/public/components/app/dependency_operation_detail_view/index.tsx index 9acd060f5fe68..1a43cbda91773 100644 --- a/x-pack/plugins/apm/public/components/app/dependency_operation_detail_view/index.tsx +++ b/x-pack/plugins/apm/public/components/app/dependency_operation_detail_view/index.tsx @@ -23,7 +23,6 @@ import { push, replace } from '../../shared/links/url_helpers'; import { SortFunction } from '../../shared/managed_table'; import { useWaterfallFetcher } from '../transaction_details/use_waterfall_fetcher'; import { WaterfallWithSummary } from '../transaction_details/waterfall_with_summary'; -import { DependencyOperationDetailTraceList } from './dependency_operation_detail_trace_list'; import { DependencyOperationDistributionChart } from './dependency_operation_distribution_chart'; import { maybeRedirectToAvailableSpanSample } from './maybe_redirect_to_available_span_sample'; @@ -167,14 +166,6 @@ export function DependencyOperationDetailView() {
    - - - - - diff --git a/x-pack/plugins/apm/public/components/app/error_group_details/top_erroneous_transactions/index.tsx b/x-pack/plugins/apm/public/components/app/error_group_details/top_erroneous_transactions/index.tsx index 7b5dce8226aee..2fc31645b5d39 100644 --- a/x-pack/plugins/apm/public/components/app/error_group_details/top_erroneous_transactions/index.tsx +++ b/x-pack/plugins/apm/public/components/app/error_group_details/top_erroneous_transactions/index.tsx @@ -25,7 +25,11 @@ import { isTimeComparison } from '../../../shared/time_comparison/get_comparison import { useApmParams } from '../../../../hooks/use_apm_params'; import { TransactionDetailLink } from '../../../shared/links/apm/transaction_detail_link'; import { TruncateWithTooltip } from '../../../shared/truncate_with_tooltip'; -import { useFetcher, FETCH_STATUS } from '../../../../hooks/use_fetcher'; +import { + useFetcher, + FETCH_STATUS, + isPending, +} from '../../../../hooks/use_fetcher'; import { useTimeRange } from '../../../../hooks/use_time_range'; import { asInteger } from '../../../../../common/utils/formatters'; @@ -90,8 +94,7 @@ export function TopErroneousTransactions({ serviceName }: Props) { ] ); - const loading = - status === FETCH_STATUS.LOADING || status === FETCH_STATUS.NOT_INITIATED; + const loading = isPending(status); const columns: Array< EuiBasicTableColumn< diff --git a/x-pack/plugins/apm/public/components/app/error_group_overview/index.tsx b/x-pack/plugins/apm/public/components/app/error_group_overview/index.tsx index 8e62a5fe7d81c..514759c2b3dfe 100644 --- a/x-pack/plugins/apm/public/components/app/error_group_overview/index.tsx +++ b/x-pack/plugins/apm/public/components/app/error_group_overview/index.tsx @@ -21,7 +21,7 @@ import { useApmServiceContext } from '../../../context/apm_service/use_apm_servi import { ChartPointerEventContextProvider } from '../../../context/chart_pointer_event/chart_pointer_event_context'; import { useApmParams } from '../../../hooks/use_apm_params'; import { useErrorGroupDistributionFetcher } from '../../../hooks/use_error_group_distribution_fetcher'; -import { useFetcher, FETCH_STATUS } from '../../../hooks/use_fetcher'; +import { useFetcher, isPending } from '../../../hooks/use_fetcher'; import { useTimeRange } from '../../../hooks/use_time_range'; import { APIReturnType } from '../../../services/rest/create_call_apm_api'; import { FailedTransactionRateChart } from '../../shared/charts/failed_transaction_rate_chart'; @@ -212,10 +212,9 @@ export function ErrorGroupOverview() { - { setName(e.target.value); @@ -172,6 +173,7 @@ export function GroupDetails({ }} color="danger" isDisabled={isLoading} + data-test-subj="apmDeleteGroupButton" > {i18n.translate( 'xpack.apm.serviceGroups.groupDetailsForm.deleteGroup', diff --git a/x-pack/plugins/apm/public/components/app/service_groups/service_groups_list/index.tsx b/x-pack/plugins/apm/public/components/app/service_groups/service_groups_list/index.tsx index 2ee0224acda13..97a51698cd330 100644 --- a/x-pack/plugins/apm/public/components/app/service_groups/service_groups_list/index.tsx +++ b/x-pack/plugins/apm/public/components/app/service_groups/service_groups_list/index.tsx @@ -17,7 +17,7 @@ import { import { i18n } from '@kbn/i18n'; import { isEmpty, sortBy } from 'lodash'; import React, { useState, useCallback, useMemo } from 'react'; -import { FETCH_STATUS, useFetcher } from '../../../../hooks/use_fetcher'; +import { isPending, useFetcher } from '../../../../hooks/use_fetcher'; import { ServiceGroupsListItems } from './service_groups_list'; import { Sort } from './sort'; import { RefreshServiceGroupsSubscriber } from '../refresh_service_groups_subscriber'; @@ -60,8 +60,7 @@ export function ServiceGroupsList() { [start, end, serviceGroups.length] ); - const isLoading = - status === FETCH_STATUS.NOT_INITIATED || status === FETCH_STATUS.LOADING; + const isLoading = isPending(status); const filteredItems = isEmpty(filter) ? serviceGroups diff --git a/x-pack/plugins/apm/public/components/app/service_groups/service_groups_list/service_group_card.tsx b/x-pack/plugins/apm/public/components/app/service_groups/service_groups_list/service_group_card.tsx index bc8a424c922b9..080b3772b6250 100644 --- a/x-pack/plugins/apm/public/components/app/service_groups/service_groups_list/service_group_card.tsx +++ b/x-pack/plugins/apm/public/components/app/service_groups/service_groups_list/service_group_card.tsx @@ -81,7 +81,11 @@ export function ServiceGroupsCard({ return ( - + ); } diff --git a/x-pack/plugins/apm/public/components/app/service_overview/index.tsx b/x-pack/plugins/apm/public/components/app/service_overview/index.tsx index bfd5b0c21ee8b..fe522e6483822 100644 --- a/x-pack/plugins/apm/public/components/app/service_overview/index.tsx +++ b/x-pack/plugins/apm/public/components/app/service_overview/index.tsx @@ -6,7 +6,13 @@ */ import React from 'react'; -import { EuiFlexGroupProps } from '@elastic/eui'; +import { + EuiFlexGroup, + EuiFlexGroupProps, + EuiFlexItem, + EuiLoadingLogo, + EuiSpacer, +} from '@elastic/eui'; import { isMobileAgentName } from '../../../../common/agent_name'; import { AnnotationsContextProvider } from '../../../context/annotations/annotations_context'; import { useApmServiceContext } from '../../../context/apm_service/use_apm_service_context'; @@ -16,6 +22,7 @@ import { useApmParams } from '../../../hooks/use_apm_params'; import { useTimeRange } from '../../../hooks/use_time_range'; import { ServiceOverviewCharts } from './service_overview_charts/service_overview_charts'; import { ServiceOverviewMobileCharts } from './service_overview_charts/service_oveview_mobile_charts'; +import { isPending } from '../../../hooks/use_fetcher'; /** * The height a chart should be if it's next to a table with 5 rows and a title. @@ -24,7 +31,7 @@ import { ServiceOverviewMobileCharts } from './service_overview_charts/service_o export const chartHeight = 288; export function ServiceOverview() { - const { agentName, serviceName } = useApmServiceContext(); + const { agentName, serviceName, serviceAgentStatus } = useApmServiceContext(); const { query: { environment, rangeFrom, rangeTo }, @@ -54,6 +61,8 @@ export function ServiceOverview() { isSingleColumn, }; + const isPendingServiceAgent = !agentName && isPending(serviceAgentStatus); + return ( - {isMobileAgent ? ( - + {isPendingServiceAgent ? ( + + + + + + ) : ( - + <> + {isMobileAgent ? ( + + ) : ( + + )} + )} diff --git a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_charts/latency_map/get_layer_list.ts b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_charts/latency_map/get_layer_list.ts index 31150b73fcf51..e17d5d4b5663c 100644 --- a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_charts/latency_map/get_layer_list.ts +++ b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_charts/latency_map/get_layer_list.ts @@ -14,6 +14,7 @@ import { COLOR_MAP_TYPE, FIELD_ORIGIN, LABEL_BORDER_SIZES, + LABEL_POSITIONS, LAYER_TYPE, SOURCE_TYPES, STYLE_TYPE, @@ -74,6 +75,11 @@ function getLayerStyle(): VectorStyleDescriptor { }, }, }, + labelPosition: { + options: { + position: LABEL_POSITIONS.CENTER, + }, + }, labelZoomRange: { options: { useLayerZoomRange: true, diff --git a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_instances_chart_and_table.tsx b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_instances_chart_and_table.tsx index 5b1e792ec5a20..e426899c7390f 100644 --- a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_instances_chart_and_table.tsx +++ b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_instances_chart_and_table.tsx @@ -12,7 +12,11 @@ import uuid from 'uuid'; import { isTimeComparison } from '../../shared/time_comparison/get_comparison_options'; import { useApmServiceContext } from '../../../context/apm_service/use_apm_service_context'; import { useApmParams } from '../../../hooks/use_apm_params'; -import { FETCH_STATUS, useFetcher } from '../../../hooks/use_fetcher'; +import { + FETCH_STATUS, + isPending, + useFetcher, +} from '../../../hooks/use_fetcher'; import { useTimeRange } from '../../../hooks/use_time_range'; import { APIReturnType } from '../../../services/rest/create_call_apm_api'; import { InstancesLatencyDistributionChart } from '../../shared/charts/instances_latency_distribution_chart'; @@ -232,10 +236,7 @@ export function ServiceOverviewInstancesChartAndTable({ mainStatsItems={currentPeriodOrderedItems} mainStatsStatus={mainStatsStatus} mainStatsItemCount={currentPeriodItemsCount} - detailedStatsLoading={ - detailedStatsStatus === FETCH_STATUS.LOADING || - detailedStatsStatus === FETCH_STATUS.NOT_INITIATED - } + detailedStatsLoading={isPending(detailedStatsStatus)} detailedStatsData={detailedStatsData} serviceName={serviceName} tableOptions={tableOptions} diff --git a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_instances_table/instance_actions_menu/index.tsx b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_instances_table/instance_actions_menu/index.tsx index f37e9b52b7093..c7f35509e494b 100644 --- a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_instances_table/instance_actions_menu/index.tsx +++ b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_instances_table/instance_actions_menu/index.tsx @@ -18,7 +18,7 @@ import { import { isJavaAgentName } from '../../../../../../common/agent_name'; import { SERVICE_NODE_NAME } from '../../../../../../common/es_fields/apm'; import { useApmPluginContext } from '../../../../../context/apm_plugin/use_apm_plugin_context'; -import { FETCH_STATUS } from '../../../../../hooks/use_fetcher'; +import { isPending } from '../../../../../hooks/use_fetcher'; import { pushNewItemToKueryBar } from '../../../../shared/kuery_bar/utils'; import { useMetricOverviewHref } from '../../../../shared/links/apm/metric_overview_link'; import { useServiceNodeMetricOverviewHref } from '../../../../shared/links/apm/service_node_metric_overview_link'; @@ -52,10 +52,7 @@ export function InstanceActionsMenu({ const metricOverviewHref = useMetricOverviewHref(serviceName); const history = useHistory(); - if ( - status === FETCH_STATUS.LOADING || - status === FETCH_STATUS.NOT_INITIATED - ) { + if (isPending(status)) { return (
    diff --git a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_throughput_chart.tsx b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_throughput_chart.tsx index 3c1ab8cde0b17..5e7c02610d7c8 100644 --- a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_throughput_chart.tsx +++ b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_throughput_chart.tsx @@ -24,7 +24,7 @@ import { useApmParams } from '../../../hooks/use_apm_params'; import { useFetcher } from '../../../hooks/use_fetcher'; import { usePreferredServiceAnomalyTimeseries } from '../../../hooks/use_preferred_service_anomaly_timeseries'; import { useTimeRange } from '../../../hooks/use_time_range'; -import { TimeseriesChart } from '../../shared/charts/timeseries_chart'; +import { TimeseriesChartWithContext } from '../../shared/charts/timeseries_chart_with_context'; import { getComparisonChartTheme } from '../../shared/time_comparison/get_comparison_chart_theme'; import { ChartType, @@ -152,7 +152,7 @@ export function ServiceOverviewThroughputChart({ - @@ -80,10 +103,8 @@ export function AgentContextualInformation({ width: '25%', }, { - label: i18n.translate('xpack.apm.agentInstancesDetails.intancesLabel', { - defaultMessage: 'Instances', - }), - fieldName: 'instances', + label: instancesLabel, + fieldName: instancesLabel, val: ( @@ -94,13 +115,8 @@ export function AgentContextualInformation({ width: '25%', }, { - label: i18n.translate( - 'xpack.apm.agentInstancesDetails.agentDocsUrlLabel', - { - defaultMessage: 'Agent documentation', - } - ), - fieldName: AgentExplorerFieldName.AgentDocsPageUrl, + label: agentDocsLabel, + fieldName: agentDocsLabel, val: ( > { +): Array> { return [ { field: AgentExplorerInstanceFieldName.InstanceName, @@ -190,28 +191,28 @@ export function AgentInstancesDetails({ items, isLoading, }: Props) { - if (isLoading) { - return ( -
    - -
    - ); - } - return ( - + + /> + ); } diff --git a/x-pack/plugins/apm/public/components/app/settings/agent_explorer/agent_list/index.tsx b/x-pack/plugins/apm/public/components/app/settings/agent_explorer/agent_list/index.tsx index 8d934c7570413..51055428a9a34 100644 --- a/x-pack/plugins/apm/public/components/app/settings/agent_explorer/agent_list/index.tsx +++ b/x-pack/plugins/apm/public/components/app/settings/agent_explorer/agent_list/index.tsx @@ -17,7 +17,6 @@ import { ValuesType } from 'utility-types'; import { AgentExplorerFieldName } from '../../../../../../common/agent_explorer'; import { AgentName } from '../../../../../../typings/es_schemas/ui/fields/agent'; import { APIReturnType } from '../../../../../services/rest/create_call_apm_api'; -import { unit } from '../../../../../utils/style'; import { AgentIcon } from '../../../../shared/agent_icon'; import { EnvironmentBadge } from '../../../../shared/environment_badge'; import { ItemsBadge } from '../../../../shared/item_badge'; @@ -41,7 +40,7 @@ export function getAgentsColumns({ { field: AgentExplorerFieldName.ServiceName, name: '', - width: `${unit * 3}px`, + width: '5%', render: (_, agent) => { const isSelected = selectedAgent === agent; @@ -78,6 +77,8 @@ export function getAgentsColumns({ } ), sortable: true, + width: '35%', + truncateText: true, render: (_, { serviceName, agentName }) => ( ( @@ -117,12 +119,12 @@ export function getAgentsColumns({ defaultMessage: 'Instances', } ), - width: `${unit * 8}px`, + width: '10%', sortable: true, }, { field: AgentExplorerFieldName.AgentName, - width: `${unit * 12}px`, + width: '15%', name: i18n.translate( 'xpack.apm.agentExplorerTable.agentNameColumnLabel', { defaultMessage: 'Agent Name' } @@ -135,7 +137,8 @@ export function getAgentsColumns({ 'xpack.apm.agentExplorerTable.agentVersionColumnLabel', { defaultMessage: 'Agent Version' } ), - width: `${unit * 8}px`, + width: '10%', + truncateText: true, render: (_, { agentVersion }) => ( ( - +

    {i18n.translate('xpack.apm.settings.agentExplorer.title', { @@ -117,9 +122,16 @@ export function AgentExplorer() { - + - + + + + + + diff --git a/x-pack/plugins/apm/public/components/app/storage_explorer/services_table/storage_details_per_service.tsx b/x-pack/plugins/apm/public/components/app/storage_explorer/services_table/storage_details_per_service.tsx index 4005c01f1b8f5..ba8005f12759c 100644 --- a/x-pack/plugins/apm/public/components/app/storage_explorer/services_table/storage_details_per_service.tsx +++ b/x-pack/plugins/apm/public/components/app/storage_explorer/services_table/storage_details_per_service.tsx @@ -32,7 +32,7 @@ import { ProcessorEvent } from '@kbn/observability-plugin/common'; import { IndexLifecyclePhaseSelectOption } from '../../../../../common/storage_explorer_types'; import { useApmParams } from '../../../../hooks/use_apm_params'; import { useTimeRange } from '../../../../hooks/use_time_range'; -import { FETCH_STATUS } from '../../../../hooks/use_fetcher'; +import { isPending } from '../../../../hooks/use_fetcher'; import { useProgressiveFetcher } from '../../../../hooks/use_progressive_fetcher'; import { useApmRouter } from '../../../../hooks/use_apm_router'; import { asInteger } from '../../../../../common/utils/formatters/formatters'; @@ -131,10 +131,7 @@ export function StorageDetailsPerService({ [indexLifecyclePhase, start, end, environment, kuery, serviceName] ); - if ( - status === FETCH_STATUS.LOADING || - status === FETCH_STATUS.NOT_INITIATED - ) { + if (isPending(status)) { return (
    diff --git a/x-pack/plugins/apm/public/components/app/storage_explorer/summary_stats.tsx b/x-pack/plugins/apm/public/components/app/storage_explorer/summary_stats.tsx index b6f6e5167f3d3..506b966f0d3ca 100644 --- a/x-pack/plugins/apm/public/components/app/storage_explorer/summary_stats.tsx +++ b/x-pack/plugins/apm/public/components/app/storage_explorer/summary_stats.tsx @@ -29,7 +29,7 @@ import { useApmParams } from '../../../hooks/use_apm_params'; import { asDynamicBytes, asPercent } from '../../../../common/utils/formatters'; import { useApmRouter } from '../../../hooks/use_apm_router'; import { useApmPluginContext } from '../../../context/apm_plugin/use_apm_plugin_context'; -import { FETCH_STATUS } from '../../../hooks/use_fetcher'; +import { isPending } from '../../../hooks/use_fetcher'; import { asTransactionRate } from '../../../../common/utils/formatters'; import { getIndexManagementHref } from './get_storage_explorer_links'; @@ -78,8 +78,7 @@ export function SummaryStats() { [indexLifecyclePhase, environment, kuery, start, end] ); - const loading = - status === FETCH_STATUS.LOADING || status === FETCH_STATUS.NOT_INITIATED; + const loading = isPending(status); const hasData = !isEmpty(data); diff --git a/x-pack/plugins/apm/public/components/routing/templates/apm_service_template/analyze_data_button.stories.tsx b/x-pack/plugins/apm/public/components/routing/templates/apm_service_template/analyze_data_button.stories.tsx index 2708c46b52960..57a0e789267b1 100644 --- a/x-pack/plugins/apm/public/components/routing/templates/apm_service_template/analyze_data_button.stories.tsx +++ b/x-pack/plugins/apm/public/components/routing/templates/apm_service_template/analyze_data_button.stories.tsx @@ -14,6 +14,7 @@ import { ENVIRONMENT_ALL } from '../../../../../common/environment_filter_values import { MockApmPluginContextWrapper } from '../../../../context/apm_plugin/mock_apm_plugin_context'; import { APMServiceContext } from '../../../../context/apm_service/apm_service_context'; import { AnalyzeDataButton } from './analyze_data_button'; +import { FETCH_STATUS } from '../../../../hooks/use_fetcher'; interface Args { agentName: string; @@ -51,6 +52,7 @@ export default { transactionTypes: [], serviceName, fallbackToTransactions: false, + serviceAgentStatus: FETCH_STATUS.SUCCESS, }} > diff --git a/x-pack/plugins/apm/public/components/routing/templates/apm_service_template/index.tsx b/x-pack/plugins/apm/public/components/routing/templates/apm_service_template/index.tsx index 58d97d7916384..a29360bc103e0 100644 --- a/x-pack/plugins/apm/public/components/routing/templates/apm_service_template/index.tsx +++ b/x-pack/plugins/apm/public/components/routing/templates/apm_service_template/index.tsx @@ -270,7 +270,7 @@ function useTabs({ selectedTab }: { selectedTab: Tab['key'] }) { path: { serviceName }, query, }), - append: , + append: , label: i18n.translate('xpack.apm.home.infraTabLabel', { defaultMessage: 'Infrastructure', }), diff --git a/x-pack/plugins/apm/public/components/routing/templates/dependency_detail_template.tsx b/x-pack/plugins/apm/public/components/routing/templates/dependency_detail_template.tsx index 57ff4ba98a04a..833b489004bf3 100644 --- a/x-pack/plugins/apm/public/components/routing/templates/dependency_detail_template.tsx +++ b/x-pack/plugins/apm/public/components/routing/templates/dependency_detail_template.tsx @@ -6,21 +6,22 @@ */ import { EuiFlexGroup, EuiFlexItem, EuiTitle } from '@elastic/eui'; -import React from 'react'; import { i18n } from '@kbn/i18n'; -import { ApmMainTemplate } from './apm_main_template'; -import { SpanIcon } from '../../shared/span_icon'; -import { useApmParams } from '../../../hooks/use_apm_params'; -import { useTimeRange } from '../../../hooks/use_time_range'; -import { useFetcher } from '../../../hooks/use_fetcher'; -import { useApmRouter } from '../../../hooks/use_apm_router'; -import { useApmRoutePath } from '../../../hooks/use_apm_route_path'; -import { SearchBar } from '../../shared/search_bar'; +import React from 'react'; import { getKueryBarBoolFilter, kueryBarPlaceholder, } from '../../../../common/dependencies'; +import { useApmParams } from '../../../hooks/use_apm_params'; +import { useApmRouter } from '../../../hooks/use_apm_router'; +import { useApmRoutePath } from '../../../hooks/use_apm_route_path'; +import { useFetcher } from '../../../hooks/use_fetcher'; import { useOperationBreakdownEnabledSetting } from '../../../hooks/use_operations_breakdown_enabled_setting'; +import { useTimeRange } from '../../../hooks/use_time_range'; +import { BetaBadge } from '../../shared/beta_badge'; +import { SearchBar } from '../../shared/search_bar'; +import { SpanIcon } from '../../shared/span_icon'; +import { ApmMainTemplate } from './apm_main_template'; interface Props { children: React.ReactNode; @@ -90,6 +91,7 @@ export function DependencyDetailTemplate({ children }: Props) { isSelected: path === '/dependencies/operations' || path === '/dependencies/operation', + append: , }, ] : []; diff --git a/x-pack/plugins/apm/public/components/shared/agent_icon/icons/functions.svg b/x-pack/plugins/apm/public/components/shared/agent_icon/icons/functions.svg new file mode 100644 index 0000000000000..172fe00a49d85 --- /dev/null +++ b/x-pack/plugins/apm/public/components/shared/agent_icon/icons/functions.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/x-pack/plugins/apm/public/components/shared/apm_header_action_menu/labs/labs_flyout.tsx b/x-pack/plugins/apm/public/components/shared/apm_header_action_menu/labs/labs_flyout.tsx index 9c6983adabc06..6c5d38d0f1f7d 100644 --- a/x-pack/plugins/apm/public/components/shared/apm_header_action_menu/labs/labs_flyout.tsx +++ b/x-pack/plugins/apm/public/components/shared/apm_header_action_menu/labs/labs_flyout.tsx @@ -26,7 +26,7 @@ import { i18n } from '@kbn/i18n'; import React from 'react'; import { useApmPluginContext } from '../../../../context/apm_plugin/use_apm_plugin_context'; import { useApmEditableSettings } from '../../../../hooks/use_apm_editable_settings'; -import { useFetcher, FETCH_STATUS } from '../../../../hooks/use_fetcher'; +import { useFetcher, isPending } from '../../../../hooks/use_fetcher'; interface Props { onClose: () => void; @@ -79,8 +79,7 @@ export function LabsFlyout({ onClose }: Props) { onClose(); } - const isLoading = - status === FETCH_STATUS.NOT_INITIATED || status === FETCH_STATUS.LOADING; + const isLoading = isPending(status); return ( diff --git a/x-pack/plugins/apm/public/components/shared/charts/chart_container.test.tsx b/x-pack/plugins/apm/public/components/shared/charts/chart_container.test.tsx index efb1fa8d8655f..0411c03b30f73 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/chart_container.test.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/chart_container.test.tsx @@ -12,14 +12,10 @@ import { ChartContainer } from './chart_container'; describe('ChartContainer', () => { describe('loading indicator', () => { - it('shows loading when status equals to Loading or Pending and has no data', () => { - [FETCH_STATUS.NOT_INITIATED, FETCH_STATUS.LOADING].map((status) => { + it('shows loading when status equals to Loading or Not initiated and has no data', () => { + [FETCH_STATUS.NOT_INITIATED, FETCH_STATUS.LOADING].forEach((status) => { const { queryAllByTestId } = render( - +
    My amazing component
    ); @@ -28,7 +24,7 @@ describe('ChartContainer', () => { }); }); it('does not show loading when status equals to Loading or Pending and has data', () => { - [FETCH_STATUS.NOT_INITIATED, FETCH_STATUS.LOADING].map((status) => { + [FETCH_STATUS.NOT_INITIATED, FETCH_STATUS.LOADING].forEach((status) => { const { queryAllByText } = render(
    My amazing component
    diff --git a/x-pack/plugins/apm/public/components/shared/charts/chart_container.tsx b/x-pack/plugins/apm/public/components/shared/charts/chart_container.tsx index 695e62b3b7d78..ce676c6077594 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/chart_container.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/chart_container.tsx @@ -8,7 +8,7 @@ import { EuiLoadingChart, EuiText } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React from 'react'; -import { FETCH_STATUS } from '../../../hooks/use_fetcher'; +import { FETCH_STATUS, isPending } from '../../../hooks/use_fetcher'; export interface ChartContainerProps { hasData: boolean; @@ -25,7 +25,7 @@ export function ChartContainer({ hasData, id, }: ChartContainerProps) { - if (!hasData && status === FETCH_STATUS.LOADING) { + if (!hasData && isPending(status)) { return ; } diff --git a/x-pack/plugins/apm/public/components/shared/charts/failed_transaction_rate_chart/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/failed_transaction_rate_chart/index.tsx index dacb295a011dd..508d3d0f0ed68 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/failed_transaction_rate_chart/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/failed_transaction_rate_chart/index.tsx @@ -15,7 +15,7 @@ import { APIReturnType } from '../../../../services/rest/create_call_apm_api'; import { asPercent } from '../../../../../common/utils/formatters'; import { useFetcher } from '../../../../hooks/use_fetcher'; import { useLegacyUrlParams } from '../../../../context/url_params_context/use_url_params'; -import { TimeseriesChart } from '../timeseries_chart'; +import { TimeseriesChartWithContext } from '../timeseries_chart_with_context'; import { useApmServiceContext } from '../../../../context/apm_service/use_apm_service_context'; import { getComparisonChartTheme } from '../../time_comparison/get_comparison_chart_theme'; import { useApmParams } from '../../../../hooks/use_apm_params'; @@ -49,6 +49,10 @@ const INITIAL_STATE: ErrorRate = { }, }; +export const errorRateI18n = i18n.translate('xpack.apm.errorRate.tip', { + defaultMessage: + "The percentage of failed transactions for the selected service. HTTP server transactions with a 4xx status code (client error) aren't considered failures because the caller, not the server, caused the failure.", +}); export function FailedTransactionRateChart({ height, showAnnotations = true, @@ -154,17 +158,11 @@ export function FailedTransactionRateChart({ - + - = [ { value: LatencyAggregationType.p99, text: '99th percentile' }, ]; -function filterNil(value: T | null | undefined): value is T { +export function filterNil(value: T | null | undefined): value is T { return value != null; } @@ -126,7 +126,7 @@ export function LatencyChart({ height, kuery }: Props) { - ; @@ -85,6 +86,7 @@ const stories: Meta = { transactionType, transactionTypes: [], fallbackToTransactions: false, + serviceAgentStatus: FETCH_STATUS.SUCCESS, }} > diff --git a/x-pack/plugins/apm/public/components/shared/charts/metrics_chart/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/metrics_chart/index.tsx index 41ad3acda28a0..ee08d44804ad5 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/metrics_chart/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/metrics_chart/index.tsx @@ -17,7 +17,7 @@ import { } from '../../../../../common/utils/formatters'; import { Maybe } from '../../../../../typings/common'; import { FETCH_STATUS } from '../../../../hooks/use_fetcher'; -import { TimeseriesChart } from '../timeseries_chart'; +import { TimeseriesChartWithContext } from '../timeseries_chart_with_context'; import { getMaxY, getResponseTimeTickFormatter, @@ -76,7 +76,7 @@ export function MetricsChart({ chart, fetchStatus }: Props) { )} - >; - /** - * Formatter for y-axis tick values - */ - yLabelFormat: (y: number) => string; - /** - * Formatter for legend and tooltip values - */ - yTickFormat?: (y: number) => string; - showAnnotations?: boolean; - yDomain?: YDomainRange; - anomalyTimeseries?: AnomalyTimeseries; - customTheme?: Record; - anomalyTimeseriesColor?: string; -} +import { TimeseriesChartWithContextProps } from './timeseries_chart_with_context'; const END_ZONE_LABEL = i18n.translate('xpack.apm.timeseries.endzone', { defaultMessage: 'The selected time range does not include this entire bucket. It might contain partial data.', }); - +interface TimeseriesChartProps extends TimeseriesChartWithContextProps { + comparisonEnabled: boolean; + offset?: string; + timeZone: string; +} export function TimeseriesChart({ id, height = unit * 16, @@ -91,30 +64,22 @@ export function TimeseriesChart({ yDomain, anomalyTimeseries, customTheme = {}, -}: Props) { + comparisonEnabled, + offset, + timeZone, +}: TimeseriesChartProps) { const history = useHistory(); - const { core } = useApmPluginContext(); const { annotations } = useAnnotationsContext(); const { chartRef, updatePointerEvent } = useChartPointerEventContext(); const theme = useTheme(); const chartTheme = useChartTheme(); - const { - query: { comparisonEnabled, offset }, - } = useAnyOfApmParams( - '/services', - '/dependencies/*', - '/services/{serviceName}' - ); - const anomalyChartTimeseries = getChartAnomalyTimeseries({ anomalyTimeseries, theme, anomalyTimeseriesColor: anomalyTimeseries?.color, }); - const isEmpty = isTimeseriesEmpty(timeseries); const annotationColor = theme.eui.euiColorSuccess; - const isComparingExpectedBounds = comparisonEnabled && isExpectedBoundsComparison(offset); const allSeries = [ @@ -134,20 +99,14 @@ export function TimeseriesChart({ ); const xValues = timeseries.flatMap(({ data }) => data.map(({ x }) => x)); - const xValuesExpectedBounds = anomalyChartTimeseries?.boundaries?.flatMap(({ data }) => data.map(({ x }) => x) ) ?? []; - - const timeZone = getTimeZone(core.uiSettings); - const min = Math.min(...xValues); const max = Math.max(...xValues, ...xValuesExpectedBounds); const xFormatter = niceTimeFormatter([min, max]); - const xDomain = isEmpty ? { min: 0, max: 1 } : { min, max }; - // Using custom legendSort here when comparing expected bounds // because by default elastic-charts will show legends for expected bounds first // but for consistency, we are making `Expected bounds` last diff --git a/x-pack/plugins/apm/public/components/shared/charts/timeseries_chart_with_context.tsx b/x-pack/plugins/apm/public/components/shared/charts/timeseries_chart_with_context.tsx new file mode 100644 index 0000000000000..5c9aac5d28bdf --- /dev/null +++ b/x-pack/plugins/apm/public/components/shared/charts/timeseries_chart_with_context.tsx @@ -0,0 +1,84 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { LegendItemListener, YDomainRange } from '@elastic/charts'; +import React from 'react'; +import { useAnyOfApmParams } from '../../../hooks/use_apm_params'; +import { ServiceAnomalyTimeseries } from '../../../../common/anomaly_detection/service_anomaly_timeseries'; +import { Coordinate, TimeSeries } from '../../../../typings/timeseries'; +import { useApmPluginContext } from '../../../context/apm_plugin/use_apm_plugin_context'; +import { FETCH_STATUS } from '../../../hooks/use_fetcher'; +import { unit } from '../../../utils/style'; +import { getTimeZone } from './helper/timezone'; +import { TimeseriesChart } from './timeseries_chart'; + +interface AnomalyTimeseries extends ServiceAnomalyTimeseries { + color?: string; +} +export interface TimeseriesChartWithContextProps { + id: string; + fetchStatus: FETCH_STATUS; + height?: number; + onToggleLegend?: LegendItemListener; + timeseries: Array>; + /** + * Formatter for y-axis tick values + */ + yLabelFormat: (y: number) => string; + /** + * Formatter for legend and tooltip values + */ + yTickFormat?: (y: number) => string; + showAnnotations?: boolean; + yDomain?: YDomainRange; + anomalyTimeseries?: AnomalyTimeseries; + customTheme?: Record; + anomalyTimeseriesColor?: string; +} + +export function TimeseriesChartWithContext({ + id, + height = unit * 16, + fetchStatus, + onToggleLegend, + timeseries, + yLabelFormat, + yTickFormat, + showAnnotations = true, + yDomain, + anomalyTimeseries, + customTheme = {}, +}: TimeseriesChartWithContextProps) { + const { + query: { comparisonEnabled, offset }, + } = useAnyOfApmParams( + '/services', + '/dependencies/*', + '/services/{serviceName}' + ); + const { core } = useApmPluginContext(); + const timeZone = getTimeZone(core.uiSettings); + + return ( + + ); +} diff --git a/x-pack/plugins/apm/public/components/shared/charts/transaction_coldstart_rate_chart/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/transaction_coldstart_rate_chart/index.tsx index a3143bb7b6849..3dfc22a1c2809 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/transaction_coldstart_rate_chart/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/transaction_coldstart_rate_chart/index.tsx @@ -20,7 +20,7 @@ import { APIReturnType } from '../../../../services/rest/create_call_apm_api'; import { asPercent } from '../../../../../common/utils/formatters'; import { useFetcher } from '../../../../hooks/use_fetcher'; import { useTheme } from '../../../../hooks/use_theme'; -import { TimeseriesChart } from '../timeseries_chart'; +import { TimeseriesChartWithContext } from '../timeseries_chart_with_context'; import { useApmServiceContext } from '../../../../context/apm_service/use_apm_service_context'; import { getComparisonChartTheme } from '../../time_comparison/get_comparison_chart_theme'; import { useApmParams } from '../../../../hooks/use_apm_params'; @@ -161,7 +161,7 @@ export function TransactionColdstartRateChart({ /> - { if (!criticalPath) { diff --git a/x-pack/plugins/apm/public/components/shared/date_picker/apm_date_picker.tsx b/x-pack/plugins/apm/public/components/shared/date_picker/apm_date_picker.tsx index 96d9ae768f352..5f2cede9b308c 100644 --- a/x-pack/plugins/apm/public/components/shared/date_picker/apm_date_picker.tsx +++ b/x-pack/plugins/apm/public/components/shared/date_picker/apm_date_picker.tsx @@ -7,7 +7,6 @@ import React from 'react'; import { useApmParams } from '../../../hooks/use_apm_params'; - import { DatePicker } from '.'; import { useTimeRangeId } from '../../../context/time_range_id/use_time_range_id'; import { @@ -15,6 +14,8 @@ import { toNumber, } from '../../../context/url_params_context/helpers'; +export const DEFAULT_REFRESH_INTERVAL = 60000; + export function ApmDatePicker() { const { query } = useApmParams('/*'); @@ -26,12 +27,12 @@ export function ApmDatePicker() { rangeFrom, rangeTo, refreshPaused: refreshPausedFromUrl = 'true', - refreshInterval: refreshIntervalFromUrl = '0', + refreshInterval: refreshIntervalFromUrl, } = query; const refreshPaused = toBoolean(refreshPausedFromUrl); - - const refreshInterval = toNumber(refreshIntervalFromUrl); + const refreshInterval = + toNumber(refreshIntervalFromUrl) ?? DEFAULT_REFRESH_INTERVAL; const { incrementTimeRangeId } = useTimeRangeId(); diff --git a/x-pack/plugins/apm/public/components/shared/dependencies_table/get_span_metric_columns.tsx b/x-pack/plugins/apm/public/components/shared/dependencies_table/get_span_metric_columns.tsx index 49d9d342947f9..d477e7269cace 100644 --- a/x-pack/plugins/apm/public/components/shared/dependencies_table/get_span_metric_columns.tsx +++ b/x-pack/plugins/apm/public/components/shared/dependencies_table/get_span_metric_columns.tsx @@ -20,7 +20,7 @@ import { } from '../charts/helper/get_timeseries_color'; import { ListMetric } from '../list_metric'; import { ITableColumn } from '../managed_table'; -import { FETCH_STATUS } from '../../../hooks/use_fetcher'; +import { FETCH_STATUS, isPending } from '../../../hooks/use_fetcher'; import { asMillisecondDuration, asPercent, @@ -61,9 +61,8 @@ export function getSpanMetricColumns({ }): Array> { const { isLarge } = breakpoints; const shouldShowSparkPlots = !isLarge; - const isLoading = - comparisonFetchStatus === FETCH_STATUS.LOADING || - comparisonFetchStatus === FETCH_STATUS.NOT_INITIATED; + const isLoading = isPending(comparisonFetchStatus); + return [ { field: 'latency', diff --git a/x-pack/plugins/apm/public/components/shared/dependency_metric_charts/dependency_failed_transaction_rate_chart.tsx b/x-pack/plugins/apm/public/components/shared/dependency_metric_charts/dependency_failed_transaction_rate_chart.tsx index b5fbcb0608956..c259815b569d0 100644 --- a/x-pack/plugins/apm/public/components/shared/dependency_metric_charts/dependency_failed_transaction_rate_chart.tsx +++ b/x-pack/plugins/apm/public/components/shared/dependency_metric_charts/dependency_failed_transaction_rate_chart.tsx @@ -12,7 +12,7 @@ import { asPercent } from '../../../../common/utils/formatters'; import { useFetcher } from '../../../hooks/use_fetcher'; import { useTimeRange } from '../../../hooks/use_time_range'; import { Coordinate, TimeSeries } from '../../../../typings/timeseries'; -import { TimeseriesChart } from '../charts/timeseries_chart'; +import { TimeseriesChartWithContext } from '../charts/timeseries_chart_with_context'; import { ChartType, getTimeSeriesColor, @@ -115,7 +115,7 @@ export function DependencyFailedTransactionRateChart({ }, [data, currentPeriodColor, previousPeriodColor, previousPeriodLabel]); return ( - updateEnvironmentUrl(history, location, changeValue) } diff --git a/x-pack/plugins/apm/public/components/shared/environment_select/index.tsx b/x-pack/plugins/apm/public/components/shared/environment_select/index.tsx index a6b14b4f0a4e8..3ddc57a78f56d 100644 --- a/x-pack/plugins/apm/public/components/shared/environment_select/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/environment_select/index.tsx @@ -17,7 +17,6 @@ import { import { SERVICE_ENVIRONMENT } from '../../../../common/es_fields/apm'; import { FETCH_STATUS, useFetcher } from '../../../hooks/use_fetcher'; import { useTimeRange } from '../../../hooks/use_time_range'; -import { useApmParams } from '../../../hooks/use_apm_params'; import { Environment } from '../../../../common/environment_rt'; function getEnvironmentOptions(environments: Environment[]) { @@ -41,18 +40,20 @@ export function EnvironmentSelect({ environment, availableEnvironments, status, + serviceName, + rangeFrom, + rangeTo, onChange, }: { environment: Environment; availableEnvironments: Environment[]; status: FETCH_STATUS; + serviceName?: string; + rangeFrom: string; + rangeTo: string; onChange: (value: string) => void; }) { const [searchValue, setSearchValue] = useState(''); - const { - path: { serviceName }, - query: { rangeFrom, rangeTo }, - } = useApmParams('/services/{serviceName}/*'); const { start, end } = useTimeRange({ rangeFrom, rangeTo }); diff --git a/x-pack/plugins/apm/public/components/shared/links/discover_links/discover_link.tsx b/x-pack/plugins/apm/public/components/shared/links/discover_links/discover_link.tsx index e052688d73474..1455e53730a78 100644 --- a/x-pack/plugins/apm/public/components/shared/links/discover_links/discover_link.tsx +++ b/x-pack/plugins/apm/public/components/shared/links/discover_links/discover_link.tsx @@ -10,7 +10,7 @@ import { Location } from 'history'; import { IBasePath } from '@kbn/core/public'; import React from 'react'; import { useLocation } from 'react-router-dom'; -import rison, { RisonValue } from 'rison-node'; +import rison from '@kbn/rison'; import url from 'url'; import { APM_STATIC_DATA_VIEW_ID } from '../../../../../common/data_view_constants'; import { useApmPluginContext } from '../../../../context/apm_plugin/use_apm_plugin_context'; @@ -53,7 +53,7 @@ export const getDiscoverHref = ({ const href = url.format({ pathname: basePath.prepend('/app/discover'), hash: `/?_g=${rison.encode(risonQuery._g)}&_a=${rison.encode( - risonQuery._a as RisonValue + risonQuery._a )}`, }); return href; diff --git a/x-pack/plugins/apm/public/components/shared/links/machine_learning_links/mlexplorer_link.test.tsx b/x-pack/plugins/apm/public/components/shared/links/machine_learning_links/mlexplorer_link.test.tsx index ad0b06b73df53..4438220c9d915 100644 --- a/x-pack/plugins/apm/public/components/shared/links/machine_learning_links/mlexplorer_link.test.tsx +++ b/x-pack/plugins/apm/public/components/shared/links/machine_learning_links/mlexplorer_link.test.tsx @@ -23,7 +23,7 @@ describe('MLExplorerLink', () => { ); expect(href).toMatchInlineSnapshot( - `"/app/ml/explorer?_g=(ml:(jobIds:!(myservicename-mytransactiontype-high_mean_response_time)),refreshInterval:(pause:!t,value:10000),time:(from:now%2Fw,to:now-4h))&_a=(explorer:(mlExplorerFilter:(),mlExplorerSwimlane:()))"` + `"/app/ml/explorer?_g=(ml:(jobIds:!(myservicename-mytransactiontype-high_mean_response_time)),refreshInterval:(pause:!t,value:60000),time:(from:now%2Fw,to:now-4h))&_a=(explorer:(mlExplorerFilter:(),mlExplorerSwimlane:()))"` ); }); @@ -34,12 +34,12 @@ describe('MLExplorerLink', () => { ), { search: - '?rangeFrom=2020-07-29T17:27:29.000Z&rangeTo=2020-07-29T18:45:00.000Z&refreshInterval=10000&refreshPaused=true', + '?rangeFrom=2020-07-29T17:27:29.000Z&rangeTo=2020-07-29T18:45:00.000Z&refreshInterval=60000&refreshPaused=true', } as Location ); expect(href).toMatchInlineSnapshot( - `"/app/ml/explorer?_g=(ml:(jobIds:!(apm-production-485b-high_mean_transaction_duration)),refreshInterval:(pause:!t,value:10000),time:(from:'2020-07-29T17:27:29.000Z',to:'2020-07-29T18:45:00.000Z'))&_a=(explorer:(mlExplorerFilter:(),mlExplorerSwimlane:()))"` + `"/app/ml/explorer?_g=(ml:(jobIds:!(apm-production-485b-high_mean_transaction_duration)),refreshInterval:(pause:!t,value:60000),time:(from:'2020-07-29T17:27:29.000Z',to:'2020-07-29T18:45:00.000Z'))&_a=(explorer:(mlExplorerFilter:(),mlExplorerSwimlane:()))"` ); }); }); diff --git a/x-pack/plugins/apm/public/components/shared/links/machine_learning_links/mlexplorer_link.tsx b/x-pack/plugins/apm/public/components/shared/links/machine_learning_links/mlexplorer_link.tsx index 0990a961c0d4b..48bdfc69150ad 100644 --- a/x-pack/plugins/apm/public/components/shared/links/machine_learning_links/mlexplorer_link.tsx +++ b/x-pack/plugins/apm/public/components/shared/links/machine_learning_links/mlexplorer_link.tsx @@ -10,6 +10,7 @@ import { EuiLink } from '@elastic/eui'; import { useMlHref, ML_PAGES } from '@kbn/ml-plugin/public'; import { useApmPluginContext } from '../../../../context/apm_plugin/use_apm_plugin_context'; import { useLegacyUrlParams } from '../../../../context/url_params_context/use_url_params'; +import { DEFAULT_REFRESH_INTERVAL } from '../../date_picker/apm_date_picker'; interface Props { children?: ReactNode; @@ -48,7 +49,7 @@ export function useExplorerHref({ jobId }: { jobId: string }) { pageState: { jobIds: [jobId], timeRange: { from: rangeFrom, to: rangeTo }, - refreshInterval: { pause: true, value: 10000 }, + refreshInterval: { pause: true, value: DEFAULT_REFRESH_INTERVAL }, }, }); diff --git a/x-pack/plugins/apm/public/components/shared/links/machine_learning_links/mlsingle_metric_link.test.tsx b/x-pack/plugins/apm/public/components/shared/links/machine_learning_links/mlsingle_metric_link.test.tsx index eb6ae275563a3..bb6f5c524e7ac 100644 --- a/x-pack/plugins/apm/public/components/shared/links/machine_learning_links/mlsingle_metric_link.test.tsx +++ b/x-pack/plugins/apm/public/components/shared/links/machine_learning_links/mlsingle_metric_link.test.tsx @@ -23,7 +23,7 @@ describe('MLSingleMetricLink', () => { ); expect(href).toMatchInlineSnapshot( - `"/app/ml/timeseriesexplorer?_g=(ml:(jobIds:!(myservicename-mytransactiontype-high_mean_response_time)),refreshInterval:(pause:!t,value:10000),time:(from:now%2Fw,to:now-4h))&_a=(timeseriesexplorer:(mlTimeSeriesExplorer:()))"` + `"/app/ml/timeseriesexplorer?_g=(ml:(jobIds:!(myservicename-mytransactiontype-high_mean_response_time)),refreshInterval:(pause:!t,value:60000),time:(from:now%2Fw,to:now-4h))&_a=(timeseriesexplorer:(mlTimeSeriesExplorer:()))"` ); }); it('should produce the correct URL with jobId, serviceName, and transactionType', async () => { @@ -42,7 +42,7 @@ describe('MLSingleMetricLink', () => { ); expect(href).toMatchInlineSnapshot( - `"/app/ml/timeseriesexplorer?_g=(ml:(jobIds:!(myservicename-mytransactiontype-high_mean_response_time)),refreshInterval:(pause:!t,value:10000),time:(from:now%2Fw,to:now-4h))&_a=(timeseriesexplorer:(mlTimeSeriesExplorer:(entities:(service.name:opbeans-test,transaction.type:request))))"` + `"/app/ml/timeseriesexplorer?_g=(ml:(jobIds:!(myservicename-mytransactiontype-high_mean_response_time)),refreshInterval:(pause:!t,value:60000),time:(from:now%2Fw,to:now-4h))&_a=(timeseriesexplorer:(mlTimeSeriesExplorer:(entities:(service.name:opbeans-test,transaction.type:request))))"` ); }); @@ -57,12 +57,12 @@ describe('MLSingleMetricLink', () => { ), { search: - '?rangeFrom=2020-07-29T17:27:29.000Z&rangeTo=2020-07-29T18:45:00.000Z&refreshInterval=10000&refreshPaused=true', + '?rangeFrom=2020-07-29T17:27:29.000Z&rangeTo=2020-07-29T18:45:00.000Z&refreshInterval=60000&refreshPaused=true', } as Location ); expect(href).toMatchInlineSnapshot( - `"/app/ml/timeseriesexplorer?_g=(ml:(jobIds:!(apm-production-485b-high_mean_transaction_duration)),refreshInterval:(pause:!t,value:10000),time:(from:'2020-07-29T17:27:29.000Z',to:'2020-07-29T18:45:00.000Z'))&_a=(timeseriesexplorer:(mlTimeSeriesExplorer:(entities:(service.name:opbeans-java,transaction.type:request))))"` + `"/app/ml/timeseriesexplorer?_g=(ml:(jobIds:!(apm-production-485b-high_mean_transaction_duration)),refreshInterval:(pause:!t,value:60000),time:(from:'2020-07-29T17:27:29.000Z',to:'2020-07-29T18:45:00.000Z'))&_a=(timeseriesexplorer:(mlTimeSeriesExplorer:(entities:(service.name:opbeans-java,transaction.type:request))))"` ); }); }); diff --git a/x-pack/plugins/apm/public/components/shared/links/machine_learning_links/mlsingle_metric_link.tsx b/x-pack/plugins/apm/public/components/shared/links/machine_learning_links/mlsingle_metric_link.tsx index 7c4232866157b..ff8b350ac0454 100644 --- a/x-pack/plugins/apm/public/components/shared/links/machine_learning_links/mlsingle_metric_link.tsx +++ b/x-pack/plugins/apm/public/components/shared/links/machine_learning_links/mlsingle_metric_link.tsx @@ -10,6 +10,7 @@ import { EuiLink } from '@elastic/eui'; import { useMlHref, ML_PAGES } from '@kbn/ml-plugin/public'; import { useApmPluginContext } from '../../../../context/apm_plugin/use_apm_plugin_context'; import { useLegacyUrlParams } from '../../../../context/url_params_context/use_url_params'; +import { DEFAULT_REFRESH_INTERVAL } from '../../date_picker/apm_date_picker'; interface Props { children?: ReactNode; @@ -74,7 +75,7 @@ function useSingleMetricHref({ pageState: { jobIds: [jobId], timeRange: { from: rangeFrom, to: rangeTo }, - refreshInterval: { pause: true, value: 10000 }, + refreshInterval: { pause: true, value: DEFAULT_REFRESH_INTERVAL }, ...entities, }, }); diff --git a/x-pack/plugins/apm/public/components/shared/ml_callout/index.tsx b/x-pack/plugins/apm/public/components/shared/ml_callout/index.tsx index 7ae0440bbef3a..12b4e021e2302 100644 --- a/x-pack/plugins/apm/public/components/shared/ml_callout/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/ml_callout/index.tsx @@ -9,7 +9,7 @@ import { EuiButton, EuiButtonEmpty, EuiCallOut, - EuiFlexGrid, + EuiFlexGroup, EuiFlexItem, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; @@ -167,7 +167,7 @@ export function MLCallout({ const hasAnyActions = properties.primaryAction || dismissable; const actions = hasAnyActions ? ( - + {properties.primaryAction && ( {properties.primaryAction} )} @@ -180,7 +180,7 @@ export function MLCallout({ )} - + ) : null; return ( diff --git a/x-pack/plugins/apm/public/components/shared/service_icons/icon_popover.tsx b/x-pack/plugins/apm/public/components/shared/service_icons/icon_popover.tsx index 8a90a1cffb890..4909cb77639c2 100644 --- a/x-pack/plugins/apm/public/components/shared/service_icons/icon_popover.tsx +++ b/x-pack/plugins/apm/public/components/shared/service_icons/icon_popover.tsx @@ -11,9 +11,7 @@ import { EuiPopover, EuiPopoverTitle, } from '@elastic/eui'; -import { rgba } from 'polished'; import React from 'react'; -import styled from 'styled-components'; import { PopoverItem } from '.'; import { FETCH_STATUS } from '../../../hooks/use_fetcher'; @@ -27,15 +25,6 @@ interface IconPopoverProps { icon: PopoverItem['icon']; } -const StyledButtonIcon = styled(EuiButtonIcon)` - &.serviceIcon_button { - box-shadow: ${({ theme }) => { - const shadowColor = theme.eui.euiShadowColor; - return `0px 0.7px 1.4px ${rgba(shadowColor, 0.07)}, - 0px 1.9px 4px ${rgba(shadowColor, 0.05)}, - 0px 4.5px 10px ${rgba(shadowColor, 0.05)} !important;`; - }} -`; export function IconPopover({ icon, title, @@ -54,7 +43,8 @@ export function IconPopover({ anchorPosition="downCenter" ownFocus={false} button={ - + + + + + + + + + + + + + + + diff --git a/x-pack/plugins/apm/public/components/shared/span_icon/icons/cosmos_db.svg b/x-pack/plugins/apm/public/components/shared/span_icon/icons/cosmos_db.svg new file mode 100644 index 0000000000000..26205c2292a1f --- /dev/null +++ b/x-pack/plugins/apm/public/components/shared/span_icon/icons/cosmos_db.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/x-pack/plugins/apm/public/components/shared/span_icon/icons/dynamo_db.svg b/x-pack/plugins/apm/public/components/shared/span_icon/icons/dynamo_db.svg new file mode 100644 index 0000000000000..a8f80e39c6ca3 --- /dev/null +++ b/x-pack/plugins/apm/public/components/shared/span_icon/icons/dynamo_db.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/x-pack/plugins/apm/public/components/shared/span_icon/icons/file_share_storage.svg b/x-pack/plugins/apm/public/components/shared/span_icon/icons/file_share_storage.svg new file mode 100644 index 0000000000000..9c8b135e945f3 --- /dev/null +++ b/x-pack/plugins/apm/public/components/shared/span_icon/icons/file_share_storage.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/x-pack/plugins/apm/public/components/shared/span_icon/icons/s3.svg b/x-pack/plugins/apm/public/components/shared/span_icon/icons/s3.svg new file mode 100644 index 0000000000000..1dfa8fccf7765 --- /dev/null +++ b/x-pack/plugins/apm/public/components/shared/span_icon/icons/s3.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/x-pack/plugins/apm/public/components/shared/span_icon/icons/service_bus.svg b/x-pack/plugins/apm/public/components/shared/span_icon/icons/service_bus.svg new file mode 100644 index 0000000000000..76fc82312752f --- /dev/null +++ b/x-pack/plugins/apm/public/components/shared/span_icon/icons/service_bus.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/x-pack/plugins/apm/public/components/shared/span_icon/icons/sns.svg b/x-pack/plugins/apm/public/components/shared/span_icon/icons/sns.svg new file mode 100644 index 0000000000000..f668c7019baa0 --- /dev/null +++ b/x-pack/plugins/apm/public/components/shared/span_icon/icons/sns.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/x-pack/plugins/apm/public/components/shared/span_icon/icons/sqs.svg b/x-pack/plugins/apm/public/components/shared/span_icon/icons/sqs.svg new file mode 100644 index 0000000000000..21fe40a46c9c2 --- /dev/null +++ b/x-pack/plugins/apm/public/components/shared/span_icon/icons/sqs.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/x-pack/plugins/apm/public/components/shared/span_icon/icons/storage_queue.svg b/x-pack/plugins/apm/public/components/shared/span_icon/icons/storage_queue.svg new file mode 100644 index 0000000000000..3b540975b17d1 --- /dev/null +++ b/x-pack/plugins/apm/public/components/shared/span_icon/icons/storage_queue.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/x-pack/plugins/apm/public/components/shared/span_icon/icons/table_storage.svg b/x-pack/plugins/apm/public/components/shared/span_icon/icons/table_storage.svg new file mode 100644 index 0000000000000..9c3ba79e0371e --- /dev/null +++ b/x-pack/plugins/apm/public/components/shared/span_icon/icons/table_storage.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/x-pack/plugins/apm/public/components/shared/span_links/index.tsx b/x-pack/plugins/apm/public/components/shared/span_links/index.tsx index 77d0451adca55..070993c81a2c5 100644 --- a/x-pack/plugins/apm/public/components/shared/span_links/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/span_links/index.tsx @@ -15,7 +15,7 @@ import { i18n } from '@kbn/i18n'; import React, { useMemo, useState } from 'react'; import { ProcessorEvent } from '@kbn/observability-plugin/common'; import { useApmParams } from '../../../hooks/use_apm_params'; -import { FETCH_STATUS, useFetcher } from '../../../hooks/use_fetcher'; +import { isPending, useFetcher } from '../../../hooks/use_fetcher'; import { useTimeRange } from '../../../hooks/use_time_range'; import { SpanLinksCount } from '../../app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/waterfall_helpers/waterfall_helpers'; import { KueryBar } from '../kuery_bar'; @@ -100,11 +100,7 @@ export function SpanLinks({ [spanLinksCount] ); - if ( - !data || - status === FETCH_STATUS.LOADING || - status === FETCH_STATUS.NOT_INITIATED - ) { + if (!data || isPending(status)) { return (
    diff --git a/x-pack/plugins/apm/public/components/shared/transactions_table/index.tsx b/x-pack/plugins/apm/public/components/shared/transactions_table/index.tsx index 63315c90a95d6..5b3ec70bf7af4 100644 --- a/x-pack/plugins/apm/public/components/shared/transactions_table/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/transactions_table/index.tsx @@ -21,7 +21,11 @@ import { EuiCode } from '@elastic/eui'; import { useHistory } from 'react-router-dom'; import { APIReturnType } from '../../../services/rest/create_call_apm_api'; import { useApmServiceContext } from '../../../context/apm_service/use_apm_service_context'; -import { FETCH_STATUS, useFetcher } from '../../../hooks/use_fetcher'; +import { + FETCH_STATUS, + isPending, + useFetcher, +} from '../../../hooks/use_fetcher'; import { TransactionOverviewLink } from '../links/apm/transaction_overview_link'; import { OverviewTableContainer } from '../overview_table_container'; import { getColumns } from './get_columns'; @@ -241,9 +245,9 @@ export function TransactionsTable({ const columns = getColumns({ serviceName, latencyAggregationType: latencyAggregationType as LatencyAggregationType, - transactionGroupDetailedStatisticsLoading: - transactionGroupDetailedStatisticsStatus === FETCH_STATUS.LOADING || - transactionGroupDetailedStatisticsStatus === FETCH_STATUS.NOT_INITIATED, + transactionGroupDetailedStatisticsLoading: isPending( + transactionGroupDetailedStatisticsStatus + ), transactionGroupDetailedStatistics, comparisonEnabled, shouldShowSparkPlots, diff --git a/x-pack/plugins/apm/public/context/apm_service/apm_service_context.tsx b/x-pack/plugins/apm/public/context/apm_service/apm_service_context.tsx index b9d9d4012a6c0..ce74feab48102 100644 --- a/x-pack/plugins/apm/public/context/apm_service/apm_service_context.tsx +++ b/x-pack/plugins/apm/public/context/apm_service/apm_service_context.tsx @@ -19,6 +19,7 @@ import { useApmParams } from '../../hooks/use_apm_params'; import { useTimeRange } from '../../hooks/use_time_range'; import { useFallbackToTransactionsFetcher } from '../../hooks/use_fallback_to_transactions_fetcher'; import { replace } from '../../components/shared/links/url_helpers'; +import { FETCH_STATUS } from '../../hooks/use_fetcher'; export interface APMServiceContextValue { serviceName: string; @@ -27,12 +28,14 @@ export interface APMServiceContextValue { transactionTypes: string[]; runtimeName?: string; fallbackToTransactions: boolean; + serviceAgentStatus: FETCH_STATUS; } export const APMServiceContext = createContext({ serviceName: '', transactionTypes: [], fallbackToTransactions: false, + serviceAgentStatus: FETCH_STATUS.NOT_INITIATED, }); export function ApmServiceContextProvider({ @@ -50,7 +53,11 @@ export function ApmServiceContextProvider({ const { start, end } = useTimeRange({ rangeFrom, rangeTo }); - const { agentName, runtimeName } = useServiceAgentFetcher({ + const { + agentName, + runtimeName, + status: serviceAgentStatus, + } = useServiceAgentFetcher({ serviceName, start, end, @@ -62,7 +69,7 @@ export function ApmServiceContextProvider({ end, }); - const transactionType = getOrRedirectToTransactionType({ + const currentTransactionType = getOrRedirectToTransactionType({ transactionType: query.transactionType, transactionTypes, agentName, @@ -78,34 +85,55 @@ export function ApmServiceContextProvider({ value={{ serviceName, agentName, - transactionType, + transactionType: currentTransactionType, transactionTypes, runtimeName, fallbackToTransactions, + serviceAgentStatus, }} children={children} /> ); } -export function getOrRedirectToTransactionType({ +const isTypeExistsInTransactionTypesList = ({ + transactionType, + transactionTypes, +}: { + transactionType?: string; + transactionTypes: string[]; +}): boolean => !!transactionType && transactionTypes.includes(transactionType); + +const isNoAgentAndNoTransactionTypes = ({ + transactionTypes, + agentName, +}: { + transactionTypes: string[]; + agentName?: string; +}): boolean => !agentName || transactionTypes.length === 0; + +export function getTransactionType({ transactionType, transactionTypes, agentName, - history, }: { transactionType?: string; transactionTypes: string[]; agentName?: string; - history: History; -}) { - if (transactionType && transactionTypes.includes(transactionType)) { - return transactionType; - } +}): string | undefined { + const isTransactionTypeExists = isTypeExistsInTransactionTypesList({ + transactionType, + transactionTypes, + }); - if (!agentName || transactionTypes.length === 0) { - return; - } + if (isTransactionTypeExists) return transactionType; + + const isNoAgentAndNoTransactionTypesExists = isNoAgentAndNoTransactionTypes({ + transactionTypes, + agentName, + }); + + if (isNoAgentAndNoTransactionTypesExists) return undefined; // The default transaction type is "page-load" for RUM agents and "request" for all others const defaultTransactionType = isRumAgentName(agentName) @@ -119,7 +147,42 @@ export function getOrRedirectToTransactionType({ ? defaultTransactionType : transactionTypes[0]; + return currentTransactionType; +} + +export function getOrRedirectToTransactionType({ + transactionType, + transactionTypes, + agentName, + history, +}: { + transactionType?: string; + transactionTypes: string[]; + agentName?: string; + history: History; +}) { + const isTransactionTypeExists = isTypeExistsInTransactionTypesList({ + transactionType, + transactionTypes, + }); + + if (isTransactionTypeExists) return transactionType; + + const isNoAgentAndNoTransactionTypesExists = isNoAgentAndNoTransactionTypes({ + transactionTypes, + agentName, + }); + + if (isNoAgentAndNoTransactionTypesExists) return undefined; + + const currentTransactionType = getTransactionType({ + transactionTypes, + transactionType, + agentName, + }); + // Replace transactionType in the URL in case it is not one of the types returned by the API - replace(history, { query: { transactionType: currentTransactionType } }); + replace(history, { query: { transactionType: currentTransactionType! } }); + return currentTransactionType; } diff --git a/x-pack/plugins/apm/public/context/environments_context/environments_context.tsx b/x-pack/plugins/apm/public/context/environments_context/environments_context.tsx index 23e5a79084526..a065d82bbc1ac 100644 --- a/x-pack/plugins/apm/public/context/environments_context/environments_context.tsx +++ b/x-pack/plugins/apm/public/context/environments_context/environments_context.tsx @@ -8,15 +8,18 @@ import React from 'react'; import { ENVIRONMENT_ALL } from '../../../common/environment_filter_values'; import { Environment } from '../../../common/environment_rt'; import { useApmParams } from '../../hooks/use_apm_params'; +import { useEnvironmentsFetcher } from '../../hooks/use_environments_fetcher'; import { FETCH_STATUS } from '../../hooks/use_fetcher'; import { useTimeRange } from '../../hooks/use_time_range'; -import { useEnvironmentsFetcher } from '../../hooks/use_environments_fetcher'; export const EnvironmentsContext = React.createContext<{ environment: Environment; environments: Environment[]; status: FETCH_STATUS; preferredEnvironment: Environment; + serviceName?: string; + rangeFrom?: string; + rangeTo?: string; }>({ environment: ENVIRONMENT_ALL.value, environments: [], @@ -26,8 +29,10 @@ export const EnvironmentsContext = React.createContext<{ export function EnvironmentsContextProvider({ children, + customTimeRange, }: { children: React.ReactElement; + customTimeRange?: { rangeFrom: string; rangeTo: string }; }) { const { path, query } = useApmParams('/*'); @@ -36,8 +41,11 @@ export function EnvironmentsContextProvider({ ('environment' in query && (query.environment as Environment)) || ENVIRONMENT_ALL.value; - const rangeFrom = 'rangeFrom' in query ? query.rangeFrom : undefined; - const rangeTo = 'rangeTo' in query ? query.rangeTo : undefined; + const queryRangeFrom = 'rangeFrom' in query ? query.rangeFrom : undefined; + const queryRangeTo = 'rangeTo' in query ? query.rangeTo : undefined; + + const rangeFrom = customTimeRange?.rangeFrom || queryRangeFrom; + const rangeTo = customTimeRange?.rangeTo || queryRangeTo; const { start, end } = useTimeRange({ rangeFrom, rangeTo, optional: true }); @@ -58,6 +66,9 @@ export function EnvironmentsContextProvider({ environments, status, preferredEnvironment, + serviceName, + rangeFrom, + rangeTo, }} > {children} diff --git a/x-pack/plugins/apm/public/hooks/use_fetcher.test.tsx b/x-pack/plugins/apm/public/hooks/use_fetcher.test.tsx index 279124cbcb88c..a2abc1bd66214 100644 --- a/x-pack/plugins/apm/public/hooks/use_fetcher.test.tsx +++ b/x-pack/plugins/apm/public/hooks/use_fetcher.test.tsx @@ -10,7 +10,12 @@ import React, { ReactNode } from 'react'; import { CoreStart } from '@kbn/core/public'; import { createKibanaReactContext } from '@kbn/kibana-react-plugin/public'; import { delay } from '../utils/test_helpers'; -import { FetcherResult, useFetcher } from './use_fetcher'; +import { + FetcherResult, + useFetcher, + isPending, + FETCH_STATUS, +} from './use_fetcher'; // Wrap the hook with a provider so it can useKibana const KibanaReactContext = createKibanaReactContext({ @@ -223,4 +228,18 @@ describe('useFetcher', () => { expect(secondResult === thirdResult).toEqual(false); }); }); + + describe('isPending', () => { + [FETCH_STATUS.NOT_INITIATED, FETCH_STATUS.LOADING].forEach((status) => { + it(`returns true when ${status}`, () => { + expect(isPending(status)).toBeTruthy(); + }); + }); + + [FETCH_STATUS.FAILURE, FETCH_STATUS.SUCCESS].forEach((status) => { + it(`returns false when ${status}`, () => { + expect(isPending(status)).toBeFalsy(); + }); + }); + }); }); diff --git a/x-pack/plugins/apm/public/hooks/use_fetcher.tsx b/x-pack/plugins/apm/public/hooks/use_fetcher.tsx index 66479aed8a535..80f2451863b42 100644 --- a/x-pack/plugins/apm/public/hooks/use_fetcher.tsx +++ b/x-pack/plugins/apm/public/hooks/use_fetcher.tsx @@ -26,6 +26,10 @@ export enum FETCH_STATUS { NOT_INITIATED = 'not_initiated', } +export const isPending = (fetchStatus: FETCH_STATUS) => + fetchStatus === FETCH_STATUS.LOADING || + fetchStatus === FETCH_STATUS.NOT_INITIATED; + export interface FetcherResult { data?: Data; status: FETCH_STATUS; diff --git a/x-pack/plugins/apm/server/index.ts b/x-pack/plugins/apm/server/index.ts index 17682706571cc..19b34766a1bf2 100644 --- a/x-pack/plugins/apm/server/index.ts +++ b/x-pack/plugins/apm/server/index.ts @@ -20,10 +20,10 @@ const configSchema = schema.object({ autoCreateApmDataView: schema.boolean({ defaultValue: true }), serviceMapEnabled: schema.boolean({ defaultValue: true }), serviceMapFingerprintBucketSize: schema.number({ defaultValue: 100 }), - serviceMapTraceIdBucketSize: schema.number({ defaultValue: 65 }), serviceMapFingerprintGlobalBucketSize: schema.number({ defaultValue: 1000, }), + serviceMapTraceIdBucketSize: schema.number({ defaultValue: 65 }), serviceMapTraceIdGlobalBucketSize: schema.number({ defaultValue: 6 }), serviceMapMaxTracesPerRequest: schema.number({ defaultValue: 50 }), ui: schema.object({ diff --git a/x-pack/plugins/apm/server/routes/fleet/merge_package_policy_with_apm.ts b/x-pack/plugins/apm/server/routes/fleet/merge_package_policy_with_apm.ts index 37f0705904770..1a3fcc6cd1967 100644 --- a/x-pack/plugins/apm/server/routes/fleet/merge_package_policy_with_apm.ts +++ b/x-pack/plugins/apm/server/routes/fleet/merge_package_policy_with_apm.ts @@ -12,7 +12,10 @@ import { getPackagePolicyWithAgentConfigurations, PackagePolicy, } from './register_fleet_policy_callbacks'; -import { getPackagePolicyWithSourceMap, listArtifacts } from './source_maps'; +import { + getPackagePolicyWithSourceMap, + listSourceMapArtifacts, +} from './source_maps'; export async function mergePackagePolicyWithApm({ packagePolicy, @@ -24,7 +27,7 @@ export async function mergePackagePolicyWithApm({ fleetPluginStart: NonNullable; }) { const agentConfigurations = await listConfigurations(internalESClient); - const artifacts = await listArtifacts({ fleetPluginStart }); + const { artifacts } = await listSourceMapArtifacts({ fleetPluginStart }); return getPackagePolicyWithAgentConfigurations( getPackagePolicyWithSourceMap({ packagePolicy, artifacts }), agentConfigurations diff --git a/x-pack/plugins/apm/server/routes/fleet/source_maps.ts b/x-pack/plugins/apm/server/routes/fleet/source_maps.ts index 6000b79956665..9cba744cdb689 100644 --- a/x-pack/plugins/apm/server/routes/fleet/source_maps.ts +++ b/x-pack/plugins/apm/server/routes/fleet/source_maps.ts @@ -32,37 +32,44 @@ export type FleetPluginStart = NonNullable; const doUnzip = promisify(unzip); -function decodeArtifacts(artifacts: Artifact[]): Promise { - return Promise.all( - artifacts.map(async (artifact) => { - const body = await doUnzip(Buffer.from(artifact.body, 'base64')); - return { - ...artifact, - body: JSON.parse(body.toString()) as ApmArtifactBody, - }; - }) - ); +async function unzipArtifactBody( + artifact: Artifact +): Promise { + const body = await doUnzip(Buffer.from(artifact.body, 'base64')); + + return { + ...artifact, + body: JSON.parse(body.toString()) as ApmArtifactBody, + }; } function getApmArtifactClient(fleetPluginStart: FleetPluginStart) { return fleetPluginStart.createArtifactsClient('apm'); } -export async function listArtifacts({ +export async function listSourceMapArtifacts({ fleetPluginStart, + perPage = 20, + page = 1, }: { fleetPluginStart: FleetPluginStart; + perPage?: number; + page?: number; }) { const apmArtifactClient = getApmArtifactClient(fleetPluginStart); - const fleetArtifactsResponse = await apmArtifactClient.listArtifacts({ + const artifactsResponse = await apmArtifactClient.listArtifacts({ kuery: 'type: sourcemap', - perPage: 20, - page: 1, + perPage, + page, sortOrder: 'desc', sortField: 'created', }); - return decodeArtifacts(fleetArtifactsResponse.items); + const artifacts = await Promise.all( + artifactsResponse.items.map(unzipArtifactBody) + ); + + return { artifacts, total: artifactsResponse.total }; } export async function createApmArtifact({ @@ -141,8 +148,7 @@ export async function updateSourceMapsOnFleetPolicies({ savedObjectsClient: SavedObjectsClientContract; elasticsearchClient: ElasticsearchClient; }) { - const artifacts = await listArtifacts({ fleetPluginStart }); - + const { artifacts } = await listSourceMapArtifacts({ fleetPluginStart }); const apmFleetPolicies = await getApmPackagePolicies({ core, fleetPluginStart, diff --git a/x-pack/plugins/apm/server/routes/metrics/serverless/get_compute_usage_chart.ts b/x-pack/plugins/apm/server/routes/metrics/serverless/get_compute_usage_chart.ts index 725cc7e893025..5464c6364d8f0 100644 --- a/x-pack/plugins/apm/server/routes/metrics/serverless/get_compute_usage_chart.ts +++ b/x-pack/plugins/apm/server/routes/metrics/serverless/get_compute_usage_chart.ts @@ -25,7 +25,13 @@ import { environmentQuery } from '../../../../common/utils/environment_query'; import { getMetricsDateHistogramParams } from '../../../lib/helpers/metrics'; import { GenericMetricsChart } from '../fetch_and_transform_metrics'; import { APMEventClient } from '../../../lib/helpers/create_es_client/create_apm_event_client'; -import { calcComputeUsageGBSeconds } from './helper'; +import { convertComputeUsageToGbSec } from './helper'; + +export const computeUsageAvgScript = { + avg: { + script: `return doc['${METRIC_SYSTEM_TOTAL_MEMORY}'].value * doc['${FAAS_BILLED_DURATION}'].value`, + }, +}; export async function getComputeUsageChart({ environment, @@ -47,9 +53,8 @@ export async function getComputeUsageChart({ serverlessId?: string; }): Promise { const aggs = { - avgFaasBilledDuration: { avg: { field: FAAS_BILLED_DURATION } }, - avgTotalMemory: { avg: { field: METRIC_SYSTEM_TOTAL_MEMORY } }, countInvocations: { value_count: { field: FAAS_BILLED_DURATION } }, + avgComputeUsageBytesMs: computeUsageAvgScript, }; const params = { @@ -117,17 +122,16 @@ export async function getComputeUsageChart({ key: 'compute_usage', type: 'bar', overallValue: - calcComputeUsageGBSeconds({ - billedDuration: aggregations?.avgFaasBilledDuration.value, - totalMemory: aggregations?.avgTotalMemory.value, + convertComputeUsageToGbSec({ + computeUsageBytesMs: + aggregations?.avgComputeUsageBytesMs.value, countInvocations: aggregations?.countInvocations.value, }) ?? 0, color: theme.euiColorVis0, data: timeseriesData.buckets.map((bucket) => { const computeUsage = - calcComputeUsageGBSeconds({ - billedDuration: bucket.avgFaasBilledDuration.value, - totalMemory: bucket.avgTotalMemory.value, + convertComputeUsageToGbSec({ + computeUsageBytesMs: bucket.avgComputeUsageBytesMs.value, countInvocations: bucket.countInvocations.value, }) ?? 0; return { diff --git a/x-pack/plugins/apm/server/routes/metrics/serverless/get_serverless_summary.ts b/x-pack/plugins/apm/server/routes/metrics/serverless/get_serverless_summary.ts index 4e5bac125d2f3..c8d3a1db23dce 100644 --- a/x-pack/plugins/apm/server/routes/metrics/serverless/get_serverless_summary.ts +++ b/x-pack/plugins/apm/server/routes/metrics/serverless/get_serverless_summary.ts @@ -22,7 +22,12 @@ import { } from '../../../../common/es_fields/apm'; import { environmentQuery } from '../../../../common/utils/environment_query'; import { APMEventClient } from '../../../lib/helpers/create_es_client/create_apm_event_client'; -import { calcEstimatedCost, calcMemoryUsedRate } from './helper'; +import { computeUsageAvgScript } from './get_compute_usage_chart'; +import { + calcEstimatedCost, + calcMemoryUsedRate, + convertComputeUsageToGbSec, +} from './helper'; export type AwsLambdaArchitecture = 'arm' | 'x86_64'; @@ -121,6 +126,7 @@ export async function getServerlessSummary({ avgTotalMemory: { avg: { field: METRIC_SYSTEM_TOTAL_MEMORY } }, avgFreeMemory: { avg: { field: METRIC_SYSTEM_FREE_MEMORY } }, countInvocations: { value_count: { field: FAAS_BILLED_DURATION } }, + avgComputeUsageBytesMs: computeUsageAvgScript, sample: { top_metrics: { metrics: [{ field: HOST_ARCHITECTURE }], @@ -159,9 +165,11 @@ export async function getServerlessSummary({ HOST_ARCHITECTURE ] as AwsLambdaArchitecture | undefined, transactionThroughput, - billedDuration: response.aggregations?.faasBilledDurationAvg.value, - totalMemory: response.aggregations?.avgTotalMemory.value, - countInvocations: response.aggregations?.countInvocations.value, + computeUsageGbSec: convertComputeUsageToGbSec({ + computeUsageBytesMs: + response.aggregations?.avgComputeUsageBytesMs.value, + countInvocations: response.aggregations?.countInvocations.value, + }), }), }; } diff --git a/x-pack/plugins/apm/server/routes/metrics/serverless/helper.test.ts b/x-pack/plugins/apm/server/routes/metrics/serverless/helper.test.ts index 23b322663353e..7e17f34e057c9 100644 --- a/x-pack/plugins/apm/server/routes/metrics/serverless/helper.test.ts +++ b/x-pack/plugins/apm/server/routes/metrics/serverless/helper.test.ts @@ -8,7 +8,32 @@ import { calcMemoryUsed, calcMemoryUsedRate, calcEstimatedCost, + convertComputeUsageToGbSec, } from './helper'; + +describe('convertComputeUsageToGbSec', () => { + it('returns undefined', () => { + [ + { computeUsageBytesMs: undefined, countInvocations: 1 }, + { computeUsageBytesMs: null, countInvocations: 1 }, + { computeUsageBytesMs: 1, countInvocations: undefined }, + { computeUsageBytesMs: 1, countInvocations: null }, + ].forEach(({ computeUsageBytesMs, countInvocations }) => { + expect( + convertComputeUsageToGbSec({ computeUsageBytesMs, countInvocations }) + ).toBeUndefined(); + }); + }); + + it('converts to gb sec', () => { + const totalMemory = 536870912; // 0.5gb + const billedDuration = 4000; + const computeUsageBytesMs = totalMemory * billedDuration; + expect( + convertComputeUsageToGbSec({ computeUsageBytesMs, countInvocations: 1 }) + ).toBe(computeUsageBytesMs / 1024 ** 3 / 1000); + }); +}); describe('calcMemoryUsed', () => { it('returns undefined when memory values are no a number', () => { [ @@ -49,13 +74,19 @@ const AWS_LAMBDA_PRICE_FACTOR = { }; describe('calcEstimatedCost', () => { + const totalMemory = 536870912; // 0.5gb + const billedDuration = 4000; + const computeUsageBytesMs = totalMemory * billedDuration; + const computeUsageGbSec = convertComputeUsageToGbSec({ + computeUsageBytesMs, + countInvocations: 1, + }); it('returns undefined when price factor is not defined', () => { expect( calcEstimatedCost({ - totalMemory: 1, - billedDuration: 1, transactionThroughput: 1, architecture: 'arm', + computeUsageGbSec, }) ).toBeUndefined(); }); @@ -63,10 +94,9 @@ describe('calcEstimatedCost', () => { it('returns undefined when architecture is not defined', () => { expect( calcEstimatedCost({ - totalMemory: 1, - billedDuration: 1, transactionThroughput: 1, awsLambdaPriceFactor: AWS_LAMBDA_PRICE_FACTOR, + computeUsageGbSec, }) ).toBeUndefined(); }); @@ -84,11 +114,10 @@ describe('calcEstimatedCost', () => { it('returns undefined when request cost per million is not defined', () => { expect( calcEstimatedCost({ - totalMemory: 1, - billedDuration: 1, transactionThroughput: 1, awsLambdaPriceFactor: AWS_LAMBDA_PRICE_FACTOR, architecture: 'arm', + computeUsageGbSec, }) ).toBeUndefined(); }); @@ -100,11 +129,9 @@ describe('calcEstimatedCost', () => { calcEstimatedCost({ awsLambdaPriceFactor: AWS_LAMBDA_PRICE_FACTOR, architecture, - billedDuration: 4000, - totalMemory: 536870912, // 0.5gb transactionThroughput: 100000, awsLambdaRequestCostPerMillion: 0.2, - countInvocations: 1, + computeUsageGbSec, }) ).toEqual(0.03); }); @@ -112,15 +139,20 @@ describe('calcEstimatedCost', () => { describe('arm architecture', () => { const architecture = 'arm'; it('returns correct cost', () => { + const _totalMemory = 536870912; // 0.5gb + const _billedDuration = 8000; + const _computeUsageBytesMs = _totalMemory * _billedDuration; + const _computeUsageGbSec = convertComputeUsageToGbSec({ + computeUsageBytesMs: _computeUsageBytesMs, + countInvocations: 1, + }); expect( calcEstimatedCost({ awsLambdaPriceFactor: AWS_LAMBDA_PRICE_FACTOR, architecture, - billedDuration: 8000, - totalMemory: 536870912, // 0.5gb transactionThroughput: 200000, awsLambdaRequestCostPerMillion: 0.2, - countInvocations: 1, + computeUsageGbSec: _computeUsageGbSec, }) ).toEqual(0.05); }); diff --git a/x-pack/plugins/apm/server/routes/metrics/serverless/helper.ts b/x-pack/plugins/apm/server/routes/metrics/serverless/helper.ts index df3ba8c4b9898..9842ebc6a3382 100644 --- a/x-pack/plugins/apm/server/routes/metrics/serverless/helper.ts +++ b/x-pack/plugins/apm/server/routes/metrics/serverless/helper.ts @@ -44,57 +44,43 @@ const GB = 1024 ** 3; * But the result of this calculation is in Bytes-milliseconds, as the "system.memory.total" is stored in bytes and the "faas.billed_duration" is stored in milliseconds. * But to calculate the overall cost AWS uses GB-second, so we need to convert the result to this unit. */ -export function calcComputeUsageGBSeconds({ - billedDuration, - totalMemory, +export function convertComputeUsageToGbSec({ + computeUsageBytesMs, countInvocations, }: { - billedDuration?: number | null; - totalMemory?: number | null; + computeUsageBytesMs?: number | null; countInvocations?: number | null; }) { if ( - !isFiniteNumber(billedDuration) || - !isFiniteNumber(totalMemory) || + !isFiniteNumber(computeUsageBytesMs) || !isFiniteNumber(countInvocations) ) { return undefined; } - - const totalMemoryGB = totalMemory / GB; - const faasBilledDurationSec = billedDuration / 1000; - return totalMemoryGB * faasBilledDurationSec * countInvocations; + const computeUsageGbSec = computeUsageBytesMs / GB / 1000; + return computeUsageGbSec * countInvocations; } export function calcEstimatedCost({ awsLambdaPriceFactor, architecture, transactionThroughput, - billedDuration, - totalMemory, awsLambdaRequestCostPerMillion, - countInvocations, + computeUsageGbSec, }: { awsLambdaPriceFactor?: AWSLambdaPriceFactor; architecture?: AwsLambdaArchitecture; transactionThroughput: number; - billedDuration?: number | null; - totalMemory?: number | null; awsLambdaRequestCostPerMillion?: number; - countInvocations?: number | null; + computeUsageGbSec?: number; }) { try { - const computeUsage = calcComputeUsageGBSeconds({ - billedDuration, - totalMemory, - countInvocations, - }); if ( !awsLambdaPriceFactor || !architecture || !isFiniteNumber(awsLambdaRequestCostPerMillion) || !isFiniteNumber(awsLambdaPriceFactor?.[architecture]) || - !isFiniteNumber(computeUsage) + !isFiniteNumber(computeUsageGbSec) ) { return undefined; } @@ -102,7 +88,7 @@ export function calcEstimatedCost({ const priceFactor = awsLambdaPriceFactor?.[architecture]; const estimatedCost = - computeUsage * priceFactor + + computeUsageGbSec * priceFactor + transactionThroughput * (awsLambdaRequestCostPerMillion / 1000000); // Rounds up the decimals diff --git a/x-pack/plugins/apm/server/routes/service_map/get_service_map.ts b/x-pack/plugins/apm/server/routes/service_map/get_service_map.ts index 35c422c53156e..fb1d0eed0a151 100644 --- a/x-pack/plugins/apm/server/routes/service_map/get_service_map.ts +++ b/x-pack/plugins/apm/server/routes/service_map/get_service_map.ts @@ -7,14 +7,7 @@ import { Logger } from '@kbn/core/server'; import { chunk } from 'lodash'; -import { rangeQuery, termsQuery } from '@kbn/observability-plugin/server'; -import { ProcessorEvent } from '@kbn/observability-plugin/common'; -import { - AGENT_NAME, - SERVICE_ENVIRONMENT, - SERVICE_NAME, -} from '../../../common/es_fields/apm'; -import { environmentQuery } from '../../../common/utils/environment_query'; + import { withApmSpan } from '../../utils/with_apm_span'; import { MlClient } from '../../lib/helpers/get_ml_client'; import { @@ -24,44 +17,41 @@ import { import { getServiceMapFromTraceIds } from './get_service_map_from_trace_ids'; import { getTraceSampleIds } from './get_trace_sample_ids'; import { transformServiceMapResponses } from './transform_service_map_responses'; -import { ENVIRONMENT_ALL } from '../../../common/environment_filter_values'; -import { getProcessorEventForTransactions } from '../../lib/helpers/transactions'; -import { ServiceGroup } from '../../../common/service_groups'; -import { serviceGroupQuery } from '../../lib/service_group_query'; import { APMEventClient } from '../../lib/helpers/create_es_client/create_apm_event_client'; import { APMConfig } from '../..'; +import { getServiceStats } from './get_service_stats'; export interface IEnvOptions { mlClient?: MlClient; config: APMConfig; apmEventClient: APMEventClient; - serviceNames?: string[]; + serviceName?: string; environment: string; searchAggregatedTransactions: boolean; logger: Logger; start: number; end: number; - serviceGroup: ServiceGroup | null; + serviceGroupKuery?: string; } async function getConnectionData({ config, apmEventClient, - serviceNames, + serviceName, environment, start, end, - serviceGroup, + serviceGroupKuery, }: IEnvOptions) { return withApmSpan('get_service_map_connections', async () => { const { traceIds } = await getTraceSampleIds({ config, apmEventClient, - serviceNames, + serviceName, environment, start, end, - serviceGroup, + serviceGroupKuery, }); const chunks = chunk(traceIds, config.serviceMapMaxTracesPerRequest); @@ -101,79 +91,8 @@ async function getConnectionData({ }); } -async function getServicesData( - options: IEnvOptions & { maxNumberOfServices: number } -) { - const { - environment, - apmEventClient, - searchAggregatedTransactions, - start, - end, - maxNumberOfServices, - serviceGroup, - } = options; - const params = { - apm: { - events: [ - getProcessorEventForTransactions(searchAggregatedTransactions), - ProcessorEvent.metric as const, - ProcessorEvent.error as const, - ], - }, - body: { - track_total_hits: false, - size: 0, - query: { - bool: { - filter: [ - ...rangeQuery(start, end), - ...environmentQuery(environment), - ...termsQuery(SERVICE_NAME, ...(options.serviceNames ?? [])), - ...serviceGroupQuery(serviceGroup), - ], - }, - }, - aggs: { - services: { - terms: { - field: SERVICE_NAME, - size: maxNumberOfServices, - }, - aggs: { - agent_name: { - terms: { - field: AGENT_NAME, - }, - }, - }, - }, - }, - }, - }; - - const response = await apmEventClient.search( - 'get_service_stats_for_service_map', - params - ); - - return ( - response.aggregations?.services.buckets.map((bucket) => { - return { - [SERVICE_NAME]: bucket.key as string, - [AGENT_NAME]: - (bucket.agent_name.buckets[0]?.key as string | undefined) || '', - [SERVICE_ENVIRONMENT]: - options.environment === ENVIRONMENT_ALL.value - ? null - : options.environment, - }; - }) || [] - ); -} - export type ConnectionsResponse = Awaited>; -export type ServicesResponse = Awaited>; +export type ServicesResponse = Awaited>; export function getServiceMap( options: IEnvOptions & { maxNumberOfServices: number } @@ -192,7 +111,7 @@ export function getServiceMap( const [connectionData, servicesData, anomalies] = await Promise.all([ getConnectionData(options), - getServicesData(options), + getServiceStats(options), anomaliesPromise, ]); diff --git a/x-pack/plugins/apm/server/routes/service_map/get_service_stats.ts b/x-pack/plugins/apm/server/routes/service_map/get_service_stats.ts new file mode 100644 index 0000000000000..1f43a35d3b1ef --- /dev/null +++ b/x-pack/plugins/apm/server/routes/service_map/get_service_stats.ts @@ -0,0 +1,89 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + kqlQuery, + rangeQuery, + termsQuery, +} from '@kbn/observability-plugin/server'; +import { ProcessorEvent } from '@kbn/observability-plugin/common'; +import { + AGENT_NAME, + SERVICE_ENVIRONMENT, + SERVICE_NAME, +} from '../../../common/es_fields/apm'; +import { environmentQuery } from '../../../common/utils/environment_query'; +import { ENVIRONMENT_ALL } from '../../../common/environment_filter_values'; +import { getProcessorEventForTransactions } from '../../lib/helpers/transactions'; +import { IEnvOptions } from './get_service_map'; + +export async function getServiceStats({ + environment, + apmEventClient, + searchAggregatedTransactions, + start, + end, + maxNumberOfServices, + serviceGroupKuery, + serviceName, +}: IEnvOptions & { maxNumberOfServices: number }) { + const params = { + apm: { + events: [ + getProcessorEventForTransactions(searchAggregatedTransactions), + ProcessorEvent.metric as const, + ProcessorEvent.error as const, + ], + }, + body: { + track_total_hits: false, + size: 0, + query: { + bool: { + filter: [ + ...rangeQuery(start, end), + ...environmentQuery(environment), + ...termsQuery(SERVICE_NAME, serviceName), + ...kqlQuery(serviceGroupKuery), + ], + }, + }, + aggs: { + services: { + terms: { + field: SERVICE_NAME, + size: maxNumberOfServices, + }, + aggs: { + agent_name: { + terms: { + field: AGENT_NAME, + }, + }, + }, + }, + }, + }, + }; + + const response = await apmEventClient.search( + 'get_service_stats_for_service_map', + params + ); + + return ( + response.aggregations?.services.buckets.map((bucket) => { + return { + [SERVICE_NAME]: bucket.key as string, + [AGENT_NAME]: + (bucket.agent_name.buckets[0]?.key as string | undefined) || '', + [SERVICE_ENVIRONMENT]: + environment === ENVIRONMENT_ALL.value ? null : environment, + }; + }) || [] + ); +} diff --git a/x-pack/plugins/apm/server/routes/service_map/get_trace_sample_ids.ts b/x-pack/plugins/apm/server/routes/service_map/get_trace_sample_ids.ts index d65e97c6988e5..42935dbdcda88 100644 --- a/x-pack/plugins/apm/server/routes/service_map/get_trace_sample_ids.ts +++ b/x-pack/plugins/apm/server/routes/service_map/get_trace_sample_ids.ts @@ -7,7 +7,11 @@ import Boom from '@hapi/boom'; import { sortBy, take, uniq } from 'lodash'; -import { rangeQuery } from '@kbn/observability-plugin/server'; +import { + kqlQuery, + rangeQuery, + termQuery, +} from '@kbn/observability-plugin/server'; import { ProcessorEvent } from '@kbn/observability-plugin/common'; import { asMutableArray } from '../../../common/utils/as_mutable_array'; import { @@ -18,62 +22,59 @@ import { } from '../../../common/es_fields/apm'; import { SERVICE_MAP_TIMEOUT_ERROR } from '../../../common/service_map'; import { environmentQuery } from '../../../common/utils/environment_query'; -import { serviceGroupQuery } from '../../lib/service_group_query'; -import { ServiceGroup } from '../../../common/service_groups'; + import { APMEventClient } from '../../lib/helpers/create_es_client/create_apm_event_client'; import { APMConfig } from '../..'; const MAX_TRACES_TO_INSPECT = 1000; export async function getTraceSampleIds({ - serviceNames, + serviceName, environment, config, apmEventClient, start, end, - serviceGroup, + serviceGroupKuery, }: { - serviceNames?: string[]; + serviceName?: string; environment: string; config: APMConfig; apmEventClient: APMEventClient; start: number; end: number; - serviceGroup: ServiceGroup | null; + serviceGroupKuery?: string; }) { const query = { bool: { - filter: [...rangeQuery(start, end), ...serviceGroupQuery(serviceGroup)], + filter: [ + ...rangeQuery(start, end), + ...environmentQuery(environment), + ...kqlQuery(serviceGroupKuery), + ...termQuery(SERVICE_NAME, serviceName), + ], }, }; - let events: ProcessorEvent[]; + const isGlobalServiceMap = !serviceName && !serviceGroupKuery; + let events = [ProcessorEvent.span, ProcessorEvent.transaction]; - const hasServiceNamesFilter = (serviceNames?.length ?? 0) > 0; - - if (hasServiceNamesFilter) { - query.bool.filter.push({ - terms: { [SERVICE_NAME]: serviceNames as string[] }, - }); - events = [ProcessorEvent.span, ProcessorEvent.transaction]; - } else { + // perf optimization that is only possible on the global service map with no filters + if (isGlobalServiceMap) { events = [ProcessorEvent.span]; query.bool.filter.push({ - exists: { - field: SPAN_DESTINATION_SERVICE_RESOURCE, - }, + exists: { field: SPAN_DESTINATION_SERVICE_RESOURCE }, }); } - query.bool.filter.push(...environmentQuery(environment)); + const fingerprintBucketSize = isGlobalServiceMap + ? config.serviceMapFingerprintGlobalBucketSize + : config.serviceMapFingerprintBucketSize; + + const traceIdBucketSize = isGlobalServiceMap + ? config.serviceMapTraceIdGlobalBucketSize + : config.serviceMapTraceIdBucketSize; - const fingerprintBucketSize = hasServiceNamesFilter - ? config.serviceMapFingerprintBucketSize - : config.serviceMapFingerprintGlobalBucketSize; - const traceIdBucketSize = hasServiceNamesFilter - ? config.serviceMapTraceIdBucketSize - : config.serviceMapTraceIdGlobalBucketSize; const samplerShardSize = traceIdBucketSize * 10; const params = { diff --git a/x-pack/plugins/apm/server/routes/service_map/route.ts b/x-pack/plugins/apm/server/routes/service_map/route.ts index 1cf4dd40afbcf..69f4422ad337a 100644 --- a/x-pack/plugins/apm/server/routes/service_map/route.ts +++ b/x-pack/plugins/apm/server/routes/service_map/route.ts @@ -7,7 +7,6 @@ import Boom from '@hapi/boom'; import * as t from 'io-ts'; -import { compact } from 'lodash'; import { apmServiceGroupMaxNumberOfServices } from '@kbn/observability-plugin/common'; import { isActivePlatinumLicense } from '../../../common/license_check'; import { invalidLicenseMessage } from '../../../common/service_map'; @@ -129,8 +128,6 @@ const serviceMapRoute = createApmServerRoute({ uiSettingsClient.get(apmServiceGroupMaxNumberOfServices), ]); - const serviceNames = compact([serviceName]); - const searchAggregatedTransactions = await getSearchTransactionsEvents({ apmEventClient, config, @@ -142,14 +139,14 @@ const serviceMapRoute = createApmServerRoute({ mlClient, config, apmEventClient, - serviceNames, + serviceName, environment, searchAggregatedTransactions, logger, start, end, maxNumberOfServices, - serviceGroup, + serviceGroupKuery: serviceGroup?.kuery, }); }, }); diff --git a/x-pack/plugins/apm/server/routes/services/route.ts b/x-pack/plugins/apm/server/routes/services/route.ts index 071b04faa52e7..a8ebb633aa956 100644 --- a/x-pack/plugins/apm/server/routes/services/route.ts +++ b/x-pack/plugins/apm/server/routes/services/route.ts @@ -7,7 +7,6 @@ import Boom from '@hapi/boom'; import { isoToEpochRt, jsonRt, toNumberRt } from '@kbn/io-ts-utils'; -import { enableServiceMetrics } from '@kbn/observability-plugin/common'; import * as t from 'io-ts'; import { uniq, mergeWith } from 'lodash'; import { @@ -126,7 +125,6 @@ const servicesRoute = createApmServerRoute({ probability, } = params.query; const savedObjectsClient = (await context.core).savedObjects.client; - const coreContext = await resources.context.core; const [mlClient, apmEventClient, serviceGroup, randomSampler] = await Promise.all([ @@ -138,12 +136,9 @@ const servicesRoute = createApmServerRoute({ getRandomSampler({ security, request, probability }), ]); - const serviceMetricsEnabled = - await coreContext.uiSettings.client.get(enableServiceMetrics); - const { searchAggregatedTransactions, searchAggregatedServiceMetrics } = await getServiceInventorySearchSource({ - serviceMetricsEnabled, + serviceMetricsEnabled: false, // Disable serviceMetrics for 8.5 & 8.6 config, apmEventClient, kuery, @@ -220,7 +215,6 @@ const servicesDetailedStatisticsRoute = createApmServerRoute({ request, plugins: { security }, } = resources; - const coreContext = await resources.context.core; const { environment, kuery, offset, start, end, probability } = params.query; @@ -232,12 +226,9 @@ const servicesDetailedStatisticsRoute = createApmServerRoute({ getRandomSampler({ security, request, probability }), ]); - const serviceMetricsEnabled = - await coreContext.uiSettings.client.get(enableServiceMetrics); - const { searchAggregatedTransactions, searchAggregatedServiceMetrics } = await getServiceInventorySearchSource({ - serviceMetricsEnabled, + serviceMetricsEnabled: false, // Disable serviceMetrics for 8.5 & 8.6 config, apmEventClient, kuery, diff --git a/x-pack/plugins/apm/server/routes/source_maps/route.ts b/x-pack/plugins/apm/server/routes/source_maps/route.ts index 738192377beb0..b4d005373c4be 100644 --- a/x-pack/plugins/apm/server/routes/source_maps/route.ts +++ b/x-pack/plugins/apm/server/routes/source_maps/route.ts @@ -7,12 +7,12 @@ import Boom from '@hapi/boom'; import * as t from 'io-ts'; import { SavedObjectsClientContract } from '@kbn/core/server'; -import { jsonRt } from '@kbn/io-ts-utils'; +import { jsonRt, toNumberRt } from '@kbn/io-ts-utils'; import { Artifact } from '@kbn/fleet-plugin/server'; import { createApmArtifact, deleteApmArtifact, - listArtifacts, + listSourceMapArtifacts, updateSourceMapsOnFleetPolicies, getCleanedBundleFilePath, ArtifactSourceMap, @@ -40,14 +40,28 @@ export type SourceMap = t.TypeOf; const listSourceMapRoute = createApmServerRoute({ endpoint: 'GET /api/apm/sourcemaps', options: { tags: ['access:apm'] }, + params: t.partial({ + query: t.partial({ + page: toNumberRt, + perPage: toNumberRt, + }), + }), async handler({ + params, plugins, - }): Promise<{ artifacts: ArtifactSourceMap[] } | undefined> { + }): Promise<{ artifacts: ArtifactSourceMap[]; total: number } | undefined> { + const { page, perPage } = params.query; + try { const fleetPluginStart = await plugins.fleet?.start(); if (fleetPluginStart) { - const artifacts = await listArtifacts({ fleetPluginStart }); - return { artifacts }; + const { artifacts, total } = await listSourceMapArtifacts({ + fleetPluginStart, + page, + perPage, + }); + + return { artifacts, total }; } } catch (e) { throw Boom.internal( diff --git a/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/editor.tsx b/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/editor.tsx index c96024658048c..1a09116ba1196 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/editor.tsx +++ b/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/editor.tsx @@ -52,7 +52,7 @@ const EditorArg: FC = ({ argValue, typeInstance, onValueChange, const { language } = typeInstance?.options ?? {}; return ( - + { return ( -
    +
    diff --git a/x-pack/plugins/cases/public/components/add_comment/schema.tsx b/x-pack/plugins/cases/public/components/add_comment/schema.tsx index 5df5769ef62ab..980a03e76b772 100644 --- a/x-pack/plugins/cases/public/components/add_comment/schema.tsx +++ b/x-pack/plugins/cases/public/components/add_comment/schema.tsx @@ -9,6 +9,7 @@ import type { FormSchema } from '@kbn/es-ui-shared-plugin/static/forms/hook_form import { FIELD_TYPES } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib'; import { fieldValidators } from '@kbn/es-ui-shared-plugin/static/forms/helpers'; import type { CommentRequestUserType } from '../../../common/api'; + import * as i18n from './translations'; const { emptyField } = fieldValidators; @@ -22,7 +23,7 @@ export const schema: FormSchema = { type: FIELD_TYPES.TEXTAREA, validations: [ { - validator: emptyField(i18n.COMMENT_REQUIRED), + validator: emptyField(i18n.EMPTY_COMMENTS_NOT_ALLOWED), }, ], }, diff --git a/x-pack/plugins/cases/public/components/all_cases/selector_modal/use_cases_add_to_existing_case_modal.test.tsx b/x-pack/plugins/cases/public/components/all_cases/selector_modal/use_cases_add_to_existing_case_modal.test.tsx index d14787fee4a53..cc164b2396f60 100644 --- a/x-pack/plugins/cases/public/components/all_cases/selector_modal/use_cases_add_to_existing_case_modal.test.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/selector_modal/use_cases_add_to_existing_case_modal.test.tsx @@ -83,6 +83,7 @@ describe('use cases add to existing case modal hook', () => { const defaultParams = () => { return { onRowClick: jest.fn() }; }; + beforeEach(() => { appMockRender = createAppMockRenderer(); dispatch.mockReset(); @@ -166,7 +167,7 @@ describe('use cases add to existing case modal hook', () => { expect(mockedToastSuccess).toHaveBeenCalled(); }); - it('should not call createAttachments nor show toast success when a case is not selected', async () => { + it('should not call createAttachments nor show toast success when a case is not selected', async () => { const mockBulkCreateAttachments = jest.fn(); useCreateAttachmentsMock.mockReturnValueOnce({ createAttachments: mockBulkCreateAttachments, @@ -178,11 +179,11 @@ describe('use cases add to existing case modal hook', () => { }); AllCasesSelectorModalMock.mockImplementation(({ onRowClick }) => { - onRowClick(); return null; }); const result = appMockRender.render(); + userEvent.click(result.getByTestId('open-modal')); // give a small delay for the reducer to run diff --git a/x-pack/plugins/cases/public/components/all_cases/selector_modal/use_cases_add_to_existing_case_modal.tsx b/x-pack/plugins/cases/public/components/all_cases/selector_modal/use_cases_add_to_existing_case_modal.tsx index 2936ebf56d1e3..057973f82e0d2 100644 --- a/x-pack/plugins/cases/public/components/all_cases/selector_modal/use_cases_add_to_existing_case_modal.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/selector_modal/use_cases_add_to_existing_case_modal.tsx @@ -123,6 +123,7 @@ export const useCasesAddToExistingCaseModal = (props: AddToExistingFlyoutProps = }, [closeModal, dispatch, handleOnRowClick, props] ); + return { open: openModal, close: closeModal, diff --git a/x-pack/plugins/cases/public/components/all_cases/use_all_cases_query_params.tsx b/x-pack/plugins/cases/public/components/all_cases/use_all_cases_query_params.tsx index 9a094d3f45d55..d2c03d5e6ddab 100644 --- a/x-pack/plugins/cases/public/components/all_cases/use_all_cases_query_params.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/use_all_cases_query_params.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { useCallback, useRef, useState } from 'react'; +import { useCallback, useRef, useState, useEffect } from 'react'; import { useLocation, useHistory } from 'react-router-dom'; import { isEqual } from 'lodash'; @@ -121,10 +121,12 @@ export function useAllCasesQueryParams(isModalView: boolean = false) { ] ); - if (isFirstRenderRef.current) { - persistAndUpdateQueryParams(isModalView ? DEFAULT_QUERY_PARAMS : {}); - isFirstRenderRef.current = false; - } + useEffect(() => { + if (isFirstRenderRef.current) { + persistAndUpdateQueryParams(isModalView ? DEFAULT_QUERY_PARAMS : {}); + isFirstRenderRef.current = false; + } + }, [isModalView, persistAndUpdateQueryParams]); return { queryParams, diff --git a/x-pack/plugins/cases/public/components/all_cases/use_cases_columns.tsx b/x-pack/plugins/cases/public/components/all_cases/use_cases_columns.tsx index cb08bf4b6e526..df261b613b0b2 100644 --- a/x-pack/plugins/cases/public/components/all_cases/use_cases_columns.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/use_cases_columns.tsx @@ -65,8 +65,11 @@ const LineClampedEuiBadgeGroup = euiStyled(EuiBadgeGroup)` word-break: normal; `; +// margin-right is required here because -webkit-box-orient: vertical; +// in the EuiBadgeGroup prevents us from defining gutterSize. const StyledEuiBadge = euiStyled(EuiBadge)` - max-width: 100px + max-width: 100px; + margin-right: 5px; `; // to allow for ellipsis const renderStringField = (field: string, dataTestSubj: string) => diff --git a/x-pack/plugins/cases/public/components/case_view/case_view_page.test.tsx b/x-pack/plugins/cases/public/components/case_view/case_view_page.test.tsx index 7a299257b8017..aff92e15c3f29 100644 --- a/x-pack/plugins/cases/public/components/case_view/case_view_page.test.tsx +++ b/x-pack/plugins/cases/public/components/case_view/case_view_page.test.tsx @@ -171,9 +171,9 @@ describe('CaseViewPage', () => { userEvent.click(dropdown.querySelector('button')!); await waitForEuiPopoverOpen(); userEvent.click(result.getByTestId('case-view-status-dropdown-closed')); + const updateObject = updateCaseProperty.mock.calls[0][0]; await waitFor(() => { - const updateObject = updateCaseProperty.mock.calls[0][0]; expect(updateCaseProperty).toHaveBeenCalledTimes(1); expect(updateObject.updateKey).toEqual('status'); expect(updateObject.updateValue).toEqual('closed'); @@ -187,8 +187,11 @@ describe('CaseViewPage', () => { updateKey: 'title', })); const result = appMockRenderer.render(); - expect(result.getByTestId('editable-title-loading')).toBeInTheDocument(); - expect(result.queryByTestId('editable-title-edit-icon')).not.toBeInTheDocument(); + + await waitFor(() => { + expect(result.getByTestId('editable-title-loading')).toBeInTheDocument(); + expect(result.queryByTestId('editable-title-edit-icon')).not.toBeInTheDocument(); + }); }); it('should display description isLoading', async () => { @@ -197,13 +200,17 @@ describe('CaseViewPage', () => { isLoading: true, updateKey: 'description', })); + const result = appMockRenderer.render(); - expect( - within(result.getByTestId('description-action')).getByTestId('user-action-title-loading') - ).toBeInTheDocument(); - expect( - within(result.getByTestId('description-action')).queryByTestId('property-actions') - ).not.toBeInTheDocument(); + + await waitFor(() => { + expect( + within(result.getByTestId('description-action')).getByTestId('user-action-title-loading') + ).toBeInTheDocument(); + expect( + within(result.getByTestId('description-action')).queryByTestId('property-actions') + ).not.toBeInTheDocument(); + }); }); it('should display tags isLoading', async () => { @@ -212,11 +219,18 @@ describe('CaseViewPage', () => { isLoading: true, updateKey: 'tags', })); + const result = appMockRenderer.render(); - expect( - within(result.getByTestId('case-view-tag-list')).getByTestId('tag-list-loading') - ).toBeInTheDocument(); - expect(result.queryByTestId('tag-list-edit')).not.toBeInTheDocument(); + + await waitFor(() => { + expect( + within(result.getByTestId('case-view-tag-list')).getByTestId('tag-list-loading') + ).toBeInTheDocument(); + }); + + await waitFor(() => { + expect(result.queryByTestId('tag-list-edit')).not.toBeInTheDocument(); + }); }); it('should update title', async () => { @@ -228,8 +242,10 @@ describe('CaseViewPage', () => { userEvent.click(result.getByTestId('editable-title-submit-btn')); const updateObject = updateCaseProperty.mock.calls[0][0]; - expect(updateObject.updateKey).toEqual('title'); - expect(updateObject.updateValue).toEqual(newTitle); + await waitFor(() => { + expect(updateObject.updateKey).toEqual('title'); + expect(updateObject.updateValue).toEqual(newTitle); + }); }); it('should push updates on button click', async () => { @@ -244,6 +260,7 @@ describe('CaseViewPage', () => { const result = appMockRenderer.render(); expect(result.getByTestId('has-data-to-push-button')).toBeInTheDocument(); + userEvent.click(result.getByTestId('push-to-external-service')); await waitFor(() => { @@ -260,7 +277,9 @@ describe('CaseViewPage', () => { }} /> ); - expect(result.getByTestId('push-to-external-service')).toBeDisabled(); + await waitFor(() => { + expect(result.getByTestId('push-to-external-service')).toBeDisabled(); + }); }); it('should update connector', async () => { @@ -328,8 +347,10 @@ describe('CaseViewPage', () => { const result = appMockRenderer.render( ); - expect(result.getByTestId('case-view-loading-content')).toBeInTheDocument(); - expect(result.queryByTestId('user-actions')).not.toBeInTheDocument(); + await waitFor(() => { + expect(result.getByTestId('case-view-loading-content')).toBeInTheDocument(); + expect(result.queryByTestId('user-actions')).not.toBeInTheDocument(); + }); }); it('should call show alert details with expected arguments', async () => { @@ -348,19 +369,21 @@ describe('CaseViewPage', () => { it('should show the rule name', async () => { const result = appMockRenderer.render(); - expect( - result - .getByTestId('user-action-alert-comment-create-action-alert-action-id') - .querySelector('.euiCommentEvent__headerEvent') - ).toHaveTextContent('added an alert from Awesome rule'); + await waitFor(() => { + expect( + result + .getByTestId('user-action-alert-comment-create-action-alert-action-id') + .querySelector('.euiCommentEvent__headerEvent') + ).toHaveTextContent('added an alert from Awesome rule'); + }); }); it('should update settings', async () => { const result = appMockRenderer.render(); userEvent.click(result.getByTestId('sync-alerts-switch')); + const updateObject = updateCaseProperty.mock.calls[0][0]; await waitFor(() => { - const updateObject = updateCaseProperty.mock.calls[0][0]; expect(updateObject.updateKey).toEqual('settings'); expect(updateObject.updateValue).toEqual({ syncAlerts: false }); }); @@ -379,6 +402,7 @@ describe('CaseViewPage', () => { const result = appMockRenderer.render( ); + await waitFor(() => { expect(result.getByTestId('has-data-to-push-button')).toHaveTextContent('My Connector 2'); }); diff --git a/x-pack/plugins/cases/public/components/case_view/components/case_view_activity.tsx b/x-pack/plugins/cases/public/components/case_view/components/case_view_activity.tsx index 1fa5e94e6df98..dc1db2651f5bf 100644 --- a/x-pack/plugins/cases/public/components/case_view/components/case_view_activity.tsx +++ b/x-pack/plugins/cases/public/components/case_view/components/case_view_activity.tsx @@ -189,7 +189,7 @@ export const CaseViewActivity = ({ )} - + {caseAssignmentAuthorized ? ( <> { + let appMock: AppMockRenderer; + const props = { + title: 'My title', + confirmButtonText: 'My confirm button text', + cancelButtonText: 'My cancel button text', + onCancel: jest.fn(), + onConfirm: jest.fn(), + }; + + beforeEach(() => { + appMock = createAppMockRenderer(); + jest.clearAllMocks(); + }); + + it('renders correctly', async () => { + const result = appMock.render(); + + expect(result.getByTestId('cancel-creation-confirmation-modal')).toBeInTheDocument(); + expect(result.getByText(props.title)).toBeInTheDocument(); + expect(result.getByText(props.confirmButtonText)).toBeInTheDocument(); + expect(result.getByText(props.cancelButtonText)).toBeInTheDocument(); + }); + + it('calls onConfirm', async () => { + const result = appMock.render(); + + expect(result.getByText(props.confirmButtonText)).toBeInTheDocument(); + userEvent.click(result.getByText(props.confirmButtonText)); + + expect(props.onConfirm).toHaveBeenCalled(); + }); + + it('calls onCancel', async () => { + const result = appMock.render(); + + expect(result.getByText(props.cancelButtonText)).toBeInTheDocument(); + userEvent.click(result.getByText(props.cancelButtonText)); + + expect(props.onCancel).toHaveBeenCalled(); + }); +}); diff --git a/x-pack/plugins/cases/public/components/create/cancel_creation_confirmation_modal.tsx b/x-pack/plugins/cases/public/components/create/cancel_creation_confirmation_modal.tsx new file mode 100644 index 0000000000000..0f73d90a60986 --- /dev/null +++ b/x-pack/plugins/cases/public/components/create/cancel_creation_confirmation_modal.tsx @@ -0,0 +1,41 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import type { EuiConfirmModalProps } from '@elastic/eui'; +import { EuiConfirmModal } from '@elastic/eui'; +import * as i18n from './translations'; + +type Props = Pick< + EuiConfirmModalProps, + 'title' | 'confirmButtonText' | 'cancelButtonText' | 'onConfirm' | 'onCancel' +>; + +const CancelCreationConfirmationModalComponent: React.FC = ({ + title, + confirmButtonText = i18n.CONFIRM_MODAL_BUTTON, + cancelButtonText = i18n.CANCEL_MODAL_BUTTON, + onConfirm, + onCancel, +}) => { + return ( + + ); +}; + +CancelCreationConfirmationModalComponent.displayName = 'CancelCreationConfirmationModal'; + +export const CancelCreationConfirmationModal = React.memo(CancelCreationConfirmationModalComponent); diff --git a/x-pack/plugins/cases/public/components/create/form.tsx b/x-pack/plugins/cases/public/components/create/form.tsx index 4ec587667e18d..68ec55bbc956b 100644 --- a/x-pack/plugins/cases/public/components/create/form.tsx +++ b/x-pack/plugins/cases/public/components/create/form.tsx @@ -38,6 +38,8 @@ import { useAvailableCasesOwners } from '../app/use_available_owners'; import type { CaseAttachmentsWithoutOwner } from '../../types'; import { Severity } from './severity'; import { Assignees } from './assignees'; +import { useCancelCreationAction } from './use_cancel_creation_action'; +import { CancelCreationConfirmationModal } from './cancel_creation_confirmation_modal'; interface ContainerProps { big?: boolean; @@ -184,45 +186,59 @@ export const CreateCaseForm: React.FC = React.memo( timelineIntegration, attachments, initialValue, - }) => ( - - - - - - - - {i18n.CANCEL} - - - - - - - - - - - ) + }) => { + const { showConfirmationModal, onOpenModal, onConfirmModal, onCancelModal } = + useCancelCreationAction({ + onConfirmationCallback: onCancel, + }); + + return ( + + + + + + + + {i18n.CANCEL} + + {showConfirmationModal && ( + + )} + + + + + + + + + + ); + } ); CreateCaseForm.displayName = 'CreateCaseForm'; diff --git a/x-pack/plugins/cases/public/components/create/form_context.test.tsx b/x-pack/plugins/cases/public/components/create/form_context.test.tsx index 1454046547516..ff65444c914c3 100644 --- a/x-pack/plugins/cases/public/components/create/form_context.test.tsx +++ b/x-pack/plugins/cases/public/components/create/form_context.test.tsx @@ -446,7 +446,8 @@ describe('Create case', () => { }); }); - describe('Step 2 - Connector Fields', () => { + // FLAKY: https://github.com/elastic/kibana/issues/143408 + describe.skip('Step 2 - Connector Fields', () => { it(`should submit and push to resilient connector`, async () => { useGetConnectorsMock.mockReturnValue({ ...sampleConnectorData, diff --git a/x-pack/plugins/cases/public/components/create/index.test.tsx b/x-pack/plugins/cases/public/components/create/index.test.tsx index bd2ef3ca1068f..24798c114fede 100644 --- a/x-pack/plugins/cases/public/components/create/index.test.tsx +++ b/x-pack/plugins/cases/public/components/create/index.test.tsx @@ -8,7 +8,8 @@ import React from 'react'; import type { ReactWrapper } from 'enzyme'; import { mount } from 'enzyme'; -import { act } from '@testing-library/react'; +import { act, waitFor } from '@testing-library/react'; + import type { EuiComboBoxOptionOption } from '@elastic/eui'; import { EuiComboBox } from '@elastic/eui'; @@ -105,16 +106,66 @@ describe('CreateCase case', () => { }); }); - it('should call cancel on cancel click', async () => { + it('should open modal on cancel click', async () => { const wrapper = mount( ); - await act(async () => { - wrapper.find(`[data-test-subj="create-case-cancel"]`).first().simulate('click'); + + wrapper.find(`[data-test-subj="create-case-cancel"]`).first().simulate('click'); + + await waitFor(() => { + expect( + wrapper.find(`[data-test-subj="cancel-creation-confirmation-modal"]`).exists() + ).toBeTruthy(); + }); + }); + + it('should confirm cancelation on modal confirm click', async () => { + const wrapper = mount( + + + + ); + + wrapper.find(`[data-test-subj="create-case-cancel"]`).first().simulate('click'); + + await waitFor(() => { + expect( + wrapper.find(`[data-test-subj="cancel-creation-confirmation-modal"]`).exists() + ).toBeTruthy(); + }); + + wrapper.find(`button[data-test-subj="confirmModalConfirmButton"]`).simulate('click'); + + await waitFor(() => { + expect(defaultProps.onCancel).toHaveBeenCalled(); + }); + }); + + it('should close modal on modal cancel click', async () => { + const wrapper = mount( + + + + ); + + wrapper.find(`[data-test-subj="create-case-cancel"]`).first().simulate('click'); + + await waitFor(() => { + expect( + wrapper.find(`[data-test-subj="cancel-creation-confirmation-modal"]`).exists() + ).toBeTruthy(); + }); + + wrapper.find(`button[data-test-subj="confirmModalCancelButton"]`).simulate('click'); + + await waitFor(() => { + expect( + wrapper.find(`[data-test-subj="cancel-creation-confirmation-modal"]`).exists() + ).toBeFalsy(); }); - expect(defaultProps.onCancel).toHaveBeenCalled(); }); it('should redirect to new case when posting the case', async () => { @@ -128,6 +179,7 @@ describe('CreateCase case', () => { fillForm(wrapper); wrapper.find(`button[data-test-subj="create-case-submit"]`).first().simulate('click'); }); + expect(defaultProps.onSuccess).toHaveBeenCalled(); }); }); diff --git a/x-pack/plugins/cases/public/components/create/index.tsx b/x-pack/plugins/cases/public/components/create/index.tsx index 386b64f04bd1c..a765bb0f7b801 100644 --- a/x-pack/plugins/cases/public/components/create/index.tsx +++ b/x-pack/plugins/cases/public/components/create/index.tsx @@ -9,6 +9,7 @@ import React from 'react'; import { Field } from '@kbn/es-ui-shared-plugin/static/forms/components'; import { getUseField } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib'; +import { EuiPageSection } from '@elastic/eui'; import * as i18n from './translations'; import type { CreateCaseFormProps } from './form'; import { CreateCaseForm } from './form'; @@ -23,7 +24,7 @@ export const CreateCase = React.memo( useCasesBreadcrumbs(CasesDeepLinkId.casesCreate); return ( - <> + ( timelineIntegration={timelineIntegration} withSteps={withSteps} /> - + ); } ); diff --git a/x-pack/plugins/cases/public/components/create/translations.ts b/x-pack/plugins/cases/public/components/create/translations.ts index 780a1bbd1d02f..4bb7471e1c648 100644 --- a/x-pack/plugins/cases/public/components/create/translations.ts +++ b/x-pack/plugins/cases/public/components/create/translations.ts @@ -29,3 +29,15 @@ export const SYNC_ALERTS_LABEL = i18n.translate('xpack.cases.create.syncAlertsLa export const ASSIGN_YOURSELF = i18n.translate('xpack.cases.create.assignYourself', { defaultMessage: 'Assign yourself', }); + +export const MODAL_TITLE = i18n.translate('xpack.cases.create.modalTitle', { + defaultMessage: 'Discard case?', +}); + +export const CANCEL_MODAL_BUTTON = i18n.translate('xpack.cases.create.cancelModalButton', { + defaultMessage: 'Cancel', +}); + +export const CONFIRM_MODAL_BUTTON = i18n.translate('xpack.cases.create.confirmModalButton', { + defaultMessage: 'Exit without saving', +}); diff --git a/x-pack/plugins/cases/public/components/create/use_cancel_creation_action.test.tsx b/x-pack/plugins/cases/public/components/create/use_cancel_creation_action.test.tsx new file mode 100644 index 0000000000000..4174d33c44d2f --- /dev/null +++ b/x-pack/plugins/cases/public/components/create/use_cancel_creation_action.test.tsx @@ -0,0 +1,78 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { renderHook, act } from '@testing-library/react-hooks'; +import type { AppMockRenderer } from '../../common/mock'; +import { createAppMockRenderer } from '../../common/mock'; +import { useCancelCreationAction } from './use_cancel_creation_action'; + +describe('UseConfirmationModal', () => { + let appMockRender: AppMockRenderer; + const onConfirmationCallback = jest.fn(); + + beforeEach(() => { + appMockRender = createAppMockRenderer(); + jest.clearAllMocks(); + }); + + it('init', async () => { + const { result } = renderHook(() => useCancelCreationAction({ onConfirmationCallback }), { + wrapper: appMockRender.AppWrapper, + }); + + expect(result.current.showConfirmationModal).toBe(false); + }); + + it('opens the modal', async () => { + const { result } = renderHook(() => useCancelCreationAction({ onConfirmationCallback }), { + wrapper: appMockRender.AppWrapper, + }); + + act(() => { + result.current.onOpenModal(); + }); + + expect(result.current.showConfirmationModal).toBe(true); + }); + + it('closes the modal', async () => { + const { result } = renderHook(() => useCancelCreationAction({ onConfirmationCallback }), { + wrapper: appMockRender.AppWrapper, + }); + + act(() => { + result.current.onOpenModal(); + }); + + expect(result.current.showConfirmationModal).toBe(true); + + act(() => { + result.current.onCancelModal(); + }); + + expect(result.current.showConfirmationModal).toBe(false); + }); + + it('calls onConfirmationCallback on confirm', async () => { + const { result } = renderHook(() => useCancelCreationAction({ onConfirmationCallback }), { + wrapper: appMockRender.AppWrapper, + }); + + act(() => { + result.current.onOpenModal(); + }); + + expect(result.current.showConfirmationModal).toBe(true); + + act(() => { + result.current.onConfirmModal(); + }); + + expect(result.current.showConfirmationModal).toBe(false); + expect(onConfirmationCallback).toHaveBeenCalled(); + }); +}); diff --git a/x-pack/plugins/cases/public/components/create/use_cancel_creation_action.tsx b/x-pack/plugins/cases/public/components/create/use_cancel_creation_action.tsx new file mode 100644 index 0000000000000..461125b739ee7 --- /dev/null +++ b/x-pack/plugins/cases/public/components/create/use_cancel_creation_action.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 { useCallback, useState } from 'react'; + +interface Props { + onConfirmationCallback: () => void; +} + +export const useCancelCreationAction = ({ onConfirmationCallback }: Props) => { + const [showConfirmationModal, setShowConfirmationModal] = useState(false); + + const onOpenModal = useCallback(() => { + setShowConfirmationModal(true); + }, []); + + const onConfirmModal = useCallback(() => { + setShowConfirmationModal(false); + onConfirmationCallback(); + }, [onConfirmationCallback]); + + const onCancelModal = useCallback(() => { + setShowConfirmationModal(false); + }, []); + + return { showConfirmationModal, onOpenModal, onConfirmModal, onCancelModal }; +}; diff --git a/x-pack/plugins/cases/public/components/use_create_case_modal/create_case_modal.test.tsx b/x-pack/plugins/cases/public/components/use_create_case_modal/create_case_modal.test.tsx index 9ee4a78b7d817..e1ffe92e0c4c1 100644 --- a/x-pack/plugins/cases/public/components/use_create_case_modal/create_case_modal.test.tsx +++ b/x-pack/plugins/cases/public/components/use_create_case_modal/create_case_modal.test.tsx @@ -61,7 +61,7 @@ describe('CreateCaseModal', () => { ); - wrapper.find('.euiModal__closeIcon').first().simulate('click'); + wrapper.find('button.euiModal__closeIcon').first().simulate('click'); expect(onCloseCaseModal).toBeCalled(); }); diff --git a/x-pack/plugins/cases/public/components/user_actions/markdown_form.test.tsx b/x-pack/plugins/cases/public/components/user_actions/markdown_form.test.tsx index f9fb8594ea51e..c6b6c0e59f004 100644 --- a/x-pack/plugins/cases/public/components/user_actions/markdown_form.test.tsx +++ b/x-pack/plugins/cases/public/components/user_actions/markdown_form.test.tsx @@ -16,6 +16,7 @@ const onChangeEditable = jest.fn(); const onSaveContent = jest.fn(); const newValue = 'Hello from Tehas'; +const emptyValue = ''; const hyperlink = `[hyperlink](http://elastic.co)`; const defaultProps = { content: `A link to a timeline ${hyperlink}`, @@ -61,6 +62,7 @@ describe('UserActionMarkdown ', () => { expect(onChangeEditable).toHaveBeenCalledWith(defaultProps.id); }); }); + it('Does not call onSaveContent if no change from current text', async () => { const wrapper = mount( @@ -75,6 +77,28 @@ describe('UserActionMarkdown ', () => { }); expect(onSaveContent).not.toHaveBeenCalled(); }); + + it('Save button disabled if current text is empty', async () => { + const wrapper = mount( + + + + ); + + wrapper + .find(`.euiMarkdownEditorTextArea`) + .first() + .simulate('change', { + target: { value: emptyValue }, + }); + + await waitFor(() => { + expect( + wrapper.find(`button[data-test-subj="user-action-save-markdown"]`).first().prop('disabled') + ).toBeTruthy(); + }); + }); + it('Cancel button click calls only onChangeEditable', async () => { const wrapper = mount( diff --git a/x-pack/plugins/cases/public/components/user_actions/markdown_form.tsx b/x-pack/plugins/cases/public/components/user_actions/markdown_form.tsx index b2b7443e001e8..42abc55f336c9 100644 --- a/x-pack/plugins/cases/public/components/user_actions/markdown_form.tsx +++ b/x-pack/plugins/cases/public/components/user_actions/markdown_form.tsx @@ -5,15 +5,14 @@ * 2.0. */ -import { EuiFlexGroup, EuiFlexItem, EuiButtonEmpty, EuiButton } from '@elastic/eui'; -import React, { forwardRef, useCallback, useImperativeHandle, useMemo, useRef } from 'react'; +import React, { forwardRef, useCallback, useImperativeHandle, useRef } from 'react'; import styled from 'styled-components'; import { Form, useForm, UseField } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib'; -import * as i18n from '../case_view/translations'; import type { Content } from './schema'; import { schema } from './schema'; import { MarkdownRenderer, MarkdownEditorForm } from '../markdown_editor'; +import { UserActionMarkdownFooter } from './markdown_form_footer'; export const ContentWrapper = styled.div` padding: ${({ theme }) => `${theme.eui.euiSizeM} ${theme.eui.euiSizeL}`}; @@ -66,36 +65,6 @@ const UserActionMarkdownComponent = forwardRef< [setFieldValue] ); - const EditorButtons = useMemo( - () => ( - - - - {i18n.CANCEL} - - - - - {i18n.SAVE} - - - - ), - [handleCancelAction, handleSaveAction] - ); - useImperativeHandle(ref, () => ({ setComment, editor: editorRef.current, @@ -111,7 +80,12 @@ const UserActionMarkdownComponent = forwardRef< 'aria-label': 'Cases markdown editor', value: content, id, - bottomRightContent: EditorButtons, + bottomRightContent: ( + + ), }} /> diff --git a/x-pack/plugins/cases/public/components/user_actions/markdown_form_footer.tsx b/x-pack/plugins/cases/public/components/user_actions/markdown_form_footer.tsx new file mode 100644 index 0000000000000..ad047c68313f1 --- /dev/null +++ b/x-pack/plugins/cases/public/components/user_actions/markdown_form_footer.tsx @@ -0,0 +1,57 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiFlexGroup, EuiFlexItem, EuiButtonEmpty, EuiButton } from '@elastic/eui'; +import React from 'react'; + +import { useFormData } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib'; + +import * as i18n from '../case_view/translations'; + +interface UserActionMarkdownFooterProps { + handleSaveAction: () => Promise; + handleCancelAction: () => void; +} + +const UserActionMarkdownFooterComponent: React.FC = ({ + handleSaveAction, + handleCancelAction, +}) => { + const [{ content }] = useFormData<{ content: string }>({ watch: ['content'] }); + + return ( + + + + {i18n.CANCEL} + + + + + {i18n.SAVE} + + + + ); +}; + +UserActionMarkdownFooterComponent.displayName = 'UserActionMarkdownFooterComponent'; + +export const UserActionMarkdownFooter = React.memo(UserActionMarkdownFooterComponent); diff --git a/x-pack/plugins/cases/public/components/user_actions/tags.tsx b/x-pack/plugins/cases/public/components/user_actions/tags.tsx index bbcad0e8486f5..ad944f1cf49b1 100644 --- a/x-pack/plugins/cases/public/components/user_actions/tags.tsx +++ b/x-pack/plugins/cases/public/components/user_actions/tags.tsx @@ -25,7 +25,7 @@ const getLabelTitle = (userAction: UserActionResponse) => { {userAction.action === Actions.delete && i18n.REMOVED_FIELD} {i18n.TAGS.toLowerCase()} - + ); diff --git a/x-pack/plugins/cases/public/containers/use_get_feature_ids.test.tsx b/x-pack/plugins/cases/public/containers/use_get_feature_ids.test.tsx index b173ea4ad19e0..3d4382bc709ab 100644 --- a/x-pack/plugins/cases/public/containers/use_get_feature_ids.test.tsx +++ b/x-pack/plugins/cases/public/containers/use_get_feature_ids.test.tsx @@ -10,7 +10,6 @@ import React from 'react'; import { TestProviders } from '../common/mock'; import { useGetFeatureIds } from './use_get_feature_ids'; import * as api from './api'; -import { waitFor } from '@testing-library/dom'; jest.mock('./api'); jest.mock('../common/lib/kibana'); @@ -27,29 +26,31 @@ describe('useGetFeaturesIds', () => { wrapper: ({ children }) => {children}, }); - act(() => { + await act(async () => { expect(result.current.alertFeatureIds).toEqual([]); expect(result.current.isLoading).toEqual(true); expect(result.current.isError).toEqual(false); }); }); - // + it('fetches data and returns it correctly', async () => { const spy = jest.spyOn(api, 'getFeatureIds'); const { result } = renderHook(() => useGetFeatureIds(['context1']), { wrapper: ({ children }) => {children}, }); - await waitFor(() => { + await act(async () => { expect(spy).toHaveBeenCalledWith( { registrationContext: ['context1'] }, expect.any(AbortSignal) ); }); - expect(result.current.alertFeatureIds).toEqual(['siem', 'observability']); - expect(result.current.isLoading).toEqual(false); - expect(result.current.isError).toEqual(false); + await act(async () => { + expect(result.current.alertFeatureIds).toEqual(['siem', 'observability']); + expect(result.current.isLoading).toEqual(false); + expect(result.current.isError).toEqual(false); + }); }); it('sets isError to true when an error occurs', async () => { diff --git a/x-pack/plugins/cases/server/authorization/audit_logger.test.ts b/x-pack/plugins/cases/server/authorization/audit_logger.test.ts index b2bbf1f4afac3..7e973f2685990 100644 --- a/x-pack/plugins/cases/server/authorization/audit_logger.test.ts +++ b/x-pack/plugins/cases/server/authorization/audit_logger.test.ts @@ -38,23 +38,6 @@ describe('audit_logger', () => { logger = new AuthorizationAuditLogger(mockLogger); }); - it('does not throw an error when the underlying audit logger is undefined', () => { - const authLogger = new AuthorizationAuditLogger(); - jest.spyOn(authLogger, 'log'); - - expect(() => { - authLogger.log({ - operation: Operations.createCase, - entity: { - owner: 'a', - id: '1', - }, - }); - }).not.toThrow(); - - expect(authLogger.log).toHaveBeenCalledTimes(1); - }); - it('logs a message with a saved object ID in the message field', () => { logger.log({ operation: Operations.createCase, diff --git a/x-pack/plugins/cases/server/authorization/audit_logger.ts b/x-pack/plugins/cases/server/authorization/audit_logger.ts index 8a415e1b69559..88293689446f8 100644 --- a/x-pack/plugins/cases/server/authorization/audit_logger.ts +++ b/x-pack/plugins/cases/server/authorization/audit_logger.ts @@ -21,9 +21,9 @@ interface CreateAuditMsgParams { * Audit logger for authorization operations */ export class AuthorizationAuditLogger { - private readonly auditLogger?: AuditLogger; + private readonly auditLogger: AuditLogger; - constructor(logger?: AuditLogger) { + constructor(logger: AuditLogger) { this.auditLogger = logger; } @@ -97,6 +97,6 @@ export class AuthorizationAuditLogger { * Logs an audit event based on the status of an operation. */ public log(auditMsgParams: CreateAuditMsgParams) { - this.auditLogger?.log(AuthorizationAuditLogger.createAuditMsg(auditMsgParams)); + this.auditLogger.log(AuthorizationAuditLogger.createAuditMsg(auditMsgParams)); } } diff --git a/x-pack/plugins/cases/server/authorization/authorization.test.ts b/x-pack/plugins/cases/server/authorization/authorization.test.ts index 0483489d6c8a2..e7bbcb0abbb9b 100644 --- a/x-pack/plugins/cases/server/authorization/authorization.test.ts +++ b/x-pack/plugins/cases/server/authorization/authorization.test.ts @@ -61,7 +61,7 @@ describe('authorization', () => { securityAuth: securityStart.authz, spaces: spacesStart, features: featuresStart, - auditLogger: new AuthorizationAuditLogger(), + auditLogger: new AuthorizationAuditLogger(mockLogger), logger: loggingSystemMock.createLogger(), }); @@ -81,7 +81,7 @@ describe('authorization', () => { securityAuth: securityStart.authz, spaces: spacesStart, features: featuresStart, - auditLogger: new AuthorizationAuditLogger(), + auditLogger: new AuthorizationAuditLogger(mockLogger), logger: loggingSystemMock.createLogger(), }); @@ -140,7 +140,7 @@ describe('authorization', () => { request, spaces: spacesStart, features: featuresStart, - auditLogger: new AuthorizationAuditLogger(), + auditLogger: new AuthorizationAuditLogger(mockLogger), logger: loggingSystemMock.createLogger(), }); @@ -266,7 +266,7 @@ describe('authorization', () => { securityAuth: securityStart.authz, spaces: spacesStart, features: featuresStart, - auditLogger: new AuthorizationAuditLogger(), + auditLogger: new AuthorizationAuditLogger(mockLogger), logger: loggingSystemMock.createLogger(), }); @@ -295,7 +295,7 @@ describe('authorization', () => { securityAuth: securityStart.authz, spaces: spacesStart, features: featuresStart, - auditLogger: new AuthorizationAuditLogger(), + auditLogger: new AuthorizationAuditLogger(mockLogger), logger: loggingSystemMock.createLogger(), }); @@ -322,7 +322,7 @@ describe('authorization', () => { securityAuth: securityStart.authz, spaces: spacesStart, features: featuresStart, - auditLogger: new AuthorizationAuditLogger(), + auditLogger: new AuthorizationAuditLogger(mockLogger), logger: loggingSystemMock.createLogger(), }); diff --git a/x-pack/plugins/cloud_integrations/cloud_full_story/.i18nrc.json b/x-pack/plugins/cloud_integrations/cloud_full_story/.i18nrc.json index aa690dec41fc1..e86db74eed77a 100755 --- a/x-pack/plugins/cloud_integrations/cloud_full_story/.i18nrc.json +++ b/x-pack/plugins/cloud_integrations/cloud_full_story/.i18nrc.json @@ -3,5 +3,5 @@ "paths": { "cloudFullStory": "." }, - "translations": ["translations/ja-JP.json"] + "translations": [] } diff --git a/x-pack/plugins/cloud_security_posture/common/constants.ts b/x-pack/plugins/cloud_security_posture/common/constants.ts index a78266a51f279..539048af7bee1 100644 --- a/x-pack/plugins/cloud_security_posture/common/constants.ts +++ b/x-pack/plugins/cloud_security_posture/common/constants.ts @@ -46,3 +46,19 @@ export const CSP_RULE_TEMPLATE_SAVED_OBJECT_TYPE = 'csp-rule-template'; export const CLOUDBEAT_VANILLA = 'cloudbeat/cis_k8s'; // Integration input export const CLOUDBEAT_EKS = 'cloudbeat/cis_eks'; // Integration input +export const CLOUDBEAT_AWS = 'cloudbeat/cis_aws'; // Integration input +export const CLOUDBEAT_GCP = 'cloudbeat/cis_gcp'; // Integration input +export const CLOUDBEAT_AZURE = 'cloudbeat/cis_azure'; // Integration input +export const KSPM_POLICY_TEMPLATE = 'kspm'; +export const CSPM_POLICY_TEMPLATE = 'cspm'; +export const SUPPORTED_POLICY_TEMPLATES = [KSPM_POLICY_TEMPLATE, CSPM_POLICY_TEMPLATE]; +export const SUPPORTED_CLOUDBEAT_INPUTS = [ + CLOUDBEAT_VANILLA, + CLOUDBEAT_EKS, + CLOUDBEAT_AWS, + CLOUDBEAT_GCP, + CLOUDBEAT_AZURE, +]; + +export type CLOUDBEAT_INTEGRATION = typeof SUPPORTED_CLOUDBEAT_INPUTS[number]; +export type POLICY_TEMPLATE = typeof SUPPORTED_POLICY_TEMPLATES[number]; diff --git a/x-pack/plugins/cloud_security_posture/common/types.ts b/x-pack/plugins/cloud_security_posture/common/types.ts index f75289bfa020b..2421e22b8b5d7 100644 --- a/x-pack/plugins/cloud_security_posture/common/types.ts +++ b/x-pack/plugins/cloud_security_posture/common/types.ts @@ -54,14 +54,27 @@ export interface ComplianceDashboardData { export type CspStatusCode = | 'indexed' // latest findings index exists and has results | 'indexing' // index timeout was not surpassed since installation, assumes data is being indexed + | 'unprivileged' // user lacks privileges for the latest findings index | 'index-timeout' // index timeout was surpassed since installation | 'not-deployed' // no healthy agents were deployed | 'not-installed'; // number of installed csp integrations is 0; +export type IndexStatus = + | 'not-empty' // Index contains documents + | 'empty' // Index doesn't contain documents (or doesn't exist) + | 'unprivileged'; // User doesn't have access to query the index + +export interface IndexDetails { + index: string; + status: IndexStatus; +} + interface BaseCspSetupStatus { + indicesDetails: IndexDetails[]; latestPackageVersion: string; installedPackagePolicies: number; healthyAgents: number; + isPluginInitialized: boolean; } interface CspSetupNotInstalledStatus extends BaseCspSetupStatus { diff --git a/x-pack/plugins/cloud_security_posture/public/common/constants.ts b/x-pack/plugins/cloud_security_posture/public/common/constants.ts index 9c02bfe1ba4f6..0590b60d73f68 100644 --- a/x-pack/plugins/cloud_security_posture/public/common/constants.ts +++ b/x-pack/plugins/cloud_security_posture/public/common/constants.ts @@ -7,7 +7,15 @@ import { i18n } from '@kbn/i18n'; import { euiThemeVars } from '@kbn/ui-theme'; -import { CLOUDBEAT_EKS, CLOUDBEAT_VANILLA } from '../../common/constants'; +import { + CLOUDBEAT_EKS, + CLOUDBEAT_VANILLA, + CLOUDBEAT_AWS, + CLOUDBEAT_GCP, + CLOUDBEAT_AZURE, + CLOUDBEAT_INTEGRATION, + POLICY_TEMPLATE, +} from '../../common/constants'; export const statusColors = { passed: euiThemeVars.euiColorVis0, @@ -22,9 +30,57 @@ export const LOCAL_STORAGE_PAGE_SIZE_FINDINGS_KEY = 'cloudPosture:findings:pageS export const LOCAL_STORAGE_PAGE_SIZE_BENCHMARK_KEY = 'cloudPosture:benchmark:pageSize'; export const LOCAL_STORAGE_PAGE_SIZE_RULES_KEY = 'cloudPosture:rules:pageSize'; -export type CloudPostureIntegrations = typeof cloudPostureIntegrations; +export type CloudPostureIntegrations = Record; +export interface CloudPostureIntegrationProps { + policyTemplate: POLICY_TEMPLATE; + name: string; + shortName: string; + options: Array<{ + type: CLOUDBEAT_INTEGRATION; + name: string; + benchmark: string; + }>; +} -export const cloudPostureIntegrations = { +export const cloudPostureIntegrations: CloudPostureIntegrations = { + cspm: { + policyTemplate: 'cspm', + name: i18n.translate('xpack.csp.cspmIntegration.integration.nameTitle', { + defaultMessage: 'Cloud Security Posture Management', + }), + shortName: i18n.translate('xpack.csp.cspmIntegration.integration.shortNameTitle', { + defaultMessage: 'CSPM', + }), + options: [ + { + type: CLOUDBEAT_AWS, + name: i18n.translate('xpack.csp.cspmIntegration.awsOption.nameTitle', { + defaultMessage: 'Amazon Web Services', + }), + benchmark: i18n.translate('xpack.csp.cspmIntegration.awsOption.benchmarkTitle', { + defaultMessage: 'CIS AWS', + }), + }, + { + type: CLOUDBEAT_GCP, + name: i18n.translate('xpack.csp.cspmIntegration.gcpOption.nameTitle', { + defaultMessage: 'GCP', + }), + benchmark: i18n.translate('xpack.csp.cspmIntegration.gcpOption.benchmarkTitle', { + defaultMessage: 'CIS GCP', + }), + }, + { + type: CLOUDBEAT_AZURE, + name: i18n.translate('xpack.csp.cspmIntegration.azureOption.nameTitle', { + defaultMessage: 'Azure', + }), + benchmark: i18n.translate('xpack.csp.cspmIntegration.azureOption.benchmarkTitle', { + defaultMessage: 'CIS Azure', + }), + }, + ], + }, kspm: { policyTemplate: 'kspm', name: i18n.translate('xpack.csp.kspmIntegration.integration.nameTitle', { @@ -54,4 +110,4 @@ export const cloudPostureIntegrations = { }, ], }, -} as const; +}; 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 0573d77e6f9c8..8bc6ff96b4c4f 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 @@ -6,7 +6,6 @@ */ import { useEffect, useCallback, useMemo } from 'react'; import { useHistory, useLocation } from 'react-router-dom'; -import type { RisonObject } from 'rison-node'; import { decodeQuery, encodeQuery } from '../navigation/query_utils'; /** @@ -35,7 +34,7 @@ export const useUrlQuery = (getDefaultQuery: () => T) => { useEffect(() => { if (search) return; - replace({ search: encodeQuery(getDefaultQuery() as RisonObject) }); + replace({ search: encodeQuery(getDefaultQuery()) }); }, [getDefaultQuery, search, replace]); return { diff --git a/x-pack/plugins/cloud_security_posture/public/common/navigation/query_utils.ts b/x-pack/plugins/cloud_security_posture/public/common/navigation/query_utils.ts index 601ad3097b7a8..3a051456733a6 100644 --- a/x-pack/plugins/cloud_security_posture/public/common/navigation/query_utils.ts +++ b/x-pack/plugins/cloud_security_posture/public/common/navigation/query_utils.ts @@ -4,10 +4,10 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { encode, decode, type RisonObject } from 'rison-node'; +import { encode, decode } from '@kbn/rison'; import type { LocationDescriptorObject } from 'history'; -const encodeRison = (v: RisonObject): string | undefined => { +const encodeRison = (v: any): string | undefined => { try { return encode(v); } catch (e) { @@ -27,7 +27,7 @@ const decodeRison = (query: string): T | undefined => { const QUERY_PARAM_KEY = 'cspq'; -export const encodeQuery = (query: RisonObject): LocationDescriptorObject['search'] => { +export const encodeQuery = (query: any): LocationDescriptorObject['search'] => { const risonQuery = encodeRison(query); if (!risonQuery) return; return `${QUERY_PARAM_KEY}=${risonQuery}`; diff --git a/x-pack/plugins/cloud_security_posture/public/common/utils/get_enabled_csp_integration_details.ts b/x-pack/plugins/cloud_security_posture/public/common/utils/get_enabled_csp_integration_details.ts index 6cc02ac9aedc1..2fed31c2f8cc4 100644 --- a/x-pack/plugins/cloud_security_posture/public/common/utils/get_enabled_csp_integration_details.ts +++ b/x-pack/plugins/cloud_security_posture/public/common/utils/get_enabled_csp_integration_details.ts @@ -6,6 +6,7 @@ */ import { PackagePolicy } from '@kbn/fleet-plugin/common'; +import { SUPPORTED_CLOUDBEAT_INPUTS } from '../../../common/constants'; import { cloudPostureIntegrations, CloudPostureIntegrations } from '../constants'; const isPolicyTemplate = (name: unknown): name is keyof CloudPostureIntegrations => @@ -13,7 +14,14 @@ const isPolicyTemplate = (name: unknown): name is keyof CloudPostureIntegrations export const getEnabledCspIntegrationDetails = (packageInfo?: PackagePolicy) => { const enabledInput = packageInfo?.inputs.find((input) => input.enabled); - if (!enabledInput || !isPolicyTemplate(enabledInput.policy_template)) return null; + + // Check for valid and support input + if ( + !enabledInput || + !isPolicyTemplate(enabledInput.policy_template) || + !SUPPORTED_CLOUDBEAT_INPUTS.includes(enabledInput.type) + ) + return null; const integration = cloudPostureIntegrations[enabledInput.policy_template]; const enabledIntegrationOption = integration.options.find( diff --git a/x-pack/plugins/cloud_security_posture/public/components/csp_evaluation_badge.tsx b/x-pack/plugins/cloud_security_posture/public/components/csp_evaluation_badge.tsx index 0d838daa1e660..24ca4cd4fe4eb 100644 --- a/x-pack/plugins/cloud_security_posture/public/components/csp_evaluation_badge.tsx +++ b/x-pack/plugins/cloud_security_posture/public/components/csp_evaluation_badge.tsx @@ -24,7 +24,11 @@ const getColor = (type: Props['type']): EuiBadgeProps['color'] => { }; export const CspEvaluationBadge = ({ type }: Props) => ( - + {type === 'failed' ? ( ) : ( diff --git a/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/deployment_type_select.tsx b/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/deployment_type_select.tsx index b01b5073a0e1b..bc5c61fa0370b 100644 --- a/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/deployment_type_select.tsx +++ b/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/deployment_type_select.tsx @@ -17,18 +17,19 @@ import { } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; import { cloudPostureIntegrations } from '../../common/constants'; -import { CLOUDBEAT_EKS, CLOUDBEAT_VANILLA } from '../../../common/constants'; - -export type InputType = typeof CLOUDBEAT_EKS | typeof CLOUDBEAT_VANILLA; +import { CLOUDBEAT_INTEGRATION, POLICY_TEMPLATE } from '../../../common/constants'; interface Props { - type: InputType; - onChange?: (type: InputType) => void; + policyTemplate: POLICY_TEMPLATE; + type: CLOUDBEAT_INTEGRATION; + onChange?: (type: CLOUDBEAT_INTEGRATION) => void; isDisabled?: boolean; } -const kubeDeployOptions: Array> = - cloudPostureIntegrations.kspm.options.map((o) => ({ value: o.type, label: o.name })); +const kubeDeployOptions = ( + policyTemplate: POLICY_TEMPLATE +): Array> => + cloudPostureIntegrations[policyTemplate].options.map((o) => ({ value: o.type, label: o.name })); const KubernetesDeploymentFieldLabel = () => ( ( ); -export const DeploymentTypeSelect = ({ type, isDisabled, onChange }: Props) => ( +export const DeploymentTypeSelect = ({ policyTemplate, type, isDisabled, onChange }: Props) => ( }> }> o.value === type)} + options={kubeDeployOptions(policyTemplate)} + selectedOptions={kubeDeployOptions(policyTemplate).filter((o) => o.value === type)} isDisabled={isDisabled} onChange={(options) => !isDisabled && onChange?.(options[0].value!)} /> diff --git a/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/policy_extension_create.tsx b/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/policy_extension_create.tsx index ae5d79984da7e..790cd8978725c 100644 --- a/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/policy_extension_create.tsx +++ b/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/policy_extension_create.tsx @@ -7,16 +7,22 @@ import React, { memo } from 'react'; import { EuiForm } from '@elastic/eui'; import type { PackagePolicyCreateExtensionComponentProps } from '@kbn/fleet-plugin/public'; -import { CLOUDBEAT_EKS } from '../../../common/constants'; -import { DeploymentTypeSelect, InputType } from './deployment_type_select'; +import { CLOUDBEAT_AWS, CLOUDBEAT_EKS, CLOUDBEAT_INTEGRATION } from '../../../common/constants'; +import { DeploymentTypeSelect } from './deployment_type_select'; import { EksFormWrapper } from './eks_form'; -import { getEnabledInputType, getUpdatedDeploymentType, getUpdatedEksVar } from './utils'; +import { + getEnabledInput, + getEnabledInputType, + getUpdatedDeploymentType, + getUpdatedEksVar, +} from './utils'; export const CspCreatePolicyExtension = memo( ({ newPolicy, onChange }) => { const selectedDeploymentType = getEnabledInputType(newPolicy.inputs); - - const updateDeploymentType = (inputType: InputType) => + const selectedInput = getEnabledInput(newPolicy.inputs); + const policyTemplate = selectedInput?.policy_template; + const updateDeploymentType = (inputType: CLOUDBEAT_INTEGRATION) => onChange(getUpdatedDeploymentType(newPolicy, inputType)); const updateEksVar = (key: string, value: string) => @@ -24,9 +30,18 @@ export const CspCreatePolicyExtension = memo - - {selectedDeploymentType === CLOUDBEAT_EKS && ( - + {selectedInput && (policyTemplate === 'kspm' || policyTemplate === 'cspm') && ( + <> + + {(selectedDeploymentType === CLOUDBEAT_EKS || + selectedDeploymentType === CLOUDBEAT_AWS) && ( + + )} + )} ); diff --git a/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/policy_extension_edit.tsx b/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/policy_extension_edit.tsx index 33ed6dced08ad..e268dac8cd14e 100644 --- a/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/policy_extension_edit.tsx +++ b/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/policy_extension_edit.tsx @@ -7,23 +7,34 @@ import React, { memo } from 'react'; import { EuiForm } from '@elastic/eui'; import type { PackagePolicyEditExtensionComponentProps } from '@kbn/fleet-plugin/public'; -import { CLOUDBEAT_EKS } from '../../../common/constants'; +import { CLOUDBEAT_EKS, CLOUDBEAT_AWS } from '../../../common/constants'; import { DeploymentTypeSelect } from './deployment_type_select'; import { EksFormWrapper } from './eks_form'; -import { getEnabledInputType, getUpdatedEksVar } from './utils'; +import { getEnabledInput, getEnabledInputType, getUpdatedEksVar } from './utils'; export const CspEditPolicyExtension = memo( ({ newPolicy, onChange }) => { const selectedDeploymentType = getEnabledInputType(newPolicy.inputs); + const selectedInput = getEnabledInput(newPolicy.inputs); + const policyTemplate = selectedInput?.policy_template; const updateEksVar = (key: string, value: string) => onChange(getUpdatedEksVar(newPolicy, key, value)); return ( - - {selectedDeploymentType === CLOUDBEAT_EKS && ( - + {(policyTemplate === 'kspm' || policyTemplate === 'cspm') && ( + <> + + {(selectedDeploymentType === CLOUDBEAT_EKS || + selectedDeploymentType === CLOUDBEAT_AWS) && ( + + )} + )} ); diff --git a/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/utils.ts b/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/utils.ts index 9641606abfcf7..2f2285ad5c4ce 100644 --- a/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/utils.ts +++ b/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/utils.ts @@ -5,15 +5,42 @@ * 2.0. */ import type { NewPackagePolicy, NewPackagePolicyInput } from '@kbn/fleet-plugin/common'; -import { CLOUDBEAT_EKS, CLOUDBEAT_VANILLA } from '../../../common/constants'; -import type { InputType } from './deployment_type_select'; +import { + CLOUDBEAT_AWS, + CLOUDBEAT_EKS, + CLOUDBEAT_VANILLA, + CLOUDBEAT_INTEGRATION, + SUPPORTED_POLICY_TEMPLATES, + POLICY_TEMPLATE, +} from '../../../common/constants'; export const isEksInput = (input: NewPackagePolicyInput) => input.type === CLOUDBEAT_EKS; +export const inputsWithVars = [CLOUDBEAT_EKS, CLOUDBEAT_AWS]; +const defaultInputType: Record = { + kspm: CLOUDBEAT_VANILLA, + cspm: CLOUDBEAT_AWS, +}; +export const getEnabledInputType = (inputs: NewPackagePolicy['inputs']): CLOUDBEAT_INTEGRATION => { + const enabledInput = getEnabledInput(inputs); + + if (enabledInput) return enabledInput.type as CLOUDBEAT_INTEGRATION; + + const policyTemplate = inputs[0].policy_template as POLICY_TEMPLATE | undefined; + + if (policyTemplate && SUPPORTED_POLICY_TEMPLATES.includes(policyTemplate)) + return defaultInputType[policyTemplate]; + + throw new Error('unsupported policy template'); +}; -export const getEnabledInputType = (inputs: NewPackagePolicy['inputs']): InputType => - (inputs.find((input) => input.enabled)?.type as InputType) || CLOUDBEAT_VANILLA; +export const getEnabledInput = ( + inputs: NewPackagePolicy['inputs'] +): NewPackagePolicyInput | undefined => inputs.find((input) => input.enabled); -export const getUpdatedDeploymentType = (newPolicy: NewPackagePolicy, inputType: InputType) => ({ +export const getUpdatedDeploymentType = ( + newPolicy: NewPackagePolicy, + inputType: CLOUDBEAT_INTEGRATION +) => ({ isValid: true, // TODO: add validations updatedPolicy: { ...newPolicy, @@ -33,7 +60,7 @@ export const getUpdatedEksVar = (newPolicy: NewPackagePolicy, key: string, value updatedPolicy: { ...newPolicy, inputs: newPolicy.inputs.map((item) => - isEksInput(item) ? getUpdatedStreamVars(item, key, value) : item + inputsWithVars.includes(item.type) ? getUpdatedStreamVars(item, key, value) : item ), }, }); diff --git a/x-pack/plugins/cloud_security_posture/public/components/no_findings_states.tsx b/x-pack/plugins/cloud_security_posture/public/components/no_findings_states.tsx index 89fc1c14a93e8..04346d284724d 100644 --- a/x-pack/plugins/cloud_security_posture/public/components/no_findings_states.tsx +++ b/x-pack/plugins/cloud_security_posture/public/components/no_findings_states.tsx @@ -6,14 +6,23 @@ */ import React from 'react'; -import { EuiLoadingLogo, EuiButton, EuiEmptyPrompt } from '@elastic/eui'; +import { + EuiLoadingLogo, + EuiButton, + EuiEmptyPrompt, + EuiIcon, + EuiMarkdownFormat, +} from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; +import { i18n } from '@kbn/i18n'; +import { css } from '@emotion/react'; import { FullSizeCenteredPage } from './full_size_centered_page'; import { useCspBenchmarkIntegrations } from '../pages/benchmarks/use_csp_benchmark_integrations'; import { useCISIntegrationPoliciesLink } from '../common/navigation/use_navigate_to_cis_integration_policies'; import { NO_FINDINGS_STATUS_TEST_SUBJ } from './test_subjects'; import { CloudPosturePage } from './cloud_posture_page'; import { useCspSetupStatusApi } from '../common/api/use_setup_status_api'; +import type { IndexDetails } from '../../common/types'; const REFETCH_INTERVAL_MS = 20000; @@ -116,6 +125,43 @@ const IndexTimeout = () => ( /> ); +const Unprivileged = ({ unprivilegedIndices }: { unprivilegedIndices: string[] }) => ( + } + title={ +

    + +

    + } + body={ +

    + +

    + } + footer={ + `\n- \`${idx}\``) + } + /> + } + /> +); + /** * This component will return the render states based on cloud posture setup status API * since 'not-installed' is being checked globally by CloudPosturePage and 'indexed' is the pass condition, those states won't be handled here @@ -125,11 +171,20 @@ export const NoFindingsStates = () => { options: { refetchInterval: REFETCH_INTERVAL_MS }, }); const status = getSetupStatus.data?.status; + const indicesStatus = getSetupStatus.data?.indicesDetails; + const unprivilegedIndices = + indicesStatus && + indicesStatus + .filter((idxDetails) => idxDetails.status === 'unprivileged') + .map((idxDetails: IndexDetails) => idxDetails.index) + .sort((a, b) => a.localeCompare(b)); const render = () => { if (status === 'not-deployed') return ; // integration installed, but no agents added if (status === 'indexing') return ; // agent added, index timeout hasn't passed since installation if (status === 'index-timeout') return ; // agent added, index timeout has passed + if (status === 'unprivileged') + return ; // user has no privileges for our indices }; return ( diff --git a/x-pack/plugins/cloud_security_posture/public/components/test_subjects.ts b/x-pack/plugins/cloud_security_posture/public/components/test_subjects.ts index 93f940f80c614..4f154805aae05 100644 --- a/x-pack/plugins/cloud_security_posture/public/components/test_subjects.ts +++ b/x-pack/plugins/cloud_security_posture/public/components/test_subjects.ts @@ -16,4 +16,5 @@ export const NO_FINDINGS_STATUS_TEST_SUBJ = { NO_AGENTS_DEPLOYED: 'status-api-no-agent-deployed', INDEXING: 'status-api-indexing', INDEX_TIMEOUT: 'status-api-index-timeout', + UNPRIVILEGED: 'status-api-unprivileged', }; diff --git a/x-pack/plugins/cloud_security_posture/public/pages/compliance_dashboard/compliance_dashboard.test.tsx b/x-pack/plugins/cloud_security_posture/public/pages/compliance_dashboard/compliance_dashboard.test.tsx index c658eac5462a5..d7a03e0cb679c 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/compliance_dashboard/compliance_dashboard.test.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/compliance_dashboard/compliance_dashboard.test.tsx @@ -247,6 +247,7 @@ describe('', () => { DASHBOARD_CONTAINER, NO_FINDINGS_STATUS_TEST_SUBJ.INDEXING, NO_FINDINGS_STATUS_TEST_SUBJ.INDEX_TIMEOUT, + NO_FINDINGS_STATUS_TEST_SUBJ.UNPRIVILEGED, ], }); }); @@ -268,6 +269,7 @@ describe('', () => { DASHBOARD_CONTAINER, NO_FINDINGS_STATUS_TEST_SUBJ.NO_AGENTS_DEPLOYED, NO_FINDINGS_STATUS_TEST_SUBJ.INDEX_TIMEOUT, + NO_FINDINGS_STATUS_TEST_SUBJ.UNPRIVILEGED, ], }); }); @@ -289,6 +291,29 @@ describe('', () => { DASHBOARD_CONTAINER, NO_FINDINGS_STATUS_TEST_SUBJ.NO_AGENTS_DEPLOYED, NO_FINDINGS_STATUS_TEST_SUBJ.INDEXING, + NO_FINDINGS_STATUS_TEST_SUBJ.UNPRIVILEGED, + ], + }); + }); + + it('no findings state: unprivileged - shows Unprivileged instead of dashboard', () => { + (useCspSetupStatusApi as jest.Mock).mockImplementation(() => + createReactQueryResponse({ + status: 'success', + data: { status: 'unprivileged' }, + }) + ); + (useCISIntegrationLink as jest.Mock).mockImplementation(() => chance.url()); + + renderComplianceDashboardPage(); + + expectIdsInDoc({ + be: [NO_FINDINGS_STATUS_TEST_SUBJ.UNPRIVILEGED], + notToBe: [ + DASHBOARD_CONTAINER, + NO_FINDINGS_STATUS_TEST_SUBJ.NO_AGENTS_DEPLOYED, + NO_FINDINGS_STATUS_TEST_SUBJ.INDEXING, + NO_FINDINGS_STATUS_TEST_SUBJ.INDEX_TIMEOUT, ], }); }); @@ -308,6 +333,7 @@ describe('', () => { NO_FINDINGS_STATUS_TEST_SUBJ.INDEX_TIMEOUT, NO_FINDINGS_STATUS_TEST_SUBJ.NO_AGENTS_DEPLOYED, NO_FINDINGS_STATUS_TEST_SUBJ.INDEXING, + NO_FINDINGS_STATUS_TEST_SUBJ.UNPRIVILEGED, ], }); }); diff --git a/x-pack/plugins/cloud_security_posture/public/pages/findings/__mocks__/findings.ts b/x-pack/plugins/cloud_security_posture/public/pages/findings/__mocks__/findings.ts new file mode 100644 index 0000000000000..48956856bf31c --- /dev/null +++ b/x-pack/plugins/cloud_security_posture/public/pages/findings/__mocks__/findings.ts @@ -0,0 +1,117 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { CspFinding } from '../../../../common/schemas/csp_finding'; + +export const mockFindingsHit: CspFinding = { + result: { + evaluation: 'passed', + evidence: { + serviceAccounts: [], + serviceAccount: [], + }, + // TODO: wrong type + // expected: null, + }, + orchestrator: { + cluster: { + name: 'kind-multi', + }, + }, + agent: { + name: 'kind-multi-worker', + id: '41b2ba39-fd4e-474d-8c61-d79c9204e793', + // TODO: missing + // ephemeral_id: '20964f94-a4fe-48c1-8bf3-4b7140baf03c', + type: 'cloudbeat', + version: '8.6.0', + }, + cluster_id: '087606d6-c71a-4892-9b27-67ab937770ce', + '@timestamp': '2022-11-24T22:27:19.515Z', + ecs: { + version: '8.0.0', + }, + resource: { + sub_type: 'ServiceAccount', + name: 'certificate-controller', + raw: { + metadata: { + uid: '597cd43e-90a5-4aea-95aa-35f177429794', + resourceVersion: '277', + creationTimestamp: '2022-11-15T16:08:49Z', + name: 'certificate-controller', + namespace: 'kube-system', + }, + apiVersion: 'v1', + kind: 'ServiceAccount', + secrets: [ + { + name: 'certificate-controller-token-ql8wn', + }, + ], + }, + id: '597cd43e-90a5-4aea-95aa-35f177429794', + type: 'k8s_object', + }, + host: { + id: '', // TODO: missing + hostname: 'kind-multi-worker', + os: { + kernel: '5.10.76-linuxkit', + codename: 'bullseye', + name: 'Debian GNU/Linux', + type: 'linux', + family: 'debian', + version: '11 (bullseye)', + platform: 'debian', + }, + containerized: false, + ip: ['172.19.0.3', 'fc00:f853:ccd:e793::3', 'fe80::42:acff:fe13:3'], + name: 'kind-multi-worker', + mac: ['02-42-AC-13-00-03'], + architecture: 'x86_64', + }, + rule: { + references: + '1. [https://kubernetes.io/docs/tasks/configure-pod-container/configure-service-account/](https://kubernetes.io/docs/tasks/configure-pod-container/configure-service-account/)\n', + impact: + 'All workloads which require access to the Kubernetes API will require an explicit service account to be created.\n', + description: + 'The `default` service account should not be used to ensure that rights granted to applications can be more easily audited and reviewed.\n', + default_value: + 'By default the `default` service account allows for its service account token\nto be mounted\nin pods in its namespace.\n', + section: 'RBAC and Service Accounts', + rationale: + 'Kubernetes provides a `default` service account which is used by cluster workloads where no specific service account is assigned to the pod. Where access to the Kubernetes API from a pod is required, a specific service account should be created for that pod, and rights granted to that service account. The default service account should be configured such that it does not provide a service account token and does not have any explicit rights assignments.\n', + version: '1.0', + benchmark: { + name: 'CIS Kubernetes V1.23', + id: 'cis_k8s', + version: 'v1.0.0', + }, + tags: ['CIS', 'Kubernetes', 'CIS 5.1.5', 'RBAC and Service Accounts'], + remediation: + 'Create explicit service accounts wherever a Kubernetes workload requires\nspecific access\nto the Kubernetes API server.\nModify the configuration of each default service account to include this value\n```\nautomountServiceAccountToken: false\n```\n', + audit: + 'For each namespace in the cluster, review the rights assigned to the default service account and ensure that it has no roles or cluster roles bound to it apart from the defaults. Additionally ensure that the `automountServiceAccountToken: false` setting is in place for each default service account.\n', + name: 'Ensure that default service accounts are not actively used. (Manual)', + id: '2b399496-f79d-5533-8a86-4ea00b95e3bd', + profile_applicability: '* Level 1 - Master Node\n', + rego_rule_id: '', + }, + event: { + agent_id_status: 'auth_metadata_missing', + sequence: 1669328831, + ingested: '2022-11-24T22:28:25Z', + created: '2022-11-24T22:27:19.514650003Z', + kind: 'state', + id: 'ce5c1501-90a3-4543-bf28-cd6c9e4d73e8', + type: ['info'], + category: ['configuration'], + outcome: 'success', + }, +}; diff --git a/x-pack/plugins/cloud_security_posture/public/pages/findings/findings.test.tsx b/x-pack/plugins/cloud_security_posture/public/pages/findings/findings.test.tsx index d0744239a975b..70420f61d2176 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/findings/findings.test.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/findings/findings.test.tsx @@ -84,6 +84,7 @@ describe('', () => { TEST_SUBJECTS.FINDINGS_CONTAINER, NO_FINDINGS_STATUS_TEST_SUBJ.INDEXING, NO_FINDINGS_STATUS_TEST_SUBJ.INDEX_TIMEOUT, + NO_FINDINGS_STATUS_TEST_SUBJ.UNPRIVILEGED, ], }); }); @@ -105,6 +106,7 @@ describe('', () => { TEST_SUBJECTS.FINDINGS_CONTAINER, NO_FINDINGS_STATUS_TEST_SUBJ.NO_AGENTS_DEPLOYED, NO_FINDINGS_STATUS_TEST_SUBJ.INDEX_TIMEOUT, + NO_FINDINGS_STATUS_TEST_SUBJ.UNPRIVILEGED, ], }); }); @@ -126,6 +128,29 @@ describe('', () => { TEST_SUBJECTS.FINDINGS_CONTAINER, NO_FINDINGS_STATUS_TEST_SUBJ.NO_AGENTS_DEPLOYED, NO_FINDINGS_STATUS_TEST_SUBJ.INDEXING, + NO_FINDINGS_STATUS_TEST_SUBJ.UNPRIVILEGED, + ], + }); + }); + + it('no findings state: unprivileged - shows Unprivileged instead of findings', () => { + (useCspSetupStatusApi as jest.Mock).mockImplementation(() => + createReactQueryResponse({ + status: 'success', + data: { status: 'unprivileged' }, + }) + ); + (useCISIntegrationLink as jest.Mock).mockImplementation(() => chance.url()); + + renderFindingsPage(); + + expectIdsInDoc({ + be: [NO_FINDINGS_STATUS_TEST_SUBJ.UNPRIVILEGED], + notToBe: [ + TEST_SUBJECTS.FINDINGS_CONTAINER, + NO_FINDINGS_STATUS_TEST_SUBJ.NO_AGENTS_DEPLOYED, + NO_FINDINGS_STATUS_TEST_SUBJ.INDEXING, + NO_FINDINGS_STATUS_TEST_SUBJ.INDEX_TIMEOUT, ], }); }); @@ -156,6 +181,7 @@ describe('', () => { NO_FINDINGS_STATUS_TEST_SUBJ.INDEX_TIMEOUT, NO_FINDINGS_STATUS_TEST_SUBJ.NO_AGENTS_DEPLOYED, NO_FINDINGS_STATUS_TEST_SUBJ.INDEXING, + NO_FINDINGS_STATUS_TEST_SUBJ.UNPRIVILEGED, ], }); }); diff --git a/x-pack/plugins/cloud_security_posture/public/pages/findings/findings_flyout/findings_flyout.test.tsx b/x-pack/plugins/cloud_security_posture/public/pages/findings/findings_flyout/findings_flyout.test.tsx new file mode 100644 index 0000000000000..40b87da1245ef --- /dev/null +++ b/x-pack/plugins/cloud_security_posture/public/pages/findings/findings_flyout/findings_flyout.test.tsx @@ -0,0 +1,69 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React from 'react'; +import userEvent from '@testing-library/user-event'; +import { FindingsRuleFlyout } from './findings_flyout'; +import { render, screen } from '@testing-library/react'; +import { TestProvider } from '../../../test/test_provider'; +import { mockFindingsHit } from '../__mocks__/findings'; +import { LATEST_FINDINGS_INDEX_DEFAULT_NS } from '../../../../common/constants'; + +const TestComponent = () => ( + + + +); + +describe('', () => { + describe('Overview Tab', () => { + it('details and remediation accordions are open', () => { + const { getAllByRole } = render(); + + getAllByRole('button', { expanded: true, name: 'Details' }); + getAllByRole('button', { expanded: true, name: 'Remediation' }); + }); + + it('displays text details summary info', () => { + const { getAllByText, getByText } = render(); + + getAllByText(mockFindingsHit.rule.name); + getByText(mockFindingsHit.resource.id); + getByText(mockFindingsHit.resource.name); + getAllByText(mockFindingsHit.rule.section); + getByText(LATEST_FINDINGS_INDEX_DEFAULT_NS); + mockFindingsHit.rule.tags.forEach((tag) => { + getAllByText(tag); + }); + }); + }); + + describe('Rule Tab', () => { + it('displays rule text details', () => { + const { getByText, getAllByText } = render(); + + userEvent.click(screen.getByTestId('findings_flyout_tab_rule')); + + getAllByText(mockFindingsHit.rule.name); + getByText(mockFindingsHit.rule.benchmark.name); + getAllByText(mockFindingsHit.rule.section); + mockFindingsHit.rule.tags.forEach((tag) => { + getAllByText(tag); + }); + }); + }); + + describe('Resource Tab', () => { + it('displays resource name and id', () => { + const { getAllByText } = render(); + + userEvent.click(screen.getByTestId('findings_flyout_tab_resource')); + + getAllByText(mockFindingsHit.resource.name); + getAllByText(mockFindingsHit.resource.id); + }); + }); +}); 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 0b00136b165c5..8229084c10dd9 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 @@ -129,7 +129,12 @@ export const FindingsRuleFlyout = ({ onClose, findings }: FindingFlyoutProps) => {tabs.map((v) => ( - setTab(v)}> + setTab(v)} + data-test-subj={`findings_flyout_tab_${v.id}`} + > {v.title} ))} 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 index 5dba83b9019b8..a0c5d330d22a3 100644 --- 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 @@ -46,7 +46,7 @@ const getDetailsList = (data: CspFinding, discoverIndexLink: string | undefined) description: ( <> {data.rule.tags.map((tag) => ( - {tag} + {tag} ))} ), diff --git a/x-pack/plugins/cloud_security_posture/public/pages/findings/findings_flyout/rule_tab.tsx b/x-pack/plugins/cloud_security_posture/public/pages/findings/findings_flyout/rule_tab.tsx index 74904041888a4..e51abb0bd3e9d 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/findings/findings_flyout/rule_tab.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/findings/findings_flyout/rule_tab.tsx @@ -31,7 +31,7 @@ export const getRuleList = (rule: CspFinding['rule']) => [ description: ( <> {rule.tags.map((tag) => ( - {tag} + {tag} ))} ), 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 2d709433e7fc5..3c6b51f881989 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 @@ -16,7 +16,6 @@ import { TestProvider } from '../../../test/test_provider'; import { getFindingsQuery } from './use_latest_findings'; import { encodeQuery } from '../../../common/navigation/query_utils'; import { useLocation } from 'react-router-dom'; -import { RisonObject } from 'rison-node'; import { buildEsQuery } from '@kbn/es-query'; import { getPaginationQuery } from '../utils/utils'; import { chartPluginMock } from '@kbn/charts-plugin/public/mocks'; @@ -52,7 +51,7 @@ describe('', () => { }); (useLocation as jest.Mock).mockReturnValue({ - search: encodeQuery(query as unknown as RisonObject), + search: encodeQuery(query), }); render( 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 f293b82341a61..9db41a7786174 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 @@ -126,6 +126,7 @@ const DistributionBar: React.FC> = ({ distributionOnClick={() => { distributionOnClick(RULE_PASSED); }} + data-test-subj="distribution_bar_passed" /> > = ({ distributionOnClick={() => { distributionOnClick(RULE_FAILED); }} + data-test-subj="distribution_bar_failed" /> ); @@ -142,12 +144,15 @@ const DistributionBarPart = ({ value, color, distributionOnClick, + ...rest }: { value: number; color: string; distributionOnClick: () => void; + ['data-test-subj']: string; }) => (

    } body={ @@ -38,7 +49,7 @@ export const EmptyPrompt: React.FunctionComponent = ({ history

    } - actions={} + actions={readOnly ? null : } data-test-subj="roleMappingsEmptyPrompt" /> ); diff --git a/x-pack/plugins/security/public/management/role_mappings/role_mappings_grid/role_mappings_grid_page.test.tsx b/x-pack/plugins/security/public/management/role_mappings/role_mappings_grid/role_mappings_grid_page.test.tsx index 79d97346de8dd..04dbc1907ec00 100644 --- a/x-pack/plugins/security/public/management/role_mappings/role_mappings_grid/role_mappings_grid_page.test.tsx +++ b/x-pack/plugins/security/public/management/role_mappings/role_mappings_grid/role_mappings_grid_page.test.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { EuiLink } from '@elastic/eui'; +import { EuiEmptyPrompt, EuiLink } from '@elastic/eui'; import { act } from '@testing-library/react'; import React from 'react'; @@ -26,8 +26,15 @@ describe('RoleMappingsGridPage', () => { const renderView = ( roleMappingsAPI: ReturnType, - rolesAPI: ReturnType = rolesAPIClientMock.create() + rolesAPI: ReturnType = rolesAPIClientMock.create(), + readOnly: boolean = false ) => { + coreStart.application.capabilities = { + ...coreStart.application.capabilities, + role_mappings: { + save: !readOnly, + }, + }; return mountWithIntl( { docLinks={coreStart.docLinks} history={history} navigateToApp={coreStart.application.navigateToApp} + readOnly={!coreStart.application.capabilities.role_mappings.save} /> ); @@ -48,7 +56,7 @@ describe('RoleMappingsGridPage', () => { coreStart = coreMock.createStart(); }); - it('renders an empty prompt when no role mappings exist', async () => { + it('renders a create prompt when no role mappings exist', async () => { const roleMappingsAPI = roleMappingsAPIClientMock.create(); roleMappingsAPI.getRoleMappings.mockResolvedValue([]); roleMappingsAPI.checkRoleMappingFeatures.mockResolvedValue({ @@ -65,7 +73,17 @@ describe('RoleMappingsGridPage', () => { expect(wrapper.find(SectionLoading)).toHaveLength(0); expect(wrapper.find(NoCompatibleRealms)).toHaveLength(0); - expect(wrapper.find(EmptyPrompt)).toHaveLength(1); + + const emptyPrompts = wrapper.find(EmptyPrompt); + expect(emptyPrompts).toHaveLength(1); + expect(emptyPrompts.at(0).props().readOnly).toBeFalsy(); + + const euiEmptyPrompts = wrapper.find(EuiEmptyPrompt); + expect(euiEmptyPrompts).toHaveLength(1); + expect(euiEmptyPrompts.at(0).props().actions).not.toBeNull(); + + const createButton = wrapper.find('EuiButton[data-test-subj="createRoleMappingButton"]'); + expect(createButton).toHaveLength(1); }); it('renders a permission denied message when unauthorized to manage role mappings', async () => { @@ -299,4 +317,85 @@ describe('RoleMappingsGridPage', () => { ); expect(deleteButton).toHaveLength(1); }); + + describe('read-only', () => { + it('renders an empty prompt when no role mappings exist', async () => { + const roleMappingsAPI = roleMappingsAPIClientMock.create(); + roleMappingsAPI.getRoleMappings.mockResolvedValue([]); + roleMappingsAPI.checkRoleMappingFeatures.mockResolvedValue({ + canManageRoleMappings: true, + hasCompatibleRealms: true, + }); + + const wrapper = renderView(roleMappingsAPI, undefined, true); + expect(wrapper.find(SectionLoading)).toHaveLength(1); + expect(wrapper.find(EmptyPrompt)).toHaveLength(0); + + await nextTick(); + wrapper.update(); + + expect(wrapper.find(SectionLoading)).toHaveLength(0); + expect(wrapper.find(NoCompatibleRealms)).toHaveLength(0); + + const emptyPrompts = wrapper.find(EmptyPrompt); + expect(emptyPrompts).toHaveLength(1); + expect(emptyPrompts.at(0).props().readOnly).toBeTruthy(); + + const euiEmptyPrompts = wrapper.find(EuiEmptyPrompt); + expect(euiEmptyPrompts).toHaveLength(1); + expect(euiEmptyPrompts.at(0).props().actions).toBeNull(); + }); + + it('hides controls when `readOnly` is enabled', async () => { + const roleMappingsAPI = roleMappingsAPIClientMock.create(); + roleMappingsAPI.getRoleMappings.mockResolvedValue([ + { + name: 'some-realm', + enabled: true, + roles: ['superuser'], + rules: { field: { username: '*' } }, + }, + ]); + roleMappingsAPI.checkRoleMappingFeatures.mockResolvedValue({ + canManageRoleMappings: true, + hasCompatibleRealms: true, + }); + roleMappingsAPI.deleteRoleMappings.mockResolvedValue([ + { + name: 'some-realm', + success: true, + }, + ]); + + const wrapper = renderView(roleMappingsAPI, undefined, true); + await nextTick(); + wrapper.update(); + + const bulkButton = wrapper.find('EuiButtonEmpty[data-test-subj="bulkDeleteActionButton"]'); + expect(bulkButton).toHaveLength(0); + + const createButton = wrapper.find('EuiButton[data-test-subj="createRoleMappingButton"]'); + expect(createButton).toHaveLength(0); + + const editButton = wrapper.find( + 'EuiButtonEmpty[data-test-subj="editRoleMappingButton-some-realm"]' + ); + expect(editButton).toHaveLength(0); + + const cloneButton = wrapper.find( + 'EuiButtonEmpty[data-test-subj="cloneRoleMappingButton-some-realm"]' + ); + expect(cloneButton).toHaveLength(0); + + const deleteButton = wrapper.find( + 'EuiButtonEmpty[data-test-subj="deleteRoleMappingButton-some-realm"]' + ); + expect(deleteButton).toHaveLength(0); + + const actionMenuButton = wrapper.find( + 'EuiButtonIcon[data-test-subj="euiCollapsedItemActionsButton"]' + ); + expect(actionMenuButton).toHaveLength(0); + }); + }); }); diff --git a/x-pack/plugins/security/public/management/role_mappings/role_mappings_grid/role_mappings_grid_page.tsx b/x-pack/plugins/security/public/management/role_mappings/role_mappings_grid/role_mappings_grid_page.tsx index 886e42709132c..e1f037eea22f7 100644 --- a/x-pack/plugins/security/public/management/role_mappings/role_mappings_grid/role_mappings_grid_page.tsx +++ b/x-pack/plugins/security/public/management/role_mappings/role_mappings_grid/role_mappings_grid_page.tsx @@ -4,6 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ +import type { EuiBasicTableColumn } from '@elastic/eui'; import { EuiButton, EuiButtonEmpty, @@ -12,8 +13,8 @@ import { EuiFlexItem, EuiInMemoryTable, EuiLink, - EuiPageContent_Deprecated as EuiPageContent, EuiPageHeader, + EuiPageSection, EuiSpacer, EuiToolTip, } from '@elastic/eui'; @@ -56,6 +57,7 @@ interface Props { docLinks: DocLinksStart; history: ScopedHistory; navigateToApp: ApplicationStart['navigateToApp']; + readOnly?: boolean; } interface State { @@ -68,6 +70,10 @@ interface State { } export class RoleMappingsGridPage extends Component { + static defaultProps: Partial = { + readOnly: false, + }; + private tableRef: React.RefObject>; constructor(props: any) { super(props); @@ -95,14 +101,14 @@ export class RoleMappingsGridPage extends Component { if (loadState === 'loadingApp') { return ( - + - + ); } @@ -112,7 +118,7 @@ export class RoleMappingsGridPage extends Component { } = error; return ( - + { > {statusCode}: {errorTitle} - {message} - + ); } if (loadState === 'finished' && roleMappings && roleMappings.length === 0) { return ( - - - + + + ); } @@ -163,19 +169,23 @@ export class RoleMappingsGridPage extends Component { }} /> } - rightSideItems={[ - - - , - ]} + rightSideItems={ + this.props.readOnly + ? undefined + : [ + + + , + ] + } /> @@ -287,7 +297,7 @@ export class RoleMappingsGridPage extends Component { hasActions={true} search={search} sorting={sorting} - selection={selection} + selection={this.props.readOnly ? undefined : selection} pagination={pagination} loading={loadState === 'loadingTable'} message={message} @@ -307,7 +317,7 @@ export class RoleMappingsGridPage extends Component { }; private getColumnConfig = (deleteRoleMappingPrompt: DeleteRoleMappings) => { - const config = [ + const config: Array> = [ { field: 'name', name: i18n.translate('xpack.security.management.roleMappings.nameColumnName', { @@ -377,10 +387,14 @@ export class RoleMappingsGridPage extends Component { return ; }, }, - { + ]; + + if (!this.props.readOnly) { + config.push({ name: i18n.translate('xpack.security.management.roleMappings.actionsColumnName', { defaultMessage: 'Actions', }), + width: '80px', actions: [ { isPrimary: true, @@ -478,8 +492,8 @@ export class RoleMappingsGridPage extends Component { }, }, ], - }, - ]; + }); + } return config; }; @@ -498,12 +512,14 @@ export class RoleMappingsGridPage extends Component { const { canManageRoleMappings, hasCompatibleRealms } = await this.props.roleMappingsAPI.checkRoleMappingFeatures(); + const canLoad = canManageRoleMappings || this.props.readOnly; + this.setState({ - loadState: canManageRoleMappings ? this.state.loadState : 'permissionDenied', + loadState: canLoad ? this.state.loadState : 'permissionDenied', hasCompatibleRealms, }); - if (canManageRoleMappings) { + if (canLoad) { this.performInitialLoad(); } } catch (e) { diff --git a/x-pack/plugins/security/public/management/role_mappings/role_mappings_management_app.test.tsx b/x-pack/plugins/security/public/management/role_mappings/role_mappings_management_app.test.tsx index c085bafb562c2..2f0910627be9b 100644 --- a/x-pack/plugins/security/public/management/role_mappings/role_mappings_management_app.test.tsx +++ b/x-pack/plugins/security/public/management/role_mappings/role_mappings_management_app.test.tsx @@ -31,11 +31,22 @@ jest.mock('./edit_role_mapping', () => ({ })}`, })); -async function mountApp(basePath: string, pathname: string) { +async function mountApp( + basePath: string, + pathname: string, + roleMappingSaveCapability: boolean = true +) { const container = document.createElement('div'); const setBreadcrumbs = jest.fn(); const startServices = await coreMock.createSetup().getStartServices(); + const [{ application }] = startServices; + application.capabilities = { + ...application.capabilities, + role_mappings: { + save: roleMappingSaveCapability, + }, + }; let unmount: Unmount = noop; await act(async () => { @@ -78,7 +89,29 @@ describe('roleMappingsManagementApp', () => { expect(docTitle.reset).not.toHaveBeenCalled(); expect(container).toMatchInlineSnapshot(`
    - Role Mappings Page: {"notifications":{"toasts":{}},"rolesAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}}},"roleMappingsAPI":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}}},"docLinks":{},"history":{"action":"PUSH","length":1,"location":{"pathname":"/","search":"","hash":""}}} + Role Mappings Page: {"notifications":{"toasts":{}},"rolesAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}}},"roleMappingsAPI":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}}},"docLinks":{},"history":{"action":"PUSH","length":1,"location":{"pathname":"/","search":"","hash":""}},"readOnly":false} +
    + `); + + act(() => { + unmount(); + }); + + expect(docTitle.reset).toHaveBeenCalledTimes(1); + + expect(container).toMatchInlineSnapshot(`
    `); + }); + + it('mount() works for the `grid` page in read-only mode', async () => { + const { setBreadcrumbs, container, unmount, docTitle } = await mountApp('/', '/', false); + + expect(setBreadcrumbs).toHaveBeenCalledTimes(1); + expect(setBreadcrumbs).toHaveBeenCalledWith([{ text: 'Role Mappings' }]); + expect(docTitle.change).toHaveBeenCalledWith('Role Mappings'); + expect(docTitle.reset).not.toHaveBeenCalled(); + expect(container).toMatchInlineSnapshot(` +
    + Role Mappings Page: {"notifications":{"toasts":{}},"rolesAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}}},"roleMappingsAPI":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}}},"docLinks":{},"history":{"action":"PUSH","length":1,"location":{"pathname":"/","search":"","hash":""}},"readOnly":true}
    `); @@ -103,7 +136,7 @@ describe('roleMappingsManagementApp', () => { expect(docTitle.reset).not.toHaveBeenCalled(); expect(container).toMatchInlineSnapshot(`
    - Role Mapping Edit Page: {"action":"edit","roleMappingsAPI":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}}},"rolesAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}}},"notifications":{"toasts":{}},"docLinks":{},"history":{"action":"PUSH","length":1,"location":{"pathname":"/edit","search":"","hash":""}}} + Role Mapping Edit Page: {"action":"edit","roleMappingsAPI":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}}},"rolesAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}}},"notifications":{"toasts":{}},"docLinks":{},"history":{"action":"PUSH","length":1,"location":{"pathname":"/edit","search":"","hash":""}},"readOnly":false}
    `); @@ -133,7 +166,38 @@ describe('roleMappingsManagementApp', () => { expect(docTitle.reset).not.toHaveBeenCalled(); expect(container).toMatchInlineSnapshot(`
    - Role Mapping Edit Page: {"action":"edit","name":"role@mapping","roleMappingsAPI":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}}},"rolesAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}}},"notifications":{"toasts":{}},"docLinks":{},"history":{"action":"PUSH","length":1,"location":{"pathname":"/edit/role@mapping","search":"","hash":""}}} + Role Mapping Edit Page: {"action":"edit","name":"role@mapping","roleMappingsAPI":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}}},"rolesAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}}},"notifications":{"toasts":{}},"docLinks":{},"history":{"action":"PUSH","length":1,"location":{"pathname":"/edit/role@mapping","search":"","hash":""}},"readOnly":false} +
    + `); + + act(() => { + unmount(); + }); + + expect(docTitle.reset).toHaveBeenCalledTimes(1); + + expect(container).toMatchInlineSnapshot(`
    `); + }); + + it('mount() works for the `viewing role mapping` page', async () => { + const roleMappingName = 'role@mapping'; + + const { setBreadcrumbs, container, unmount, docTitle } = await mountApp( + '/', + `/edit/${roleMappingName}`, + false + ); + + expect(setBreadcrumbs).toHaveBeenCalledTimes(1); + expect(setBreadcrumbs).toHaveBeenCalledWith([ + { href: '/', text: 'Role Mappings' }, + { text: roleMappingName }, + ]); + expect(docTitle.change).toHaveBeenCalledWith('Role Mappings'); + expect(docTitle.reset).not.toHaveBeenCalled(); + expect(container).toMatchInlineSnapshot(` +
    + Role Mapping Edit Page: {"action":"edit","name":"role@mapping","roleMappingsAPI":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}}},"rolesAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}}},"notifications":{"toasts":{}},"docLinks":{},"history":{"action":"PUSH","length":1,"location":{"pathname":"/edit/role@mapping","search":"","hash":""}},"readOnly":true}
    `); diff --git a/x-pack/plugins/security/public/management/role_mappings/role_mappings_management_app.tsx b/x-pack/plugins/security/public/management/role_mappings/role_mappings_management_app.tsx index 981638ccaca28..c5b50c21401c0 100644 --- a/x-pack/plugins/security/public/management/role_mappings/role_mappings_management_app.tsx +++ b/x-pack/plugins/security/public/management/role_mappings/role_mappings_management_app.tsx @@ -20,6 +20,7 @@ import { createBreadcrumbsChangeHandler, } from '../../components/breadcrumb'; import type { PluginStartDependencies } from '../../plugin'; +import { ReadonlyBadge } from '../badges/readonly_badge'; import { tryDecodeURIComponent } from '../url_utils'; interface CreateParams { @@ -82,6 +83,7 @@ export const roleMappingsManagementApp = Object.freeze({ notifications={core.notifications} docLinks={core.docLinks} history={history} + readOnly={!core.application.capabilities.role_mappings.save} /> ); @@ -92,6 +94,16 @@ export const roleMappingsManagementApp = Object.freeze({ + @@ -104,6 +116,7 @@ export const roleMappingsManagementApp = Object.freeze({ docLinks={core.docLinks} history={history} navigateToApp={core.application.navigateToApp} + readOnly={!core.application.capabilities.role_mappings.save} /> diff --git a/x-pack/plugins/security/public/management/users/edit_user/change_password_modal.tsx b/x-pack/plugins/security/public/management/users/edit_user/change_password_modal.tsx index beba64e6b2fb9..c7bd7dd3664dc 100644 --- a/x-pack/plugins/security/public/management/users/edit_user/change_password_modal.tsx +++ b/x-pack/plugins/security/public/management/users/edit_user/change_password_modal.tsx @@ -102,7 +102,7 @@ export const ChangePasswordModal: FunctionComponent = const isCurrentUser = currentUser?.username === username; const isSystemUser = username === 'kibana' || username === 'kibana_system'; - const [form, eventHandlers] = useForm({ + const [form, { onBlur, ...eventHandlers }] = useForm({ onSubmit: async (values) => { try { await new UserAPIClient(services.http!).changePassword( @@ -141,6 +141,13 @@ export const ChangePasswordModal: FunctionComponent = defaultValues, }); + // For some reason, the focus-lock dependency that EuiModal uses to accessibly trap focus + // is fighting the form `onBlur` and causing focus to be lost when clicking between password + // fields, so this workaround waits a tick before validating the form on blur + const validateFormOnBlur = (event: React.FocusEvent) => { + requestAnimationFrame(() => onBlur(event)); + }; + const firstFieldRef = useInitialFocus([isLoading]); const modalFormId = useGeneratedHtmlId({ prefix: 'modalForm' }); @@ -158,7 +165,13 @@ export const ChangePasswordModal: FunctionComponent = {isLoading ? ( ) : ( - + {isSystemUser ? ( <> ({ version: 1, }); +export const getPrebuiltRuleWithExceptionsMock = (): PrebuiltRuleToInstall => ({ + description: 'A rule with an exception list', + name: 'A rule with an exception list', + query: 'user.name: root or user.name: admin', + severity: 'high', + type: 'query', + risk_score: 42, + language: 'kuery', + rule_id: 'rule-with-exceptions', + exceptions_list: [ + { + id: 'endpoint_list', + list_id: 'endpoint_list', + namespace_type: 'agnostic', + type: 'endpoint', + }, + ], + version: 2, +}); + export const getPrebuiltThreatMatchRuleMock = (): PrebuiltRuleToInstall => ({ description: 'some description', name: 'Query with a rule id', diff --git a/x-pack/plugins/security_solution/common/ecs/signal/index.ts b/x-pack/plugins/security_solution/common/ecs/signal/index.ts index 05ed30639bbb7..be2d18c4fec3a 100644 --- a/x-pack/plugins/security_solution/common/ecs/signal/index.ts +++ b/x-pack/plugins/security_solution/common/ecs/signal/index.ts @@ -22,4 +22,7 @@ export type SignalEcsAAD = Exclude & { severity?: string[]; building_block_type?: string[]; workflow_status?: string[]; + suppression?: { + docs_count: string[]; + }; }; diff --git a/x-pack/plugins/security_solution/common/endpoint/constants.ts b/x-pack/plugins/security_solution/common/endpoint/constants.ts index c34236a908e49..433b9c6445b5f 100644 --- a/x-pack/plugins/security_solution/common/endpoint/constants.ts +++ b/x-pack/plugins/security_solution/common/endpoint/constants.ts @@ -57,6 +57,9 @@ export const BASE_POLICY_RESPONSE_ROUTE = `${BASE_ENDPOINT_ROUTE}/policy_respons export const BASE_POLICY_ROUTE = `${BASE_ENDPOINT_ROUTE}/policy`; export const AGENT_POLICY_SUMMARY_ROUTE = `${BASE_POLICY_ROUTE}/summaries`; +/** Suggestions routes */ +export const SUGGESTIONS_ROUTE = `${BASE_ENDPOINT_ROUTE}/suggestions/{suggestion_type}`; + /** Host Isolation Routes */ export const ISOLATE_HOST_ROUTE = `${BASE_ENDPOINT_ROUTE}/isolate`; export const UNISOLATE_HOST_ROUTE = `${BASE_ENDPOINT_ROUTE}/unisolate`; @@ -87,3 +90,5 @@ export const ENDPOINT_DEFAULT_PAGE_SIZE = 10; export const ENDPOINT_ERROR_CODES: Record = { ES_CONNECTION_ERROR: -272, }; + +export const ENDPOINT_FIELDS_SEARCH_STRATEGY = 'endpointFields'; diff --git a/x-pack/plugins/security_solution/common/endpoint/schema/suggestions.ts b/x-pack/plugins/security_solution/common/endpoint/schema/suggestions.ts new file mode 100644 index 0000000000000..1786caf34fb6f --- /dev/null +++ b/x-pack/plugins/security_solution/common/endpoint/schema/suggestions.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 type { TypeOf } from '@kbn/config-schema'; +import { schema } from '@kbn/config-schema'; + +export const EndpointSuggestionsSchema = { + body: schema.object({ + field: schema.string(), + query: schema.string(), + filters: schema.maybe(schema.any()), + fieldMeta: schema.maybe(schema.any()), + }), + params: schema.object({ + // Ready to be used with other suggestion types like endpoints + suggestion_type: schema.oneOf([schema.literal('eventFilters')]), + }), +}; + +export type EndpointSuggestionsBody = TypeOf; diff --git a/x-pack/plugins/security_solution/common/experimental_features.ts b/x-pack/plugins/security_solution/common/experimental_features.ts index 9a48abc68f466..ac3a0e2f1bfc6 100644 --- a/x-pack/plugins/security_solution/common/experimental_features.ts +++ b/x-pack/plugins/security_solution/common/experimental_features.ts @@ -80,6 +80,16 @@ export const allowedExperimentalValues = Object.freeze({ * Enables the `get-file` endpoint response action */ responseActionGetFileEnabled: false, + + /** + * Keep DEPRECATED experimental flags that are documented to prevent failed upgrades. + * https://www.elastic.co/guide/en/security/current/user-risk-score.html + * https://www.elastic.co/guide/en/security/current/host-risk-score.html + * + * Issue: https://github.com/elastic/kibana/issues/146777 + */ + riskyHostsEnabled: false, // DEPRECATED + riskyUsersEnabled: false, // DEPRECATED }); type ExperimentalConfigKeys = Array; diff --git a/x-pack/plugins/security_solution/common/search_strategy/common/index.ts b/x-pack/plugins/security_solution/common/search_strategy/common/index.ts index c33c3f9abae6b..851ffaeba2227 100644 --- a/x-pack/plugins/security_solution/common/search_strategy/common/index.ts +++ b/x-pack/plugins/security_solution/common/search_strategy/common/index.ts @@ -19,6 +19,23 @@ export type Maybe = T | null; export type SearchHit = IEsSearchResponse['rawResponse']['hits']['hits'][0]; +export interface KpiHistogramData { + x?: Maybe; + y?: Maybe; +} + +export interface KpiHistogram { + key_as_string: string; + key: number; + doc_count: number; + count: T; +} + +export interface KpiGeneralHistogramCount { + value?: number; + doc_count?: number; +} + export interface PageInfoPaginated { activePage: number; fakeTotalCount: number; diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/kpi/common/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/kpi/common/index.ts index dd4e3b1bef58f..7a13e661e914e 100644 --- a/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/kpi/common/index.ts +++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/kpi/common/index.ts @@ -11,14 +11,3 @@ export interface HostsKpiHistogramData { x?: Maybe; y?: Maybe; } - -export interface HostsKpiHistogram { - key_as_string: string; - key: number; - doc_count: number; - count: T; -} - -export interface HostsKpiGeneralHistogramCount { - value: number; -} diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/users/kpi/authentications/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/users/kpi/authentications/index.ts index 27bd722ce14dc..84090bb7ab49f 100644 --- a/x-pack/plugins/security_solution/common/search_strategy/security_solution/users/kpi/authentications/index.ts +++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/users/kpi/authentications/index.ts @@ -6,8 +6,8 @@ */ import type { IEsSearchResponse } from '@kbn/data-plugin/common'; -import type { Inspect, Maybe } from '../../../../common'; -import type { KpiHistogramData, RequestBasicOptions } from '../../..'; +import type { Inspect, KpiHistogramData, Maybe } from '../../../../common'; +import type { RequestBasicOptions } from '../../..'; export type UsersKpiAuthenticationsRequestOptions = RequestBasicOptions; diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/users/kpi/common/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/users/kpi/common/index.ts deleted file mode 100644 index a151b39fe95aa..0000000000000 --- a/x-pack/plugins/security_solution/common/search_strategy/security_solution/users/kpi/common/index.ts +++ /dev/null @@ -1,13 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type { Maybe } from '../../../..'; - -export interface KpiHistogramData { - x?: Maybe; - y?: Maybe; -} diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/users/kpi/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/users/kpi/index.ts index bc735313a7314..c89414639b417 100644 --- a/x-pack/plugins/security_solution/common/search_strategy/security_solution/users/kpi/index.ts +++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/users/kpi/index.ts @@ -5,5 +5,4 @@ * 2.0. */ -export * from './common'; export * from './total_users'; diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/users/kpi/total_users/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/users/kpi/total_users/index.ts index b493685244ee4..5fffe4ebe40c7 100644 --- a/x-pack/plugins/security_solution/common/search_strategy/security_solution/users/kpi/total_users/index.ts +++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/users/kpi/total_users/index.ts @@ -6,9 +6,8 @@ */ import type { IEsSearchResponse } from '@kbn/data-plugin/common'; -import type { Inspect, Maybe } from '../../../../common'; +import type { Inspect, KpiHistogramData, Maybe } from '../../../../common'; import type { RequestBasicOptions } from '../../..'; -import type { KpiHistogramData } from '../common'; export type TotalUsersKpiRequestOptions = RequestBasicOptions; diff --git a/x-pack/plugins/security_solution/common/types/bulk_actions/index.ts b/x-pack/plugins/security_solution/common/types/bulk_actions/index.ts new file mode 100644 index 0000000000000..bbf766d02de78 --- /dev/null +++ b/x-pack/plugins/security_solution/common/types/bulk_actions/index.ts @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { TimelineItem } from '../../search_strategy'; +export interface CustomBulkAction { + key: string; + label: string; + disableOnQuery?: boolean; + disabledLabel?: string; + onClick: (items?: TimelineItem[]) => void; + ['data-test-subj']?: string; +} + +export type CustomBulkActionProp = Omit & { + onClick: (eventIds: string[]) => void; +}; diff --git a/x-pack/plugins/security_solution/common/types/data_table/index.ts b/x-pack/plugins/security_solution/common/types/data_table/index.ts new file mode 100644 index 0000000000000..d96f8e8f4c268 --- /dev/null +++ b/x-pack/plugins/security_solution/common/types/data_table/index.ts @@ -0,0 +1,49 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import * as runtimeTypes from 'io-ts'; + +export enum Direction { + asc = 'asc', + desc = 'desc', +} + +export type SortDirectionTable = 'none' | 'asc' | 'desc' | Direction; +export interface SortColumnTable { + columnId: string; + columnType: string; + esTypes?: string[]; + sortDirection: SortDirectionTable; +} + +export type { TableById } from '../../../public/common/store/data_table/types'; + +export enum TableId { + usersPageEvents = 'users-page-events', + hostsPageEvents = 'hosts-page-events', + networkPageEvents = 'network-page-events', + hostsPageSessions = 'hosts-page-sessions-v2', // the v2 is to cache bust localstorage settings as default columns were reworked. + alertsOnRuleDetailsPage = 'alerts-rules-details-page', + alertsOnAlertsPage = 'alerts-page', + test = 'table-test', // Reserved for testing purposes + alternateTest = 'alternateTest', + rulePreview = 'rule-preview', + kubernetesPageSessions = 'kubernetes-page-sessions', +} + +const TableIdLiteralRt = runtimeTypes.union([ + runtimeTypes.literal(TableId.usersPageEvents), + runtimeTypes.literal(TableId.hostsPageEvents), + runtimeTypes.literal(TableId.networkPageEvents), + runtimeTypes.literal(TableId.hostsPageSessions), + runtimeTypes.literal(TableId.alertsOnRuleDetailsPage), + runtimeTypes.literal(TableId.alertsOnAlertsPage), + runtimeTypes.literal(TableId.test), + runtimeTypes.literal(TableId.rulePreview), + runtimeTypes.literal(TableId.kubernetesPageSessions), +]); +export type TableIdLiteral = runtimeTypes.TypeOf; diff --git a/x-pack/plugins/security_solution/common/types/detail_panel/index.ts b/x-pack/plugins/security_solution/common/types/detail_panel/index.ts new file mode 100644 index 0000000000000..8c78908c8ac84 --- /dev/null +++ b/x-pack/plugins/security_solution/common/types/detail_panel/index.ts @@ -0,0 +1,62 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { FlowTargetSourceDest } from '../../search_strategy'; +import type { TimelineTabs } from '../timeline'; + +type EmptyObject = Record; + +export type ExpandedEventType = + | { + panelView?: 'eventDetail'; + params?: { + eventId: string; + indexName: string; + refetch?: () => void; + }; + } + | EmptyObject; + +export type ExpandedHostType = + | { + panelView?: 'hostDetail'; + params?: { + hostName: string; + }; + } + | EmptyObject; + +export type ExpandedNetworkType = + | { + panelView?: 'networkDetail'; + params?: { + ip: string; + flowTarget: FlowTargetSourceDest; + }; + } + | EmptyObject; + +export type ExpandedUserType = + | { + panelView?: 'userDetail'; + params?: { + userName: string; + }; + } + | EmptyObject; + +export type ExpandedDetailType = + | ExpandedEventType + | ExpandedHostType + | ExpandedNetworkType + | ExpandedUserType; + +export type ExpandedDetailTimeline = { + [tab in TimelineTabs]?: ExpandedDetailType; +}; + +export type ExpandedDetail = Partial>; diff --git a/x-pack/plugins/security_solution/common/types/header_actions/index.ts b/x-pack/plugins/security_solution/common/types/header_actions/index.ts new file mode 100644 index 0000000000000..03a3a0679ae10 --- /dev/null +++ b/x-pack/plugins/security_solution/common/types/header_actions/index.ts @@ -0,0 +1,175 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { + EuiDataGridCellValueElementProps, + EuiDataGridColumn, + EuiDataGridColumnCellActionProps, + EuiDataGridControlColumn, +} from '@elastic/eui'; +import type { IFieldSubType } from '@kbn/es-query'; +import type { FieldBrowserOptions } from '@kbn/triggers-actions-ui-plugin/public'; +import type { ComponentType, JSXElementConstructor, ReactNode } from 'react'; +import type { OnRowSelected, SetEventsDeleted, SetEventsLoading } from '..'; +import type { Ecs } from '../../ecs'; +import type { BrowserFields, TimelineNonEcsData } from '../../search_strategy'; +import type { SortColumnTable } from '../data_table'; + +export type ColumnHeaderType = 'not-filtered' | 'text-filter'; + +/** Uniquely identifies a column */ +export type ColumnId = string; + +/** + * A `DataTableCellAction` function accepts `data`, where each row of data is + * represented as a `TimelineNonEcsData[]`. For example, `data[0]` would + * contain a `TimelineNonEcsData[]` with the first row of data. + * + * A `DataTableCellAction` returns a function that has access to all the + * `EuiDataGridColumnCellActionProps`, _plus_ access to `data`, + * which enables code like the following example to be written: + * + * Example: + * ``` + * ({ data }: { data: TimelineNonEcsData[][] }) => ({ rowIndex, columnId, Component }) => { + * const value = getMappedNonEcsValue({ + * data: data[rowIndex], // access a specific row's values + * fieldName: columnId, + * }); + * + * return ( + * alert(`row ${rowIndex} col ${columnId} has value ${value}`)} iconType="heart"> + * {'Love it'} + * + * ); + * }; + * ``` + */ +export type DataTableCellAction = ({ + browserFields, + data, + ecsData, + header, + pageSize, + scopeId, + closeCellPopover, +}: { + browserFields: BrowserFields; + /** each row of data is represented as one TimelineNonEcsData[] */ + data: TimelineNonEcsData[][]; + ecsData: Ecs[]; + header?: ColumnHeaderOptions; + pageSize: number; + scopeId: string; + closeCellPopover?: () => void; +}) => (props: EuiDataGridColumnCellActionProps) => ReactNode; + +/** The specification of a column header */ +export type ColumnHeaderOptions = Pick< + EuiDataGridColumn, + | 'actions' + | 'defaultSortDirection' + | 'display' + | 'displayAsText' + | 'id' + | 'initialWidth' + | 'isSortable' + | 'schema' +> & { + aggregatable?: boolean; + dataTableCellActions?: DataTableCellAction[]; + category?: string; + columnHeaderType: ColumnHeaderType; + description?: string | null; + esTypes?: string[]; + example?: string | number | null; + format?: string; + linkField?: string; + placeholder?: string; + subType?: IFieldSubType; + type?: string; +}; +export interface HeaderActionProps { + width: number; + browserFields: BrowserFields; + columnHeaders: ColumnHeaderOptions[]; + fieldBrowserOptions?: FieldBrowserOptions; + isEventViewer?: boolean; + isSelectAllChecked: boolean; + onSelectAll: ({ isSelected }: { isSelected: boolean }) => void; + showEventsSelect: boolean; + showSelectAllCheckbox: boolean; + sort: SortColumnTable[]; + tabType: string; + timelineId: string; +} + +export type HeaderCellRender = ComponentType | ComponentType; + +type GenericActionRowCellRenderProps = Pick< + EuiDataGridCellValueElementProps, + 'rowIndex' | 'columnId' +>; + +export type RowCellRender = + | JSXElementConstructor + | ((props: GenericActionRowCellRenderProps) => JSX.Element) + | JSXElementConstructor + | ((props: ActionProps) => JSX.Element); + +export interface ActionProps { + action?: RowCellRender; + ariaRowindex: number; + checked: boolean; + columnId: string; + columnValues: string; + data: TimelineNonEcsData[]; + disabled?: boolean; + ecsData: Ecs; + eventId: string; + eventIdToNoteIds?: Readonly>; + index: number; + isEventPinned?: boolean; + isEventViewer?: boolean; + loadingEventIds: Readonly; + onEventDetailsPanelOpened: () => void; + onRowSelected: OnRowSelected; + onRuleChange?: () => void; + refetch?: () => void; + rowIndex: number; + setEventsDeleted: SetEventsDeleted; + setEventsLoading: SetEventsLoading; + showCheckboxes: boolean; + showNotes?: boolean; + tabType?: string; + timelineId: string; + toggleShowNotes?: () => void; + width?: number; +} + +interface AdditionalControlColumnProps { + ariaRowindex: number; + actionsColumnWidth: number; + columnValues: string; + checked: boolean; + onRowSelected: OnRowSelected; + eventId: string; + id: string; + columnId: string; + loadingEventIds: Readonly; + onEventDetailsPanelOpened: () => void; + showCheckboxes: boolean; + // Override these type definitions to support either a generic custom component or the one used in security_solution today. + headerCellRender: HeaderCellRender; + rowCellRender: RowCellRender; +} + +export type ControlColumnProps = Omit< + EuiDataGridControlColumn, + keyof AdditionalControlColumnProps +> & + Partial; diff --git a/x-pack/plugins/security_solution/common/types/index.ts b/x-pack/plugins/security_solution/common/types/index.ts index 9464a33082a49..cb155d2ec738c 100644 --- a/x-pack/plugins/security_solution/common/types/index.ts +++ b/x-pack/plugins/security_solution/common/types/index.ts @@ -5,4 +5,18 @@ * 2.0. */ +import type { Status } from '../detection_engine/schemas/common'; + export * from './timeline'; +export * from './data_table'; +export * from './detail_panel'; +export * from './header_actions'; +export * from './session_view'; +export * from './bulk_actions'; + +export const FILTER_OPEN: Status = 'open'; +export const FILTER_CLOSED: Status = 'closed'; +export const FILTER_ACKNOWLEDGED: Status = 'acknowledged'; + +export type SetEventsLoading = (params: { eventIds: string[]; isLoading: boolean }) => void; +export type SetEventsDeleted = (params: { eventIds: string[]; isDeleted: boolean }) => void; diff --git a/x-pack/plugins/security_solution/common/types/session_view/index.ts b/x-pack/plugins/security_solution/common/types/session_view/index.ts new file mode 100644 index 0000000000000..105e5cc6b1d84 --- /dev/null +++ b/x-pack/plugins/security_solution/common/types/session_view/index.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export interface SessionViewConfig { + sessionEntityId: string; + jumpToEntityId?: string; + jumpToCursor?: string; + investigatedAlertId?: string; +} diff --git a/x-pack/plugins/security_solution/common/types/timeline/actions/index.ts b/x-pack/plugins/security_solution/common/types/timeline/actions/index.ts deleted file mode 100644 index c3778b43bcae9..0000000000000 --- a/x-pack/plugins/security_solution/common/types/timeline/actions/index.ts +++ /dev/null @@ -1,14 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ -export type { - ActionProps, - HeaderActionProps, - GenericActionRowCellRenderProps, - HeaderCellRender, - RowCellRender, - ControlColumnProps, -} from '@kbn/timelines-plugin/common'; diff --git a/x-pack/plugins/security_solution/common/types/timeline/cells/index.ts b/x-pack/plugins/security_solution/common/types/timeline/cells/index.ts index 22c230db559f1..903c555aa96d0 100644 --- a/x-pack/plugins/security_solution/common/types/timeline/cells/index.ts +++ b/x-pack/plugins/security_solution/common/types/timeline/cells/index.ts @@ -5,4 +5,30 @@ * 2.0. */ -export type { CellValueElementProps } from '@kbn/timelines-plugin/common'; +import type { EuiDataGridCellValueElementProps } from '@elastic/eui'; +import type { Filter } from '@kbn/es-query'; +import type { ColumnHeaderOptions, RowRenderer } from '../..'; +import type { Ecs } from '../../../ecs'; +import type { BrowserFields, TimelineNonEcsData } from '../../../search_strategy'; + +/** The following props are provided to the function called by `renderCellValue` */ +export type CellValueElementProps = EuiDataGridCellValueElementProps & { + asPlainText?: boolean; + browserFields?: BrowserFields; + data: TimelineNonEcsData[]; + ecsData?: Ecs; + eventId: string; // _id + globalFilters?: Filter[]; + header: ColumnHeaderOptions; + isDraggable: boolean; + isTimeline?: boolean; // Default cell renderer is used for both the alert table and timeline. This allows us to cheaply separate concerns + linkValues: string[] | undefined; + rowRenderers?: RowRenderer[]; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + setFlyoutAlert?: (data: any) => void; + scopeId: string; + truncate?: boolean; + key?: string; + closeCellPopover?: () => void; + enableActions?: boolean; +}; diff --git a/x-pack/plugins/security_solution/common/types/timeline/columns/index.ts b/x-pack/plugins/security_solution/common/types/timeline/columns/index.ts index f58558be948de..92812b730a3fe 100644 --- a/x-pack/plugins/security_solution/common/types/timeline/columns/index.ts +++ b/x-pack/plugins/security_solution/common/types/timeline/columns/index.ts @@ -5,9 +5,4 @@ * 2.0. */ -export type { - ColumnHeaderType, - ColumnId, - ColumnHeaderOptions, - ColumnRenderer, -} from '@kbn/timelines-plugin/common'; +export type { ColumnHeaderOptions } from '../../header_actions'; diff --git a/x-pack/plugins/security_solution/common/types/timeline/index.ts b/x-pack/plugins/security_solution/common/types/timeline/index.ts index b5923bb4f3b36..ba4a8a13501fa 100644 --- a/x-pack/plugins/security_solution/common/types/timeline/index.ts +++ b/x-pack/plugins/security_solution/common/types/timeline/index.ts @@ -22,12 +22,11 @@ import { success, success_count as successCount, } from '../../detection_engine/schemas/common/schemas'; -import type { FlowTargetSourceDest } from '../../search_strategy/security_solution/network'; import { errorSchema } from '../../detection_engine/schemas/response/error_schema'; import type { Maybe } from '../../search_strategy'; import { Direction } from '../../search_strategy'; +import type { ExpandedDetailType } from '../detail_panel'; -export * from './actions'; export * from './cells'; export * from './columns'; export * from './data_provider'; @@ -326,32 +325,6 @@ export enum TimelineId { detectionsAlertDetailsPage = 'detections-alert-details-page', } -export enum TableId { - usersPageEvents = 'users-page-events', - hostsPageEvents = 'hosts-page-events', - networkPageEvents = 'network-page-events', - hostsPageSessions = 'hosts-page-sessions-v2', // the v2 is to cache bust localstorage settings as default columns were reworked. - alertsOnRuleDetailsPage = 'alerts-rules-details-page', - alertsOnAlertsPage = 'alerts-page', - test = 'table-test', // Reserved for testing purposes - alternateTest = 'alternateTest', - rulePreview = 'rule-preview', - kubernetesPageSessions = 'kubernetes-page-sessions', -} - -export const TableIdLiteralRt = runtimeTypes.union([ - runtimeTypes.literal(TableId.usersPageEvents), - runtimeTypes.literal(TableId.hostsPageEvents), - runtimeTypes.literal(TableId.networkPageEvents), - runtimeTypes.literal(TableId.hostsPageSessions), - runtimeTypes.literal(TableId.alertsOnRuleDetailsPage), - runtimeTypes.literal(TableId.alertsOnAlertsPage), - runtimeTypes.literal(TableId.test), - runtimeTypes.literal(TableId.rulePreview), - runtimeTypes.literal(TableId.kubernetesPageSessions), -]); -export type TableIdLiteral = runtimeTypes.TypeOf; - export const TimelineSavedToReturnObjectRuntimeType = runtimeTypes.intersection([ SavedTimelineRuntimeType, runtimeTypes.type({ @@ -484,59 +457,7 @@ export interface ScrollToTopEvent { timestamp: number; } -// eslint-disable-next-line @typescript-eslint/no-explicit-any -type EmptyObject = Record; - -export type TimelineExpandedEventType = - | { - panelView?: 'eventDetail'; - params?: { - eventId: string; - indexName: string; - refetch?: () => void; - }; - } - | EmptyObject; - -export type TimelineExpandedHostType = - | { - panelView?: 'hostDetail'; - params?: { - hostName: string; - }; - } - | EmptyObject; - -export type TimelineExpandedNetworkType = - | { - panelView?: 'networkDetail'; - params?: { - ip: string; - flowTarget: FlowTargetSourceDest; - }; - } - | EmptyObject; - -export type TimelineExpandedUserType = - | { - panelView?: 'userDetail'; - params?: { - userName: string; - }; - } - | EmptyObject; - -export type TimelineExpandedDetailType = - | TimelineExpandedEventType - | TimelineExpandedHostType - | TimelineExpandedNetworkType - | TimelineExpandedUserType; - -export type TimelineExpandedDetail = { - [tab in TimelineTabs]?: TimelineExpandedDetailType; -}; - -export type ToggleDetailPanel = TimelineExpandedDetailType & { +export type ToggleDetailPanel = ExpandedDetailType & { tabType?: TimelineTabs; id: string; }; diff --git a/x-pack/plugins/security_solution/common/types/timeline/store.ts b/x-pack/plugins/security_solution/common/types/timeline/store.ts index 047c8d20e15b7..c4a6c3460d38e 100644 --- a/x-pack/plugins/security_solution/common/types/timeline/store.ts +++ b/x-pack/plugins/security_solution/common/types/timeline/store.ts @@ -6,15 +6,11 @@ */ import type { Filter } from '@kbn/es-query'; -import type { - ColumnHeaderOptions, - ColumnId, - RowRendererId, - TimelineExpandedDetail, - TimelineTypeLiteral, -} from '.'; +import type { RowRendererId, TimelineTypeLiteral } from '.'; import type { Direction } from '../../search_strategy'; +import type { ExpandedDetailTimeline } from '../detail_panel'; +import type { ColumnHeaderOptions, ColumnId } from '../header_actions'; import type { DataProvider } from './data_provider'; export type KueryFilterQueryKind = 'kuery' | 'lucene' | 'eql'; @@ -47,7 +43,7 @@ export interface TimelinePersistInput { }; defaultColumns?: ColumnHeaderOptions[]; excludedRowRendererIds?: RowRendererId[]; - expandedDetail?: TimelineExpandedDetail; + expandedDetail?: ExpandedDetailTimeline; filters?: Filter[]; id: string; indexNames: string[]; diff --git a/x-pack/plugins/security_solution/cypress/e2e/detection_rules/custom_query_rule.cy.ts b/x-pack/plugins/security_solution/cypress/e2e/detection_rules/custom_query_rule.cy.ts index b5869987f4d58..9938dd176574a 100644 --- a/x-pack/plugins/security_solution/cypress/e2e/detection_rules/custom_query_rule.cy.ts +++ b/x-pack/plugins/security_solution/cypress/e2e/detection_rules/custom_query_rule.cy.ts @@ -76,7 +76,6 @@ import { editFirstRule, goToRuleDetails, selectNumberOfRules, - waitForRulesTableToBeRefreshed, } from '../../tasks/alerts_detection_rules'; import { createCustomRuleEnabled } from '../../tasks/api_calls/rules'; import { createTimeline } from '../../tasks/api_calls/timelines'; @@ -263,7 +262,6 @@ describe('Custom query rules', () => { }); deleteFirstRule(); - waitForRulesTableToBeRefreshed(); cy.get(RULES_TABLE) .find(RULES_ROW) @@ -290,7 +288,6 @@ describe('Custom query rules', () => { selectNumberOfRules(numberOfRulesToBeDeleted); deleteSelectedRules(); - waitForRulesTableToBeRefreshed(); cy.get(RULES_TABLE) .find(RULES_ROW) diff --git a/x-pack/plugins/security_solution/cypress/e2e/exceptions/exceptions_management_flow/all_exception_lists_read_only.cy.ts b/x-pack/plugins/security_solution/cypress/e2e/exceptions/exceptions_management_flow/all_exception_lists_read_only.cy.ts index c1c4fc18960e6..ae16df825d125 100644 --- a/x-pack/plugins/security_solution/cypress/e2e/exceptions/exceptions_management_flow/all_exception_lists_read_only.cy.ts +++ b/x-pack/plugins/security_solution/cypress/e2e/exceptions/exceptions_management_flow/all_exception_lists_read_only.cy.ts @@ -5,16 +5,16 @@ * 2.0. */ +import { cleanKibana } from '../../../tasks/common'; import { ROLES } from '../../../../common/test'; import { getExceptionList } from '../../../objects/exception'; import { EXCEPTIONS_TABLE_SHOWING_LISTS } from '../../../screens/exceptions'; -import { createExceptionList } from '../../../tasks/api_calls/exceptions'; +import { createExceptionList, deleteExceptionList } from '../../../tasks/api_calls/exceptions'; import { dismissCallOut, getCallOut, waitForCallOutToBeShown, } from '../../../tasks/common/callouts'; -import { esArchiverResetKibana } from '../../../tasks/es_archiver'; import { login, visitWithoutDateRange } from '../../../tasks/login'; import { EXCEPTIONS_URL } from '../../../urls/navigation'; @@ -22,7 +22,11 @@ const MISSING_PRIVILEGES_CALLOUT = 'missing-user-privileges'; describe('All exception lists - read only', () => { before(() => { - esArchiverResetKibana(); + cleanKibana(); + }); + + beforeEach(() => { + deleteExceptionList(getExceptionList().list_id, getExceptionList().namespace_type); // Create exception list not used by any rules createExceptionList(getExceptionList(), getExceptionList().list_id); @@ -30,8 +34,6 @@ describe('All exception lists - read only', () => { login(ROLES.reader); visitWithoutDateRange(EXCEPTIONS_URL, ROLES.reader); - cy.reload(); - // Using cy.contains because we do not care about the exact text, // just checking number of lists shown cy.contains(EXCEPTIONS_TABLE_SHOWING_LISTS, '1'); diff --git a/x-pack/plugins/security_solution/cypress/e2e/header/search_bar.cy.ts b/x-pack/plugins/security_solution/cypress/e2e/header/search_bar.cy.ts index 8e7bd5f38b068..e09dac46c04e7 100644 --- a/x-pack/plugins/security_solution/cypress/e2e/header/search_bar.cy.ts +++ b/x-pack/plugins/security_solution/cypress/e2e/header/search_bar.cy.ts @@ -6,8 +6,17 @@ */ import { login, visit } from '../../tasks/login'; -import { openAddFilterPopover, fillAddFilterForm } from '../../tasks/search_bar'; -import { GLOBAL_SEARCH_BAR_FILTER_ITEM } from '../../screens/search_bar'; +import { + openAddFilterPopover, + fillAddFilterForm, + openKqlQueryBar, + fillKqlQueryBar, +} from '../../tasks/search_bar'; +import { + AUTO_SUGGEST_AGENT_NAME, + AUTO_SUGGEST_HOST_NAME_VALUE, + GLOBAL_SEARCH_BAR_FILTER_ITEM, +} from '../../screens/search_bar'; import { getHostIpFilter } from '../../objects/filter'; import { HOSTS_URL } from '../../urls/navigation'; @@ -29,4 +38,11 @@ describe('SearchBar', () => { `${getHostIpFilter().key}: ${getHostIpFilter().value}` ); }); + + it('auto suggests fields from the data view and auto completes available field values', () => { + openKqlQueryBar(); + cy.get(AUTO_SUGGEST_AGENT_NAME).should('be.visible'); + fillKqlQueryBar(`host.name:`); + cy.get(AUTO_SUGGEST_HOST_NAME_VALUE).should('be.visible'); + }); }); diff --git a/x-pack/plugins/security_solution/cypress/screens/exceptions.ts b/x-pack/plugins/security_solution/cypress/screens/exceptions.ts index fcd82827c05e4..2776f4a35ff0d 100644 --- a/x-pack/plugins/security_solution/cypress/screens/exceptions.ts +++ b/x-pack/plugins/security_solution/cypress/screens/exceptions.ts @@ -101,7 +101,7 @@ export const ADD_TO_SHARED_LIST_RADIO_LABEL = '[data-test-subj="addToListsRadioO export const ADD_TO_SHARED_LIST_RADIO_INPUT = 'input[id="add_to_lists"]'; -export const SHARED_LIST_CHECKBOX = '.euiTableRow .euiCheckbox__input'; +export const SHARED_LIST_SWITCH = '[data-test-subj="addToSharedListSwitch"]'; export const ADD_TO_RULE_RADIO_LABEL = 'label [data-test-subj="addToRuleRadioOption"]'; diff --git a/x-pack/plugins/security_solution/cypress/screens/search_bar.ts b/x-pack/plugins/security_solution/cypress/screens/search_bar.ts index d5f3e7743c9b1..352a841a1c7be 100644 --- a/x-pack/plugins/security_solution/cypress/screens/search_bar.ts +++ b/x-pack/plugins/security_solution/cypress/screens/search_bar.ts @@ -32,3 +32,10 @@ export const GLOBAL_SEARCH_BAR_FILTER_ITEM = '#popoverFor_filter0'; export const GLOBAL_SEARCH_BAR_FILTER_ITEM_AT = (value: number) => `#popoverFor_filter${value}`; export const GLOBAL_SEARCH_BAR_PINNED_FILTER = '.globalFilterItem-isPinned'; + +export const GLOBAL_KQL_INPUT = + '[data-test-subj="filters-global-container"] [data-test-subj="unifiedQueryInput"] textarea'; + +export const AUTO_SUGGEST_AGENT_NAME = `[data-test-subj="autocompleteSuggestion-field-agent.name-"]`; + +export const AUTO_SUGGEST_HOST_NAME_VALUE = `[data-test-subj='autocompleteSuggestion-value-"siem-kibana"-']`; diff --git a/x-pack/plugins/security_solution/cypress/tasks/exceptions.ts b/x-pack/plugins/security_solution/cypress/tasks/exceptions.ts index 1e89e14c1280e..ac7e5572cc1d1 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/exceptions.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/exceptions.ts @@ -21,7 +21,7 @@ import { CLOSE_SINGLE_ALERT_CHECKBOX, ADD_TO_RULE_RADIO_LABEL, ADD_TO_SHARED_LIST_RADIO_LABEL, - SHARED_LIST_CHECKBOX, + SHARED_LIST_SWITCH, OS_SELECTION_SECTION, OS_INPUT, } from '../screens/exceptions'; @@ -124,10 +124,9 @@ export const selectAddToRuleRadio = () => { export const selectSharedListToAddExceptionTo = (numListsToCheck = 1) => { cy.get(ADD_TO_SHARED_LIST_RADIO_LABEL).click(); for (let i = 0; i < numListsToCheck; i++) { - cy.get(SHARED_LIST_CHECKBOX) + cy.get(SHARED_LIST_SWITCH) .eq(i) - .pipe(($el) => $el.trigger('click')) - .should('be.checked'); + .pipe(($el) => $el.trigger('click')); } }; diff --git a/x-pack/plugins/security_solution/cypress/tasks/search_bar.ts b/x-pack/plugins/security_solution/cypress/tasks/search_bar.ts index 3a6a078ca1e54..fb5e978befdda 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/search_bar.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/search_bar.ts @@ -16,6 +16,7 @@ import { ADD_FILTER_FORM_OPERATOR_FIELD, ADD_FILTER_FORM_FIELD_OPTION, ADD_FILTER_FORM_FILTER_VALUE_INPUT, + GLOBAL_KQL_INPUT, } from '../screens/search_bar'; export const openAddFilterPopover = () => { @@ -24,6 +25,16 @@ export const openAddFilterPopover = () => { cy.get(GLOBAL_SEARCH_BAR_ADD_FILTER).click(); }; +export const openKqlQueryBar = () => { + cy.get(GLOBAL_KQL_INPUT).should('be.visible'); + cy.get(GLOBAL_KQL_INPUT).click(); +}; + +export const fillKqlQueryBar = (query: string) => { + cy.get(GLOBAL_KQL_INPUT).should('be.visible'); + cy.get(GLOBAL_KQL_INPUT).type(query); +}; + export const fillAddFilterForm = ({ key, value, operator }: SearchBarFilter) => { cy.get(ADD_FILTER_FORM_FIELD_INPUT).should('exist'); cy.get(ADD_FILTER_FORM_FIELD_INPUT).should('be.visible'); diff --git a/x-pack/plugins/security_solution/public/app/home/global_header/index.test.tsx b/x-pack/plugins/security_solution/public/app/home/global_header/index.test.tsx index 38cd8f190bc6e..bd5049909c95d 100644 --- a/x-pack/plugins/security_solution/public/app/home/global_header/index.test.tsx +++ b/x-pack/plugins/security_solution/public/app/home/global_header/index.test.tsx @@ -26,7 +26,6 @@ import { TimelineId } from '../../../../common/types/timeline'; import { createStore } from '../../../common/store'; import { kibanaObservable } from '@kbn/timelines-plugin/public/mock'; import { sourcererPaths } from '../../../common/containers/sourcerer'; -import { tGridReducer } from '@kbn/timelines-plugin/public'; jest.mock('react-router-dom', () => { const actual = jest.requireActual('react-router-dom'); @@ -73,13 +72,7 @@ describe('global header', () => { }, }; const { storage } = createSecuritySolutionStorageMock(); - const store = createStore( - state, - SUB_PLUGINS_REDUCER, - { dataTable: tGridReducer }, - kibanaObservable, - storage - ); + const store = createStore(state, SUB_PLUGINS_REDUCER, kibanaObservable, storage); beforeEach(() => { useVariationMock.mockReset(); @@ -176,13 +169,7 @@ describe('global header', () => { }, }, }; - const mockStore = createStore( - mockstate, - SUB_PLUGINS_REDUCER, - { dataTable: tGridReducer }, - kibanaObservable, - storage - ); + const mockStore = createStore(mockstate, SUB_PLUGINS_REDUCER, kibanaObservable, storage); (useLocation as jest.Mock).mockReturnValue({ pathname: sourcererPaths[2] }); diff --git a/x-pack/plugins/security_solution/public/app/home/index.test.tsx b/x-pack/plugins/security_solution/public/app/home/index.test.tsx index 21c968130602c..5939205317b26 100644 --- a/x-pack/plugins/security_solution/public/app/home/index.test.tsx +++ b/x-pack/plugins/security_solution/public/app/home/index.test.tsx @@ -33,7 +33,6 @@ import type { TimelineUrl } from '../../timelines/store/timeline/model'; import { timelineDefaults } from '../../timelines/store/timeline/defaults'; import { URL_PARAM_KEY } from '../../common/hooks/use_url_state'; import { InputsModelId } from '../../common/store/inputs/constants'; -import { tGridReducer } from '@kbn/timelines-plugin/public'; jest.mock('../../common/store/inputs/actions'); @@ -299,13 +298,7 @@ describe('HomePage', () => { }, }; - const mockStore = createStore( - mockstate, - SUB_PLUGINS_REDUCER, - { dataTable: tGridReducer }, - kibanaObservable, - storage - ); + const mockStore = createStore(mockstate, SUB_PLUGINS_REDUCER, kibanaObservable, storage); render( @@ -459,13 +452,7 @@ describe('HomePage', () => { }; const { storage } = createSecuritySolutionStorageMock(); - const mockStore = createStore( - mockstate, - SUB_PLUGINS_REDUCER, - { dataTable: tGridReducer }, - kibanaObservable, - storage - ); + const mockStore = createStore(mockstate, SUB_PLUGINS_REDUCER, kibanaObservable, storage); const TestComponent = () => ( @@ -522,13 +509,7 @@ describe('HomePage', () => { }; const { storage } = createSecuritySolutionStorageMock(); - const mockStore = createStore( - mockstate, - SUB_PLUGINS_REDUCER, - { dataTable: tGridReducer }, - kibanaObservable, - storage - ); + const mockStore = createStore(mockstate, SUB_PLUGINS_REDUCER, kibanaObservable, storage); const TestComponent = () => ( @@ -588,13 +569,7 @@ describe('HomePage', () => { it('it removes empty timeline state from URL', async () => { const { storage } = createSecuritySolutionStorageMock(); - const store = createStore( - mockGlobalState, - SUB_PLUGINS_REDUCER, - { dataTable: tGridReducer }, - kibanaObservable, - storage - ); + const store = createStore(mockGlobalState, SUB_PLUGINS_REDUCER, kibanaObservable, storage); mockUseInitializeUrlParam(URL_PARAM_KEY.timeline, { id: 'testSavedTimelineId', @@ -621,13 +596,7 @@ describe('HomePage', () => { it('it updates URL when timeline store changes', async () => { const { storage } = createSecuritySolutionStorageMock(); - const store = createStore( - mockGlobalState, - SUB_PLUGINS_REDUCER, - { dataTable: tGridReducer }, - kibanaObservable, - storage - ); + const store = createStore(mockGlobalState, SUB_PLUGINS_REDUCER, kibanaObservable, storage); const savedObjectId = 'testTimelineId'; mockUseInitializeUrlParam(URL_PARAM_KEY.timeline, { diff --git a/x-pack/plugins/security_solution/public/app/types.ts b/x-pack/plugins/security_solution/public/app/types.ts index a5cfc603a49fd..d2b449d484089 100644 --- a/x-pack/plugins/security_solution/public/app/types.ts +++ b/x-pack/plugins/security_solution/public/app/types.ts @@ -18,8 +18,6 @@ import type { import type { RouteProps } from 'react-router-dom'; import type { AppMountParameters } from '@kbn/core/public'; import type { UsageCollectionSetup } from '@kbn/usage-collection-plugin/public'; -import type { TableState } from '@kbn/timelines-plugin/public'; - import type { StartServices } from '../types'; /** @@ -35,6 +33,7 @@ export interface RenderAppProps extends AppMountParameters { import type { State, SubPluginsInitReducer } from '../common/store'; import type { Immutable } from '../../common/endpoint/types'; import type { AppAction } from '../common/store/actions'; +import type { TableState } from '../common/store/data_table/types'; export { SecurityPageName } from '../../common/constants'; diff --git a/x-pack/plugins/security_solution/public/common/components/chart_settings_popover/configurations/default/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/chart_settings_popover/configurations/default/index.test.tsx index 4daf4847e381c..915cac434f57b 100644 --- a/x-pack/plugins/security_solution/public/common/components/chart_settings_popover/configurations/default/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/chart_settings_popover/configurations/default/index.test.tsx @@ -19,7 +19,6 @@ import type { State } from '../../../../store'; import { createStore } from '../../../../store'; import * as i18n from './translations'; import { useChartSettingsPopoverConfiguration } from '.'; -import { tGridReducer } from '@kbn/timelines-plugin/public'; const mockHandleClick = jest.fn(); @@ -33,13 +32,7 @@ describe('useChartSettingsPopoverConfiguration', () => { const state: State = mockGlobalState; const { storage } = createSecuritySolutionStorageMock(); - const store = createStore( - state, - SUB_PLUGINS_REDUCER, - { dataTable: tGridReducer }, - kibanaObservable, - storage - ); + const store = createStore(state, SUB_PLUGINS_REDUCER, kibanaObservable, storage); const wrapper = ({ children }: { children: React.ReactNode }) => ( {children} ); diff --git a/x-pack/plugins/timelines/public/components/t_grid/body/control_columns/checkbox.test.tsx b/x-pack/plugins/security_solution/public/common/components/control_columns/checkbox.test.tsx similarity index 93% rename from x-pack/plugins/timelines/public/components/t_grid/body/control_columns/checkbox.test.tsx rename to x-pack/plugins/security_solution/public/common/components/control_columns/checkbox.test.tsx index ca001c2f804b7..0dd74cd229748 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/body/control_columns/checkbox.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/control_columns/checkbox.test.tsx @@ -6,9 +6,10 @@ */ import { render, fireEvent } from '@testing-library/react'; -import { ActionProps, HeaderActionProps } from '../../../../../common/types'; import { HeaderCheckBox, RowCheckBox } from './checkbox'; import React from 'react'; +import type { ActionProps, HeaderActionProps } from '../../../../common/types'; +import { TimelineTabs } from '../../../../common/types'; describe('checkbox control column', () => { describe('RowCheckBox', () => { @@ -61,7 +62,7 @@ describe('checkbox control column', () => { showEventsSelect: true, showSelectAllCheckbox: true, sort: [], - tabType: 'query', + tabType: TimelineTabs.query, timelineId: 'test-timelineId', }; diff --git a/x-pack/plugins/timelines/public/components/t_grid/body/control_columns/checkbox.tsx b/x-pack/plugins/security_solution/public/common/components/control_columns/checkbox.tsx similarity index 98% rename from x-pack/plugins/timelines/public/components/t_grid/body/control_columns/checkbox.tsx rename to x-pack/plugins/security_solution/public/common/components/control_columns/checkbox.tsx index b1a16cf5b3abd..725ed784b0ec6 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/body/control_columns/checkbox.tsx +++ b/x-pack/plugins/security_solution/public/common/components/control_columns/checkbox.tsx @@ -7,7 +7,7 @@ import { EuiCheckbox, EuiLoadingSpinner } from '@elastic/eui'; import React, { useCallback } from 'react'; -import type { ActionProps, HeaderActionProps } from '../../../../../common/types'; +import type { ActionProps, HeaderActionProps } from '../../../../common/types'; import * as i18n from './translations'; export const RowCheckBox = ({ diff --git a/x-pack/plugins/security_solution/public/common/components/control_columns/index.tsx b/x-pack/plugins/security_solution/public/common/components/control_columns/index.tsx new file mode 100644 index 0000000000000..deb5482cd7e7c --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/control_columns/index.tsx @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { ControlColumnProps } from '../../../../common/types'; +import { HeaderCheckBox, RowCheckBox } from './checkbox'; + +export const checkBoxControlColumn: ControlColumnProps = { + id: 'checkbox-control-column', + width: 32, + headerCellRender: HeaderCheckBox, + rowCellRender: RowCheckBox, +}; + +export * from './transform_control_columns'; diff --git a/x-pack/plugins/security_solution/public/common/components/control_columns/row_action/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/control_columns/row_action/index.test.tsx new file mode 100644 index 0000000000000..2e74209287d57 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/control_columns/row_action/index.test.tsx @@ -0,0 +1,74 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { TableId } from '../../../../../common/types'; +import { render } from '@testing-library/react'; +import React from 'react'; +import { RowAction } from '.'; +import { defaultHeaders, TestProviders } from '../../../mock'; +import { getDefaultControlColumn } from '../../../../timelines/components/timeline/body/control_columns'; +import { useIsExperimentalFeatureEnabled } from '../../../hooks/use_experimental_features'; + +jest.mock('../../../hooks/use_experimental_features', () => ({ + useIsExperimentalFeatureEnabled: jest.fn().mockReturnValue(true), +})); +const useIsExperimentalFeatureEnabledMock = useIsExperimentalFeatureEnabled as jest.Mock; +useIsExperimentalFeatureEnabledMock.mockReturnValue(true); + +const mockDispatch = jest.fn(); +jest.mock('react-redux', () => { + const original = jest.requireActual('react-redux'); + + return { + ...original, + useDispatch: () => mockDispatch, + }; +}); + +describe('RowAction', () => { + const sampleData = { + _id: '1', + data: [], + ecs: { + _id: '1', + }, + }; + const defaultProps = { + columnHeaders: defaultHeaders, + controlColumn: getDefaultControlColumn(5)[0], + data: [sampleData], + disabled: false, + index: 1, + isEventViewer: false, + loadingEventIds: [], + onRowSelected: jest.fn(), + onRuleChange: jest.fn(), + selectedEventIds: {}, + tableId: TableId.test, + width: 100, + setEventsLoading: jest.fn(), + setEventsDeleted: jest.fn(), + pageRowIndex: 0, + columnId: 'test-columnId', + isDetails: false, + isExpanded: false, + isExpandable: false, + rowIndex: 0, + colIndex: 0, + setCellProps: jest.fn(), + tabType: 'query', + showCheckboxes: false, + }; + test('displays expand events button', () => { + const wrapper = render( + + + + ); + expect(wrapper.getAllByTestId('expand-event')).not.toBeNull(); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/components/control_columns/row_action/index.tsx b/x-pack/plugins/security_solution/public/common/components/control_columns/row_action/index.tsx new file mode 100644 index 0000000000000..09bb1baa3cbc0 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/control_columns/row_action/index.tsx @@ -0,0 +1,146 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { EuiDataGridCellValueElementProps } from '@elastic/eui'; +import React, { useCallback, useMemo } from 'react'; +import { useDispatch } from 'react-redux'; +import type { + SetEventsDeleted, + SetEventsLoading, + ControlColumnProps, + ExpandedDetailType, +} from '../../../../../common/types'; +import { getMappedNonEcsValue } from '../../../../timelines/components/timeline/body/data_driven_columns'; + +import type { TimelineItem, TimelineNonEcsData } from '../../../../../common/search_strategy'; +import type { ColumnHeaderOptions, OnRowSelected } from '../../../../../common/types/timeline'; +import { dataTableActions } from '../../../store/data_table'; + +type Props = EuiDataGridCellValueElementProps & { + columnHeaders: ColumnHeaderOptions[]; + controlColumn: ControlColumnProps; + data: TimelineItem[]; + disabled: boolean; + index: number; + isEventViewer: boolean; + loadingEventIds: Readonly; + onRowSelected: OnRowSelected; + onRuleChange?: () => void; + selectedEventIds: Readonly>; + showCheckboxes: boolean; + tabType?: string; + tableId: string; + width: number; + setEventsLoading: SetEventsLoading; + setEventsDeleted: SetEventsDeleted; + pageRowIndex: number; +}; + +const RowActionComponent = ({ + columnHeaders, + controlColumn, + data, + disabled, + index, + isEventViewer, + loadingEventIds, + onRowSelected, + onRuleChange, + pageRowIndex, + rowIndex, + selectedEventIds, + showCheckboxes, + tabType, + tableId, + setEventsLoading, + setEventsDeleted, + width, +}: Props) => { + const { + data: timelineNonEcsData, + ecs: ecsData, + _id: eventId, + _index: indexName, + } = useMemo(() => { + const rowData: Partial = data[pageRowIndex]; + return rowData ?? {}; + }, [data, pageRowIndex]); + + const dispatch = useDispatch(); + + const columnValues = useMemo( + () => + timelineNonEcsData && + columnHeaders + .map( + (header) => + getMappedNonEcsValue({ + data: timelineNonEcsData, + fieldName: header.id, + }) ?? [] + ) + .join(' '), + [columnHeaders, timelineNonEcsData] + ); + + const handleOnEventDetailPanelOpened = useCallback(() => { + const updatedExpandedDetail: ExpandedDetailType = { + panelView: 'eventDetail', + params: { + eventId: eventId ?? '', + indexName: indexName ?? '', + }, + }; + + dispatch( + dataTableActions.toggleDetailPanel({ + ...updatedExpandedDetail, + tabType, + id: tableId, + }) + ); + }, [dispatch, eventId, indexName, tabType, tableId]); + + const Action = controlColumn.rowCellRender; + + if (!timelineNonEcsData || !ecsData || !eventId) { + return ; + } + + return ( + <> + {Action && ( + + )} + + ); +}; + +export const RowAction = React.memo(RowActionComponent); diff --git a/x-pack/plugins/security_solution/public/common/components/control_columns/transform_control_columns.test.tsx b/x-pack/plugins/security_solution/public/common/components/control_columns/transform_control_columns.test.tsx new file mode 100644 index 0000000000000..eb0c2379ff609 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/control_columns/transform_control_columns.test.tsx @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { TransformColumnsProps } from './transform_control_columns'; +import { transformControlColumns } from './transform_control_columns'; + +describe('transformControlColumns', () => { + const defaultProps: TransformColumnsProps = { + onRowSelected: jest.fn(), + loadingEventIds: [], + showCheckboxes: true, + data: [], + timelineId: 'test-timelineId', + setEventsLoading: jest.fn(), + setEventsDeleted: jest.fn(), + columnHeaders: [], + controlColumns: [], + disabledCellActions: [], + selectedEventIds: {}, + tabType: '', + isSelectAllChecked: false, + browserFields: {}, + onSelectPage: jest.fn(), + pageSize: 0, + sort: [], + // eslint-disable-next-line @typescript-eslint/no-explicit-any + theme: {} as any, + }; + test('displays loader when id is included on loadingEventIds', () => { + const res = transformControlColumns(defaultProps); + expect(res.find).not.toBeNull(); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/components/control_columns/transform_control_columns.tsx b/x-pack/plugins/security_solution/public/common/components/control_columns/transform_control_columns.tsx new file mode 100644 index 0000000000000..94980890d1530 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/control_columns/transform_control_columns.tsx @@ -0,0 +1,152 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { FieldBrowserOptions } from '@kbn/triggers-actions-ui-plugin/public'; +import type { EuiDataGridCellValueElementProps, EuiDataGridControlColumn } from '@elastic/eui'; +import type { ComponentType } from 'react'; +import React from 'react'; +import type { EuiTheme } from '@kbn/kibana-react-plugin/common'; +import type { + BrowserFields, + TimelineItem, + TimelineNonEcsData, +} from '../../../../common/search_strategy'; +import type { + SetEventsDeleted, + SetEventsLoading, + ColumnHeaderOptions, + ControlColumnProps, + OnRowSelected, + OnSelectAll, + SortColumnTable, +} from '../../../../common/types'; +import { addBuildingBlockStyle } from '../data_table/helpers'; +import { getPageRowIndex } from '../data_table/pagination'; +import { RowAction } from './row_action'; + +const EmptyHeaderCellRender: ComponentType = () => null; + +export interface TransformColumnsProps { + columnHeaders: ColumnHeaderOptions[]; + controlColumns: ControlColumnProps[]; + data: TimelineItem[]; + disabledCellActions: string[]; + fieldBrowserOptions?: FieldBrowserOptions; + loadingEventIds: string[]; + onRowSelected: OnRowSelected; + onRuleChange?: () => void; + selectedEventIds: Record; + showCheckboxes: boolean; + tabType: string; + timelineId: string; + isSelectAllChecked: boolean; + browserFields: BrowserFields; + onSelectPage: OnSelectAll; + pageSize: number; + sort: SortColumnTable[]; + theme: EuiTheme; + setEventsLoading: SetEventsLoading; + setEventsDeleted: SetEventsDeleted; +} + +export const transformControlColumns = ({ + columnHeaders, + controlColumns, + data, + fieldBrowserOptions, + loadingEventIds, + onRowSelected, + onRuleChange, + selectedEventIds, + showCheckboxes, + tabType, + timelineId, + isSelectAllChecked, + onSelectPage, + browserFields, + pageSize, + sort, + theme, + setEventsLoading, + setEventsDeleted, +}: TransformColumnsProps): EuiDataGridControlColumn[] => + controlColumns.map( + ({ id: columnId, headerCellRender = EmptyHeaderCellRender, rowCellRender, width }, i) => ({ + id: `${columnId}`, + headerCellRender: () => { + const HeaderActions = headerCellRender; + return ( + <> + {HeaderActions && ( + + )} + + ); + }, + rowCellRender: ({ + isDetails, + isExpandable, + isExpanded, + rowIndex, + colIndex, + setCellProps, + }: EuiDataGridCellValueElementProps) => { + const pageRowIndex = getPageRowIndex(rowIndex, pageSize); + const rowData = data[pageRowIndex]; + + if (rowData) { + addBuildingBlockStyle(rowData.ecs, theme, setCellProps); + } else { + // disable the cell when it has no data + setCellProps({ style: { display: 'none' } }); + } + + return ( + + ); + }, + width, + }) + ); diff --git a/x-pack/plugins/security_solution/public/common/components/control_columns/translations.ts b/x-pack/plugins/security_solution/public/common/components/control_columns/translations.ts new file mode 100644 index 0000000000000..31872dd0087a0 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/control_columns/translations.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { i18n } from '@kbn/i18n'; + +export const CHECKBOX_FOR_ROW = ({ + ariaRowindex, + columnValues, + checked, +}: { + ariaRowindex: number; + columnValues: string; + checked: boolean; +}) => + i18n.translate('xpack.securitySolution.controlColumns.checkboxForRowAriaLabel', { + values: { ariaRowindex, checked, columnValues }, + defaultMessage: + '{checked, select, false {unchecked} true {checked}} checkbox for the alert or event in row {ariaRowindex}, with columns {columnValues}', + }); diff --git a/x-pack/plugins/security_solution/public/common/components/data_table/column_headers/default_headers.ts b/x-pack/plugins/security_solution/public/common/components/data_table/column_headers/default_headers.ts new file mode 100644 index 0000000000000..8d1736cc172c3 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/data_table/column_headers/default_headers.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 type { ColumnHeaderType } from '../../../../../common/types'; +import type { ColumnHeaderOptions } from '../../../../../common/types/timeline'; +import { DEFAULT_TABLE_COLUMN_MIN_WIDTH, DEFAULT_TABLE_DATE_COLUMN_MIN_WIDTH } from '../constants'; + +export const defaultColumnHeaderType: ColumnHeaderType = 'not-filtered'; + +export const defaultHeaders: ColumnHeaderOptions[] = [ + { + columnHeaderType: defaultColumnHeaderType, + id: '@timestamp', + initialWidth: DEFAULT_TABLE_DATE_COLUMN_MIN_WIDTH, + esTypes: ['date'], + type: 'date', + }, + { + columnHeaderType: defaultColumnHeaderType, + id: 'message', + initialWidth: DEFAULT_TABLE_COLUMN_MIN_WIDTH, + }, + { + columnHeaderType: defaultColumnHeaderType, + id: 'event.category', + initialWidth: DEFAULT_TABLE_COLUMN_MIN_WIDTH, + }, + { + columnHeaderType: defaultColumnHeaderType, + id: 'event.action', + initialWidth: DEFAULT_TABLE_COLUMN_MIN_WIDTH, + }, + { + columnHeaderType: defaultColumnHeaderType, + id: 'host.name', + initialWidth: DEFAULT_TABLE_COLUMN_MIN_WIDTH, + }, + { + columnHeaderType: defaultColumnHeaderType, + id: 'source.ip', + initialWidth: DEFAULT_TABLE_COLUMN_MIN_WIDTH, + }, + { + columnHeaderType: defaultColumnHeaderType, + id: 'destination.ip', + initialWidth: DEFAULT_TABLE_COLUMN_MIN_WIDTH, + }, + { + columnHeaderType: defaultColumnHeaderType, + id: 'user.name', + initialWidth: DEFAULT_TABLE_COLUMN_MIN_WIDTH, + }, +]; diff --git a/x-pack/plugins/security_solution/public/common/components/data_table/column_headers/helpers.test.tsx b/x-pack/plugins/security_solution/public/common/components/data_table/column_headers/helpers.test.tsx new file mode 100644 index 0000000000000..9320a0270f5aa --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/data_table/column_headers/helpers.test.tsx @@ -0,0 +1,530 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor 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 { mount } from 'enzyme'; +import { omit, set } from 'lodash/fp'; +import React from 'react'; + +import type { BUILT_IN_SCHEMA } from './helpers'; +import { + getColumnWidthFromType, + getColumnHeaders, + getSchema, + getColumnHeader, + allowSorting, +} from './helpers'; +import { DEFAULT_TABLE_COLUMN_MIN_WIDTH, DEFAULT_TABLE_DATE_COLUMN_MIN_WIDTH } from '../constants'; +import type { ColumnHeaderOptions } from '../../../../../common/types'; +import { mockBrowserFields } from '../../../containers/source/mock'; +import { defaultHeaders } from '../../../store/data_table/defaults'; + +window.matchMedia = jest.fn().mockImplementation((query) => { + return { + matches: false, + media: query, + onchange: null, + addListener: jest.fn(), + removeListener: jest.fn(), + }; +}); + +describe('helpers', () => { + describe('getColumnWidthFromType', () => { + test('it returns the expected width for a non-date column', () => { + expect(getColumnWidthFromType('keyword')).toEqual(DEFAULT_TABLE_COLUMN_MIN_WIDTH); + }); + + test('it returns the expected width for a date column', () => { + expect(getColumnWidthFromType('date')).toEqual(DEFAULT_TABLE_DATE_COLUMN_MIN_WIDTH); + }); + }); + + describe('getSchema', () => { + const expected: Record = { + date: 'datetime', + date_nanos: 'datetime', + double: 'numeric', + long: 'numeric', + number: 'numeric', + object: 'json', + boolean: 'boolean', + }; + + Object.keys(expected).forEach((type) => + test(`it returns the expected schema for type '${type}'`, () => { + expect(getSchema(type)).toEqual(expected[type]); + }) + ); + + test('it returns `undefined` when `type` does NOT match a built-in schema type', () => { + expect(getSchema('string')).toBeUndefined(); // 'keyword` doesn't have a schema + }); + + test('it returns `undefined` when `type` is undefined', () => { + expect(getSchema(undefined)).toBeUndefined(); + }); + }); + + describe('getColumnHeader', () => { + test('it should return column header non existing in defaultHeaders', () => { + const field = 'test_field_1'; + + expect(getColumnHeader(field, [])).toEqual({ + columnHeaderType: 'not-filtered', + id: field, + initialWidth: DEFAULT_TABLE_COLUMN_MIN_WIDTH, + }); + }); + + test('it should return column header existing in defaultHeaders', () => { + const field = 'test_field_1'; + + expect( + getColumnHeader(field, [ + { + columnHeaderType: 'not-filtered', + id: field, + initialWidth: DEFAULT_TABLE_DATE_COLUMN_MIN_WIDTH, + esTypes: ['date'], + type: 'date', + }, + ]) + ).toEqual({ + columnHeaderType: 'not-filtered', + id: field, + initialWidth: DEFAULT_TABLE_DATE_COLUMN_MIN_WIDTH, + esTypes: ['date'], + type: 'date', + }); + }); + }); + + describe('getColumnHeaders', () => { + // additional properties used by `EuiDataGrid`: + const actions = { + showHide: false, + showSortAsc: true, + showSortDesc: true, + }; + const defaultSortDirection = 'desc'; + const isSortable = true; + + const mockHeader = defaultHeaders.filter((h) => + ['@timestamp', 'source.ip', 'destination.ip'].includes(h.id) + ); + + describe('display', () => { + const renderedByDisplay = 'I am rendered via a React component: header.display'; + const renderedByDisplayAsText = 'I am rendered by header.displayAsText'; + + test('it renders via `display` when the header has JUST a `display` property (`displayAsText` is undefined)', () => { + const headerWithJustDisplay = mockHeader.map((x) => + x.id === '@timestamp' + ? { + ...x, + display: {renderedByDisplay}, + } + : x + ); + + const wrapper = mount( + <>{getColumnHeaders(headerWithJustDisplay, mockBrowserFields)[0].display} + ); + + expect(wrapper.text()).toEqual(renderedByDisplay); + }); + + test('it (also) renders via `display` when the header has BOTH a `display` property AND a `displayAsText`', () => { + const headerWithBoth = mockHeader.map((x) => + x.id === '@timestamp' + ? { + ...x, + display: {renderedByDisplay}, // this has a higher priority... + displayAsText: renderedByDisplayAsText, // ...so this text won't be rendered + } + : x + ); + + const wrapper = mount( + <>{getColumnHeaders(headerWithBoth, mockBrowserFields)[0].display} + ); + + expect(wrapper.text()).toEqual(renderedByDisplay); + }); + + test('it renders via `displayAsText` when the header does NOT have a `display`, BUT it has `displayAsText`', () => { + const headerWithJustDisplayAsText = mockHeader.map((x) => + x.id === '@timestamp' + ? { + ...x, + displayAsText: renderedByDisplayAsText, // fallback to rendering via displayAsText + } + : x + ); + + const wrapper = mount( + <>{getColumnHeaders(headerWithJustDisplayAsText, mockBrowserFields)[0].display} + ); + + expect(wrapper.text()).toEqual(renderedByDisplayAsText); + }); + + test('it renders `header.id` when the header does NOT have a `display`, AND it does NOT have a `displayAsText`', () => { + const wrapper = mount(<>{getColumnHeaders(mockHeader, mockBrowserFields)[0].display}); + + expect(wrapper.text()).toEqual('@timestamp'); // fallback to rendering by header.id + }); + }); + + test('it renders the default actions when the header does NOT have custom actions', () => { + expect(getColumnHeaders(mockHeader, mockBrowserFields)[0].actions).toEqual(actions); + }); + + test('it renders custom actions when `actions` is defined in the header', () => { + const customActions = { + showSortAsc: { + label: 'A custom sort ascending', + }, + showSortDesc: { + label: 'A custom sort descending', + }, + }; + + const headerWithCustomActions = mockHeader.map((x) => + x.id === '@timestamp' + ? { + ...x, + actions: customActions, + } + : x + ); + + expect(getColumnHeaders(headerWithCustomActions, mockBrowserFields)[0].actions).toEqual( + customActions + ); + }); + + describe('isSortable', () => { + test("it is sortable, because `@timestamp`'s `aggregatable` BrowserFields property is `true`", () => { + expect(getColumnHeaders(mockHeader, mockBrowserFields)[0].isSortable).toEqual(true); + }); + + test("it is NOT sortable, when `@timestamp`'s `aggregatable` BrowserFields property is `false`", () => { + const withAggregatableOverride = set( + 'base.fields.@timestamp.aggregatable', + false, // override `aggregatable` for `@timestamp`, a date field that is normally aggregatable + mockBrowserFields + ); + + expect(getColumnHeaders(mockHeader, withAggregatableOverride)[0].isSortable).toEqual(false); + }); + + test('it is NOT sortable when BrowserFields does not have metadata for the field', () => { + const noBrowserFieldEntry = omit('base', mockBrowserFields); // omit the 'base` category, which contains `@timestamp` + + expect(getColumnHeaders(mockHeader, noBrowserFieldEntry)[0].isSortable).toEqual(false); + }); + }); + + test('should return a full object of ColumnHeader from the default header', () => { + const expectedData = [ + { + actions, + aggregatable: true, + category: 'base', + columnHeaderType: 'not-filtered', + defaultSortDirection, + description: + 'Date/time when the event originated. For log events this is the date/time when the event was generated, and not when it was read. Required field for all events.', + esTypes: ['date'], + example: '2016-05-23T08:05:34.853Z', + format: '', + id: '@timestamp', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + isSortable, + name: '@timestamp', + readFromDocValues: true, + schema: 'datetime', + searchable: true, + type: 'date', + initialWidth: 190, + }, + { + actions, + aggregatable: true, + category: 'source', + columnHeaderType: 'not-filtered', + defaultSortDirection, + description: 'IP address of the source. Can be one or multiple IPv4 or IPv6 addresses.', + esTypes: ['ip'], + example: '', + format: '', + id: 'source.ip', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + isSortable, + name: 'source.ip', + schema: undefined, + searchable: true, + type: 'ip', + initialWidth: 180, + }, + { + actions, + aggregatable: true, + category: 'destination', + columnHeaderType: 'not-filtered', + defaultSortDirection, + description: + 'IP address of the destination. Can be one or multiple IPv4 or IPv6 addresses.', + esTypes: ['ip'], + example: '', + format: '', + id: 'destination.ip', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + isSortable, + name: 'destination.ip', + schema: undefined, + searchable: true, + type: 'ip', + initialWidth: 180, + }, + ]; + + // NOTE: the omitted `display` (`React.ReactNode`) property is tested separately above + expect(getColumnHeaders(mockHeader, mockBrowserFields).map(omit('display'))).toEqual( + expectedData + ); + }); + + test('it should NOT override a custom `schema` when the `header` provides it', () => { + const expected = [ + { + actions, + aggregatable: true, + category: 'base', + columnHeaderType: 'not-filtered', + defaultSortDirection, + description: + 'Date/time when the event originated. For log events this is the date/time when the event was generated, and not when it was read. Required field for all events.', + esTypes: ['date'], + example: '2016-05-23T08:05:34.853Z', + format: '', + id: '@timestamp', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + isSortable, + name: '@timestamp', + readFromDocValues: true, + schema: 'custom', // <-- we expect our custom schema will NOT be overridden by a built-in schema + searchable: true, + type: 'date', // <-- the built-in schema for `type: 'date'` is 'datetime', but the custom schema overrides it + initialWidth: 190, + }, + ]; + + const headerWithCustomSchema: ColumnHeaderOptions = { + columnHeaderType: 'not-filtered', + id: '@timestamp', + initialWidth: 190, + schema: 'custom', // <-- overrides the default of 'datetime' + }; + + expect( + getColumnHeaders([headerWithCustomSchema], mockBrowserFields).map(omit('display')) + ).toEqual(expected); + }); + + test('it should return an `undefined` `schema` when a `header` does NOT have an entry in `BrowserFields`', () => { + const expected = [ + { + actions, + columnHeaderType: 'not-filtered', + defaultSortDirection, + id: 'no_matching_browser_field', + isSortable: false, + schema: undefined, // <-- no `BrowserFields` entry for this field + }, + ]; + + const headerDoesNotMatchBrowserField: ColumnHeaderOptions = { + columnHeaderType: 'not-filtered', + id: 'no_matching_browser_field', + }; + + expect( + getColumnHeaders([headerDoesNotMatchBrowserField], mockBrowserFields).map(omit('display')) + ).toEqual(expected); + }); + + describe('augment the `header` with metadata from `browserFields`', () => { + test('it should augment the `header` when field category is base', () => { + const fieldName = 'test_field'; + const testField = { + aggregatable: true, + category: 'base', + description: + 'Date/time when the event originated. For log events this is the date/time when the event was generated, and not when it was read. Required field for all events.', + example: '2016-05-23T08:05:34.853Z', + format: 'date', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: fieldName, + searchable: true, + type: 'date', + }; + + const browserField = { base: { fields: { [fieldName]: testField } } }; + + const header: ColumnHeaderOptions = { + columnHeaderType: 'not-filtered', + id: fieldName, + }; + + expect( + getColumnHeaders([header], browserField).map( + omit(['display', 'actions', 'isSortable', 'defaultSortDirection', 'schema']) + ) + ).toEqual([ + { + ...header, + ...browserField.base.fields[fieldName], + }, + ]); + }); + + test("it should augment the `header` when field is top level and name isn't splittable", () => { + const fieldName = 'testFieldName'; + const testField = { + aggregatable: true, + category: fieldName, + description: 'test field description', + example: '2016-05-23T08:05:34.853Z', + format: 'date', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: fieldName, + searchable: true, + type: 'date', + }; + + const browserField = { [fieldName]: { fields: { [fieldName]: testField } } }; + + const header: ColumnHeaderOptions = { + columnHeaderType: 'not-filtered', + id: fieldName, + }; + + expect( + getColumnHeaders([header], browserField).map( + omit(['display', 'actions', 'isSortable', 'defaultSortDirection', 'schema']) + ) + ).toEqual([ + { + ...header, + ...browserField[fieldName].fields[fieldName], + }, + ]); + }); + + test('it should augment the `header` when field is splittable', () => { + const fieldName = 'test.field.splittable'; + const testField = { + aggregatable: true, + category: 'test', + description: 'test field description', + example: '2016-05-23T08:05:34.853Z', + format: 'date', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: fieldName, + searchable: true, + type: 'date', + }; + + const browserField = { test: { fields: { [fieldName]: testField } } }; + + const header: ColumnHeaderOptions = { + columnHeaderType: 'not-filtered', + id: fieldName, + }; + + expect( + getColumnHeaders([header], browserField).map( + omit(['display', 'actions', 'isSortable', 'defaultSortDirection', 'schema']) + ) + ).toEqual([ + { + ...header, + ...browserField.test.fields[fieldName], + }, + ]); + }); + }); + }); + + describe('allowSorting', () => { + const aggregatableField = { + category: 'cloud', + description: + 'The cloud account or organization id used to identify different entities in a multi-tenant environment. Examples: AWS account id, Google Cloud ORG Id, or other unique identifier.', + example: '666777888999', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'cloud.account.id', + searchable: true, + type: 'string', + aggregatable: true, // <-- allow sorting when this is true + format: '', + }; + + test('it returns true for an aggregatable field', () => { + expect( + allowSorting({ + browserField: aggregatableField, + fieldName: aggregatableField.name, + }) + ).toBe(true); + }); + + test('it returns true for a allow-listed non-BrowserField', () => { + expect( + allowSorting({ + browserField: undefined, // no BrowserField metadata for this field + fieldName: 'kibana.alert.rule.name', // an allow-listed field name + }) + ).toBe(true); + }); + + test('it returns false for a NON-aggregatable field (aggregatable is false)', () => { + const nonaggregatableField = { + ...aggregatableField, + aggregatable: false, // <-- NON-aggregatable + }; + + expect( + allowSorting({ + browserField: nonaggregatableField, + fieldName: nonaggregatableField.name, + }) + ).toBe(false); + }); + + test('it returns false if the BrowserField is missing the aggregatable property', () => { + const missingAggregatable = omit('aggregatable', aggregatableField); + + expect( + allowSorting({ + browserField: missingAggregatable, + fieldName: missingAggregatable.name, + }) + ).toBe(false); + }); + + test("it returns false for a non-allowlisted field we don't have `BrowserField` metadata for it", () => { + expect( + allowSorting({ + browserField: undefined, // <-- no metadata for this field + fieldName: 'non-allowlisted', + }) + ).toBe(false); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/components/data_table/column_headers/helpers.tsx b/x-pack/plugins/security_solution/public/common/components/data_table/column_headers/helpers.tsx new file mode 100644 index 0000000000000..2ddac64639a7e --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/data_table/column_headers/helpers.tsx @@ -0,0 +1,202 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { EuiDataGridColumnActions } from '@elastic/eui'; +import { keyBy } from 'lodash/fp'; +import React from 'react'; + +import type { + BrowserField, + BrowserFields, +} from '../../../../../common/search_strategy/index_fields'; +import type { ColumnHeaderOptions } from '../../../../../common/types/timeline'; +import { DEFAULT_TABLE_COLUMN_MIN_WIDTH, DEFAULT_TABLE_DATE_COLUMN_MIN_WIDTH } from '../constants'; +import { defaultColumnHeaderType } from '../../../store/data_table/defaults'; + +const defaultActions: EuiDataGridColumnActions = { + showSortAsc: true, + showSortDesc: true, + showHide: false, +}; + +export const allowSorting = ({ + browserField, + fieldName, +}: { + browserField: Partial | undefined; + fieldName: string; +}): boolean => { + const isAggregatable = browserField?.aggregatable ?? false; + + const isAllowlistedNonBrowserField = [ + 'kibana.alert.ancestors.depth', + 'kibana.alert.ancestors.id', + 'kibana.alert.ancestors.rule', + 'kibana.alert.ancestors.type', + 'kibana.alert.original_event.action', + 'kibana.alert.original_event.category', + 'kibana.alert.original_event.code', + 'kibana.alert.original_event.created', + 'kibana.alert.original_event.dataset', + 'kibana.alert.original_event.duration', + 'kibana.alert.original_event.end', + 'kibana.alert.original_event.hash', + 'kibana.alert.original_event.id', + 'kibana.alert.original_event.kind', + 'kibana.alert.original_event.module', + 'kibana.alert.original_event.original', + 'kibana.alert.original_event.outcome', + 'kibana.alert.original_event.provider', + 'kibana.alert.original_event.risk_score', + 'kibana.alert.original_event.risk_score_norm', + 'kibana.alert.original_event.sequence', + 'kibana.alert.original_event.severity', + 'kibana.alert.original_event.start', + 'kibana.alert.original_event.timezone', + 'kibana.alert.original_event.type', + 'kibana.alert.original_time', + 'kibana.alert.reason', + 'kibana.alert.rule.created_by', + 'kibana.alert.rule.description', + 'kibana.alert.rule.enabled', + 'kibana.alert.rule.false_positives', + 'kibana.alert.rule.from', + 'kibana.alert.rule.uuid', + 'kibana.alert.rule.immutable', + 'kibana.alert.rule.interval', + 'kibana.alert.rule.max_signals', + 'kibana.alert.rule.name', + 'kibana.alert.rule.note', + 'kibana.alert.rule.references', + 'kibana.alert.risk_score', + 'kibana.alert.rule.rule_id', + 'kibana.alert.severity', + 'kibana.alert.rule.size', + 'kibana.alert.rule.tags', + 'kibana.alert.rule.threat', + 'kibana.alert.rule.threat.tactic.id', + 'kibana.alert.rule.threat.tactic.name', + 'kibana.alert.rule.threat.tactic.reference', + 'kibana.alert.rule.threat.technique.id', + 'kibana.alert.rule.threat.technique.name', + 'kibana.alert.rule.threat.technique.reference', + 'kibana.alert.rule.timeline_id', + 'kibana.alert.rule.timeline_title', + 'kibana.alert.rule.to', + 'kibana.alert.rule.type', + 'kibana.alert.rule.updated_by', + 'kibana.alert.rule.version', + 'kibana.alert.workflow_status', + ].includes(fieldName); + + return isAllowlistedNonBrowserField || isAggregatable; +}; + +const getAllBrowserFields = (browserFields: BrowserFields): Array> => + Object.values(browserFields).reduce>>( + (acc, namespace) => [ + ...acc, + ...Object.values(namespace.fields != null ? namespace.fields : {}), + ], + [] + ); + +const getAllFieldsByName = ( + browserFields: BrowserFields +): { [fieldName: string]: Partial } => + keyBy('name', getAllBrowserFields(browserFields)); + +/** + * Valid built-in schema types for the `schema` property of `EuiDataGridColumn` + * are enumerated in the following comment in the EUI repository (permalink): + * https://github.com/elastic/eui/blob/edc71160223c8d74e1293501f7199fba8fa57c6c/src/components/datagrid/data_grid_types.ts#L417 + */ +export type BUILT_IN_SCHEMA = 'boolean' | 'currency' | 'datetime' | 'numeric' | 'json'; + +/** + * Returns a valid value for the `EuiDataGridColumn` `schema` property, or + * `undefined` when the specified `BrowserFields` `type` doesn't match a + * built-in schema type + * + * Notes: + * + * - At the time of this writing, the type definition of the + * `EuiDataGridColumn` `schema` property is: + * + * ```ts + * schema?: string; + * ``` + * - At the time of this writing, Elasticsearch Field data types are documented here: + * https://www.elastic.co/guide/en/elasticsearch/reference/7.14/mapping-types.html + */ +export const getSchema = (type: string | undefined): BUILT_IN_SCHEMA | undefined => { + switch (type) { + case 'date': // fall through + case 'date_nanos': + return 'datetime'; + case 'double': // fall through + case 'long': // fall through + case 'number': + return 'numeric'; + case 'object': + return 'json'; + case 'boolean': + return 'boolean'; + default: + return undefined; + } +}; + +/** Enriches the column headers with field details from the specified browserFields */ +export const getColumnHeaders = ( + headers: ColumnHeaderOptions[], + browserFields: BrowserFields +): ColumnHeaderOptions[] => { + const browserFieldByName = getAllFieldsByName(browserFields); + return headers + ? headers.map((header) => { + const browserField: Partial | undefined = browserFieldByName[header.id]; + + // augment the header with metadata from browserFields: + const augmentedHeader = { + ...header, + ...browserField, + schema: header.schema ?? getSchema(browserField?.type), + }; + + const content = <>{header.display ?? header.displayAsText ?? header.id}; + + // return the augmentedHeader with additional properties used by `EuiDataGrid` + return { + ...augmentedHeader, + actions: header.actions ?? defaultActions, + defaultSortDirection: 'desc', // the default action when a user selects a field via `EuiDataGrid`'s `Pick fields to sort by` UI + display: <>{content}, + isSortable: allowSorting({ + browserField, + fieldName: header.id, + }), + }; + }) + : []; +}; + +/** + * Returns the column header with field details from the defaultHeaders + */ +export const getColumnHeader = ( + fieldName: string, + defaultHeaders: ColumnHeaderOptions[] +): ColumnHeaderOptions => ({ + columnHeaderType: defaultColumnHeaderType, + id: fieldName, + initialWidth: DEFAULT_TABLE_COLUMN_MIN_WIDTH, + ...(defaultHeaders.find((c) => c.id === fieldName) ?? {}), +}); + +export const getColumnWidthFromType = (type: string): number => + type !== 'date' ? DEFAULT_TABLE_COLUMN_MIN_WIDTH : DEFAULT_TABLE_DATE_COLUMN_MIN_WIDTH; diff --git a/x-pack/plugins/security_solution/public/common/components/data_table/column_headers/translations.ts b/x-pack/plugins/security_solution/public/common/components/data_table/column_headers/translations.ts new file mode 100644 index 0000000000000..c7e32ef1d15da --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/data_table/column_headers/translations.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; + +export const REMOVE_COLUMN = i18n.translate( + 'xpack.securitySolution.columnHeaders.flyout.pane.removeColumnButtonLabel', + { + defaultMessage: 'Remove column', + } +); diff --git a/x-pack/plugins/security_solution/public/common/components/data_table/constants.ts b/x-pack/plugins/security_solution/public/common/components/data_table/constants.ts new file mode 100644 index 0000000000000..3658382879300 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/data_table/constants.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +/** The default minimum width of a column (when a width for the column type is not specified) */ +export const DEFAULT_TABLE_COLUMN_MIN_WIDTH = 180; // px + +/** The default minimum width of a column of type `date` */ +export const DEFAULT_TABLE_DATE_COLUMN_MIN_WIDTH = 190; // px diff --git a/x-pack/plugins/security_solution/public/common/components/data_table/helpers.test.tsx b/x-pack/plugins/security_solution/public/common/components/data_table/helpers.test.tsx new file mode 100644 index 0000000000000..db337d3bdaf8f --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/data_table/helpers.test.tsx @@ -0,0 +1,223 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { ColumnHeaderOptions } from '../../../../common/types'; +import { + hasCellActions, + mapSortDirectionToDirection, + mapSortingColumns, + addBuildingBlockStyle, +} from './helpers'; + +import { euiThemeVars } from '@kbn/ui-theme'; +import { mockDnsEvent } from '../../mock'; + +describe('helpers', () => { + describe('mapSortDirectionToDirection', () => { + test('it returns the expected direction when sortDirection is `asc`', () => { + expect(mapSortDirectionToDirection('asc')).toBe('asc'); + }); + + test('it returns the expected direction when sortDirection is `desc`', () => { + expect(mapSortDirectionToDirection('desc')).toBe('desc'); + }); + + test('it returns the expected direction when sortDirection is `none`', () => { + expect(mapSortDirectionToDirection('none')).toBe('desc'); // defaults to a valid direction accepted by `EuiDataGrid` + }); + }); + + describe('mapSortingColumns', () => { + const columns: Array<{ + id: string; + direction: 'asc' | 'desc'; + }> = [ + { + id: 'kibana.rac.alert.status', + direction: 'asc', + }, + { + id: 'kibana.rac.alert.start', + direction: 'desc', + }, + ]; + + const columnHeaders: ColumnHeaderOptions[] = [ + { + columnHeaderType: 'not-filtered', + displayAsText: 'Status', + id: 'kibana.rac.alert.status', + initialWidth: 79, + category: 'kibana', + type: 'string', + aggregatable: true, + actions: { + showSortAsc: { + label: 'Sort A-Z', + }, + showSortDesc: { + label: 'Sort Z-A', + }, + }, + defaultSortDirection: 'desc', + display: { + key: null, + ref: null, + props: { + children: { + key: null, + ref: null, + props: { + children: 'Status', + }, + _owner: null, + }, + }, + _owner: null, + }, + isSortable: true, + }, + { + columnHeaderType: 'not-filtered', + displayAsText: 'Triggered', + id: 'kibana.rac.alert.start', + initialWidth: 176, + category: 'kibana', + type: 'date', + esTypes: ['date'], + aggregatable: true, + actions: { + showSortAsc: { + label: 'Sort A-Z', + }, + showSortDesc: { + label: 'Sort Z-A', + }, + }, + defaultSortDirection: 'desc', + display: { + key: null, + ref: null, + props: { + children: { + key: null, + ref: null, + props: { + children: 'Triggered', + }, + _owner: null, + }, + }, + _owner: null, + }, + isSortable: true, + }, + ]; + + test('it returns the expected results when each column has a corresponding entry in `columnHeaders`', () => { + expect(mapSortingColumns({ columns, columnHeaders })).toEqual([ + { + columnId: 'kibana.rac.alert.status', + columnType: 'string', + esTypes: [], + sortDirection: 'asc', + }, + { + columnId: 'kibana.rac.alert.start', + columnType: 'date', + esTypes: ['date'], + sortDirection: 'desc', + }, + ]); + }); + + test('it defaults to a `columnType` of empty string when a column does NOT have a corresponding entry in `columnHeaders`', () => { + const withUnknownColumn: Array<{ + id: string; + direction: 'asc' | 'desc'; + }> = [ + { + id: 'kibana.rac.alert.status', + direction: 'asc', + }, + { + id: 'kibana.rac.alert.start', + direction: 'desc', + }, + { + id: 'unknown', // <-- no entry for this in `columnHeaders` + direction: 'asc', + }, + ]; + + expect(mapSortingColumns({ columns: withUnknownColumn, columnHeaders })).toEqual([ + { + columnId: 'kibana.rac.alert.status', + columnType: 'string', + esTypes: [], + sortDirection: 'asc', + }, + { + columnId: 'kibana.rac.alert.start', + columnType: 'date', + esTypes: ['date'], + sortDirection: 'desc', + }, + { + columnId: 'unknown', + columnType: '', // <-- mapped to the default + esTypes: [], // <-- mapped to the default + sortDirection: 'asc', + }, + ]); + }); + }); + + describe('addBuildingBlockStyle', () => { + const THEME = { eui: euiThemeVars, darkMode: false }; + + test('it calls `setCellProps` with background color when event is a building block', () => { + const mockedSetCellProps = jest.fn(); + const ecs = { + ...mockDnsEvent, + ...{ kibana: { alert: { building_block_type: ['default'] } } }, + }; + + addBuildingBlockStyle(ecs, THEME, mockedSetCellProps); + + expect(mockedSetCellProps).toBeCalledWith({ + style: { + backgroundColor: euiThemeVars.euiColorHighlight, + }, + }); + }); + + test('it call `setCellProps` reseting the background color when event is not a building block', () => { + const mockedSetCellProps = jest.fn(); + + addBuildingBlockStyle(mockDnsEvent, THEME, mockedSetCellProps); + + expect(mockedSetCellProps).toBeCalledWith({ style: { backgroundColor: 'inherit' } }); + }); + }); + + describe('hasCellActions', () => { + const columnId = '@timestamp'; + + test('it returns false when the columnId is included in `disabledCellActions` ', () => { + const disabledCellActions = ['foo', '@timestamp', 'bar', 'baz']; // includes @timestamp + + expect(hasCellActions({ columnId, disabledCellActions })).toBe(false); + }); + + test('it returns true when the columnId is NOT included in `disabledCellActions` ', () => { + const disabledCellActions = ['foo', 'bar', 'baz']; + + expect(hasCellActions({ columnId, disabledCellActions })).toBe(true); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/components/data_table/helpers.tsx b/x-pack/plugins/security_solution/public/common/components/data_table/helpers.tsx new file mode 100644 index 0000000000000..d9d3345b94393 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/data_table/helpers.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 { isEmpty } from 'lodash/fp'; + +import type { EuiDataGridCellValueElementProps } from '@elastic/eui'; +import type { EuiTheme } from '@kbn/kibana-react-plugin/common'; +import type { SortColumnTable } from '../../../../common/types'; +import type { Ecs } from '../../../../common/ecs'; +import type { TimelineItem, TimelineNonEcsData } from '../../../../common/search_strategy'; +import type { ColumnHeaderOptions, SortDirection } from '../../../../common/types/timeline'; + +/** + * Creates mapping of eventID -> fieldData for given fieldsToKeep. Used to store additional field + * data necessary for custom timeline actions in conjunction with selection state + * @param data + * @param eventIds + * @param fieldsToKeep + */ +export const getEventIdToDataMapping = ( + timelineData: TimelineItem[], + eventIds: string[], + fieldsToKeep: string[], + hasAlertsCrud: boolean +): Record => + timelineData.reduce((acc, v) => { + const fvm = + hasAlertsCrud && eventIds.includes(v._id) + ? { [v._id]: v.data.filter((ti) => fieldsToKeep.includes(ti.field)) } + : {}; + return { + ...acc, + ...fvm, + }; + }, {}); + +export const isEventBuildingBlockType = (event: Ecs): boolean => + !isEmpty(event.kibana?.alert?.building_block_type); + +/** Maps (Redux) `SortDirection` to the `direction` values used by `EuiDataGrid` */ +export const mapSortDirectionToDirection = (sortDirection: SortDirection): 'asc' | 'desc' => { + switch (sortDirection) { + case 'asc': // fall through + case 'desc': + return sortDirection; + default: + return 'desc'; + } +}; + +/** + * Maps `EuiDataGrid` columns to their Redux representation by combining the + * `columns` with metadata from `columnHeaders` + */ +export const mapSortingColumns = ({ + columns, + columnHeaders, +}: { + columnHeaders: ColumnHeaderOptions[]; + columns: Array<{ + id: string; + direction: 'asc' | 'desc'; + }>; +}): SortColumnTable[] => + columns.map(({ id, direction }) => { + const columnHeader = columnHeaders.find((ch) => ch.id === id); + const columnType = columnHeader?.type ?? ''; + const esTypes = columnHeader?.esTypes ?? []; + + return { + columnId: id, + columnType, + esTypes, + sortDirection: direction, + }; + }); + +export const addBuildingBlockStyle = ( + ecs: Ecs, + theme: EuiTheme, + setCellProps: EuiDataGridCellValueElementProps['setCellProps'], + defaultStyles?: React.CSSProperties +) => { + const currentStyles = defaultStyles ?? {}; + if (isEventBuildingBlockType(ecs)) { + setCellProps({ + style: { + ...currentStyles, + backgroundColor: `${theme.eui.euiColorHighlight}`, + }, + }); + } else { + // reset cell style + setCellProps({ + style: { + ...currentStyles, + backgroundColor: 'inherit', + }, + }); + } +}; + +/** Returns true when the specified column has cell actions */ +export const hasCellActions = ({ + columnId, + disabledCellActions, +}: { + columnId: string; + disabledCellActions: string[]; +}) => !disabledCellActions.includes(columnId); diff --git a/x-pack/plugins/security_solution/public/common/components/data_table/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/data_table/index.test.tsx new file mode 100644 index 0000000000000..b4110c1e78340 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/data_table/index.test.tsx @@ -0,0 +1,339 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { fireEvent, render, screen } from '@testing-library/react'; + +import type { DataTableProps } from '.'; +import { DataTableComponent } from '.'; +import { REMOVE_COLUMN } from './column_headers/translations'; +import { useMountAppended } from '../../utils/use_mount_appended'; +import type { EuiDataGridColumn } from '@elastic/eui'; +import { defaultHeaders, mockGlobalState, mockTimelineData, TestProviders } from '../../mock'; +import { defaultColumnHeaderType } from '../../store/data_table/defaults'; +import { mockBrowserFields } from '../../containers/source/mock'; +import { getMappedNonEcsValue } from '../../../timelines/components/timeline/body/data_driven_columns'; +import type { CellValueElementProps } from '../../../../common/types'; +import { TableId } from '../../../../common/types'; + +const mockDispatch = jest.fn(); +jest.mock('react-redux', () => { + const original = jest.requireActual('react-redux'); + + return { + ...original, + useDispatch: () => mockDispatch, + }; +}); + +jest.mock('@kbn/kibana-react-plugin/public', () => { + const originalModule = jest.requireActual('@kbn/kibana-react-plugin/public'); + return { + ...originalModule, + useKibana: () => ({ + services: { + triggersActionsUi: { + getFieldBrowser: jest.fn(), + }, + }, + }), + }; +}); + +jest.mock('../../hooks/use_selector', () => ({ + useShallowEqualSelector: () => mockGlobalState.dataTable.tableById['table-test'], + useDeepEqualSelector: () => mockGlobalState.dataTable.tableById['table-test'], +})); + +jest.mock( + 'react-visibility-sensor', + () => + ({ children }: { children: (args: { isVisible: boolean }) => React.ReactNode }) => + children({ isVisible: true }) +); + +window.matchMedia = jest.fn().mockImplementation((query) => { + return { + matches: false, + media: query, + onchange: null, + addListener: jest.fn(), + removeListener: jest.fn(), + }; +}); + +export const TestCellRenderer: React.FC = ({ columnId, data }) => ( + <> + {getMappedNonEcsValue({ + data, + fieldName: columnId, + })?.reduce((x) => x[0]) ?? ''} + +); + +describe('DataTable', () => { + const mount = useMountAppended(); + const props: DataTableProps = { + browserFields: mockBrowserFields, + data: mockTimelineData, + defaultCellActions: [], + disabledCellActions: ['signal.rule.risk_score', 'signal.reason'], + id: TableId.test, + loadPage: jest.fn(), + renderCellValue: TestCellRenderer, + rowRenderers: [], + totalItems: 1, + leadingControlColumns: [], + unitCountText: '10 events', + pagination: { + pageSize: 25, + pageIndex: 0, + onChangeItemsPerPage: jest.fn(), + onChangePage: jest.fn(), + }, + }; + + beforeEach(() => { + mockDispatch.mockReset(); + }); + + describe('rendering', () => { + test('it renders the body data grid', () => { + const wrapper = mount( + + + + ); + expect(wrapper.find('[data-test-subj="body-data-grid"]').first().exists()).toEqual(true); + }); + + test('it renders the column headers', () => { + const wrapper = mount( + + + + ); + + expect(wrapper.find('[data-test-subj="dataGridHeader"]').first().exists()).toEqual(true); + }); + + test('it renders the scroll container', () => { + const wrapper = mount( + + + + ); + + expect(wrapper.find('div.euiDataGrid__virtualized').first().exists()).toEqual(true); + }); + + test('it renders events', () => { + const wrapper = mount( + + + + ); + + expect(wrapper.find('div.euiDataGridRowCell').first().exists()).toEqual(true); + }); + + test('it renders cell value', () => { + const headersJustTimestamp = defaultHeaders.filter((h) => h.id === '@timestamp'); + const testProps = { + ...props, + columnHeaders: headersJustTimestamp, + data: mockTimelineData.slice(0, 1), + }; + const wrapper = mount( + + + + ); + wrapper.update(); + expect( + wrapper + .find('[data-test-subj="dataGridRowCell"]') + .at(0) + .find('.euiDataGridRowCell__truncate') + .childAt(0) + .text() + ).toEqual(mockTimelineData[0].ecs.timestamp); + }); + + test('timestamp column renders cell actions', () => { + const headersJustTimestamp = defaultHeaders.filter((h) => h.id === '@timestamp'); + const testProps = { + ...props, + columnHeaders: headersJustTimestamp, + data: mockTimelineData.slice(0, 1), + }; + const wrapper = mount( + + + + ); + wrapper.update(); + + expect( + wrapper + .find('[data-test-subj="body-data-grid"]') + .first() + .prop('columns') + .find((c) => c.id === '@timestamp')?.cellActions + ).toBeDefined(); + }); + + test("signal.rule.risk_score column doesn't render cell actions", () => { + const columnHeaders = [ + { + category: 'signal', + columnHeaderType: defaultColumnHeaderType, + id: 'signal.rule.risk_score', + type: 'number', + aggregatable: true, + initialWidth: 105, + }, + ]; + const testProps = { + ...props, + columnHeaders, + data: mockTimelineData.slice(0, 1), + }; + const wrapper = mount( + + + + ); + wrapper.update(); + + expect( + wrapper + .find('[data-test-subj="body-data-grid"]') + .first() + .prop('columns') + .find((c) => c.id === 'signal.rule.risk_score')?.cellActions + ).toBeUndefined(); + }); + + test("signal.reason column doesn't render cell actions", () => { + const columnHeaders = [ + { + category: 'signal', + columnHeaderType: defaultColumnHeaderType, + id: 'signal.reason', + type: 'string', + aggregatable: true, + initialWidth: 450, + }, + ]; + const testProps = { + ...props, + columnHeaders, + data: mockTimelineData.slice(0, 1), + }; + const wrapper = mount( + + + + ); + wrapper.update(); + + expect( + wrapper + .find('[data-test-subj="body-data-grid"]') + .first() + .prop('columns') + .find((c) => c.id === 'signal.reason')?.cellActions + ).toBeUndefined(); + }); + }); + + test("signal.rule.risk_score column doesn't render cell actions", () => { + const columnHeaders = [ + { + category: 'signal', + columnHeaderType: defaultColumnHeaderType, + id: 'signal.rule.risk_score', + type: 'number', + aggregatable: true, + initialWidth: 105, + }, + ]; + const testProps = { + ...props, + columnHeaders, + data: mockTimelineData.slice(0, 1), + }; + const wrapper = mount( + + + + ); + wrapper.update(); + + expect( + wrapper + .find('[data-test-subj="body-data-grid"]') + .first() + .prop('columns') + .find((c) => c.id === 'signal.rule.risk_score')?.cellActions + ).toBeUndefined(); + }); + + test('it does NOT render switches for hiding columns in the `EuiDataGrid` `Columns` popover', async () => { + render( + + + + ); + + // Click the `EuidDataGrid` `Columns` button to open the popover: + fireEvent.click(screen.getByTestId('dataGridColumnSelectorButton')); + + // `EuiDataGrid` renders switches for hiding in the `Columns` popover when `showColumnSelector.allowHide` is `true` + const switches = await screen.queryAllByRole('switch'); + + expect(switches.length).toBe(0); // no switches are rendered, because `allowHide` is `false` + }); + + test('it dispatches the `REMOVE_COLUMN` action when a user clicks `Remove column` in the column header popover', async () => { + render( + + + + ); + + // click the `@timestamp` column header to display the popover + fireEvent.click(screen.getByText('@timestamp')); + + // click the `Remove column` action in the popover + fireEvent.click(await screen.getByText(REMOVE_COLUMN)); + + expect(mockDispatch).toBeCalledWith({ + payload: { columnId: '@timestamp', id: 'table-test' }, + type: 'x-pack/security_solution/data-table/REMOVE_COLUMN', + }); + }); + + test('it dispatches the `UPDATE_COLUMN_WIDTH` action when a user resizes a column', async () => { + render( + + + + ); + + // simulate resizing the column + fireEvent.mouseDown(screen.getAllByTestId('dataGridColumnResizer')[0]); + fireEvent.mouseMove(screen.getAllByTestId('dataGridColumnResizer')[0]); + fireEvent.mouseUp(screen.getAllByTestId('dataGridColumnResizer')[0]); + + expect(mockDispatch).toBeCalledWith({ + payload: { columnId: '@timestamp', id: 'table-test', width: NaN }, + type: 'x-pack/security_solution/data-table/UPDATE_COLUMN_WIDTH', + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/components/data_table/index.tsx b/x-pack/plugins/security_solution/public/common/components/data_table/index.tsx new file mode 100644 index 0000000000000..b27caf9d4b408 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/data_table/index.tsx @@ -0,0 +1,425 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { + EuiDataGridRefProps, + EuiDataGridColumn, + EuiDataGridCellValueElementProps, + EuiDataGridStyle, + EuiDataGridToolBarVisibilityOptions, + EuiDataGridControlColumn, + EuiDataGridPaginationProps, +} from '@elastic/eui'; +import { EuiDataGrid, EuiProgress } from '@elastic/eui'; +import { getOr } from 'lodash/fp'; +import memoizeOne from 'memoize-one'; +import React, { useCallback, useEffect, useMemo, useContext, useRef } from 'react'; +import { useDispatch } from 'react-redux'; + +import styled, { ThemeContext } from 'styled-components'; +import type { Filter } from '@kbn/es-query'; +import type { EuiTheme } from '@kbn/kibana-react-plugin/common'; +import type { FieldBrowserOptions } from '@kbn/triggers-actions-ui-plugin/public'; +import { i18n } from '@kbn/i18n'; +import type { DataTableCellAction } from '../../../../common/types'; +import type { + CellValueElementProps, + ColumnHeaderOptions, + RowRenderer, +} from '../../../../common/types/timeline'; + +import type { TimelineItem } from '../../../../common/search_strategy/timeline'; + +import { getColumnHeader, getColumnHeaders } from './column_headers/helpers'; +import { + addBuildingBlockStyle, + hasCellActions, + mapSortDirectionToDirection, + mapSortingColumns, +} from './helpers'; + +import type { BrowserFields } from '../../../../common/search_strategy/index_fields'; +import { REMOVE_COLUMN } from './column_headers/translations'; +import { dataTableActions, dataTableSelectors } from '../../store/data_table'; +import type { BulkActionsProp } from '../toolbar/bulk_actions/types'; +import { useKibana } from '../../lib/kibana'; +import { getPageRowIndex } from './pagination'; +import { UnitCount } from '../toolbar/unit'; +import { useShallowEqualSelector } from '../../hooks/use_selector'; +import { tableDefaults } from '../../store/data_table/defaults'; + +const DATA_TABLE_ARIA_LABEL = i18n.translate('xpack.securitySolution.dataTable.ariaLabel', { + defaultMessage: 'Alerts', +}); + +export interface DataTableProps { + additionalControls?: React.ReactNode; + browserFields: BrowserFields; + bulkActions?: BulkActionsProp; + data: TimelineItem[]; + defaultCellActions?: DataTableCellAction[]; + disabledCellActions: string[]; + fieldBrowserOptions?: FieldBrowserOptions; + filters?: Filter[]; + id: string; + leadingControlColumns: EuiDataGridControlColumn[]; + loadPage: (newActivePage: number) => void; + renderCellValue: (props: CellValueElementProps) => React.ReactNode; + rowRenderers: RowRenderer[]; + hasCrudPermissions?: boolean; + unitCountText: string; + pagination: EuiDataGridPaginationProps; + totalItems: number; +} + +const ES_LIMIT_COUNT = 9999; + +const gridStyle: EuiDataGridStyle = { border: 'none', fontSize: 's', header: 'underline' }; + +const EuiDataGridContainer = styled.div<{ hideLastPage: boolean }>` + ul.euiPagination__list { + li.euiPagination__item:last-child { + ${({ hideLastPage }) => `${hideLastPage ? 'display:none' : ''}`}; + } + } +`; + +export const DataTableComponent = React.memo( + ({ + additionalControls, + browserFields, + bulkActions = true, + data, + defaultCellActions, + disabledCellActions, + fieldBrowserOptions, + filters, + hasCrudPermissions, + id, + leadingControlColumns, + loadPage, + renderCellValue, + rowRenderers, + pagination, + unitCountText, + totalItems, + }) => { + const { + triggersActionsUi: { getFieldBrowser }, + } = useKibana().services; + const memoizedColumnHeaders: ( + headers: ColumnHeaderOptions[], + browserFields: BrowserFields + ) => ColumnHeaderOptions[] = memoizeOne(getColumnHeaders); + + const getDataTable = dataTableSelectors.getTableByIdSelector(); + const dataTable = useShallowEqualSelector((state) => getDataTable(state, id) ?? tableDefaults); + const { columns, selectedEventIds, showCheckboxes, sort, isLoading, defaultColumns } = + dataTable; + const columnHeaders = memoizedColumnHeaders(columns, browserFields); + + const dataGridRef = useRef(null); + + const dispatch = useDispatch(); + + const selectedCount = useMemo(() => Object.keys(selectedEventIds).length, [selectedEventIds]); + + const theme: EuiTheme = useContext(ThemeContext); + + const showBulkActions = useMemo(() => { + if (!hasCrudPermissions) { + return false; + } + + if (selectedCount === 0 || !showCheckboxes) { + return false; + } + if (typeof bulkActions === 'boolean') { + return bulkActions; + } + return (bulkActions?.customBulkActions?.length || bulkActions?.alertStatusActions) ?? true; + }, [hasCrudPermissions, selectedCount, showCheckboxes, bulkActions]); + + const onResetColumns = useCallback(() => { + dispatch(dataTableActions.updateColumns({ id, columns: defaultColumns })); + }, [defaultColumns, dispatch, id]); + + const onToggleColumn = useCallback( + (columnId: string) => { + if (columnHeaders.some(({ id: columnHeaderId }) => columnId === columnHeaderId)) { + dispatch( + dataTableActions.removeColumn({ + columnId, + id, + }) + ); + } else { + dispatch( + dataTableActions.upsertColumn({ + column: getColumnHeader(columnId, defaultColumns), + id, + index: 1, + }) + ); + } + }, + [columnHeaders, dispatch, id, defaultColumns] + ); + + const toolbarVisibility: EuiDataGridToolBarVisibilityOptions = useMemo( + () => ({ + additionalControls: { + left: { + append: ( + <> + {isLoading && } + {unitCountText} + {additionalControls ?? null} + {getFieldBrowser({ + browserFields, + options: fieldBrowserOptions, + columnIds: columnHeaders.map(({ id: columnId }) => columnId), + onResetColumns, + onToggleColumn, + })} + + ), + }, + }, + ...(showBulkActions + ? { + showColumnSelector: false, + showSortSelector: false, + showFullScreenSelector: false, + } + : { + showColumnSelector: { allowHide: false, allowReorder: true }, + showSortSelector: true, + showFullScreenSelector: true, + }), + showDisplaySelector: false, + }), + [ + isLoading, + unitCountText, + additionalControls, + getFieldBrowser, + browserFields, + fieldBrowserOptions, + columnHeaders, + onResetColumns, + onToggleColumn, + showBulkActions, + ] + ); + + const sortingColumns: Array<{ + id: string; + direction: 'asc' | 'desc'; + }> = useMemo( + () => + sort.map((x) => ({ + id: x.columnId, + direction: mapSortDirectionToDirection(x.sortDirection), + })), + [sort] + ); + + const onSort = useCallback( + ( + nextSortingColumns: Array<{ + id: string; + direction: 'asc' | 'desc'; + }> + ) => { + dispatch( + dataTableActions.updateSort({ + id, + sort: mapSortingColumns({ columns: nextSortingColumns, columnHeaders }), + }) + ); + + setTimeout(() => { + // schedule the query to be re-executed from page 0, (but only after the + // store has been updated with the new sort): + if (loadPage != null) { + loadPage(0); + } + }, 0); + }, + [columnHeaders, dispatch, id, loadPage] + ); + + const visibleColumns = useMemo(() => columnHeaders.map(({ id: cid }) => cid), [columnHeaders]); // the full set of columns + + const onColumnResize = useCallback( + ({ columnId, width }: { columnId: string; width: number }) => { + dispatch( + dataTableActions.updateColumnWidth({ + columnId, + id, + width, + }) + ); + }, + [dispatch, id] + ); + + const onSetVisibleColumns = useCallback( + (newVisibleColumns: string[]) => { + dispatch( + dataTableActions.updateColumnOrder({ + columnIds: newVisibleColumns, + id, + }) + ); + }, + [dispatch, id] + ); + + const columnsWithCellActions: EuiDataGridColumn[] = useMemo( + () => + columnHeaders.map((header) => { + const buildAction = (dataTableCellAction: DataTableCellAction) => + dataTableCellAction({ + browserFields, + data: data.map((row) => row.data), + ecsData: data.map((row) => row.ecs), + header: columnHeaders.find((h) => h.id === header.id), + pageSize: pagination.pageSize, + scopeId: id, + closeCellPopover: dataGridRef.current?.closeCellPopover, + }); + return { + ...header, + actions: { + ...header.actions, + additional: [ + { + iconType: 'cross', + label: REMOVE_COLUMN, + onClick: () => { + dispatch(dataTableActions.removeColumn({ id, columnId: header.id })); + }, + size: 'xs', + }, + ], + }, + ...(hasCellActions({ + columnId: header.id, + disabledCellActions, + }) + ? { + cellActions: + header.dataTableCellActions?.map(buildAction) ?? + defaultCellActions?.map(buildAction), + visibleCellActions: 3, + } + : {}), + }; + }), + [ + browserFields, + columnHeaders, + data, + defaultCellActions, + disabledCellActions, + dispatch, + id, + pagination.pageSize, + ] + ); + + const renderTGridCellValue = useMemo(() => { + const Cell: React.FC = ({ + columnId, + rowIndex, + colIndex, + setCellProps, + isDetails, + }): React.ReactElement | null => { + const pageRowIndex = getPageRowIndex(rowIndex, pagination.pageSize); + const rowData = pageRowIndex < data.length ? data[pageRowIndex].data : null; + const header = columnHeaders.find((h) => h.id === columnId); + const eventId = pageRowIndex < data.length ? data[pageRowIndex]._id : null; + const ecs = pageRowIndex < data.length ? data[pageRowIndex].ecs : null; + + useEffect(() => { + const defaultStyles = { overflow: 'hidden' }; + setCellProps({ style: { ...defaultStyles } }); + if (ecs && rowData) { + addBuildingBlockStyle(ecs, theme, setCellProps, defaultStyles); + } else { + // disable the cell when it has no data + setCellProps({ style: { display: 'none' } }); + } + }, [rowIndex, setCellProps, ecs, rowData]); + + if (rowData == null || header == null || eventId == null || ecs === null) { + return null; + } + + return renderCellValue({ + browserFields, + columnId: header.id, + data: rowData, + ecsData: ecs, + eventId, + globalFilters: filters, + header, + isDetails, + isDraggable: false, + isExpandable: true, + isExpanded: false, + linkValues: getOr([], header.linkField ?? '', ecs), + rowIndex, + colIndex, + rowRenderers, + setCellProps, + scopeId: id, + truncate: isDetails ? false : true, + }) as React.ReactElement; + }; + return Cell; + }, [ + browserFields, + columnHeaders, + data, + filters, + id, + pagination.pageSize, + renderCellValue, + rowRenderers, + theme, + ]); + + return ( + <> + ES_LIMIT_COUNT}> + + + + ); + } +); + +DataTableComponent.displayName = 'DataTableComponent'; diff --git a/x-pack/plugins/timelines/common/utils/pagination.ts b/x-pack/plugins/security_solution/public/common/components/data_table/pagination.ts similarity index 100% rename from x-pack/plugins/timelines/common/utils/pagination.ts rename to x-pack/plugins/security_solution/public/common/components/data_table/pagination.ts diff --git a/x-pack/plugins/security_solution/public/common/components/data_table/types.ts b/x-pack/plugins/security_solution/public/common/components/data_table/types.ts new file mode 100644 index 0000000000000..679a73a29bea5 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/data_table/types.ts @@ -0,0 +1,50 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { ColumnHeaderOptions, ColumnId } from '../../../../common/types'; +import type { SortDirectionTable as SortDirection } from '../../../../common/types/data_table'; + +export type KueryFilterQueryKind = 'kuery' | 'lucene' | 'eql'; + +export interface KueryFilterQuery { + kind: KueryFilterQueryKind; + expression: string; +} + +export interface SerializedFilterQuery { + kuery: KueryFilterQuery | null; + serializedQuery: string; +} + +/** Invoked when a column is sorted */ +export type OnColumnSorted = (sorted: { columnId: ColumnId; sortDirection: SortDirection }) => void; + +export type OnColumnsSorted = ( + sorted: Array<{ columnId: ColumnId; sortDirection: SortDirection }> +) => void; + +export type OnColumnRemoved = (columnId: ColumnId) => void; + +export type OnColumnResized = ({ columnId, delta }: { columnId: ColumnId; delta: number }) => void; + +/** Invoked when a user clicks to load more item */ +export type OnChangePage = (nextPage: number) => void; + +/** Invoked when a user checks/un-checks a row */ +export type OnRowSelected = ({ + eventIds, + isSelected, +}: { + eventIds: string[]; + isSelected: boolean; +}) => void; + +/** Invoked when a user checks/un-checks the select all checkbox */ +export type OnSelectAll = ({ isSelected }: { isSelected: boolean }) => void; + +/** Invoked when columns are updated */ +export type OnUpdateColumns = (columns: ColumnHeaderOptions[]) => void; diff --git a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/drag_drop_context_wrapper.tsx b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/drag_drop_context_wrapper.tsx index fa0d2ffda0391..44b59bb8a56cf 100644 --- a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/drag_drop_context_wrapper.tsx +++ b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/drag_drop_context_wrapper.tsx @@ -14,10 +14,6 @@ import type { Dispatch } from 'redux'; import deepEqual from 'fast-deep-equal'; import { IS_DRAGGING_CLASS_NAME } from '@kbn/securitysolution-t-grid'; -import { - addFieldToTimelineColumns, - getTimelineIdFromColumnDroppableId, -} from '@kbn/timelines-plugin/public'; import type { BeforeCapture } from './drag_drop_context'; import type { BrowserFields } from '../../containers/source'; import { dragAndDropSelectors } from '../../store'; @@ -38,6 +34,8 @@ import { providerWasDroppedOnTimeline, draggableIsField, userIsReArrangingProviders, + getIdFromColumnDroppableId, + addFieldToColumns, } from './helpers'; import { useDeepEqualSelector } from '../../hooks/use_selector'; import { useKibana } from '../../lib/kibana'; @@ -87,12 +85,12 @@ const onDragEndHandler = ({ timelineId: TimelineId.active, }); } else if (fieldWasDroppedOnTimelineColumns(result)) { - addFieldToTimelineColumns({ + addFieldToColumns({ browserFields, defaultsHeader: defaultAlertsHeaders, dispatch, result, - timelineId: getTimelineIdFromColumnDroppableId(result.destination?.droppableId ?? ''), + scopeId: getIdFromColumnDroppableId(result.destination?.droppableId ?? ''), }); } }; diff --git a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_keyboard_wrapper_hook/helpers.test.ts b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_keyboard_wrapper_hook/helpers.test.ts new file mode 100644 index 0000000000000..f9414e90af8f4 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_keyboard_wrapper_hook/helpers.test.ts @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { draggableKeyDownHandler } from './helpers'; + +jest.mock('../../../lib/kibana'); +describe('draggableKeyDownHandler', () => { + test('it calles the proper function cancelDragActions when Escape key was pressed', () => { + const mockElement = document.createElement('div'); + const keyboardEvent = new KeyboardEvent('keydown', { + ctrlKey: false, + key: 'Escape', + metaKey: false, + }) as unknown as React.KeyboardEvent; + + const cancelDragActions = jest.fn(); + draggableKeyDownHandler({ + closePopover: jest.fn(), + openPopover: jest.fn(), + beginDrag: jest.fn(), + cancelDragActions, + draggableElement: mockElement, + dragActions: null, + dragToLocation: jest.fn(), + endDrag: jest.fn(), + keyboardEvent, + setDragActions: jest.fn(), + }); + expect(cancelDragActions).toBeCalled(); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_keyboard_wrapper_hook/helpers.ts b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_keyboard_wrapper_hook/helpers.ts new file mode 100644 index 0000000000000..e9f5915516351 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_keyboard_wrapper_hook/helpers.ts @@ -0,0 +1,126 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { FluidDragActions, Position } from 'react-beautiful-dnd'; +import { KEYBOARD_DRAG_OFFSET } from '@kbn/securitysolution-t-grid'; + +import { stopPropagationAndPreventDefault } from '@kbn/timelines-plugin/public'; + +/** + * Temporarily disables tab focus on child links of the draggable to work + * around an issue where tab focus becomes stuck on the interactive children + * + * NOTE: This function is (intentionally) only effective when used in a key + * event handler, because it automatically restores focus capabilities on + * the next tick. + */ +export const temporarilyDisableInteractiveChildTabIndexes = (draggableElement: HTMLDivElement) => { + const interactiveChildren = draggableElement.querySelectorAll('a, button'); + interactiveChildren.forEach((interactiveChild) => { + interactiveChild.setAttribute('tabindex', '-1'); // DOM mutation + }); + + // restore the default tabindexs on the next tick: + setTimeout(() => { + interactiveChildren.forEach((interactiveChild) => { + interactiveChild.setAttribute('tabindex', '0'); // DOM mutation + }); + }, 0); +}; + +export interface DraggableKeyDownHandlerProps { + beginDrag: () => FluidDragActions | null; + cancelDragActions: () => void; + closePopover?: () => void; + draggableElement: HTMLDivElement; + dragActions: FluidDragActions | null; + dragToLocation: ({ + dragActions, + position, + }: { + dragActions: FluidDragActions | null; + position: Position; + }) => void; + keyboardEvent: React.KeyboardEvent; + endDrag: (dragActions: FluidDragActions | null) => void; + openPopover?: () => void; + setDragActions: (value: React.SetStateAction) => void; +} + +export const draggableKeyDownHandler = ({ + beginDrag, + cancelDragActions, + closePopover, + draggableElement, + dragActions, + dragToLocation, + endDrag, + keyboardEvent, + openPopover, + setDragActions, +}: DraggableKeyDownHandlerProps) => { + let currentPosition: DOMRect | null = null; + + switch (keyboardEvent.key) { + case ' ': + if (!dragActions) { + // start dragging, because space was pressed + if (closePopover != null) { + closePopover(); + } + setDragActions(beginDrag()); + } else { + // end dragging, because space was pressed + endDrag(dragActions); + setDragActions(null); + } + break; + case 'Escape': + cancelDragActions(); + break; + case 'Tab': + // IMPORTANT: we do NOT want to stop propagation and prevent default when Tab is pressed + temporarilyDisableInteractiveChildTabIndexes(draggableElement); + break; + case 'ArrowUp': + currentPosition = draggableElement.getBoundingClientRect(); + dragToLocation({ + dragActions, + position: { x: currentPosition.x, y: currentPosition.y - KEYBOARD_DRAG_OFFSET }, + }); + break; + case 'ArrowDown': + currentPosition = draggableElement.getBoundingClientRect(); + dragToLocation({ + dragActions, + position: { x: currentPosition.x, y: currentPosition.y + KEYBOARD_DRAG_OFFSET }, + }); + break; + case 'ArrowLeft': + currentPosition = draggableElement.getBoundingClientRect(); + dragToLocation({ + dragActions, + position: { x: currentPosition.x - KEYBOARD_DRAG_OFFSET, y: currentPosition.y }, + }); + break; + case 'ArrowRight': + currentPosition = draggableElement.getBoundingClientRect(); + dragToLocation({ + dragActions, + position: { x: currentPosition.x + KEYBOARD_DRAG_OFFSET, y: currentPosition.y }, + }); + break; + case 'Enter': + stopPropagationAndPreventDefault(keyboardEvent); // prevents the first item in the popover from getting an errant ENTER + if (!dragActions && openPopover != null) { + openPopover(); + } + break; + default: + break; + } +}; diff --git a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_keyboard_wrapper_hook/index.tsx b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_keyboard_wrapper_hook/index.tsx new file mode 100644 index 0000000000000..dcfab43fbbe80 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_keyboard_wrapper_hook/index.tsx @@ -0,0 +1,104 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type React from 'react'; +import { useCallback, useMemo, useState } from 'react'; +import type { FluidDragActions } from 'react-beautiful-dnd'; +import { useKibana } from '../../../lib/kibana'; +import { draggableKeyDownHandler } from './helpers'; + +export interface UseDraggableKeyboardWrapperProps { + closePopover?: () => void; + draggableId: string; + fieldName: string; + keyboardHandlerRef: React.MutableRefObject; + openPopover?: () => void; +} + +export interface UseDraggableKeyboardWrapper { + onBlur: () => void; + onKeyDown: (keyboardEvent: React.KeyboardEvent) => void; +} + +export const useDraggableKeyboardWrapper = ({ + closePopover, + draggableId, + fieldName, + keyboardHandlerRef, + openPopover, +}: UseDraggableKeyboardWrapperProps): UseDraggableKeyboardWrapper => { + const { timelines } = useKibana().services; + const useAddToTimeline = timelines.getUseAddToTimeline(); + const { beginDrag, cancelDrag, dragToLocation, endDrag, hasDraggableLock } = useAddToTimeline({ + draggableId, + fieldName, + }); + const [dragActions, setDragActions] = useState(null); + + const cancelDragActions = useCallback(() => { + setDragActions((prevDragAction) => { + if (prevDragAction) { + cancelDrag(prevDragAction); + return null; + } + return null; + }); + }, [cancelDrag]); + + const onKeyDown = useCallback( + (keyboardEvent: React.KeyboardEvent) => { + const draggableElement = document.querySelector( + `[data-rbd-drag-handle-draggable-id="${draggableId}"]` + ); + + if (draggableElement) { + if (hasDraggableLock() || (!hasDraggableLock() && keyboardEvent.key === ' ')) { + keyboardEvent.preventDefault(); + keyboardEvent.stopPropagation(); + } + + draggableKeyDownHandler({ + beginDrag, + cancelDragActions, + closePopover, + dragActions, + draggableElement, + dragToLocation, + endDrag, + keyboardEvent, + openPopover, + setDragActions, + }); + + keyboardHandlerRef.current?.focus(); // to handle future key presses + } + }, + [ + beginDrag, + cancelDragActions, + closePopover, + dragActions, + draggableId, + dragToLocation, + endDrag, + hasDraggableLock, + keyboardHandlerRef, + openPopover, + setDragActions, + ] + ); + + const memoizedReturn = useMemo( + () => ({ + onBlur: cancelDragActions, + onKeyDown, + }), + [cancelDragActions, onKeyDown] + ); + + return memoizedReturn; +}; diff --git a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper.tsx b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper.tsx index 95720eb94f22f..c09554f12ec9c 100644 --- a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper.tsx +++ b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper.tsx @@ -30,8 +30,8 @@ import { getDraggableId, getDroppableId } from './helpers'; import { ProviderContainer } from './provider_container'; import * as i18n from './translations'; -import { useKibana } from '../../lib/kibana'; import { useHoverActions } from '../hover_actions/use_hover_actions'; +import { useDraggableKeyboardWrapper } from './draggable_keyboard_wrapper_hook'; // As right now, we do not know what we want there, we will keep it as a placeholder export const DragEffects = styled.div``; @@ -144,7 +144,6 @@ const DraggableOnWrapperComponent: React.FC = ({ const [providerRegistered, setProviderRegistered] = useState(false); const isDisabled = dataProvider.id.includes(`-${ROW_RENDERER_BROWSER_EXAMPLE_TIMELINE_ID}-`); const dispatch = useDispatch(); - const { timelines } = useKibana().services; const { closePopOverTrigger, handleClosePopOverTrigger, @@ -248,7 +247,7 @@ const DraggableOnWrapperComponent: React.FC = ({ [dataProvider, registerProvider, render, setContainerRef, truncate] ); - const { onBlur, onKeyDown } = timelines.getUseDraggableKeyboardWrapper()({ + const { onBlur, onKeyDown } = useDraggableKeyboardWrapper({ closePopover: handleClosePopOverTrigger, draggableId: getDraggableId(dataProvider.id), fieldName: dataProvider.queryMatch.field, diff --git a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/helpers.test.ts b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/helpers.test.ts index 25206ce7da209..a91eb66a83c55 100644 --- a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/helpers.test.ts +++ b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/helpers.test.ts @@ -7,7 +7,6 @@ import { omit } from 'lodash/fp'; import type { DropResult } from 'react-beautiful-dnd'; -import { getTimelineIdFromColumnDroppableId } from '@kbn/timelines-plugin/public'; import type { IdToDataProvider } from '../../store/drag_and_drop/model'; @@ -998,16 +997,4 @@ describe('helpers', () => { }); }); }); - - describe('getTimelineIdFromColumnDroppableId', () => { - test('it returns the expected timelineId from a column droppableId', () => { - expect(getTimelineIdFromColumnDroppableId(DROPPABLE_ID_TIMELINE_COLUMNS)).toEqual( - 'timeline-1' - ); - }); - - test('it returns an empty string when the droppableId is an empty string', () => { - expect(getTimelineIdFromColumnDroppableId('')).toEqual(''); - }); - }); }); diff --git a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/helpers.ts b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/helpers.ts index ad38235d1b757..7a48dbcb5a450 100644 --- a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/helpers.ts +++ b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/helpers.ts @@ -4,11 +4,17 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ +import { isString, keyBy } from 'lodash/fp'; import type { DropResult } from 'react-beautiful-dnd'; import type { Dispatch } from 'redux'; import type { ActionCreator } from 'typescript-fsa'; -import { getProviderIdFromDraggable } from '@kbn/securitysolution-t-grid'; +import { getFieldIdFromDraggable, getProviderIdFromDraggable } from '@kbn/securitysolution-t-grid'; +import { DEFAULT_COLUMN_MIN_WIDTH } from '../../../timelines/components/timeline/body/constants'; +import { getScopedActions } from '../../../helpers'; +import type { ColumnHeaderOptions } from '../../../../common/types'; +import { TableId } from '../../../../common/types'; +import type { BrowserField, BrowserFields } from '../../../../common/search_strategy'; import { dragAndDropActions } from '../../store/actions'; import type { IdToDataProvider } from '../../store/drag_and_drop/model'; import { addContentToTimeline } from '../../../timelines/components/timeline/data_providers/helpers'; @@ -177,3 +183,84 @@ export const allowTopN = ({ return isAllowlistedNonBrowserField || (isAggregatable && isAllowedType); }; + +const getAllBrowserFields = (browserFields: BrowserFields): Array> => + Object.values(browserFields).reduce>>( + (acc, namespace) => [ + ...acc, + ...Object.values(namespace.fields != null ? namespace.fields : {}), + ], + [] + ); + +const getAllFieldsByName = ( + browserFields: BrowserFields +): { [fieldName: string]: Partial } => + keyBy('name', getAllBrowserFields(browserFields)); + +const linkFields: Record = { + 'kibana.alert.rule.name': 'kibana.alert.rule.uuid', + 'event.module': 'rule.reference', +}; + +interface AddFieldToTimelineColumnsParams { + defaultsHeader: ColumnHeaderOptions[]; + browserFields: BrowserFields; + dispatch: Dispatch; + result: DropResult; + scopeId: string; +} + +export const addFieldToColumns = ({ + browserFields, + dispatch, + result, + scopeId, + defaultsHeader, +}: AddFieldToTimelineColumnsParams): void => { + const fieldId = getFieldIdFromDraggable(result); + const allColumns = getAllFieldsByName(browserFields); + const column = allColumns[fieldId]; + const initColumnHeader = + scopeId === TableId.alertsOnAlertsPage || scopeId === TableId.alertsOnRuleDetailsPage + ? defaultsHeader.find((c) => c.id === fieldId) ?? {} + : {}; + + const scopedActions = getScopedActions(scopeId); + if (column != null && scopedActions) { + dispatch( + scopedActions.upsertColumn({ + column: { + category: column.category, + columnHeaderType: 'not-filtered', + description: isString(column.description) ? column.description : undefined, + example: isString(column.example) ? column.example : undefined, + id: fieldId, + linkField: linkFields[fieldId] ?? undefined, + type: column.type, + aggregatable: column.aggregatable, + initialWidth: DEFAULT_COLUMN_MIN_WIDTH, + ...initColumnHeader, + }, + id: scopeId, + index: result.destination != null ? result.destination.index : 0, + }) + ); + } else if (scopedActions) { + // create a column definition, because it doesn't exist in the browserFields: + dispatch( + scopedActions.upsertColumn({ + column: { + columnHeaderType: 'not-filtered', + id: fieldId, + initialWidth: DEFAULT_COLUMN_MIN_WIDTH, + }, + id: scopeId, + index: result.destination != null ? result.destination.index : 0, + }) + ); + } +}; + +export const getIdFromColumnDroppableId = (droppableId: string) => + droppableId.slice(droppableId.lastIndexOf('.') + 1); diff --git a/x-pack/plugins/security_solution/public/common/components/error_toast_dispatcher/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/error_toast_dispatcher/index.test.tsx index 42318973da4f0..51888a1a6bda8 100644 --- a/x-pack/plugins/security_solution/public/common/components/error_toast_dispatcher/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/error_toast_dispatcher/index.test.tsx @@ -19,27 +19,14 @@ import { createStore } from '../../store/store'; import { ErrorToastDispatcher } from '.'; import type { State } from '../../store/types'; -import { tGridReducer } from '@kbn/timelines-plugin/public'; describe('Error Toast Dispatcher', () => { const state: State = mockGlobalState; const { storage } = createSecuritySolutionStorageMock(); - let store = createStore( - state, - SUB_PLUGINS_REDUCER, - { dataTable: tGridReducer }, - kibanaObservable, - storage - ); + let store = createStore(state, SUB_PLUGINS_REDUCER, kibanaObservable, storage); beforeEach(() => { - store = createStore( - state, - SUB_PLUGINS_REDUCER, - { dataTable: tGridReducer }, - kibanaObservable, - storage - ); + store = createStore(state, SUB_PLUGINS_REDUCER, kibanaObservable, storage); }); describe('rendering', () => { diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/insights/insights.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/insights/insights.tsx index b43bd15ff1871..e7d743e12c5c1 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/insights/insights.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/insights/insights.tsx @@ -6,7 +6,9 @@ */ import React from 'react'; -import { EuiFlexGroup, EuiFlexItem, EuiTitle } from '@elastic/eui'; +import { EuiBetaBadge, EuiFlexGroup, EuiFlexItem, EuiIcon, EuiTitle } from '@elastic/eui'; +import { euiStyled } from '@kbn/kibana-react-plugin/common'; +import { ALERT_SUPPRESSION_DOCS_COUNT } from '@kbn/rule-data-utils'; import { find } from 'lodash/fp'; import * as i18n from './translations'; @@ -23,6 +25,13 @@ import { RelatedAlertsBySourceEvent } from './related_alerts_by_source_event'; import { RelatedAlertsBySession } from './related_alerts_by_session'; import { RelatedAlertsUpsell } from './related_alerts_upsell'; +const StyledInsightItem = euiStyled(EuiFlexItem)` + border: 1px solid ${({ theme }) => theme.eui.euiColorLightShade}; + padding: 10px 8px; + border-radius: 6px; + display: inline-flex; +`; + interface Props { browserFields: BrowserFields; eventId: string; @@ -68,6 +77,12 @@ export const Insights = React.memo( ); const hasSourceEventInfo = hasData(sourceEventField); + const alertSuppressionField = find( + { category: 'kibana', field: ALERT_SUPPRESSION_DOCS_COUNT }, + data + ); + const hasAlertSuppressionField = hasData(alertSuppressionField); + const userCasesPermissions = useGetUserCasesPermissions(); const hasCasesReadPermissions = userCasesPermissions.read; @@ -101,6 +116,20 @@ export const Insights = React.memo( + {hasAlertSuppressionField && ( + +
    + + {i18n.SUPPRESSED_ALERTS_COUNT(parseInt(alertSuppressionField.values[0], 10))} + +
    +
    + )} + {hasCasesReadPermissions && ( diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/insights/translations.ts b/x-pack/plugins/security_solution/public/common/components/event_details/insights/translations.ts index 270c1eedb62c0..4b2056566ea79 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/insights/translations.ts +++ b/x-pack/plugins/security_solution/public/common/components/event_details/insights/translations.ts @@ -149,3 +149,16 @@ export const INSIGHTS_UPSELL = i18n.translate( defaultMessage: 'Get more insights with a platinum subscription', } ); + +export const SUPPRESSED_ALERTS_COUNT = (count?: number) => + i18n.translate('xpack.securitySolution.alertDetails.overview.insights.suppressedAlertsCount', { + defaultMessage: '{count} suppressed {count, plural, =1 {alert} other {alerts}}', + values: { count }, + }); + +export const SUPPRESSED_ALERTS_COUNT_TECHNICAL_PREVIEW = i18n.translate( + 'xpack.securitySolution.alertDetails.overview.insights.suppressedAlertsCountTechnicalPreview', + { + defaultMessage: 'Technical Preview', + } +); diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/osquery_tab.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/osquery_tab.tsx index acadbd6746d70..a20f0b2701a29 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/osquery_tab.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/osquery_tab.tsx @@ -80,7 +80,7 @@ export const useOsqueryTab = ({ titleSize="xs" body={ = ({ asEmptyButton, children, dataProviders, filters, timeRange, keepDataView, ...rest }) => { +} + +export const InvestigateInTimelineButton: React.FunctionComponent< + InvestigateInTimelineButtonProps +> = ({ asEmptyButton, children, dataProviders, filters, timeRange, keepDataView, ...rest }) => { const dispatch = useDispatch(); const getDataViewsSelector = useMemo( diff --git a/x-pack/plugins/security_solution/public/common/components/event_rendered_view/helpers.ts b/x-pack/plugins/security_solution/public/common/components/event_rendered_view/helpers.ts new file mode 100644 index 0000000000000..e4782bc8e7967 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/event_rendered_view/helpers.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { isEmpty } from 'lodash/fp'; +import type { Ecs } from '../../../../common/ecs'; + +export const isEventBuildingBlockType = (event: Ecs): boolean => + !isEmpty(event.kibana?.alert?.building_block_type); + +/** This local storage key stores the `Grid / Event rendered view` selection */ +export const ALERTS_TABLE_VIEW_SELECTION_KEY = 'securitySolution.alerts.table.view-selection'; diff --git a/x-pack/plugins/security_solution/public/common/components/event_rendered_view/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/event_rendered_view/index.test.tsx new file mode 100644 index 0000000000000..c93bcf0012862 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/event_rendered_view/index.test.tsx @@ -0,0 +1,170 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { render, screen } from '@testing-library/react'; +import React from 'react'; +import type { EventRenderedViewProps } from '.'; +import { EventRenderedView } from '.'; +import { RowRendererId, TableId } from '../../../../common/types'; +import { mockTimelineData, TestProviders } from '../../mock'; + +const eventRenderedProps: EventRenderedViewProps = { + events: mockTimelineData, + leadingControlColumns: [], + onChangePage: () => null, + onChangeItemsPerPage: () => null, + pagination: { + pageIndex: 0, + pageSize: 10, + pageSizeOptions: [10, 25, 50, 100], + totalItemCount: 100, + }, + rowRenderers: [], + scopeId: TableId.alertsOnAlertsPage, + unitCountText: '10 events', +}; + +describe('event_rendered_view', () => { + beforeEach(() => jest.clearAllMocks()); + + test('it renders the timestamp correctly', () => { + render( + + + + ); + expect(screen.queryAllByTestId('moment-date')[0].textContent).toEqual( + 'Nov 5, 2018 @ 19:03:25.937' + ); + }); + + describe('getRowRenderer', () => { + const props = { + ...eventRenderedProps, + rowRenderers: [ + { + id: RowRendererId.auditd_file, + isInstance: jest.fn().mockReturnValue(false), + renderRow: jest.fn(), + }, + { + id: RowRendererId.netflow, + isInstance: jest.fn().mockReturnValue(true), // matches any data + renderRow: jest.fn(), + }, + { + id: RowRendererId.registry, + isInstance: jest.fn().mockReturnValue(true), // also matches any data + renderRow: jest.fn(), + }, + ], + }; + + test(`it (only) renders the first matching renderer when 'getRowRenderer' is NOT provided as a prop`, () => { + render( + + + + ); + + expect(props.rowRenderers[0].renderRow).not.toBeCalled(); // did not match + expect(props.rowRenderers[1].renderRow).toBeCalled(); // the first matching renderer + expect(props.rowRenderers[2].renderRow).not.toBeCalled(); // also matches, but should not be rendered + }); + + test(`it (only) renders the renderer returned by 'getRowRenderer' when it's provided as a prop`, () => { + const withGetRowRenderer = { + ...props, + getRowRenderer: jest.fn().mockImplementation(() => props.rowRenderers[2]), // only match the last renderer + }; + + render( + + + + ); + + expect(props.rowRenderers[0].renderRow).not.toBeCalled(); + expect(props.rowRenderers[1].renderRow).not.toBeCalled(); + expect(props.rowRenderers[2].renderRow).toBeCalled(); + }); + + test(`it does NOT render the plain text version of the reason when a renderer is found`, () => { + render( + + + + ); + + expect(screen.queryByTestId('plain-text-reason')).not.toBeInTheDocument(); + }); + + test(`it renders the plain text reason when no row renderer was found, but the data contains an 'ecs.signal.reason'`, () => { + const reason = 'why not?'; + const noRendererFound = { + ...props, + events: [ + ...props.events, + { + _id: 'abcd', + data: [{ field: '@timestamp', value: ['2018-11-05T19:03:25.937Z'] }], + ecs: { + _id: 'abcd', + timestamp: '2018-11-05T19:03:25.937Z', + signal: { + reason, + }, + }, + }, + ], + getRowRenderer: jest.fn().mockImplementation(() => null), // no renderer was found + }; + + render( + + + + ); + + expect(screen.getAllByTestId('plain-text-reason')[0]).toHaveTextContent('why not?'); + }); + + test(`it renders the plain text reason when no row renderer was found, but the data contains an 'ecs.kibana.alert.reason'`, () => { + const reason = 'do you really need a reason?'; + const noRendererFound = { + ...props, + events: [ + ...props.events, + { + _id: 'abcd', + data: [], + ecs: { + _id: 'abcd', + timestamp: '2018-11-05T19:03:25.937Z', + kibana: { + alert: { + reason, + }, + }, + }, + }, + ], + getRowRenderer: jest.fn().mockImplementation(() => null), // no renderer was found + }; + + render( + + + + ); + + expect(screen.getAllByTestId('plain-text-reason')[0]).toHaveTextContent( + 'do you really need a reason?' + ); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/components/event_rendered_view/index.tsx b/x-pack/plugins/security_solution/public/common/components/event_rendered_view/index.tsx new file mode 100644 index 0000000000000..9fc25f4f79da7 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/event_rendered_view/index.tsx @@ -0,0 +1,273 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import type { + CriteriaWithPagination, + EuiBasicTableProps, + EuiDataGridCellValueElementProps, + EuiDataGridControlColumn, + Pagination, +} from '@elastic/eui'; +import { EuiBasicTable, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { ALERT_REASON, ALERT_RULE_NAME, ALERT_RULE_UUID } from '@kbn/rule-data-utils'; +import { get } from 'lodash'; +import moment from 'moment'; +import type { ComponentType } from 'react'; +import React, { useCallback, useMemo } from 'react'; +import styled from 'styled-components'; + +import { useUiSetting } from '@kbn/kibana-react-plugin/public'; + +import type { Ecs } from '../../../../common/ecs'; +import { APP_UI_ID } from '../../../../common/constants'; +import type { TimelineItem } from '../../../../common/search_strategy'; +import type { RowRenderer } from '../../../../common/types'; +import { RuleName } from '../rule_name'; +import { isEventBuildingBlockType } from './helpers'; +import { UnitCount } from '../toolbar/unit'; + +const EventRenderedFlexItem = styled(EuiFlexItem)` + div:first-child { + padding-left: 0px; + div { + margin: 0px; + } + } +`; + +const ActionsContainer = styled.div` + display: flex; + align-items: center; + div div:first-child div.siemEventsTable__tdContent { + margin-left: ${({ theme }) => theme.eui.euiSizeM}; + } +`; + +// Fix typing issue with EuiBasicTable and styled +type BasicTableType = ComponentType>; + +const StyledEuiBasicTable = styled(EuiBasicTable as BasicTableType)` + padding-top: ${({ theme }) => theme.eui.euiSizeM}; + .EventRenderedView__buildingBlock { + background: ${({ theme }) => theme.eui.euiColorHighlight}; + } + + & > div:last-child { + height: 72px; + } + + & tr:nth-child(even) { + background-color: ${({ theme }) => theme.eui.euiColorLightestShade}; + } + + & tr:nth-child(odd) { + background-color: ${({ theme }) => theme.eui.euiColorEmptyShade}; + } +`; + +export interface EventRenderedViewProps { + events: TimelineItem[]; + leadingControlColumns: EuiDataGridControlColumn[]; + onChangePage: (newActivePage: number) => void; + onChangeItemsPerPage: (newItemsPerPage: number) => void; + rowRenderers: RowRenderer[]; + scopeId: string; + pagination: Pagination; + unitCountText: string; + additionalControls?: React.ReactNode; + getRowRenderer?: ({ + data, + rowRenderers, + }: { + data: Ecs; + rowRenderers: RowRenderer[]; + }) => RowRenderer | null; +} + +const PreferenceFormattedDateComponent = ({ value }: { value: Date }) => { + const tz = useUiSetting('dateFormat:tz'); + const dateFormat = useUiSetting('dateFormat'); + const zone: string = moment.tz.zone(tz)?.name ?? moment.tz.guess(); + + return {moment.tz(value, zone).format(dateFormat)}; +}; +export const PreferenceFormattedDate = React.memo(PreferenceFormattedDateComponent); + +const EventRenderedViewComponent = ({ + additionalControls, + events, + getRowRenderer, + leadingControlColumns, + onChangePage, + onChangeItemsPerPage, + rowRenderers, + scopeId, + pagination, + unitCountText, +}: EventRenderedViewProps) => { + const ActionTitle = useMemo( + () => ( + + {leadingControlColumns.map((action) => { + const ActionHeader = action.headerCellRender; + return ( + + + + ); + })} + + ), + [leadingControlColumns] + ); + + const columns = useMemo( + () => [ + { + field: 'actions', + name: ActionTitle, + truncateText: false, + mobileOptions: { show: true }, + render: (name: unknown, item: unknown) => { + const alertId = get(item, '_id'); + const rowIndex = events.findIndex((evt) => evt._id === alertId); + return ( + + {leadingControlColumns.length > 0 + ? leadingControlColumns.map((action) => { + const getActions = action.rowCellRender as ( + props: Omit + ) => React.ReactNode; + return getActions({ + columnId: 'actions', + isDetails: false, + isExpandable: false, + isExpanded: false, + rowIndex, + setCellProps: () => null, + }); + }) + : null} + + ); + }, + // TODO: derive this from ACTION_BUTTON_COUNT as other columns are done + width: '184px', + }, + { + field: 'ecs.timestamp', + name: i18n.translate('xpack.securitySolution.EventRenderedView.timestamp.column', { + defaultMessage: 'Timestamp', + }), + truncateText: false, + mobileOptions: { show: true }, + render: (name: unknown, item: TimelineItem) => { + const timestamp = get(item, `ecs.timestamp`); + return ; + }, + }, + { + field: `ecs.${ALERT_RULE_NAME}`, + name: i18n.translate('xpack.securitySolution.EventRenderedView.rule.column', { + defaultMessage: 'Rule', + }), + truncateText: false, + mobileOptions: { show: true }, + render: (name: unknown, item: TimelineItem) => { + const ruleName = get(item, `ecs.signal.rule.name`) ?? get(item, `ecs.${ALERT_RULE_NAME}`); + const ruleId = get(item, `ecs.signal.rule.id`) ?? get(item, `ecs.${ALERT_RULE_UUID}`); + return ; + }, + }, + { + field: 'eventSummary', + name: i18n.translate('xpack.securitySolution.EventRenderedView.eventSummary.column', { + defaultMessage: 'Event Summary', + }), + truncateText: false, + mobileOptions: { show: true }, + render: (name: unknown, item: TimelineItem) => { + const ecsData = get(item, 'ecs'); + const reason = get(item, `ecs.signal.reason`) ?? get(item, `ecs.${ALERT_REASON}`); + const rowRenderer = + getRowRenderer != null + ? getRowRenderer({ data: ecsData, rowRenderers }) + : rowRenderers.find((x) => x.isInstance(ecsData)) ?? null; + + return ( + + {rowRenderer != null ? ( + +
    + {rowRenderer.renderRow({ + data: ecsData, + isDraggable: false, + scopeId, + })} +
    +
    + ) : ( + <> + {reason && {reason}} + + )} +
    + ); + }, + width: '60%', + }, + ], + [ActionTitle, events, getRowRenderer, leadingControlColumns, rowRenderers, scopeId] + ); + + const handleTableChange = useCallback( + (pageChange: CriteriaWithPagination) => { + if (pageChange.page.index !== pagination.pageIndex) { + onChangePage(pageChange.page.index); + } + if (pageChange.page.size !== pagination.pageSize) { + onChangeItemsPerPage(pageChange.page.size); + } + }, + [pagination.pageIndex, pagination.pageSize, onChangePage, onChangeItemsPerPage] + ); + + const toolbar = useMemo( + () => ( + + + {unitCountText} + + {additionalControls} + + ), + [additionalControls, unitCountText] + ); + + return ( + <> + {toolbar} + + isEventBuildingBlockType(ecs) + ? { + className: `EventRenderedView__buildingBlock`, + } + : {} + } + /> + + ); +}; + +export const EventRenderedView = React.memo(EventRenderedViewComponent); diff --git a/x-pack/plugins/security_solution/public/common/components/events_tab/events_query_tab_body.test.tsx b/x-pack/plugins/security_solution/public/common/components/events_tab/events_query_tab_body.test.tsx index 5550e5c00f2e2..9e622ab2d718e 100644 --- a/x-pack/plugins/security_solution/public/common/components/events_tab/events_query_tab_body.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/events_tab/events_query_tab_body.test.tsx @@ -13,9 +13,9 @@ import { TestProviders } from '../../mock'; import type { EventsQueryTabBodyComponentProps } from './events_query_tab_body'; import { EventsQueryTabBody, ALERTS_EVENTS_HISTOGRAM_ID } from './events_query_tab_body'; import { useGlobalFullScreen } from '../../containers/use_full_screen'; -import * as tGridActions from '@kbn/timelines-plugin/public/store/t_grid/actions'; import { licenseService } from '../../hooks/use_license'; import { mockHistory } from '../../mock/router'; +import { dataTableActions } from '../../store/data_table'; const mockGetDefaultControlColumn = jest.fn(); jest.mock('../../../timelines/components/timeline/body/control_columns', () => ({ @@ -172,7 +172,7 @@ describe('EventsQueryTabBody', () => { }); it('initializes t-grid', () => { - const spy = jest.spyOn(tGridActions, 'initializeTGridSettings'); + const spy = jest.spyOn(dataTableActions, 'initializeDataTableSettings'); render( diff --git a/x-pack/plugins/security_solution/public/common/components/events_tab/events_query_tab_body.tsx b/x-pack/plugins/security_solution/public/common/components/events_tab/events_query_tab_body.tsx index 720a973726404..e3b9245348a2a 100644 --- a/x-pack/plugins/security_solution/public/common/components/events_tab/events_query_tab_body.tsx +++ b/x-pack/plugins/security_solution/public/common/components/events_tab/events_query_tab_body.tsx @@ -10,11 +10,8 @@ import { useDispatch } from 'react-redux'; import { EuiCheckbox } from '@elastic/eui'; import type { Filter } from '@kbn/es-query'; -import type { EntityType } from '@kbn/timelines-plugin/common'; - -import type { BulkActionsProp } from '@kbn/timelines-plugin/common/types'; +import type { TableId } from '../../../../common/types'; import { dataTableActions } from '../../store/data_table'; -import type { TableId } from '../../../../common/types/timeline'; import { RowRendererId } from '../../../../common/types/timeline'; import { StatefulEventsViewer } from '../events_viewer'; import { eventsDefaultModel } from '../events_viewer/default_model'; @@ -48,6 +45,7 @@ import { useGetInitialUrlParamValue, useReplaceUrlParams, } from '../../utils/global_query_string/helpers'; +import type { BulkActionsProp } from '../toolbar/bulk_actions/types'; export const ALERTS_EVENTS_HISTOGRAM_ID = 'alertsOrEventsHistogramQuery'; @@ -100,7 +98,7 @@ const EventsQueryTabBodyComponent: React.FC = useEffect(() => { dispatch( - dataTableActions.initializeTGridSettings({ + dataTableActions.initializeDataTableSettings({ id: tableId, defaultColumns: eventsDefaultModel.columns.map((c) => !tGridEnabled && c.initialWidth == null @@ -187,13 +185,12 @@ const EventsQueryTabBodyComponent: React.FC = defaultCellActions={defaultCellActions} start={startDate} end={endDate} - entityType={'events' as EntityType} leadingControlColumns={leadingControlColumns} renderCellValue={DefaultCellRenderer} rowRenderers={defaultRowRenderers} - scopeId={SourcererScopeName.default} + sourcererScope={SourcererScopeName.default} tableId={tableId} - unit={showExternalAlerts ? i18n.ALERTS_UNIT : i18n.EVENTS_UNIT} + unit={showExternalAlerts ? i18n.EXTERNAL_ALERTS_UNIT : i18n.EVENTS_UNIT} defaultModel={defaultModel} pageFilters={composedPageFilters} bulkActions={bulkActions} diff --git a/x-pack/plugins/security_solution/public/common/components/events_tab/histogram_configurations.ts b/x-pack/plugins/security_solution/public/common/components/events_tab/histogram_configurations.ts index fce972a013834..b1e23d518c6b6 100644 --- a/x-pack/plugins/security_solution/public/common/components/events_tab/histogram_configurations.ts +++ b/x-pack/plugins/security_solution/public/common/components/events_tab/histogram_configurations.ts @@ -17,7 +17,7 @@ const DEFAULT_EVENTS_STACK_BY = 'event.action'; export const getSubtitleFunction = (defaultNumberFormat: string, isAlert: boolean) => (totalCount: number) => `${i18n.SHOWING}: ${numeral(totalCount).format(defaultNumberFormat)} ${ - isAlert ? i18n.ALERTS_UNIT(totalCount) : i18n.EVENTS_UNIT(totalCount) + isAlert ? i18n.EXTERNAL_ALERTS_UNIT(totalCount) : i18n.EVENTS_UNIT(totalCount) }`; export const eventsStackByOptions: MatrixHistogramOption[] = [ diff --git a/x-pack/plugins/security_solution/public/common/components/events_tab/translations.ts b/x-pack/plugins/security_solution/public/common/components/events_tab/translations.ts index abcb1511cb1cc..95300554df00c 100644 --- a/x-pack/plugins/security_solution/public/common/components/events_tab/translations.ts +++ b/x-pack/plugins/security_solution/public/common/components/events_tab/translations.ts @@ -7,8 +7,8 @@ import { i18n } from '@kbn/i18n'; -export const ALERTS_UNIT = (totalCount: number) => - i18n.translate('xpack.securitySolution.eventsTab.unit', { +export const EXTERNAL_ALERTS_UNIT = (totalCount: number) => + i18n.translate('xpack.securitySolution.eventsTab.externalAlertsUnit', { values: { totalCount }, defaultMessage: `external {totalCount, plural, =1 {alert} other {alerts}}`, }); diff --git a/x-pack/plugins/security_solution/public/common/components/events_viewer/default_model.tsx b/x-pack/plugins/security_solution/public/common/components/events_viewer/default_model.tsx index 28765b2db71a6..eb2bf8454a308 100644 --- a/x-pack/plugins/security_solution/public/common/components/events_viewer/default_model.tsx +++ b/x-pack/plugins/security_solution/public/common/components/events_viewer/default_model.tsx @@ -6,10 +6,10 @@ */ import { tableDefaults } from '../../store/data_table/defaults'; -import type { SubsetTGridModel } from '../../store/data_table/model'; +import type { SubsetDataTableModel } from '../../store/data_table/model'; import { defaultEventHeaders } from './default_event_headers'; -export const eventsDefaultModel: SubsetTGridModel = { +export const eventsDefaultModel: SubsetDataTableModel = { ...tableDefaults, columns: defaultEventHeaders, }; diff --git a/x-pack/plugins/security_solution/public/common/components/events_viewer/helpers.tsx b/x-pack/plugins/security_solution/public/common/components/events_viewer/helpers.tsx new file mode 100644 index 0000000000000..93176993c2dd7 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/events_viewer/helpers.tsx @@ -0,0 +1,65 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { TableId } from '../../../../common/types'; +import type { CombineQueries } from '../../lib/kuery'; +import { buildTimeRangeFilter, combineQueries } from '../../lib/kuery'; + +import { EVENTS_TABLE_CLASS_NAME } from './styles'; +import type { ViewSelection } from './summary_view_select'; + +export const getCombinedFilterQuery = ({ + from, + to, + filters, + ...combineQueriesParams +}: CombineQueries & { from: string; to: string }): string | undefined => { + const combinedQueries = combineQueries({ + ...combineQueriesParams, + filters: [...filters, buildTimeRangeFilter(from, to)], + }); + + return combinedQueries ? combinedQueries.filterQuery : undefined; +}; + +export const resolverIsShowing = (graphEventId: string | undefined): boolean => + graphEventId != null && graphEventId !== ''; + +export const EVENTS_COUNT_BUTTON_CLASS_NAME = 'local-events-count-button'; + +/** Returns `true` when the element, or one of it's children has focus */ +export const elementOrChildrenHasFocus = (element: HTMLElement | null | undefined): boolean => + element === document.activeElement || element?.querySelector(':focus-within') != null; + +/** Returns true if the events table has focus */ +export const tableHasFocus = (containerElement: HTMLElement | null): boolean => + elementOrChildrenHasFocus( + containerElement?.querySelector(`.${EVENTS_TABLE_CLASS_NAME}`) + ); + +export const isSelectableView = (tableId: string): boolean => + tableId === TableId.alertsOnAlertsPage || tableId === TableId.alertsOnRuleDetailsPage; + +export const isViewSelection = (value: unknown): value is ViewSelection => + value === 'gridView' || value === 'eventRenderedView'; + +/** always returns a valid default `ViewSelection` */ +export const getDefaultViewSelection = ({ + tableId, + value, +}: { + tableId: string; + value: unknown; +}): ViewSelection => { + const defaultViewSelection = 'gridView'; + + if (!isSelectableView(tableId)) { + return defaultViewSelection; + } else { + return isViewSelection(value) ? value : defaultViewSelection; + } +}; diff --git a/x-pack/plugins/security_solution/public/common/components/events_viewer/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/events_viewer/index.test.tsx index 56f7511c8bdf6..81f8498b39796 100644 --- a/x-pack/plugins/security_solution/public/common/components/events_viewer/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/events_viewer/index.test.tsx @@ -16,7 +16,6 @@ import { mockEventViewerResponse } from './mock'; import { StatefulEventsViewer } from '.'; import { eventsDefaultModel } from './default_model'; import { EntityType } from '@kbn/timelines-plugin/common'; -import { TableId } from '../../../../common/types/timeline'; import { SourcererScopeName } from '../../store/sourcerer/model'; import { DefaultCellRenderer } from '../../../timelines/components/timeline/cell_rendering/default_cell_renderer'; import { useTimelineEvents } from '../../../timelines/containers'; @@ -25,9 +24,21 @@ import { defaultRowRenderers } from '../../../timelines/components/timeline/body import { defaultCellActions } from '../../lib/cell_actions/default_cell_actions'; import type { UseFieldBrowserOptionsProps } from '../../../timelines/components/fields_browser'; import { useGetUserCasesPermissions } from '../../lib/kibana'; +import { TableId } from '../../../../common/types'; +import { mount } from 'enzyme'; jest.mock('../../lib/kibana'); +const mockDispatch = jest.fn(); +jest.mock('react-redux', () => { + const original = jest.requireActual('react-redux'); + + return { + ...original, + useDispatch: () => mockDispatch, + }; +}); + const originalKibanaLib = jest.requireActual('../../lib/kibana'); // Restore the useGetUserCasesPermissions so the calling functions can receive a valid permissions object @@ -46,6 +57,12 @@ jest.mock('../../../timelines/components/fields_browser', () => ({ useFieldBrowserOptions: (props: UseFieldBrowserOptionsProps) => mockUseFieldBrowserOptions(props), })); +jest.mock('./helpers', () => ({ + getDefaultViewSelection: () => 'gridView', + resolverIsShowing: () => false, + getCombinedFilterQuery: () => undefined, +})); + const mockUseResizeObserver: jest.Mock = useResizeObserver as jest.Mock; jest.mock('use-resize-observer/polyfilled'); mockUseResizeObserver.mockImplementation(() => ({})); @@ -64,35 +81,36 @@ const testProps = { leadingControlColumns: getDefaultControlColumn(ACTION_BUTTON_COUNT), renderCellValue: DefaultCellRenderer, rowRenderers: defaultRowRenderers, - scopeId: SourcererScopeName.default, + sourcererScope: SourcererScopeName.default, start: from, bulkActions: false, + hasCrudPermissions: true, }; describe('StatefulEventsViewer', () => { (useTimelineEvents as jest.Mock).mockReturnValue([false, mockEventViewerResponse]); - test('it renders the events viewer', async () => { - const wrapper = render( + test('it renders the events viewer', () => { + const wrapper = mount( ); - expect(wrapper.getByText('hello grid')).toBeTruthy(); + expect(wrapper.find(`[data-test-subj="events-viewer-panel"]`).exists()).toBeTruthy(); }); // InspectButtonContainer controls displaying InspectButton components - test('it renders InspectButtonContainer', async () => { - const wrapper = render( + test('it renders InspectButtonContainer', () => { + const wrapper = mount( ); - expect(wrapper.getByTestId(`hoverVisibilityContainer`)).toBeTruthy(); + expect(wrapper.find(`[data-test-subj="hoverVisibilityContainer"]`).exists()).toBeTruthy(); }); - test('it closes field editor when unmounted', async () => { + test('it closes field editor when unmounted', () => { const mockCloseEditor = jest.fn(); mockUseFieldBrowserOptions.mockImplementation(({ editorActionsRef }) => { editorActionsRef.current = { closeEditor: mockCloseEditor }; diff --git a/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx b/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx index 697dd90fccb14..39507f6a90992 100644 --- a/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx @@ -5,65 +5,97 @@ * 2.0. */ -import React, { useRef, useCallback, useMemo, useEffect } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; -import styled from 'styled-components'; +import { Storage } from '@kbn/kibana-utils-plugin/public'; +import { AlertConsumers } from '@kbn/rule-data-utils'; +import React, { useRef, useCallback, useMemo, useEffect, useState, useContext } from 'react'; +import type { ConnectedProps } from 'react-redux'; +import { connect, useDispatch, useSelector } from 'react-redux'; +import { ThemeContext } from 'styled-components'; import type { Filter } from '@kbn/es-query'; -import type { EntityType, RowRenderer } from '@kbn/timelines-plugin/common'; -import type { TGridCellAction, BulkActionsProp } from '@kbn/timelines-plugin/common/types'; -import type { ControlColumnProps, TableId } from '../../../../common/types'; +import type { Direction, EntityType, RowRenderer } from '@kbn/timelines-plugin/common'; +import { isEmpty } from 'lodash'; +import { getEsQueryConfig } from '@kbn/data-plugin/common'; +import type { EuiTheme } from '@kbn/kibana-react-plugin/common'; +import { getRowRenderer } from '../../../timelines/components/timeline/body/renderers/get_row_renderer'; +import type { Sort } from '../../../timelines/components/timeline/body/sort'; +import type { + ControlColumnProps, + DataTableCellAction, + OnRowSelected, + OnSelectAll, + SetEventsDeleted, + SetEventsLoading, + TableId, +} from '../../../../common/types'; import { dataTableActions } from '../../store/data_table'; import { InputsModelId } from '../../store/inputs/constants'; import type { State } from '../../store'; import { inputsActions } from '../../store/actions'; -import { APP_UI_ID } from '../../../../common/constants'; -import type { Status } from '../../../../common/detection_engine/schemas/common/schemas'; import { InspectButtonContainer } from '../inspect'; import { useGlobalFullScreen } from '../../containers/use_full_screen'; -import { useIsExperimentalFeatureEnabled } from '../../hooks/use_experimental_features'; import { eventsViewerSelector } from './selectors'; import type { SourcererScopeName } from '../../store/sourcerer/model'; import { useSourcererDataView } from '../../containers/sourcerer'; import type { CellValueElementProps } from '../../../timelines/components/timeline/cell_rendering'; -import { FIELDS_WITHOUT_CELL_ACTIONS } from '../../lib/cell_actions/constants'; import { useKibana } from '../../lib/kibana'; import { GraphOverlay } from '../../../timelines/components/graph_overlay'; import type { FieldEditorActions } from '../../../timelines/components/fields_browser'; import { useFieldBrowserOptions } from '../../../timelines/components/fields_browser'; -import { getRowRenderer } from '../../../timelines/components/timeline/body/renderers/get_row_renderer'; import { useSessionViewNavigation, useSessionView, } from '../../../timelines/components/timeline/session_tab_content/use_session_view'; -import type { SubsetTGridModel } from '../../store/data_table/model'; +import type { SubsetDataTableModel } from '../../store/data_table/model'; +import { + EventsContainerLoading, + FullScreenContainer, + FullWidthFlexGroupTable, + ScrollableFlexItem, + StyledEuiPanel, +} from './styles'; +import { getDefaultViewSelection, getCombinedFilterQuery } from './helpers'; +import { ALERTS_TABLE_VIEW_SELECTION_KEY } from '../event_rendered_view/helpers'; +import { useTimelineEvents } from './use_timelines_events'; +import { TableContext, EmptyTable, TableLoading } from './shared'; +import { DataTableComponent } from '../data_table'; +import { FIELDS_WITHOUT_CELL_ACTIONS } from '../../lib/cell_actions/constants'; +import type { AlertWorkflowStatus } from '../../types'; +import { EventRenderedView } from '../event_rendered_view'; +import { useQueryInspector } from '../page/manage_query'; +import type { SetQuery } from '../../containers/use_global_time/types'; +import { defaultHeaders } from '../../store/data_table/defaults'; +import { checkBoxControlColumn, transformControlColumns } from '../control_columns'; +import { getEventIdToDataMapping } from '../data_table/helpers'; +import type { ViewSelection } from './summary_view_select'; +import { RightTopMenu } from './right_top_menu'; +import { useAlertBulkActions } from './use_alert_bulk_actions'; +import type { BulkActionsProp } from '../toolbar/bulk_actions/types'; +import { StatefulEventContext } from './stateful_event_context'; +import { defaultUnit } from '../toolbar/unit'; -const EMPTY_CONTROL_COLUMNS: ControlColumnProps[] = []; +const storage = new Storage(localStorage); -const FullScreenContainer = styled.div<{ $isFullScreen: boolean }>` - height: ${({ $isFullScreen }) => ($isFullScreen ? '100%' : undefined)}; - flex: 1 1 auto; - display: flex; - width: 100%; -`; +const SECURITY_ALERTS_CONSUMERS = [AlertConsumers.SIEM]; -export interface Props { - defaultCellActions?: TGridCellAction[]; - defaultModel: SubsetTGridModel; +export interface EventsViewerProps { + defaultCellActions?: DataTableCellAction[]; + defaultModel: SubsetDataTableModel; end: string; - entityType: EntityType; + entityType?: EntityType; tableId: TableId; leadingControlColumns: ControlColumnProps[]; - scopeId: SourcererScopeName; + sourcererScope: SourcererScopeName; start: string; showTotalCount?: boolean; pageFilters?: Filter[]; - currentFilter?: Status; + currentFilter?: AlertWorkflowStatus; onRuleChange?: () => void; renderCellValue: (props: CellValueElementProps) => React.ReactNode; rowRenderers: RowRenderer[]; additionalFilters?: React.ReactNode; - hasAlertsCrud?: boolean; + hasCrudPermissions?: boolean; unit?: (n: number) => string; + indexNames?: string[]; bulkActions: boolean | BulkActionsProp; } @@ -72,11 +104,11 @@ export interface Props { * timeline is used BESIDES the flyout. The flyout makes use of the `EventsViewer` component which is a subcomponent here * NOTE: As of writting, it is not used in the Case_View component */ -const StatefulEventsViewerComponent: React.FC = ({ +const StatefulEventsViewerComponent: React.FC = ({ defaultCellActions, defaultModel, end, - entityType, + entityType = 'events', tableId, leadingControlColumns, pageFilters, @@ -85,15 +117,21 @@ const StatefulEventsViewerComponent: React.FC = ({ renderCellValue, rowRenderers, start, - scopeId, + sourcererScope, additionalFilters, - unit, + hasCrudPermissions = true, + unit = defaultUnit, + indexNames, bulkActions, + setSelected, + clearSelected, }) => { const dispatch = useDispatch(); + const theme: EuiTheme = useContext(ThemeContext); + const tableContext = useMemo(() => ({ tableId }), [tableId]); + const { filters, - input, query, dataTable: { columns, @@ -105,10 +143,23 @@ const StatefulEventsViewerComponent: React.FC = ({ sessionViewConfig, showCheckboxes, sort, + queryFields, + selectAll, + selectedEventIds, + isSelectAllChecked, + loadingEventIds, + title, } = defaultModel, } = useSelector((state: State) => eventsViewerSelector(state, tableId)); - const { timelines: timelinesUi } = useKibana().services; + const { uiSettings, data } = useKibana().services; + + const [tableView, setTableView] = useState( + getDefaultViewSelection({ + tableId, + value: storage.get(ALERTS_TABLE_VIEW_SELECTION_KEY), + }) + ); const { browserFields, @@ -118,28 +169,24 @@ const StatefulEventsViewerComponent: React.FC = ({ selectedPatterns, dataViewId: selectedDataViewId, loading: isLoadingIndexPattern, - } = useSourcererDataView(scopeId); + } = useSourcererDataView(sourcererScope); const { globalFullScreen } = useGlobalFullScreen(); - const tGridEventRenderedViewEnabled = useIsExperimentalFeatureEnabled( - 'tGridEventRenderedViewEnabled' - ); - const editorActionsRef = useRef(null); + const editorActionsRef = useRef(null); useEffect(() => { dispatch( - dataTableActions.createTGrid({ + dataTableActions.createDataTable({ columns, dataViewId: selectedDataViewId, defaultColumns, id: tableId, - indexNames: selectedPatterns, + indexNames: indexNames ?? selectedPatterns, itemsPerPage, showCheckboxes, sort, }) ); - return () => { dispatch(inputsActions.deleteOneQuery({ id: tableId, inputId: InputsModelId.global })); if (editorActionsRef.current) { @@ -151,7 +198,6 @@ const StatefulEventsViewerComponent: React.FC = ({ }, []); const globalFilters = useMemo(() => [...filters, ...(pageFilters ?? [])], [filters, pageFilters]); - const trailingControlColumns: ControlColumnProps[] = EMPTY_CONTROL_COLUMNS; const { Navigation } = useSessionViewNavigation({ scopeId: tableId, @@ -170,73 +216,386 @@ const StatefulEventsViewerComponent: React.FC = ({ ) : null; }, [graphEventId, tableId, sessionViewConfig, SessionView, Navigation]); const setQuery = useCallback( - (inspect, loading, refetch) => { + ({ id, inspect, loading, refetch }: SetQuery) => dispatch( inputsActions.setQuery({ - id: tableId, + id, inputId: InputsModelId.global, inspect, loading, refetch, }) - ); - }, - [dispatch, tableId] + ), + [dispatch] ); const fieldBrowserOptions = useFieldBrowserOptions({ - sourcererScope: scopeId, + sourcererScope, editorActionsRef, upsertColumn: (column, index) => dispatch(dataTableActions.upsertColumn({ column, id: tableId, index })), removeColumn: (columnId) => dispatch(dataTableActions.removeColumn({ columnId, id: tableId })), }); - const isLive = input.policy.kind === 'interval'; + const columnHeaders = isEmpty(columns) ? defaultHeaders : columns; + const esQueryConfig = getEsQueryConfig(uiSettings); + + const filterQuery = useMemo( + () => + getCombinedFilterQuery({ + config: esQueryConfig, + browserFields, + dataProviders: [], + filters: globalFilters, + from: start, + indexPattern, + kqlMode: 'filter', + kqlQuery: query, + to: end, + }), + [esQueryConfig, browserFields, globalFilters, start, indexPattern, query, end] + ); + + const canQueryTimeline = useMemo( + () => + filterQuery != null && + isLoadingIndexPattern != null && + !isLoadingIndexPattern && + !isEmpty(start) && + !isEmpty(end), + [isLoadingIndexPattern, filterQuery, start, end] + ); + + const fields = useMemo( + () => [...columnHeaders.map((c: { id: string }) => c.id), ...(queryFields ?? [])], + [columnHeaders, queryFields] + ); + + const sortField = useMemo( + () => + (sort as Sort[]).map(({ columnId, columnType, esTypes, sortDirection }) => ({ + field: columnId, + type: columnType, + direction: sortDirection as Direction, + esTypes: esTypes ?? [], + })), + [sort] + ); + + const [loading, { events, loadPage, pageInfo, refetch, totalCount = 0, inspect }] = + useTimelineEvents({ + // We rely on entityType to determine Events vs Alerts + alertConsumers: SECURITY_ALERTS_CONSUMERS, + data, + dataViewId, + endDate: end, + entityType, + fields, + filterQuery, + id: tableId, + indexNames: indexNames ?? selectedPatterns, + limit: itemsPerPage, + runtimeMappings, + skip: !canQueryTimeline, + sort: sortField, + startDate: start, + filterStatus: currentFilter, + }); + + useEffect(() => { + dispatch(dataTableActions.updateIsLoading({ id: tableId, isLoading: loading })); + }, [dispatch, tableId, loading]); + + const deleteQuery = useCallback( + ({ id }) => dispatch(inputsActions.deleteOneQuery({ inputId: InputsModelId.global, id })), + [dispatch] + ); + + useQueryInspector({ + queryId: tableId, + loading, + refetch, + setQuery, + deleteQuery, + inspect, + }); + + const totalCountMinusDeleted = useMemo( + () => (totalCount > 0 ? totalCount - deletedEventIds.length : 0), + [deletedEventIds.length, totalCount] + ); + + const hasAlerts = totalCountMinusDeleted > 0; + + // Only show the table-spanning loading indicator when the query is loading and we + // don't have data (e.g. for the initial fetch). + // Subsequent fetches (e.g. for pagination) will show a small loading indicator on + // top of the table and the table will display the current page until the next page + // is fetched. This prevents a flicker when paginating. + const showFullLoading = loading && !hasAlerts; + + const nonDeletedEvents = useMemo( + () => events.filter((e) => !deletedEventIds.includes(e._id)), + [deletedEventIds, events] + ); + useEffect(() => { + setQuery({ id: tableId, inspect, loading, refetch }); + }, [inspect, loading, refetch, setQuery, tableId]); + + // Clear checkbox selection when new events are fetched + useEffect(() => { + dispatch(dataTableActions.clearSelected({ id: tableId })); + dispatch( + dataTableActions.setDataTableSelectAll({ + id: tableId, + selectAll: false, + }) + ); + }, [nonDeletedEvents, dispatch, tableId]); + + const onChangeItemsPerPage = useCallback( + (itemsChangedPerPage) => { + dispatch( + dataTableActions.updateItemsPerPage({ id: tableId, itemsPerPage: itemsChangedPerPage }) + ); + }, + [tableId, dispatch] + ); + + const onChangePage = useCallback( + (page) => { + loadPage(page); + }, + [loadPage] + ); + + const setEventsLoading = useCallback( + ({ eventIds, isLoading }) => { + dispatch(dataTableActions.setEventsLoading({ id: tableId, eventIds, isLoading })); + }, + [dispatch, tableId] + ); + + const setEventsDeleted = useCallback( + ({ eventIds, isDeleted }) => { + dispatch(dataTableActions.setEventsDeleted({ id: tableId, eventIds, isDeleted })); + }, + [dispatch, tableId] + ); + + const selectedCount = useMemo(() => Object.keys(selectedEventIds).length, [selectedEventIds]); + + const onRowSelected: OnRowSelected = useCallback( + ({ eventIds, isSelected }: { eventIds: string[]; isSelected: boolean }) => { + setSelected({ + id: tableId, + eventIds: getEventIdToDataMapping( + nonDeletedEvents, + eventIds, + queryFields, + hasCrudPermissions + ), + isSelected, + isSelectAllChecked: isSelected && selectedCount + 1 === nonDeletedEvents.length, + }); + }, + [setSelected, tableId, nonDeletedEvents, queryFields, hasCrudPermissions, selectedCount] + ); + + const onSelectPage: OnSelectAll = useCallback( + ({ isSelected }: { isSelected: boolean }) => + isSelected + ? setSelected({ + id: tableId, + eventIds: getEventIdToDataMapping( + nonDeletedEvents, + nonDeletedEvents.map((event) => event._id), + queryFields, + hasCrudPermissions + ), + isSelected, + isSelectAllChecked: isSelected, + }) + : clearSelected({ id: tableId }), + [setSelected, tableId, nonDeletedEvents, queryFields, hasCrudPermissions, clearSelected] + ); + + // Sync to selectAll so parent components can select all events + useEffect(() => { + if (selectAll && !isSelectAllChecked) { + onSelectPage({ isSelected: true }); + } + }, [isSelectAllChecked, onSelectPage, selectAll]); + + const [transformedLeadingControlColumns] = useMemo(() => { + return [ + showCheckboxes ? [checkBoxControlColumn, ...leadingControlColumns] : leadingControlColumns, + ].map((controlColumns) => + transformControlColumns({ + columnHeaders, + controlColumns, + data: nonDeletedEvents, + disabledCellActions: FIELDS_WITHOUT_CELL_ACTIONS, + fieldBrowserOptions, + loadingEventIds, + onRowSelected, + onRuleChange, + selectedEventIds, + showCheckboxes, + tabType: 'query', + timelineId: tableId, + isSelectAllChecked, + sort, + browserFields, + onSelectPage, + theme, + setEventsLoading, + setEventsDeleted, + pageSize: itemsPerPage, + }) + ); + }, [ + showCheckboxes, + leadingControlColumns, + columnHeaders, + nonDeletedEvents, + fieldBrowserOptions, + loadingEventIds, + onRowSelected, + onRuleChange, + selectedEventIds, + tableId, + isSelectAllChecked, + sort, + browserFields, + onSelectPage, + theme, + setEventsLoading, + setEventsDeleted, + itemsPerPage, + ]); + + const alertBulkActions = useAlertBulkActions({ + tableId, + data: nonDeletedEvents, + totalItems: totalCountMinusDeleted, + refetch, + indexNames: selectedPatterns, + hasAlertsCrud: hasCrudPermissions, + showCheckboxes, + filterStatus: currentFilter, + filterQuery, + bulkActions, + selectedCount, + }); + + // Store context in state rather than creating object in provider value={} to prevent re-renders caused by a new object being created + const [activeStatefulEventContext] = useState({ + timelineID: tableId, + tabType: 'query', + enableHostDetailsFlyout: true, + enableIpDetailsFlyout: true, + }); + + const unitCountText = useMemo( + () => `${totalCountMinusDeleted.toLocaleString()} ${unit(totalCountMinusDeleted)}`, + [totalCountMinusDeleted, unit] + ); + return ( <> - {timelinesUi.getTGrid<'embedded'>({ - additionalFilters, - appId: APP_UI_ID, - browserFields, - bulkActions, - columns, - dataViewId, - defaultCellActions, - deletedEventIds, - disabledCellActions: FIELDS_WITHOUT_CELL_ACTIONS, - end, - entityType, - fieldBrowserOptions, - filters: globalFilters, - filterStatus: currentFilter, - getRowRenderer, - globalFullScreen, - graphEventId, - graphOverlay, - id: tableId, - indexNames: selectedPatterns, - indexPattern, - isLive, - isLoadingIndexPattern, - itemsPerPage, - itemsPerPageOptions, - leadingControlColumns, - onRuleChange, - query, - renderCellValue, - rowRenderers, - runtimeMappings, - setQuery, - sort, - start, - tGridEventRenderedViewEnabled, - trailingControlColumns, - type: 'embedded', - unit, - })} + + {showFullLoading && } + + {graphOverlay} + + {canQueryTimeline && ( + + + setTableView(selectedView)} + additionalFilters={additionalFilters} + hasRightOffset={tableView === 'gridView' && nonDeletedEvents.length > 0} + /> + + {!hasAlerts && !loading && !graphOverlay && } + {hasAlerts && ( + + + + {tableView === 'gridView' && ( + + )} + {tableView === 'eventRenderedView' && ( + + )} + + + + )} + + + )} + {DetailsPanel} @@ -244,4 +603,15 @@ const StatefulEventsViewerComponent: React.FC = ({ ); }; -export const StatefulEventsViewer = React.memo(StatefulEventsViewerComponent); +const mapDispatchToProps = { + clearSelected: dataTableActions.clearSelected, + setSelected: dataTableActions.setSelected, +}; + +const connector = connect(undefined, mapDispatchToProps); + +type PropsFromRedux = ConnectedProps; + +export const StatefulEventsViewer: React.FunctionComponent = connector( + StatefulEventsViewerComponent +); diff --git a/x-pack/plugins/security_solution/public/common/components/events_viewer/mock.ts b/x-pack/plugins/security_solution/public/common/components/events_viewer/mock.ts index 446191f28a45f..0d9ae75d2afe5 100644 --- a/x-pack/plugins/security_solution/public/common/components/events_viewer/mock.ts +++ b/x-pack/plugins/security_solution/public/common/components/events_viewer/mock.ts @@ -12,4 +12,10 @@ export const mockEventViewerResponse = { fakeTotalCount: 100, }, events: [], + inspect: { + dsl: [], + response: [], + }, + loadPage: jest.fn(), + refetch: jest.fn(), }; diff --git a/x-pack/plugins/security_solution/public/common/components/events_viewer/right_top_menu.tsx b/x-pack/plugins/security_solution/public/common/components/events_viewer/right_top_menu.tsx new file mode 100644 index 0000000000000..d22c2cae61f8f --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/events_viewer/right_top_menu.tsx @@ -0,0 +1,68 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useMemo } from 'react'; +import styled from 'styled-components'; +import { TableId } from '../../../../common/types'; +import { useIsExperimentalFeatureEnabled } from '../../hooks/use_experimental_features'; +import { InspectButton } from '../inspect'; +import { UpdatedFlexGroup, UpdatedFlexItem } from './styles'; +import type { ViewSelection } from './summary_view_select'; +import { SummaryViewSelector } from './summary_view_select'; + +const TitleText = styled.span` + margin-right: 12px; +`; + +interface Props { + tableView: ViewSelection; + loading: boolean; + tableId: TableId; + title: string; + onViewChange: (viewSelection: ViewSelection) => void; + additionalFilters?: React.ReactNode; + hasRightOffset?: boolean; +} + +export const RightTopMenu = ({ + tableView, + loading, + tableId, + title, + onViewChange, + additionalFilters, + hasRightOffset, +}: Props) => { + const alignItems = tableView === 'gridView' ? 'baseline' : 'center'; + const justTitle = useMemo(() => {title}, [title]); + + const tGridEventRenderedViewEnabled = useIsExperimentalFeatureEnabled( + 'tGridEventRenderedViewEnabled' + ); + return ( + + + + + + {additionalFilters} + + {tGridEventRenderedViewEnabled && + [TableId.alertsOnRuleDetailsPage, TableId.alertsOnAlertsPage].includes(tableId) && ( + + + + )} + + ); +}; diff --git a/x-pack/plugins/security_solution/public/common/components/events_viewer/shared/index.tsx b/x-pack/plugins/security_solution/public/common/components/events_viewer/shared/index.tsx new file mode 100644 index 0000000000000..6e5dd732ff96c --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/events_viewer/shared/index.tsx @@ -0,0 +1,92 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { createContext } from 'react'; +import { + EuiPanel, + EuiFlexGroup, + EuiFlexItem, + EuiLoadingSpinner, + EuiImage, + EuiText, + EuiTitle, +} from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { useKibana } from '@kbn/kibana-react-plugin/public'; +import type { CoreStart } from '@kbn/core/public'; + +const heights = { + tall: 490, + short: 250, +}; + +export const TableContext = createContext<{ tableId: string | null }>({ tableId: null }); + +export const TableLoading: React.FC<{ height?: keyof typeof heights }> = ({ height = 'tall' }) => { + return ( + + + + + + + + ); +}; + +const panelStyle = { + maxWidth: 500, +}; + +export const EmptyTable: React.FC<{ height?: keyof typeof heights }> = ({ height = 'tall' }) => { + const { http } = useKibana().services; + + return ( + + + + + + + + +

    + +

    +
    +

    + +

    +
    +
    + + + +
    +
    +
    +
    +
    + ); +}; diff --git a/x-pack/plugins/security_solution/public/common/components/events_viewer/stateful_event_context.ts b/x-pack/plugins/security_solution/public/common/components/events_viewer/stateful_event_context.ts new file mode 100644 index 0000000000000..0d723c01127e4 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/events_viewer/stateful_event_context.ts @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { createContext } from 'react'; +export interface StatefulEventContextType { + tabType: string | undefined; + timelineID: string; + enableHostDetailsFlyout: boolean; + enableIpDetailsFlyout: boolean; +} + +export const StatefulEventContext = createContext(null); diff --git a/x-pack/plugins/security_solution/public/common/components/events_viewer/styles.tsx b/x-pack/plugins/security_solution/public/common/components/events_viewer/styles.tsx new file mode 100644 index 0000000000000..8b1fb7cabd5a0 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/events_viewer/styles.tsx @@ -0,0 +1,79 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiFlexGroup, EuiFlexItem, EuiPanel } from '@elastic/eui'; +import styled from 'styled-components'; +export const SELECTOR_TIMELINE_GLOBAL_CONTAINER = 'securitySolutionTimeline__container'; +export const EVENTS_TABLE_CLASS_NAME = 'siemEventsTable'; + +export const FullScreenContainer = styled.div<{ $isFullScreen: boolean }>` + height: ${({ $isFullScreen }) => ($isFullScreen ? '100%' : undefined)}; + flex: 1 1 auto; + display: flex; + width: 100%; +`; + +export const FullWidthFlexGroupTable = styled(EuiFlexGroup)<{ $visible: boolean }>` + overflow: hidden; + margin: 0; + display: ${({ $visible }) => ($visible ? 'flex' : 'none')}; +`; + +export const ScrollableFlexItem = styled(EuiFlexItem)` + overflow: auto; +`; + +export const FullWidthFlexGroup = styled(EuiFlexGroup)<{ $visible?: boolean }>` + overflow: hidden; + margin: 0; + min-height: 490px; + display: ${({ $visible = true }) => ($visible ? 'flex' : 'none')}; +`; + +export const UpdatedFlexGroup = styled(EuiFlexGroup)<{ + $hasRightOffset?: boolean; +}>` + ${({ $hasRightOffset, theme }) => + $hasRightOffset + ? `margin-right: ${theme.eui.euiSizeXL};` + : `margin-right: ${theme.eui.euiSizeXS};`} + position: absolute; + z-index: ${({ theme }) => theme.eui.euiZLevel1 - 3}; + ${({ $hasRightOffset, theme }) => + $hasRightOffset ? `right: ${theme.eui.euiSizeXL};` : `right: ${theme.eui.euiSizeXS};`} +`; + +export const UpdatedFlexItem = styled(EuiFlexItem)<{ $show: boolean }>` + ${({ $show }) => ($show ? '' : 'visibility: hidden;')} +`; + +export const EventsContainerLoading = styled.div.attrs(({ className = '' }) => ({ + className: `${SELECTOR_TIMELINE_GLOBAL_CONTAINER} ${className}`, +}))` + position: relative; + width: 100%; + overflow: hidden; + flex: 1; + display: flex; + flex-direction: column; +`; + +export const StyledEuiPanel = styled(EuiPanel)<{ $isFullScreen: boolean }>` + display: flex; + flex-direction: column; + position: relative; + width: 100%; + + ${({ $isFullScreen }) => + $isFullScreen && + ` + border: 0; + box-shadow: none; + padding-top: 0; + padding-bottom: 0; +`} +`; diff --git a/x-pack/plugins/security_solution/public/common/components/events_viewer/summary_view_select/index.tsx b/x-pack/plugins/security_solution/public/common/components/events_viewer/summary_view_select/index.tsx new file mode 100644 index 0000000000000..fc32ca3eefe1a --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/events_viewer/summary_view_select/index.tsx @@ -0,0 +1,163 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { EuiSelectableOption } from '@elastic/eui'; +import { EuiButtonEmpty, EuiPopover, EuiSelectable, EuiTitle, EuiTextColor } from '@elastic/eui'; +import { Storage } from '@kbn/kibana-utils-plugin/public'; +import { i18n } from '@kbn/i18n'; +import React, { useCallback, useMemo, useState } from 'react'; +import styled from 'styled-components'; +import { ALERTS_TABLE_VIEW_SELECTION_KEY } from '../../event_rendered_view/helpers'; + +const storage = new Storage(localStorage); + +export type ViewSelection = 'gridView' | 'eventRenderedView'; + +const ContainerEuiSelectable = styled.div` + width: 300px; + .euiSelectableListItem__text { + white-space: pre-wrap !important; + line-height: normal; + } +`; + +const gridView = i18n.translate('xpack.securitySolution.selector.summaryView.gridView.label', { + defaultMessage: 'Grid view', +}); + +const eventRenderedView = i18n.translate( + 'xpack.securitySolution.selector.summaryView.eventRendererView.label', + { + defaultMessage: 'Event rendered view', + } +); + +interface SummaryViewSelectorProps { + onViewChange: (viewSelection: ViewSelection) => void; + viewSelected: ViewSelection; +} + +const SummaryViewSelectorComponent = ({ viewSelected, onViewChange }: SummaryViewSelectorProps) => { + const [isPopoverOpen, setIsPopoverOpen] = useState(false); + + const onButtonClick = useCallback(() => setIsPopoverOpen((currentVal) => !currentVal), []); + const closePopover = useCallback(() => setIsPopoverOpen(false), []); + const onChangeSelectable = useCallback( + (opts: EuiSelectableOption[]) => { + const selected = opts.filter((i) => i.checked === 'on'); + storage.set(ALERTS_TABLE_VIEW_SELECTION_KEY, selected[0]?.key ?? 'gridView'); + + if (selected.length > 0) { + onViewChange((selected[0]?.key ?? 'gridView') as ViewSelection); + } + setIsPopoverOpen(false); + }, + [onViewChange] + ); + + const button = useMemo( + () => ( + + {viewSelected === 'gridView' ? gridView : eventRenderedView} + + ), + [onButtonClick, viewSelected] + ); + + const options = useMemo( + () => [ + { + label: gridView, + key: 'gridView', + checked: (viewSelected === 'gridView' ? 'on' : undefined) as EuiSelectableOption['checked'], + meta: [ + { + text: i18n.translate( + 'xpack.securitySolution.selector.summaryView.options.default.description', + { + defaultMessage: + 'View as tabular data with the ability to group and sort by specific fields', + } + ), + }, + ], + }, + { + label: eventRenderedView, + key: 'eventRenderedView', + checked: (viewSelected === 'eventRenderedView' + ? 'on' + : undefined) as EuiSelectableOption['checked'], + meta: [ + { + text: i18n.translate( + 'xpack.securitySolution.selector.summaryView.options.summaryView.description', + { + defaultMessage: 'View a rendering of the event flow for each alert', + } + ), + }, + ], + }, + ], + [viewSelected] + ); + + const renderOption = useCallback((option) => { + return ( + <> + +
    {option.label}
    +
    + + {option.meta[0].text} + + + ); + }, []); + + const listProps = useMemo( + () => ({ + rowHeight: 80, + showIcons: true, + }), + [] + ); + + return ( + + + + {(list) => list} + + + + ); +}; + +export const SummaryViewSelector = React.memo(SummaryViewSelectorComponent); diff --git a/x-pack/plugins/security_solution/public/common/components/events_viewer/translations.ts b/x-pack/plugins/security_solution/public/common/components/events_viewer/translations.ts index 85b4ce59ff9d8..cc8fe3116df5d 100644 --- a/x-pack/plugins/security_solution/public/common/components/events_viewer/translations.ts +++ b/x-pack/plugins/security_solution/public/common/components/events_viewer/translations.ts @@ -24,3 +24,10 @@ export const UNIT = (totalCount: number) => export const ACTIONS = i18n.translate('xpack.securitySolution.eventsViewer.actionsColumnLabel', { defaultMessage: 'Actions', }); + +export const ERROR_TIMELINE_EVENTS = i18n.translate( + 'xpack.securitySolution.eventsViewer.timelineEvents.errorSearchDescription', + { + defaultMessage: `An error has occurred on timeline events search`, + } +); diff --git a/x-pack/plugins/security_solution/public/common/components/events_viewer/use_alert_bulk_actions.tsx b/x-pack/plugins/security_solution/public/common/components/events_viewer/use_alert_bulk_actions.tsx new file mode 100644 index 0000000000000..657af5696dae5 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/events_viewer/use_alert_bulk_actions.tsx @@ -0,0 +1,130 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor 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 { EuiLoadingSpinner } from '@elastic/eui'; +import React, { lazy, Suspense, useMemo } from 'react'; +import type { TimelineItem } from '../../../../common/search_strategy'; +import type { AlertWorkflowStatus, Refetch } from '../../types'; +import type { BulkActionsProp } from '../toolbar/bulk_actions/types'; + +const StatefulAlertBulkActions = lazy(() => import('../toolbar/bulk_actions/alert_bulk_actions')); + +interface OwnProps { + tableId: string; + data: TimelineItem[]; + totalItems: number; + refetch: Refetch; + indexNames: string[]; + hasAlertsCrud: boolean; + showCheckboxes: boolean; + filterStatus?: AlertWorkflowStatus; + filterQuery?: string; + bulkActions?: BulkActionsProp; + selectedCount?: number; +} +export const useAlertBulkActions = ({ + tableId, + data, + totalItems, + refetch, + indexNames, + hasAlertsCrud, + showCheckboxes, + filterStatus, + filterQuery, + bulkActions, + selectedCount, +}: OwnProps) => { + const showBulkActions = useMemo(() => { + if (!hasAlertsCrud) { + return false; + } + + if (selectedCount === 0 || !showCheckboxes) { + return false; + } + if (typeof bulkActions === 'boolean') { + return bulkActions; + } + return (bulkActions?.customBulkActions?.length || bulkActions?.alertStatusActions) ?? true; + }, [hasAlertsCrud, selectedCount, showCheckboxes, bulkActions]); + + const onAlertStatusActionSuccess = useMemo(() => { + if (bulkActions && bulkActions !== true) { + return bulkActions.onAlertStatusActionSuccess; + } + }, [bulkActions]); + + const onAlertStatusActionFailure = useMemo(() => { + if (bulkActions && bulkActions !== true) { + return bulkActions.onAlertStatusActionFailure; + } + }, [bulkActions]); + + const showAlertStatusActions = useMemo(() => { + if (!hasAlertsCrud) { + return false; + } + if (typeof bulkActions === 'boolean') { + return bulkActions; + } + return (bulkActions && bulkActions.alertStatusActions) ?? true; + }, [bulkActions, hasAlertsCrud]); + + const additionalBulkActions = useMemo(() => { + if (bulkActions && bulkActions !== true && bulkActions.customBulkActions !== undefined) { + return bulkActions.customBulkActions.map((action) => { + return { + ...action, + onClick: (eventIds: string[]) => { + const items = data.filter((item) => { + return eventIds.find((event) => item._id === event); + }); + action.onClick(items); + }, + }; + }); + } + }, [bulkActions, data]); + const alertBulkActions = useMemo( + () => ( + <> + {showBulkActions && ( + }> + + + )} + + ), + [ + additionalBulkActions, + filterQuery, + filterStatus, + indexNames, + onAlertStatusActionFailure, + onAlertStatusActionSuccess, + refetch, + showAlertStatusActions, + showBulkActions, + tableId, + totalItems, + ] + ); + return alertBulkActions; +}; diff --git a/x-pack/plugins/security_solution/public/common/components/events_viewer/use_timelines_events.tsx b/x-pack/plugins/security_solution/public/common/components/events_viewer/use_timelines_events.tsx new file mode 100644 index 0000000000000..c64bb7cae14cb --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/events_viewer/use_timelines_events.tsx @@ -0,0 +1,481 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import type { AlertConsumers } from '@kbn/rule-data-utils'; +import deepEqual from 'fast-deep-equal'; +import { isEmpty, isString, noop } from 'lodash/fp'; +import { useCallback, useEffect, useRef, useState } from 'react'; +import { useDispatch } from 'react-redux'; +import { Subscription } from 'rxjs'; +import type { DataView } from '@kbn/data-views-plugin/public'; +import type { DataPublicPluginStart } from '@kbn/data-plugin/public'; +import { isCompleteResponse, isErrorResponse } from '@kbn/data-plugin/common'; +import type { + Inspect, + PaginationInputPaginated, + TimelineEdges, + TimelineEventsAllRequestOptions, + TimelineEventsAllStrategyResponse, + TimelineItem, +} from '@kbn/timelines-plugin/common'; +import type { + EntityType, + TimelineFactoryQueryTypes, + TimelineRequestSortField, + TimelineStrategyResponseType, +} from '@kbn/timelines-plugin/common/search_strategy'; +import type { MappingRuntimeFields } from '@elastic/elasticsearch/lib/api/types'; +import { TimelineEventsQueries } from '../../../../common/search_strategy'; +import type { KueryFilterQueryKind } from '../../../../common/types'; +import { Direction, TableId } from '../../../../common/types'; +import type { ESQuery } from '../../../../common/typed_json'; +import { useAppToasts } from '../../hooks/use_app_toasts'; +import { dataTableActions } from '../../store/data_table'; +import { ERROR_TIMELINE_EVENTS } from './translations'; +import type { AlertWorkflowStatus } from '../../types'; +import { getSearchTransactionName, useStartTransaction } from '../../lib/apm/use_start_transaction'; +export type InspectResponse = Inspect & { response: string[] }; + +export const detectionsTimelineIds = [TableId.alertsOnAlertsPage, TableId.alertsOnRuleDetailsPage]; + +export type Refetch = () => void; + +export interface TimelineArgs { + consumers: Record; + events: TimelineItem[]; + id: string; + inspect: InspectResponse; + loadPage: LoadPage; + pageInfo: Pick; + refetch: Refetch; + totalCount: number; + updatedAt: number; +} + +type OnNextResponseHandler = (response: TimelineArgs) => Promise | void; + +type TimelineEventsSearchHandler = (onNextResponse?: OnNextResponseHandler) => void; + +type LoadPage = (newActivePage: number) => void; + +type TimelineRequest = TimelineEventsAllRequestOptions; + +type TimelineResponse = TimelineEventsAllStrategyResponse; + +export interface UseTimelineEventsProps { + alertConsumers?: AlertConsumers[]; + data?: DataPublicPluginStart; + dataViewId: string | null; + endDate: string; + entityType: EntityType; + excludeEcsData?: boolean; + fields: string[]; + filterQuery?: ESQuery | string; + id: string; + indexNames: string[]; + language?: KueryFilterQueryKind; + limit: number; + runtimeMappings: MappingRuntimeFields; + skip?: boolean; + sort?: TimelineRequestSortField[]; + startDate: string; + timerangeKind?: 'absolute' | 'relative'; + filterStatus?: AlertWorkflowStatus; +} + +const createFilter = (filterQuery: ESQuery | string | undefined) => + isString(filterQuery) ? filterQuery : JSON.stringify(filterQuery); + +const getTimelineEvents = (timelineEdges: TimelineEdges[]): TimelineItem[] => + timelineEdges.map((e: TimelineEdges) => e.node); + +const getInspectResponse = ( + response: TimelineStrategyResponseType, + prevResponse: InspectResponse +): InspectResponse => ({ + dsl: response?.inspect?.dsl ?? prevResponse?.dsl ?? [], + response: + response != null ? [JSON.stringify(response.rawResponse, null, 2)] : prevResponse?.response, +}); + +const ID = 'eventsQuery'; +export const initSortDefault = [ + { + direction: Direction.desc, + esTypes: ['date'], + field: '@timestamp', + type: 'date', + }, +]; + +const useApmTracking = (tableId: string) => { + const { startTransaction } = useStartTransaction(); + + const startTracking = useCallback(() => { + // Create the transaction, the managed flag is turned off to prevent it from being polluted by non-related automatic spans. + // The managed flag can be turned on to investigate high latency requests in APM. + // However, note that by enabling the managed flag, the transaction trace may be distorted by other requests information. + const transaction = startTransaction({ + name: getSearchTransactionName(tableId), + type: 'http-request', + options: { managed: false }, + }); + // Create a blocking span to control the transaction time and prevent it from closing automatically with partial batch responses. + // The blocking span needs to be ended manually when the batched request finishes. + const span = transaction?.startSpan('batched search', 'http-request', { blocking: true }); + return { + endTracking: (result: 'success' | 'error' | 'aborted' | 'invalid') => { + transaction?.addLabels({ result }); + span?.end(); + }, + }; + }, [startTransaction, tableId]); + + return { startTracking }; +}; + +const NO_CONSUMERS: AlertConsumers[] = []; +export const useTimelineEventsHandler = ({ + alertConsumers = NO_CONSUMERS, + dataViewId, + endDate, + entityType, + excludeEcsData = false, + id = ID, + indexNames, + fields, + filterQuery, + startDate, + language = 'kuery', + limit, + runtimeMappings, + sort = initSortDefault, + skip = false, + data, + filterStatus, +}: UseTimelineEventsProps): [boolean, TimelineArgs, TimelineEventsSearchHandler] => { + const dispatch = useDispatch(); + const { startTracking } = useApmTracking(id); + const refetch = useRef(noop); + const abortCtrl = useRef(new AbortController()); + const searchSubscription$ = useRef(new Subscription()); + const [loading, setLoading] = useState(true); + const [activePage, setActivePage] = useState(0); + const [timelineRequest, setTimelineRequest] = useState | null>( + null + ); + const [prevFilterStatus, setFilterStatus] = useState(filterStatus); + const prevTimelineRequest = useRef | null>(null); + + const clearSignalsState = useCallback(() => { + if (id != null && detectionsTimelineIds.some((timelineId) => timelineId === id)) { + dispatch(dataTableActions.clearEventsLoading({ id })); + dispatch(dataTableActions.clearEventsDeleted({ id })); + } + }, [dispatch, id]); + + const wrappedLoadPage = useCallback( + (newActivePage: number) => { + clearSignalsState(); + setActivePage(newActivePage); + }, + [clearSignalsState] + ); + + const refetchGrid = useCallback(() => { + if (refetch.current != null) { + refetch.current(); + } + wrappedLoadPage(0); + }, [wrappedLoadPage]); + + const setUpdated = useCallback( + (updatedAt: number) => { + dispatch(dataTableActions.setTableUpdatedAt({ id, updated: updatedAt })); + }, + [dispatch, id] + ); + + const setTotalCount = useCallback( + (totalCount: number) => dispatch(dataTableActions.updateTotalCount({ id, totalCount })), + [dispatch, id] + ); + + const [timelineResponse, setTimelineResponse] = useState({ + consumers: {}, + id, + inspect: { + dsl: [], + response: [], + }, + refetch: refetchGrid, + totalCount: -1, + pageInfo: { + activePage: 0, + querySize: 0, + }, + events: [], + loadPage: wrappedLoadPage, + updatedAt: 0, + }); + const { addWarning } = useAppToasts(); + + const timelineSearch = useCallback( + (request: TimelineRequest | null, onNextHandler?: OnNextResponseHandler) => { + if (request == null || skip) { + return; + } + + const asyncSearch = async () => { + prevTimelineRequest.current = request; + abortCtrl.current = new AbortController(); + setLoading(true); + if (data && data.search) { + const { endTracking } = startTracking(); + const abortSignal = abortCtrl.current.signal; + searchSubscription$.current = data.search + .search, TimelineResponse>( + { ...request, entityType }, + { + strategy: + request.language === 'eql' + ? 'timelineEqlSearchStrategy' + : 'timelineSearchStrategy', + abortSignal, + // we only need the id to throw better errors + indexPattern: { id: dataViewId } as unknown as DataView, + } + ) + .subscribe({ + next: (response) => { + if (isCompleteResponse(response)) { + setTimelineResponse((prevResponse) => { + const newTimelineResponse = { + ...prevResponse, + consumers: response.consumers, + events: getTimelineEvents(response.edges), + inspect: getInspectResponse(response, prevResponse.inspect), + pageInfo: response.pageInfo, + totalCount: response.totalCount, + updatedAt: Date.now(), + }; + setUpdated(newTimelineResponse.updatedAt); + setTotalCount(newTimelineResponse.totalCount); + if (onNextHandler) onNextHandler(newTimelineResponse); + return newTimelineResponse; + }); + if (prevFilterStatus !== request.filterStatus) { + dispatch(dataTableActions.updateGraphEventId({ id, graphEventId: '' })); + } + setFilterStatus(request.filterStatus); + setLoading(false); + + searchSubscription$.current.unsubscribe(); + } else if (isErrorResponse(response)) { + setLoading(false); + endTracking('invalid'); + addWarning(ERROR_TIMELINE_EVENTS); + searchSubscription$.current.unsubscribe(); + } + }, + error: (msg) => { + setLoading(false); + data.search.showError(msg); + searchSubscription$.current.unsubscribe(); + }, + }); + } + }; + + searchSubscription$.current.unsubscribe(); + abortCtrl.current.abort(); + asyncSearch(); + refetch.current = asyncSearch; + }, + [ + skip, + data, + setTotalCount, + entityType, + dataViewId, + setUpdated, + addWarning, + startTracking, + dispatch, + id, + prevFilterStatus, + ] + ); + + useEffect(() => { + if (indexNames.length === 0) { + return; + } + + setTimelineRequest((prevRequest) => { + const prevSearchParameters = { + defaultIndex: prevRequest?.defaultIndex ?? [], + filterQuery: prevRequest?.filterQuery ?? '', + querySize: prevRequest?.pagination.querySize ?? 0, + sort: prevRequest?.sort ?? initSortDefault, + timerange: prevRequest?.timerange ?? {}, + runtimeMappings: prevRequest?.runtimeMappings ?? {}, + filterStatus: prevRequest?.filterStatus, + }; + + const currentSearchParameters = { + defaultIndex: indexNames, + filterQuery: createFilter(filterQuery), + querySize: limit, + sort, + runtimeMappings, + timerange: { + interval: '12h', + from: startDate, + to: endDate, + }, + filterStatus, + }; + + const newActivePage = deepEqual(prevSearchParameters, currentSearchParameters) + ? activePage + : 0; + + const currentRequest = { + alertConsumers, + defaultIndex: indexNames, + excludeEcsData, + factoryQueryType: TimelineEventsQueries.all, + fieldRequested: fields, + fields: [], + filterQuery: createFilter(filterQuery), + pagination: { + activePage: newActivePage, + querySize: limit, + }, + language, + runtimeMappings, + sort, + timerange: { + interval: '12h', + from: startDate, + to: endDate, + }, + filterStatus, + }; + + if (activePage !== newActivePage) { + setActivePage(newActivePage); + } + if (!deepEqual(prevRequest, currentRequest)) { + return currentRequest; + } + return prevRequest; + }); + }, [ + alertConsumers, + dispatch, + indexNames, + activePage, + endDate, + excludeEcsData, + filterQuery, + id, + language, + limit, + startDate, + sort, + fields, + runtimeMappings, + filterStatus, + ]); + + const timelineEventsSearchHandler = useCallback( + (onNextHandler?: OnNextResponseHandler) => { + if (!deepEqual(prevTimelineRequest.current, timelineRequest)) { + timelineSearch(timelineRequest, onNextHandler); + } + }, + [timelineRequest, timelineSearch] + ); + + /* + cleanup timeline events response when the filters were removed completely + to avoid displaying previous query results + */ + useEffect(() => { + if (isEmpty(filterQuery)) { + setTimelineResponse({ + consumers: {}, + id, + inspect: { + dsl: [], + response: [], + }, + refetch: refetchGrid, + totalCount: -1, + pageInfo: { + activePage: 0, + querySize: 0, + }, + events: [], + loadPage: wrappedLoadPage, + updatedAt: 0, + }); + } + }, [filterQuery, id, refetchGrid, wrappedLoadPage]); + + return [loading, timelineResponse, timelineEventsSearchHandler]; +}; + +export const useTimelineEvents = ({ + alertConsumers = NO_CONSUMERS, + dataViewId, + endDate, + entityType, + excludeEcsData = false, + id = ID, + indexNames, + fields, + filterQuery, + filterStatus, + startDate, + language = 'kuery', + limit, + runtimeMappings, + sort = initSortDefault, + skip = false, + timerangeKind, + data, +}: UseTimelineEventsProps): [boolean, TimelineArgs] => { + const [loading, timelineResponse, timelineSearchHandler] = useTimelineEventsHandler({ + alertConsumers, + dataViewId, + endDate, + entityType, + excludeEcsData, + filterStatus, + id, + indexNames, + fields, + filterQuery, + startDate, + language, + limit, + runtimeMappings, + sort, + skip, + timerangeKind, + data, + }); + + useEffect(() => { + if (!timelineSearchHandler) return; + timelineSearchHandler(); + }, [timelineSearchHandler]); + + return [loading, timelineResponse]; +}; diff --git a/x-pack/plugins/security_solution/public/common/components/guided_onboarding_tour/README.md b/x-pack/plugins/security_solution/public/common/components/guided_onboarding_tour/README.md index a369669613bb4..9723d0ed92a76 100644 --- a/x-pack/plugins/security_solution/public/common/components/guided_onboarding_tour/README.md +++ b/x-pack/plugins/security_solution/public/common/components/guided_onboarding_tour/README.md @@ -27,13 +27,13 @@ It was important that the `EuiTourStep` **anchor** is in the DOM when the tour s ...defaultConfig, step: 1, title: i18n.translate('xpack.securitySolution.guided_onboarding.tour.ruleNameStep.tourTitle', { - defaultMessage: 'Test alert for practice', + defaultMessage: 'Examine the Alerts Table', }), content: i18n.translate( 'xpack.securitySolution.guided_onboarding.tour.ruleNameStep.tourContent', { defaultMessage: - 'To help you practice triaging alerts, we enabled a rule to create your first alert.', + 'To help you practice triaging alerts, here is the alert from the rule that we enabled in the previous step.', } ), anchorPosition: 'downCenter', diff --git a/x-pack/plugins/security_solution/public/common/components/guided_onboarding_tour/tour_config.ts b/x-pack/plugins/security_solution/public/common/components/guided_onboarding_tour/tour_config.ts index 6b0300ed80110..ca52149bfc329 100644 --- a/x-pack/plugins/security_solution/public/common/components/guided_onboarding_tour/tour_config.ts +++ b/x-pack/plugins/security_solution/public/common/components/guided_onboarding_tour/tour_config.ts @@ -64,13 +64,13 @@ const alertsCasesConfig: StepConfig[] = [ ...defaultConfig, step: AlertsCasesTourSteps.pointToAlertName, title: i18n.translate('xpack.securitySolution.guided_onboarding.tour.ruleNameStep.tourTitle', { - defaultMessage: 'Test alert for practice', + defaultMessage: 'Examine the Alerts Table', }), content: i18n.translate( 'xpack.securitySolution.guided_onboarding.tour.ruleNameStep.tourContent', { defaultMessage: - 'To help you practice triaging alerts, we enabled a rule to create your first alert.', + 'To help you practice triaging alerts, here is the alert from the rule that we enabled in the previous step.', } ), anchorPosition: 'downCenter', @@ -105,8 +105,7 @@ const alertsCasesConfig: StepConfig[] = [ content: i18n.translate( 'xpack.securitySolution.guided_onboarding.tour.flyoutOverview.tourContent', { - defaultMessage: - 'Learn more about alerts by checking out all the information available on each tab.', + defaultMessage: 'Learn more about alerts by checking out all the information available.', } ), // needs to use anchor to properly place tour step @@ -128,7 +127,7 @@ const alertsCasesConfig: StepConfig[] = [ defaultMessage: 'Create a case', }), content: i18n.translate('xpack.securitySolution.guided_onboarding.tour.addToCase.tourContent', { - defaultMessage: 'From the Take action menu, add the alert to a new case.', + defaultMessage: 'From the Take action menu, select "Add to new case".', }), anchorPosition: 'upRight', dataTestSubj: getTourAnchor(AlertsCasesTourSteps.addAlertToCase, SecurityStepId.alertsCases), @@ -137,12 +136,12 @@ const alertsCasesConfig: StepConfig[] = [ ...defaultConfig, step: AlertsCasesTourSteps.createCase, title: i18n.translate('xpack.securitySolution.guided_onboarding.tour.createCase.tourTitle', { - defaultMessage: `Add details`, + defaultMessage: `Add Case details`, }), content: i18n.translate( 'xpack.securitySolution.guided_onboarding.tour.createCase.tourContent', { - defaultMessage: `In addition to the alert, you can add any relevant information you need to the case.`, + defaultMessage: `Provide the relevant information to create the case. We have included sample text for you.`, } ), anchor: `[tour-step="create-case-flyout"] label`, @@ -155,12 +154,12 @@ const alertsCasesConfig: StepConfig[] = [ ...defaultConfig, step: AlertsCasesTourSteps.submitCase, title: i18n.translate('xpack.securitySolution.guided_onboarding.tour.submitCase.tourTitle', { - defaultMessage: `Submit case`, + defaultMessage: `Create a case`, }), content: i18n.translate( 'xpack.securitySolution.guided_onboarding.tour.submitCase.tourContent', { - defaultMessage: `Press Create case to advance the tour.`, + defaultMessage: `Press "Create case" to continue.`, } ), anchor: `[tour-step="create-case-flyout"] [tour-step="create-case-submit"]`, @@ -178,22 +177,22 @@ const alertsCasesConfig: StepConfig[] = [ defaultMessage: 'View the case', }), content: i18n.translate('xpack.securitySolution.guided_onboarding.tour.viewCase.tourContent', { - defaultMessage: 'From the Insights, click through to view the new case', + defaultMessage: 'Cases are shown under Insights, in the alert details.', }), - anchorPosition: 'leftUp', + anchorPosition: 'rightUp', dataTestSubj: getTourAnchor(AlertsCasesTourSteps.viewCase, SecurityStepId.alertsCases), }, ]; export const sampleCase = { title: i18n.translate('xpack.securitySolution.guided_onboarding.tour.createCase.title', { - defaultMessage: `Demo signal detected`, + defaultMessage: `This is a test case`, }), description: i18n.translate( 'xpack.securitySolution.guided_onboarding.tour.createCase.description', { defaultMessage: - "This is where you'd document a malicious signal. You can include whatever information is relevant to the case and would be helpful for anyone else that needs to read up on it. `Markdown` **formatting** _is_ [supported](https://www.markdownguide.org/cheat-sheet/).", + 'Add a description and other relevant information. The alert will be added to the case.', } ), }; diff --git a/x-pack/plugins/security_solution/public/common/components/guided_onboarding_tour/tour_step.test.tsx b/x-pack/plugins/security_solution/public/common/components/guided_onboarding_tour/tour_step.test.tsx index 1673d9072fe1b..b30e72a6024f0 100644 --- a/x-pack/plugins/security_solution/public/common/components/guided_onboarding_tour/tour_step.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/guided_onboarding_tour/tour_step.test.tsx @@ -13,7 +13,6 @@ import { useTourContext } from './tour'; import { mockGlobalState, SUB_PLUGINS_REDUCER, TestProviders } from '../../mock'; import { TimelineId } from '../../../../common/types'; import { createStore } from '../../store'; -import { tGridReducer } from '@kbn/timelines-plugin/public'; import { kibanaObservable } from '@kbn/timelines-plugin/public/mock'; import { createSecuritySolutionStorageMock } from '@kbn/timelines-plugin/public/mock/mock_local_storage'; @@ -274,13 +273,7 @@ describe('SecurityTourStep', () => { }, }; const { storage } = createSecuritySolutionStorageMock(); - const mockStore = createStore( - mockstate, - SUB_PLUGINS_REDUCER, - { dataTable: tGridReducer }, - kibanaObservable, - storage - ); + const mockStore = createStore(mockstate, SUB_PLUGINS_REDUCER, kibanaObservable, storage); render( diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/action_icon_item.tsx b/x-pack/plugins/security_solution/public/common/components/header_actions/action_icon_item.tsx similarity index 94% rename from x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/action_icon_item.tsx rename to x-pack/plugins/security_solution/public/common/components/header_actions/action_icon_item.tsx index 9d74310cd807e..33ce078fe31df 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/action_icon_item.tsx +++ b/x-pack/plugins/security_solution/public/common/components/header_actions/action_icon_item.tsx @@ -8,9 +8,8 @@ import type { MouseEvent } from 'react'; import React from 'react'; import { EuiContextMenuItem, EuiButtonIcon, EuiToolTip, EuiText } from '@elastic/eui'; - -import { DEFAULT_ACTION_BUTTON_WIDTH } from '@kbn/timelines-plugin/public'; -import { EventsTdContent } from '../../styles'; +import { EventsTdContent } from '../../../timelines/components/timeline/styles'; +import { DEFAULT_ACTION_BUTTON_WIDTH } from '.'; interface ActionIconItemProps { ariaLabel?: string; diff --git a/x-pack/plugins/security_solution/public/common/components/header_actions/actions.test.tsx b/x-pack/plugins/security_solution/public/common/components/header_actions/actions.test.tsx new file mode 100644 index 0000000000000..46adaf05ed05b --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/header_actions/actions.test.tsx @@ -0,0 +1,440 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor 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 { mount } from 'enzyme'; +import React from 'react'; +import { mockCasesContract } from '@kbn/cases-plugin/public/mocks'; +import { mockTimelineData, mockTimelineModel, TestProviders } from '../../mock'; +import { useShallowEqualSelector } from '../../hooks/use_selector'; +import { useIsExperimentalFeatureEnabled } from '../../hooks/use_experimental_features'; +import { licenseService } from '../../hooks/use_license'; +import { TableId } from '../../../../common/types'; +import { useTourContext } from '../guided_onboarding_tour'; +import { GuidedOnboardingTourStep, SecurityTourStep } from '../guided_onboarding_tour/tour_step'; +import { SecurityStepId } from '../guided_onboarding_tour/tour_config'; +import { Actions } from './actions'; +import { initialUserPrivilegesState as mockInitialUserPrivilegesState } from '../user_privileges/user_privileges_context'; +import { useUserPrivileges } from '../user_privileges'; + +jest.mock('../guided_onboarding_tour'); +jest.mock('../user_privileges'); +jest.mock('../../../detections/components/user_info', () => ({ + useUserData: jest.fn().mockReturnValue([{ canUserCRUD: true, hasIndexWrite: true }]), +})); +jest.mock('../../hooks/use_experimental_features', () => ({ + useIsExperimentalFeatureEnabled: jest.fn().mockReturnValue(false), +})); +jest.mock('../../hooks/use_selector'); +jest.mock( + '../../../detections/components/alerts_table/timeline_actions/use_investigate_in_timeline', + () => ({ + useInvestigateInTimeline: jest.fn().mockReturnValue({ + investigateInTimelineActionItems: [], + investigateInTimelineAlertClick: jest.fn(), + showInvestigateInTimelineAction: false, + }), + }) +); + +jest.mock('../../lib/kibana', () => { + const originalKibanaLib = jest.requireActual('../../lib/kibana'); + + return { + useKibana: () => ({ + services: { + application: { + navigateToApp: jest.fn(), + getUrlForApp: jest.fn(), + capabilities: { + siem: { crud_alerts: true, read_alerts: true }, + }, + }, + cases: mockCasesContract(), + uiSettings: { + get: jest.fn(), + }, + savedObjects: { + client: {}, + }, + }, + }), + useToasts: jest.fn().mockReturnValue({ + addError: jest.fn(), + addSuccess: jest.fn(), + addWarning: jest.fn(), + remove: jest.fn(), + }), + useNavigateTo: jest.fn().mockReturnValue({ + navigateTo: jest.fn(), + }), + useGetUserCasesPermissions: originalKibanaLib.useGetUserCasesPermissions, + }; +}); + +jest.mock('../../hooks/use_license', () => { + const licenseServiceInstance = { + isPlatinumPlus: jest.fn(), + isEnterprise: jest.fn(() => false), + }; + return { + licenseService: licenseServiceInstance, + useLicense: () => { + return licenseServiceInstance; + }, + }; +}); + +const defaultProps = { + ariaRowindex: 2, + checked: false, + columnId: '', + columnValues: 'abc def', + data: mockTimelineData[0].data, + ecsData: mockTimelineData[0].ecs, + eventId: 'abc', + eventIdToNoteIds: {}, + index: 2, + isEventPinned: false, + loadingEventIds: [], + onEventDetailsPanelOpened: () => {}, + onRowSelected: () => {}, + refetch: () => {}, + rowIndex: 10, + setEventsDeleted: () => {}, + setEventsLoading: () => {}, + showCheckboxes: true, + showNotes: false, + timelineId: 'test', + toggleShowNotes: () => {}, +}; + +describe('Actions', () => { + beforeAll(() => { + (useTourContext as jest.Mock).mockReturnValue({ + activeStep: 1, + incrementStep: () => null, + isTourShown: () => false, + }); + (useShallowEqualSelector as jest.Mock).mockReturnValue(mockTimelineModel); + }); + + test('it renders a checkbox for selecting the event when `showCheckboxes` is `true`', () => { + const wrapper = mount( + + + + ); + + expect(wrapper.find('[data-test-subj="select-event"]').exists()).toEqual(true); + }); + + test('it does NOT render a checkbox for selecting the event when `showCheckboxes` is `false`', () => { + const wrapper = mount( + + + + ); + + expect(wrapper.find('[data-test-subj="select-event"]').exists()).toBe(false); + }); + + test('it does NOT render a checkbox for selecting the event when `tGridEnabled` is `true`', () => { + (useIsExperimentalFeatureEnabled as jest.Mock).mockReturnValue(true); + const wrapper = mount( + + + + ); + + expect(wrapper.find('[data-test-subj="select-event"]').exists()).toBe(false); + }); + + describe('Guided Onboarding Step', () => { + const incrementStepMock = jest.fn(); + beforeEach(() => { + (useTourContext as jest.Mock).mockReturnValue({ + activeStep: 2, + incrementStep: incrementStepMock, + isTourShown: () => true, + }); + jest.clearAllMocks(); + }); + + const ecsData = { + ...mockTimelineData[0].ecs, + kibana: { alert: { rule: { uuid: ['123'], parameters: {} } } }, + }; + const isTourAnchorConditions: { [key: string]: unknown } = { + ecsData, + timelineId: TableId.alertsOnAlertsPage, + ariaRowindex: 1, + }; + + test('if isTourShown is false [isTourAnchor=false], SecurityTourStep is not active', () => { + (useTourContext as jest.Mock).mockReturnValue({ + activeStep: 2, + incrementStep: jest.fn(), + isTourShown: () => false, + }); + + const wrapper = mount( + + + + ); + + expect(wrapper.find(GuidedOnboardingTourStep).exists()).toEqual(true); + expect(wrapper.find(SecurityTourStep).exists()).toEqual(false); + }); + + test('if all conditions make isTourAnchor=true, SecurityTourStep is active', () => { + const wrapper = mount( + + + + ); + + expect(wrapper.find(GuidedOnboardingTourStep).exists()).toEqual(true); + expect(wrapper.find(SecurityTourStep).exists()).toEqual(true); + }); + + test('on expand event click and SecurityTourStep is active, incrementStep', () => { + const wrapper = mount( + + + + ); + + wrapper.find('[data-test-subj="expand-event"]').first().simulate('click'); + + expect(incrementStepMock).toHaveBeenCalledWith(SecurityStepId.alertsCases); + }); + + test('on expand event click and SecurityTourStep is active, but step is not 2, do not incrementStep', () => { + (useTourContext as jest.Mock).mockReturnValue({ + activeStep: 1, + incrementStep: incrementStepMock, + isTourShown: () => true, + }); + + const wrapper = mount( + + + + ); + + wrapper.find('[data-test-subj="expand-event"]').first().simulate('click'); + + expect(incrementStepMock).not.toHaveBeenCalled(); + }); + + test('on expand event click and SecurityTourStep is not active, do not incrementStep', () => { + const wrapper = mount( + + + + ); + + wrapper.find('[data-test-subj="expand-event"]').first().simulate('click'); + + expect(incrementStepMock).not.toHaveBeenCalled(); + }); + + test('if isTourAnchor=false, SecurityTourStep is not active', () => { + const wrapper = mount( + + + + ); + + expect(wrapper.find(GuidedOnboardingTourStep).exists()).toEqual(true); + expect(wrapper.find(SecurityTourStep).exists()).toEqual(false); + }); + describe.each(Object.keys(isTourAnchorConditions))('tour condition true: %s', (key: string) => { + it('Single condition does not make tour step exist', () => { + const wrapper = mount( + + + + ); + + expect(wrapper.find(GuidedOnboardingTourStep).exists()).toEqual(true); + expect(wrapper.find(SecurityTourStep).exists()).toEqual(false); + }); + }); + }); + + describe('Alert context menu enabled?', () => { + beforeEach(() => { + (useUserPrivileges as jest.Mock).mockReturnValue({ + ...mockInitialUserPrivilegesState(), + endpointPrivileges: { loading: false, canWriteEventFilters: true }, + }); + }); + test('it disables for eventType=raw', () => { + const wrapper = mount( + + + + ); + + expect( + wrapper.find('[data-test-subj="timeline-context-menu-button"]').first().prop('isDisabled') + ).toBe(true); + }); + test('it enables for eventType=signal', () => { + const ecsData = { + ...mockTimelineData[0].ecs, + kibana: { alert: { rule: { uuid: ['123'], parameters: {} } } }, + }; + const wrapper = mount( + + + + ); + + expect( + wrapper.find('[data-test-subj="timeline-context-menu-button"]').first().prop('isDisabled') + ).toBe(false); + }); + test('it disables for event.kind: undefined and agent.type: endpoint', () => { + const ecsData = { + ...mockTimelineData[0].ecs, + agent: { type: ['endpoint'] }, + }; + const wrapper = mount( + + + + ); + + expect( + wrapper.find('[data-test-subj="timeline-context-menu-button"]').first().prop('isDisabled') + ).toBe(true); + }); + test('it enables for event.kind: event and agent.type: endpoint', () => { + const ecsData = { + ...mockTimelineData[0].ecs, + event: { kind: ['event'] }, + agent: { type: ['endpoint'] }, + }; + const wrapper = mount( + + + + ); + + expect( + wrapper.find('[data-test-subj="timeline-context-menu-button"]').first().prop('isDisabled') + ).toBe(false); + }); + test('it disables for event.kind: alert and agent.type: endpoint', () => { + const ecsData = { + ...mockTimelineData[0].ecs, + event: { kind: ['alert'] }, + agent: { type: ['endpoint'] }, + }; + const wrapper = mount( + + + + ); + + expect( + wrapper.find('[data-test-subj="timeline-context-menu-button"]').first().prop('isDisabled') + ).toBe(true); + }); + test('it shows the analyze event button when the event is from an endpoint', () => { + const ecsData = { + ...mockTimelineData[0].ecs, + event: { kind: ['alert'] }, + agent: { type: ['endpoint'] }, + process: { entity_id: ['1'] }, + }; + const wrapper = mount( + + + + ); + + expect(wrapper.find('[data-test-subj="view-in-analyzer"]').exists()).toBe(true); + }); + test('it does not render the analyze event button when the event is from an unsupported source', () => { + const ecsData = { + ...mockTimelineData[0].ecs, + event: { kind: ['alert'] }, + agent: { type: ['notendpoint'] }, + }; + const wrapper = mount( + + + + ); + + expect(wrapper.find('[data-test-subj="view-in-analyzer"]').exists()).toBe(false); + }); + + test('it should not show session view button on action tabs for basic users', () => { + const ecsData = { + ...mockTimelineData[0].ecs, + event: { kind: ['alert'] }, + agent: { type: ['endpoint'] }, + process: { entry_leader: { entity_id: ['test_id'] } }, + }; + + const wrapper = mount( + + + + ); + + expect(wrapper.find('[data-test-subj="session-view-button"]').exists()).toEqual(false); + }); + + test('it should show session view button on action tabs when user access the session viewer via K8S dashboard', () => { + const ecsData = { + ...mockTimelineData[0].ecs, + event: { kind: ['alert'] }, + agent: { type: ['endpoint'] }, + process: { entry_leader: { entity_id: ['test_id'] } }, + }; + + const wrapper = mount( + + + + ); + + expect(wrapper.find('[data-test-subj="session-view-button"]').exists()).toEqual(true); + }); + + test('it should show session view button on action tabs for enterprise users', () => { + const licenseServiceMock = licenseService as jest.Mocked; + + licenseServiceMock.isEnterprise.mockReturnValue(true); + + const ecsData = { + ...mockTimelineData[0].ecs, + event: { kind: ['alert'] }, + agent: { type: ['endpoint'] }, + process: { entry_leader: { entity_id: ['test_id'] } }, + }; + + const wrapper = mount( + + + + ); + + expect(wrapper.find('[data-test-subj="session-view-button"]').exists()).toEqual(true); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/components/header_actions/actions.tsx b/x-pack/plugins/security_solution/public/common/components/header_actions/actions.tsx new file mode 100644 index 0000000000000..4ca8f2d0fae28 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/header_actions/actions.tsx @@ -0,0 +1,344 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useCallback, useMemo } from 'react'; +import { useDispatch } from 'react-redux'; +import { EuiButtonIcon, EuiCheckbox, EuiLoadingSpinner, EuiToolTip } from '@elastic/eui'; +import styled from 'styled-components'; + +import { + eventHasNotes, + getEventType, + getPinOnClick, +} from '../../../timelines/components/timeline/body/helpers'; +import { getScopedActions, isTimelineScope } from '../../../helpers'; +import { isInvestigateInResolverActionEnabled } from '../../../detections/components/alerts_table/timeline_actions/investigate_in_resolver'; +import { timelineActions, timelineSelectors } from '../../../timelines/store/timeline'; +import type { ActionProps, OnPinEvent } from '../../../../common/types'; +import { TableId, TimelineId, TimelineTabs } from '../../../../common/types'; +import { AddEventNoteAction } from './add_note_icon_item'; +import { PinEventAction } from './pin_event_action'; +import { useShallowEqualSelector } from '../../hooks/use_selector'; +import { timelineDefaults } from '../../../timelines/store/timeline/defaults'; +import { useStartTransaction } from '../../lib/apm/use_start_transaction'; +import { useLicense } from '../../hooks/use_license'; +import { useGlobalFullScreen, useTimelineFullScreen } from '../../containers/use_full_screen'; +import { ALERTS_ACTIONS } from '../../lib/apm/user_actions'; +import { setActiveTabTimeline } from '../../../timelines/store/timeline/actions'; +import { EventsTdContent } from '../../../timelines/components/timeline/styles'; +import { useIsExperimentalFeatureEnabled } from '../../hooks/use_experimental_features'; +import { AlertContextMenu } from '../../../detections/components/alerts_table/timeline_actions/alert_context_menu'; +import { InvestigateInTimelineAction } from '../../../detections/components/alerts_table/timeline_actions/investigate_in_timeline_action'; +import * as i18n from './translations'; +import { useTourContext } from '../guided_onboarding_tour'; +import { AlertsCasesTourSteps, SecurityStepId } from '../guided_onboarding_tour/tour_config'; +import { isDetectionsAlertsTable } from '../top_n/helpers'; +import { GuidedOnboardingTourStep } from '../guided_onboarding_tour/tour_step'; +import { DEFAULT_ACTION_BUTTON_WIDTH, isAlert } from './helpers'; + +const ActionsContainer = styled.div` + align-items: center; + display: flex; +`; + +const ActionsComponent: React.FC = ({ + ariaRowindex, + checked, + columnValues, + ecsData, + eventId, + eventIdToNoteIds, + isEventPinned = false, + isEventViewer = false, + loadingEventIds, + onEventDetailsPanelOpened, + onRowSelected, + onRuleChange, + showCheckboxes, + showNotes, + timelineId, + toggleShowNotes, +}) => { + const dispatch = useDispatch(); + const tGridEnabled = useIsExperimentalFeatureEnabled('tGridEnabled'); + const emptyNotes: string[] = []; + const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []); + const timelineType = useShallowEqualSelector( + (state) => + (isTimelineScope(timelineId) ? getTimeline(state, timelineId) : timelineDefaults).timelineType + ); + const { startTransaction } = useStartTransaction(); + + const isEnterprisePlus = useLicense().isEnterprise(); + + const onPinEvent: OnPinEvent = useCallback( + (evtId) => dispatch(timelineActions.pinEvent({ id: timelineId, eventId: evtId })), + [dispatch, timelineId] + ); + + const onUnPinEvent: OnPinEvent = useCallback( + (evtId) => dispatch(timelineActions.unPinEvent({ id: timelineId, eventId: evtId })), + [dispatch, timelineId] + ); + + const handleSelectEvent = useCallback( + (event: React.ChangeEvent) => + onRowSelected({ + eventIds: [eventId], + isSelected: event.currentTarget.checked, + }), + [eventId, onRowSelected] + ); + + const handlePinClicked = useCallback( + () => + getPinOnClick({ + allowUnpinning: eventIdToNoteIds ? !eventHasNotes(eventIdToNoteIds[eventId]) : true, + eventId, + onPinEvent, + onUnPinEvent, + isEventPinned, + }), + [eventIdToNoteIds, eventId, isEventPinned, onPinEvent, onUnPinEvent] + ); + const eventType = getEventType(ecsData); + + const isContextMenuDisabled = useMemo(() => { + return ( + eventType !== 'signal' && + !(ecsData.event?.kind?.includes('event') && ecsData.agent?.type?.includes('endpoint')) + ); + }, [ecsData, eventType]); + + const isDisabled = useMemo(() => !isInvestigateInResolverActionEnabled(ecsData), [ecsData]); + const { setGlobalFullScreen } = useGlobalFullScreen(); + const { setTimelineFullScreen } = useTimelineFullScreen(); + const scopedActions = getScopedActions(timelineId); + const handleClick = useCallback(() => { + startTransaction({ name: ALERTS_ACTIONS.OPEN_ANALYZER }); + + const dataGridIsFullScreen = document.querySelector('.euiDataGrid--fullScreen'); + if (scopedActions) { + dispatch(scopedActions.updateGraphEventId({ id: timelineId, graphEventId: ecsData._id })); + } + if (timelineId === TimelineId.active) { + if (dataGridIsFullScreen) { + setTimelineFullScreen(true); + } + dispatch(setActiveTabTimeline({ id: timelineId, activeTab: TimelineTabs.graph })); + } else { + if (dataGridIsFullScreen) { + setGlobalFullScreen(true); + } + } + }, [ + startTransaction, + scopedActions, + timelineId, + dispatch, + ecsData._id, + setTimelineFullScreen, + setGlobalFullScreen, + ]); + + const sessionViewConfig = useMemo(() => { + const { process, _id, timestamp } = ecsData; + const sessionEntityId = process?.entry_leader?.entity_id?.[0]; + + if (sessionEntityId === undefined) { + return null; + } + + const jumpToEntityId = process?.entity_id?.[0]; + const investigatedAlertId = eventType === 'signal' || eventType === 'eql' ? _id : undefined; + const jumpToCursor = + (investigatedAlertId && ecsData.kibana?.alert.original_time?.[0]) || timestamp; + + return { + sessionEntityId, + jumpToEntityId, + jumpToCursor, + investigatedAlertId, + }; + }, [ecsData, eventType]); + + const openSessionView = useCallback(() => { + const dataGridIsFullScreen = document.querySelector('.euiDataGrid--fullScreen'); + startTransaction({ name: ALERTS_ACTIONS.OPEN_SESSION_VIEW }); + + if (timelineId === TimelineId.active) { + if (dataGridIsFullScreen) { + setTimelineFullScreen(true); + } + if (sessionViewConfig !== null) { + dispatch(setActiveTabTimeline({ id: timelineId, activeTab: TimelineTabs.session })); + } + } else { + if (dataGridIsFullScreen) { + setGlobalFullScreen(true); + } + } + if (sessionViewConfig !== null) { + if (scopedActions) { + dispatch(scopedActions.updateSessionViewConfig({ id: timelineId, sessionViewConfig })); + } + } + }, [ + startTransaction, + timelineId, + sessionViewConfig, + setTimelineFullScreen, + dispatch, + setGlobalFullScreen, + scopedActions, + ]); + + const { activeStep, isTourShown, incrementStep } = useTourContext(); + + const isTourAnchor = useMemo( + () => + isTourShown(SecurityStepId.alertsCases) && + eventType === 'signal' && + isDetectionsAlertsTable(timelineId) && + ariaRowindex === 1, + [isTourShown, ariaRowindex, eventType, timelineId] + ); + + const onExpandEvent = useCallback(() => { + if ( + isTourAnchor && + activeStep === AlertsCasesTourSteps.expandEvent && + isTourShown(SecurityStepId.alertsCases) + ) { + incrementStep(SecurityStepId.alertsCases); + } + onEventDetailsPanelOpened(); + }, [activeStep, incrementStep, isTourAnchor, isTourShown, onEventDetailsPanelOpened]); + + return ( + + {showCheckboxes && !tGridEnabled && ( +
    + + {loadingEventIds.includes(eventId) ? ( + + ) : ( + + )} + +
    + )} + +
    + + + + + +
    +
    + <> + {timelineId !== TimelineId.active && ( + + )} + + {!isEventViewer && toggleShowNotes && ( + <> + + + + )} + + {isDisabled === false ? ( +
    + + + + + +
    + ) : null} + {sessionViewConfig !== null && + (isEnterprisePlus || timelineId === TableId.kubernetesPageSessions) ? ( +
    + + + + + +
    + ) : null} + +
    + ); +}; + +ActionsComponent.displayName = 'ActionsComponent'; + +export const Actions = React.memo(ActionsComponent); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/add_note_icon_item.test.tsx b/x-pack/plugins/security_solution/public/common/components/header_actions/add_note_icon_item.test.tsx similarity index 82% rename from x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/add_note_icon_item.test.tsx rename to x-pack/plugins/security_solution/public/common/components/header_actions/add_note_icon_item.test.tsx index 178dbe9804ac0..0bb7e575d3c87 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/add_note_icon_item.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/header_actions/add_note_icon_item.test.tsx @@ -5,16 +5,16 @@ * 2.0. */ +import { TimelineType } from '../../../../common/types'; import { render, screen } from '@testing-library/react'; import React from 'react'; +import { TestProviders } from '../../mock'; +import { useUserPrivileges } from '../user_privileges'; +import { getEndpointPrivilegesInitialStateMock } from '../user_privileges/endpoint/mocks'; import { AddEventNoteAction } from './add_note_icon_item'; -import { useUserPrivileges } from '../../../../../common/components/user_privileges'; -import { getEndpointPrivilegesInitialStateMock } from '../../../../../common/components/user_privileges/endpoint/mocks'; -import { TestProviders } from '../../../../../common/mock'; -import { TimelineType } from '../../../../../../common/types'; -jest.mock('../../../../../common/components/user_privileges'); +jest.mock('../user_privileges'); const useUserPrivilegesMock = useUserPrivileges as jest.Mock; describe('AddEventNoteAction', () => { diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/add_note_icon_item.tsx b/x-pack/plugins/security_solution/public/common/components/header_actions/add_note_icon_item.tsx similarity index 83% rename from x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/add_note_icon_item.tsx rename to x-pack/plugins/security_solution/public/common/components/header_actions/add_note_icon_item.tsx index 784685997e5ff..43269f2dc269d 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/add_note_icon_item.tsx +++ b/x-pack/plugins/security_solution/public/common/components/header_actions/add_note_icon_item.tsx @@ -6,12 +6,11 @@ */ import React from 'react'; - -import { TimelineType } from '../../../../../../common/types/timeline'; -import * as i18n from '../translations'; -import { NotesButton } from '../../properties/helpers'; +import { NotesButton } from '../../../timelines/components/timeline/properties/helpers'; +import { TimelineType } from '../../../../common/types'; +import { useUserPrivileges } from '../user_privileges'; +import * as i18n from './translations'; import { ActionIconItem } from './action_icon_item'; -import { useUserPrivileges } from '../../../../../common/components/user_privileges'; interface AddEventNoteActionProps { ariaLabel?: string; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/header_actions.test.tsx b/x-pack/plugins/security_solution/public/common/components/header_actions/header_actions.test.tsx similarity index 83% rename from x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/header_actions.test.tsx rename to x-pack/plugins/security_solution/public/common/components/header_actions/header_actions.test.tsx index a851c90405994..6a8f06696bada 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/header_actions.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/header_actions/header_actions.test.tsx @@ -7,19 +7,15 @@ import React from 'react'; import { render } from '@testing-library/react'; - -import { TestProviders, mockTimelineModel } from '../../../../../common/mock'; +import { mockTimelineModel, TestProviders } from '../../mock'; +import { mockTriggersActionsUi } from '../../mock/mock_triggers_actions_ui_plugin'; +import type { ColumnHeaderOptions, HeaderActionProps } from '../../../../common/types'; +import { TimelineTabs } from '../../../../common/types'; import { HeaderActions } from './header_actions'; -import { mockTriggersActionsUi } from '../../../../../common/mock/mock_triggers_actions_ui_plugin'; -import type { - ColumnHeaderOptions, - HeaderActionProps, -} from '../../../../../../common/types/timeline'; -import { TimelineId, TimelineTabs } from '../../../../../../common/types/timeline'; -import { timelineActions } from '../../../../store/timeline'; -import { getColumnHeader } from '../column_headers/helpers'; - -jest.mock('../../../row_renderers_browser', () => ({ +import { timelineActions } from '../../../timelines/store/timeline'; +import { getColumnHeader } from '../../../timelines/components/timeline/body/column_headers/helpers'; + +jest.mock('../../../timelines/components/row_renderers_browser', () => ({ StatefulRowRenderersBrowser: () => null, })); @@ -29,13 +25,13 @@ jest.mock('react-redux', () => ({ useDispatch: () => mockDispatch, })); -jest.mock('../../../../../common/hooks/use_selector', () => ({ +jest.mock('../../hooks/use_selector', () => ({ useDeepEqualSelector: () => mockTimelineModel, useShallowEqualSelector: jest.fn(), })); const columnId = 'test-field'; -const timelineId = TimelineId.test; +const timelineId = 'test-timeline'; /* eslint-disable jsx-a11y/click-events-have-key-events */ mockTriggersActionsUi.getFieldBrowser.mockImplementation( @@ -53,7 +49,7 @@ mockTriggersActionsUi.getFieldBrowser.mockImplementation( ) ); -jest.mock('../../../../../common/lib/kibana', () => ({ +jest.mock('../../lib/kibana', () => ({ useKibana: () => ({ services: { triggersActionsUi: { ...mockTriggersActionsUi }, diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/header_actions.tsx b/x-pack/plugins/security_solution/public/common/components/header_actions/header_actions.tsx similarity index 87% rename from x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/header_actions.tsx rename to x-pack/plugins/security_solution/public/common/components/header_actions/header_actions.tsx index 3b5761b18d92f..51b6634547719 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/header_actions.tsx +++ b/x-pack/plugins/security_solution/public/common/components/header_actions/header_actions.tsx @@ -11,25 +11,22 @@ import { EuiButtonIcon, EuiCheckbox, EuiToolTip, useDataGridColumnSorting } from import { useDispatch } from 'react-redux'; import styled from 'styled-components'; -import { DEFAULT_ACTION_BUTTON_WIDTH } from '@kbn/timelines-plugin/public'; -import { isActiveTimeline } from '../../../../../helpers'; -import type { HeaderActionProps, SortDirection } from '../../../../../../common/types/timeline'; -import { TimelineId, TimelineTabs } from '../../../../../../common/types/timeline'; -import { EXIT_FULL_SCREEN } from '../../../../../common/components/exit_full_screen/translations'; -import { FULL_SCREEN_TOGGLED_CLASS_NAME } from '../../../../../../common/constants'; -import { - useGlobalFullScreen, - useTimelineFullScreen, -} from '../../../../../common/containers/use_full_screen'; -import { useDeepEqualSelector } from '../../../../../common/hooks/use_selector'; -import { StatefulRowRenderersBrowser } from '../../../row_renderers_browser'; -import { EventsTh, EventsThContent } from '../../styles'; -import { EventsSelect } from '../column_headers/events_select'; -import * as i18n from '../column_headers/translations'; -import { timelineActions, timelineSelectors } from '../../../../store/timeline'; -import { isFullScreen } from '../column_headers'; -import { useKibana } from '../../../../../common/lib/kibana'; -import { getColumnHeader } from '../column_headers/helpers'; +import type { HeaderActionProps, SortDirection } from '../../../../common/types'; +import { TimelineTabs, TimelineId } from '../../../../common/types'; +import { isFullScreen } from '../../../timelines/components/timeline/body/column_headers'; +import { isActiveTimeline } from '../../../helpers'; +import { getColumnHeader } from '../../../timelines/components/timeline/body/column_headers/helpers'; +import { timelineActions, timelineSelectors } from '../../../timelines/store/timeline'; +import { useDeepEqualSelector } from '../../hooks/use_selector'; +import { useGlobalFullScreen, useTimelineFullScreen } from '../../containers/use_full_screen'; +import { useKibana } from '../../lib/kibana'; +import { DEFAULT_ACTION_BUTTON_WIDTH } from '.'; +import { EventsTh, EventsThContent } from '../../../timelines/components/timeline/styles'; +import { StatefulRowRenderersBrowser } from '../../../timelines/components/row_renderers_browser'; +import { EXIT_FULL_SCREEN } from '../exit_full_screen/translations'; +import { FULL_SCREEN_TOGGLED_CLASS_NAME } from '../../../../common/constants'; +import { EventsSelect } from '../../../timelines/components/timeline/body/column_headers/events_select'; +import * as i18n from './translations'; const SortingColumnsContainer = styled.div` button { diff --git a/x-pack/plugins/security_solution/public/common/components/header_actions/helpers.test.ts b/x-pack/plugins/security_solution/public/common/components/header_actions/helpers.test.ts new file mode 100644 index 0000000000000..6edd44ba74eb1 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/header_actions/helpers.test.ts @@ -0,0 +1,50 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { euiThemeVars } from '@kbn/ui-theme'; +import { DEFAULT_ACTION_BUTTON_WIDTH, getActionsColumnWidth, isAlert } from './helpers'; + +describe('isAlert', () => { + test('it returns true when the eventType is an alert', () => { + expect(isAlert('signal')).toBe(true); + }); + + test('it returns false when the eventType is NOT an alert', () => { + expect(isAlert('raw')).toBe(false); + }); +}); + +describe('getActionsColumnWidth', () => { + // ideally the following implementation detail wouldn't be part of these tests, + // but without it, the test would be brittle when `euiDataGridCellPaddingM` changes: + const expectedPadding = parseInt(euiThemeVars.euiDataGridCellPaddingM, 10) * 2; + + test('it returns the expected width', () => { + const ACTION_BUTTON_COUNT = 5; + const expectedContentWidth = ACTION_BUTTON_COUNT * DEFAULT_ACTION_BUTTON_WIDTH; + + expect(getActionsColumnWidth(ACTION_BUTTON_COUNT)).toEqual( + expectedContentWidth + expectedPadding + ); + }); + + test('it returns the minimum width when the button count is zero', () => { + const ACTION_BUTTON_COUNT = 0; + + expect(getActionsColumnWidth(ACTION_BUTTON_COUNT)).toEqual( + DEFAULT_ACTION_BUTTON_WIDTH + expectedPadding + ); + }); + + test('it returns the minimum width when the button count is negative', () => { + const ACTION_BUTTON_COUNT = -1; + + expect(getActionsColumnWidth(ACTION_BUTTON_COUNT)).toEqual( + DEFAULT_ACTION_BUTTON_WIDTH + expectedPadding + ); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/components/header_actions/helpers.ts b/x-pack/plugins/security_solution/public/common/components/header_actions/helpers.ts new file mode 100644 index 0000000000000..7530b591dac73 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/header_actions/helpers.ts @@ -0,0 +1,52 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { euiThemeVars } from '@kbn/ui-theme'; +import type { TimelineEventsType } from '../../../../common/types'; + +/** + * This is the effective width in pixels of an action button used with + * `EuiDataGrid` `leadingControlColumns`. (See Notes below for details) + * + * Notes: + * 1) This constant is necessary because `width` is a required property of + * the `EuiDataGridControlColumn` interface, so it must be calculated before + * content is rendered. (The width of a `EuiDataGridControlColumn` does not + * automatically size itself to fit all the content.) + * + * 2) This is the *effective* width, because at the time of this writing, + * `EuiButtonIcon` has a `margin-left: -4px`, which is subtracted from the + * `width` + */ +export const DEFAULT_ACTION_BUTTON_WIDTH = + parseInt(euiThemeVars.euiSizeXL, 10) - parseInt(euiThemeVars.euiSizeXS, 10); // px + +export const isAlert = (eventType: TimelineEventsType | Omit): boolean => + eventType === 'signal'; + +/** + * Returns the width of the Actions column based on the number of buttons being + * displayed + * + * NOTE: This function is necessary because `width` is a required property of + * the `EuiDataGridControlColumn` interface, so it must be calculated before + * content is rendered. (The width of a `EuiDataGridControlColumn` does not + * automatically size itself to fit all the content.) + */ +export const getActionsColumnWidth = (actionButtonCount: number): number => { + const contentWidth = + actionButtonCount > 0 + ? actionButtonCount * DEFAULT_ACTION_BUTTON_WIDTH + : DEFAULT_ACTION_BUTTON_WIDTH; + + // `EuiDataGridRowCell` applies additional `padding-left` and + // `padding-right`, which must be added to the content width to prevent the + // content from being partially hidden due to the space occupied by padding: + const leftRightCellPadding = parseInt(euiThemeVars.euiDataGridCellPaddingM, 10) * 2; // parseInt ignores the trailing `px`, e.g. `6px` + + return contentWidth + leftRightCellPadding; +}; diff --git a/x-pack/plugins/security_solution/public/common/components/header_actions/index.tsx b/x-pack/plugins/security_solution/public/common/components/header_actions/index.tsx new file mode 100644 index 0000000000000..7a9dfc769324a --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/header_actions/index.tsx @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export * from './helpers'; +export * from './actions'; +export * from './header_actions'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/pin_event_action.test.tsx b/x-pack/plugins/security_solution/public/common/components/header_actions/pin_event_action.test.tsx similarity index 82% rename from x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/pin_event_action.test.tsx rename to x-pack/plugins/security_solution/public/common/components/header_actions/pin_event_action.test.tsx index eedf717484694..fc878d77b9235 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/pin_event_action.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/header_actions/pin_event_action.test.tsx @@ -9,12 +9,12 @@ import { render, screen } from '@testing-library/react'; import React from 'react'; import { PinEventAction } from './pin_event_action'; -import { useUserPrivileges } from '../../../../../common/components/user_privileges'; -import { getEndpointPrivilegesInitialStateMock } from '../../../../../common/components/user_privileges/endpoint/mocks'; -import { TestProviders } from '../../../../../common/mock'; -import { TimelineType } from '../../../../../../common/types'; +import { useUserPrivileges } from '../user_privileges'; +import { getEndpointPrivilegesInitialStateMock } from '../user_privileges/endpoint/mocks'; +import { TestProviders } from '../../mock'; +import { TimelineType } from '../../../../common/types'; -jest.mock('../../../../../common/components/user_privileges'); +jest.mock('../user_privileges'); const useUserPrivilegesMock = useUserPrivileges as jest.Mock; describe('PinEventAction', () => { diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/pin_event_action.tsx b/x-pack/plugins/security_solution/public/common/components/header_actions/pin_event_action.tsx similarity index 81% rename from x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/pin_event_action.tsx rename to x-pack/plugins/security_solution/public/common/components/header_actions/pin_event_action.tsx index 97280b9dd029b..92f6c082e5475 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/pin_event_action.tsx +++ b/x-pack/plugins/security_solution/public/common/components/header_actions/pin_event_action.tsx @@ -7,13 +7,12 @@ import React, { useMemo } from 'react'; import { EuiToolTip } from '@elastic/eui'; - -import { DEFAULT_ACTION_BUTTON_WIDTH } from '@kbn/timelines-plugin/public'; -import { EventsTdContent } from '../../styles'; -import { eventHasNotes, getPinTooltip } from '../helpers'; -import { Pin } from '../../pin'; -import type { TimelineType } from '../../../../../../common/types/timeline'; -import { useUserPrivileges } from '../../../../../common/components/user_privileges'; +import { EventsTdContent } from '../../../timelines/components/timeline/styles'; +import { eventHasNotes, getPinTooltip } from '../../../timelines/components/timeline/body/helpers'; +import type { TimelineType } from '../../../../common/types'; +import { useUserPrivileges } from '../user_privileges'; +import { DEFAULT_ACTION_BUTTON_WIDTH } from '.'; +import { Pin } from '../../../timelines/components/timeline/pin'; interface PinEventActionProps { ariaLabel?: string; diff --git a/x-pack/plugins/security_solution/public/common/components/header_actions/translations.ts b/x-pack/plugins/security_solution/public/common/components/header_actions/translations.ts new file mode 100644 index 0000000000000..9c783fd9d2dba --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/header_actions/translations.ts @@ -0,0 +1,144 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor 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 OPEN_SESSION_VIEW = i18n.translate( + 'xpack.securitySolution.timeline.body.openSessionViewLabel', + { + defaultMessage: 'Open Session View', + } +); + +export const NOTES_DISABLE_TOOLTIP = i18n.translate( + 'xpack.securitySolution.timeline.body.notes.disableEventTooltip', + { + defaultMessage: 'Notes may not be added here while editing a template timeline', + } +); + +export const NOTES_TOOLTIP = i18n.translate( + 'xpack.securitySolution.timeline.body.notes.addNoteTooltip', + { + defaultMessage: 'Add note', + } +); + +export const SORT_FIELDS = i18n.translate('xpack.securitySolution.timeline.sortFieldsButton', { + defaultMessage: 'Sort fields', +}); + +export const FULL_SCREEN = i18n.translate('xpack.securitySolution.timeline.fullScreenButton', { + defaultMessage: 'Full screen', +}); + +export const VIEW_DETAILS = i18n.translate( + 'xpack.securitySolution.hoverActions.viewDetailsAriaLabel', + { + defaultMessage: 'View details', + } +); + +export const VIEW_DETAILS_FOR_ROW = ({ + ariaRowindex, + columnValues, +}: { + ariaRowindex: number; + columnValues: string; +}) => + i18n.translate('xpack.securitySolution.hoverActions.viewDetailsForRowAriaLabel', { + values: { ariaRowindex, columnValues }, + defaultMessage: + 'View details for the alert or event in row {ariaRowindex}, with columns {columnValues}', + }); + +export const ACTION_INVESTIGATE_IN_RESOLVER = i18n.translate( + 'xpack.securitySolution.hoverActions.investigateInResolverTooltip', + { + defaultMessage: 'Analyze event', + } +); + +export const CHECKBOX_FOR_ROW = ({ + ariaRowindex, + columnValues, + checked, +}: { + ariaRowindex: number; + columnValues: string; + checked: boolean; +}) => + i18n.translate('xpack.securitySolution.hoverActions.checkboxForRowAriaLabel', { + values: { ariaRowindex, checked, columnValues }, + defaultMessage: + '{checked, select, false {unchecked} true {checked}} checkbox for the alert or event in row {ariaRowindex}, with columns {columnValues}', + }); + +export const ACTION_INVESTIGATE_IN_RESOLVER_FOR_ROW = ({ + ariaRowindex, + columnValues, +}: { + ariaRowindex: number; + columnValues: string; +}) => + i18n.translate('xpack.securitySolution.hoverActions.investigateInResolverForRowAriaLabel', { + values: { ariaRowindex, columnValues }, + defaultMessage: 'Analyze the alert or event in row {ariaRowindex}, with columns {columnValues}', + }); + +export const SEND_ALERT_TO_TIMELINE_FOR_ROW = ({ + ariaRowindex, + columnValues, +}: { + ariaRowindex: number; + columnValues: string; +}) => + i18n.translate('xpack.securitySolution.hoverActions.sendAlertToTimelineForRowAriaLabel', { + values: { ariaRowindex, columnValues }, + defaultMessage: 'Send the alert in row {ariaRowindex} to timeline, with columns {columnValues}', + }); + +export const ADD_NOTES_FOR_ROW = ({ + ariaRowindex, + columnValues, +}: { + ariaRowindex: number; + columnValues: string; +}) => + i18n.translate('xpack.securitySolution.hoverActions.addNotesForRowAriaLabel', { + values: { ariaRowindex, columnValues }, + defaultMessage: + 'Add notes for the event in row {ariaRowindex} to timeline, with columns {columnValues}', + }); + +export const PIN_EVENT_FOR_ROW = ({ + ariaRowindex, + columnValues, + isEventPinned, +}: { + ariaRowindex: number; + columnValues: string; + isEventPinned: boolean; +}) => + i18n.translate('xpack.securitySolution.hoverActions.pinEventForRowAriaLabel', { + values: { ariaRowindex, columnValues, isEventPinned }, + defaultMessage: + '{isEventPinned, select, false {Pin} true {Unpin}} the event in row {ariaRowindex} to timeline, with columns {columnValues}', + }); + +export const MORE_ACTIONS_FOR_ROW = ({ + ariaRowindex, + columnValues, +}: { + ariaRowindex: number; + columnValues: string; +}) => + i18n.translate('xpack.securitySolution.hoverActions.moreActionsForRowAriaLabel', { + values: { ariaRowindex, columnValues }, + defaultMessage: + 'Select more actions for the alert or event in row {ariaRowindex}, with columns {columnValues}', + }); diff --git a/x-pack/plugins/security_solution/public/common/components/hover_actions/use_hover_actions.tsx b/x-pack/plugins/security_solution/public/common/components/hover_actions/use_hover_actions.tsx index bb780af23c82c..298b1e8650363 100644 --- a/x-pack/plugins/security_solution/public/common/components/hover_actions/use_hover_actions.tsx +++ b/x-pack/plugins/security_solution/public/common/components/hover_actions/use_hover_actions.tsx @@ -7,13 +7,13 @@ import React, { useCallback, useMemo, useState, useRef, useContext } from 'react'; import type { DraggableProvided, DraggableStateSnapshot } from 'react-beautiful-dnd'; -import { TableContext } from '@kbn/timelines-plugin/public'; import { TimelineContext } from '../../../timelines/components/timeline'; import { HoverActions } from '.'; import type { DataProvider } from '../../../../common/types'; import { ProviderContentWrapper } from '../drag_and_drop/draggable_wrapper'; import { getDraggableId } from '../drag_and_drop/helpers'; +import { TableContext } from '../events_viewer/shared'; import { useTopNPopOver } from './utils'; const draggableContainsLinks = (draggableElement: HTMLDivElement | null) => { diff --git a/x-pack/plugins/security_solution/public/common/components/import_data_modal/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/common/components/import_data_modal/__snapshots__/index.test.tsx.snap index 179ef9b666aad..6d3605f16e741 100644 --- a/x-pack/plugins/security_solution/public/common/components/import_data_modal/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/common/components/import_data_modal/__snapshots__/index.test.tsx.snap @@ -14,13 +14,13 @@ Object { data-eui="EuiFocusTrap" >
    -
    -

    - title -

    -
    + title +
    + +
    + , + "container":
    + + + +
    , + "debug": [Function], + "findAllByAltText": [Function], + "findAllByDisplayValue": [Function], + "findAllByLabelText": [Function], + "findAllByPlaceholderText": [Function], + "findAllByRole": [Function], + "findAllByTestId": [Function], + "findAllByText": [Function], + "findAllByTitle": [Function], + "findByAltText": [Function], + "findByDisplayValue": [Function], + "findByLabelText": [Function], + "findByPlaceholderText": [Function], + "findByRole": [Function], + "findByTestId": [Function], + "findByText": [Function], + "findByTitle": [Function], + "getAllByAltText": [Function], + "getAllByDisplayValue": [Function], + "getAllByLabelText": [Function], + "getAllByPlaceholderText": [Function], + "getAllByRole": [Function], + "getAllByTestId": [Function], + "getAllByText": [Function], + "getAllByTitle": [Function], + "getByAltText": [Function], + "getByDisplayValue": [Function], + "getByLabelText": [Function], + "getByPlaceholderText": [Function], + "getByRole": [Function], + "getByTestId": [Function], + "getByText": [Function], + "getByTitle": [Function], + "queryAllByAltText": [Function], + "queryAllByDisplayValue": [Function], + "queryAllByLabelText": [Function], + "queryAllByPlaceholderText": [Function], + "queryAllByRole": [Function], + "queryAllByTestId": [Function], + "queryAllByText": [Function], + "queryAllByTitle": [Function], + "queryByAltText": [Function], + "queryByDisplayValue": [Function], + "queryByLabelText": [Function], + "queryByPlaceholderText": [Function], + "queryByRole": [Function], + "queryByTestId": [Function], + "queryByText": [Function], + "queryByTitle": [Function], + "rerender": [Function], + "unmount": [Function], +} +`; diff --git a/x-pack/plugins/threat_intelligence/public/modules/indicators/components/copy_to_clipboard/copy_to_clipboard.stories.tsx b/x-pack/plugins/threat_intelligence/public/modules/indicators/components/copy_to_clipboard/copy_to_clipboard.stories.tsx index 8c9a3ccbeee81..bb44c30d7ac5d 100644 --- a/x-pack/plugins/threat_intelligence/public/modules/indicators/components/copy_to_clipboard/copy_to_clipboard.stories.tsx +++ b/x-pack/plugins/threat_intelligence/public/modules/indicators/components/copy_to_clipboard/copy_to_clipboard.stories.tsx @@ -8,7 +8,11 @@ import React from 'react'; import { Story } from '@storybook/react'; import { EuiContextMenuPanel } from '@elastic/eui'; -import { CopyToClipboardButtonEmpty, CopyToClipboardContextMenu } from '.'; +import { + CopyToClipboardButtonEmpty, + CopyToClipboardButtonIcon, + CopyToClipboardContextMenu, +} from '.'; export default { title: 'CopyToClipboard', @@ -25,3 +29,7 @@ export const ContextMenu: Story = () => { return ; }; + +export const ButtonIcon: Story = () => { + return ; +}; diff --git a/x-pack/plugins/threat_intelligence/public/modules/indicators/components/copy_to_clipboard/copy_to_clipboard.test.tsx b/x-pack/plugins/threat_intelligence/public/modules/indicators/components/copy_to_clipboard/copy_to_clipboard.test.tsx index c54c205274b4c..1e01adea97dc8 100644 --- a/x-pack/plugins/threat_intelligence/public/modules/indicators/components/copy_to_clipboard/copy_to_clipboard.test.tsx +++ b/x-pack/plugins/threat_intelligence/public/modules/indicators/components/copy_to_clipboard/copy_to_clipboard.test.tsx @@ -7,7 +7,11 @@ import React from 'react'; import { render } from '@testing-library/react'; -import { CopyToClipboardButtonEmpty, CopyToClipboardContextMenu } from '.'; +import { + CopyToClipboardButtonEmpty, + CopyToClipboardButtonIcon, + CopyToClipboardContextMenu, +} from '.'; const mockValue: string = 'Text copied'; @@ -30,4 +34,12 @@ describe(' ', () => expect(component).toMatchSnapshot(); }); + + it('should render one EuibuttonIcon', () => { + const component = render( + + ); + + expect(component).toMatchSnapshot(); + }); }); diff --git a/x-pack/plugins/threat_intelligence/public/modules/indicators/components/copy_to_clipboard/copy_to_clipboard.tsx b/x-pack/plugins/threat_intelligence/public/modules/indicators/components/copy_to_clipboard/copy_to_clipboard.tsx index 6041a10cddac7..a78df8e8fad36 100644 --- a/x-pack/plugins/threat_intelligence/public/modules/indicators/components/copy_to_clipboard/copy_to_clipboard.tsx +++ b/x-pack/plugins/threat_intelligence/public/modules/indicators/components/copy_to_clipboard/copy_to_clipboard.tsx @@ -6,7 +6,7 @@ */ import React, { VFC } from 'react'; -import { EuiButtonEmpty, EuiContextMenuItem, EuiCopy } from '@elastic/eui'; +import { EuiButtonEmpty, EuiButtonIcon, EuiContextMenuItem, EuiCopy } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; const COPY_ICON = 'copyClipboard'; @@ -80,3 +80,30 @@ export const CopyToClipboardContextMenu: VFC = ({ )} ); + +/** + * Takes a string and copies it to the clipboard. + * + * This component renders an {@link EuiButtonIcon}. + * + * @returns An EuiCopy element + */ +export const CopyToClipboardButtonIcon: VFC = ({ + value, + 'data-test-subj': dataTestSub, +}) => ( + + {(copy) => ( + + {COPY_TITLE} + + )} + +); diff --git a/x-pack/plugins/threat_intelligence/public/modules/indicators/components/flyout/context.ts b/x-pack/plugins/threat_intelligence/public/modules/indicators/components/flyout/context.ts new file mode 100644 index 0000000000000..d4abd38e4b584 --- /dev/null +++ b/x-pack/plugins/threat_intelligence/public/modules/indicators/components/flyout/context.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { createContext } from 'react'; + +export interface IndicatorsFlyoutContextValue { + kqlBarIntegration: boolean; + indicatorName?: string | undefined; +} + +export const IndicatorsFlyoutContext = createContext( + undefined +); diff --git a/x-pack/plugins/threat_intelligence/public/modules/indicators/components/flyout/fields_table/fields_table.stories.tsx b/x-pack/plugins/threat_intelligence/public/modules/indicators/components/flyout/fields_table/fields_table.stories.tsx index 80bd24d59adc9..41cb3633609ba 100644 --- a/x-pack/plugins/threat_intelligence/public/modules/indicators/components/flyout/fields_table/fields_table.stories.tsx +++ b/x-pack/plugins/threat_intelligence/public/modules/indicators/components/flyout/fields_table/fields_table.stories.tsx @@ -11,6 +11,7 @@ import { IndicatorFieldsTable } from '.'; import { generateMockIndicator } from '../../../../../../common/types/indicator'; import { StoryProvidersComponent } from '../../../../../common/mocks/story_providers'; import { IndicatorsFiltersContext } from '../../../containers/filters'; +import { IndicatorsFlyoutContext } from '../context'; export default { component: IndicatorFieldsTable, @@ -19,15 +20,41 @@ export default { export function WithIndicators() { const indicator = generateMockIndicator(); + const context = { + kqlBarIntegration: false, + }; return ( - + + + + + + ); +} + +export function NoFilterButtons() { + const indicator = generateMockIndicator(); + const context = { + kqlBarIntegration: true, + }; + + return ( + + + + + ); diff --git a/x-pack/plugins/threat_intelligence/public/modules/indicators/components/flyout/fields_table/fields_table.tsx b/x-pack/plugins/threat_intelligence/public/modules/indicators/components/flyout/fields_table/fields_table.tsx index 3fe1f62599059..47c4a5cf9b61b 100644 --- a/x-pack/plugins/threat_intelligence/public/modules/indicators/components/flyout/fields_table/fields_table.tsx +++ b/x-pack/plugins/threat_intelligence/public/modules/indicators/components/flyout/fields_table/fields_table.tsx @@ -65,7 +65,7 @@ export const IndicatorFieldsTable: VFC = ({ return ( void; + /** + * Boolean deciding if we show or hide the filter in/out feature in the flyout. + * We should be showing the filter in and out buttons when the flyout is used in the cases view. + */ + kqlBarIntegration?: boolean; + /** + * Name of the indicator, used only when the flyout is rendered in the Cases view. + * Because the indicator name is a runtime field, when querying for the indicator from within + * the Cases view, this logic is not ran. Therefore, passing the name to the flyout is an + * easy (hopefully temporary) solution to display it within the flyout. + */ + indicatorName?: string; } /** * Leverages the {@link EuiFlyout} from the @elastic/eui library to dhow the details of a specific {@link Indicator}. */ -export const IndicatorsFlyout: VFC = ({ indicator, closeFlyout }) => { +export const IndicatorsFlyout: VFC = ({ + indicator, + closeFlyout, + kqlBarIntegration = false, + indicatorName, +}) => { const [selectedTabId, setSelectedTabId] = useState(TAB_IDS.overview); const tabs = useMemo( @@ -146,7 +164,11 @@ export const IndicatorsFlyout: VFC = ({ indicator, closeF {renderTabs} - {selectedTabContent} + + + {selectedTabContent} + + diff --git a/x-pack/plugins/threat_intelligence/public/modules/indicators/components/flyout/indicator_value_actions/__snapshots__/indicator_value_actions.test.tsx.snap b/x-pack/plugins/threat_intelligence/public/modules/indicators/components/flyout/indicator_value_actions/__snapshots__/indicator_value_actions.test.tsx.snap new file mode 100644 index 0000000000000..c7990f75d0ecd --- /dev/null +++ b/x-pack/plugins/threat_intelligence/public/modules/indicators/components/flyout/indicator_value_actions/__snapshots__/indicator_value_actions.test.tsx.snap @@ -0,0 +1,386 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`IndicatorValueActions should only render add to timeline and copy to clipboard 1`] = ` +Object { + "asFragment": [Function], + "baseElement": +
    +
    + +
    + + Add To Timeline + +
    +
    + + + +
    +
    + , + "container":
    +
    + +
    + + Add To Timeline + +
    +
    + + + +
    +
    , + "debug": [Function], + "findAllByAltText": [Function], + "findAllByDisplayValue": [Function], + "findAllByLabelText": [Function], + "findAllByPlaceholderText": [Function], + "findAllByRole": [Function], + "findAllByTestId": [Function], + "findAllByText": [Function], + "findAllByTitle": [Function], + "findByAltText": [Function], + "findByDisplayValue": [Function], + "findByLabelText": [Function], + "findByPlaceholderText": [Function], + "findByRole": [Function], + "findByTestId": [Function], + "findByText": [Function], + "findByTitle": [Function], + "getAllByAltText": [Function], + "getAllByDisplayValue": [Function], + "getAllByLabelText": [Function], + "getAllByPlaceholderText": [Function], + "getAllByRole": [Function], + "getAllByTestId": [Function], + "getAllByText": [Function], + "getAllByTitle": [Function], + "getByAltText": [Function], + "getByDisplayValue": [Function], + "getByLabelText": [Function], + "getByPlaceholderText": [Function], + "getByRole": [Function], + "getByTestId": [Function], + "getByText": [Function], + "getByTitle": [Function], + "queryAllByAltText": [Function], + "queryAllByDisplayValue": [Function], + "queryAllByLabelText": [Function], + "queryAllByPlaceholderText": [Function], + "queryAllByRole": [Function], + "queryAllByTestId": [Function], + "queryAllByText": [Function], + "queryAllByTitle": [Function], + "queryByAltText": [Function], + "queryByDisplayValue": [Function], + "queryByLabelText": [Function], + "queryByPlaceholderText": [Function], + "queryByRole": [Function], + "queryByTestId": [Function], + "queryByText": [Function], + "queryByTitle": [Function], + "rerender": [Function], + "unmount": [Function], +} +`; + +exports[`IndicatorValueActions should render filter in/out and dropdown for add to timeline and copy to clipboard 1`] = ` +Object { + "asFragment": [Function], + "baseElement": +
    +
    + + + + + + +
    +
    + + + +
    +
    +
    +
    + , + "container":
    +
    + + + + + + +
    +
    + + + +
    +
    +
    +
    , + "debug": [Function], + "findAllByAltText": [Function], + "findAllByDisplayValue": [Function], + "findAllByLabelText": [Function], + "findAllByPlaceholderText": [Function], + "findAllByRole": [Function], + "findAllByTestId": [Function], + "findAllByText": [Function], + "findAllByTitle": [Function], + "findByAltText": [Function], + "findByDisplayValue": [Function], + "findByLabelText": [Function], + "findByPlaceholderText": [Function], + "findByRole": [Function], + "findByTestId": [Function], + "findByText": [Function], + "findByTitle": [Function], + "getAllByAltText": [Function], + "getAllByDisplayValue": [Function], + "getAllByLabelText": [Function], + "getAllByPlaceholderText": [Function], + "getAllByRole": [Function], + "getAllByTestId": [Function], + "getAllByText": [Function], + "getAllByTitle": [Function], + "getByAltText": [Function], + "getByDisplayValue": [Function], + "getByLabelText": [Function], + "getByPlaceholderText": [Function], + "getByRole": [Function], + "getByTestId": [Function], + "getByText": [Function], + "getByTitle": [Function], + "queryAllByAltText": [Function], + "queryAllByDisplayValue": [Function], + "queryAllByLabelText": [Function], + "queryAllByPlaceholderText": [Function], + "queryAllByRole": [Function], + "queryAllByTestId": [Function], + "queryAllByText": [Function], + "queryAllByTitle": [Function], + "queryByAltText": [Function], + "queryByDisplayValue": [Function], + "queryByLabelText": [Function], + "queryByPlaceholderText": [Function], + "queryByRole": [Function], + "queryByTestId": [Function], + "queryByText": [Function], + "queryByTitle": [Function], + "rerender": [Function], + "unmount": [Function], +} +`; + +exports[`IndicatorValueActions should return null if field and value are invalid 1`] = ` +Object { + "asFragment": [Function], + "baseElement": +
    + , + "container":
    , + "debug": [Function], + "findAllByAltText": [Function], + "findAllByDisplayValue": [Function], + "findAllByLabelText": [Function], + "findAllByPlaceholderText": [Function], + "findAllByRole": [Function], + "findAllByTestId": [Function], + "findAllByText": [Function], + "findAllByTitle": [Function], + "findByAltText": [Function], + "findByDisplayValue": [Function], + "findByLabelText": [Function], + "findByPlaceholderText": [Function], + "findByRole": [Function], + "findByTestId": [Function], + "findByText": [Function], + "findByTitle": [Function], + "getAllByAltText": [Function], + "getAllByDisplayValue": [Function], + "getAllByLabelText": [Function], + "getAllByPlaceholderText": [Function], + "getAllByRole": [Function], + "getAllByTestId": [Function], + "getAllByText": [Function], + "getAllByTitle": [Function], + "getByAltText": [Function], + "getByDisplayValue": [Function], + "getByLabelText": [Function], + "getByPlaceholderText": [Function], + "getByRole": [Function], + "getByTestId": [Function], + "getByText": [Function], + "getByTitle": [Function], + "queryAllByAltText": [Function], + "queryAllByDisplayValue": [Function], + "queryAllByLabelText": [Function], + "queryAllByPlaceholderText": [Function], + "queryAllByRole": [Function], + "queryAllByTestId": [Function], + "queryAllByText": [Function], + "queryAllByTitle": [Function], + "queryByAltText": [Function], + "queryByDisplayValue": [Function], + "queryByLabelText": [Function], + "queryByPlaceholderText": [Function], + "queryByRole": [Function], + "queryByTestId": [Function], + "queryByText": [Function], + "queryByTitle": [Function], + "rerender": [Function], + "unmount": [Function], +} +`; diff --git a/x-pack/plugins/threat_intelligence/public/modules/indicators/components/flyout/indicator_value_actions/indicator_value_actions.stories.tsx b/x-pack/plugins/threat_intelligence/public/modules/indicators/components/flyout/indicator_value_actions/indicator_value_actions.stories.tsx new file mode 100644 index 0000000000000..a0a065b9abc2c --- /dev/null +++ b/x-pack/plugins/threat_intelligence/public/modules/indicators/components/flyout/indicator_value_actions/indicator_value_actions.stories.tsx @@ -0,0 +1,46 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { Story } from '@storybook/react'; +import { StoryProvidersComponent } from '../../../../../common/mocks/story_providers'; +import { generateMockFileIndicator, Indicator } from '../../../../../../common/types/indicator'; +import { IndicatorValueActions } from '.'; +import { IndicatorsFlyoutContext } from '../context'; + +export default { + title: 'IndicatorValueActions', +}; + +const indicator: Indicator = generateMockFileIndicator(); +const field: string = 'threat.indicator.name'; + +export const Default: Story = () => { + const context = { + kqlBarIntegration: true, + }; + return ( + + + + + + ); +}; + +export const WithoutFilterInOut: Story = () => { + const context = { + kqlBarIntegration: false, + }; + return ( + + + + + + ); +}; diff --git a/x-pack/plugins/threat_intelligence/public/modules/indicators/components/flyout/indicator_value_actions/indicator_value_actions.test.tsx b/x-pack/plugins/threat_intelligence/public/modules/indicators/components/flyout/indicator_value_actions/indicator_value_actions.test.tsx new file mode 100644 index 0000000000000..8bfe289892626 --- /dev/null +++ b/x-pack/plugins/threat_intelligence/public/modules/indicators/components/flyout/indicator_value_actions/indicator_value_actions.test.tsx @@ -0,0 +1,60 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { generateMockFileIndicator, Indicator } from '../../../../../../common/types/indicator'; +import { render } from '@testing-library/react'; +import { IndicatorValueActions } from './indicator_value_actions'; +import { IndicatorsFlyoutContext } from '../context'; +import { TestProvidersComponent } from '../../../../../common/mocks/test_providers'; + +describe('IndicatorValueActions', () => { + const indicator: Indicator = generateMockFileIndicator(); + + it('should return null if field and value are invalid', () => { + const field: string = 'invalid'; + const context = { + kqlBarIntegration: true, + }; + const component = render( + + + + ); + expect(component).toMatchSnapshot(); + }); + + it('should only render add to timeline and copy to clipboard', () => { + const field: string = 'threat.indicator.name'; + const context = { + kqlBarIntegration: true, + }; + const component = render( + + + + + + ); + expect(component).toMatchSnapshot(); + }); + + it('should render filter in/out and dropdown for add to timeline and copy to clipboard', () => { + const field: string = 'threat.indicator.name'; + const context = { + kqlBarIntegration: false, + }; + const component = render( + + + + + + ); + expect(component).toMatchSnapshot(); + }); +}); diff --git a/x-pack/plugins/threat_intelligence/public/modules/indicators/components/flyout/indicator_value_actions/indicator_value_actions.tsx b/x-pack/plugins/threat_intelligence/public/modules/indicators/components/flyout/indicator_value_actions/indicator_value_actions.tsx index 4d4f1d94d84e5..d944adcef44d3 100644 --- a/x-pack/plugins/threat_intelligence/public/modules/indicators/components/flyout/indicator_value_actions/indicator_value_actions.tsx +++ b/x-pack/plugins/threat_intelligence/public/modules/indicators/components/flyout/indicator_value_actions/indicator_value_actions.tsx @@ -14,11 +14,12 @@ import { EuiToolTip, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import { useIndicatorsFlyoutContext } from '../use_context'; import { Indicator } from '../../../../../../common/types/indicator'; import { FilterInButtonIcon, FilterOutButtonIcon } from '../../../../query_bar'; -import { AddToTimelineContextMenu } from '../../../../timeline'; +import { AddToTimelineButtonIcon, AddToTimelineContextMenu } from '../../../../timeline'; import { fieldAndValueValid, getIndicatorFieldAndValue } from '../../../utils'; -import { CopyToClipboardContextMenu } from '../../copy_to_clipboard'; +import { CopyToClipboardButtonIcon, CopyToClipboardContextMenu } from '../../copy_to_clipboard'; export const TIMELINE_BUTTON_TEST_ID = 'TimelineButton'; export const FILTER_IN_BUTTON_TEST_ID = 'FilterInButton'; @@ -45,11 +46,21 @@ interface IndicatorValueActions { ['data-test-subj']?: string; } +/** + * This component render a set of actions for the user. + * Currently used in the indicators flyout (overview and table tabs). + * + * It gets a readOnly boolean from context, that drives what is displayed. + * - in the cases view usage, we only display add to timeline and copy to clipboard. + * - in the indicators table usave, we display all options + */ export const IndicatorValueActions: VFC = ({ indicator, field, 'data-test-subj': dataTestSubj, }) => { + const { kqlBarIntegration } = useIndicatorsFlyoutContext(); + const [isPopoverOpen, setPopover] = useState(false); const { key, value } = getIndicatorFieldAndValue(indicator, field); @@ -63,13 +74,22 @@ export const IndicatorValueActions: VFC = ({ const copyToClipboardTestId = `${dataTestSubj}${COPY_TO_CLIPBOARD_BUTTON_TEST_ID}`; const popoverTestId = `${dataTestSubj}${POPOVER_BUTTON_TEST_ID}`; + if (kqlBarIntegration) { + return ( + + + + + ); + } + const popoverItems = [ , , ]; return ( - + - + + + + + + ); +} + +export function NoFilterButtons() { + const mockField = 'threat.indicator.ip'; + const context = { + kqlBarIntegration: true, + }; + + return ( + + + + + ); diff --git a/x-pack/plugins/threat_intelligence/public/modules/indicators/components/flyout/overview_tab/overview_tab.stories.tsx b/x-pack/plugins/threat_intelligence/public/modules/indicators/components/flyout/overview_tab/overview_tab.stories.tsx index 4c74ea25330d7..633a6a2042d22 100644 --- a/x-pack/plugins/threat_intelligence/public/modules/indicators/components/flyout/overview_tab/overview_tab.stories.tsx +++ b/x-pack/plugins/threat_intelligence/public/modules/indicators/components/flyout/overview_tab/overview_tab.stories.tsx @@ -11,6 +11,7 @@ import { StoryProvidersComponent } from '../../../../../common/mocks/story_provi import { generateMockIndicator, Indicator } from '../../../../../../common/types/indicator'; import { IndicatorsFlyoutOverview } from '.'; import { IndicatorsFiltersContext } from '../../../containers/filters'; +import { IndicatorsFlyoutContext } from '../context'; export default { component: IndicatorsFlyoutOverview, @@ -25,11 +26,16 @@ export default { export const Default: Story = () => { const mockIndicator: Indicator = generateMockIndicator(); + const context = { + kqlBarIntegration: false, + }; return ( - {}} indicator={mockIndicator} /> + + {}} indicator={mockIndicator} /> + ); diff --git a/x-pack/plugins/threat_intelligence/public/modules/indicators/components/flyout/overview_tab/overview_tab.test.tsx b/x-pack/plugins/threat_intelligence/public/modules/indicators/components/flyout/overview_tab/overview_tab.test.tsx index df4201761a98e..40c35a4bca100 100644 --- a/x-pack/plugins/threat_intelligence/public/modules/indicators/components/flyout/overview_tab/overview_tab.test.tsx +++ b/x-pack/plugins/threat_intelligence/public/modules/indicators/components/flyout/overview_tab/overview_tab.test.tsx @@ -13,18 +13,26 @@ import { IndicatorsFlyoutOverview, TI_FLYOUT_OVERVIEW_HIGH_LEVEL_BLOCKS, TI_FLYOUT_OVERVIEW_TABLE, + TI_FLYOUT_OVERVIEW_TITLE, } from '.'; import { EMPTY_PROMPT_TEST_ID } from '../empty_prompt'; +import { IndicatorsFlyoutContext } from '../context'; describe('', () => { describe('invalid indicator', () => { it('should render error message on invalid indicator', () => { + const context = { + kqlBarIntegration: false, + }; + render( - {}} - indicator={{ fields: {} } as unknown as Indicator} - /> + + {}} + indicator={{ fields: {} } as unknown as Indicator} + /> + ); @@ -33,16 +41,62 @@ describe('', () => { }); it('should render the highlighted blocks and table when valid indicator is passed', () => { + const context = { + kqlBarIntegration: false, + }; + render( - {}} - indicator={generateMockIndicator()} - /> + + {}} + indicator={generateMockIndicator()} + /> + ); expect(screen.queryByTestId(TI_FLYOUT_OVERVIEW_TABLE)).toBeInTheDocument(); expect(screen.queryByTestId(TI_FLYOUT_OVERVIEW_HIGH_LEVEL_BLOCKS)).toBeInTheDocument(); }); + + it('should render the indicator name value in the title', () => { + const context = { + kqlBarIntegration: false, + }; + const indicator: Indicator = generateMockIndicator(); + const indicatorName: string = (indicator.fields['threat.indicator.name'] as string[])[0]; + + render( + + + {}} indicator={indicator} /> + + + ); + + expect(screen.queryByTestId(TI_FLYOUT_OVERVIEW_TITLE)?.innerHTML).toContain(indicatorName); + }); + + it('should render the indicator name passed via context in the title', () => { + const context = { + kqlBarIntegration: false, + indicatorName: '123', + }; + + render( + + + {}} + indicator={generateMockIndicator()} + /> + + + ); + + expect(screen.queryByTestId(TI_FLYOUT_OVERVIEW_TITLE)?.innerHTML).toContain( + context.indicatorName + ); + }); }); diff --git a/x-pack/plugins/threat_intelligence/public/modules/indicators/components/flyout/overview_tab/overview_tab.tsx b/x-pack/plugins/threat_intelligence/public/modules/indicators/components/flyout/overview_tab/overview_tab.tsx index 2c3e6dee5ffcb..9b53276667297 100644 --- a/x-pack/plugins/threat_intelligence/public/modules/indicators/components/flyout/overview_tab/overview_tab.tsx +++ b/x-pack/plugins/threat_intelligence/public/modules/indicators/components/flyout/overview_tab/overview_tab.tsx @@ -16,6 +16,7 @@ import { } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; import React, { useMemo, VFC } from 'react'; +import { useIndicatorsFlyoutContext } from '../use_context'; import { EMPTY_VALUE } from '../../../../../common/constants'; import { Indicator, RawIndicatorFieldId } from '../../../../../../common/types/indicator'; import { unwrapValue } from '../../../utils'; @@ -30,6 +31,7 @@ const highLevelFields = [ RawIndicatorFieldId.Confidence, ]; +export const TI_FLYOUT_OVERVIEW_TITLE = 'tiFlyoutOverviewTitle'; export const TI_FLYOUT_OVERVIEW_TABLE = 'tiFlyoutOverviewTableRow'; export const TI_FLYOUT_OVERVIEW_HIGH_LEVEL_BLOCKS = 'tiFlyoutOverviewHighLevelBlocks'; @@ -42,6 +44,8 @@ export const IndicatorsFlyoutOverview: VFC = ({ indicator, onViewAllFieldsInTable, }) => { + const { indicatorName } = useIndicatorsFlyoutContext(); + const indicatorType = unwrapValue(indicator, RawIndicatorFieldId.Type); const highLevelBlocks = useMemo( @@ -67,7 +71,10 @@ export const IndicatorsFlyoutOverview: VFC = ({ return unwrappedDescription ? {unwrappedDescription} : null; }, [indicator]); - const indicatorName = unwrapValue(indicator, RawIndicatorFieldId.Name) || EMPTY_VALUE; + const title = + indicatorName != null + ? indicatorName + : unwrapValue(indicator, RawIndicatorFieldId.Name) || EMPTY_VALUE; if (!indicatorType) { return ; @@ -76,7 +83,7 @@ export const IndicatorsFlyoutOverview: VFC = ({ return ( <> -

    {indicatorName}

    +

    {title}

    {indicatorDescription} diff --git a/x-pack/plugins/threat_intelligence/public/modules/indicators/components/flyout/table_tab/table_tab.stories.tsx b/x-pack/plugins/threat_intelligence/public/modules/indicators/components/flyout/table_tab/table_tab.stories.tsx index 1842d52171db3..9553098f56499 100644 --- a/x-pack/plugins/threat_intelligence/public/modules/indicators/components/flyout/table_tab/table_tab.stories.tsx +++ b/x-pack/plugins/threat_intelligence/public/modules/indicators/components/flyout/table_tab/table_tab.stories.tsx @@ -15,15 +15,18 @@ import { mockKibanaTimelinesService } from '../../../../../common/mocks/mock_kib import { generateMockIndicator, Indicator } from '../../../../../../common/types/indicator'; import { IndicatorsFlyoutTable } from '.'; import { IndicatorsFiltersContext } from '../../../containers/filters'; +import { IndicatorsFlyoutContext } from '../context'; export default { component: IndicatorsFlyoutTable, title: 'IndicatorsFlyoutTable', }; +const context = { + kqlBarIntegration: false, +}; export const Default: Story = () => { const mockIndicator: Indicator = generateMockIndicator(); - const KibanaReactContext = createKibanaReactContext({ uiSettings: mockUiSettingsService(), timelines: mockKibanaTimelinesService, @@ -32,12 +35,18 @@ export const Default: Story = () => { return ( - + + + ); }; export const EmptyIndicator: Story = () => { - return ; + return ( + + + + ); }; diff --git a/x-pack/plugins/threat_intelligence/public/modules/indicators/components/flyout/table_tab/table_tab.test.tsx b/x-pack/plugins/threat_intelligence/public/modules/indicators/components/flyout/table_tab/table_tab.test.tsx index aae9aa41cbf2f..b70305deb5a0a 100644 --- a/x-pack/plugins/threat_intelligence/public/modules/indicators/components/flyout/table_tab/table_tab.test.tsx +++ b/x-pack/plugins/threat_intelligence/public/modules/indicators/components/flyout/table_tab/table_tab.test.tsx @@ -16,14 +16,21 @@ import { import { IndicatorsFlyoutTable, TABLE_TEST_ID } from '.'; import { unwrapValue } from '../../../utils'; import { EMPTY_PROMPT_TEST_ID } from '../empty_prompt'; +import { IndicatorsFlyoutContext } from '../context'; const mockIndicator: Indicator = generateMockIndicator(); describe('', () => { it('should render fields and values in table', () => { + const context = { + kqlBarIntegration: false, + }; + const { getByTestId, getByText, getAllByText } = render( - + + + ); diff --git a/x-pack/plugins/threat_intelligence/public/modules/indicators/components/flyout/use_context.ts b/x-pack/plugins/threat_intelligence/public/modules/indicators/components/flyout/use_context.ts new file mode 100644 index 0000000000000..cdb97e187279c --- /dev/null +++ b/x-pack/plugins/threat_intelligence/public/modules/indicators/components/flyout/use_context.ts @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useContext } from 'react'; +import { IndicatorsFlyoutContext, IndicatorsFlyoutContextValue } from './context'; + +/** + * Hook to retrieve {@link IndicatorsFiltersContext} (contains FilterManager) + */ +export const useIndicatorsFlyoutContext = (): IndicatorsFlyoutContextValue => { + const contextValue = useContext(IndicatorsFlyoutContext); + + if (!contextValue) { + throw new Error( + 'IndicatorsFlyoutContext can only be used within IndicatorsFlyoutContext provider' + ); + } + + return contextValue; +}; diff --git a/x-pack/plugins/threat_intelligence/public/modules/indicators/components/table/components/field_browser/field_browser.test.tsx b/x-pack/plugins/threat_intelligence/public/modules/indicators/components/table/components/field_browser/field_browser.test.tsx index 26b25ad88bd6e..afc64d73c9499 100644 --- a/x-pack/plugins/threat_intelligence/public/modules/indicators/components/table/components/field_browser/field_browser.test.tsx +++ b/x-pack/plugins/threat_intelligence/public/modules/indicators/components/table/components/field_browser/field_browser.test.tsx @@ -37,7 +37,7 @@ describe('', () => { onResetColumns: stub, onToggleColumn: stub, options: { - preselectedCategoryIds: ['threat'], + preselectedCategoryIds: ['threat', 'base', 'event', 'agent'], }, }) ); diff --git a/x-pack/plugins/threat_intelligence/public/modules/indicators/components/table/components/field_browser/field_browser.tsx b/x-pack/plugins/threat_intelligence/public/modules/indicators/components/table/components/field_browser/field_browser.tsx index 20882bb399072..3ae766eae2a68 100644 --- a/x-pack/plugins/threat_intelligence/public/modules/indicators/components/table/components/field_browser/field_browser.tsx +++ b/x-pack/plugins/threat_intelligence/public/modules/indicators/components/table/components/field_browser/field_browser.tsx @@ -30,7 +30,7 @@ export const IndicatorsFieldBrowser: VFC = ({ onResetColumns, onToggleColumn, options: { - preselectedCategoryIds: ['threat'], + preselectedCategoryIds: ['threat', 'base', 'event', 'agent'], }, }); }; diff --git a/x-pack/plugins/threat_intelligence/public/modules/indicators/hooks/use_aggregated_indicators.test.tsx b/x-pack/plugins/threat_intelligence/public/modules/indicators/hooks/use_aggregated_indicators.test.tsx index 04ca3311955f0..69c00247079c7 100644 --- a/x-pack/plugins/threat_intelligence/public/modules/indicators/hooks/use_aggregated_indicators.test.tsx +++ b/x-pack/plugins/threat_intelligence/public/modules/indicators/hooks/use_aggregated_indicators.test.tsx @@ -95,6 +95,11 @@ describe('useAggregatedIndicators()', () => { "isFetching": false, "isLoading": false, "onFieldChange": [Function], + "query": Object { + "id": "indicatorsBarchart", + "loading": false, + "refetch": [Function], + }, "selectedField": "threat.feed.name", "series": Array [], } diff --git a/x-pack/plugins/threat_intelligence/public/modules/indicators/hooks/use_aggregated_indicators.ts b/x-pack/plugins/threat_intelligence/public/modules/indicators/hooks/use_aggregated_indicators.ts index bdfd9fafa77e0..805342f87a2fd 100644 --- a/x-pack/plugins/threat_intelligence/public/modules/indicators/hooks/use_aggregated_indicators.ts +++ b/x-pack/plugins/threat_intelligence/public/modules/indicators/hooks/use_aggregated_indicators.ts @@ -56,10 +56,14 @@ export interface UseAggregatedIndicatorsValue { /** Is data update in progress? */ isFetching?: boolean; + + query: { refetch: VoidFunction; id: string; loading: boolean }; } const DEFAULT_FIELD = RawIndicatorFieldId.Feed; +const QUERY_ID = 'indicatorsBarchart'; + export const useAggregatedIndicators = ({ timeRange, filters, @@ -87,9 +91,9 @@ export const useAggregatedIndicators = ({ [inspectorAdapters, queryService, searchService] ); - const { data, isLoading, isFetching } = useQuery( + const { data, isLoading, isFetching, refetch } = useQuery( [ - 'indicatorsBarchart', + QUERY_ID, { filters, field, @@ -113,6 +117,11 @@ export const useAggregatedIndicators = ({ [queryService.timefilter.timefilter, timeRange] ); + const query = useMemo( + () => ({ refetch, id: QUERY_ID, loading: isLoading }), + [isLoading, refetch] + ); + return { dateRange, series: data || [], @@ -120,5 +129,6 @@ export const useAggregatedIndicators = ({ selectedField: field, isLoading, isFetching, + query, }; }; diff --git a/x-pack/plugins/threat_intelligence/public/modules/indicators/hooks/use_indicators.test.tsx b/x-pack/plugins/threat_intelligence/public/modules/indicators/hooks/use_indicators.test.tsx index 9292321658e86..0eac3f2cca674 100644 --- a/x-pack/plugins/threat_intelligence/public/modules/indicators/hooks/use_indicators.test.tsx +++ b/x-pack/plugins/threat_intelligence/public/modules/indicators/hooks/use_indicators.test.tsx @@ -106,7 +106,6 @@ describe('useIndicators()', () => { expect(hookResult.result.current).toMatchInlineSnapshot(` Object { "dataUpdatedAt": 0, - "handleRefresh": [Function], "indicatorCount": 0, "indicators": Array [], "isFetching": false, @@ -122,6 +121,11 @@ describe('useIndicators()', () => { 50, ], }, + "query": Object { + "id": "indicatorsTable", + "loading": false, + "refetch": [Function], + }, } `); }); diff --git a/x-pack/plugins/threat_intelligence/public/modules/indicators/hooks/use_indicators.ts b/x-pack/plugins/threat_intelligence/public/modules/indicators/hooks/use_indicators.ts index 3011d9b5101f6..b1f0b17a8fd2b 100644 --- a/x-pack/plugins/threat_intelligence/public/modules/indicators/hooks/use_indicators.ts +++ b/x-pack/plugins/threat_intelligence/public/modules/indicators/hooks/use_indicators.ts @@ -26,8 +26,6 @@ export interface UseIndicatorsParams { } export interface UseIndicatorsValue { - handleRefresh: () => void; - /** * Array of {@link Indicator} ready to render inside the IndicatorTable component */ @@ -48,8 +46,12 @@ export interface UseIndicatorsValue { isFetching: boolean; dataUpdatedAt: number; + + query: { refetch: VoidFunction; id: string; loading: boolean }; } +const QUERY_ID = 'indicatorsTable'; + export const useIndicators = ({ filters, filterQuery, @@ -98,7 +100,7 @@ export const useIndicators = ({ const { isLoading, isFetching, data, refetch, dataUpdatedAt } = useQuery( [ - 'indicatorsTable', + QUERY_ID, { timeRange, filterQuery, @@ -119,10 +121,10 @@ export const useIndicators = ({ } ); - const handleRefresh = useCallback(() => { - onChangePage(0); - refetch(); - }, [onChangePage, refetch]); + const query = useMemo( + () => ({ refetch, id: QUERY_ID, loading: isLoading }), + [isLoading, refetch] + ); return { indicators: data?.indicators || [], @@ -132,7 +134,7 @@ export const useIndicators = ({ onChangeItemsPerPage, isLoading, isFetching, - handleRefresh, dataUpdatedAt, + query, }; }; diff --git a/x-pack/plugins/threat_intelligence/public/modules/indicators/pages/indicators.test.tsx b/x-pack/plugins/threat_intelligence/public/modules/indicators/pages/indicators.test.tsx index d51410857c26e..d8f48689c1df0 100644 --- a/x-pack/plugins/threat_intelligence/public/modules/indicators/pages/indicators.test.tsx +++ b/x-pack/plugins/threat_intelligence/public/modules/indicators/pages/indicators.test.tsx @@ -30,6 +30,11 @@ describe('', () => { series: [], selectedField: '', onFieldChange: () => {}, + query: { + id: 'chart', + loading: false, + refetch: stub, + }, }); (useIndicators as jest.MockedFunction).mockReturnValue({ @@ -40,8 +45,12 @@ describe('', () => { pagination: { pageIndex: 0, pageSize: 10, pageSizeOptions: [10] }, onChangeItemsPerPage: stub, onChangePage: stub, - handleRefresh: stub, dataUpdatedAt: Date.now(), + query: { + id: 'list', + loading: false, + refetch: stub, + }, }); (useFilters as jest.MockedFunction).mockReturnValue({ diff --git a/x-pack/plugins/threat_intelligence/public/modules/indicators/pages/indicators.tsx b/x-pack/plugins/threat_intelligence/public/modules/indicators/pages/indicators.tsx index f161b7ac1d85c..aaac18f3fb215 100644 --- a/x-pack/plugins/threat_intelligence/public/modules/indicators/pages/indicators.tsx +++ b/x-pack/plugins/threat_intelligence/public/modules/indicators/pages/indicators.tsx @@ -16,8 +16,8 @@ import { FieldTypesProvider } from '../../../containers/field_types_provider'; import { InspectorProvider } from '../../../containers/inspector'; import { useColumnSettings } from '../components/table/hooks'; import { IndicatorsFilters } from '../containers/filters'; -import { useSecurityContext } from '../../../hooks'; import { UpdateStatus } from '../../../components/update_status'; +import { QueryBar } from '../../query_bar/query_bar'; const IndicatorsPageProviders: FC = ({ children }) => ( @@ -43,6 +43,7 @@ const IndicatorsPageContent: VFC = () => { isLoading: isLoadingIndicators, isFetching: isFetchingIndicators, dataUpdatedAt, + query: indicatorListQuery, } = useIndicators({ filters, filterQuery, @@ -57,14 +58,13 @@ const IndicatorsPageContent: VFC = () => { onFieldChange, isLoading: isLoadingAggregatedIndicators, isFetching: isFetchingAggregatedIndicators, + query: indicatorChartQuery, } = useAggregatedIndicators({ timeRange, filters, filterQuery, }); - const { SiemSearchBar } = useSecurityContext(); - return ( { subHeader={} > - + ; +} + +export const QueryBar: VFC = ({ indexPattern, queries }) => { + const { SiemSearchBar, registerQuery, deregisterQuery } = useSecurityContext(); + + useEffect(() => { + queries.forEach(registerQuery); + + return () => queries.forEach(deregisterQuery); + }, [queries, deregisterQuery, registerQuery]); + + return ; +}; diff --git a/x-pack/plugins/threat_intelligence/public/modules/timeline/hooks/use_investigate_in_timeline.ts b/x-pack/plugins/threat_intelligence/public/modules/timeline/hooks/use_investigate_in_timeline.ts index 94d697fb62fed..7783976895cbd 100644 --- a/x-pack/plugins/threat_intelligence/public/modules/timeline/hooks/use_investigate_in_timeline.ts +++ b/x-pack/plugins/threat_intelligence/public/modules/timeline/hooks/use_investigate_in_timeline.ts @@ -52,8 +52,13 @@ export const useInvestigateInTimeline = ({ generateDataProvider(e, value as string) ); - const to = unwrapValue(indicator, RawIndicatorFieldId.TimeStamp) as string; - const from = moment(to).subtract(10, 'm').toISOString(); + const indicatorTimestamp: string = unwrapValue( + indicator, + RawIndicatorFieldId.TimeStamp + ) as string; + + const from = moment(indicatorTimestamp).subtract(7, 'd').toISOString(); + const to = moment(indicatorTimestamp).add(7, 'd').toISOString(); if (!to || !from) { return {} as unknown as UseInvestigateInTimelineValue; diff --git a/x-pack/plugins/threat_intelligence/public/plugin.tsx b/x-pack/plugins/threat_intelligence/public/plugin.tsx index 6fc058131af9b..3fcbdde60f560 100755 --- a/x-pack/plugins/threat_intelligence/public/plugin.tsx +++ b/x-pack/plugins/threat_intelligence/public/plugin.tsx @@ -32,7 +32,7 @@ const LazyIndicatorsPageWrapper = React.lazy(() => import('./containers/indicato /** * This is used here: - * x-pack/plugins/security_solution/public/threat_intelligence/pages/threat_intelligence.tsx + * x-pack/plugins/security_solution/public/threat_intelligence/routes.tsx */ export const createApp = (services: Services) => @@ -40,7 +40,7 @@ export const createApp = ({ securitySolutionContext }: AppProps) => ( - + diff --git a/x-pack/plugins/threat_intelligence/public/types.ts b/x-pack/plugins/threat_intelligence/public/types.ts index 673a44494ac18..9b11c705a20d0 100644 --- a/x-pack/plugins/threat_intelligence/public/types.ts +++ b/x-pack/plugins/threat_intelligence/public/types.ts @@ -99,7 +99,7 @@ export interface SecuritySolutionPluginContext { /** * Security Solution store */ - getSecuritySolutionStore: Store; + securitySolutionStore: Store; /** * Pass UseInvestigateInTimeline functionality to TI plugin */ @@ -114,4 +114,14 @@ export interface SecuritySolutionPluginContext { useGlobalTime: () => TimeRange; SiemSearchBar: VFC; + + /** + * Register query in security solution store for tracking and centralized refresh support + */ + registerQuery: (query: { id: string; loading: boolean; refetch: VoidFunction }) => void; + + /** + * Deregister stale query + */ + deregisterQuery: (query: { id: string }) => void; } diff --git a/x-pack/plugins/timelines/common/constants.ts b/x-pack/plugins/timelines/common/constants.ts index e41333eb03697..779e32584a64e 100644 --- a/x-pack/plugins/timelines/common/constants.ts +++ b/x-pack/plugins/timelines/common/constants.ts @@ -5,23 +5,10 @@ * 2.0. */ -import { AlertStatus } from './types/timeline/actions'; - export const DEFAULT_MAX_TABLE_QUERY_SIZE = 10000; -export const DEFAULT_NUMBER_FORMAT = 'format:number:defaultPattern'; - -export const FILTER_OPEN: AlertStatus = 'open'; -export const FILTER_CLOSED: AlertStatus = 'closed'; - -/** - * @deprecated - * TODO: Remove after `acknowledged` migration - */ -export const FILTER_IN_PROGRESS: AlertStatus = 'in-progress'; -export const FILTER_ACKNOWLEDGED: AlertStatus = 'acknowledged'; - -export const RAC_ALERTS_BULK_UPDATE_URL = '/internal/rac/alerts/bulk_update'; -export const DETECTION_ENGINE_SIGNALS_STATUS_URL = '/api/detection_engine/signals/status'; export const DELETED_SECURITY_SOLUTION_DATA_VIEW = 'DELETED_SECURITY_SOLUTION_DATA_VIEW'; export const ENRICHMENT_DESTINATION_PATH = 'threat.enrichments'; + +/** The default minimum width of a column (when a width for the column type is not specified) */ +export const DEFAULT_COLUMN_MIN_WIDTH = 180; // px diff --git a/x-pack/plugins/timelines/common/ecs/index.ts b/x-pack/plugins/timelines/common/ecs/index.ts index faac79cfc2e74..6c3313668ba95 100644 --- a/x-pack/plugins/timelines/common/ecs/index.ts +++ b/x-pack/plugins/timelines/common/ecs/index.ts @@ -35,6 +35,9 @@ export type SignalEcsAAD = Exclude & { rule?: Exclude & { parameters: Record; uuid: string[] }; building_block_type?: string[]; workflow_status?: string[]; + suppression?: { + docs_count: string[]; + }; }; export interface Ecs { _id: string; diff --git a/x-pack/plugins/timelines/common/index.ts b/x-pack/plugins/timelines/common/index.ts index ac3d1da4bc759..e46907611fe5f 100644 --- a/x-pack/plugins/timelines/common/index.ts +++ b/x-pack/plugins/timelines/common/index.ts @@ -17,26 +17,14 @@ export { DELETED_SECURITY_SOLUTION_DATA_VIEW } from './constants'; export type { - ActionProps, - AlertWorkflowStatus, CellValueElementProps, - ColumnId, - ColumnRenderer, - ColumnHeaderType, - ColumnHeaderOptions, - ControlColumnProps, DataProvidersAnd, DataProvider, - GenericActionRowCellRenderProps, - HeaderActionProps, - HeaderCellRender, QueryOperator, QueryMatch, - RowCellRender, RowRenderer, - SetEventsDeleted, - SetEventsLoading, TimelineType, + ColumnHeaderOptions, } from './types'; export { IS_OPERATOR, EXISTS_OPERATOR, DataProviderType } from './types'; diff --git a/x-pack/plugins/timelines/common/search_strategy/timeline/events/all/index.ts b/x-pack/plugins/timelines/common/search_strategy/timeline/events/all/index.ts index 9f1dec9a2737b..2fa24cc2466ce 100644 --- a/x-pack/plugins/timelines/common/search_strategy/timeline/events/all/index.ts +++ b/x-pack/plugins/timelines/common/search_strategy/timeline/events/all/index.ts @@ -12,7 +12,7 @@ import type { IEsSearchResponse } from '@kbn/data-plugin/common'; import type { Ecs } from '../../../../ecs'; import type { CursorType, Inspect, Maybe, PaginationInputPaginated } from '../../../common'; import type { TimelineRequestOptionsPaginated } from '../..'; -import type { AlertStatus } from '../../../../types/timeline'; + export interface TimelineEdges { node: TimelineItem; cursor: CursorType; @@ -38,6 +38,7 @@ export interface TimelineEventsAllStrategyResponse extends IEsSearchResponse { inspect?: Maybe; } +type AlertWorkflowStatus = 'open' | 'closed' | 'acknowledged'; export interface TimelineEventsAllRequestOptions extends TimelineRequestOptionsPaginated { authFilter?: JsonObject; excludeEcsData?: boolean; @@ -45,5 +46,5 @@ export interface TimelineEventsAllRequestOptions extends TimelineRequestOptionsP fields: string[] | Array<{ field: string; include_unmapped: boolean }>; language: 'eql' | 'kuery' | 'lucene'; runtimeMappings: MappingRuntimeFields; - filterStatus?: AlertStatus; + filterStatus?: AlertWorkflowStatus; } diff --git a/x-pack/plugins/timelines/common/types/timeline/actions/index.ts b/x-pack/plugins/timelines/common/types/timeline/actions/index.ts index 76bd03b3a6725..a4c24b6f78f56 100644 --- a/x-pack/plugins/timelines/common/types/timeline/actions/index.ts +++ b/x-pack/plugins/timelines/common/types/timeline/actions/index.ts @@ -4,159 +4,83 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { ComponentType, JSXElementConstructor } from 'react'; -import { EuiDataGridControlColumn, EuiDataGridCellValueElementProps } from '@elastic/eui'; -// Temporary import from triggers-actions-ui public types, it will not be needed after alerts table migrated -import type { - FieldBrowserOptions, - CreateFieldComponent, - GetFieldTableColumns, - FieldBrowserProps, - BrowserFieldItem, -} from '@kbn/triggers-actions-ui-plugin/public/types'; - -import { OnRowSelected, SortColumnTable } from '..'; -import { BrowserFields } from '../../../search_strategy/index_fields'; -import { ColumnHeaderOptions } from '../columns'; -import { TimelineItem, TimelineNonEcsData } from '../../../search_strategy'; +import { EuiDataGridColumn, EuiDataGridColumnCellActionProps } from '@elastic/eui'; +import { IFieldSubType } from '@kbn/es-query'; +import { ReactNode } from 'react'; import { Ecs } from '../../../ecs'; +import { BrowserFields, TimelineNonEcsData } from '../../../search_strategy'; -export type { - FieldBrowserOptions, - CreateFieldComponent, - GetFieldTableColumns, - FieldBrowserProps, - BrowserFieldItem, -}; -export interface ActionProps { - action?: RowCellRender; - ariaRowindex: number; - checked: boolean; - columnId: string; - columnValues: string; - data: TimelineNonEcsData[]; - disabled?: boolean; - ecsData: Ecs; - eventId: string; - eventIdToNoteIds?: Readonly>; - index: number; - isEventPinned?: boolean; - isEventViewer?: boolean; - loadingEventIds: Readonly; - onEventDetailsPanelOpened: () => void; - onRowSelected: OnRowSelected; - onRuleChange?: () => void; - refetch?: () => void; - rowIndex: number; - setEventsDeleted: SetEventsDeleted; - setEventsLoading: SetEventsLoading; - showCheckboxes: boolean; - showNotes?: boolean; - tabType?: string; - timelineId: string; - toggleShowNotes?: () => void; - width?: number; -} - -export type SetEventsLoading = (params: { eventIds: string[]; isLoading: boolean }) => void; -export type SetEventsDeleted = (params: { eventIds: string[]; isDeleted: boolean }) => void; -export type OnUpdateAlertStatusSuccess = ( - updated: number, - conflicts: number, - status: AlertStatus -) => void; -export type OnUpdateAlertStatusError = (status: AlertStatus, error: Error) => void; - -export interface CustomBulkAction { - key: string; - label: string; - disableOnQuery?: boolean; - disabledLabel?: string; - onClick: (items?: TimelineItem[]) => void; - ['data-test-subj']?: string; -} +export type ColumnHeaderType = 'not-filtered' | 'text-filter'; -export type CustomBulkActionProp = Omit & { - onClick: (eventIds: string[]) => void; -}; - -export interface BulkActionsProps { - eventIds: string[]; - currentStatus?: AlertStatus; - query?: string; - indexName: string; - setEventsLoading: SetEventsLoading; - setEventsDeleted: SetEventsDeleted; - showAlertStatusActions?: boolean; - onUpdateSuccess?: OnUpdateAlertStatusSuccess; - onUpdateFailure?: OnUpdateAlertStatusError; - customBulkActions?: CustomBulkActionProp[]; -} - -export interface HeaderActionProps { - width: number; - browserFields: BrowserFields; - columnHeaders: ColumnHeaderOptions[]; - fieldBrowserOptions?: FieldBrowserOptions; - isEventViewer?: boolean; - isSelectAllChecked: boolean; - onSelectAll: ({ isSelected }: { isSelected: boolean }) => void; - showEventsSelect: boolean; - showSelectAllCheckbox: boolean; - sort: SortColumnTable[]; - tabType: string; - timelineId: string; -} - -export type GenericActionRowCellRenderProps = Pick< - EuiDataGridCellValueElementProps, - 'rowIndex' | 'columnId' ->; - -export type HeaderCellRender = ComponentType | ComponentType; -export type RowCellRender = - | JSXElementConstructor - | ((props: GenericActionRowCellRenderProps) => JSX.Element) - | JSXElementConstructor - | ((props: ActionProps) => JSX.Element); - -interface AdditionalControlColumnProps { - ariaRowindex: number; - actionsColumnWidth: number; - columnValues: string; - checked: boolean; - onRowSelected: OnRowSelected; - eventId: string; - id: string; - columnId: string; - loadingEventIds: Readonly; - onEventDetailsPanelOpened: () => void; - showCheckboxes: boolean; - // Override these type definitions to support either a generic custom component or the one used in security_solution today. - headerCellRender: HeaderCellRender; - rowCellRender: RowCellRender; -} - -export type ControlColumnProps = Omit< - EuiDataGridControlColumn, - keyof AdditionalControlColumnProps -> & - Partial; -export interface BulkActionsObjectProp { - alertStatusActions?: boolean; - onAlertStatusActionSuccess?: OnUpdateAlertStatusSuccess; - onAlertStatusActionFailure?: OnUpdateAlertStatusError; - customBulkActions?: CustomBulkAction[]; -} -export type BulkActionsProp = boolean | BulkActionsObjectProp; - -export type AlertWorkflowStatus = 'open' | 'closed' | 'acknowledged'; +export const defaultColumnHeaderType: ColumnHeaderType = 'not-filtered'; /** - * @deprecated - * TODO: remove when `acknowledged` migrations are finished + * A `DataTableCellAction` function accepts `data`, where each row of data is + * represented as a `TimelineNonEcsData[]`. For example, `data[0]` would + * contain a `TimelineNonEcsData[]` with the first row of data. + * + * A `DataTableCellAction` returns a function that has access to all the + * `EuiDataGridColumnCellActionProps`, _plus_ access to `data`, + * which enables code like the following example to be written: + * + * Example: + * ``` + * ({ data }: { data: TimelineNonEcsData[][] }) => ({ rowIndex, columnId, Component }) => { + * const value = getMappedNonEcsValue({ + * data: data[rowIndex], // access a specific row's values + * fieldName: columnId, + * }); + * + * return ( + * alert(`row ${rowIndex} col ${columnId} has value ${value}`)} iconType="heart"> + * {'Love it'} + * + * ); + * }; + * ``` */ -export type InProgressStatus = 'in-progress'; +export type DataTableCellAction = ({ + browserFields, + data, + ecsData, + header, + pageSize, + scopeId, + closeCellPopover, +}: { + browserFields: BrowserFields; + /** each row of data is represented as one TimelineNonEcsData[] */ + data: TimelineNonEcsData[][]; + ecsData: Ecs[]; + header?: ColumnHeaderOptions; + pageSize: number; + scopeId: string; + closeCellPopover?: () => void; +}) => (props: EuiDataGridColumnCellActionProps) => ReactNode; -export type AlertStatus = AlertWorkflowStatus | InProgressStatus; +/** The specification of a column header */ +export type ColumnHeaderOptions = Pick< + EuiDataGridColumn, + | 'actions' + | 'defaultSortDirection' + | 'display' + | 'displayAsText' + | 'id' + | 'initialWidth' + | 'isSortable' + | 'schema' +> & { + aggregatable?: boolean; + dataTableCellActions?: DataTableCellAction[]; + category?: string; + columnHeaderType: ColumnHeaderType; + description?: string | null; + esTypes?: string[]; + example?: string | number | null; + format?: string; + linkField?: string; + placeholder?: string; + subType?: IFieldSubType; + type?: string; +}; diff --git a/x-pack/plugins/timelines/common/types/timeline/cells/index.ts b/x-pack/plugins/timelines/common/types/timeline/cells/index.ts index 52130cf52354d..923d9092f1c5b 100644 --- a/x-pack/plugins/timelines/common/types/timeline/cells/index.ts +++ b/x-pack/plugins/timelines/common/types/timeline/cells/index.ts @@ -7,10 +7,9 @@ import { EuiDataGridCellValueElementProps } from '@elastic/eui'; import type { Filter } from '@kbn/es-query'; -import { RowRenderer } from '../..'; +import { ColumnHeaderOptions, RowRenderer } from '../..'; import { Ecs } from '../../../ecs'; import { BrowserFields, TimelineNonEcsData } from '../../../search_strategy'; -import { ColumnHeaderOptions } from '../columns'; /** The following props are provided to the function called by `renderCellValue` */ export type CellValueElementProps = EuiDataGridCellValueElementProps & { @@ -31,4 +30,5 @@ export type CellValueElementProps = EuiDataGridCellValueElementProps & { truncate?: boolean; key?: string; closeCellPopover?: () => void; + enableActions?: boolean; }; diff --git a/x-pack/plugins/timelines/common/types/timeline/columns/index.tsx b/x-pack/plugins/timelines/common/types/timeline/columns/index.tsx deleted file mode 100644 index 254642fda4738..0000000000000 --- a/x-pack/plugins/timelines/common/types/timeline/columns/index.tsx +++ /dev/null @@ -1,117 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { ReactNode } from 'react'; - -import { EuiDataGridColumn, EuiDataGridColumnCellActionProps } from '@elastic/eui'; -import type { IFieldSubType } from '@kbn/es-query'; -import { BrowserFields } from '../../../search_strategy/index_fields'; -import { TimelineNonEcsData } from '../../../search_strategy/timeline'; -import { Ecs } from '../../../ecs'; - -export type ColumnHeaderType = 'not-filtered' | 'text-filter'; - -/** Uniquely identifies a column */ -export type ColumnId = string; - -/** - * A `TGridCellAction` function accepts `data`, where each row of data is - * represented as a `TimelineNonEcsData[]`. For example, `data[0]` would - * contain a `TimelineNonEcsData[]` with the first row of data. - * - * A `TGridCellAction` returns a function that has access to all the - * `EuiDataGridColumnCellActionProps`, _plus_ access to `data`, - * which enables code like the following example to be written: - * - * Example: - * ``` - * ({ data }: { data: TimelineNonEcsData[][] }) => ({ rowIndex, columnId, Component }) => { - * const value = getMappedNonEcsValue({ - * data: data[rowIndex], // access a specific row's values - * fieldName: columnId, - * }); - * - * return ( - * alert(`row ${rowIndex} col ${columnId} has value ${value}`)} iconType="heart"> - * {'Love it'} - * - * ); - * }; - * ``` - */ -export type TGridCellAction = ({ - browserFields, - data, - ecsData, - header, - pageSize, - scopeId, - closeCellPopover, -}: { - browserFields: BrowserFields; - /** each row of data is represented as one TimelineNonEcsData[] */ - data: TimelineNonEcsData[][]; - ecsData: Ecs[]; - header?: ColumnHeaderOptions; - pageSize: number; - scopeId: string; - closeCellPopover?: () => void; -}) => (props: EuiDataGridColumnCellActionProps) => ReactNode; - -/** The specification of a column header */ -export type ColumnHeaderOptions = Pick< - EuiDataGridColumn, - | 'actions' - | 'defaultSortDirection' - | 'display' - | 'displayAsText' - | 'id' - | 'initialWidth' - | 'isSortable' - | 'schema' -> & { - aggregatable?: boolean; - tGridCellActions?: TGridCellAction[]; - category?: string; - columnHeaderType: ColumnHeaderType; - description?: string | null; - esTypes?: string[]; - example?: string | number | null; - format?: string; - linkField?: string; - placeholder?: string; - subType?: IFieldSubType; - type?: string; -}; - -export interface ColumnRenderer { - isInstance: (columnName: string, data: TimelineNonEcsData[]) => boolean; - renderColumn: ({ - columnName, - eventId, - field, - timelineId, - truncate, - values, - linkValues, - }: { - columnName: string; - eventId: string; - field: ColumnHeaderOptions; - timelineId: string; - truncate?: boolean; - values: string[] | null | undefined; - linkValues?: string[] | null | undefined; - }) => React.ReactNode; -} - -export interface SessionViewConfig { - sessionEntityId: string; - jumpToEntityId?: string; - jumpToCursor?: string; - investigatedAlertId?: string; -} diff --git a/x-pack/plugins/timelines/common/types/timeline/index.ts b/x-pack/plugins/timelines/common/types/timeline/index.ts index 3debe94be39b1..ae3a5969f84c9 100644 --- a/x-pack/plugins/timelines/common/types/timeline/index.ts +++ b/x-pack/plugins/timelines/common/types/timeline/index.ts @@ -13,10 +13,8 @@ import { Ecs } from '../../ecs'; export * from './actions'; export * from './cells'; -export * from './columns'; export * from './data_provider'; export * from './rows'; -export * from './store'; /* * DataProvider Types diff --git a/x-pack/plugins/timelines/common/types/timeline/store.ts b/x-pack/plugins/timelines/common/types/timeline/store.ts deleted file mode 100644 index 57b24b24dfa43..0000000000000 --- a/x-pack/plugins/timelines/common/types/timeline/store.ts +++ /dev/null @@ -1,98 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type { Filter } from '@kbn/es-query'; -import { - ColumnHeaderOptions, - ColumnId, - RowRendererId, - Sort, - DataExpandedDetail, - TimelineTypeLiteral, -} from '.'; - -import { Direction } from '../../search_strategy'; -import { DataProvider } from './data_provider'; - -export type KueryFilterQueryKind = 'kuery' | 'lucene' | 'eql'; - -export interface KueryFilterQuery { - kind: KueryFilterQueryKind; - expression: string; -} - -export interface SerializedFilterQuery { - kuery: KueryFilterQuery | null; - serializedQuery: string; -} - -export type SortDirection = 'none' | 'asc' | 'desc' | Direction; -export interface SortColumnTable { - columnId: string; - columnType: string; - esTypes?: string[]; - sortDirection: SortDirection; -} - -export interface TimelinePersistInput { - id: string; - dataProviders?: DataProvider[]; - dateRange?: { - start: string; - end: string; - }; - excludedRowRendererIds?: RowRendererId[]; - expandedDetail?: DataExpandedDetail; - filters?: Filter[]; - columns: ColumnHeaderOptions[]; - itemsPerPage?: number; - indexNames: string[]; - kqlQuery?: { - filterQuery: SerializedFilterQuery | null; - }; - show?: boolean; - sort?: Sort[]; - showCheckboxes?: boolean; - timelineType?: TimelineTypeLiteral; - templateTimelineId?: string | null; - templateTimelineVersion?: number | null; -} - -/** Invoked when a column is sorted */ -export type OnColumnSorted = (sorted: { columnId: ColumnId; sortDirection: SortDirection }) => void; - -export type OnColumnsSorted = ( - sorted: Array<{ columnId: ColumnId; sortDirection: SortDirection }> -) => void; - -export type OnColumnRemoved = (columnId: ColumnId) => void; - -export type OnColumnResized = ({ columnId, delta }: { columnId: ColumnId; delta: number }) => void; - -/** Invoked when a user clicks to load more item */ -export type OnChangePage = (nextPage: number) => void; - -/** Invoked when a user checks/un-checks a row */ -export type OnRowSelected = ({ - eventIds, - isSelected, -}: { - eventIds: string[]; - isSelected: boolean; -}) => void; - -/** Invoked when a user checks/un-checks the select all checkbox */ -export type OnSelectAll = ({ isSelected }: { isSelected: boolean }) => void; - -/** Invoked when columns are updated */ -export type OnUpdateColumns = (columns: ColumnHeaderOptions[]) => void; - -/** Invoked when a user pins an event */ -export type OnPinEvent = (eventId: string) => void; - -/** Invoked when a user unpins an event */ -export type OnUnPinEvent = (eventId: string) => void; diff --git a/x-pack/plugins/timelines/kibana.json b/x-pack/plugins/timelines/kibana.json index 52e125d678af5..77a5dcc699bdc 100644 --- a/x-pack/plugins/timelines/kibana.json +++ b/x-pack/plugins/timelines/kibana.json @@ -10,6 +10,6 @@ "extraPublicDirs": ["common"], "server": true, "ui": true, - "requiredPlugins": ["alerting", "cases", "data", "kibanaReact", "kibanaUtils", "triggersActionsUi"], + "requiredPlugins": ["alerting", "cases", "data", "kibanaReact", "kibanaUtils"], "optionalPlugins": ["security"] } diff --git a/x-pack/plugins/timelines/public/components/clipboard/index.tsx b/x-pack/plugins/timelines/public/components/clipboard/clipboard.tsx similarity index 100% rename from x-pack/plugins/timelines/public/components/clipboard/index.tsx rename to x-pack/plugins/timelines/public/components/clipboard/clipboard.tsx diff --git a/x-pack/plugins/timelines/public/components/clipboard/translations.ts b/x-pack/plugins/timelines/public/components/clipboard/translations.ts index 25b5ea7bf7b83..a92c9656f3cf8 100644 --- a/x-pack/plugins/timelines/public/components/clipboard/translations.ts +++ b/x-pack/plugins/timelines/public/components/clipboard/translations.ts @@ -25,9 +25,3 @@ export const COPY_TO_THE_CLIPBOARD = i18n.translate( defaultMessage: 'Copy to the clipboard', } ); - -export const SUCCESS_TOAST_TITLE = (field: string) => - i18n.translate('xpack.timelines.clipboard.copy.successToastTitle', { - values: { field }, - defaultMessage: 'Copied field {field} to the clipboard', - }); diff --git a/x-pack/plugins/timelines/public/components/clipboard/with_copy_to_clipboard.tsx b/x-pack/plugins/timelines/public/components/clipboard/with_copy_to_clipboard.tsx index 714e2c5fcb8fe..24ce4c39dd2c0 100644 --- a/x-pack/plugins/timelines/public/components/clipboard/with_copy_to_clipboard.tsx +++ b/x-pack/plugins/timelines/public/components/clipboard/with_copy_to_clipboard.tsx @@ -8,13 +8,10 @@ import { EuiToolTip } from '@elastic/eui'; import React from 'react'; -import { i18n } from '@kbn/i18n'; +import { COPY_TO_CLIPBOARD } from '../hover_actions/actions/translations'; import { TooltipWithKeyboardShortcut } from '../tooltip_with_keyboard_shortcut'; -import { Clipboard } from '.'; -export const COPY_TO_CLIPBOARD = i18n.translate('xpack.timelines.copyToClipboardTooltip', { - defaultMessage: 'Copy to Clipboard', -}); +import { Clipboard } from './clipboard'; /** * Renders `children` with an adjacent icon that when clicked, copies `text` to diff --git a/x-pack/plugins/timelines/public/components/drag_and_drop/draggable_keyboard_wrapper_hook/index.tsx b/x-pack/plugins/timelines/public/components/drag_and_drop/draggable_keyboard_wrapper_hook/index.tsx deleted file mode 100644 index 9eb5d7dc640c7..0000000000000 --- a/x-pack/plugins/timelines/public/components/drag_and_drop/draggable_keyboard_wrapper_hook/index.tsx +++ /dev/null @@ -1,103 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 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 type { FluidDragActions } from 'react-beautiful-dnd'; - -import { useAddToTimeline } from '../../../hooks/use_add_to_timeline'; - -import { draggableKeyDownHandler } from '../helpers'; - -export interface UseDraggableKeyboardWrapperProps { - closePopover?: () => void; - draggableId: string; - fieldName: string; - keyboardHandlerRef: React.MutableRefObject; - openPopover?: () => void; -} - -export interface UseDraggableKeyboardWrapper { - onBlur: () => void; - onKeyDown: (keyboardEvent: React.KeyboardEvent) => void; -} - -export const useDraggableKeyboardWrapper = ({ - closePopover, - draggableId, - fieldName, - keyboardHandlerRef, - openPopover, -}: UseDraggableKeyboardWrapperProps): UseDraggableKeyboardWrapper => { - const { beginDrag, cancelDrag, dragToLocation, endDrag, hasDraggableLock } = useAddToTimeline({ - draggableId, - fieldName, - }); - const [dragActions, setDragActions] = useState(null); - - const cancelDragActions = useCallback(() => { - setDragActions((prevDragAction) => { - if (prevDragAction) { - cancelDrag(prevDragAction); - return null; - } - return null; - }); - }, [cancelDrag]); - - const onKeyDown = useCallback( - (keyboardEvent: React.KeyboardEvent) => { - const draggableElement = document.querySelector( - `[data-rbd-drag-handle-draggable-id="${draggableId}"]` - ); - - if (draggableElement) { - if (hasDraggableLock() || (!hasDraggableLock() && keyboardEvent.key === ' ')) { - keyboardEvent.preventDefault(); - keyboardEvent.stopPropagation(); - } - - draggableKeyDownHandler({ - beginDrag, - cancelDragActions, - closePopover, - dragActions, - draggableElement, - dragToLocation, - endDrag, - keyboardEvent, - openPopover, - setDragActions, - }); - - keyboardHandlerRef.current?.focus(); // to handle future key presses - } - }, - [ - beginDrag, - cancelDragActions, - closePopover, - dragActions, - draggableId, - dragToLocation, - endDrag, - hasDraggableLock, - keyboardHandlerRef, - openPopover, - setDragActions, - ] - ); - - const memoizedReturn = useMemo( - () => ({ - onBlur: cancelDragActions, - onKeyDown, - }), - [cancelDragActions, onKeyDown] - ); - - return memoizedReturn; -}; diff --git a/x-pack/plugins/timelines/public/components/drag_and_drop/helpers.ts b/x-pack/plugins/timelines/public/components/drag_and_drop/helpers.ts deleted file mode 100644 index 60163006f5878..0000000000000 --- a/x-pack/plugins/timelines/public/components/drag_and_drop/helpers.ts +++ /dev/null @@ -1,211 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type { DropResult, FluidDragActions, Position } from 'react-beautiful-dnd'; -import { KEYBOARD_DRAG_OFFSET, getFieldIdFromDraggable } from '@kbn/securitysolution-t-grid'; -import { Dispatch } from 'redux'; -import { isString, keyBy } from 'lodash/fp'; - -import { stopPropagationAndPreventDefault } from '../../../common/utils/accessibility'; -import type { BrowserField, BrowserFields } from '../../../common/search_strategy'; -import type { ColumnHeaderOptions } from '../../../common/types'; -import { TableId, tGridActions } from '../../store/t_grid'; -import { DEFAULT_COLUMN_MIN_WIDTH } from '../t_grid/body/constants'; - -/** - * Temporarily disables tab focus on child links of the draggable to work - * around an issue where tab focus becomes stuck on the interactive children - * - * NOTE: This function is (intentionally) only effective when used in a key - * event handler, because it automatically restores focus capabilities on - * the next tick. - */ -export const temporarilyDisableInteractiveChildTabIndexes = (draggableElement: HTMLDivElement) => { - const interactiveChildren = draggableElement.querySelectorAll('a, button'); - interactiveChildren.forEach((interactiveChild) => { - interactiveChild.setAttribute('tabindex', '-1'); // DOM mutation - }); - - // restore the default tabindexs on the next tick: - setTimeout(() => { - interactiveChildren.forEach((interactiveChild) => { - interactiveChild.setAttribute('tabindex', '0'); // DOM mutation - }); - }, 0); -}; - -export interface DraggableKeyDownHandlerProps { - beginDrag: () => FluidDragActions | null; - cancelDragActions: () => void; - closePopover?: () => void; - draggableElement: HTMLDivElement; - dragActions: FluidDragActions | null; - dragToLocation: ({ - dragActions, - position, - }: { - dragActions: FluidDragActions | null; - position: Position; - }) => void; - keyboardEvent: React.KeyboardEvent; - endDrag: (dragActions: FluidDragActions | null) => void; - openPopover?: () => void; - setDragActions: (value: React.SetStateAction) => void; -} - -export const draggableKeyDownHandler = ({ - beginDrag, - cancelDragActions, - closePopover, - draggableElement, - dragActions, - dragToLocation, - endDrag, - keyboardEvent, - openPopover, - setDragActions, -}: DraggableKeyDownHandlerProps) => { - let currentPosition: DOMRect | null = null; - - switch (keyboardEvent.key) { - case ' ': - if (!dragActions) { - // start dragging, because space was pressed - if (closePopover != null) { - closePopover(); - } - setDragActions(beginDrag()); - } else { - // end dragging, because space was pressed - endDrag(dragActions); - setDragActions(null); - } - break; - case 'Escape': - cancelDragActions(); - break; - case 'Tab': - // IMPORTANT: we do NOT want to stop propagation and prevent default when Tab is pressed - temporarilyDisableInteractiveChildTabIndexes(draggableElement); - break; - case 'ArrowUp': - currentPosition = draggableElement.getBoundingClientRect(); - dragToLocation({ - dragActions, - position: { x: currentPosition.x, y: currentPosition.y - KEYBOARD_DRAG_OFFSET }, - }); - break; - case 'ArrowDown': - currentPosition = draggableElement.getBoundingClientRect(); - dragToLocation({ - dragActions, - position: { x: currentPosition.x, y: currentPosition.y + KEYBOARD_DRAG_OFFSET }, - }); - break; - case 'ArrowLeft': - currentPosition = draggableElement.getBoundingClientRect(); - dragToLocation({ - dragActions, - position: { x: currentPosition.x - KEYBOARD_DRAG_OFFSET, y: currentPosition.y }, - }); - break; - case 'ArrowRight': - currentPosition = draggableElement.getBoundingClientRect(); - dragToLocation({ - dragActions, - position: { x: currentPosition.x + KEYBOARD_DRAG_OFFSET, y: currentPosition.y }, - }); - break; - case 'Enter': - stopPropagationAndPreventDefault(keyboardEvent); // prevents the first item in the popover from getting an errant ENTER - if (!dragActions && openPopover != null) { - openPopover(); - } - break; - default: - break; - } -}; -const getAllBrowserFields = (browserFields: BrowserFields): Array> => - Object.values(browserFields).reduce>>( - (acc, namespace) => [ - ...acc, - ...Object.values(namespace.fields != null ? namespace.fields : {}), - ], - [] - ); - -const getAllFieldsByName = ( - browserFields: BrowserFields -): { [fieldName: string]: Partial } => - keyBy('name', getAllBrowserFields(browserFields)); - -const linkFields: Record = { - 'kibana.alert.rule.name': 'kibana.alert.rule.uuid', - 'event.module': 'rule.reference', -}; - -interface AddFieldToTimelineColumnsParams { - defaultsHeader: ColumnHeaderOptions[]; - browserFields: BrowserFields; - dispatch: Dispatch; - result: DropResult; - timelineId: string; -} - -export const addFieldToTimelineColumns = ({ - browserFields, - dispatch, - result, - timelineId, - defaultsHeader, -}: AddFieldToTimelineColumnsParams): void => { - const fieldId = getFieldIdFromDraggable(result); - const allColumns = getAllFieldsByName(browserFields); - const column = allColumns[fieldId]; - const initColumnHeader = - timelineId === TableId.alertsOnAlertsPage || timelineId === TableId.alertsOnRuleDetailsPage - ? defaultsHeader.find((c) => c.id === fieldId) ?? {} - : {}; - - if (column != null) { - dispatch( - tGridActions.upsertColumn({ - column: { - category: column.category, - columnHeaderType: 'not-filtered', - description: isString(column.description) ? column.description : undefined, - example: isString(column.example) ? column.example : undefined, - id: fieldId, - linkField: linkFields[fieldId] ?? undefined, - type: column.type, - aggregatable: column.aggregatable, - initialWidth: DEFAULT_COLUMN_MIN_WIDTH, - ...initColumnHeader, - }, - id: timelineId, - index: result.destination != null ? result.destination.index : 0, - }) - ); - } else { - // create a column definition, because it doesn't exist in the browserFields: - dispatch( - tGridActions.upsertColumn({ - column: { - columnHeaderType: 'not-filtered', - id: fieldId, - initialWidth: DEFAULT_COLUMN_MIN_WIDTH, - }, - id: timelineId, - index: result.destination != null ? result.destination.index : 0, - }) - ); - } -}; - -export const getTimelineIdFromColumnDroppableId = (droppableId: string) => - droppableId.slice(droppableId.lastIndexOf('.') + 1); diff --git a/x-pack/plugins/timelines/public/components/drag_and_drop/index.tsx b/x-pack/plugins/timelines/public/components/drag_and_drop/index.tsx deleted file mode 100644 index 33c568ed0231c..0000000000000 --- a/x-pack/plugins/timelines/public/components/drag_and_drop/index.tsx +++ /dev/null @@ -1,94 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { - IS_DRAGGING_CLASS_NAME, - draggableIsField, - fieldWasDroppedOnTimelineColumns, - IS_TIMELINE_FIELD_DRAGGING_CLASS_NAME, -} from '@kbn/securitysolution-t-grid'; -import { noop } from 'lodash/fp'; -import deepEqual from 'fast-deep-equal'; -import React, { useCallback } from 'react'; -import { DropResult, DragDropContext, BeforeCapture } from 'react-beautiful-dnd'; -import { useDispatch } from 'react-redux'; - -import type { BrowserFields } from '../../../common/search_strategy'; -import type { ColumnHeaderOptions } from '../../../common/types'; -import { useAddToTimelineSensor } from '../../hooks/use_add_to_timeline'; -import { addFieldToTimelineColumns, getTimelineIdFromColumnDroppableId } from './helpers'; - -export * from './draggable_keyboard_wrapper_hook'; -export * from './helpers'; - -interface Props { - browserFields: BrowserFields; - defaultsHeader: ColumnHeaderOptions[]; - children: React.ReactNode; -} - -const sensors = [useAddToTimelineSensor]; - -const DragDropContextWrapperComponent: React.FC = ({ - browserFields, - defaultsHeader, - children, -}) => { - const dispatch = useDispatch(); - - const onDragEnd = useCallback( - (result: DropResult) => { - try { - enableScrolling(); - - if (fieldWasDroppedOnTimelineColumns(result)) { - addFieldToTimelineColumns({ - browserFields, - defaultsHeader, - dispatch, - result, - timelineId: getTimelineIdFromColumnDroppableId(result.destination?.droppableId ?? ''), - }); - } - } finally { - document.body.classList.remove(IS_DRAGGING_CLASS_NAME); - - if (draggableIsField(result)) { - document.body.classList.remove(IS_TIMELINE_FIELD_DRAGGING_CLASS_NAME); - } - } - }, - [browserFields, defaultsHeader, dispatch] - ); - return ( - - {children} - - ); -}; - -DragDropContextWrapperComponent.displayName = 'DragDropContextWrapperComponent'; - -export const DragDropContextWrapper = React.memo( - DragDropContextWrapperComponent, - // prevent re-renders when data providers are added or removed, but all other props are the same - (prevProps, nextProps) => deepEqual(prevProps.children, nextProps.children) -); - -DragDropContextWrapper.displayName = 'DragDropContextWrapper'; - -const onBeforeCapture = (before: BeforeCapture) => { - if (!draggableIsField(before)) { - document.body.classList.add(IS_DRAGGING_CLASS_NAME); - } - - if (draggableIsField(before)) { - document.body.classList.add(IS_TIMELINE_FIELD_DRAGGING_CLASS_NAME); - } -}; - -const enableScrolling = () => (window.onscroll = () => noop); diff --git a/x-pack/plugins/timelines/public/components/empty_value/empty_value.test.tsx b/x-pack/plugins/timelines/public/components/empty_value/empty_value.test.tsx deleted file mode 100644 index 78f0ec9f42712..0000000000000 --- a/x-pack/plugins/timelines/public/components/empty_value/empty_value.test.tsx +++ /dev/null @@ -1,14 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { getEmptyValue } from '.'; - -describe('EmptyValue', () => { - describe('#getEmptyValue', () => { - test('should return an empty value', () => expect(getEmptyValue()).toBe('—')); - }); -}); diff --git a/x-pack/plugins/timelines/public/components/empty_value/index.tsx b/x-pack/plugins/timelines/public/components/empty_value/index.tsx deleted file mode 100644 index 9de9e22eb5b89..0000000000000 --- a/x-pack/plugins/timelines/public/components/empty_value/index.tsx +++ /dev/null @@ -1,8 +0,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. - */ - -export const getEmptyValue = () => '—'; diff --git a/x-pack/plugins/timelines/public/components/hover_actions/actions/add_to_timeline.tsx b/x-pack/plugins/timelines/public/components/hover_actions/actions/add_to_timeline.tsx index 706fb0f243e15..dcd86b494e546 100644 --- a/x-pack/plugins/timelines/public/components/hover_actions/actions/add_to_timeline.tsx +++ b/x-pack/plugins/timelines/public/components/hover_actions/actions/add_to_timeline.tsx @@ -12,7 +12,7 @@ import { isEmpty } from 'lodash'; import { useDispatch } from 'react-redux'; import { toMountPoint } from '@kbn/kibana-react-plugin/public'; -import { TimelineId } from '../../../types'; +import { TimelineId } from '../../../store/timeline'; import { addProviderToTimeline } from '../../../store/timeline/actions'; import { stopPropagationAndPreventDefault } from '../../../../common/utils/accessibility'; import { DataProvider } from '../../../../common/types'; diff --git a/x-pack/plugins/timelines/public/components/hover_actions/actions/column_toggle.tsx b/x-pack/plugins/timelines/public/components/hover_actions/actions/column_toggle.tsx index 9a6c3b51e3f6c..728d144d91e3c 100644 --- a/x-pack/plugins/timelines/public/components/hover_actions/actions/column_toggle.tsx +++ b/x-pack/plugins/timelines/public/components/hover_actions/actions/column_toggle.tsx @@ -9,12 +9,11 @@ import React, { useCallback, useEffect, useMemo } from 'react'; import { EuiContextMenuItem, EuiButtonEmpty, EuiButtonIcon, EuiToolTip } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import { DEFAULT_COLUMN_MIN_WIDTH } from '../../../../common/constants'; +import { ColumnHeaderOptions, defaultColumnHeaderType } from '../../../../common/types'; import { stopPropagationAndPreventDefault } from '../../../../common/utils/accessibility'; import { TooltipWithKeyboardShortcut } from '../../tooltip_with_keyboard_shortcut'; import { getAdditionalScreenReaderOnlyContext } from '../utils'; -import { defaultColumnHeaderType } from '../../t_grid/body/column_headers/default_headers'; -import { DEFAULT_COLUMN_MIN_WIDTH } from '../../t_grid/body/constants'; -import { ColumnHeaderOptions } from '../../../../common/types/timeline'; import { HoverActionComponentProps } from './types'; export const COLUMN_TOGGLE = (field: string) => diff --git a/x-pack/plugins/timelines/public/components/hover_actions/actions/copy.tsx b/x-pack/plugins/timelines/public/components/hover_actions/actions/copy.tsx index 3c4b7379188b9..8ccc08bf22264 100644 --- a/x-pack/plugins/timelines/public/components/hover_actions/actions/copy.tsx +++ b/x-pack/plugins/timelines/public/components/hover_actions/actions/copy.tsx @@ -11,18 +11,17 @@ import React, { useCallback, useEffect, useMemo, useRef } from 'react'; import { i18n } from '@kbn/i18n'; import { stopPropagationAndPreventDefault } from '../../../../common/utils/accessibility'; -import { WithCopyToClipboard } from '../../clipboard/with_copy_to_clipboard'; import { HoverActionComponentProps } from './types'; -import { COPY_TO_CLIPBOARD_BUTTON_CLASS_NAME } from '../../clipboard'; import { useAppToasts } from '../../../hooks/use_app_toasts'; -import { COPY_TO_CLIPBOARD } from '../../t_grid/body/translations'; -import { SUCCESS_TOAST_TITLE } from '../../clipboard/translations'; +import { COPY_TO_CLIPBOARD, SUCCESS_TOAST_TITLE } from './translations'; +import { WithCopyToClipboard } from '../../clipboard/with_copy_to_clipboard'; export const FIELD = i18n.translate('xpack.timelines.hoverActions.fieldLabel', { defaultMessage: 'Field', }); export const COPY_TO_CLIPBOARD_KEYBOARD_SHORTCUT = 'c'; +export const COPY_TO_CLIPBOARD_BUTTON_CLASS_NAME = 'copy-to-clipboard'; export interface CopyProps extends HoverActionComponentProps { /** `Component` is only used with `EuiDataGrid`; the grid keeps a reference to `Component` for show / hide functionality */ diff --git a/x-pack/plugins/timelines/public/components/hover_actions/actions/translations.tsx b/x-pack/plugins/timelines/public/components/hover_actions/actions/translations.tsx index 10b20377f6c1c..3af1ca445bf1f 100644 --- a/x-pack/plugins/timelines/public/components/hover_actions/actions/translations.tsx +++ b/x-pack/plugins/timelines/public/components/hover_actions/actions/translations.tsx @@ -16,3 +16,16 @@ export const ADDED_TO_TIMELINE_OR_TEMPLATE_MESSAGE = (fieldOrValue: string, isTi values: { fieldOrValue, isTimeline }, defaultMessage: `Added {fieldOrValue} to {isTimeline, select, true {timeline} false {template}}`, }); + +export const COPY_TO_CLIPBOARD = i18n.translate( + 'xpack.timelines.dragAndDrop.copyToClipboardTooltip', + { + defaultMessage: 'Copy to Clipboard', + } +); + +export const SUCCESS_TOAST_TITLE = (field: string) => + i18n.translate('xpack.timelines.clipboard.copy.successToastTitle', { + values: { field }, + defaultMessage: 'Copied field {field} to the clipboard', + }); diff --git a/x-pack/plugins/timelines/public/components/index.tsx b/x-pack/plugins/timelines/public/components/index.tsx index cc8446eea1411..323afe334093f 100644 --- a/x-pack/plugins/timelines/public/components/index.tsx +++ b/x-pack/plugins/timelines/public/components/index.tsx @@ -5,49 +5,5 @@ * 2.0. */ -import React from 'react'; -import { I18nProvider } from '@kbn/i18n-react'; -import type { Store } from 'redux'; - -import { Storage } from '@kbn/kibana-utils-plugin/public'; -import type { DataPublicPluginStart } from '@kbn/data-plugin/public'; - -import { Provider } from 'react-redux'; -import { TGrid as TGridComponent } from './t_grid'; -import type { TGridProps } from '../types'; -import { DragDropContextWrapper } from './drag_and_drop'; -import type { TGridIntegratedProps } from './t_grid/integrated'; - -const EMPTY_BROWSER_FIELDS = {}; - -type TGridComponent = TGridProps & { - store?: Store; - storage: Storage; - data?: DataPublicPluginStart; - setStore: (store: Store) => void; -}; - -export const TGrid = (props: TGridComponent) => { - const { store, storage, setStore, ...tGridProps } = props; - let browserFields = EMPTY_BROWSER_FIELDS; - if ((tGridProps as TGridIntegratedProps).browserFields != null) { - browserFields = (tGridProps as TGridIntegratedProps).browserFields; - } - return ( - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - - - - - - - - ); -}; - -// eslint-disable-next-line import/no-default-export -export { TGrid as default }; - -export * from './drag_and_drop'; export * from './last_updated'; export * from './loading'; diff --git a/x-pack/plugins/timelines/public/components/inspect/index.test.tsx b/x-pack/plugins/timelines/public/components/inspect/index.test.tsx deleted file mode 100644 index 5d8af0a0653bd..0000000000000 --- a/x-pack/plugins/timelines/public/components/inspect/index.test.tsx +++ /dev/null @@ -1,105 +0,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 { mount } from 'enzyme'; -import React from 'react'; -import { cloneDeep } from 'lodash/fp'; - -import { InspectButton, InspectButtonContainer, BUTTON_CLASS, InspectButtonProps } from '.'; - -describe('Inspect Button', () => { - const newQuery: InspectButtonProps = { - inspect: null, - loading: false, - title: 'My title', - }; - - describe('Render', () => { - test('Eui Icon Button', () => { - const wrapper = mount(); - expect(wrapper.find('button[data-test-subj="inspect-icon-button"]').first().exists()).toBe( - true - ); - }); - - test('Eui Icon Button disabled', () => { - const wrapper = mount(); - expect(wrapper.find('.euiButtonIcon').get(0).props.disabled).toBe(true); - }); - - describe('InspectButtonContainer', () => { - test('it renders a transparent inspect button by default', async () => { - const wrapper = mount( - - - - ); - - expect(wrapper.find(`InspectButtonContainer`)).toHaveStyleRule('opacity', '0', { - modifier: `.${BUTTON_CLASS}`, - }); - }); - - test('it renders an opaque inspect button when it has mouse focus', async () => { - const wrapper = mount( - - - - ); - - expect(wrapper.find(`InspectButtonContainer`)).toHaveStyleRule('opacity', '1', { - modifier: `:hover .${BUTTON_CLASS}`, - }); - }); - }); - }); - - describe('Modal Inspect - happy path', () => { - const myQuery = cloneDeep(newQuery); - beforeEach(() => { - myQuery.inspect = { - dsl: ['my dsl'], - response: ['my response'], - }; - }); - test('Open Inspect Modal', () => { - const wrapper = mount(); - - wrapper.find('button[data-test-subj="inspect-icon-button"]').first().simulate('click'); - wrapper.update(); - expect(wrapper.find('button[data-test-subj="modal-inspect-close"]').first().exists()).toBe( - true - ); - }); - - test('Close Inspect Modal', () => { - const wrapper = mount(); - wrapper.find('button[data-test-subj="inspect-icon-button"]').first().simulate('click'); - - wrapper.update(); - wrapper.find('button[data-test-subj="modal-inspect-close"]').first().simulate('click'); - - wrapper.update(); - expect(wrapper.find('button[data-test-subj="modal-inspect-close"]').first().exists()).toBe( - false - ); - }); - - test('Do not Open Inspect Modal if it is loading', () => { - const wrapper = mount( - - ); - wrapper.find('button[data-test-subj="inspect-icon-button"]').first().simulate('click'); - - wrapper.update(); - - expect(wrapper.find('button[data-test-subj="modal-inspect-close"]').first().exists()).toBe( - false - ); - }); - }); -}); diff --git a/x-pack/plugins/timelines/public/components/inspect/index.tsx b/x-pack/plugins/timelines/public/components/inspect/index.tsx deleted file mode 100644 index 304dd8cdfcf8b..0000000000000 --- a/x-pack/plugins/timelines/public/components/inspect/index.tsx +++ /dev/null @@ -1,114 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { EuiButtonIcon } from '@elastic/eui'; -import { getOr } from 'lodash/fp'; -import React, { useCallback, useState } from 'react'; -import styled, { css } from 'styled-components'; - -import { ModalInspectQuery } from './modal'; -import * as i18n from './translations'; -import { InspectQuery } from '../../store/t_grid/inputs'; - -export const BUTTON_CLASS = 'inspectButtonComponent'; - -export const InspectButtonContainer = styled.div<{ show?: boolean }>` - width: 100%; - display: flex; - flex-grow: 1; - - > * { - max-width: 100%; - } - - .${BUTTON_CLASS} { - pointer-events: none; - opacity: 0; - transition: opacity ${(props) => getOr(250, 'theme.eui.euiAnimSpeedNormal', props)} ease; - } - - ${({ show }) => - show && - css` - &:hover .${BUTTON_CLASS} { - pointer-events: auto; - opacity: 1; - } - `} -`; - -InspectButtonContainer.displayName = 'InspectButtonContainer'; - -InspectButtonContainer.defaultProps = { - show: true, -}; - -interface OwnProps { - inspect: InspectQuery | null; - isDisabled?: boolean; - loading: boolean; - onCloseInspect?: () => void; - title: string | React.ReactElement | React.ReactNode; -} - -export type InspectButtonProps = OwnProps; - -const InspectButtonComponent: React.FC = ({ - inspect, - isDisabled, - loading, - onCloseInspect, - title = '', -}) => { - const [isInspected, setIsInspected] = useState(false); - const isShowingModal = !loading && isInspected; - const handleClick = useCallback(() => { - setIsInspected(true); - }, []); - - const handleCloseModal = useCallback(() => { - if (onCloseInspect != null) { - onCloseInspect(); - } - setIsInspected(false); - }, [onCloseInspect, setIsInspected]); - - let request: string | null = null; - if (inspect != null && inspect.dsl.length > 0) { - request = inspect.dsl[0]; - } - - let response: string | null = null; - if (inspect != null && inspect.response.length > 0) { - response = inspect.response[0]; - } - - return ( - <> - - - - ); -}; - -export const InspectButton = React.memo(InspectButtonComponent); diff --git a/x-pack/plugins/timelines/public/components/inspect/modal.test.tsx b/x-pack/plugins/timelines/public/components/inspect/modal.test.tsx deleted file mode 100644 index 1404dc583ef19..0000000000000 --- a/x-pack/plugins/timelines/public/components/inspect/modal.test.tsx +++ /dev/null @@ -1,282 +0,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 { mount } from 'enzyme'; -import React from 'react'; -import { ThemeProvider } from 'styled-components'; -import { getMockTheme } from '../../mock/kibana_react.mock'; - -import { ModalInspectQuery, formatIndexPatternRequested, NO_ALERT_INDEX } from './modal'; - -const mockTheme = getMockTheme({ - eui: { - euiBreakpoints: { - l: '1200px', - }, - }, -}); - -const request = - '{"index": ["auditbeat-*","filebeat-*","packetbeat-*","winlogbeat-*"],"allowNoIndices": true, "ignoreUnavailable": true, "body": { "aggregations": {"hosts": {"cardinality": {"field": "host.name" } }, "hosts_histogram": {"auto_date_histogram": {"field": "@timestamp","buckets": "6"},"aggs": { "count": {"cardinality": {"field": "host.name" }}}}}, "query": {"bool": {"filter": [{"range": { "@timestamp": {"gte": 1562290224506,"lte": 1562376624506 }}}]}}, "size": 0, "track_total_hits": false}}'; -const response = - '{"took": 880,"timed_out": false,"_shards": {"total": 26,"successful": 26,"skipped": 0,"failed": 0},"hits": {"max_score": null,"hits": []},"aggregations": {"hosts": {"value": 541},"hosts_histogram": {"buckets": [{"key_as_string": "2019 - 07 - 05T01: 00: 00.000Z", "key": 1562288400000, "doc_count": 1492321, "count": { "value": 105 }}, {"key_as_string": "2019 - 07 - 05T13: 00: 00.000Z", "key": 1562331600000, "doc_count": 2412761, "count": { "value": 453}},{"key_as_string": "2019 - 07 - 06T01: 00: 00.000Z", "key": 1562374800000, "doc_count": 111658, "count": { "value": 15}}],"interval": "12h"}},"status": 200}'; - -describe('Modal Inspect', () => { - const closeModal = jest.fn(); - - describe('rendering', () => { - test('when isShowing is positive and request and response are not null', () => { - const wrapper = mount( - - - - ); - expect(wrapper.find('[data-test-subj="modal-inspect-euiModal"]').first().exists()).toBe(true); - expect(wrapper.find('.euiModalHeader__title').first().text()).toBe('Inspect My title'); - }); - - test('when isShowing is negative and request and response are not null', () => { - const wrapper = mount( - - ); - expect(wrapper.find('[data-test-subj="modal-inspect-euiModal"]').first().exists()).toBe( - false - ); - }); - - test('when isShowing is positive and request is null and response is not null', () => { - const wrapper = mount( - - ); - expect(wrapper.find('[data-test-subj="modal-inspect-euiModal"]').first().exists()).toBe( - false - ); - }); - - test('when isShowing is positive and request is not null and response is null', () => { - const wrapper = mount( - - ); - expect(wrapper.find('[data-test-subj="modal-inspect-euiModal"]').first().exists()).toBe( - false - ); - }); - }); - - describe('functionality from tab statistics/request/response', () => { - test('Click on statistic Tab', () => { - const wrapper = mount( - - - - ); - - wrapper.find('button.euiTab').first().simulate('click'); - wrapper.update(); - - expect( - wrapper.find('.euiDescriptionList__title span[data-test-subj="index-pattern-title"]').text() - ).toContain('Index pattern '); - expect( - wrapper - .find('.euiDescriptionList__description span[data-test-subj="index-pattern-description"]') - .text() - ).toBe('auditbeat-*, filebeat-*, packetbeat-*, winlogbeat-*'); - expect( - wrapper.find('.euiDescriptionList__title span[data-test-subj="query-time-title"]').text() - ).toContain('Query time '); - expect( - wrapper - .find('.euiDescriptionList__description span[data-test-subj="query-time-description"]') - .text() - ).toBe('880ms'); - expect( - wrapper - .find('.euiDescriptionList__title span[data-test-subj="request-timestamp-title"]') - .text() - ).toContain('Request timestamp '); - }); - - test('Click on request Tab', () => { - const wrapper = mount( - - - - ); - - wrapper.find('button.euiTab').at(2).simulate('click'); - wrapper.update(); - - expect(JSON.parse(wrapper.find('EuiCodeBlock').first().text())).toEqual({ - took: 880, - timed_out: false, - _shards: { - total: 26, - successful: 26, - skipped: 0, - failed: 0, - }, - hits: { - max_score: null, - hits: [], - }, - aggregations: { - hosts: { - value: 541, - }, - hosts_histogram: { - buckets: [ - { - key_as_string: '2019 - 07 - 05T01: 00: 00.000Z', - key: 1562288400000, - doc_count: 1492321, - count: { - value: 105, - }, - }, - { - key_as_string: '2019 - 07 - 05T13: 00: 00.000Z', - key: 1562331600000, - doc_count: 2412761, - count: { - value: 453, - }, - }, - { - key_as_string: '2019 - 07 - 06T01: 00: 00.000Z', - key: 1562374800000, - doc_count: 111658, - count: { - value: 15, - }, - }, - ], - interval: '12h', - }, - }, - status: 200, - }); - }); - - test('Click on response Tab', () => { - const wrapper = mount( - - - - ); - - wrapper.find('button.euiTab').at(1).simulate('click'); - wrapper.update(); - - expect(JSON.parse(wrapper.find('EuiCodeBlock').first().text())).toEqual({ - aggregations: { - hosts: { cardinality: { field: 'host.name' } }, - hosts_histogram: { - aggs: { count: { cardinality: { field: 'host.name' } } }, - auto_date_histogram: { buckets: '6', field: '@timestamp' }, - }, - }, - query: { - bool: { - filter: [{ range: { '@timestamp': { gte: 1562290224506, lte: 1562376624506 } } }], - }, - }, - size: 0, - track_total_hits: false, - }); - }); - }); - - describe('events', () => { - test('Make sure that toggle function has been called when you click on the close button', () => { - const wrapper = mount( - - - - ); - - wrapper.find('button[data-test-subj="modal-inspect-close"]').simulate('click'); - wrapper.update(); - expect(closeModal).toHaveBeenCalled(); - }); - }); - - describe('formatIndexPatternRequested', () => { - test('Return specific messages to NO_ALERT_INDEX if we only have one index and we match the index name `NO_ALERT_INDEX`', () => { - const expected = formatIndexPatternRequested([NO_ALERT_INDEX]); - expect(expected).toEqual({'No alert index found'}); - }); - - test('Ignore NO_ALERT_INDEX if you have more than one indices', () => { - const expected = formatIndexPatternRequested([NO_ALERT_INDEX, 'indice-1']); - expect(expected).toEqual('indice-1'); - }); - - test('Happy path', () => { - const expected = formatIndexPatternRequested(['indice-1, indice-2']); - expect(expected).toEqual('indice-1, indice-2'); - }); - - test('Empty array with no indices', () => { - const expected = formatIndexPatternRequested([]); - expect(expected).toEqual('Sorry about that, something went wrong.'); - }); - - test('Undefined indices', () => { - const expected = formatIndexPatternRequested(undefined); - expect(expected).toEqual('Sorry about that, something went wrong.'); - }); - }); -}); diff --git a/x-pack/plugins/timelines/public/components/inspect/modal.tsx b/x-pack/plugins/timelines/public/components/inspect/modal.tsx deleted file mode 100644 index 54cfc9827bb5f..0000000000000 --- a/x-pack/plugins/timelines/public/components/inspect/modal.tsx +++ /dev/null @@ -1,253 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { - EuiButton, - EuiCodeBlock, - EuiDescriptionList, - EuiIconTip, - EuiModal, - EuiModalBody, - EuiModalHeader, - EuiModalHeaderTitle, - EuiModalFooter, - EuiSpacer, - EuiTabbedContent, -} from '@elastic/eui'; -import numeral from '@elastic/numeral'; -import React, { Fragment, ReactNode } from 'react'; -import styled from 'styled-components'; - -import * as i18n from './translations'; - -export const NO_ALERT_INDEX = 'no-alert-index-049FC71A-4C2C-446F-9901-37XMC5024C51'; - -const DescriptionListStyled = styled(EuiDescriptionList)` - @media only screen and (min-width: ${(props) => - props?.theme?.eui?.euiBreakpoints?.s ?? '600px'}) { - .euiDescriptionList__title { - width: 30% !important; - } - - .euiDescriptionList__description { - width: 70% !important; - } - } -`; - -DescriptionListStyled.displayName = 'DescriptionListStyled'; - -interface ModalInspectProps { - closeModal: () => void; - isShowing: boolean; - request: string | null; - response: string | null; - additionalRequests?: string[] | null; - additionalResponses?: string[] | null; - title: string | React.ReactElement | React.ReactNode; -} - -interface Request { - index: string[]; - allowNoIndices: boolean; - ignoreUnavailable: boolean; - body: Record; -} - -interface Response { - took: number; - timed_out: boolean; - _shards: Record; - hits: Record; - aggregations: Record; -} - -const MyEuiModal = styled(EuiModal)` - .euiModal__flex { - width: 60vw; - } - .euiCodeBlock { - height: auto !important; - max-width: 718px; - } -`; - -MyEuiModal.displayName = 'MyEuiModal'; -const parseInspectStrings = function (stringsArray: string[]): T[] { - try { - return stringsArray.map((objectStringify) => JSON.parse(objectStringify)); - } catch { - return []; - } -}; - -const manageStringify = (object: Record | Response): string => { - try { - return JSON.stringify(object, null, 2); - } catch { - return i18n.SOMETHING_WENT_WRONG; - } -}; - -export const formatIndexPatternRequested = (indices: string[] = []) => { - if (indices.length === 1 && indices[0] === NO_ALERT_INDEX) { - return {i18n.NO_ALERT_INDEX_FOUND}; - } - return indices.length > 0 - ? indices.filter((i) => i !== NO_ALERT_INDEX).join(', ') - : i18n.SOMETHING_WENT_WRONG; -}; - -export const ModalInspectQuery = ({ - closeModal, - isShowing = false, - request, - response, - additionalRequests, - additionalResponses, - title, -}: ModalInspectProps) => { - if (!isShowing || request == null || response == null) { - return null; - } - - const requests: string[] = [request, ...(additionalRequests != null ? additionalRequests : [])]; - const responses: string[] = [ - response, - ...(additionalResponses != null ? additionalResponses : []), - ]; - - const inspectRequests: Request[] = parseInspectStrings(requests); - const inspectResponses: Response[] = parseInspectStrings(responses); - - const statistics: Array<{ - title: NonNullable; - description: NonNullable; - }> = [ - { - title: ( - - {i18n.INDEX_PATTERN}{' '} - - - ), - description: ( - - {formatIndexPatternRequested(inspectRequests[0]?.index ?? [])} - - ), - }, - - { - title: ( - - {i18n.QUERY_TIME}{' '} - - - ), - description: ( - - {inspectResponses[0]?.took - ? `${numeral(inspectResponses[0].took).format('0,0')}ms` - : i18n.SOMETHING_WENT_WRONG} - - ), - }, - { - title: ( - - {i18n.REQUEST_TIMESTAMP}{' '} - - - ), - description: ( - {new Date().toISOString()} - ), - }, - ]; - - const tabs = [ - { - id: 'statistics', - name: 'Statistics', - content: ( - <> - - - - ), - }, - { - id: 'request', - name: 'Request', - content: - inspectRequests.length > 0 ? ( - inspectRequests.map((inspectRequest, index) => ( - - - - {manageStringify(inspectRequest.body)} - - - )) - ) : ( - {i18n.SOMETHING_WENT_WRONG} - ), - }, - { - id: 'response', - name: 'Response', - content: - inspectResponses.length > 0 ? ( - responses.map((responseText, index) => ( - - - - {responseText} - - - )) - ) : ( - {i18n.SOMETHING_WENT_WRONG} - ), - }, - ]; - - return ( - - - - {i18n.INSPECT} {title} - - - - - - - - - - {i18n.CLOSE} - - - - ); -}; diff --git a/x-pack/plugins/timelines/public/components/inspect/translations.ts b/x-pack/plugins/timelines/public/components/inspect/translations.ts deleted file mode 100644 index 286ec9d10c287..0000000000000 --- a/x-pack/plugins/timelines/public/components/inspect/translations.ts +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { i18n } from '@kbn/i18n'; - -export const INSPECT = i18n.translate('xpack.timelines.inspectDescription', { - defaultMessage: 'Inspect', -}); - -export const CLOSE = i18n.translate('xpack.timelines.inspect.modal.closeTitle', { - defaultMessage: 'Close', -}); - -export const SOMETHING_WENT_WRONG = i18n.translate( - 'xpack.timelines.inspect.modal.somethingWentWrongDescription', - { - defaultMessage: 'Sorry about that, something went wrong.', - } -); -export const INDEX_PATTERN = i18n.translate('xpack.timelines.inspect.modal.indexPatternLabel', { - defaultMessage: 'Index pattern', -}); - -export const INDEX_PATTERN_DESC = i18n.translate( - 'xpack.timelines.inspect.modal.indexPatternDescription', - { - defaultMessage: - 'The index pattern that connected to the Elasticsearch indices. These indices can be configured in Kibana > Advanced Settings.', - } -); - -export const QUERY_TIME = i18n.translate('xpack.timelines.inspect.modal.queryTimeLabel', { - defaultMessage: 'Query time', -}); - -export const QUERY_TIME_DESC = i18n.translate( - 'xpack.timelines.inspect.modal.queryTimeDescription', - { - defaultMessage: - 'The time it took to process the query. Does not include the time to send the request or parse it in the browser.', - } -); - -export const REQUEST_TIMESTAMP = i18n.translate('xpack.timelines.inspect.modal.reqTimestampLabel', { - defaultMessage: 'Request timestamp', -}); - -export const REQUEST_TIMESTAMP_DESC = i18n.translate( - 'xpack.timelines.inspect.modal.reqTimestampDescription', - { - defaultMessage: 'Time when the start of the request has been logged', - } -); - -export const NO_ALERT_INDEX_FOUND = i18n.translate( - 'xpack.timelines.inspect.modal.noAlertIndexFound', - { - defaultMessage: 'No alert index found', - } -); diff --git a/x-pack/plugins/timelines/public/components/last_updated/translations.ts b/x-pack/plugins/timelines/public/components/last_updated/translations.ts index 975c6972e90cd..93faad5ceb445 100644 --- a/x-pack/plugins/timelines/public/components/last_updated/translations.ts +++ b/x-pack/plugins/timelines/public/components/last_updated/translations.ts @@ -7,10 +7,10 @@ import { i18n } from '@kbn/i18n'; -export const UPDATING = i18n.translate('xpack.timelines.lastUpdated.updating', { +export const UPDATING = i18n.translate('xpack.timelines.updating', { defaultMessage: 'Updating...', }); -export const UPDATED = i18n.translate('xpack.timelines.lastUpdated.updated', { +export const UPDATED = i18n.translate('xpack.timelines.updated', { defaultMessage: 'Updated', }); diff --git a/x-pack/plugins/timelines/public/components/rule_name/index.tsx b/x-pack/plugins/timelines/public/components/rule_name/index.tsx deleted file mode 100644 index 2f51c5f78ac5a..0000000000000 --- a/x-pack/plugins/timelines/public/components/rule_name/index.tsx +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { EuiLink } from '@elastic/eui'; -import { isEmpty } from 'lodash'; -import React, { useCallback, useMemo } from 'react'; -import { CoreStart } from '@kbn/core/public'; -import { useKibana } from '@kbn/kibana-react-plugin/public'; - -interface RuleNameProps { - name: string; - id: string; - appId: string; -} - -const appendSearch = (search?: string) => - isEmpty(search) ? '' : `${search?.startsWith('?') ? search : `?${search}`}`; - -const RuleNameComponents = ({ name, id, appId }: RuleNameProps) => { - const { navigateToApp, getUrlForApp } = useKibana().services.application; - - const hrefRuleDetails = useMemo( - () => - getUrlForApp(appId, { - deepLinkId: 'rules', - path: `/id/${id}${appendSearch(window.location.search)}`, - }), - [getUrlForApp, id, appId] - ); - const goToRuleDetails = useCallback( - (ev) => { - ev.preventDefault(); - navigateToApp(appId, { - deepLinkId: 'rules', - path: `/id/${id}${appendSearch(window.location.search)}`, - }); - }, - [navigateToApp, id, appId] - ); - return ( - // eslint-disable-next-line @elastic/eui/href-or-on-click - - {name} - - ); -}; - -export const RuleName = React.memo(RuleNameComponents); diff --git a/x-pack/plugins/timelines/public/components/stateful_event_context.ts b/x-pack/plugins/timelines/public/components/stateful_event_context.ts deleted file mode 100644 index 830a28a3c34e2..0000000000000 --- a/x-pack/plugins/timelines/public/components/stateful_event_context.ts +++ /dev/null @@ -1,11 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { createContext } from 'react'; -import { StatefulEventContextType } from '../types'; - -export const StatefulEventContext = createContext(null); diff --git a/x-pack/plugins/timelines/public/components/t_grid/body/column_headers/default_headers.ts b/x-pack/plugins/timelines/public/components/t_grid/body/column_headers/default_headers.ts deleted file mode 100644 index 9312ac17bf691..0000000000000 --- a/x-pack/plugins/timelines/public/components/t_grid/body/column_headers/default_headers.ts +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type { ColumnHeaderOptions, ColumnHeaderType } from '../../../../../common/types/timeline'; -import { DEFAULT_COLUMN_MIN_WIDTH, DEFAULT_DATE_COLUMN_MIN_WIDTH } from '../constants'; - -export const defaultColumnHeaderType: ColumnHeaderType = 'not-filtered'; - -export const defaultHeaders: ColumnHeaderOptions[] = [ - { - columnHeaderType: defaultColumnHeaderType, - id: '@timestamp', - initialWidth: DEFAULT_DATE_COLUMN_MIN_WIDTH, - esTypes: ['date'], - type: 'date', - }, - { - columnHeaderType: defaultColumnHeaderType, - id: 'message', - initialWidth: DEFAULT_COLUMN_MIN_WIDTH, - }, - { - columnHeaderType: defaultColumnHeaderType, - id: 'event.category', - initialWidth: DEFAULT_COLUMN_MIN_WIDTH, - }, - { - columnHeaderType: defaultColumnHeaderType, - id: 'event.action', - initialWidth: DEFAULT_COLUMN_MIN_WIDTH, - }, - { - columnHeaderType: defaultColumnHeaderType, - id: 'host.name', - initialWidth: DEFAULT_COLUMN_MIN_WIDTH, - }, - { - columnHeaderType: defaultColumnHeaderType, - id: 'source.ip', - initialWidth: DEFAULT_COLUMN_MIN_WIDTH, - }, - { - columnHeaderType: defaultColumnHeaderType, - id: 'destination.ip', - initialWidth: DEFAULT_COLUMN_MIN_WIDTH, - }, - { - columnHeaderType: defaultColumnHeaderType, - id: 'user.name', - initialWidth: DEFAULT_COLUMN_MIN_WIDTH, - }, -]; diff --git a/x-pack/plugins/timelines/public/components/t_grid/body/column_headers/helpers.test.tsx b/x-pack/plugins/timelines/public/components/t_grid/body/column_headers/helpers.test.tsx deleted file mode 100644 index 66fd0e828aece..0000000000000 --- a/x-pack/plugins/timelines/public/components/t_grid/body/column_headers/helpers.test.tsx +++ /dev/null @@ -1,492 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ -import { euiThemeVars } from '@kbn/ui-theme'; -import { mount } from 'enzyme'; -import { omit, set } from 'lodash/fp'; -import React from 'react'; - -import { defaultHeaders } from './default_headers'; -import { - BUILT_IN_SCHEMA, - getActionsColumnWidth, - getColumnWidthFromType, - getColumnHeaders, - getSchema, - getColumnHeader, -} from './helpers'; -import { - DEFAULT_ACTION_BUTTON_WIDTH, - DEFAULT_COLUMN_MIN_WIDTH, - DEFAULT_DATE_COLUMN_MIN_WIDTH, -} from '../constants'; -import { mockBrowserFields } from '../../../../mock/browser_fields'; -import { ColumnHeaderOptions } from '../../../../../common/types'; - -window.matchMedia = jest.fn().mockImplementation((query) => { - return { - matches: false, - media: query, - onchange: null, - addListener: jest.fn(), - removeListener: jest.fn(), - }; -}); - -describe('helpers', () => { - describe('getColumnWidthFromType', () => { - test('it returns the expected width for a non-date column', () => { - expect(getColumnWidthFromType('keyword')).toEqual(DEFAULT_COLUMN_MIN_WIDTH); - }); - - test('it returns the expected width for a date column', () => { - expect(getColumnWidthFromType('date')).toEqual(DEFAULT_DATE_COLUMN_MIN_WIDTH); - }); - }); - - describe('getSchema', () => { - const expected: Record = { - date: 'datetime', - date_nanos: 'datetime', - double: 'numeric', - long: 'numeric', - number: 'numeric', - object: 'json', - boolean: 'boolean', - }; - - Object.keys(expected).forEach((type) => - test(`it returns the expected schema for type '${type}'`, () => { - expect(getSchema(type)).toEqual(expected[type]); - }) - ); - - test('it returns `undefined` when `type` does NOT match a built-in schema type', () => { - expect(getSchema('string')).toBeUndefined(); // 'keyword` doesn't have a schema - }); - - test('it returns `undefined` when `type` is undefined', () => { - expect(getSchema(undefined)).toBeUndefined(); - }); - }); - - describe('getColumnHeader', () => { - test('it should return column header non existing in defaultHeaders', () => { - const field = 'test_field_1'; - - expect(getColumnHeader(field, [])).toEqual({ - columnHeaderType: 'not-filtered', - id: field, - initialWidth: DEFAULT_COLUMN_MIN_WIDTH, - }); - }); - - test('it should return column header existing in defaultHeaders', () => { - const field = 'test_field_1'; - - expect( - getColumnHeader(field, [ - { - columnHeaderType: 'not-filtered', - id: field, - initialWidth: DEFAULT_DATE_COLUMN_MIN_WIDTH, - esTypes: ['date'], - type: 'date', - }, - ]) - ).toEqual({ - columnHeaderType: 'not-filtered', - id: field, - initialWidth: DEFAULT_DATE_COLUMN_MIN_WIDTH, - esTypes: ['date'], - type: 'date', - }); - }); - }); - - describe('getColumnHeaders', () => { - // additional properties used by `EuiDataGrid`: - const actions = { - showHide: false, - showSortAsc: true, - showSortDesc: true, - }; - const defaultSortDirection = 'desc'; - const isSortable = true; - - const mockHeader = defaultHeaders.filter((h) => - ['@timestamp', 'source.ip', 'destination.ip'].includes(h.id) - ); - - describe('display', () => { - const renderedByDisplay = 'I am rendered via a React component: header.display'; - const renderedByDisplayAsText = 'I am rendered by header.displayAsText'; - - test('it renders via `display` when the header has JUST a `display` property (`displayAsText` is undefined)', () => { - const headerWithJustDisplay = mockHeader.map((x) => - x.id === '@timestamp' - ? { - ...x, - display: {renderedByDisplay}, - } - : x - ); - - const wrapper = mount( - <>{getColumnHeaders(headerWithJustDisplay, mockBrowserFields)[0].display} - ); - - expect(wrapper.text()).toEqual(renderedByDisplay); - }); - - test('it (also) renders via `display` when the header has BOTH a `display` property AND a `displayAsText`', () => { - const headerWithBoth = mockHeader.map((x) => - x.id === '@timestamp' - ? { - ...x, - display: {renderedByDisplay}, // this has a higher priority... - displayAsText: renderedByDisplayAsText, // ...so this text won't be rendered - } - : x - ); - - const wrapper = mount( - <>{getColumnHeaders(headerWithBoth, mockBrowserFields)[0].display} - ); - - expect(wrapper.text()).toEqual(renderedByDisplay); - }); - - test('it renders via `displayAsText` when the header does NOT have a `display`, BUT it has `displayAsText`', () => { - const headerWithJustDisplayAsText = mockHeader.map((x) => - x.id === '@timestamp' - ? { - ...x, - displayAsText: renderedByDisplayAsText, // fallback to rendering via displayAsText - } - : x - ); - - const wrapper = mount( - <>{getColumnHeaders(headerWithJustDisplayAsText, mockBrowserFields)[0].display} - ); - - expect(wrapper.text()).toEqual(renderedByDisplayAsText); - }); - - test('it renders `header.id` when the header does NOT have a `display`, AND it does NOT have a `displayAsText`', () => { - const wrapper = mount(<>{getColumnHeaders(mockHeader, mockBrowserFields)[0].display}); - - expect(wrapper.text()).toEqual('@timestamp'); // fallback to rendering by header.id - }); - }); - - test('it renders the default actions when the header does NOT have custom actions', () => { - expect(getColumnHeaders(mockHeader, mockBrowserFields)[0].actions).toEqual(actions); - }); - - test('it renders custom actions when `actions` is defined in the header', () => { - const customActions = { - showSortAsc: { - label: 'A custom sort ascending', - }, - showSortDesc: { - label: 'A custom sort descending', - }, - }; - - const headerWithCustomActions = mockHeader.map((x) => - x.id === '@timestamp' - ? { - ...x, - actions: customActions, - } - : x - ); - - expect(getColumnHeaders(headerWithCustomActions, mockBrowserFields)[0].actions).toEqual( - customActions - ); - }); - - describe('isSortable', () => { - test("it is sortable, because `@timestamp`'s `aggregatable` BrowserFields property is `true`", () => { - expect(getColumnHeaders(mockHeader, mockBrowserFields)[0].isSortable).toEqual(true); - }); - - test("it is NOT sortable, when `@timestamp`'s `aggregatable` BrowserFields property is `false`", () => { - const withAggregatableOverride = set( - 'base.fields.@timestamp.aggregatable', - false, // override `aggregatable` for `@timestamp`, a date field that is normally aggregatable - mockBrowserFields - ); - - expect(getColumnHeaders(mockHeader, withAggregatableOverride)[0].isSortable).toEqual(false); - }); - - test('it is NOT sortable when BrowserFields does not have metadata for the field', () => { - const noBrowserFieldEntry = omit('base', mockBrowserFields); // omit the 'base` category, which contains `@timestamp` - - expect(getColumnHeaders(mockHeader, noBrowserFieldEntry)[0].isSortable).toEqual(false); - }); - }); - - test('should return a full object of ColumnHeader from the default header', () => { - const expectedData = [ - { - actions, - aggregatable: true, - category: 'base', - columnHeaderType: 'not-filtered', - defaultSortDirection, - description: - 'Date/time when the event originated. For log events this is the date/time when the event was generated, and not when it was read. Required field for all events.', - esTypes: ['date'], - example: '2016-05-23T08:05:34.853Z', - format: '', - id: '@timestamp', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - isSortable, - name: '@timestamp', - schema: 'datetime', - searchable: true, - type: 'date', - initialWidth: 190, - }, - { - actions, - aggregatable: true, - category: 'source', - columnHeaderType: 'not-filtered', - defaultSortDirection, - description: 'IP address of the source. Can be one or multiple IPv4 or IPv6 addresses.', - example: '', - format: '', - id: 'source.ip', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - isSortable, - name: 'source.ip', - searchable: true, - type: 'ip', - initialWidth: 180, - }, - { - actions, - aggregatable: true, - category: 'destination', - columnHeaderType: 'not-filtered', - defaultSortDirection, - description: - 'IP address of the destination. Can be one or multiple IPv4 or IPv6 addresses.', - example: '', - format: '', - id: 'destination.ip', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - isSortable, - name: 'destination.ip', - searchable: true, - type: 'ip', - initialWidth: 180, - }, - ]; - - // NOTE: the omitted `display` (`React.ReactNode`) property is tested separately above - expect(getColumnHeaders(mockHeader, mockBrowserFields).map(omit('display'))).toEqual( - expectedData - ); - }); - - test('it should NOT override a custom `schema` when the `header` provides it', () => { - const expected = [ - { - actions, - aggregatable: true, - category: 'base', - columnHeaderType: 'not-filtered', - defaultSortDirection, - description: - 'Date/time when the event originated. For log events this is the date/time when the event was generated, and not when it was read. Required field for all events.', - example: '2016-05-23T08:05:34.853Z', - format: '', - id: '@timestamp', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - isSortable, - name: '@timestamp', - schema: 'custom', // <-- we expect our custom schema will NOT be overridden by a built-in schema - searchable: true, - type: 'date', // <-- the built-in schema for `type: 'date'` is 'datetime', but the custom schema overrides it - initialWidth: 190, - }, - ]; - - const headerWithCustomSchema: ColumnHeaderOptions = { - columnHeaderType: 'not-filtered', - id: '@timestamp', - initialWidth: 190, - schema: 'custom', // <-- overrides the default of 'datetime' - }; - - expect( - getColumnHeaders([headerWithCustomSchema], mockBrowserFields).map(omit('display')) - ).toEqual(expected); - }); - - test('it should return an `undefined` `schema` when a `header` does NOT have an entry in `BrowserFields`', () => { - const expected = [ - { - actions, - columnHeaderType: 'not-filtered', - defaultSortDirection, - id: 'no_matching_browser_field', - isSortable: false, - schema: undefined, // <-- no `BrowserFields` entry for this field - }, - ]; - - const headerDoesNotMatchBrowserField: ColumnHeaderOptions = { - columnHeaderType: 'not-filtered', - id: 'no_matching_browser_field', - }; - - expect( - getColumnHeaders([headerDoesNotMatchBrowserField], mockBrowserFields).map(omit('display')) - ).toEqual(expected); - }); - - describe('augment the `header` with metadata from `browserFields`', () => { - test('it should augment the `header` when field category is base', () => { - const fieldName = 'test_field'; - const testField = { - aggregatable: true, - category: 'base', - description: - 'Date/time when the event originated. For log events this is the date/time when the event was generated, and not when it was read. Required field for all events.', - example: '2016-05-23T08:05:34.853Z', - format: 'date', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: fieldName, - searchable: true, - type: 'date', - }; - - const browserField = { base: { fields: { [fieldName]: testField } } }; - - const header: ColumnHeaderOptions = { - columnHeaderType: 'not-filtered', - id: fieldName, - }; - - expect( - getColumnHeaders([header], browserField).map( - omit(['display', 'actions', 'isSortable', 'defaultSortDirection', 'schema']) - ) - ).toEqual([ - { - ...header, - ...browserField.base.fields[fieldName], - }, - ]); - }); - - test("it should augment the `header` when field is top level and name isn't splittable", () => { - const fieldName = 'testFieldName'; - const testField = { - aggregatable: true, - category: fieldName, - description: 'test field description', - example: '2016-05-23T08:05:34.853Z', - format: 'date', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: fieldName, - searchable: true, - type: 'date', - }; - - const browserField = { [fieldName]: { fields: { [fieldName]: testField } } }; - - const header: ColumnHeaderOptions = { - columnHeaderType: 'not-filtered', - id: fieldName, - }; - - expect( - getColumnHeaders([header], browserField).map( - omit(['display', 'actions', 'isSortable', 'defaultSortDirection', 'schema']) - ) - ).toEqual([ - { - ...header, - ...browserField[fieldName].fields[fieldName], - }, - ]); - }); - - test('it should augment the `header` when field is splittable', () => { - const fieldName = 'test.field.splittable'; - const testField = { - aggregatable: true, - category: 'test', - description: 'test field description', - example: '2016-05-23T08:05:34.853Z', - format: 'date', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: fieldName, - searchable: true, - type: 'date', - }; - - const browserField = { test: { fields: { [fieldName]: testField } } }; - - const header: ColumnHeaderOptions = { - columnHeaderType: 'not-filtered', - id: fieldName, - }; - - expect( - getColumnHeaders([header], browserField).map( - omit(['display', 'actions', 'isSortable', 'defaultSortDirection', 'schema']) - ) - ).toEqual([ - { - ...header, - ...browserField.test.fields[fieldName], - }, - ]); - }); - }); - }); - - describe('getActionsColumnWidth', () => { - // ideally the following implementation detail wouldn't be part of these tests, - // but without it, the test would be brittle when `euiDataGridCellPaddingM` changes: - const expectedPadding = parseInt(euiThemeVars.euiDataGridCellPaddingM, 10) * 2; - - test('it returns the expected width', () => { - const ACTION_BUTTON_COUNT = 5; - const expectedContentWidth = ACTION_BUTTON_COUNT * DEFAULT_ACTION_BUTTON_WIDTH; - - expect(getActionsColumnWidth(ACTION_BUTTON_COUNT)).toEqual( - expectedContentWidth + expectedPadding - ); - }); - - test('it returns the minimum width when the button count is zero', () => { - const ACTION_BUTTON_COUNT = 0; - - expect(getActionsColumnWidth(ACTION_BUTTON_COUNT)).toEqual( - DEFAULT_ACTION_BUTTON_WIDTH + expectedPadding - ); - }); - - test('it returns the minimum width when the button count is negative', () => { - const ACTION_BUTTON_COUNT = -1; - - expect(getActionsColumnWidth(ACTION_BUTTON_COUNT)).toEqual( - DEFAULT_ACTION_BUTTON_WIDTH + expectedPadding - ); - }); - }); -}); diff --git a/x-pack/plugins/timelines/public/components/t_grid/body/column_headers/helpers.tsx b/x-pack/plugins/timelines/public/components/t_grid/body/column_headers/helpers.tsx deleted file mode 100644 index ccd3bd3c0fa71..0000000000000 --- a/x-pack/plugins/timelines/public/components/t_grid/body/column_headers/helpers.tsx +++ /dev/null @@ -1,158 +0,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 { euiThemeVars } from '@kbn/ui-theme'; -import { EuiDataGridColumnActions } from '@elastic/eui'; -import { keyBy } from 'lodash/fp'; -import React from 'react'; - -import type { - BrowserField, - BrowserFields, -} from '../../../../../common/search_strategy/index_fields'; -import type { ColumnHeaderOptions } from '../../../../../common/types/timeline'; -import { - DEFAULT_ACTION_BUTTON_WIDTH, - DEFAULT_COLUMN_MIN_WIDTH, - DEFAULT_DATE_COLUMN_MIN_WIDTH, -} from '../constants'; -import { allowSorting } from '../helpers'; -import { defaultColumnHeaderType } from './default_headers'; - -const defaultActions: EuiDataGridColumnActions = { - showSortAsc: true, - showSortDesc: true, - showHide: false, -}; - -const getAllBrowserFields = (browserFields: BrowserFields): Array> => - Object.values(browserFields).reduce>>( - (acc, namespace) => [ - ...acc, - ...Object.values(namespace.fields != null ? namespace.fields : {}), - ], - [] - ); - -const getAllFieldsByName = ( - browserFields: BrowserFields -): { [fieldName: string]: Partial } => - keyBy('name', getAllBrowserFields(browserFields)); - -/** - * Valid built-in schema types for the `schema` property of `EuiDataGridColumn` - * are enumerated in the following comment in the EUI repository (permalink): - * https://github.com/elastic/eui/blob/edc71160223c8d74e1293501f7199fba8fa57c6c/src/components/datagrid/data_grid_types.ts#L417 - */ -export type BUILT_IN_SCHEMA = 'boolean' | 'currency' | 'datetime' | 'numeric' | 'json'; - -/** - * Returns a valid value for the `EuiDataGridColumn` `schema` property, or - * `undefined` when the specified `BrowserFields` `type` doesn't match a - * built-in schema type - * - * Notes: - * - * - At the time of this writing, the type definition of the - * `EuiDataGridColumn` `schema` property is: - * - * ```ts - * schema?: string; - * ``` - * - At the time of this writing, Elasticsearch Field data types are documented here: - * https://www.elastic.co/guide/en/elasticsearch/reference/7.14/mapping-types.html - */ -export const getSchema = (type: string | undefined): BUILT_IN_SCHEMA | undefined => { - switch (type) { - case 'date': // fall through - case 'date_nanos': - return 'datetime'; - case 'double': // fall through - case 'long': // fall through - case 'number': - return 'numeric'; - case 'object': - return 'json'; - case 'boolean': - return 'boolean'; - default: - return undefined; - } -}; - -/** Enriches the column headers with field details from the specified browserFields */ -export const getColumnHeaders = ( - headers: ColumnHeaderOptions[], - browserFields: BrowserFields -): ColumnHeaderOptions[] => { - const browserFieldByName = getAllFieldsByName(browserFields); - return headers - ? headers.map((header) => { - const browserField: Partial | undefined = browserFieldByName[header.id]; - - // augment the header with metadata from browserFields: - const augmentedHeader = { - ...header, - ...browserField, - schema: header.schema ?? getSchema(browserField?.type), - }; - - const content = <>{header.display ?? header.displayAsText ?? header.id}; - - // return the augmentedHeader with additional properties used by `EuiDataGrid` - return { - ...augmentedHeader, - actions: header.actions ?? defaultActions, - defaultSortDirection: 'desc', // the default action when a user selects a field via `EuiDataGrid`'s `Pick fields to sort by` UI - display: <>{content}, - isSortable: allowSorting({ - browserField, - fieldName: header.id, - }), - }; - }) - : []; -}; - -/** - * Returns the column header with field details from the defaultHeaders - */ -export const getColumnHeader = ( - fieldName: string, - defaultHeaders: ColumnHeaderOptions[] -): ColumnHeaderOptions => ({ - columnHeaderType: defaultColumnHeaderType, - id: fieldName, - initialWidth: DEFAULT_COLUMN_MIN_WIDTH, - ...(defaultHeaders.find((c) => c.id === fieldName) ?? {}), -}); - -export const getColumnWidthFromType = (type: string): number => - type !== 'date' ? DEFAULT_COLUMN_MIN_WIDTH : DEFAULT_DATE_COLUMN_MIN_WIDTH; - -/** - * Returns the width of the Actions column based on the number of buttons being - * displayed - * - * NOTE: This function is necessary because `width` is a required property of - * the `EuiDataGridControlColumn` interface, so it must be calculated before - * content is rendered. (The width of a `EuiDataGridControlColumn` does not - * automatically size itself to fit all the content.) - */ -export const getActionsColumnWidth = (actionButtonCount: number): number => { - const contentWidth = - actionButtonCount > 0 - ? actionButtonCount * DEFAULT_ACTION_BUTTON_WIDTH - : DEFAULT_ACTION_BUTTON_WIDTH; - - // `EuiDataGridRowCell` applies additional `padding-left` and - // `padding-right`, which must be added to the content width to prevent the - // content from being partially hidden due to the space occupied by padding: - const leftRightCellPadding = parseInt(euiThemeVars.euiDataGridCellPaddingM, 10) * 2; // parseInt ignores the trailing `px`, e.g. `6px` - - return contentWidth + leftRightCellPadding; -}; diff --git a/x-pack/plugins/timelines/public/components/t_grid/body/column_headers/translations.ts b/x-pack/plugins/timelines/public/components/t_grid/body/column_headers/translations.ts deleted file mode 100644 index 202eef8d675b8..0000000000000 --- a/x-pack/plugins/timelines/public/components/t_grid/body/column_headers/translations.ts +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { i18n } from '@kbn/i18n'; - -export const CATEGORY = i18n.translate('xpack.timelines.timeline.categoryTooltip', { - defaultMessage: 'Category', -}); - -export const DESCRIPTION = i18n.translate('xpack.timelines.timeline.descriptionTooltip', { - defaultMessage: 'Description', -}); - -export const FIELD = i18n.translate('xpack.timelines.timeline.fieldTooltip', { - defaultMessage: 'Field', -}); - -export const FULL_SCREEN = i18n.translate('xpack.timelines.timeline.fullScreenButton', { - defaultMessage: 'Full screen', -}); - -export const SORT_AZ = i18n.translate('xpack.timelines.timeline.sortAZLabel', { - defaultMessage: 'Sort A-Z', -}); - -export const SORT_FIELDS = i18n.translate('xpack.timelines.timeline.sortFieldsButton', { - defaultMessage: 'Sort fields', -}); - -export const SORT_ZA = i18n.translate('xpack.timelines.timeline.sortZALabel', { - defaultMessage: 'Sort Z-A', -}); - -export const TYPE = i18n.translate('xpack.timelines.timeline.typeTooltip', { - defaultMessage: 'Type', -}); - -export const REMOVE_COLUMN = i18n.translate( - 'xpack.timelines.timeline.flyout.pane.removeColumnButtonLabel', - { - defaultMessage: 'Remove column', - } -); diff --git a/x-pack/plugins/timelines/public/components/t_grid/body/constants.ts b/x-pack/plugins/timelines/public/components/t_grid/body/constants.ts deleted file mode 100644 index 0af2efb593f55..0000000000000 --- a/x-pack/plugins/timelines/public/components/t_grid/body/constants.ts +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { euiThemeVars } from '@kbn/ui-theme'; - -/** - * This is the effective width in pixels of an action button used with - * `EuiDataGrid` `leadingControlColumns`. (See Notes below for details) - * - * Notes: - * 1) This constant is necessary because `width` is a required property of - * the `EuiDataGridControlColumn` interface, so it must be calculated before - * content is rendered. (The width of a `EuiDataGridControlColumn` does not - * automatically size itself to fit all the content.) - * - * 2) This is the *effective* width, because at the time of this writing, - * `EuiButtonIcon` has a `margin-left: -4px`, which is subtracted from the - * `width` - */ -export const DEFAULT_ACTION_BUTTON_WIDTH = - parseInt(euiThemeVars.euiSizeXL, 10) - parseInt(euiThemeVars.euiSizeXS, 10); // px - -/** The default minimum width of a column (when a width for the column type is not specified) */ -export const DEFAULT_COLUMN_MIN_WIDTH = 180; // px - -/** The default minimum width of a column of type `date` */ -export const DEFAULT_DATE_COLUMN_MIN_WIDTH = 190; // px diff --git a/x-pack/plugins/timelines/public/components/t_grid/body/control_columns/index.tsx b/x-pack/plugins/timelines/public/components/t_grid/body/control_columns/index.tsx deleted file mode 100644 index b11bdd2b54c22..0000000000000 --- a/x-pack/plugins/timelines/public/components/t_grid/body/control_columns/index.tsx +++ /dev/null @@ -1,16 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type { ControlColumnProps } from '../../../../../common/types'; -import { HeaderCheckBox, RowCheckBox } from './checkbox'; - -export const checkBoxControlColumn: ControlColumnProps = { - id: 'checkbox-control-column', - width: 32, - headerCellRender: HeaderCheckBox, - rowCellRender: RowCheckBox, -}; diff --git a/x-pack/plugins/timelines/public/components/t_grid/body/control_columns/translations.ts b/x-pack/plugins/timelines/public/components/t_grid/body/control_columns/translations.ts deleted file mode 100644 index 9cc4bfd58357c..0000000000000 --- a/x-pack/plugins/timelines/public/components/t_grid/body/control_columns/translations.ts +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ -import { i18n } from '@kbn/i18n'; - -export const CHECKBOX_FOR_ROW = ({ - ariaRowindex, - columnValues, - checked, -}: { - ariaRowindex: number; - columnValues: string; - checked: boolean; -}) => - i18n.translate('xpack.timelines.timeline.body.actions.checkboxForRowAriaLabel', { - values: { ariaRowindex, checked, columnValues }, - defaultMessage: - '{checked, select, false {unchecked} true {checked}} checkbox for the alert or event in row {ariaRowindex}, with columns {columnValues}', - }); diff --git a/x-pack/plugins/timelines/public/components/t_grid/body/data_driven_columns/index.tsx b/x-pack/plugins/timelines/public/components/t_grid/body/data_driven_columns/index.tsx deleted file mode 100644 index 570581bcb640e..0000000000000 --- a/x-pack/plugins/timelines/public/components/t_grid/body/data_driven_columns/index.tsx +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type { TimelineNonEcsData } from '../../../../../common/search_strategy'; - -export const getMappedNonEcsValue = ({ - data, - fieldName, -}: { - data: TimelineNonEcsData[]; - fieldName: string; -}): string[] | undefined => { - const item = data.find((d) => d.field === fieldName); - if (item != null && item.value != null) { - return item.value; - } - return undefined; -}; diff --git a/x-pack/plugins/timelines/public/components/t_grid/body/events/stateful_event_context.tsx b/x-pack/plugins/timelines/public/components/t_grid/body/events/stateful_event_context.tsx deleted file mode 100644 index 695e61725e4b4..0000000000000 --- a/x-pack/plugins/timelines/public/components/t_grid/body/events/stateful_event_context.tsx +++ /dev/null @@ -1,15 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -interface StatefulEventContext { - tabType: string | undefined; - timelineID: string; -} - -// This context is available to all children of the stateful_event component where the provider is currently set -export const StatefulEventContext = React.createContext(null); diff --git a/x-pack/plugins/timelines/public/components/t_grid/body/events/stateful_row_renderer/index.tsx b/x-pack/plugins/timelines/public/components/t_grid/body/events/stateful_row_renderer/index.tsx deleted file mode 100644 index e5cda29116e38..0000000000000 --- a/x-pack/plugins/timelines/public/components/t_grid/body/events/stateful_row_renderer/index.tsx +++ /dev/null @@ -1,100 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { noop } from 'lodash/fp'; -import { EuiFocusTrap, EuiOutsideClickDetector, EuiScreenReaderOnly } from '@elastic/eui'; -import React, { useMemo } from 'react'; - -import { - ARIA_COLINDEX_ATTRIBUTE, - ARIA_ROWINDEX_ATTRIBUTE, - getRowRendererClassName, -} from '../../../../../../common/utils/accessibility'; -import { useStatefulEventFocus } from '../use_stateful_event_focus'; - -import * as i18n from '../translations'; -import type { TimelineItem } from '../../../../../../common/search_strategy'; -import type { RowRenderer } from '../../../../../../common/types/timeline'; -import { getRowRenderer } from '../../renderers/get_row_renderer'; - -/** - * This component addresses the accessibility of row renderers. - * - * accessibility details: - * - This component has a 'dialog' `role` because it's rendered as a dialog - * "outside" the current row for screen readers, similar to a popover - * - It has tabIndex="0" to allow for keyboard focus - * - It traps keyboard focus when a user clicks inside a row renderer, to - * allow for tabbing through the contents of row renderers - * - The "dialog" can be dismissed via the up arrow key, down arrow key, - * which focuses the current or next row, respectively. - * - A screen-reader-only message provides additional context and instruction - */ -export const StatefulRowRenderer = ({ - ariaRowindex, - containerRef, - event, - lastFocusedAriaColindex, - rowRenderers, - timelineId, -}: { - ariaRowindex: number; - containerRef: React.MutableRefObject; - event: TimelineItem; - lastFocusedAriaColindex: number; - rowRenderers: RowRenderer[]; - timelineId: string; -}) => { - const { focusOwnership, onFocus, onKeyDown, onOutsideClick } = useStatefulEventFocus({ - ariaRowindex, - colindexAttribute: ARIA_COLINDEX_ATTRIBUTE, - containerRef, - lastFocusedAriaColindex, - onColumnFocused: noop, - rowindexAttribute: ARIA_ROWINDEX_ATTRIBUTE, - }); - - const rowRenderer = useMemo( - () => getRowRenderer(event.ecs, rowRenderers), - [event.ecs, rowRenderers] - ); - - const content = useMemo( - () => - rowRenderer && ( - // eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions -
    - - - -

    {i18n.YOU_ARE_IN_AN_EVENT_RENDERER(ariaRowindex)}

    -
    -
    - {rowRenderer.renderRow({ - data: event.ecs, - isDraggable: false, - scopeId: timelineId, - })} -
    -
    -
    -
    - ), - [ - ariaRowindex, - event.ecs, - focusOwnership, - onFocus, - onKeyDown, - onOutsideClick, - rowRenderer, - timelineId, - ] - ); - - return content; -}; diff --git a/x-pack/plugins/timelines/public/components/t_grid/body/events/translations.ts b/x-pack/plugins/timelines/public/components/t_grid/body/events/translations.ts deleted file mode 100644 index 9d1071a80071b..0000000000000 --- a/x-pack/plugins/timelines/public/components/t_grid/body/events/translations.ts +++ /dev/null @@ -1,15 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { i18n } from '@kbn/i18n'; - -export const YOU_ARE_IN_AN_EVENT_RENDERER = (row: number) => - i18n.translate('xpack.timelines.timeline.youAreInAnEventRendererScreenReaderOnly', { - values: { row }, - defaultMessage: - 'You are in an event renderer for row: {row}. Press the up arrow key to exit and return to the current row, or the down arrow key to exit and advance to the next row.', - }); diff --git a/x-pack/plugins/timelines/public/components/t_grid/body/events/use_stateful_event_focus/index.tsx b/x-pack/plugins/timelines/public/components/t_grid/body/events/use_stateful_event_focus/index.tsx deleted file mode 100644 index 2710b8c463623..0000000000000 --- a/x-pack/plugins/timelines/public/components/t_grid/body/events/use_stateful_event_focus/index.tsx +++ /dev/null @@ -1,98 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React, { useCallback, useState, useMemo } from 'react'; -import { - focusColumn, - isArrowDownOrArrowUp, - isArrowUp, - isEscape, -} from '../../../../../../common/utils/accessibility'; -import type { OnColumnFocused } from '../../../../../../common/utils/accessibility'; - -type FocusOwnership = 'not-owned' | 'owned'; - -export const getSameOrNextAriaRowindex = ({ - ariaRowindex, - event, -}: { - ariaRowindex: number; - event: React.KeyboardEvent; -}): number => (isArrowUp(event) ? ariaRowindex : ariaRowindex + 1); - -export const useStatefulEventFocus = ({ - ariaRowindex, - colindexAttribute, - containerRef, - lastFocusedAriaColindex, - onColumnFocused, - rowindexAttribute, -}: { - ariaRowindex: number; - colindexAttribute: string; - containerRef: React.MutableRefObject; - lastFocusedAriaColindex: number; - onColumnFocused: OnColumnFocused; - rowindexAttribute: string; -}) => { - const [focusOwnership, setFocusOwnership] = useState('not-owned'); - - const onFocus = useCallback(() => { - setFocusOwnership((prevFocusOwnership) => { - if (prevFocusOwnership !== 'owned') { - return 'owned'; - } - return prevFocusOwnership; - }); - }, []); - - const onOutsideClick = useCallback(() => { - setFocusOwnership('not-owned'); - }, []); - - const onKeyDown = useCallback( - (e: React.KeyboardEvent) => { - if (isArrowDownOrArrowUp(e) || isEscape(e)) { - e.preventDefault(); - e.stopPropagation(); - - setFocusOwnership('not-owned'); - - const newAriaRowindex = isEscape(e) - ? ariaRowindex // return focus to the same row - : getSameOrNextAriaRowindex({ ariaRowindex, event: e }); - - setTimeout(() => { - onColumnFocused( - focusColumn({ - ariaColindex: lastFocusedAriaColindex, - ariaRowindex: newAriaRowindex, - colindexAttribute, - containerElement: containerRef.current, - rowindexAttribute, - }) - ); - }, 0); - } - }, - [ - ariaRowindex, - colindexAttribute, - containerRef, - lastFocusedAriaColindex, - onColumnFocused, - rowindexAttribute, - ] - ); - - const memoizedReturn = useMemo( - () => ({ focusOwnership, onFocus, onOutsideClick, onKeyDown }), - [focusOwnership, onFocus, onKeyDown, onOutsideClick] - ); - - return memoizedReturn; -}; diff --git a/x-pack/plugins/timelines/public/components/t_grid/body/helpers.test.tsx b/x-pack/plugins/timelines/public/components/t_grid/body/helpers.test.tsx deleted file mode 100644 index 253c3ca78b487..0000000000000 --- a/x-pack/plugins/timelines/public/components/t_grid/body/helpers.test.tsx +++ /dev/null @@ -1,293 +0,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 { omit } from 'lodash/fp'; - -import { ColumnHeaderOptions } from '../../../../common/types'; -import { - allowSorting, - hasCellActions, - mapSortDirectionToDirection, - mapSortingColumns, - addBuildingBlockStyle, -} from './helpers'; - -import { euiThemeVars } from '@kbn/ui-theme'; -import { mockDnsEvent } from '../../../mock'; - -describe('helpers', () => { - describe('mapSortDirectionToDirection', () => { - test('it returns the expected direction when sortDirection is `asc`', () => { - expect(mapSortDirectionToDirection('asc')).toBe('asc'); - }); - - test('it returns the expected direction when sortDirection is `desc`', () => { - expect(mapSortDirectionToDirection('desc')).toBe('desc'); - }); - - test('it returns the expected direction when sortDirection is `none`', () => { - expect(mapSortDirectionToDirection('none')).toBe('desc'); // defaults to a valid direction accepted by `EuiDataGrid` - }); - }); - - describe('mapSortingColumns', () => { - const columns: Array<{ - id: string; - direction: 'asc' | 'desc'; - }> = [ - { - id: 'kibana.rac.alert.status', - direction: 'asc', - }, - { - id: 'kibana.rac.alert.start', - direction: 'desc', - }, - ]; - - const columnHeaders: ColumnHeaderOptions[] = [ - { - columnHeaderType: 'not-filtered', - displayAsText: 'Status', - id: 'kibana.rac.alert.status', - initialWidth: 79, - category: 'kibana', - type: 'string', - aggregatable: true, - actions: { - showSortAsc: { - label: 'Sort A-Z', - }, - showSortDesc: { - label: 'Sort Z-A', - }, - }, - defaultSortDirection: 'desc', - display: { - key: null, - ref: null, - props: { - children: { - key: null, - ref: null, - props: { - children: 'Status', - }, - _owner: null, - }, - }, - _owner: null, - }, - isSortable: true, - }, - { - columnHeaderType: 'not-filtered', - displayAsText: 'Triggered', - id: 'kibana.rac.alert.start', - initialWidth: 176, - category: 'kibana', - type: 'date', - esTypes: ['date'], - aggregatable: true, - actions: { - showSortAsc: { - label: 'Sort A-Z', - }, - showSortDesc: { - label: 'Sort Z-A', - }, - }, - defaultSortDirection: 'desc', - display: { - key: null, - ref: null, - props: { - children: { - key: null, - ref: null, - props: { - children: 'Triggered', - }, - _owner: null, - }, - }, - _owner: null, - }, - isSortable: true, - }, - ]; - - test('it returns the expected results when each column has a corresponding entry in `columnHeaders`', () => { - expect(mapSortingColumns({ columns, columnHeaders })).toEqual([ - { - columnId: 'kibana.rac.alert.status', - columnType: 'string', - esTypes: [], - sortDirection: 'asc', - }, - { - columnId: 'kibana.rac.alert.start', - columnType: 'date', - esTypes: ['date'], - sortDirection: 'desc', - }, - ]); - }); - - test('it defaults to a `columnType` of empty string when a column does NOT have a corresponding entry in `columnHeaders`', () => { - const withUnknownColumn: Array<{ - id: string; - direction: 'asc' | 'desc'; - }> = [ - { - id: 'kibana.rac.alert.status', - direction: 'asc', - }, - { - id: 'kibana.rac.alert.start', - direction: 'desc', - }, - { - id: 'unknown', // <-- no entry for this in `columnHeaders` - direction: 'asc', - }, - ]; - - expect(mapSortingColumns({ columns: withUnknownColumn, columnHeaders })).toEqual([ - { - columnId: 'kibana.rac.alert.status', - columnType: 'string', - esTypes: [], - sortDirection: 'asc', - }, - { - columnId: 'kibana.rac.alert.start', - columnType: 'date', - esTypes: ['date'], - sortDirection: 'desc', - }, - { - columnId: 'unknown', - columnType: '', // <-- mapped to the default - esTypes: [], // <-- mapped to the default - sortDirection: 'asc', - }, - ]); - }); - }); - - describe('allowSorting', () => { - const aggregatableField = { - category: 'cloud', - description: - 'The cloud account or organization id used to identify different entities in a multi-tenant environment. Examples: AWS account id, Google Cloud ORG Id, or other unique identifier.', - example: '666777888999', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: 'cloud.account.id', - searchable: true, - type: 'string', - aggregatable: true, // <-- allow sorting when this is true - format: '', - }; - - test('it returns true for an aggregatable field', () => { - expect( - allowSorting({ - browserField: aggregatableField, - fieldName: aggregatableField.name, - }) - ).toBe(true); - }); - - test('it returns true for a allow-listed non-BrowserField', () => { - expect( - allowSorting({ - browserField: undefined, // no BrowserField metadata for this field - fieldName: 'kibana.alert.rule.name', // an allow-listed field name - }) - ).toBe(true); - }); - - test('it returns false for a NON-aggregatable field (aggregatable is false)', () => { - const nonaggregatableField = { - ...aggregatableField, - aggregatable: false, // <-- NON-aggregatable - }; - - expect( - allowSorting({ - browserField: nonaggregatableField, - fieldName: nonaggregatableField.name, - }) - ).toBe(false); - }); - - test('it returns false if the BrowserField is missing the aggregatable property', () => { - const missingAggregatable = omit('aggregatable', aggregatableField); - - expect( - allowSorting({ - browserField: missingAggregatable, - fieldName: missingAggregatable.name, - }) - ).toBe(false); - }); - - test("it returns false for a non-allowlisted field we don't have `BrowserField` metadata for it", () => { - expect( - allowSorting({ - browserField: undefined, // <-- no metadata for this field - fieldName: 'non-allowlisted', - }) - ).toBe(false); - }); - }); - - describe('addBuildingBlockStyle', () => { - const THEME = { eui: euiThemeVars, darkMode: false }; - - test('it calls `setCellProps` with background color when event is a building block', () => { - const mockedSetCellProps = jest.fn(); - const ecs = { - ...mockDnsEvent, - ...{ kibana: { alert: { building_block_type: ['default'] } } }, - }; - - addBuildingBlockStyle(ecs, THEME, mockedSetCellProps); - - expect(mockedSetCellProps).toBeCalledWith({ - style: { - backgroundColor: euiThemeVars.euiColorHighlight, - }, - }); - }); - - test('it call `setCellProps` reseting the background color when event is not a building block', () => { - const mockedSetCellProps = jest.fn(); - - addBuildingBlockStyle(mockDnsEvent, THEME, mockedSetCellProps); - - expect(mockedSetCellProps).toBeCalledWith({ style: { backgroundColor: 'inherit' } }); - }); - }); - - describe('hasCellActions', () => { - const columnId = '@timestamp'; - - test('it returns false when the columnId is included in `disabledCellActions` ', () => { - const disabledCellActions = ['foo', '@timestamp', 'bar', 'baz']; // includes @timestamp - - expect(hasCellActions({ columnId, disabledCellActions })).toBe(false); - }); - - test('it returns true when the columnId is NOT included in `disabledCellActions` ', () => { - const disabledCellActions = ['foo', 'bar', 'baz']; - - expect(hasCellActions({ columnId, disabledCellActions })).toBe(true); - }); - }); -}); diff --git a/x-pack/plugins/timelines/public/components/t_grid/body/helpers.tsx b/x-pack/plugins/timelines/public/components/t_grid/body/helpers.tsx deleted file mode 100644 index d81414cf677a2..0000000000000 --- a/x-pack/plugins/timelines/public/components/t_grid/body/helpers.tsx +++ /dev/null @@ -1,214 +0,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 { ALERT_RULE_CONSUMER, ALERT_RULE_PRODUCER } from '@kbn/rule-data-utils'; -import { isEmpty } from 'lodash/fp'; - -import { EuiDataGridCellValueElementProps } from '@elastic/eui'; -import type { EuiTheme } from '@kbn/kibana-react-plugin/common'; -import type { Ecs } from '../../../../common/ecs'; -import type { - BrowserField, - TimelineItem, - TimelineNonEcsData, -} from '../../../../common/search_strategy'; -import type { - ColumnHeaderOptions, - SortColumnTable, - SortDirection, -} from '../../../../common/types/timeline'; - -/** - * Creates mapping of eventID -> fieldData for given fieldsToKeep. Used to store additional field - * data necessary for custom timeline actions in conjunction with selection state - * @param timelineData - * @param eventIds - * @param fieldsToKeep - */ -export const getEventIdToDataMapping = ( - timelineData: TimelineItem[], - eventIds: string[], - fieldsToKeep: string[], - hasAlertsCrud: boolean, - hasAlertsCrudPermissionsByRule?: ({ - ruleConsumer, - ruleProducer, - }: { - ruleConsumer: string; - ruleProducer?: string; - }) => boolean -): Record => - timelineData.reduce((acc, v) => { - // FUTURE DEVELOPER - // We only have one featureId for security solution therefore we can just use hasAlertsCrud - // but for o11y we can multiple featureIds so we need to check every consumer - // of the alert to see if they have the permission to update the alert - const ruleConsumers = v.data.find((d) => d.field === ALERT_RULE_CONSUMER)?.value ?? []; - const ruleProducers = v.data.find((d) => d.field === ALERT_RULE_PRODUCER)?.value ?? []; - const hasPermissions = hasAlertsCrudPermissionsByRule - ? hasAlertsCrudPermissionsByRule({ - ruleConsumer: ruleConsumers.length > 0 ? ruleConsumers[0] : '', - ruleProducer: ruleProducers.length > 0 ? ruleProducers[0] : undefined, - }) - : hasAlertsCrud; - - const fvm = - hasPermissions && eventIds.includes(v._id) - ? { [v._id]: v.data.filter((ti) => fieldsToKeep.includes(ti.field)) } - : {}; - return { - ...acc, - ...fvm, - }; - }, {}); - -export const isEventBuildingBlockType = (event: Ecs): boolean => - !isEmpty(event.kibana?.alert?.building_block_type); - -/** Maps (Redux) `SortDirection` to the `direction` values used by `EuiDataGrid` */ -export const mapSortDirectionToDirection = (sortDirection: SortDirection): 'asc' | 'desc' => { - switch (sortDirection) { - case 'asc': // fall through - case 'desc': - return sortDirection; - default: - return 'desc'; - } -}; - -/** - * Maps `EuiDataGrid` columns to their Redux representation by combining the - * `columns` with metadata from `columnHeaders` - */ -export const mapSortingColumns = ({ - columns, - columnHeaders, -}: { - columnHeaders: ColumnHeaderOptions[]; - columns: Array<{ - id: string; - direction: 'asc' | 'desc'; - }>; -}): SortColumnTable[] => - columns.map(({ id, direction }) => { - const columnHeader = columnHeaders.find((ch) => ch.id === id); - const columnType = columnHeader?.type ?? ''; - const esTypes = columnHeader?.esTypes ?? []; - - return { - columnId: id, - columnType, - esTypes, - sortDirection: direction, - }; - }); - -export const allowSorting = ({ - browserField, - fieldName, -}: { - browserField: Partial | undefined; - fieldName: string; -}): boolean => { - const isAggregatable = browserField?.aggregatable ?? false; - - const isAllowlistedNonBrowserField = [ - 'kibana.alert.ancestors.depth', - 'kibana.alert.ancestors.id', - 'kibana.alert.ancestors.rule', - 'kibana.alert.ancestors.type', - 'kibana.alert.original_event.action', - 'kibana.alert.original_event.category', - 'kibana.alert.original_event.code', - 'kibana.alert.original_event.created', - 'kibana.alert.original_event.dataset', - 'kibana.alert.original_event.duration', - 'kibana.alert.original_event.end', - 'kibana.alert.original_event.hash', - 'kibana.alert.original_event.id', - 'kibana.alert.original_event.kind', - 'kibana.alert.original_event.module', - 'kibana.alert.original_event.original', - 'kibana.alert.original_event.outcome', - 'kibana.alert.original_event.provider', - 'kibana.alert.original_event.risk_score', - 'kibana.alert.original_event.risk_score_norm', - 'kibana.alert.original_event.sequence', - 'kibana.alert.original_event.severity', - 'kibana.alert.original_event.start', - 'kibana.alert.original_event.timezone', - 'kibana.alert.original_event.type', - 'kibana.alert.original_time', - 'kibana.alert.reason', - 'kibana.alert.rule.created_by', - 'kibana.alert.rule.description', - 'kibana.alert.rule.enabled', - 'kibana.alert.rule.false_positives', - 'kibana.alert.rule.from', - 'kibana.alert.rule.uuid', - 'kibana.alert.rule.immutable', - 'kibana.alert.rule.interval', - 'kibana.alert.rule.max_signals', - 'kibana.alert.rule.name', - 'kibana.alert.rule.note', - 'kibana.alert.rule.references', - 'kibana.alert.risk_score', - 'kibana.alert.rule.rule_id', - 'kibana.alert.severity', - 'kibana.alert.rule.size', - 'kibana.alert.rule.tags', - 'kibana.alert.rule.threat', - 'kibana.alert.rule.threat.tactic.id', - 'kibana.alert.rule.threat.tactic.name', - 'kibana.alert.rule.threat.tactic.reference', - 'kibana.alert.rule.threat.technique.id', - 'kibana.alert.rule.threat.technique.name', - 'kibana.alert.rule.threat.technique.reference', - 'kibana.alert.rule.timeline_id', - 'kibana.alert.rule.timeline_title', - 'kibana.alert.rule.to', - 'kibana.alert.rule.type', - 'kibana.alert.rule.updated_by', - 'kibana.alert.rule.version', - 'kibana.alert.workflow_status', - ].includes(fieldName); - - return isAllowlistedNonBrowserField || isAggregatable; -}; -export const addBuildingBlockStyle = ( - ecs: Ecs, - theme: EuiTheme, - setCellProps: EuiDataGridCellValueElementProps['setCellProps'], - defaultStyles?: React.CSSProperties -) => { - const currentStyles = defaultStyles ?? {}; - if (isEventBuildingBlockType(ecs)) { - setCellProps({ - style: { - ...currentStyles, - backgroundColor: `${theme.eui.euiColorHighlight}`, - }, - }); - } else { - // reset cell style - setCellProps({ - style: { - ...currentStyles, - backgroundColor: 'inherit', - }, - }); - } -}; - -/** Returns true when the specified column has cell actions */ -export const hasCellActions = ({ - columnId, - disabledCellActions, -}: { - columnId: string; - disabledCellActions: string[]; -}) => !disabledCellActions.includes(columnId); diff --git a/x-pack/plugins/timelines/public/components/t_grid/body/index.test.tsx b/x-pack/plugins/timelines/public/components/t_grid/body/index.test.tsx deleted file mode 100644 index 637199aea107a..0000000000000 --- a/x-pack/plugins/timelines/public/components/t_grid/body/index.test.tsx +++ /dev/null @@ -1,350 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { fireEvent, render, screen } from '@testing-library/react'; - -import { BodyComponent, StatefulBodyProps } from '.'; -import { Sort } from './sort'; -import { REMOVE_COLUMN } from './column_headers/translations'; -import { Direction } from '../../../../common/search_strategy'; -import { useMountAppended } from '../../utils/use_mount_appended'; -import { defaultHeaders, mockBrowserFields, mockTimelineData, TestProviders } from '../../../mock'; -import { TestCellRenderer } from '../../../mock/cell_renderer'; -import { mockGlobalState } from '../../../mock/global_state'; -import { EuiDataGridColumn } from '@elastic/eui'; -import { defaultColumnHeaderType } from '../../../store/t_grid/defaults'; - -const mockSort: Sort[] = [ - { - columnId: '@timestamp', - columnType: 'date', - esTypes: ['date'], - sortDirection: Direction.desc, - }, -]; - -const mockDispatch = jest.fn(); -jest.mock('react-redux', () => { - const original = jest.requireActual('react-redux'); - - return { - ...original, - useDispatch: () => mockDispatch, - }; -}); - -jest.mock('@kbn/kibana-react-plugin/public', () => { - const originalModule = jest.requireActual('@kbn/kibana-react-plugin/public'); - return { - ...originalModule, - useKibana: () => ({ - services: { - triggersActionsUi: { - getFieldBrowser: jest.fn(), - }, - }, - }), - }; -}); - -jest.mock('../../../hooks/use_selector', () => ({ - useShallowEqualSelector: () => mockGlobalState.tableById['table-test'], - useDeepEqualSelector: () => mockGlobalState.tableById['table-test'], -})); - -jest.mock( - 'react-visibility-sensor', - () => - ({ children }: { children: (args: { isVisible: boolean }) => React.ReactNode }) => - children({ isVisible: true }) -); - -window.matchMedia = jest.fn().mockImplementation((query) => { - return { - matches: false, - media: query, - onchange: null, - addListener: jest.fn(), - removeListener: jest.fn(), - }; -}); - -describe('Body', () => { - const mount = useMountAppended(); - const props: StatefulBodyProps = { - activePage: 0, - browserFields: mockBrowserFields, - clearSelected: jest.fn() as unknown as StatefulBodyProps['clearSelected'], - columnHeaders: defaultHeaders, - data: mockTimelineData, - defaultCellActions: [], - disabledCellActions: ['signal.rule.risk_score', 'signal.reason'], - id: 'timeline-test', - isSelectAllChecked: false, - isLoading: false, - itemsPerPageOptions: [], - loadingEventIds: [], - loadPage: jest.fn(), - pageSize: 25, - renderCellValue: TestCellRenderer, - rowRenderers: [], - selectedEventIds: {}, - setSelected: jest.fn() as unknown as StatefulBodyProps['setSelected'], - sort: mockSort, - showCheckboxes: false, - tabType: 'query', - tableView: 'gridView', - totalItems: 1, - leadingControlColumns: [], - trailingControlColumns: [], - filterStatus: 'open', - filterQuery: '', - refetch: jest.fn(), - indexNames: [''], - }; - - beforeEach(() => { - mockDispatch.mockReset(); - }); - - describe('rendering', () => { - test('it renders the body data grid', () => { - const wrapper = mount( - - - - ); - expect(wrapper.find('[data-test-subj="body-data-grid"]').first().exists()).toEqual(true); - }); - - test('it renders the column headers', () => { - const wrapper = mount( - - - - ); - - expect(wrapper.find('[data-test-subj="dataGridHeader"]').first().exists()).toEqual(true); - }); - - test('it renders the scroll container', () => { - const wrapper = mount( - - - - ); - - expect(wrapper.find('div.euiDataGrid__virtualized').first().exists()).toEqual(true); - }); - - test('it renders events', () => { - const wrapper = mount( - - - - ); - - expect(wrapper.find('div.euiDataGridRowCell').first().exists()).toEqual(true); - }); - - test('it renders cell value', () => { - const headersJustTimestamp = defaultHeaders.filter((h) => h.id === '@timestamp'); - const testProps = { - ...props, - columnHeaders: headersJustTimestamp, - data: mockTimelineData.slice(0, 1), - }; - const wrapper = mount( - - - - ); - wrapper.update(); - expect( - wrapper - .find('[data-test-subj="dataGridRowCell"]') - .at(0) - .find('.euiDataGridRowCell__truncate') - .childAt(0) - .text() - ).toEqual(mockTimelineData[0].ecs.timestamp); - }); - - test('timestamp column renders cell actions', () => { - const headersJustTimestamp = defaultHeaders.filter((h) => h.id === '@timestamp'); - const testProps = { - ...props, - columnHeaders: headersJustTimestamp, - data: mockTimelineData.slice(0, 1), - }; - const wrapper = mount( - - - - ); - wrapper.update(); - - expect( - wrapper - .find('[data-test-subj="body-data-grid"]') - .first() - .prop('columns') - .find((c) => c.id === '@timestamp')?.cellActions - ).toBeDefined(); - }); - - test("signal.rule.risk_score column doesn't render cell actions", () => { - const columnHeaders = [ - { - category: 'signal', - columnHeaderType: defaultColumnHeaderType, - id: 'signal.rule.risk_score', - type: 'number', - aggregatable: true, - initialWidth: 105, - }, - ]; - const testProps = { - ...props, - columnHeaders, - data: mockTimelineData.slice(0, 1), - }; - const wrapper = mount( - - - - ); - wrapper.update(); - - expect( - wrapper - .find('[data-test-subj="body-data-grid"]') - .first() - .prop('columns') - .find((c) => c.id === 'signal.rule.risk_score')?.cellActions - ).toBeUndefined(); - }); - - test("signal.reason column doesn't render cell actions", () => { - const columnHeaders = [ - { - category: 'signal', - columnHeaderType: defaultColumnHeaderType, - id: 'signal.reason', - type: 'string', - aggregatable: true, - initialWidth: 450, - }, - ]; - const testProps = { - ...props, - columnHeaders, - data: mockTimelineData.slice(0, 1), - }; - const wrapper = mount( - - - - ); - wrapper.update(); - - expect( - wrapper - .find('[data-test-subj="body-data-grid"]') - .first() - .prop('columns') - .find((c) => c.id === 'signal.reason')?.cellActions - ).toBeUndefined(); - }); - }); - - test("signal.rule.risk_score column doesn't render cell actions", () => { - const columnHeaders = [ - { - category: 'signal', - columnHeaderType: defaultColumnHeaderType, - id: 'signal.rule.risk_score', - type: 'number', - aggregatable: true, - initialWidth: 105, - }, - ]; - const testProps = { - ...props, - columnHeaders, - data: mockTimelineData.slice(0, 1), - }; - const wrapper = mount( - - - - ); - wrapper.update(); - - expect( - wrapper - .find('[data-test-subj="body-data-grid"]') - .first() - .prop('columns') - .find((c) => c.id === 'signal.rule.risk_score')?.cellActions - ).toBeUndefined(); - }); - - test('it does NOT render switches for hiding columns in the `EuiDataGrid` `Columns` popover', async () => { - render( - - - - ); - - // Click the `EuidDataGrid` `Columns` button to open the popover: - fireEvent.click(screen.getByTestId('dataGridColumnSelectorButton')); - - // `EuiDataGrid` renders switches for hiding in the `Columns` popover when `showColumnSelector.allowHide` is `true` - const switches = await screen.queryAllByRole('switch'); - - expect(switches.length).toBe(0); // no switches are rendered, because `allowHide` is `false` - }); - - test('it dispatches the `REMOVE_COLUMN` action when a user clicks `Remove column` in the column header popover', async () => { - render( - - - - ); - - // click the `@timestamp` column header to display the popover - fireEvent.click(screen.getByText('@timestamp')); - - // click the `Remove column` action in the popover - fireEvent.click(await screen.getByText(REMOVE_COLUMN)); - - expect(mockDispatch).toBeCalledWith({ - payload: { columnId: '@timestamp', id: 'timeline-test' }, - type: 'x-pack/timelines/t-grid/REMOVE_COLUMN', - }); - }); - - test('it dispatches the `UPDATE_COLUMN_WIDTH` action when a user resizes a column', async () => { - render( - - - - ); - - // simulate resizing the column - fireEvent.mouseDown(screen.getAllByTestId('dataGridColumnResizer')[0]); - fireEvent.mouseMove(screen.getAllByTestId('dataGridColumnResizer')[0]); - fireEvent.mouseUp(screen.getAllByTestId('dataGridColumnResizer')[0]); - - expect(mockDispatch).toBeCalledWith({ - payload: { columnId: '@timestamp', id: 'timeline-test', width: NaN }, - type: 'x-pack/timelines/t-grid/UPDATE_COLUMN_WIDTH', - }); - }); -}); diff --git a/x-pack/plugins/timelines/public/components/t_grid/body/index.tsx b/x-pack/plugins/timelines/public/components/t_grid/body/index.tsx deleted file mode 100644 index a3b8ff5457d8f..0000000000000 --- a/x-pack/plugins/timelines/public/components/t_grid/body/index.tsx +++ /dev/null @@ -1,975 +0,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 { - EuiDataGrid, - EuiDataGridRefProps, - EuiDataGridColumn, - EuiDataGridCellValueElementProps, - EuiDataGridControlColumn, - EuiDataGridStyle, - EuiDataGridToolBarVisibilityOptions, - EuiLoadingSpinner, - EuiFlexGroup, - EuiFlexItem, - EuiProgress, -} from '@elastic/eui'; -import { getOr } from 'lodash/fp'; -import memoizeOne from 'memoize-one'; -import React, { - ComponentType, - lazy, - Suspense, - useCallback, - useEffect, - useMemo, - useState, - useContext, - useRef, -} from 'react'; -import { connect, ConnectedProps, useDispatch } from 'react-redux'; - -import styled, { ThemeContext } from 'styled-components'; -import { ALERT_RULE_CONSUMER, ALERT_RULE_PRODUCER } from '@kbn/rule-data-utils'; -import { Filter } from '@kbn/es-query'; -import type { EuiTheme } from '@kbn/kibana-react-plugin/common'; -import { FieldBrowserOptions } from '@kbn/triggers-actions-ui-plugin/public'; -import { useKibana } from '@kbn/kibana-react-plugin/public'; -import { - TGridCellAction, - BulkActionsProp, - CellValueElementProps, - ColumnHeaderOptions, - ControlColumnProps, - RowRenderer, - AlertStatus, - SortColumnTable, - SetEventsLoading, - SetEventsDeleted, -} from '../../../../common/types/timeline'; - -import type { TimelineItem, TimelineNonEcsData } from '../../../../common/search_strategy/timeline'; - -import { getColumnHeader, getColumnHeaders } from './column_headers/helpers'; -import { - addBuildingBlockStyle, - getEventIdToDataMapping, - hasCellActions, - mapSortDirectionToDirection, - mapSortingColumns, -} from './helpers'; - -import type { BrowserFields } from '../../../../common/search_strategy/index_fields'; -import type { OnRowSelected, OnSelectAll } from '../types'; -import type { Refetch } from '../../../store/t_grid/inputs'; -import { Ecs } from '../../../../common/ecs'; -import { getPageRowIndex } from '../../../../common/utils/pagination'; -import { StatefulEventContext } from '../../stateful_event_context'; -import { tGridActions, TGridModel, tGridSelectors, TableState } from '../../../store/t_grid'; -import { useDeepEqualSelector } from '../../../hooks/use_selector'; -import { RowAction } from './row_action'; -import * as i18n from './translations'; -import { AlertCount } from '../styles'; -import { checkBoxControlColumn } from './control_columns'; -import { ViewSelection } from '../event_rendered_view/selector'; -import { EventRenderedView } from '../event_rendered_view'; -import { REMOVE_COLUMN } from './column_headers/translations'; -import { TimelinesStartPlugins } from '../../../types'; - -const StatefulAlertBulkActions = lazy(() => import('../toolbar/bulk_actions/alert_bulk_actions')); - -interface OwnProps { - activePage: number; - additionalControls?: React.ReactNode; - appId?: string; - browserFields: BrowserFields; - bulkActions?: BulkActionsProp; - data: TimelineItem[]; - defaultCellActions?: TGridCellAction[]; - disabledCellActions: string[]; - fieldBrowserOptions?: FieldBrowserOptions; - filters?: Filter[]; - filterQuery?: string; - filterStatus?: AlertStatus; - getRowRenderer?: ({ - data, - rowRenderers, - }: { - data: Ecs; - rowRenderers: RowRenderer[]; - }) => RowRenderer | null; - id: string; - indexNames: string[]; - isEventViewer?: boolean; - itemsPerPageOptions: number[]; - leadingControlColumns?: ControlColumnProps[]; - loadPage: (newActivePage: number) => void; - onRuleChange?: () => void; - pageSize: number; - refetch: Refetch; - renderCellValue: (props: CellValueElementProps) => React.ReactNode; - rowRenderers: RowRenderer[]; - tableView: ViewSelection; - tabType: string; - totalItems: number; - trailingControlColumns?: ControlColumnProps[]; - unit?: (total: number) => React.ReactNode; - hasAlertsCrud?: boolean; - hasAlertsCrudPermissions?: ({ - ruleConsumer, - ruleProducer, - }: { - ruleConsumer: string; - ruleProducer?: string; - }) => boolean; - totalSelectAllAlerts?: number; - showCheckboxes?: boolean; -} - -const defaultUnit = (n: number) => i18n.ALERTS_UNIT(n); - -const ES_LIMIT_COUNT = 9999; - -const EMPTY_CONTROL_COLUMNS: ControlColumnProps[] = []; - -const EmptyHeaderCellRender: ComponentType = () => null; - -const gridStyle: EuiDataGridStyle = { border: 'none', fontSize: 's', header: 'underline' }; - -const EuiEventTableContainer = styled.div<{ hideLastPage: boolean }>` - ul.euiPagination__list { - li.euiPagination__item:last-child { - ${({ hideLastPage }) => `${hideLastPage ? 'display:none' : ''}`}; - } - } -`; - -const transformControlColumns = ({ - columnHeaders, - controlColumns, - data, - fieldBrowserOptions, - isEventViewer = false, - loadingEventIds, - onRowSelected, - onRuleChange, - selectedEventIds, - showCheckboxes, - tabType, - timelineId, - isSelectAllChecked, - onSelectPage, - browserFields, - pageSize, - sort, - theme, - setEventsLoading, - setEventsDeleted, - hasAlertsCrudPermissions, -}: { - columnHeaders: ColumnHeaderOptions[]; - controlColumns: ControlColumnProps[]; - data: TimelineItem[]; - disabledCellActions: string[]; - fieldBrowserOptions?: FieldBrowserOptions; - isEventViewer?: boolean; - loadingEventIds: string[]; - onRowSelected: OnRowSelected; - onRuleChange?: () => void; - selectedEventIds: Record; - showCheckboxes: boolean; - tabType: string; - timelineId: string; - isSelectAllChecked: boolean; - browserFields: BrowserFields; - onSelectPage: OnSelectAll; - pageSize: number; - sort: SortColumnTable[]; - theme: EuiTheme; - setEventsLoading: SetEventsLoading; - setEventsDeleted: SetEventsDeleted; - hasAlertsCrudPermissions?: ({ - ruleConsumer, - ruleProducer, - }: { - ruleConsumer: string; - ruleProducer?: string; - }) => boolean; -}): EuiDataGridControlColumn[] => - controlColumns.map( - ({ id: columnId, headerCellRender = EmptyHeaderCellRender, rowCellRender, width }, i) => ({ - id: `${columnId}`, - headerCellRender: () => { - const HeaderActions = headerCellRender; - return ( - <> - {HeaderActions && ( - - )} - - ); - }, - rowCellRender: ({ - isDetails, - isExpandable, - isExpanded, - rowIndex, - colIndex, - setCellProps, - }: EuiDataGridCellValueElementProps) => { - const pageRowIndex = getPageRowIndex(rowIndex, pageSize); - const rowData = data[pageRowIndex]; - - let disabled = false; - if (rowData) { - addBuildingBlockStyle(rowData.ecs, theme, setCellProps); - if (columnId === 'checkbox-control-column' && hasAlertsCrudPermissions != null) { - // FUTURE ENGINEER, the assumption here is you can only have one producer and consumer at this time - const ruleConsumers = - rowData.data.find((d) => d.field === ALERT_RULE_CONSUMER)?.value ?? []; - const ruleProducers = - rowData.data.find((d) => d.field === ALERT_RULE_PRODUCER)?.value ?? []; - disabled = !hasAlertsCrudPermissions({ - ruleConsumer: ruleConsumers.length > 0 ? ruleConsumers[0] : '', - ruleProducer: ruleProducers.length > 0 ? ruleProducers[0] : undefined, - }); - } - } else { - // disable the cell when it has no data - setCellProps({ style: { display: 'none' } }); - } - - return ( - - ); - }, - width, - }) - ); - -export type StatefulBodyProps = OwnProps & PropsFromRedux; - -/** - * The Body component is used everywhere timeline is used within the security application. It is the highest level component - * that is shared across all implementations of the timeline. - */ - -export const BodyComponent = React.memo( - ({ - activePage, - additionalControls, - appId = '', - browserFields, - bulkActions = true, - clearSelected, - columnHeaders, - data, - defaultCellActions, - disabledCellActions, - fieldBrowserOptions, - filterQuery, - filters, - filterStatus, - getRowRenderer, - hasAlertsCrud, - hasAlertsCrudPermissions, - id, - indexNames, - isEventViewer = false, - isLoading, - isSelectAllChecked, - itemsPerPageOptions, - leadingControlColumns = EMPTY_CONTROL_COLUMNS, - loadingEventIds, - loadPage, - onRuleChange, - pageSize, - refetch, - renderCellValue, - rowRenderers, - selectedEventIds, - setSelected, - showCheckboxes, - sort, - tableView = 'gridView', - tabType, - totalItems, - totalSelectAllAlerts, - trailingControlColumns = EMPTY_CONTROL_COLUMNS, - unit = defaultUnit, - }) => { - const { triggersActionsUi } = useKibana().services; - - const dataGridRef = useRef(null); - - const dispatch = useDispatch(); - const getManageTimeline = useMemo(() => tGridSelectors.getManageDataTableById(), []); - const { queryFields, selectAll, defaultColumns } = useDeepEqualSelector((state) => - getManageTimeline(state, id) - ); - - const alertCountText = useMemo( - () => `${totalItems.toLocaleString()} ${unit(totalItems)}`, - [totalItems, unit] - ); - - const selectedCount = useMemo(() => Object.keys(selectedEventIds).length, [selectedEventIds]); - - const theme: EuiTheme = useContext(ThemeContext); - const onRowSelected: OnRowSelected = useCallback( - ({ eventIds, isSelected }: { eventIds: string[]; isSelected: boolean }) => { - setSelected({ - id, - eventIds: getEventIdToDataMapping( - data, - eventIds, - queryFields, - hasAlertsCrud ?? false, - hasAlertsCrudPermissions - ), - isSelected, - isSelectAllChecked: isSelected && selectedCount + 1 === data.length, - }); - }, - [setSelected, id, data, queryFields, hasAlertsCrud, hasAlertsCrudPermissions, selectedCount] - ); - - const onSelectPage: OnSelectAll = useCallback( - ({ isSelected }: { isSelected: boolean }) => - isSelected - ? setSelected({ - id, - eventIds: getEventIdToDataMapping( - data, - data.map((event) => event._id), - queryFields, - hasAlertsCrud ?? false, - hasAlertsCrudPermissions - ), - isSelected, - isSelectAllChecked: isSelected, - }) - : clearSelected({ id }), - [setSelected, id, data, queryFields, hasAlertsCrud, hasAlertsCrudPermissions, clearSelected] - ); - - // Sync to selectAll so parent components can select all events - useEffect(() => { - if (selectAll && !isSelectAllChecked) { - onSelectPage({ isSelected: true }); - } - }, [isSelectAllChecked, onSelectPage, selectAll]); - - const onAlertStatusActionSuccess = useMemo(() => { - if (bulkActions && bulkActions !== true) { - return bulkActions.onAlertStatusActionSuccess; - } - }, [bulkActions]); - - const onAlertStatusActionFailure = useMemo(() => { - if (bulkActions && bulkActions !== true) { - return bulkActions.onAlertStatusActionFailure; - } - }, [bulkActions]); - - const additionalBulkActions = useMemo(() => { - if (bulkActions && bulkActions !== true && bulkActions.customBulkActions !== undefined) { - return bulkActions.customBulkActions.map((action) => { - return { - ...action, - onClick: (eventIds: string[]) => { - const items = data.filter((item) => { - return eventIds.find((event) => item._id === event); - }); - action.onClick(items); - }, - }; - }); - } - }, [bulkActions, data]); - - const showAlertStatusActions = useMemo(() => { - if (!hasAlertsCrud) { - return false; - } - if (typeof bulkActions === 'boolean') { - return bulkActions; - } - return bulkActions.alertStatusActions ?? true; - }, [bulkActions, hasAlertsCrud]); - - const showBulkActions = useMemo(() => { - if (!hasAlertsCrud) { - return false; - } - - if (selectedCount === 0 || !showCheckboxes) { - return false; - } - if (typeof bulkActions === 'boolean') { - return bulkActions; - } - return (bulkActions?.customBulkActions?.length || bulkActions?.alertStatusActions) ?? true; - }, [hasAlertsCrud, selectedCount, showCheckboxes, bulkActions]); - - const onResetColumns = useCallback(() => { - dispatch(tGridActions.updateColumns({ id, columns: defaultColumns })); - }, [defaultColumns, dispatch, id]); - - const onToggleColumn = useCallback( - (columnId: string) => { - if (columnHeaders.some(({ id: columnHeaderId }) => columnId === columnHeaderId)) { - dispatch( - tGridActions.removeColumn({ - columnId, - id, - }) - ); - } else { - dispatch( - tGridActions.upsertColumn({ - column: getColumnHeader(columnId, defaultColumns), - id, - index: 1, - }) - ); - } - }, - [columnHeaders, dispatch, id, defaultColumns] - ); - - const alertToolbar = useMemo( - () => ( - - - {alertCountText} - - {showBulkActions && ( - }> - - - )} - - ), - [ - additionalBulkActions, - alertCountText, - filterQuery, - filterStatus, - id, - indexNames, - onAlertStatusActionFailure, - onAlertStatusActionSuccess, - refetch, - showAlertStatusActions, - showBulkActions, - totalItems, - totalSelectAllAlerts, - ] - ); - - const toolbarVisibility: EuiDataGridToolBarVisibilityOptions = useMemo( - () => ({ - additionalControls: ( - <> - {isLoading && } - {alertCountText} - {showBulkActions ? ( - <> - }> - - - {additionalControls ?? null} - - ) : ( - <> - {additionalControls ?? null} - {triggersActionsUi.getFieldBrowser({ - browserFields, - options: fieldBrowserOptions, - columnIds: columnHeaders.map(({ id: columnId }) => columnId), - onResetColumns, - onToggleColumn, - })} - - )} - - ), - ...(showBulkActions - ? { - showColumnSelector: false, - showSortSelector: false, - showFullScreenSelector: false, - } - : { - showColumnSelector: { allowHide: false, allowReorder: true }, - showSortSelector: true, - showFullScreenSelector: true, - }), - showDisplaySelector: false, - }), - [ - isLoading, - alertCountText, - showBulkActions, - showAlertStatusActions, - id, - totalSelectAllAlerts, - totalItems, - filterStatus, - filterQuery, - indexNames, - onAlertStatusActionSuccess, - onAlertStatusActionFailure, - onResetColumns, - onToggleColumn, - triggersActionsUi, - additionalBulkActions, - refetch, - additionalControls, - browserFields, - fieldBrowserOptions, - columnHeaders, - ] - ); - - const sortingColumns: Array<{ - id: string; - direction: 'asc' | 'desc'; - }> = useMemo( - () => - sort.map((x) => ({ - id: x.columnId, - direction: mapSortDirectionToDirection(x.sortDirection), - })), - [sort] - ); - - const onSort = useCallback( - ( - nextSortingColumns: Array<{ - id: string; - direction: 'asc' | 'desc'; - }> - ) => { - dispatch( - tGridActions.updateSort({ - id, - sort: mapSortingColumns({ columns: nextSortingColumns, columnHeaders }), - }) - ); - - setTimeout(() => { - // schedule the query to be re-executed from page 0, (but only after the - // store has been updated with the new sort): - if (loadPage != null) { - loadPage(0); - } - }, 0); - }, - [columnHeaders, dispatch, id, loadPage] - ); - - const visibleColumns = useMemo(() => columnHeaders.map(({ id: cid }) => cid), [columnHeaders]); // the full set of columns - - const onColumnResize = useCallback( - ({ columnId, width }: { columnId: string; width: number }) => { - dispatch( - tGridActions.updateColumnWidth({ - columnId, - id, - width, - }) - ); - }, - [dispatch, id] - ); - - const onSetVisibleColumns = useCallback( - (newVisibleColumns: string[]) => { - dispatch( - tGridActions.updateColumnOrder({ - columnIds: newVisibleColumns, - id, - }) - ); - }, - [dispatch, id] - ); - - const setEventsLoading = useCallback( - ({ eventIds, isLoading: loading }) => { - dispatch(tGridActions.setEventsLoading({ id, eventIds, isLoading: loading })); - }, - [dispatch, id] - ); - - const setEventsDeleted = useCallback( - ({ eventIds, isDeleted }) => { - dispatch(tGridActions.setEventsDeleted({ id, eventIds, isDeleted })); - }, - [dispatch, id] - ); - - const [leadingTGridControlColumns, trailingTGridControlColumns] = useMemo(() => { - return [ - showCheckboxes ? [checkBoxControlColumn, ...leadingControlColumns] : leadingControlColumns, - trailingControlColumns, - ].map((controlColumns) => - transformControlColumns({ - columnHeaders, - controlColumns, - data, - disabledCellActions, - fieldBrowserOptions, - isEventViewer, - loadingEventIds, - onRowSelected, - onRuleChange, - selectedEventIds, - showCheckboxes, - tabType, - timelineId: id, - isSelectAllChecked, - sort, - browserFields, - onSelectPage, - theme, - setEventsLoading, - setEventsDeleted, - pageSize, - hasAlertsCrudPermissions, - }) - ); - }, [ - showCheckboxes, - leadingControlColumns, - trailingControlColumns, - columnHeaders, - data, - disabledCellActions, - fieldBrowserOptions, - isEventViewer, - id, - loadingEventIds, - onRowSelected, - onRuleChange, - selectedEventIds, - tabType, - isSelectAllChecked, - sort, - browserFields, - onSelectPage, - theme, - pageSize, - setEventsLoading, - setEventsDeleted, - hasAlertsCrudPermissions, - ]); - const closeCellPopoverAction = dataGridRef.current?.closeCellPopover; - const columnsWithCellActions: EuiDataGridColumn[] = useMemo( - () => - columnHeaders.map((header) => { - const buildAction = (tGridCellAction: TGridCellAction) => - tGridCellAction({ - browserFields, - data: data.map((row) => row.data), - ecsData: data.map((row) => row.ecs), - header: columnHeaders.find((h) => h.id === header.id), - pageSize, - scopeId: id, - closeCellPopover: closeCellPopoverAction, - }); - return { - ...header, - actions: { - ...header.actions, - additional: [ - { - iconType: 'cross', - label: REMOVE_COLUMN, - onClick: () => { - dispatch(tGridActions.removeColumn({ id, columnId: header.id })); - }, - size: 'xs', - }, - ], - }, - ...(hasCellActions({ - columnId: header.id, - disabledCellActions, - }) - ? { - cellActions: - header.tGridCellActions?.map(buildAction) ?? - defaultCellActions?.map(buildAction), - visibleCellActions: 3, - } - : {}), - }; - }), - [ - browserFields, - columnHeaders, - data, - defaultCellActions, - disabledCellActions, - dispatch, - id, - pageSize, - closeCellPopoverAction, - ] - ); - - const renderTGridCellValue = useMemo(() => { - const Cell: React.FC = ({ - columnId, - rowIndex, - colIndex, - setCellProps, - isDetails, - }): React.ReactElement | null => { - const pageRowIndex = getPageRowIndex(rowIndex, pageSize); - const rowData = pageRowIndex < data.length ? data[pageRowIndex].data : null; - const header = columnHeaders.find((h) => h.id === columnId); - const eventId = pageRowIndex < data.length ? data[pageRowIndex]._id : null; - const ecs = pageRowIndex < data.length ? data[pageRowIndex].ecs : null; - - useEffect(() => { - const defaultStyles = { overflow: 'hidden' }; - setCellProps({ style: { ...defaultStyles } }); - if (ecs && rowData) { - addBuildingBlockStyle(ecs, theme, setCellProps, defaultStyles); - } else { - // disable the cell when it has no data - setCellProps({ style: { display: 'none' } }); - } - }, [rowIndex, setCellProps, ecs, rowData]); - - if (rowData == null || header == null || eventId == null || ecs === null) { - return null; - } - - return renderCellValue({ - browserFields, - columnId: header.id, - data: rowData, - ecsData: ecs, - eventId, - globalFilters: filters, - header, - isDetails, - isDraggable: false, - isExpandable: true, - isExpanded: false, - linkValues: getOr([], header.linkField ?? '', ecs), - rowIndex, - colIndex, - rowRenderers, - setCellProps, - scopeId: id, - truncate: isDetails ? false : true, - closeCellPopover: closeCellPopoverAction, - }) as React.ReactElement; - }; - return Cell; - }, [ - browserFields, - columnHeaders, - data, - filters, - id, - pageSize, - renderCellValue, - rowRenderers, - theme, - closeCellPopoverAction, - ]); - - const onChangeItemsPerPage = useCallback( - (itemsChangedPerPage) => { - dispatch(tGridActions.updateItemsPerPage({ id, itemsPerPage: itemsChangedPerPage })); - }, - [id, dispatch] - ); - - const onChangePage = useCallback( - (page) => { - loadPage(page); - }, - [loadPage] - ); - - // Store context in state rather than creating object in provider value={} to prevent re-renders caused by a new object being created - const [activeStatefulEventContext] = useState({ - timelineID: id, - tabType, - enableHostDetailsFlyout: true, - enableIpDetailsFlyout: true, - }); - return ( - <> - - {tableView === 'gridView' && ( - ES_LIMIT_COUNT}> - - - )} - {tableView === 'eventRenderedView' && ( - ES_LIMIT_COUNT}> - - - )} - - - ); - } -); - -BodyComponent.displayName = 'BodyComponent'; - -const makeMapStateToProps = () => { - const memoizedColumnHeaders: ( - headers: ColumnHeaderOptions[], - browserFields: BrowserFields - ) => ColumnHeaderOptions[] = memoizeOne(getColumnHeaders); - - const getTGrid = tGridSelectors.getTGridByIdSelector(); - const mapStateToProps = (state: TableState, { browserFields, id, hasAlertsCrud }: OwnProps) => { - const dataTable: TGridModel = getTGrid(state, id); - const { - columns, - isSelectAllChecked, - loadingEventIds, - selectedEventIds, - showCheckboxes, - sort, - isLoading, - } = dataTable; - - return { - columnHeaders: memoizedColumnHeaders(columns, browserFields), - isSelectAllChecked, - loadingEventIds, - isLoading, - id, - selectedEventIds, - showCheckboxes: hasAlertsCrud === true && showCheckboxes, - sort, - }; - }; - return mapStateToProps; -}; - -const mapDispatchToProps = { - clearSelected: tGridActions.clearSelected, - setSelected: tGridActions.setSelected, -}; - -const connector = connect(makeMapStateToProps, mapDispatchToProps); - -type PropsFromRedux = ConnectedProps; - -export const StatefulBody: React.FunctionComponent = connector(BodyComponent); diff --git a/x-pack/plugins/timelines/public/components/t_grid/body/renderers/__snapshots__/plain_row_renderer.test.tsx.snap b/x-pack/plugins/timelines/public/components/t_grid/body/renderers/__snapshots__/plain_row_renderer.test.tsx.snap deleted file mode 100644 index 66a1b293cf8b9..0000000000000 --- a/x-pack/plugins/timelines/public/components/t_grid/body/renderers/__snapshots__/plain_row_renderer.test.tsx.snap +++ /dev/null @@ -1,3 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`plain_row_renderer renders correctly against snapshot 1`] = ``; diff --git a/x-pack/plugins/timelines/public/components/t_grid/body/renderers/get_column_renderer.ts b/x-pack/plugins/timelines/public/components/t_grid/body/renderers/get_column_renderer.ts deleted file mode 100644 index 78f7119124e0a..0000000000000 --- a/x-pack/plugins/timelines/public/components/t_grid/body/renderers/get_column_renderer.ts +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type { TimelineNonEcsData } from '../../../../../common/search_strategy'; -import type { ColumnRenderer } from '../../../../../common/types/timeline'; - -const unhandledColumnRenderer = (): never => { - throw new Error('Unhandled Column Renderer'); -}; - -export const getColumnRenderer = ( - columnName: string, - columnRenderers: ColumnRenderer[], - data: TimelineNonEcsData[] -): ColumnRenderer => { - const renderer = columnRenderers.find((columnRenderer) => - columnRenderer.isInstance(columnName, data) - ); - return renderer != null ? renderer : unhandledColumnRenderer(); -}; diff --git a/x-pack/plugins/timelines/public/components/t_grid/body/renderers/get_row_renderer.ts b/x-pack/plugins/timelines/public/components/t_grid/body/renderers/get_row_renderer.ts deleted file mode 100644 index eba694c935e85..0000000000000 --- a/x-pack/plugins/timelines/public/components/t_grid/body/renderers/get_row_renderer.ts +++ /dev/null @@ -1,12 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type { Ecs } from '../../../../../common/ecs'; -import type { RowRenderer } from '../../../../../common/types/timeline'; - -export const getRowRenderer = (ecs: Ecs, rowRenderers: RowRenderer[]): RowRenderer | null => - rowRenderers.find((rowRenderer) => rowRenderer.isInstance(ecs)) ?? null; diff --git a/x-pack/plugins/timelines/public/components/t_grid/body/renderers/plain_row_renderer.test.tsx b/x-pack/plugins/timelines/public/components/t_grid/body/renderers/plain_row_renderer.test.tsx deleted file mode 100644 index ca07cf2083345..0000000000000 --- a/x-pack/plugins/timelines/public/components/t_grid/body/renderers/plain_row_renderer.test.tsx +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { mount, shallow } from 'enzyme'; -import { cloneDeep } from 'lodash'; -import React from 'react'; -import { Ecs } from '../../../../../common/ecs'; -import { mockTimelineData } from '../../../../mock'; - -import { plainRowRenderer } from './plain_row_renderer'; - -describe('plain_row_renderer', () => { - let mockDatum: Ecs; - beforeEach(() => { - mockDatum = cloneDeep(mockTimelineData[0].ecs); - }); - - test('renders correctly against snapshot', () => { - const children = plainRowRenderer.renderRow({ - data: mockDatum, - isDraggable: false, - scopeId: 'test', - }); - const wrapper = shallow({children}); - expect(wrapper).toMatchSnapshot(); - }); - - test('should always return isInstance true', () => { - expect(plainRowRenderer.isInstance(mockDatum)).toBe(true); - }); - - test('should render a plain row', () => { - const children = plainRowRenderer.renderRow({ - data: mockDatum, - isDraggable: false, - scopeId: 'test', - }); - const wrapper = mount({children}); - expect(wrapper.text()).toEqual(''); - }); -}); diff --git a/x-pack/plugins/timelines/public/components/t_grid/body/renderers/plain_row_renderer.tsx b/x-pack/plugins/timelines/public/components/t_grid/body/renderers/plain_row_renderer.tsx deleted file mode 100644 index d641de4d01ba2..0000000000000 --- a/x-pack/plugins/timelines/public/components/t_grid/body/renderers/plain_row_renderer.tsx +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; - -import { RowRendererId } from '../../../../../common/types/timeline'; -import type { RowRenderer } from '../../../../../common/types/timeline'; - -const PlainRowRenderer = () => <>; - -PlainRowRenderer.displayName = 'PlainRowRenderer'; - -export const plainRowRenderer: RowRenderer = { - id: RowRendererId.plain, - isInstance: (_) => true, - renderRow: PlainRowRenderer, -}; diff --git a/x-pack/plugins/timelines/public/components/t_grid/body/renderers/row_renderer.tsx b/x-pack/plugins/timelines/public/components/t_grid/body/renderers/row_renderer.tsx deleted file mode 100644 index 64f1338b11a58..0000000000000 --- a/x-pack/plugins/timelines/public/components/t_grid/body/renderers/row_renderer.tsx +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; - -import { EventsTrSupplement } from '../../styles'; - -interface RowRendererContainerProps { - children: React.ReactNode; -} - -export const RowRendererContainer = React.memo(({ children }) => ( - - {children} - -)); -RowRendererContainer.displayName = 'RowRendererContainer'; diff --git a/x-pack/plugins/timelines/public/components/t_grid/body/row_action/index.tsx b/x-pack/plugins/timelines/public/components/t_grid/body/row_action/index.tsx deleted file mode 100644 index d006a0abef336..0000000000000 --- a/x-pack/plugins/timelines/public/components/t_grid/body/row_action/index.tsx +++ /dev/null @@ -1,147 +0,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 { EuiDataGridCellValueElementProps } from '@elastic/eui'; -import React, { useCallback, useMemo } from 'react'; -import { useDispatch } from 'react-redux'; - -import { TimelineItem, TimelineNonEcsData } from '../../../../../common/search_strategy'; -import type { - ColumnHeaderOptions, - ControlColumnProps, - OnRowSelected, - SetEventsLoading, - SetEventsDeleted, - DataExpandedDetailType, -} from '../../../../../common/types/timeline'; -import { getMappedNonEcsValue } from '../data_driven_columns'; -import { tGridActions } from '../../../../store/t_grid'; - -type Props = EuiDataGridCellValueElementProps & { - columnHeaders: ColumnHeaderOptions[]; - controlColumn: ControlColumnProps; - data: TimelineItem[]; - disabled: boolean; - index: number; - isEventViewer: boolean; - loadingEventIds: Readonly; - onRowSelected: OnRowSelected; - onRuleChange?: () => void; - selectedEventIds: Readonly>; - showCheckboxes: boolean; - tabType?: string; - tableId: string; - width: number; - setEventsLoading: SetEventsLoading; - setEventsDeleted: SetEventsDeleted; - pageRowIndex: number; -}; - -const RowActionComponent = ({ - columnHeaders, - controlColumn, - data, - disabled, - index, - isEventViewer, - loadingEventIds, - onRowSelected, - onRuleChange, - pageRowIndex, - rowIndex, - selectedEventIds, - showCheckboxes, - tabType, - tableId, - setEventsLoading, - setEventsDeleted, - width, -}: Props) => { - const { - data: timelineNonEcsData, - ecs: ecsData, - _id: eventId, - _index: indexName, - } = useMemo(() => { - const rowData: Partial = data[pageRowIndex]; - return rowData ?? {}; - }, [data, pageRowIndex]); - - const dispatch = useDispatch(); - - const columnValues = useMemo( - () => - timelineNonEcsData && - columnHeaders - .map( - (header) => - getMappedNonEcsValue({ - data: timelineNonEcsData, - fieldName: header.id, - }) ?? [] - ) - .join(' '), - [columnHeaders, timelineNonEcsData] - ); - - const handleOnEventDetailPanelOpened = useCallback(() => { - const updatedExpandedDetail: DataExpandedDetailType = { - panelView: 'eventDetail', - params: { - eventId: eventId ?? '', - indexName: indexName ?? '', - }, - }; - - dispatch( - tGridActions.toggleDetailPanel({ - ...updatedExpandedDetail, - tabType, - id: tableId, - }) - ); - }, [dispatch, eventId, indexName, tabType, tableId]); - - const Action = controlColumn.rowCellRender; - - if (!timelineNonEcsData || !ecsData || !eventId) { - return ; - } - - return ( - <> - {Action && ( - - )} - - ); -}; - -export const RowAction = React.memo(RowActionComponent); diff --git a/x-pack/plugins/timelines/public/components/t_grid/body/sort/__snapshots__/sort_indicator.test.tsx.snap b/x-pack/plugins/timelines/public/components/t_grid/body/sort/__snapshots__/sort_indicator.test.tsx.snap deleted file mode 100644 index 8a7b179da059f..0000000000000 --- a/x-pack/plugins/timelines/public/components/t_grid/body/sort/__snapshots__/sort_indicator.test.tsx.snap +++ /dev/null @@ -1,19 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`SortIndicator rendering renders correctly against snapshot 1`] = ` - - - - -`; diff --git a/x-pack/plugins/timelines/public/components/t_grid/body/sort/index.ts b/x-pack/plugins/timelines/public/components/t_grid/body/sort/index.ts deleted file mode 100644 index f5997a82658fd..0000000000000 --- a/x-pack/plugins/timelines/public/components/t_grid/body/sort/index.ts +++ /dev/null @@ -1,15 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { SortDirection } from '../../../../../common/types/timeline'; -import type { SortColumnTable } from '../../../../../common/types/timeline'; - -// TODO: Cleanup this type to match SortColumnTimeline -export type { SortDirection }; - -/** Specifies which column the timeline is sorted on */ -export type Sort = SortColumnTable; diff --git a/x-pack/plugins/timelines/public/components/t_grid/body/sort/sort_indicator.test.tsx b/x-pack/plugins/timelines/public/components/t_grid/body/sort/sort_indicator.test.tsx deleted file mode 100644 index 3812f44d95ccd..0000000000000 --- a/x-pack/plugins/timelines/public/components/t_grid/body/sort/sort_indicator.test.tsx +++ /dev/null @@ -1,85 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { mount, shallow } from 'enzyme'; -import React from 'react'; -import { Direction } from '../../../../../common/search_strategy'; - -import * as i18n from '../translations'; - -import { getDirection, SortIndicator } from './sort_indicator'; - -describe('SortIndicator', () => { - describe('rendering', () => { - test('renders correctly against snapshot', () => { - const wrapper = shallow(); - expect(wrapper).toMatchSnapshot(); - }); - - test('it renders the expected sort indicator when direction is ascending', () => { - const wrapper = mount(); - - expect(wrapper.find('[data-test-subj="sortIndicator"]').first().prop('type')).toEqual( - 'sortUp' - ); - }); - - test('it renders the expected sort indicator when direction is descending', () => { - const wrapper = mount(); - - expect(wrapper.find('[data-test-subj="sortIndicator"]').first().prop('type')).toEqual( - 'sortDown' - ); - }); - - test('it renders the expected sort indicator when direction is `none`', () => { - const wrapper = mount(); - - expect(wrapper.find('[data-test-subj="sortIndicator"]').first().prop('type')).toEqual( - 'empty' - ); - }); - }); - - describe('getDirection', () => { - test('it returns the expected symbol when the direction is ascending', () => { - expect(getDirection(Direction.asc)).toEqual('sortUp'); - }); - - test('it returns the expected symbol when the direction is descending', () => { - expect(getDirection(Direction.desc)).toEqual('sortDown'); - }); - - test('it returns the expected symbol (undefined) when the direction is neither ascending, nor descending', () => { - expect(getDirection('none')).toEqual(undefined); - }); - }); - - describe('sort indicator tooltip', () => { - test('it returns the expected tooltip when the direction is ascending', () => { - const wrapper = mount(); - - expect( - wrapper.find('[data-test-subj="sort-indicator-tooltip"]').first().props().content - ).toEqual(i18n.SORTED_ASCENDING); - }); - - test('it returns the expected tooltip when the direction is descending', () => { - const wrapper = mount(); - - expect( - wrapper.find('[data-test-subj="sort-indicator-tooltip"]').first().props().content - ).toEqual(i18n.SORTED_DESCENDING); - }); - - test('it does NOT render a tooltip when sort direction is `none`', () => { - const wrapper = mount(); - - expect(wrapper.find('[data-test-subj="sort-indicator-tooltip"]').exists()).toBe(false); - }); - }); -}); diff --git a/x-pack/plugins/timelines/public/components/t_grid/body/sort/sort_indicator.tsx b/x-pack/plugins/timelines/public/components/t_grid/body/sort/sort_indicator.tsx deleted file mode 100644 index 3c7d8a35b9021..0000000000000 --- a/x-pack/plugins/timelines/public/components/t_grid/body/sort/sort_indicator.tsx +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { EuiIcon, EuiToolTip } from '@elastic/eui'; -import React from 'react'; - -import * as i18n from '../translations'; -import { SortNumber } from './sort_number'; - -import type { SortDirection } from '.'; -import { Direction } from '../../../../../common/search_strategy'; - -enum SortDirectionIndicatorEnum { - SORT_UP = 'sortUp', - SORT_DOWN = 'sortDown', -} - -export type SortDirectionIndicator = undefined | SortDirectionIndicatorEnum; - -/** Returns the symbol that corresponds to the specified `SortDirection` */ -export const getDirection = (sortDirection: SortDirection): SortDirectionIndicator => { - switch (sortDirection) { - case Direction.asc: - return SortDirectionIndicatorEnum.SORT_UP; - case Direction.desc: - return SortDirectionIndicatorEnum.SORT_DOWN; - case 'none': - return undefined; - default: - throw new Error('Unhandled sort direction'); - } -}; - -interface Props { - sortDirection: SortDirection; - sortNumber: number; -} - -/** Renders a sort indicator */ -export const SortIndicator = React.memo(({ sortDirection, sortNumber }) => { - const direction = getDirection(sortDirection); - - if (direction != null) { - return ( - - <> - - - - - ); - } else { - return ; - } -}); - -SortIndicator.displayName = 'SortIndicator'; diff --git a/x-pack/plugins/timelines/public/components/t_grid/body/sort/sort_number.tsx b/x-pack/plugins/timelines/public/components/t_grid/body/sort/sort_number.tsx deleted file mode 100644 index 3fdd31eae5c47..0000000000000 --- a/x-pack/plugins/timelines/public/components/t_grid/body/sort/sort_number.tsx +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { EuiIcon, EuiNotificationBadge } from '@elastic/eui'; -import React from 'react'; - -interface Props { - sortNumber: number; -} - -export const SortNumber = React.memo(({ sortNumber }) => { - if (sortNumber >= 0) { - return ( - - {sortNumber + 1} - - ); - } else { - return ; - } -}); - -SortNumber.displayName = 'SortNumber'; diff --git a/x-pack/plugins/timelines/public/components/t_grid/body/translations.ts b/x-pack/plugins/timelines/public/components/t_grid/body/translations.ts deleted file mode 100644 index 33b16f130d2a4..0000000000000 --- a/x-pack/plugins/timelines/public/components/t_grid/body/translations.ts +++ /dev/null @@ -1,194 +0,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 { i18n } from '@kbn/i18n'; - -export const TGRID_BODY_ARIA_LABEL = i18n.translate('xpack.timelines.tgrid.body.ariaLabel', { - defaultMessage: 'Alerts', -}); - -export const NOTES_DISABLE_TOOLTIP = i18n.translate( - 'xpack.timelines.timeline.body.notes.disableEventTooltip', - { - defaultMessage: 'Notes may not be added here while editing a template timeline', - } -); - -export const COPY_TO_CLIPBOARD = i18n.translate( - 'xpack.timelines.timeline.body.copyToClipboardButtonLabel', - { - defaultMessage: 'Copy to Clipboard', - } -); - -export const INVESTIGATE = i18n.translate( - 'xpack.timelines.timeline.body.actions.investigateLabel', - { - defaultMessage: 'Investigate', - } -); - -export const PINNED_WITH_NOTES = i18n.translate( - 'xpack.timelines.timeline.body.pinning.pinnnedWithNotesTooltip', - { - defaultMessage: 'This event cannot be unpinned because it has notes', - } -); - -export const SORTED_ASCENDING = i18n.translate( - 'xpack.timelines.timeline.body.sort.sortedAscendingTooltip', - { - defaultMessage: 'Sorted ascending', - } -); - -export const SORTED_DESCENDING = i18n.translate( - 'xpack.timelines.timeline.body.sort.sortedDescendingTooltip', - { - defaultMessage: 'Sorted descending', - } -); - -export const DISABLE_PIN = i18n.translate( - 'xpack.timelines.timeline.body.pinning.disablePinnnedTooltip', - { - defaultMessage: 'This event may not be pinned while editing a template timeline', - } -); - -export const VIEW_DETAILS = i18n.translate( - 'xpack.timelines.timeline.body.actions.viewDetailsAriaLabel', - { - defaultMessage: 'View details', - } -); - -export const VIEW_SUMMARY = i18n.translate( - 'xpack.timelines.timeline.body.actions.viewSummaryLabel', - { - defaultMessage: 'View summary', - } -); - -export const VIEW_DETAILS_FOR_ROW = ({ - ariaRowindex, - columnValues, -}: { - ariaRowindex: number; - columnValues: string; -}) => - i18n.translate('xpack.timelines.timeline.body.actions.viewDetailsForRowAriaLabel', { - values: { ariaRowindex, columnValues }, - defaultMessage: - 'View details for the alert or event in row {ariaRowindex}, with columns {columnValues}', - }); - -export const EXPAND_EVENT = i18n.translate( - 'xpack.timelines.timeline.body.actions.expandEventTooltip', - { - defaultMessage: 'View details', - } -); - -export const COLLAPSE = i18n.translate('xpack.timelines.timeline.body.actions.collapseAriaLabel', { - defaultMessage: 'Collapse', -}); - -export const ACTION_INVESTIGATE_IN_RESOLVER = i18n.translate( - 'xpack.timelines.timeline.body.actions.investigateInResolverTooltip', - { - defaultMessage: 'Analyze event', - } -); - -export const ACTION_INVESTIGATE_IN_RESOLVER_FOR_ROW = ({ - ariaRowindex, - columnValues, -}: { - ariaRowindex: number; - columnValues: string; -}) => - i18n.translate('xpack.timelines.timeline.body.actions.investigateInResolverForRowAriaLabel', { - values: { ariaRowindex, columnValues }, - defaultMessage: 'Analyze the alert or event in row {ariaRowindex}, with columns {columnValues}', - }); - -export const SEND_ALERT_TO_TIMELINE_FOR_ROW = ({ - ariaRowindex, - columnValues, -}: { - ariaRowindex: number; - columnValues: string; -}) => - i18n.translate('xpack.timelines.timeline.body.actions.sendAlertToTimelineForRowAriaLabel', { - values: { ariaRowindex, columnValues }, - defaultMessage: 'Send the alert in row {ariaRowindex} to timeline, with columns {columnValues}', - }); - -export const ADD_NOTES_FOR_ROW = ({ - ariaRowindex, - columnValues, -}: { - ariaRowindex: number; - columnValues: string; -}) => - i18n.translate('xpack.timelines.timeline.body.actions.addNotesForRowAriaLabel', { - values: { ariaRowindex, columnValues }, - defaultMessage: - 'Add notes for the event in row {ariaRowindex} to timeline, with columns {columnValues}', - }); - -export const TIMELINE_TOGGLE_BUTTON_ARIA_LABEL = ({ - isOpen, - title, -}: { - isOpen: boolean; - title: string; -}) => - i18n.translate('xpack.timelines.timeline.properties.timelineToggleButtonAriaLabel', { - values: { isOpen, title }, - defaultMessage: '{isOpen, select, false {Open} true {Close} other {Toggle}} timeline {title}', - }); - -export const ATTACH_ALERT_TO_CASE_FOR_ROW = ({ - ariaRowindex, - columnValues, -}: { - ariaRowindex: number; - columnValues: string; -}) => - i18n.translate('xpack.timelines.timeline.body.actions.attachAlertToCaseForRowAriaLabel', { - values: { ariaRowindex, columnValues }, - defaultMessage: - 'Attach the alert or event in row {ariaRowindex} to a case, with columns {columnValues}', - }); - -export const MORE_ACTIONS_FOR_ROW = ({ - ariaRowindex, - columnValues, -}: { - ariaRowindex: number; - columnValues: string; -}) => - i18n.translate('xpack.timelines.timeline.body.actions.moreActionsForRowAriaLabel', { - values: { ariaRowindex, columnValues }, - defaultMessage: - 'Select more actions for the alert or event in row {ariaRowindex}, with columns {columnValues}', - }); - -export const INVESTIGATE_IN_RESOLVER_DISABLED = i18n.translate( - 'xpack.timelines.timeline.body.actions.investigateInResolverDisabledTooltip', - { - defaultMessage: 'This event cannot be analyzed since it has incompatible field mappings', - } -); - -export const ALERTS_UNIT = (totalCount: number) => - i18n.translate('xpack.timelines.timeline.alertsUnit', { - values: { totalCount }, - defaultMessage: `{totalCount, plural, =1 {alert} other {alerts}}`, - }); diff --git a/x-pack/plugins/timelines/public/components/t_grid/event_rendered_view/index.test.tsx b/x-pack/plugins/timelines/public/components/t_grid/event_rendered_view/index.test.tsx deleted file mode 100644 index 2357b72e63970..0000000000000 --- a/x-pack/plugins/timelines/public/components/t_grid/event_rendered_view/index.test.tsx +++ /dev/null @@ -1,153 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { render, screen } from '@testing-library/react'; -import React from 'react'; -import { eventRenderedProps, TestProviders } from '../../../mock'; -import { EventRenderedView } from '.'; -import { RowRendererId } from '../../../../common/types'; - -describe('event_rendered_view', () => { - beforeEach(() => jest.clearAllMocks()); - - test('it renders the timestamp correctly', () => { - render( - - - - ); - expect(screen.queryAllByTestId('moment-date')[0].textContent).toEqual( - '2018-11-05T14:03:25-05:00' - ); - }); - - describe('getRowRenderer', () => { - const props = { - ...eventRenderedProps, - rowRenderers: [ - { - id: RowRendererId.auditd_file, - isInstance: jest.fn().mockReturnValue(false), - renderRow: jest.fn(), - }, - { - id: RowRendererId.netflow, - isInstance: jest.fn().mockReturnValue(true), // matches any data - renderRow: jest.fn(), - }, - { - id: RowRendererId.registry, - isInstance: jest.fn().mockReturnValue(true), // also matches any data - renderRow: jest.fn(), - }, - ], - }; - - test(`it (only) renders the first matching renderer when 'getRowRenderer' is NOT provided as a prop`, () => { - render( - - - - ); - - expect(props.rowRenderers[0].renderRow).not.toBeCalled(); // did not match - expect(props.rowRenderers[1].renderRow).toBeCalled(); // the first matching renderer - expect(props.rowRenderers[2].renderRow).not.toBeCalled(); // also matches, but should not be rendered - }); - - test(`it (only) renders the renderer returned by 'getRowRenderer' when it's provided as a prop`, () => { - const withGetRowRenderer = { - ...props, - getRowRenderer: jest.fn().mockImplementation(() => props.rowRenderers[2]), // only match the last renderer - }; - - render( - - - - ); - - expect(props.rowRenderers[0].renderRow).not.toBeCalled(); - expect(props.rowRenderers[1].renderRow).not.toBeCalled(); - expect(props.rowRenderers[2].renderRow).toBeCalled(); - }); - - test(`it does NOT render the plain text version of the reason when a renderer is found`, () => { - render( - - - - ); - - expect(screen.queryByTestId('plain-text-reason')).not.toBeInTheDocument(); - }); - - test(`it renders the plain text reason when no row renderer was found, but the data contains an 'ecs.signal.reason'`, () => { - const reason = 'why not?'; - const noRendererFound = { - ...props, - events: [ - ...props.events, - { - _id: 'abcd', - data: [{ field: '@timestamp', value: ['2018-11-05T19:03:25.937Z'] }], - ecs: { - _id: 'abcd', - timestamp: '2018-11-05T19:03:25.937Z', - signal: { - reason, - }, - }, - }, - ], - getRowRenderer: jest.fn().mockImplementation(() => null), // no renderer was found - }; - - render( - - - - ); - - expect(screen.getAllByTestId('plain-text-reason')[0]).toHaveTextContent('why not?'); - }); - - test(`it renders the plain text reason when no row renderer was found, but the data contains an 'ecs.kibana.alert.reason'`, () => { - const reason = 'do you really need a reason?'; - const noRendererFound = { - ...props, - events: [ - ...props.events, - { - _id: 'abcd', - data: [], - ecs: { - _id: 'abcd', - timestamp: '2018-11-05T19:03:25.937Z', - kibana: { - alert: { - reason, - }, - }, - }, - }, - ], - getRowRenderer: jest.fn().mockImplementation(() => null), // no renderer was found - }; - - render( - - - - ); - - expect(screen.getAllByTestId('plain-text-reason')[0]).toHaveTextContent( - 'do you really need a reason?' - ); - }); - }); -}); diff --git a/x-pack/plugins/timelines/public/components/t_grid/event_rendered_view/index.tsx b/x-pack/plugins/timelines/public/components/t_grid/event_rendered_view/index.tsx deleted file mode 100644 index 210caab320203..0000000000000 --- a/x-pack/plugins/timelines/public/components/t_grid/event_rendered_view/index.tsx +++ /dev/null @@ -1,275 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ -import { - CriteriaWithPagination, - EuiBasicTable, - EuiBasicTableProps, - EuiDataGridCellValueElementProps, - EuiDataGridControlColumn, - EuiFlexGroup, - EuiFlexItem, -} from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { ALERT_REASON, ALERT_RULE_NAME, ALERT_RULE_UUID } from '@kbn/rule-data-utils'; -import { get } from 'lodash'; -import moment from 'moment'; -import React, { ComponentType, useCallback, useMemo } from 'react'; -import styled from 'styled-components'; - -import { useUiSetting } from '@kbn/kibana-react-plugin/public'; - -import { Ecs } from '../../../../common/ecs'; -import type { TimelineItem } from '../../../../common/search_strategy'; -import type { RowRenderer } from '../../../../common/types'; -import { RuleName } from '../../rule_name'; -import { isEventBuildingBlockType } from '../body/helpers'; - -const EventRenderedFlexItem = styled(EuiFlexItem)` - div:first-child { - padding-left: 0px; - div { - margin: 0px; - } - } -`; - -const ActionsContainer = styled.div` - display: flex; - align-items: center; - div div:first-child div.siemEventsTable__tdContent { - margin-left: ${({ theme }) => theme.eui.euiSizeM}; - } -`; - -// Fix typing issue with EuiBasicTable and styled -type BasicTableType = ComponentType>; - -const StyledEuiBasicTable = styled(EuiBasicTable as BasicTableType)` - padding-top: ${({ theme }) => theme.eui.euiSizeM}; - .EventRenderedView__buildingBlock { - background: ${({ theme }) => theme.eui.euiColorHighlight}; - } - - & > div:last-child { - height: 72px; - } - - & tr:nth-child(even) { - background-color: ${({ theme }) => theme.eui.euiColorLightestShade}; - } - - & tr:nth-child(odd) { - background-color: ${({ theme }) => theme.eui.euiColorEmptyShade}; - } -`; - -export interface EventRenderedViewProps { - alertToolbar: React.ReactNode; - appId: string; - events: TimelineItem[]; - getRowRenderer?: ({ - data, - rowRenderers, - }: { - data: Ecs; - rowRenderers: RowRenderer[]; - }) => RowRenderer | null; - leadingControlColumns: EuiDataGridControlColumn[]; - onChangePage: (newActivePage: number) => void; - onChangeItemsPerPage: (newItemsPerPage: number) => void; - pageIndex: number; - pageSize: number; - pageSizeOptions: number[]; - rowRenderers: RowRenderer[]; - timelineId: string; - totalItemCount: number; -} -const PreferenceFormattedDateComponent = ({ value }: { value: Date }) => { - const tz = useUiSetting('dateFormat:tz'); - const dateFormat = useUiSetting('dateFormat'); - const zone: string = moment.tz.zone(tz)?.name ?? moment.tz.guess(); - - return {moment.tz(value, zone).format(dateFormat)}; -}; -export const PreferenceFormattedDate = React.memo(PreferenceFormattedDateComponent); - -const EventRenderedViewComponent = ({ - alertToolbar, - appId, - events, - getRowRenderer, - leadingControlColumns, - onChangePage, - onChangeItemsPerPage, - pageIndex, - pageSize, - pageSizeOptions, - rowRenderers, - timelineId, - totalItemCount, -}: EventRenderedViewProps) => { - const ActionTitle = useMemo( - () => ( - - {leadingControlColumns.map((action) => { - const ActionHeader = action.headerCellRender; - return ( - - - - ); - })} - - ), - [leadingControlColumns] - ); - - const columns = useMemo( - () => [ - { - field: 'actions', - name: ActionTitle, - truncateText: false, - mobileOptions: { show: true }, - render: (name: unknown, item: unknown) => { - const alertId = get(item, '_id'); - const rowIndex = events.findIndex((evt) => evt._id === alertId); - return ( - - {leadingControlColumns.length > 0 - ? leadingControlColumns.map((action) => { - const getActions = action.rowCellRender as ( - props: Omit - ) => React.ReactNode; - return getActions({ - columnId: 'actions', - isDetails: false, - isExpandable: false, - isExpanded: false, - rowIndex, - setCellProps: () => null, - }); - }) - : null} - - ); - }, - // TODO: derive this from ACTION_BUTTON_COUNT as other columns are done - width: '184px', - }, - { - field: 'ecs.timestamp', - name: i18n.translate('xpack.timelines.alerts.EventRenderedView.timestamp.column', { - defaultMessage: 'Timestamp', - }), - truncateText: false, - mobileOptions: { show: true }, - render: (name: unknown, item: TimelineItem) => { - const timestamp = get(item, `ecs.timestamp`); - return ; - }, - }, - { - field: `ecs.${ALERT_RULE_NAME}`, - name: i18n.translate('xpack.timelines.alerts.EventRenderedView.rule.column', { - defaultMessage: 'Rule', - }), - truncateText: false, - mobileOptions: { show: true }, - render: (name: unknown, item: TimelineItem) => { - const ruleName = get(item, `ecs.signal.rule.name`) ?? get(item, `ecs.${ALERT_RULE_NAME}`); - const ruleId = get(item, `ecs.signal.rule.id`) ?? get(item, `ecs.${ALERT_RULE_UUID}`); - return ; - }, - }, - { - field: 'eventSummary', - name: i18n.translate('xpack.timelines.alerts.EventRenderedView.eventSummary.column', { - defaultMessage: 'Event Summary', - }), - truncateText: false, - mobileOptions: { show: true }, - render: (name: unknown, item: TimelineItem) => { - const ecsData = get(item, 'ecs'); - const reason = get(item, `ecs.signal.reason`) ?? get(item, `ecs.${ALERT_REASON}`); - const rowRenderer = - getRowRenderer != null - ? getRowRenderer({ data: ecsData, rowRenderers }) - : rowRenderers.find((x) => x.isInstance(ecsData)) ?? null; - - return ( - - {rowRenderer != null ? ( - -
    - {rowRenderer.renderRow({ - data: ecsData, - isDraggable: false, - scopeId: timelineId, - })} -
    -
    - ) : ( - <> - {reason && {reason}} - - )} -
    - ); - }, - width: '60%', - }, - ], - [ActionTitle, events, leadingControlColumns, appId, getRowRenderer, rowRenderers, timelineId] - ); - - const handleTableChange = useCallback( - (pageChange: CriteriaWithPagination) => { - if (pageChange.page.index !== pageIndex) { - onChangePage(pageChange.page.index); - } - if (pageChange.page.size !== pageSize) { - onChangeItemsPerPage(pageChange.page.size); - } - }, - [onChangePage, pageIndex, pageSize, onChangeItemsPerPage] - ); - - const pagination = useMemo( - () => ({ - pageIndex, - pageSize, - totalItemCount, - pageSizeOptions, - showPerPageOptions: true, - }), - [pageIndex, pageSize, pageSizeOptions, totalItemCount] - ); - - return ( - <> - {alertToolbar} - - isEventBuildingBlockType(ecs) - ? { - className: `EventRenderedView__buildingBlock`, - } - : {} - } - /> - - ); -}; - -export const EventRenderedView = React.memo(EventRenderedViewComponent); diff --git a/x-pack/plugins/timelines/public/components/t_grid/event_rendered_view/selector/index.tsx b/x-pack/plugins/timelines/public/components/t_grid/event_rendered_view/selector/index.tsx deleted file mode 100644 index f5d336c534fce..0000000000000 --- a/x-pack/plugins/timelines/public/components/t_grid/event_rendered_view/selector/index.tsx +++ /dev/null @@ -1,167 +0,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 { - EuiButtonEmpty, - EuiPopover, - EuiSelectable, - EuiSelectableOption, - EuiTitle, - EuiTextColor, -} from '@elastic/eui'; -import { Storage } from '@kbn/kibana-utils-plugin/public'; -import { i18n } from '@kbn/i18n'; -import React, { useCallback, useMemo, useState } from 'react'; -import styled from 'styled-components'; - -import { ALERTS_TABLE_VIEW_SELECTION_KEY } from '../../helpers'; - -const storage = new Storage(localStorage); - -export type ViewSelection = 'gridView' | 'eventRenderedView'; - -const ContainerEuiSelectable = styled.div` - width: 300px; - .euiSelectableListItem__text { - white-space: pre-wrap !important; - line-height: normal; - } -`; - -const gridView = i18n.translate('xpack.timelines.alerts.summaryView.gridView.label', { - defaultMessage: 'Grid view', -}); - -const eventRenderedView = i18n.translate( - 'xpack.timelines.alerts.summaryView.eventRendererView.label', - { - defaultMessage: 'Event rendered view', - } -); - -interface SummaryViewSelectorProps { - onViewChange: (viewSelection: ViewSelection) => void; - viewSelected: ViewSelection; -} - -const SummaryViewSelectorComponent = ({ viewSelected, onViewChange }: SummaryViewSelectorProps) => { - const [isPopoverOpen, setIsPopoverOpen] = useState(false); - - const onButtonClick = useCallback(() => setIsPopoverOpen((currentVal) => !currentVal), []); - const closePopover = useCallback(() => setIsPopoverOpen(false), []); - const onChangeSelectable = useCallback( - (opts: EuiSelectableOption[]) => { - const selected = opts.filter((i) => i.checked === 'on'); - storage.set(ALERTS_TABLE_VIEW_SELECTION_KEY, selected[0]?.key ?? 'gridView'); - - if (selected.length > 0) { - onViewChange((selected[0]?.key ?? 'gridView') as ViewSelection); - } - setIsPopoverOpen(false); - }, - [onViewChange] - ); - - const button = useMemo( - () => ( - - {viewSelected === 'gridView' ? gridView : eventRenderedView} - - ), - [onButtonClick, viewSelected] - ); - - const options = useMemo( - () => [ - { - label: gridView, - key: 'gridView', - checked: (viewSelected === 'gridView' ? 'on' : undefined) as EuiSelectableOption['checked'], - meta: [ - { - text: i18n.translate('xpack.timelines.alerts.summaryView.options.default.description', { - defaultMessage: - 'View as tabular data with the ability to group and sort by specific fields', - }), - }, - ], - }, - { - label: eventRenderedView, - key: 'eventRenderedView', - checked: (viewSelected === 'eventRenderedView' - ? 'on' - : undefined) as EuiSelectableOption['checked'], - meta: [ - { - text: i18n.translate( - 'xpack.timelines.alerts.summaryView.options.summaryView.description', - { - defaultMessage: 'View a rendering of the event flow for each alert', - } - ), - }, - ], - }, - ], - [viewSelected] - ); - - const renderOption = useCallback((option) => { - return ( - <> - -
    {option.label}
    -
    - - {option.meta[0].text} - - - ); - }, []); - - const listProps = useMemo( - () => ({ - rowHeight: 80, - showIcons: true, - }), - [] - ); - - return ( - - - - {(list) => list} - - - - ); -}; - -export const SummaryViewSelector = React.memo(SummaryViewSelectorComponent); diff --git a/x-pack/plugins/timelines/public/components/t_grid/footer/index.test.tsx b/x-pack/plugins/timelines/public/components/t_grid/footer/index.test.tsx deleted file mode 100644 index 191ae4210f918..0000000000000 --- a/x-pack/plugins/timelines/public/components/t_grid/footer/index.test.tsx +++ /dev/null @@ -1,250 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { mount, shallow } from 'enzyme'; -import React from 'react'; - -import { TestProviders } from '../../../mock/test_providers'; - -import { FooterComponent, PagingControlComponent } from '.'; - -describe('Footer Timeline Component', () => { - const loadMore = jest.fn(); - const serverSideEventCount = 15546; - const itemsCount = 2; - - describe('rendering', () => { - test('it renders the default timeline footer', () => { - const wrapper = mount( - - - - ); - - expect(wrapper.find('FooterContainer').exists()).toBeTruthy(); - }); - - test('it renders the loading panel at the beginning ', () => { - const wrapper = mount( - - - - ); - - expect(wrapper.find('[data-test-subj="LoadingPanelTimeline"]').exists()).toBeTruthy(); - }); - - test('it renders the loadMore button if need to fetch more', () => { - const wrapper = mount( - - - - ); - - expect(wrapper.find('[data-test-subj="timeline-pagination"]').exists()).toBeTruthy(); - }); - - test('it renders the Loading... in the more load button when fetching new data', () => { - const wrapper = shallow( - - ); - - const loadButton = wrapper.text(); - expect(wrapper.find('[data-test-subj="LoadingPanelTimeline"]').exists()).toBeFalsy(); - expect(loadButton).toContain('Loading...'); - }); - - test('it renders the Pagination in the more load button when fetching new data', () => { - const wrapper = shallow( - - ); - - expect(wrapper.find('[data-test-subj="timeline-pagination"]').exists()).toBeTruthy(); - }); - - test('it does NOT render the loadMore button because there is nothing else to fetch', () => { - const wrapper = mount( - - - - ); - - expect(wrapper.find('[data-test-subj="timeline-pagination"]').exists()).toBeFalsy(); - }); - - test('it render popover to select new itemsPerPage in timeline', () => { - const wrapper = mount( - - - - ); - - wrapper.find('[data-test-subj="timelineSizeRowPopover"] button').first().simulate('click'); - expect(wrapper.find('[data-test-subj="timelinePickSizeRow"]').exists()).toBeTruthy(); - }); - }); - - describe('Events', () => { - test('should call loadmore when clicking on the button load more', () => { - const wrapper = mount( - - - - ); - - wrapper.find('button[data-test-subj="pagination-button-next"]').first().simulate('click'); - expect(loadMore).toBeCalled(); - }); - - // test('Should call onChangeItemsPerPage when you pick a new limit', () => { - // const wrapper = mount( - // - // - // - // ); - - // wrapper.find('[data-test-subj="timelineSizeRowPopover"] button').first().simulate('click'); - // wrapper.update(); - // wrapper.find('[data-test-subj="timelinePickSizeRow"] button').first().simulate('click'); - // expect(onChangeItemsPerPage).toBeCalled(); - // }); - - test('it does render the auto-refresh message instead of load more button when stream live is on', () => { - const wrapper = mount( - - - - ); - - expect(wrapper.find('[data-test-subj="paging-control"]').exists()).toBeFalsy(); - expect(wrapper.find('[data-test-subj="is-live-on-message"]').exists()).toBeTruthy(); - }); - - test('it does render the load more button when stream live is off', () => { - const wrapper = mount( - - - - ); - - expect(wrapper.find('[data-test-subj="paging-control"]').exists()).toBeTruthy(); - expect(wrapper.find('[data-test-subj="is-live-on-message"]').exists()).toBeFalsy(); - }); - }); -}); diff --git a/x-pack/plugins/timelines/public/components/t_grid/footer/index.tsx b/x-pack/plugins/timelines/public/components/t_grid/footer/index.tsx deleted file mode 100644 index 2e34972f5e7d6..0000000000000 --- a/x-pack/plugins/timelines/public/components/t_grid/footer/index.tsx +++ /dev/null @@ -1,368 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { - EuiButtonEmpty, - EuiContextMenuItem, - EuiContextMenuPanel, - EuiFlexGroup, - EuiFlexItem, - EuiIconTip, - EuiPopover, - EuiText, - EuiPopoverProps, - EuiPagination, -} from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n-react'; -import React, { FC, useCallback, useEffect, useState, useMemo } from 'react'; -import styled from 'styled-components'; -import { useDispatch } from 'react-redux'; - -import { EVENTS_COUNT_BUTTON_CLASS_NAME } from '../helpers'; - -import * as i18n from './translations'; -import { OnChangePage } from '../types'; -import { tGridActions, tGridSelectors } from '../../../store/t_grid'; -import { useDeepEqualSelector } from '../../../hooks/use_selector'; -import { LoadingPanel } from '../../loading'; - -export const isCompactFooter = (width: number): boolean => width < 600; - -const FixedWidthLastUpdated = styled.div<{ compact?: boolean }>` - width: ${({ compact }) => (!compact ? 200 : 25)}px; - overflow: hidden; - text-align: end; -`; - -FixedWidthLastUpdated.displayName = 'FixedWidthLastUpdated'; - -interface HeightProp { - height: number; -} - -const FooterContainer = styled(EuiFlexGroup).attrs(({ height }) => ({ - style: { - height: `${height}px`, - }, -}))` - flex: 0 0 auto; -`; - -FooterContainer.displayName = 'FooterContainer'; - -const FooterFlexGroup = styled(EuiFlexGroup)` - height: 35px; - width: 100%; -`; - -FooterFlexGroup.displayName = 'FooterFlexGroup'; - -const LoadingPanelContainer = styled.div` - padding-top: 3px; -`; - -LoadingPanelContainer.displayName = 'LoadingPanelContainer'; - -const PopoverRowItems = styled(EuiPopover as unknown as FC)< - EuiPopoverProps & { - className?: string; - id?: string; - } ->` - .euiButtonEmpty__content { - padding: 0px 0px; - } -`; - -PopoverRowItems.displayName = 'PopoverRowItems'; - -export const ServerSideEventCount = styled.div` - margin: 0 5px 0 5px; -`; - -ServerSideEventCount.displayName = 'ServerSideEventCount'; - -/** The height of the footer, exported for use in height calculations */ -export const footerHeight = 40; // px - -/** Displays the server-side count of events */ -export const EventsCountComponent = ({ - closePopover, - isOpen, - items, - itemsCount, - itemsPerPage, - onClick, - serverSideEventCount, -}: { - closePopover: () => void; - isOpen: boolean; - items: React.ReactElement[]; - itemsCount: number; - itemsPerPage: number; - onClick: () => void; - serverSideEventCount: number; -}) => { - const button = useMemo( - () => ( - - {i18n.ROWS_PER_PAGE(itemsPerPage)} - - ), - [itemsPerPage, onClick] - ); - - return ( - - - - ); -}; - -EventsCountComponent.displayName = 'EventsCountComponent'; - -export const EventsCount = React.memo(EventsCountComponent); - -EventsCount.displayName = 'EventsCount'; - -interface PagingControlProps { - activePage: number; - isLoading: boolean; - onPageClick: OnChangePage; - totalCount: number; - totalPages: number; -} - -const TimelinePaginationContainer = styled.div<{ hideLastPage: boolean }>` - ul.euiPagination__list { - li.euiPagination__item:last-child { - ${({ hideLastPage }) => `${hideLastPage ? 'display:none' : ''}`}; - } - } -`; - -export const PagingControlComponent: React.FC = ({ - activePage, - isLoading, - onPageClick, - totalCount, - totalPages, -}) => { - if (isLoading) { - return <>{`${i18n.LOADING}...`}; - } - - if (!totalPages) { - return null; - } - - return ( - 9999}> - - - ); -}; - -PagingControlComponent.displayName = 'PagingControlComponent'; - -export const PagingControl = React.memo(PagingControlComponent); - -PagingControl.displayName = 'PagingControl'; -interface FooterProps { - activePage: number; - height: number; - id: string; - isLive: boolean; - isLoading: boolean; - itemsCount: number; - itemsPerPage: number; - itemsPerPageOptions: number[]; - onChangePage: OnChangePage; - totalCount: number; -} - -/** Renders a loading indicator and paging controls */ -export const FooterComponent = ({ - activePage, - height, - id, - isLive, - isLoading, - itemsCount, - itemsPerPage, - itemsPerPageOptions, - onChangePage, - totalCount, -}: FooterProps) => { - const dispatch = useDispatch(); - const [isPopoverOpen, setIsPopoverOpen] = useState(false); - const [paginationLoading, setPaginationLoading] = useState(false); - - const getManageDataTable = useMemo(() => tGridSelectors.getManageDataTableById(), []); - const { loadingText } = useDeepEqualSelector((state) => getManageDataTable(state, id)); - - const handleChangePageClick = useCallback( - (nextPage: number) => { - setPaginationLoading(true); - onChangePage(nextPage); - }, - [onChangePage] - ); - - const onButtonClick = useCallback( - () => setIsPopoverOpen(!isPopoverOpen), - [isPopoverOpen, setIsPopoverOpen] - ); - - const closePopover = useCallback(() => setIsPopoverOpen(false), [setIsPopoverOpen]); - - const onChangeItemsPerPage = useCallback( - (itemsChangedPerPage) => - dispatch(tGridActions.updateItemsPerPage({ id, itemsPerPage: itemsChangedPerPage })), - [dispatch, id] - ); - - const rowItems = useMemo( - () => - itemsPerPageOptions && - itemsPerPageOptions.map((item) => ( - { - closePopover(); - onChangeItemsPerPage(item); - }} - > - {`${item} ${i18n.ROWS}`} - - )), - [closePopover, itemsPerPage, itemsPerPageOptions, onChangeItemsPerPage] - ); - - const totalPages = useMemo( - () => Math.ceil(totalCount / itemsPerPage), - [itemsPerPage, totalCount] - ); - - useEffect(() => { - if (paginationLoading && !isLoading) { - setPaginationLoading(false); - } - }, [isLoading, paginationLoading]); - - if (isLoading && !paginationLoading) { - return ( - - - - ); - } - - return ( - - - - - - - - - - {isLive ? ( - - - {i18n.AUTO_REFRESH_ACTIVE}{' '} - - } - type="iInCircle" - /> - - - ) : ( - - )} - - - - ); -}; - -FooterComponent.displayName = 'FooterComponent'; - -export const Footer = React.memo(FooterComponent); - -Footer.displayName = 'Footer'; diff --git a/x-pack/plugins/timelines/public/components/t_grid/footer/translations.ts b/x-pack/plugins/timelines/public/components/t_grid/footer/translations.ts deleted file mode 100644 index c2417f3453065..0000000000000 --- a/x-pack/plugins/timelines/public/components/t_grid/footer/translations.ts +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { i18n } from '@kbn/i18n'; - -export const LOADING_TIMELINE_DATA = i18n.translate('xpack.timelines.footer.loadingTimelineData', { - defaultMessage: 'Loading Timeline data', -}); - -export const EVENTS = i18n.translate('xpack.timelines.footer.events', { - defaultMessage: 'Events', -}); - -export const OF = i18n.translate('xpack.timelines.footer.of', { - defaultMessage: 'of', -}); - -export const ROWS = i18n.translate('xpack.timelines.footer.rows', { - defaultMessage: 'rows', -}); - -export const LOADING = i18n.translate('xpack.timelines.footer.loadingLabel', { - defaultMessage: 'Loading', -}); - -export const ROWS_PER_PAGE = (rowsPerPage: number) => - i18n.translate('xpack.timelines.footer.rowsPerPageLabel', { - values: { rowsPerPage }, - defaultMessage: `Rows per page: {rowsPerPage}`, - }); - -export const TOTAL_COUNT_OF_EVENTS = i18n.translate('xpack.timelines.footer.totalCountOfEvents', { - defaultMessage: 'events', -}); - -export const AUTO_REFRESH_ACTIVE = i18n.translate( - 'xpack.timelines.footer.autoRefreshActiveDescription', - { - defaultMessage: 'Auto-Refresh Active', - } -); diff --git a/x-pack/plugins/timelines/public/components/t_grid/helpers.test.tsx b/x-pack/plugins/timelines/public/components/t_grid/helpers.test.tsx deleted file mode 100644 index f68d67339d19a..0000000000000 --- a/x-pack/plugins/timelines/public/components/t_grid/helpers.test.tsx +++ /dev/null @@ -1,1028 +0,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 { cloneDeep } from 'lodash/fp'; -import { Filter, EsQueryConfig, FilterStateStore } from '@kbn/es-query'; - -import { - DataProviderType, - EXISTS_OPERATOR, - IS_ONE_OF_OPERATOR, - IS_OPERATOR, -} from '../../../common/types/timeline'; -import { - buildExistsQueryMatch, - buildGlobalQuery, - buildIsOneOfQueryMatch, - buildIsQueryMatch, - combineQueries, - getDefaultViewSelection, - isSelectableView, - isStringOrNumberArray, - isViewSelection, - resolverIsShowing, -} from './helpers'; -import { mockBrowserFields, mockDataProviders, mockIndexPattern } from '../../mock'; -import { TableId } from '../../types'; - -const cleanUpKqlQuery = (str: string) => str.replace(/\n/g, '').replace(/\s\s+/g, ' '); - -describe('Build KQL Query', () => { - test('Build KQL query with one data provider', () => { - const dataProviders = cloneDeep(mockDataProviders.slice(0, 1)); - const kqlQuery = buildGlobalQuery(dataProviders, mockBrowserFields); - expect(cleanUpKqlQuery(kqlQuery)).toEqual('name : "Provider 1"'); - }); - - test('Build KQL query with one template data provider', () => { - const dataProviders = cloneDeep(mockDataProviders.slice(0, 1)); - dataProviders[0].type = DataProviderType.template; - const kqlQuery = buildGlobalQuery(dataProviders, mockBrowserFields); - expect(cleanUpKqlQuery(kqlQuery)).toEqual('name :*'); - }); - - test('Build KQL query with one disabled data provider', () => { - const dataProviders = cloneDeep(mockDataProviders.slice(0, 1)); - dataProviders[0].enabled = false; - const kqlQuery = buildGlobalQuery(dataProviders, mockBrowserFields); - expect(cleanUpKqlQuery(kqlQuery)).toEqual(''); - }); - - test('Build KQL query with one data provider as timestamp (string input)', () => { - const dataProviders = cloneDeep(mockDataProviders.slice(0, 1)); - dataProviders[0].queryMatch.field = '@timestamp'; - dataProviders[0].queryMatch.value = '2018-03-23T23:36:23.232Z'; - const kqlQuery = buildGlobalQuery(dataProviders, mockBrowserFields); - expect(cleanUpKqlQuery(kqlQuery)).toEqual('@timestamp: 1521848183232'); - }); - - test('Buld KQL query with one data provider as timestamp (numeric input)', () => { - const dataProviders = cloneDeep(mockDataProviders.slice(0, 1)); - dataProviders[0].queryMatch.field = '@timestamp'; - dataProviders[0].queryMatch.value = 1521848183232; - const kqlQuery = buildGlobalQuery(dataProviders, mockBrowserFields); - expect(cleanUpKqlQuery(kqlQuery)).toEqual('@timestamp: 1521848183232'); - }); - - test('Buld KQL query with one data provider as timestamp (numeric input as string)', () => { - const dataProviders = cloneDeep(mockDataProviders.slice(0, 1)); - dataProviders[0].queryMatch.field = '@timestamp'; - dataProviders[0].queryMatch.value = '1521848183232'; - const kqlQuery = buildGlobalQuery(dataProviders, mockBrowserFields); - expect(cleanUpKqlQuery(kqlQuery)).toEqual('@timestamp: 1521848183232'); - }); - - test('Build KQL query with one data provider as date type (string input)', () => { - const dataProviders = cloneDeep(mockDataProviders.slice(0, 1)); - dataProviders[0].queryMatch.field = 'event.end'; - dataProviders[0].queryMatch.value = '2018-03-23T23:36:23.232Z'; - const kqlQuery = buildGlobalQuery(dataProviders, mockBrowserFields); - expect(cleanUpKqlQuery(kqlQuery)).toEqual('event.end: 1521848183232'); - }); - - test('Buld KQL query with one data provider as date type (numeric input)', () => { - const dataProviders = cloneDeep(mockDataProviders.slice(0, 1)); - dataProviders[0].queryMatch.field = 'event.end'; - dataProviders[0].queryMatch.value = 1521848183232; - const kqlQuery = buildGlobalQuery(dataProviders, mockBrowserFields); - expect(cleanUpKqlQuery(kqlQuery)).toEqual('event.end: 1521848183232'); - }); - - test('Buld KQL query with one data provider as date type (numeric input as string)', () => { - const dataProviders = cloneDeep(mockDataProviders.slice(0, 1)); - dataProviders[0].queryMatch.field = 'event.end'; - dataProviders[0].queryMatch.value = '1521848183232'; - const kqlQuery = buildGlobalQuery(dataProviders, mockBrowserFields); - expect(cleanUpKqlQuery(kqlQuery)).toEqual('event.end: 1521848183232'); - }); - - test('Build KQL query with two data provider', () => { - const dataProviders = cloneDeep(mockDataProviders.slice(0, 2)); - const kqlQuery = buildGlobalQuery(dataProviders, mockBrowserFields); - expect(cleanUpKqlQuery(kqlQuery)).toEqual('(name : "Provider 1") or (name : "Provider 2")'); - }); - - test('Build KQL query with two data provider and first is disabled', () => { - const dataProviders = cloneDeep(mockDataProviders.slice(0, 2)); - dataProviders[0].enabled = false; - const kqlQuery = buildGlobalQuery(dataProviders, mockBrowserFields); - expect(cleanUpKqlQuery(kqlQuery)).toEqual('name : "Provider 2"'); - }); - - test('Build KQL query with two data provider and second is disabled', () => { - const dataProviders = cloneDeep(mockDataProviders.slice(0, 2)); - dataProviders[1].enabled = false; - const kqlQuery = buildGlobalQuery(dataProviders, mockBrowserFields); - expect(cleanUpKqlQuery(kqlQuery)).toEqual('name : "Provider 1"'); - }); - - test('Build KQL query with two data provider (first is template)', () => { - const dataProviders = cloneDeep(mockDataProviders.slice(0, 2)); - dataProviders[0].type = DataProviderType.template; - const kqlQuery = buildGlobalQuery(dataProviders, mockBrowserFields); - expect(cleanUpKqlQuery(kqlQuery)).toEqual('(name :*) or (name : "Provider 2")'); - }); - - test('Build KQL query with two data provider (second is template)', () => { - const dataProviders = cloneDeep(mockDataProviders.slice(0, 2)); - dataProviders[1].type = DataProviderType.template; - const kqlQuery = buildGlobalQuery(dataProviders, mockBrowserFields); - expect(cleanUpKqlQuery(kqlQuery)).toEqual('(name : "Provider 1") or (name :*)'); - }); - - test('Build KQL query with one data provider and one and', () => { - const dataProviders = cloneDeep(mockDataProviders.slice(0, 1)); - dataProviders[0].and = cloneDeep(mockDataProviders.slice(1, 2)); - const kqlQuery = buildGlobalQuery(dataProviders, mockBrowserFields); - expect(cleanUpKqlQuery(kqlQuery)).toEqual('name : "Provider 1" and name : "Provider 2"'); - }); - - test('Build KQL query with one disabled data provider and one and', () => { - const dataProviders = cloneDeep(mockDataProviders.slice(0, 1)); - dataProviders[0].enabled = false; - dataProviders[0].and = cloneDeep(mockDataProviders.slice(1, 2)); - const kqlQuery = buildGlobalQuery(dataProviders, mockBrowserFields); - expect(cleanUpKqlQuery(kqlQuery)).toEqual('name : "Provider 2"'); - }); - - test('Build KQL query with one data provider and one and as timestamp (string input)', () => { - const dataProviders = cloneDeep(mockDataProviders.slice(0, 1)); - dataProviders[0].and = cloneDeep(mockDataProviders.slice(1, 2)); - dataProviders[0].and[0].queryMatch.field = '@timestamp'; - dataProviders[0].and[0].queryMatch.value = '2018-03-23T23:36:23.232Z'; - const kqlQuery = buildGlobalQuery(dataProviders, mockBrowserFields); - expect(cleanUpKqlQuery(kqlQuery)).toEqual('name : "Provider 1" and @timestamp: 1521848183232'); - }); - - test('Build KQL query with one data provider and one and as timestamp (numeric input)', () => { - const dataProviders = cloneDeep(mockDataProviders.slice(0, 1)); - dataProviders[0].and = cloneDeep(mockDataProviders.slice(1, 2)); - dataProviders[0].and[0].queryMatch.field = '@timestamp'; - dataProviders[0].and[0].queryMatch.value = 1521848183232; - const kqlQuery = buildGlobalQuery(dataProviders, mockBrowserFields); - expect(cleanUpKqlQuery(kqlQuery)).toEqual('name : "Provider 1" and @timestamp: 1521848183232'); - }); - - test('Build KQL query with one data provider and one and as date type (string input)', () => { - const dataProviders = cloneDeep(mockDataProviders.slice(0, 1)); - dataProviders[0].and = cloneDeep(mockDataProviders.slice(1, 2)); - dataProviders[0].and[0].queryMatch.field = 'event.end'; - dataProviders[0].and[0].queryMatch.value = '2018-03-23T23:36:23.232Z'; - const kqlQuery = buildGlobalQuery(dataProviders, mockBrowserFields); - expect(cleanUpKqlQuery(kqlQuery)).toEqual('name : "Provider 1" and event.end: 1521848183232'); - }); - - test('Build KQL query with one data provider and one and as date type (numeric input)', () => { - const dataProviders = cloneDeep(mockDataProviders.slice(0, 1)); - dataProviders[0].and = cloneDeep(mockDataProviders.slice(1, 2)); - dataProviders[0].and[0].queryMatch.field = 'event.end'; - dataProviders[0].and[0].queryMatch.value = 1521848183232; - const kqlQuery = buildGlobalQuery(dataProviders, mockBrowserFields); - expect(cleanUpKqlQuery(kqlQuery)).toEqual('name : "Provider 1" and event.end: 1521848183232'); - }); - - test('Build KQL query with two data provider and multiple and', () => { - const dataProviders = cloneDeep(mockDataProviders.slice(0, 2)); - dataProviders[0].and = cloneDeep(mockDataProviders.slice(2, 4)); - dataProviders[1].and = cloneDeep(mockDataProviders.slice(4, 5)); - const kqlQuery = buildGlobalQuery(dataProviders, mockBrowserFields); - expect(cleanUpKqlQuery(kqlQuery)).toEqual( - '(name : "Provider 1" and name : "Provider 3" and name : "Provider 4") or (name : "Provider 2" and name : "Provider 5")' - ); - }); - - test('Build KQL query with two data provider and multiple and and first data provider is disabled', () => { - const dataProviders = cloneDeep(mockDataProviders.slice(0, 2)); - dataProviders[0].enabled = false; - dataProviders[0].and = cloneDeep(mockDataProviders.slice(2, 4)); - dataProviders[1].and = cloneDeep(mockDataProviders.slice(4, 5)); - const kqlQuery = buildGlobalQuery(dataProviders, mockBrowserFields); - expect(cleanUpKqlQuery(kqlQuery)).toEqual( - '(name : "Provider 3" and name : "Provider 4") or (name : "Provider 2" and name : "Provider 5")' - ); - }); - - test('Build KQL query with two data provider and multiple and and first and provider is disabled', () => { - const dataProviders = cloneDeep(mockDataProviders.slice(0, 2)); - dataProviders[0].and = cloneDeep(mockDataProviders.slice(2, 4)); - dataProviders[0].and[0].enabled = false; - dataProviders[1].and = cloneDeep(mockDataProviders.slice(4, 5)); - const kqlQuery = buildGlobalQuery(dataProviders, mockBrowserFields); - expect(cleanUpKqlQuery(kqlQuery)).toEqual( - '(name : "Provider 1" and name : "Provider 4") or (name : "Provider 2" and name : "Provider 5")' - ); - }); - - test('Build KQL query with all data provider', () => { - const kqlQuery = buildGlobalQuery(mockDataProviders, mockBrowserFields); - expect(cleanUpKqlQuery(kqlQuery)).toEqual( - '(name : "Provider 1") or (name : "Provider 2") or (name : "Provider 3") or (name : "Provider 4") or (name : "Provider 5") or (name : "Provider 6") or (name : "Provider 7") or (name : "Provider 8") or (name : "Provider 9") or (name : "Provider 10")' - ); - }); - - test('Build complex KQL query with and and or', () => { - const dataProviders = cloneDeep(mockDataProviders); - dataProviders[0].and = cloneDeep(mockDataProviders.slice(2, 4)); - dataProviders[1].and = cloneDeep(mockDataProviders.slice(4, 5)); - const kqlQuery = buildGlobalQuery(dataProviders, mockBrowserFields); - expect(cleanUpKqlQuery(kqlQuery)).toEqual( - '(name : "Provider 1" and name : "Provider 3" and name : "Provider 4") or (name : "Provider 2" and name : "Provider 5") or (name : "Provider 3") or (name : "Provider 4") or (name : "Provider 5") or (name : "Provider 6") or (name : "Provider 7") or (name : "Provider 8") or (name : "Provider 9") or (name : "Provider 10")' - ); - }); -}); - -describe('Combined Queries', () => { - const config: EsQueryConfig = { - allowLeadingWildcards: true, - queryStringOptions: {}, - ignoreFilterIfFieldNotInIndex: true, - dateFormatTZ: 'America/New_York', - }; - test('No Data Provider & No kqlQuery', () => { - expect( - combineQueries({ - config, - dataProviders: [], - indexPattern: mockIndexPattern, - browserFields: mockBrowserFields, - filters: [], - kqlQuery: { query: '', language: 'kuery' }, - kqlMode: 'search', - }) - ).toBeNull(); - }); - - test('No Data Provider & No kqlQuery & with Filters', () => { - expect( - combineQueries({ - config, - dataProviders: [], - indexPattern: mockIndexPattern, - browserFields: mockBrowserFields, - filters: [ - { - $state: { store: FilterStateStore.APP_STATE }, - meta: { - alias: null, - disabled: false, - key: 'event.category', - negate: false, - params: { query: 'file' }, - type: 'phrase', - }, - query: { match_phrase: { 'event.category': 'file' } }, - }, - { - $state: { store: FilterStateStore.APP_STATE }, - meta: { - alias: null, - disabled: false, - key: 'host.name', - negate: false, - type: 'exists', - value: 'exists', - }, - query: { exists: { field: 'host.name' } }, - } as Filter, - ], - kqlQuery: { query: '', language: 'kuery' }, - kqlMode: 'search', - }) - ).toEqual({ - filterQuery: - '{"bool":{"must":[],"filter":[{"exists":{"field":"host.name"}}],"should":[],"must_not":[]}}', - }); - }); - - test('Only Data Provider', () => { - const dataProviders = cloneDeep(mockDataProviders.slice(0, 1)); - const { filterQuery } = combineQueries({ - config, - dataProviders, - indexPattern: mockIndexPattern, - browserFields: mockBrowserFields, - filters: [], - kqlQuery: { query: '', language: 'kuery' }, - kqlMode: 'search', - })!; - expect(filterQuery).toEqual( - '{"bool":{"must":[],"filter":[{"bool":{"should":[{"match_phrase":{"name":"Provider 1"}}],"minimum_should_match":1}}],"should":[],"must_not":[]}}' - ); - }); - - test('Only Data Provider with timestamp (string input)', () => { - const dataProviders = cloneDeep(mockDataProviders.slice(0, 1)); - dataProviders[0].queryMatch.field = '@timestamp'; - dataProviders[0].queryMatch.value = '2018-03-23T23:36:23.232Z'; - const { filterQuery } = combineQueries({ - config, - dataProviders, - indexPattern: mockIndexPattern, - browserFields: mockBrowserFields, - filters: [], - kqlQuery: { query: '', language: 'kuery' }, - kqlMode: 'search', - })!; - expect(filterQuery).toMatchInlineSnapshot( - `"{\\"bool\\":{\\"must\\":[],\\"filter\\":[{\\"bool\\":{\\"should\\":[{\\"range\\":{\\"@timestamp\\":{\\"gte\\":\\"1521848183232\\",\\"lte\\":\\"1521848183232\\"}}}],\\"minimum_should_match\\":1}}],\\"should\\":[],\\"must_not\\":[]}}"` - ); - }); - - test('Only Data Provider with timestamp (numeric input)', () => { - const dataProviders = cloneDeep(mockDataProviders.slice(0, 1)); - dataProviders[0].queryMatch.field = '@timestamp'; - dataProviders[0].queryMatch.value = 1521848183232; - const { filterQuery } = combineQueries({ - config, - dataProviders, - indexPattern: mockIndexPattern, - browserFields: mockBrowserFields, - filters: [], - kqlQuery: { query: '', language: 'kuery' }, - kqlMode: 'search', - })!; - expect(filterQuery).toMatchInlineSnapshot( - `"{\\"bool\\":{\\"must\\":[],\\"filter\\":[{\\"bool\\":{\\"should\\":[{\\"range\\":{\\"@timestamp\\":{\\"gte\\":\\"1521848183232\\",\\"lte\\":\\"1521848183232\\"}}}],\\"minimum_should_match\\":1}}],\\"should\\":[],\\"must_not\\":[]}}"` - ); - }); - - test('Only Data Provider with a date type (string input)', () => { - const dataProviders = cloneDeep(mockDataProviders.slice(0, 1)); - dataProviders[0].queryMatch.field = 'event.end'; - dataProviders[0].queryMatch.value = '2018-03-23T23:36:23.232Z'; - const { filterQuery } = combineQueries({ - config, - dataProviders, - indexPattern: mockIndexPattern, - browserFields: mockBrowserFields, - filters: [], - kqlQuery: { query: '', language: 'kuery' }, - kqlMode: 'search', - })!; - expect(filterQuery).toMatchInlineSnapshot( - `"{\\"bool\\":{\\"must\\":[],\\"filter\\":[{\\"bool\\":{\\"should\\":[{\\"match\\":{\\"event.end\\":\\"1521848183232\\"}}],\\"minimum_should_match\\":1}}],\\"should\\":[],\\"must_not\\":[]}}"` - ); - }); - - test('Only Data Provider with date type (numeric input)', () => { - const dataProviders = cloneDeep(mockDataProviders.slice(0, 1)); - dataProviders[0].queryMatch.field = 'event.end'; - dataProviders[0].queryMatch.value = 1521848183232; - const { filterQuery } = combineQueries({ - config, - dataProviders, - indexPattern: mockIndexPattern, - browserFields: mockBrowserFields, - filters: [], - kqlQuery: { query: '', language: 'kuery' }, - kqlMode: 'search', - })!; - expect(filterQuery).toMatchInlineSnapshot( - `"{\\"bool\\":{\\"must\\":[],\\"filter\\":[{\\"bool\\":{\\"should\\":[{\\"match\\":{\\"event.end\\":\\"1521848183232\\"}}],\\"minimum_should_match\\":1}}],\\"should\\":[],\\"must_not\\":[]}}"` - ); - }); - - test('Only KQL search/filter query', () => { - const { filterQuery } = combineQueries({ - config, - dataProviders: [], - indexPattern: mockIndexPattern, - browserFields: mockBrowserFields, - filters: [], - kqlQuery: { query: 'host.name: "host-1"', language: 'kuery' }, - kqlMode: 'search', - })!; - expect(filterQuery).toEqual( - '{"bool":{"must":[],"filter":[{"bool":{"should":[{"match_phrase":{"host.name":"host-1"}}],"minimum_should_match":1}}],"should":[],"must_not":[]}}' - ); - }); - - test('Data Provider & KQL search query', () => { - const dataProviders = cloneDeep(mockDataProviders.slice(0, 1)); - const { filterQuery } = combineQueries({ - config, - dataProviders, - indexPattern: mockIndexPattern, - browserFields: mockBrowserFields, - filters: [], - kqlQuery: { query: 'host.name: "host-1"', language: 'kuery' }, - kqlMode: 'search', - })!; - expect(filterQuery).toEqual( - '{"bool":{"must":[],"filter":[{"bool":{"should":[{"bool":{"should":[{"match_phrase":{"name":"Provider 1"}}],"minimum_should_match":1}},{"bool":{"should":[{"match_phrase":{"host.name":"host-1"}}],"minimum_should_match":1}}],"minimum_should_match":1}}],"should":[],"must_not":[]}}' - ); - }); - - test('Data Provider & KQL filter query', () => { - const dataProviders = cloneDeep(mockDataProviders.slice(0, 1)); - const { filterQuery } = combineQueries({ - config, - dataProviders, - indexPattern: mockIndexPattern, - browserFields: mockBrowserFields, - filters: [], - kqlQuery: { query: 'host.name: "host-1"', language: 'kuery' }, - kqlMode: 'filter', - })!; - expect(filterQuery).toEqual( - '{"bool":{"must":[],"filter":[{"bool":{"filter":[{"bool":{"should":[{"match_phrase":{"name":"Provider 1"}}],"minimum_should_match":1}},{"bool":{"should":[{"match_phrase":{"host.name":"host-1"}}],"minimum_should_match":1}}]}}],"should":[],"must_not":[]}}' - ); - }); - - test('Data Provider & KQL search query multiple', () => { - const dataProviders = cloneDeep(mockDataProviders.slice(0, 2)); - dataProviders[0].and = cloneDeep(mockDataProviders.slice(2, 4)); - dataProviders[1].and = cloneDeep(mockDataProviders.slice(4, 5)); - const { filterQuery } = combineQueries({ - config, - dataProviders, - indexPattern: mockIndexPattern, - browserFields: mockBrowserFields, - filters: [], - kqlQuery: { query: 'host.name: "host-1"', language: 'kuery' }, - kqlMode: 'search', - })!; - expect(filterQuery).toMatchInlineSnapshot( - `"{\\"bool\\":{\\"must\\":[],\\"filter\\":[{\\"bool\\":{\\"should\\":[{\\"bool\\":{\\"should\\":[{\\"bool\\":{\\"filter\\":[{\\"bool\\":{\\"should\\":[{\\"match_phrase\\":{\\"name\\":\\"Provider 1\\"}}],\\"minimum_should_match\\":1}},{\\"bool\\":{\\"should\\":[{\\"match_phrase\\":{\\"name\\":\\"Provider 3\\"}}],\\"minimum_should_match\\":1}},{\\"bool\\":{\\"should\\":[{\\"match_phrase\\":{\\"name\\":\\"Provider 4\\"}}],\\"minimum_should_match\\":1}}]}},{\\"bool\\":{\\"filter\\":[{\\"bool\\":{\\"should\\":[{\\"match_phrase\\":{\\"name\\":\\"Provider 2\\"}}],\\"minimum_should_match\\":1}},{\\"bool\\":{\\"should\\":[{\\"match_phrase\\":{\\"name\\":\\"Provider 5\\"}}],\\"minimum_should_match\\":1}}]}}],\\"minimum_should_match\\":1}},{\\"bool\\":{\\"should\\":[{\\"match_phrase\\":{\\"host.name\\":\\"host-1\\"}}],\\"minimum_should_match\\":1}}],\\"minimum_should_match\\":1}}],\\"should\\":[],\\"must_not\\":[]}}"` - ); - }); - - test('Data Provider & KQL filter query multiple', () => { - const dataProviders = cloneDeep(mockDataProviders.slice(0, 2)); - dataProviders[0].and = cloneDeep(mockDataProviders.slice(2, 4)); - dataProviders[1].and = cloneDeep(mockDataProviders.slice(4, 5)); - const { filterQuery } = combineQueries({ - config, - dataProviders, - indexPattern: mockIndexPattern, - browserFields: mockBrowserFields, - filters: [], - kqlQuery: { query: 'host.name: "host-1"', language: 'kuery' }, - kqlMode: 'filter', - })!; - expect(filterQuery).toMatchInlineSnapshot( - `"{\\"bool\\":{\\"must\\":[],\\"filter\\":[{\\"bool\\":{\\"filter\\":[{\\"bool\\":{\\"should\\":[{\\"bool\\":{\\"filter\\":[{\\"bool\\":{\\"should\\":[{\\"match_phrase\\":{\\"name\\":\\"Provider 1\\"}}],\\"minimum_should_match\\":1}},{\\"bool\\":{\\"should\\":[{\\"match_phrase\\":{\\"name\\":\\"Provider 3\\"}}],\\"minimum_should_match\\":1}},{\\"bool\\":{\\"should\\":[{\\"match_phrase\\":{\\"name\\":\\"Provider 4\\"}}],\\"minimum_should_match\\":1}}]}},{\\"bool\\":{\\"filter\\":[{\\"bool\\":{\\"should\\":[{\\"match_phrase\\":{\\"name\\":\\"Provider 2\\"}}],\\"minimum_should_match\\":1}},{\\"bool\\":{\\"should\\":[{\\"match_phrase\\":{\\"name\\":\\"Provider 5\\"}}],\\"minimum_should_match\\":1}}]}}],\\"minimum_should_match\\":1}},{\\"bool\\":{\\"should\\":[{\\"match_phrase\\":{\\"host.name\\":\\"host-1\\"}}],\\"minimum_should_match\\":1}}]}}],\\"should\\":[],\\"must_not\\":[]}}"` - ); - }); - - test('Data Provider & kql filter query with nested field that exists', () => { - const dataProviders = cloneDeep(mockDataProviders.slice(0, 1)); - const query = combineQueries({ - config, - dataProviders, - indexPattern: mockIndexPattern, - browserFields: mockBrowserFields, - filters: [ - { - meta: { - alias: null, - negate: false, - disabled: false, - type: 'exists', - key: 'nestedField.firstAttributes', - value: 'exists', - }, - query: { - exists: { - field: 'nestedField.firstAttributes', - }, - }, - $state: { - store: FilterStateStore.APP_STATE, - }, - } as Filter, - ], - kqlQuery: { query: '', language: 'kuery' }, - kqlMode: 'filter', - }); - const filterQuery = query && query.filterQuery; - expect(filterQuery).toMatchInlineSnapshot( - `"{\\"bool\\":{\\"must\\":[],\\"filter\\":[{\\"bool\\":{\\"should\\":[{\\"match_phrase\\":{\\"name\\":\\"Provider 1\\"}}],\\"minimum_should_match\\":1}},{\\"exists\\":{\\"field\\":\\"nestedField.firstAttributes\\"}}],\\"should\\":[],\\"must_not\\":[]}}"` - ); - }); - - test('Data Provider & kql filter query with nested field of a particular value', () => { - const dataProviders = cloneDeep(mockDataProviders.slice(0, 1)); - const query = combineQueries({ - config, - dataProviders, - indexPattern: mockIndexPattern, - browserFields: mockBrowserFields, - filters: [ - { - $state: { store: FilterStateStore.APP_STATE }, - meta: { - alias: null, - disabled: false, - key: 'nestedField.secondAttributes', - negate: false, - params: { query: 'test' }, - type: 'phrase', - }, - query: { match_phrase: { 'nestedField.secondAttributes': 'test' } }, - }, - ], - kqlQuery: { query: '', language: 'kuery' }, - kqlMode: 'filter', - }); - const filterQuery = query && query.filterQuery; - expect(filterQuery).toMatchInlineSnapshot( - `"{\\"bool\\":{\\"must\\":[],\\"filter\\":[{\\"bool\\":{\\"should\\":[{\\"match_phrase\\":{\\"name\\":\\"Provider 1\\"}}],\\"minimum_should_match\\":1}},{\\"match_phrase\\":{\\"nestedField.secondAttributes\\":\\"test\\"}}],\\"should\\":[],\\"must_not\\":[]}}"` - ); - }); - - test('Disabled Data Provider and kqlQuery', () => { - const dataProviders = cloneDeep(mockDataProviders.slice(0, 1)); - dataProviders[0].enabled = false; - const { filterQuery } = combineQueries({ - config, - dataProviders, - indexPattern: mockIndexPattern, - browserFields: mockBrowserFields, - filters: [], - kqlQuery: { query: '_id:*', language: 'kuery' }, - kqlMode: 'search', - })!; - - const expectQueryString = JSON.stringify({ - bool: { - must: [], - filter: [ - { - bool: { - should: [ - { - exists: { - field: '_id', - }, - }, - ], - minimum_should_match: 1, - }, - }, - ], - should: [], - must_not: [], - }, - }); - - expect(filterQuery).toStrictEqual(expectQueryString); - }); - - test('Both disabled & enabled data provider and kqlQuery', () => { - const dataProviders = cloneDeep(mockDataProviders.slice(0, 2)); - dataProviders[0].enabled = false; - const { filterQuery } = combineQueries({ - config, - dataProviders, - indexPattern: mockIndexPattern, - browserFields: mockBrowserFields, - filters: [], - kqlQuery: { query: '_id:*', language: 'kuery' }, - kqlMode: 'search', - })!; - - const expectQueryString = JSON.stringify({ - bool: { - must: [], - filter: [ - { - bool: { - should: [ - { - bool: { - should: [ - { - match_phrase: { - [dataProviders[1].queryMatch.field]: dataProviders[1].queryMatch.value, - }, - }, - ], - minimum_should_match: 1, - }, - }, - { - bool: { - should: [ - { - exists: { - field: '_id', - }, - }, - ], - minimum_should_match: 1, - }, - }, - ], - minimum_should_match: 1, - }, - }, - ], - should: [], - must_not: [], - }, - }); - - expect(filterQuery).toStrictEqual(expectQueryString); - }); - - describe('resolverIsShowing', () => { - test('it returns true when graphEventId is NOT an empty string', () => { - expect(resolverIsShowing('a valid id')).toBe(true); - }); - - test('it returns false when graphEventId is undefined', () => { - expect(resolverIsShowing(undefined)).toBe(false); - }); - - test('it returns false when graphEventId is an empty string', () => { - expect(resolverIsShowing('')).toBe(false); - }); - }); - - describe('view selection', () => { - const validViewSelections = ['gridView', 'eventRenderedView']; - const invalidViewSelections = [ - 'gRiDvIeW', - 'EvEnTrEnDeReDvIeW', - 'anything else', - '', - 1234, - {}, - undefined, - null, - ]; - - const selectableViews: TableId[] = [ - TableId.alertsOnAlertsPage, - TableId.alertsOnRuleDetailsPage, - ]; - - const exampleNonSelectableViews: string[] = [ - TableId.hostsPageEvents, - TableId.usersPageEvents, - 'foozle', - '', - ]; - - describe('isSelectableView', () => { - selectableViews.forEach((timelineId) => { - test(`it returns true (for selectable view) timelineId ${timelineId}`, () => { - expect(isSelectableView(timelineId)).toBe(true); - }); - }); - - exampleNonSelectableViews.forEach((timelineId) => { - test(`it returns false (for NON-selectable view) timelineId ${timelineId}`, () => { - expect(isSelectableView(timelineId)).toBe(false); - }); - }); - }); - - describe('isViewSelection', () => { - validViewSelections.forEach((value) => { - test(`it returns true when value is valid: ${value}`, () => { - expect(isViewSelection(value)).toBe(true); - }); - }); - - invalidViewSelections.forEach((value) => { - test(`it returns false when value is invalid: ${value}`, () => { - expect(isViewSelection(value)).toBe(false); - }); - }); - }); - - describe('getDefaultViewSelection', () => { - describe('NON-selectable views', () => { - exampleNonSelectableViews.forEach((timelineId) => { - describe('given valid values', () => { - validViewSelections.forEach((value) => { - test(`it ALWAYS returns 'gridView' for NON-selectable timelineId ${timelineId}, with valid value: ${value}`, () => { - expect(getDefaultViewSelection({ timelineId, value })).toEqual('gridView'); - }); - }); - }); - - describe('given invalid values', () => { - invalidViewSelections.forEach((value) => { - test(`it ALWAYS returns 'gridView' for NON-selectable timelineId ${timelineId}, with invalid value: ${value}`, () => { - expect(getDefaultViewSelection({ timelineId, value })).toEqual('gridView'); - }); - }); - }); - }); - }); - }); - - describe('selectable views', () => { - selectableViews.forEach((timelineId) => { - describe('given valid values', () => { - validViewSelections.forEach((value) => { - test(`it returns ${value} for selectable timelineId ${timelineId}, with valid value: ${value}`, () => { - expect(getDefaultViewSelection({ timelineId, value })).toEqual(value); - }); - }); - }); - - describe('given INvalid values', () => { - invalidViewSelections.forEach((value) => { - test(`it ALWAYS returns 'gridView' for selectable timelineId ${timelineId}, with invalid value: ${value}`, () => { - expect(getDefaultViewSelection({ timelineId, value })).toEqual('gridView'); - }); - }); - }); - }); - }); - }); - describe('DataProvider yields same result as kqlQuery equivolent with each operator', () => { - describe('IS ONE OF operator', () => { - test('dataprovider matches kql equivolent', () => { - const dataProviders = cloneDeep(mockDataProviders.slice(0, 1)); - dataProviders[0].queryMatch.operator = IS_ONE_OF_OPERATOR; - dataProviders[0].queryMatch.value = ['a', 'b', 'c']; - const { filterQuery: filterQueryWithDataProvider } = combineQueries({ - config, - dataProviders, - indexPattern: mockIndexPattern, - browserFields: mockBrowserFields, - filters: [], - kqlQuery: { query: '', language: 'kuery' }, - kqlMode: 'search', - })!; - const { filterQuery: filterQueryWithKQLQuery } = combineQueries({ - config, - dataProviders: [], - indexPattern: mockIndexPattern, - browserFields: mockBrowserFields, - filters: [], - kqlQuery: { query: 'name: ("a" OR "b" OR "c")', language: 'kuery' }, - kqlMode: 'search', - })!; - - expect(filterQueryWithDataProvider).toEqual(filterQueryWithKQLQuery); - }); - test('dataprovider with negated IS ONE OF operator matches kql equivolent', () => { - const dataProviders = cloneDeep(mockDataProviders.slice(0, 1)); - dataProviders[0].queryMatch.operator = IS_ONE_OF_OPERATOR; - dataProviders[0].queryMatch.value = ['a', 'b', 'c']; - dataProviders[0].excluded = true; - const { filterQuery: filterQueryWithDataProvider } = combineQueries({ - config, - dataProviders, - indexPattern: mockIndexPattern, - browserFields: mockBrowserFields, - filters: [], - kqlQuery: { query: '', language: 'kuery' }, - kqlMode: 'search', - })!; - const { filterQuery: filterQueryWithKQLQuery } = combineQueries({ - config, - dataProviders: [], - indexPattern: mockIndexPattern, - browserFields: mockBrowserFields, - filters: [], - kqlQuery: { query: 'NOT name: ("a" OR "b" OR "c")', language: 'kuery' }, - kqlMode: 'search', - })!; - - expect(filterQueryWithDataProvider).toEqual(filterQueryWithKQLQuery); - }); - }); - describe('IS operator', () => { - test('dataprovider matches kql equivolent', () => { - const dataProviders = cloneDeep(mockDataProviders.slice(0, 1)); - dataProviders[0].queryMatch.operator = IS_OPERATOR; - dataProviders[0].queryMatch.value = 'a'; - const { filterQuery: filterQueryWithDataProvider } = combineQueries({ - config, - dataProviders, - indexPattern: mockIndexPattern, - browserFields: mockBrowserFields, - filters: [], - kqlQuery: { query: '', language: 'kuery' }, - kqlMode: 'search', - })!; - const { filterQuery: filterQueryWithKQLQuery } = combineQueries({ - config, - dataProviders: [], - indexPattern: mockIndexPattern, - browserFields: mockBrowserFields, - filters: [], - kqlQuery: { query: 'name: "a"', language: 'kuery' }, - kqlMode: 'search', - })!; - - expect(filterQueryWithDataProvider).toEqual(filterQueryWithKQLQuery); - }); - test('dataprovider with negated IS operator matches kql equivolent', () => { - const dataProviders = cloneDeep(mockDataProviders.slice(0, 1)); - dataProviders[0].queryMatch.operator = IS_OPERATOR; - dataProviders[0].queryMatch.value = 'a'; - dataProviders[0].excluded = true; - const { filterQuery: filterQueryWithDataProvider } = combineQueries({ - config, - dataProviders, - indexPattern: mockIndexPattern, - browserFields: mockBrowserFields, - filters: [], - kqlQuery: { query: '', language: 'kuery' }, - kqlMode: 'search', - })!; - const { filterQuery: filterQueryWithKQLQuery } = combineQueries({ - config, - dataProviders: [], - indexPattern: mockIndexPattern, - browserFields: mockBrowserFields, - filters: [], - kqlQuery: { query: 'NOT name: "a"', language: 'kuery' }, - kqlMode: 'search', - })!; - - expect(filterQueryWithDataProvider).toEqual(filterQueryWithKQLQuery); - }); - }); - describe('Exists operator', () => { - test('dataprovider matches kql equivolent', () => { - const dataProviders = cloneDeep(mockDataProviders.slice(0, 1)); - dataProviders[0].queryMatch.operator = EXISTS_OPERATOR; - const { filterQuery: filterQueryWithDataProvider } = combineQueries({ - config, - dataProviders, - indexPattern: mockIndexPattern, - browserFields: mockBrowserFields, - filters: [], - kqlQuery: { query: '', language: 'kuery' }, - kqlMode: 'search', - })!; - const { filterQuery: filterQueryWithKQLQuery } = combineQueries({ - config, - dataProviders: [], - indexPattern: mockIndexPattern, - browserFields: mockBrowserFields, - filters: [], - kqlQuery: { query: 'name : *', language: 'kuery' }, - kqlMode: 'search', - })!; - - expect(filterQueryWithDataProvider).toEqual(filterQueryWithKQLQuery); - }); - test('dataprovider with negated EXISTS operator matches kql equivolent', () => { - const dataProviders = cloneDeep(mockDataProviders.slice(0, 1)); - dataProviders[0].queryMatch.operator = EXISTS_OPERATOR; - dataProviders[0].excluded = true; - const { filterQuery: filterQueryWithDataProvider } = combineQueries({ - config, - dataProviders, - indexPattern: mockIndexPattern, - browserFields: mockBrowserFields, - filters: [], - kqlQuery: { query: '', language: 'kuery' }, - kqlMode: 'search', - })!; - const { filterQuery: filterQueryWithKQLQuery } = combineQueries({ - config, - dataProviders: [], - indexPattern: mockIndexPattern, - browserFields: mockBrowserFields, - filters: [], - kqlQuery: { query: 'NOT name : *', language: 'kuery' }, - kqlMode: 'search', - })!; - - expect(filterQueryWithDataProvider).toEqual(filterQueryWithKQLQuery); - }); - }); - }); -}); - -describe('isStringOrNumberArray', () => { - test('it returns false when value is not an array', () => { - expect(isStringOrNumberArray('just a string')).toBe(false); - }); - - test('it returns false when value is an array of mixed types', () => { - expect(isStringOrNumberArray(['mixed', 123, 'types'])).toBe(false); - }); - - test('it returns false when value is an array of bad values', () => { - const badValues = [undefined, null, {}] as unknown as string[]; - expect(isStringOrNumberArray(badValues)).toBe(false); - }); - - test('it returns true when value is an empty array', () => { - expect(isStringOrNumberArray([])).toBe(true); - }); - - test('it returns true when value is an array of all strings', () => { - expect(isStringOrNumberArray(['all', 'string', 'values'])).toBe(true); - }); - - test('it returns true when value is an array of all numbers', () => { - expect(isStringOrNumberArray([123, 456, 789])).toBe(true); - }); -}); - -describe('buildExistsQueryMatch', () => { - it('correcty computes EXISTS query with no nested field', () => { - expect( - buildExistsQueryMatch({ isFieldTypeNested: false, field: 'host', browserFields: {} }) - ).toBe(`host ${EXISTS_OPERATOR}`); - }); - - it('correcty computes EXISTS query with nested field', () => { - expect( - buildExistsQueryMatch({ - isFieldTypeNested: true, - field: 'nestedField.firstAttributes', - browserFields: mockBrowserFields, - }) - ).toBe(`nestedField: { firstAttributes: * }`); - }); -}); - -describe('buildIsQueryMatch', () => { - it('correcty computes IS query with no nested field', () => { - expect( - buildIsQueryMatch({ - isFieldTypeNested: false, - field: 'nestedField.thirdAttributes', - value: 100000, - browserFields: {}, - }) - ).toBe(`nestedField.thirdAttributes ${IS_OPERATOR} 100000`); - }); - - it('correcty computes IS query with nested date field', () => { - expect( - buildIsQueryMatch({ - isFieldTypeNested: true, - browserFields: mockBrowserFields, - field: 'nestedField.thirdAttributes', - value: 1668521970232, - }) - ).toBe(`nestedField: { thirdAttributes${IS_OPERATOR} \"1668521970232\" }`); - }); - - it('correcty computes IS query with nested string field', () => { - expect( - buildIsQueryMatch({ - isFieldTypeNested: true, - browserFields: mockBrowserFields, - field: 'nestedField.secondAttributes', - value: 'text', - }) - ).toBe(`nestedField: { secondAttributes${IS_OPERATOR} text }`); - }); -}); - -describe('buildIsOneOfQueryMatch', () => { - it('correcty computes IS ONE OF query with numbers', () => { - expect( - buildIsOneOfQueryMatch({ - field: 'kibana.alert.worflow_status', - value: [1, 2, 3], - }) - ).toBe('kibana.alert.worflow_status : (1 OR 2 OR 3)'); - }); - - it('correcty computes IS ONE OF query with strings', () => { - expect( - buildIsOneOfQueryMatch({ - field: 'kibana.alert.worflow_status', - value: ['a', 'b', 'c'], - }) - ).toBe(`kibana.alert.worflow_status : (\"a\" OR \"b\" OR \"c\")`); - }); - - it('correcty computes IS ONE OF query if value is an empty array', () => { - expect( - buildIsOneOfQueryMatch({ - field: 'kibana.alert.worflow_status', - value: [], - }) - ).toBe("kibana.alert.worflow_status : ''"); - }); - - it('correcty computes IS ONE OF query if given a single string value', () => { - expect( - buildIsOneOfQueryMatch({ - field: 'kibana.alert.worflow_status', - value: ['a'], - }) - ).toBe(`kibana.alert.worflow_status : (\"a\")`); - }); - - it('correcty computes IS ONE OF query if given a single numeric value', () => { - expect( - buildIsOneOfQueryMatch({ - field: 'kibana.alert.worflow_status', - value: [1], - }) - ).toBe(`kibana.alert.worflow_status : (1)`); - }); -}); diff --git a/x-pack/plugins/timelines/public/components/t_grid/helpers.tsx b/x-pack/plugins/timelines/public/components/t_grid/helpers.tsx deleted file mode 100644 index 64043dbefcef6..0000000000000 --- a/x-pack/plugins/timelines/public/components/t_grid/helpers.tsx +++ /dev/null @@ -1,347 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type { Filter, EsQueryConfig, Query } from '@kbn/es-query'; -import { DataViewBase, FilterStateStore } from '@kbn/es-query'; -import { get, isEmpty } from 'lodash/fp'; -import memoizeOne from 'memoize-one'; -import { elementOrChildrenHasFocus } from '../../../common/utils/accessibility'; -import type { BrowserFields } from '../../../common/search_strategy/index_fields'; -import { - DataProviderType, - EXISTS_OPERATOR, - IS_ONE_OF_OPERATOR, - IS_OPERATOR, -} from '../../../common/types/timeline'; -import type { DataProvider, DataProvidersAnd } from '../../../common/types/timeline'; -import { assertUnreachable } from '../../../common/utility_types'; -import { convertToBuildEsQuery, escapeQueryValue } from '../utils/keury'; -import { EVENTS_TABLE_CLASS_NAME } from './styles'; -import { TableId } from '../../types'; -import { ViewSelection } from './event_rendered_view/selector'; - -interface CombineQueries { - config: EsQueryConfig; - dataProviders: DataProvider[]; - indexPattern: DataViewBase; - browserFields: BrowserFields; - filters: Filter[]; - kqlQuery: Query; - kqlMode: string; -} - -const isNumber = (value: string | number): value is number => !isNaN(Number(value)); - -const convertDateFieldToQuery = (field: string, value: string | number) => - `${field}: ${isNumber(value) ? value : new Date(value).valueOf()}`; - -const getBaseFields = memoizeOne((browserFields: BrowserFields): string[] => { - const baseFields = get('base', browserFields); - if (baseFields != null && baseFields.fields != null) { - return Object.keys(baseFields.fields); - } - return []; -}); - -const getBrowserFieldPath = (field: string, browserFields: BrowserFields) => { - const splitFields = field.split('.'); - const baseFields = getBaseFields(browserFields); - if (baseFields.includes(field)) { - return ['base', 'fields', field]; - } - return [splitFields[0], 'fields', field]; -}; - -const checkIfFieldTypeIsDate = (field: string, browserFields: BrowserFields) => { - const pathBrowserField = getBrowserFieldPath(field, browserFields); - const browserField = get(pathBrowserField, browserFields); - if (browserField != null && browserField.type === 'date') { - return true; - } - return false; -}; - -const convertNestedFieldToQuery = ( - field: string, - value: string | number, - browserFields: BrowserFields -) => { - const pathBrowserField = getBrowserFieldPath(field, browserFields); - const browserField = get(pathBrowserField, browserFields); - const nestedPath = browserField.subType.nested.path; - const key = field.replace(`${nestedPath}.`, ''); - return `${nestedPath}: { ${key}: ${browserField.type === 'date' ? `"${value}"` : value} }`; -}; - -const convertNestedFieldToExistQuery = (field: string, browserFields: BrowserFields) => { - const pathBrowserField = getBrowserFieldPath(field, browserFields); - const browserField = get(pathBrowserField, browserFields); - const nestedPath = browserField.subType.nested.path; - const key = field.replace(`${nestedPath}.`, ''); - return `${nestedPath}: { ${key}: * }`; -}; - -const checkIfFieldTypeIsNested = (field: string, browserFields: BrowserFields) => { - const pathBrowserField = getBrowserFieldPath(field, browserFields); - const browserField = get(pathBrowserField, browserFields); - if (browserField != null && browserField.subType && browserField.subType.nested) { - return true; - } - return false; -}; - -const buildQueryMatch = ( - dataProvider: DataProvider | DataProvidersAnd, - browserFields: BrowserFields -) => { - const { - excluded, - type, - queryMatch: { field, operator, value }, - } = dataProvider; - - const isFieldTypeNested = checkIfFieldTypeIsNested(field, browserFields); - const isExcluded = excluded ? 'NOT ' : ''; - - switch (operator) { - case IS_OPERATOR: - if (!isStringOrNumberArray(value)) { - return `${isExcluded}${ - type !== DataProviderType.template - ? buildIsQueryMatch({ browserFields, field, isFieldTypeNested, value }) - : buildExistsQueryMatch({ browserFields, field, isFieldTypeNested }) - }`; - } else { - return `${isExcluded}${field} : ${JSON.stringify(value[0])}`; - } - - case EXISTS_OPERATOR: - return `${isExcluded}${buildExistsQueryMatch({ browserFields, field, isFieldTypeNested })}`; - - case IS_ONE_OF_OPERATOR: - if (isStringOrNumberArray(value)) { - return `${isExcluded}${buildIsOneOfQueryMatch({ field, value })}`; - } else { - return `${isExcluded}${field} : ${JSON.stringify(value)}`; - } - default: - assertUnreachable(operator); - } -}; - -export const buildGlobalQuery = (dataProviders: DataProvider[], browserFields: BrowserFields) => - dataProviders - .reduce((queries: string[], dataProvider: DataProvider) => { - const flatDataProviders = [dataProvider, ...dataProvider.and]; - const activeDataProviders = flatDataProviders.filter( - (flatDataProvider) => flatDataProvider.enabled - ); - - if (!activeDataProviders.length) return queries; - - const activeDataProvidersQueries = activeDataProviders.map((activeDataProvider) => - buildQueryMatch(activeDataProvider, browserFields) - ); - - const activeDataProvidersQueryMatch = activeDataProvidersQueries.join(' and '); - - return [...queries, activeDataProvidersQueryMatch]; - }, []) - .filter((queriesItem) => !isEmpty(queriesItem)) - .reduce((globalQuery: string, queryMatch: string, index: number, queries: string[]) => { - if (queries.length <= 1) return queryMatch; - - return !index ? `(${queryMatch})` : `${globalQuery} or (${queryMatch})`; - }, ''); - -export const isDataProviderEmpty = (dataProviders: DataProvider[]) => { - return isEmpty(dataProviders) || isEmpty(dataProviders.filter((d) => d.enabled === true)); -}; - -export const combineQueries = ({ - config, - dataProviders, - indexPattern, - browserFields, - filters = [], - kqlQuery, - kqlMode, -}: CombineQueries): { filterQuery: string | undefined; kqlError: Error | undefined } | null => { - const kuery: Query = { query: '', language: kqlQuery.language }; - if (isDataProviderEmpty(dataProviders) && isEmpty(kqlQuery.query) && isEmpty(filters)) { - return null; - } else if (isDataProviderEmpty(dataProviders) && isEmpty(kqlQuery.query) && !isEmpty(filters)) { - const [filterQuery, kqlError] = convertToBuildEsQuery({ - config, - queries: [kuery], - indexPattern, - filters, - }); - - return { - filterQuery, - kqlError, - }; - } - - const operatorKqlQuery = kqlMode === 'filter' ? 'and' : 'or'; - - const postpend = (q: string) => `${!isEmpty(q) ? `(${q})` : ''}`; - - const globalQuery = buildGlobalQuery(dataProviders, browserFields); // based on Data Providers - - const querySuffix = postpend(kqlQuery.query as string); // based on Unified Search bar - - const queryPrefix = globalQuery ? `(${globalQuery})` : ''; - - const queryOperator = queryPrefix && querySuffix ? operatorKqlQuery : ''; - - kuery.query = `(${queryPrefix} ${queryOperator} ${querySuffix})`; - - const [filterQuery, kqlError] = convertToBuildEsQuery({ - config, - queries: [kuery], - indexPattern, - filters, - }); - - return { - filterQuery, - kqlError, - }; -}; - -export const buildTimeRangeFilter = (from: string, to: string): Filter => - ({ - range: { - '@timestamp': { - gte: from, - lt: to, - format: 'strict_date_optional_time', - }, - }, - meta: { - type: 'range', - disabled: false, - negate: false, - alias: null, - key: '@timestamp', - params: { - gte: from, - lt: to, - format: 'strict_date_optional_time', - }, - }, - $state: { - store: FilterStateStore.APP_STATE, - }, - } as Filter); - -export const getCombinedFilterQuery = ({ - from, - to, - filters, - ...combineQueriesParams -}: CombineQueries & { from: string; to: string }): string | undefined => { - const combinedQueries = combineQueries({ - ...combineQueriesParams, - filters: [...filters, buildTimeRangeFilter(from, to)], - }); - - return combinedQueries ? combinedQueries.filterQuery : undefined; -}; - -export const resolverIsShowing = (graphEventId: string | undefined): boolean => - graphEventId != null && graphEventId !== ''; - -export const EVENTS_COUNT_BUTTON_CLASS_NAME = 'local-events-count-button'; - -/** Returns true if the events table has focus */ -export const tableHasFocus = (containerElement: HTMLElement | null): boolean => - elementOrChildrenHasFocus( - containerElement?.querySelector(`.${EVENTS_TABLE_CLASS_NAME}`) - ); - -export const isSelectableView = (timelineId: string): boolean => - timelineId === TableId.alertsOnAlertsPage || timelineId === TableId.alertsOnRuleDetailsPage; - -export const isViewSelection = (value: unknown): value is ViewSelection => - value === 'gridView' || value === 'eventRenderedView'; - -/** always returns a valid default `ViewSelection` */ -export const getDefaultViewSelection = ({ - timelineId, - value, -}: { - timelineId: string; - value: unknown; -}): ViewSelection => { - const defaultViewSelection = 'gridView'; - - if (!isSelectableView(timelineId)) { - return defaultViewSelection; - } else { - return isViewSelection(value) ? value : defaultViewSelection; - } -}; - -/** This local storage key stores the `Grid / Event rendered view` selection */ -export const ALERTS_TABLE_VIEW_SELECTION_KEY = 'securitySolution.alerts.table.view-selection'; - -export const buildIsQueryMatch = ({ - browserFields, - field, - isFieldTypeNested, - value, -}: { - browserFields: BrowserFields; - field: string; - isFieldTypeNested: boolean; - value: string | number; -}): string => { - if (isFieldTypeNested) { - return convertNestedFieldToQuery(field, value, browserFields); - } else if (checkIfFieldTypeIsDate(field, browserFields)) { - return convertDateFieldToQuery(field, value); - } else { - return `${field} : ${isNumber(value) ? value : escapeQueryValue(value)}`; - } -}; - -export const buildExistsQueryMatch = ({ - browserFields, - field, - isFieldTypeNested, -}: { - browserFields: BrowserFields; - field: string; - isFieldTypeNested: boolean; -}): string => { - return isFieldTypeNested - ? convertNestedFieldToExistQuery(field, browserFields) - : `${field} ${EXISTS_OPERATOR}`; -}; - -export const buildIsOneOfQueryMatch = ({ - field, - value, -}: { - field: string; - value: Array; -}): string => { - const trimmedField = field.trim(); - if (value.length) { - return `${trimmedField} : (${value - .map((item) => (isNumber(item) ? Number(item) : `${escapeQueryValue(item.trim())}`)) - .join(' OR ')})`; - } - return `${trimmedField} : ''`; -}; - -export const isStringOrNumberArray = (value: unknown): value is Array => - Array.isArray(value) && - (value.every((x) => typeof x === 'string') || value.every((x) => typeof x === 'number')); diff --git a/x-pack/plugins/timelines/public/components/t_grid/index.tsx b/x-pack/plugins/timelines/public/components/t_grid/index.tsx deleted file mode 100644 index 7512edb776398..0000000000000 --- a/x-pack/plugins/timelines/public/components/t_grid/index.tsx +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; - -import type { TGridProps } from '../../types'; -import { TGridIntegrated, TGridIntegratedProps } from './integrated'; - -export const TGrid = (props: TGridProps) => { - const { type, ...componentsProps } = props; - if (type === 'embedded') { - return ; - } - return null; -}; - -// eslint-disable-next-line import/no-default-export -export { TGrid as default }; diff --git a/x-pack/plugins/timelines/public/components/t_grid/integrated/index.test.tsx b/x-pack/plugins/timelines/public/components/t_grid/integrated/index.test.tsx deleted file mode 100644 index d33d2fb5635b8..0000000000000 --- a/x-pack/plugins/timelines/public/components/t_grid/integrated/index.test.tsx +++ /dev/null @@ -1,87 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { euiDarkVars } from '@kbn/ui-theme'; -import React from 'react'; -import { render, screen } from '@testing-library/react'; -import { TGridIntegrated, TGridIntegratedProps } from '.'; -import { TestProviders, tGridIntegratedProps } from '../../../mock'; - -const mockId = tGridIntegratedProps.id; -jest.mock('../../../container', () => ({ - useTimelineEvents: () => [ - false, - { - id: mockId, - inspect: { - dsl: [], - response: [], - }, - totalCount: -1, - pageInfo: { - activePage: 0, - querySize: 0, - }, - events: [], - updatedAt: 0, - }, - ], -})); -jest.mock('../helpers', () => { - const original = jest.requireActual('../helpers'); - return { - ...original, - getCombinedFilterQuery: () => ({ - bool: { - must: [], - filter: [], - }, - }), - }; -}); -const defaultProps: TGridIntegratedProps = tGridIntegratedProps; -describe('integrated t_grid', () => { - const dataTestSubj = 'right-here-dawg'; - it('does not render graphOverlay if graphOverlay=null', () => { - render( - - - - ); - expect(screen.queryByTestId(dataTestSubj)).toBeNull(); - }); - it('does render graphOverlay if graphOverlay=React.ReactNode', () => { - render( - - } /> - - ); - expect(screen.queryByTestId(dataTestSubj)).not.toBeNull(); - }); - - it(`prevents view selection from overlapping EuiDataGrid's 'Full screen' button`, () => { - render( - - - - ); - - expect(screen.queryByTestId('updated-flex-group')).toHaveStyleRule( - `margin-right`, - euiDarkVars.euiSizeXL - ); - }); - it(`does not render the empty state when the graph overlay is open`, () => { - render( - - } /> - - ); - - expect(screen.queryByTestId('tGridEmptyState')).toBeNull(); - }); -}); diff --git a/x-pack/plugins/timelines/public/components/t_grid/integrated/index.tsx b/x-pack/plugins/timelines/public/components/t_grid/integrated/index.tsx deleted file mode 100644 index fba382eb7ca3a..0000000000000 --- a/x-pack/plugins/timelines/public/components/t_grid/integrated/index.tsx +++ /dev/null @@ -1,399 +0,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 { Storage } from '@kbn/kibana-utils-plugin/public'; -import { AlertConsumers } from '@kbn/rule-data-utils'; -import { EuiFlexGroup, EuiFlexItem, EuiPanel } from '@elastic/eui'; -import { isEmpty } from 'lodash/fp'; -import React, { useEffect, useMemo, useState } from 'react'; -import styled from 'styled-components'; -import { useDispatch } from 'react-redux'; - -import type { MappingRuntimeFields } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; -import { DataViewBase, Filter, Query } from '@kbn/es-query'; -import { useKibana } from '@kbn/kibana-react-plugin/public'; -import type { CoreStart } from '@kbn/core/public'; -import type { DataPublicPluginStart } from '@kbn/data-plugin/public'; -import { getEsQueryConfig } from '@kbn/data-plugin/common'; -import { Direction, EntityType } from '../../../../common/search_strategy'; -import type { BrowserFields } from '../../../../common/search_strategy/index_fields'; -import { - BulkActionsProp, - FieldBrowserOptions, - TGridCellAction, -} from '../../../../common/types/timeline'; - -import type { - CellValueElementProps, - ColumnHeaderOptions, - ControlColumnProps, - RowRenderer, - AlertStatus, -} from '../../../../common/types/timeline'; - -import { useDeepEqualSelector } from '../../../hooks/use_selector'; -import { defaultHeaders } from '../body/column_headers/default_headers'; -import { - ALERTS_TABLE_VIEW_SELECTION_KEY, - getCombinedFilterQuery, - getDefaultViewSelection, - resolverIsShowing, -} from '../helpers'; -import { tGridActions, tGridSelectors } from '../../../store/t_grid'; -import { Ecs } from '../../../../common/ecs'; -import { useTimelineEvents, InspectResponse, Refetch } from '../../../container'; -import { StatefulBody } from '../body'; -import { SELECTOR_TIMELINE_GLOBAL_CONTAINER, UpdatedFlexGroup, UpdatedFlexItem } from '../styles'; -import { Sort } from '../body/sort'; -import { InspectButton, InspectButtonContainer } from '../../inspect'; -import { SummaryViewSelector, ViewSelection } from '../event_rendered_view/selector'; -import { TGridLoading, TGridEmpty, TableContext } from '../shared'; - -const storage = new Storage(localStorage); - -const TitleText = styled.span` - margin-right: 12px; -`; - -const StyledEuiPanel = styled(EuiPanel)<{ $isFullScreen: boolean }>` - display: flex; - flex-direction: column; - position: relative; - width: 100%; - - ${({ $isFullScreen }) => - $isFullScreen && - ` - border: 0; - box-shadow: none; - padding-top: 0; - padding-bottom: 0; - `} -`; - -const EventsContainerLoading = styled.div.attrs(({ className = '' }) => ({ - className: `${SELECTOR_TIMELINE_GLOBAL_CONTAINER} ${className}`, -}))` - position: relative; - width: 100%; - overflow: hidden; - flex: 1; - display: flex; - flex-direction: column; -`; - -const FullWidthFlexGroup = styled(EuiFlexGroup)<{ $visible: boolean }>` - overflow: hidden; - margin: 0; - display: ${({ $visible }) => ($visible ? 'flex' : 'none')}; -`; - -const ScrollableFlexItem = styled(EuiFlexItem)` - overflow: auto; -`; - -const SECURITY_ALERTS_CONSUMERS = [AlertConsumers.SIEM]; - -export interface TGridIntegratedProps { - additionalFilters: React.ReactNode; - appId: string; - browserFields: BrowserFields; - bulkActions?: BulkActionsProp; - columns: ColumnHeaderOptions[]; - data?: DataPublicPluginStart; - dataViewId?: string | null; - defaultCellActions?: TGridCellAction[]; - deletedEventIds: Readonly; - disabledCellActions: string[]; - end: string; - entityType: EntityType; - fieldBrowserOptions?: FieldBrowserOptions; - filters: Filter[]; - filterStatus?: AlertStatus; - getRowRenderer?: ({ - data, - rowRenderers, - }: { - data: Ecs; - rowRenderers: RowRenderer[]; - }) => RowRenderer | null; - globalFullScreen: boolean; - // If truthy, the graph viewer (Resolver) is showing - graphEventId?: string; - graphOverlay?: React.ReactNode; - height?: number; - id: string; - indexNames: string[]; - indexPattern: DataViewBase; - isLive: boolean; - isLoadingIndexPattern: boolean; - itemsPerPage: number; - itemsPerPageOptions: number[]; - leadingControlColumns?: ControlColumnProps[]; - onRuleChange?: () => void; - query: Query; - renderCellValue: (props: CellValueElementProps) => React.ReactNode; - rowRenderers: RowRenderer[]; - runtimeMappings: MappingRuntimeFields; - setQuery: (inspect: InspectResponse, loading: boolean, refetch: Refetch) => void; - sort: Sort[]; - start: string; - tGridEventRenderedViewEnabled: boolean; - trailingControlColumns?: ControlColumnProps[]; - unit?: (n: number) => string; -} - -const TGridIntegratedComponent: React.FC = ({ - additionalFilters, - appId, - browserFields, - bulkActions = true, - columns, - data, - dataViewId = null, - defaultCellActions, - deletedEventIds, - disabledCellActions, - end, - entityType, - fieldBrowserOptions, - filters, - filterStatus, - getRowRenderer, - globalFullScreen, - graphEventId, - graphOverlay = null, - id, - indexNames, - indexPattern, - isLoadingIndexPattern, - itemsPerPage, - itemsPerPageOptions, - leadingControlColumns, - onRuleChange, - query, - renderCellValue, - rowRenderers, - runtimeMappings, - setQuery, - sort, - start, - tGridEventRenderedViewEnabled, - trailingControlColumns, - unit, -}) => { - const dispatch = useDispatch(); - const columnsHeader = isEmpty(columns) ? defaultHeaders : columns; - const { uiSettings } = useKibana().services; - - const [tableView, setTableView] = useState( - getDefaultViewSelection({ timelineId: id, value: storage.get(ALERTS_TABLE_VIEW_SELECTION_KEY) }) - ); - const getManageDataTable = useMemo(() => tGridSelectors.getManageDataTableById(), []); - - const { queryFields, title } = useDeepEqualSelector((state) => - getManageDataTable(state, id ?? '') - ); - - const justTitle = useMemo(() => {title}, [title]); - const esQueryConfig = getEsQueryConfig(uiSettings); - - const filterQuery = useMemo( - () => - getCombinedFilterQuery({ - config: esQueryConfig, - browserFields, - dataProviders: [], - filters, - from: start, - indexPattern, - kqlMode: 'filter', - kqlQuery: query, - to: end, - }), - [esQueryConfig, indexPattern, browserFields, filters, start, end, query] - ); - - const canQueryTimeline = useMemo( - () => - filterQuery != null && - isLoadingIndexPattern != null && - !isLoadingIndexPattern && - !isEmpty(start) && - !isEmpty(end), - [isLoadingIndexPattern, filterQuery, start, end] - ); - - const fields = useMemo( - () => [...columnsHeader.map((c) => c.id), ...(queryFields ?? [])], - [columnsHeader, queryFields] - ); - - const sortField = useMemo( - () => - sort.map(({ columnId, columnType, esTypes, sortDirection }) => ({ - field: columnId, - type: columnType, - direction: sortDirection as Direction, - esTypes: esTypes ?? [], - })), - [sort] - ); - - const [loading, { events, loadPage, pageInfo, refetch, totalCount = 0, inspect }] = - useTimelineEvents({ - // We rely on entityType to determine Events vs Alerts - alertConsumers: SECURITY_ALERTS_CONSUMERS, - data, - dataViewId, - endDate: end, - entityType, - fields, - filterQuery, - id, - indexNames, - limit: itemsPerPage, - runtimeMappings, - skip: !canQueryTimeline, - sort: sortField, - startDate: start, - filterStatus, - }); - - useEffect(() => { - dispatch(tGridActions.updateIsLoading({ id, isLoading: loading })); - }, [dispatch, id, loading]); - - const totalCountMinusDeleted = useMemo( - () => (totalCount > 0 ? totalCount - deletedEventIds.length : 0), - [deletedEventIds.length, totalCount] - ); - - const hasAlerts = totalCountMinusDeleted > 0; - - // Only show the table-spanning loading indicator when the query is loading and we - // don't have data (e.g. for the initial fetch). - // Subsequent fetches (e.g. for pagination) will show a small loading indicator on - // top of the table and the table will display the current page until the next page - // is fetched. This prevents a flicker when paginating. - const showFullLoading = loading && !hasAlerts; - - const nonDeletedEvents = useMemo( - () => events.filter((e) => !deletedEventIds.includes(e._id)), - [deletedEventIds, events] - ); - - const alignItems = tableView === 'gridView' ? 'baseline' : 'center'; - - useEffect(() => { - setQuery(inspect, loading, refetch); - }, [inspect, loading, refetch, setQuery]); - const tableContext = useMemo(() => ({ tableId: id }), [id]); - - // Clear checkbox selection when new events are fetched - useEffect(() => { - dispatch(tGridActions.clearSelected({ id })); - dispatch( - tGridActions.setTGridSelectAll({ - id, - selectAll: false, - }) - ); - }, [nonDeletedEvents, dispatch, id]); - - return ( - - - {showFullLoading && } - - {graphOverlay} - - {canQueryTimeline && ( - - - - - - - - {!resolverIsShowing(graphEventId) && additionalFilters} - - {tGridEventRenderedViewEnabled && - ['alerts-page', 'alerts-rules-details-page'].includes(id) && ( - - - - )} - - <> - {!hasAlerts && !loading && !graphOverlay && } - {hasAlerts && ( - - - - - - )} - - - - )} - - - ); -}; - -export const TGridIntegrated = React.memo(TGridIntegratedComponent); diff --git a/x-pack/plugins/timelines/public/components/t_grid/shared/index.tsx b/x-pack/plugins/timelines/public/components/t_grid/shared/index.tsx deleted file mode 100644 index 5758531c7ea97..0000000000000 --- a/x-pack/plugins/timelines/public/components/t_grid/shared/index.tsx +++ /dev/null @@ -1,92 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React, { createContext } from 'react'; -import { - EuiPanel, - EuiFlexGroup, - EuiFlexItem, - EuiLoadingSpinner, - EuiImage, - EuiText, - EuiTitle, -} from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n-react'; -import { useKibana } from '@kbn/kibana-react-plugin/public'; -import type { CoreStart } from '@kbn/core/public'; - -const heights = { - tall: 490, - short: 250, -}; - -export const TableContext = createContext<{ tableId: string | null }>({ tableId: null }); - -export const TGridLoading: React.FC<{ height?: keyof typeof heights }> = ({ height = 'tall' }) => { - return ( - - - - - - - - ); -}; - -const panelStyle = { - maxWidth: 500, -}; - -export const TGridEmpty: React.FC<{ height?: keyof typeof heights }> = ({ height = 'tall' }) => { - const { http } = useKibana().services; - - return ( - - - - - - - - -

    - -

    -
    -

    - -

    -
    -
    - - - -
    -
    -
    -
    -
    - ); -}; diff --git a/x-pack/plugins/timelines/public/components/t_grid/styles.tsx b/x-pack/plugins/timelines/public/components/t_grid/styles.tsx deleted file mode 100644 index fc683e31bd3ea..0000000000000 --- a/x-pack/plugins/timelines/public/components/t_grid/styles.tsx +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner } from '@elastic/eui'; -import styled from 'styled-components'; -import type { ViewSelection } from './event_rendered_view/selector'; - -export const SELECTOR_TIMELINE_GLOBAL_CONTAINER = 'securitySolutionTimeline__container'; -export const EVENTS_TABLE_CLASS_NAME = 'siemEventsTable'; - -/* EVENTS BODY */ - -export const EventsTrSupplement = styled.div.attrs(({ className = '' }) => ({ - className: `siemEventsTable__trSupplement ${className}` as string, -}))<{ className: string }>` - font-size: ${({ theme }) => theme.eui.euiFontSizeXS}; - line-height: ${({ theme }) => theme.eui.euiLineHeight}; - padding-left: ${({ theme }) => theme.eui.euiSizeM}; - .euiAccordion + div { - background-color: ${({ theme }) => theme.eui.euiColorEmptyShade}; - padding: 0 ${({ theme }) => theme.eui.euiSizeS}; - border: 1px solid ${({ theme }) => theme.eui.euiColorLightShade}; - border-radius: ${({ theme }) => theme.eui.euiSizeXS}; - } -`; - -/** - * EVENTS LOADING - */ - -export const EventsLoading = styled(EuiLoadingSpinner)` - margin: 0 2px; - vertical-align: middle; -`; - -export const FullWidthFlexGroup = styled(EuiFlexGroup)<{ $visible?: boolean }>` - overflow: hidden; - margin: 0; - min-height: 490px; - display: ${({ $visible = true }) => ($visible ? 'flex' : 'none')}; -`; - -export const UpdatedFlexGroup = styled(EuiFlexGroup)<{ $view?: ViewSelection }>` - ${({ $view, theme }) => ($view === 'gridView' ? `margin-right: ${theme.eui.euiSizeXL};` : '')} - position: absolute; - z-index: ${({ theme }) => theme.eui.euiZLevel1 - 3}; - right: 0px; -`; - -export const UpdatedFlexItem = styled(EuiFlexItem)<{ $show: boolean }>` - ${({ $show }) => ($show ? '' : 'visibility: hidden;')} -`; - -export const AlertCount = styled.span` - font-size: ${({ theme }) => theme.eui.euiFontSizeXS}; - font-weight: ${({ theme }) => theme.eui.euiFontWeightSemiBold}; - border-right: ${({ theme }) => theme.eui.euiBorderThin}; - margin-right: ${({ theme }) => theme.eui.euiSizeS}; - padding-right: ${({ theme }) => theme.eui.euiSizeM}; -`; diff --git a/x-pack/plugins/timelines/public/components/t_grid/subtitle/__snapshots__/index.test.tsx.snap b/x-pack/plugins/timelines/public/components/t_grid/subtitle/__snapshots__/index.test.tsx.snap deleted file mode 100644 index 1c6ff628df1e6..0000000000000 --- a/x-pack/plugins/timelines/public/components/t_grid/subtitle/__snapshots__/index.test.tsx.snap +++ /dev/null @@ -1,11 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Subtitle it renders 1`] = ` - - - Test subtitle - - -`; diff --git a/x-pack/plugins/timelines/public/components/t_grid/subtitle/index.test.tsx b/x-pack/plugins/timelines/public/components/t_grid/subtitle/index.test.tsx deleted file mode 100644 index 00084cbe71c28..0000000000000 --- a/x-pack/plugins/timelines/public/components/t_grid/subtitle/index.test.tsx +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { mount, shallow } from 'enzyme'; -import React from 'react'; - -import { TestProviders } from '../../../mock'; - -import { Subtitle } from '.'; - -describe('Subtitle', () => { - test('it renders', () => { - const wrapper = shallow(); - - expect(wrapper).toMatchSnapshot(); - }); - - test('it renders one subtitle string item', () => { - const wrapper = mount( - - - - ); - - expect(wrapper.find('.siemSubtitle__item--text').length).toEqual(1); - }); - - test('it renders multiple subtitle string items', () => { - const wrapper = mount( - - - - ); - - expect(wrapper.find('.siemSubtitle__item--text').length).toEqual(2); - }); - - test('it renders one subtitle React.ReactNode item', () => { - const wrapper = mount( - - {'Test subtitle'}
    } /> - - ); - - expect(wrapper.find('.siemSubtitle__item--node').length).toEqual(1); - }); - - test('it renders multiple subtitle React.ReactNode items', () => { - const wrapper = mount( - - {'Test subtitle 1'}
    , {'Test subtitle 2'}]} /> - - ); - - expect(wrapper.find('.siemSubtitle__item--node').length).toEqual(2); - }); - - test('it renders multiple subtitle items of mixed type', () => { - const wrapper = mount( - - {'Test subtitle 2'}]} /> - - ); - - expect(wrapper.find('.siemSubtitle__item').length).toEqual(2); - }); -}); diff --git a/x-pack/plugins/timelines/public/components/t_grid/subtitle/index.tsx b/x-pack/plugins/timelines/public/components/t_grid/subtitle/index.tsx deleted file mode 100644 index c2f3d7d096b5c..0000000000000 --- a/x-pack/plugins/timelines/public/components/t_grid/subtitle/index.tsx +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import styled, { css } from 'styled-components'; - -const Wrapper = styled.div` - ${({ theme }) => css` - margin-top: ${theme.eui.euiSizeS}; - - .siemSubtitle__item { - color: ${theme.eui.euiTextSubduedColor}; - font-size: ${theme.eui.euiFontSizeXS}; - line-height: ${theme.eui.euiLineHeight}; - - @media only screen and (min-width: ${theme.eui.euiBreakpoints.s}) { - display: inline-block; - margin-right: ${theme.eui.euiSize}; - - &:last-child { - margin-right: 0; - } - } - } - `} -`; -Wrapper.displayName = 'Wrapper'; - -interface SubtitleItemProps { - children: string | React.ReactNode; - dataTestSubj?: string; -} - -const SubtitleItem = React.memo( - ({ children, dataTestSubj = 'header-panel-subtitle' }) => { - if (typeof children === 'string') { - return ( -

    - {children} -

    - ); - } else { - return ( -
    - {children} -
    - ); - } - } -); -SubtitleItem.displayName = 'SubtitleItem'; - -export interface SubtitleProps { - items: string | React.ReactNode | Array; -} - -export const Subtitle = React.memo(({ items }) => { - return ( - - {Array.isArray(items) ? ( - items.map((item, i) => {item}) - ) : ( - {items} - )} - - ); -}); -Subtitle.displayName = 'Subtitle'; diff --git a/x-pack/plugins/timelines/public/components/t_grid/toolbar/bulk_actions/index.tsx b/x-pack/plugins/timelines/public/components/t_grid/toolbar/bulk_actions/index.tsx deleted file mode 100644 index 94b197bbe2d05..0000000000000 --- a/x-pack/plugins/timelines/public/components/t_grid/toolbar/bulk_actions/index.tsx +++ /dev/null @@ -1,139 +0,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 { EuiPopover, EuiButtonEmpty, EuiContextMenuPanel } from '@elastic/eui'; -import numeral from '@elastic/numeral'; -import React, { useState, useCallback, useMemo } from 'react'; -import styled from 'styled-components'; -import { useUiSetting$ } from '@kbn/kibana-react-plugin/public'; -import { DEFAULT_NUMBER_FORMAT } from '../../../../../common/constants'; -import * as i18n from './translations'; - -interface BulkActionsProps { - totalItems: number; - selectedCount: number; - showClearSelection: boolean; - onSelectAll: () => void; - onClearSelection: () => void; - bulkActionItems?: JSX.Element[]; -} - -const BulkActionsContainer = styled.div` - display: inline-block; - position: relative; -`; - -BulkActionsContainer.displayName = 'BulkActionsContainer'; - -/** - * Stateless component integrating the bulk actions menu and the select all button - */ -const BulkActionsComponent: React.FC = ({ - selectedCount, - totalItems, - showClearSelection, - onSelectAll, - onClearSelection, - bulkActionItems, -}) => { - const [isActionsPopoverOpen, setIsActionsPopoverOpen] = useState(false); - const [defaultNumberFormat] = useUiSetting$(DEFAULT_NUMBER_FORMAT); - - const formattedTotalCount = useMemo( - () => numeral(totalItems).format(defaultNumberFormat), - [defaultNumberFormat, totalItems] - ); - const formattedSelectedEventsCount = useMemo( - () => numeral(selectedCount).format(defaultNumberFormat), - [defaultNumberFormat, selectedCount] - ); - - const toggleIsActionOpen = useCallback(() => { - setIsActionsPopoverOpen((currentIsOpen) => !currentIsOpen); - }, [setIsActionsPopoverOpen]); - - const closeActionPopover = useCallback(() => { - setIsActionsPopoverOpen(false); - }, [setIsActionsPopoverOpen]); - - const closeIfPopoverIsOpen = useCallback(() => { - if (isActionsPopoverOpen) { - setIsActionsPopoverOpen(false); - } - }, [isActionsPopoverOpen]); - - const toggleSelectAll = useCallback(() => { - if (!showClearSelection) { - onSelectAll(); - } else { - onClearSelection(); - } - }, [onClearSelection, onSelectAll, showClearSelection]); - - const selectedAlertsText = useMemo( - () => - showClearSelection - ? i18n.SELECTED_ALERTS(formattedTotalCount, totalItems) - : i18n.SELECTED_ALERTS(formattedSelectedEventsCount, selectedCount), - [ - showClearSelection, - formattedTotalCount, - formattedSelectedEventsCount, - totalItems, - selectedCount, - ] - ); - - const selectClearAllAlertsText = useMemo( - () => - showClearSelection - ? i18n.CLEAR_SELECTION - : i18n.SELECT_ALL_ALERTS(formattedTotalCount, totalItems), - [showClearSelection, formattedTotalCount, totalItems] - ); - - return ( - - - {selectedAlertsText} - - } - closePopover={closeActionPopover} - > - - - - - {selectClearAllAlertsText} - - - ); -}; - -export const BulkActions = React.memo(BulkActionsComponent); diff --git a/x-pack/plugins/timelines/public/components/t_grid/toolbar/bulk_actions/translations.ts b/x-pack/plugins/timelines/public/components/t_grid/toolbar/bulk_actions/translations.ts deleted file mode 100644 index 835dce763c0a8..0000000000000 --- a/x-pack/plugins/timelines/public/components/t_grid/toolbar/bulk_actions/translations.ts +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { i18n } from '@kbn/i18n'; - -export const SELECTED_ALERTS = (selectedAlertsFormatted: string, selectedAlerts: number) => - i18n.translate('xpack.timelines.toolbar.bulkActions.selectedAlertsTitle', { - values: { selectedAlertsFormatted, selectedAlerts }, - defaultMessage: - 'Selected {selectedAlertsFormatted} {selectedAlerts, plural, =1 {alert} other {alerts}}', - }); - -export const SELECT_ALL_ALERTS = (totalAlertsFormatted: string, totalAlerts: number) => - i18n.translate('xpack.timelines.toolbar.bulkActions.selectAllAlertsTitle', { - values: { totalAlertsFormatted, totalAlerts }, - defaultMessage: - 'Select all {totalAlertsFormatted} {totalAlerts, plural, =1 {alert} other {alerts}}', - }); - -export const CLEAR_SELECTION = i18n.translate( - 'xpack.timelines.toolbar.bulkActions.clearSelectionTitle', - { - defaultMessage: 'Clear selection', - } -); diff --git a/x-pack/plugins/timelines/public/components/t_grid/translations.ts b/x-pack/plugins/timelines/public/components/t_grid/translations.ts deleted file mode 100644 index f0ae8e0fc1e91..0000000000000 --- a/x-pack/plugins/timelines/public/components/t_grid/translations.ts +++ /dev/null @@ -1,118 +0,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 { i18n } from '@kbn/i18n'; - -export const EVENTS_TABLE_ARIA_LABEL = ({ - activePage, - totalPages, -}: { - activePage: number; - totalPages: number; -}) => - i18n.translate('xpack.timelines.timeline.eventsTableAriaLabel', { - values: { activePage, totalPages }, - defaultMessage: 'events; Page {activePage} of {totalPages}', - }); - -export const BULK_ACTION_OPEN_SELECTED = i18n.translate( - 'xpack.timelines.timeline.openSelectedTitle', - { - defaultMessage: 'Mark as open', - } -); - -export const BULK_ACTION_CLOSE_SELECTED = i18n.translate( - 'xpack.timelines.timeline.closeSelectedTitle', - { - defaultMessage: 'Mark as closed', - } -); - -export const BULK_ACTION_ACKNOWLEDGED_SELECTED = i18n.translate( - 'xpack.timelines.timeline.acknowledgedSelectedTitle', - { - defaultMessage: 'Mark as acknowledged', - } -); - -export const BULK_ACTION_FAILED_SINGLE_ALERT = i18n.translate( - 'xpack.timelines.timeline.updateAlertStatusFailedSingleAlert', - { - defaultMessage: 'Failed to update alert because it was already being modified.', - } -); - -export const BULK_ACTION_ATTACH_NEW_CASE = i18n.translate( - 'xpack.timelines.timeline.attachNewCase', - { - defaultMessage: 'Attach to new case', - } -); - -export const BULK_ACTION_ATTACH_EXISTING_CASE = i18n.translate( - 'xpack.timelines.timeline.attachExistingCase', - { - defaultMessage: 'Attach to existing case', - } -); - -export const CLOSED_ALERT_SUCCESS_TOAST = (totalAlerts: number) => - i18n.translate('xpack.timelines.timeline.closedAlertSuccessToastMessage', { - values: { totalAlerts }, - defaultMessage: - 'Successfully closed {totalAlerts} {totalAlerts, plural, =1 {alert} other {alerts}}.', - }); - -export const OPENED_ALERT_SUCCESS_TOAST = (totalAlerts: number) => - i18n.translate('xpack.timelines.timeline.openedAlertSuccessToastMessage', { - values: { totalAlerts }, - defaultMessage: - 'Successfully opened {totalAlerts} {totalAlerts, plural, =1 {alert} other {alerts}}.', - }); - -export const ACKNOWLEDGED_ALERT_SUCCESS_TOAST = (totalAlerts: number) => - i18n.translate('xpack.timelines.timeline.acknowledgedAlertSuccessToastMessage', { - values: { totalAlerts }, - defaultMessage: - 'Successfully marked {totalAlerts} {totalAlerts, plural, =1 {alert} other {alerts}} as acknowledged.', - }); - -export const CLOSED_ALERT_FAILED_TOAST = i18n.translate( - 'xpack.timelines.timeline.closedAlertFailedToastMessage', - { - defaultMessage: 'Failed to close alert(s).', - } -); - -export const OPENED_ALERT_FAILED_TOAST = i18n.translate( - 'xpack.timelines.timeline.openedAlertFailedToastMessage', - { - defaultMessage: 'Failed to open alert(s)', - } -); - -export const ACKNOWLEDGED_ALERT_FAILED_TOAST = i18n.translate( - 'xpack.timelines.timeline.acknowledgedAlertFailedToastMessage', - { - defaultMessage: 'Failed to mark alert(s) as acknowledged', - } -); - -export const UPDATE_ALERT_STATUS_FAILED = (conflicts: number) => - i18n.translate('xpack.timelines.timeline.updateAlertStatusFailed', { - values: { conflicts }, - defaultMessage: - 'Failed to update { conflicts } {conflicts, plural, =1 {alert} other {alerts}}.', - }); - -export const UPDATE_ALERT_STATUS_FAILED_DETAILED = (updated: number, conflicts: number) => - i18n.translate('xpack.timelines.timeline.updateAlertStatusFailedDetailed', { - values: { updated, conflicts }, - defaultMessage: `{ updated } {updated, plural, =1 {alert was} other {alerts were}} updated successfully, but { conflicts } failed to update - because { conflicts, plural, =1 {it was} other {they were}} already being modified.`, - }); diff --git a/x-pack/plugins/timelines/public/components/t_grid/types.ts b/x-pack/plugins/timelines/public/components/t_grid/types.ts deleted file mode 100644 index 0d7d307b8b05f..0000000000000 --- a/x-pack/plugins/timelines/public/components/t_grid/types.ts +++ /dev/null @@ -1,8 +0,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. - */ - -export type { OnChangePage, OnRowSelected, OnSelectAll } from '../../../common/types/timeline'; diff --git a/x-pack/plugins/timelines/public/components/truncatable_text/__snapshots__/index.test.tsx.snap b/x-pack/plugins/timelines/public/components/truncatable_text/__snapshots__/index.test.tsx.snap deleted file mode 100644 index 23b930c7a114b..0000000000000 --- a/x-pack/plugins/timelines/public/components/truncatable_text/__snapshots__/index.test.tsx.snap +++ /dev/null @@ -1,19 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`TruncatableText renders correctly against snapshot 1`] = ` -.c0, -.c0 * { - display: inline-block; - max-width: 100%; - overflow: hidden; - text-overflow: ellipsis; - vertical-align: top; - white-space: nowrap; -} - - - Hiding in plain sight - -`; diff --git a/x-pack/plugins/timelines/public/components/truncatable_text/index.test.tsx b/x-pack/plugins/timelines/public/components/truncatable_text/index.test.tsx deleted file mode 100644 index f54d9e4ed0b88..0000000000000 --- a/x-pack/plugins/timelines/public/components/truncatable_text/index.test.tsx +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { mount, shallow } from 'enzyme'; -import React from 'react'; - -import { TruncatableText } from '.'; - -describe('TruncatableText', () => { - test('renders correctly against snapshot', () => { - const wrapper = shallow({'Hiding in plain sight'}); - expect(wrapper).toMatchSnapshot(); - }); - - test('it adds the hidden overflow style', () => { - const wrapper = mount({'Hiding in plain sight'}); - - expect(wrapper).toHaveStyleRule('overflow', 'hidden'); - }); - - test('it adds the ellipsis text-overflow style', () => { - const wrapper = mount({'Dramatic pause'}); - - expect(wrapper).toHaveStyleRule('text-overflow', 'ellipsis'); - }); - - test('it adds the nowrap white-space style', () => { - const wrapper = mount({'Who stopped the beats?'}); - - expect(wrapper).toHaveStyleRule('white-space', 'nowrap'); - }); -}); diff --git a/x-pack/plugins/timelines/public/components/truncatable_text/index.tsx b/x-pack/plugins/timelines/public/components/truncatable_text/index.tsx deleted file mode 100644 index 2dd3c35f731e9..0000000000000 --- a/x-pack/plugins/timelines/public/components/truncatable_text/index.tsx +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import styled from 'styled-components'; - -/** - * Applies CSS styling to enable text to be truncated with an ellipsis. - * Example: "Don't leave me hanging..." - * - * Note: Requires a parent container with a defined width or max-width. - */ - -export const TruncatableText = styled.span` - &, - & * { - display: inline-block; - max-width: 100%; - overflow: hidden; - text-overflow: ellipsis; - vertical-align: top; - white-space: nowrap; - } -`; -TruncatableText.displayName = 'TruncatableText'; diff --git a/x-pack/plugins/timelines/public/components/utils/helpers.ts b/x-pack/plugins/timelines/public/components/utils/helpers.ts deleted file mode 100644 index 64bc328c97b05..0000000000000 --- a/x-pack/plugins/timelines/public/components/utils/helpers.ts +++ /dev/null @@ -1,81 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { getOr, isEmpty, uniqBy } from 'lodash/fp'; -import type { BrowserField, BrowserFields } from '../../../common/search_strategy'; -import { ColumnHeaderOptions } from '../../../common/types'; -import { defaultHeaders } from '../t_grid/body/column_headers/default_headers'; -import { DEFAULT_COLUMN_MIN_WIDTH } from '../t_grid/body/constants'; - -export const getColumnHeaderFromBrowserField = ({ - browserField, - width = DEFAULT_COLUMN_MIN_WIDTH, -}: { - browserField: Partial; - width?: number; -}): ColumnHeaderOptions => ({ - category: browserField.category, - columnHeaderType: 'not-filtered', - description: browserField.description != null ? browserField.description : undefined, - example: browserField.example != null ? `${browserField.example}` : undefined, - id: browserField.name || '', - type: browserField.type, - aggregatable: browserField.aggregatable, - initialWidth: width, -}); - -/** - * Returns a collection of columns, where the first column in the collection - * is a timestamp, and the remaining columns are all the columns in the - * specified category - */ -export const getColumnsWithTimestamp = ({ - browserFields, - category, -}: { - browserFields: BrowserFields; - category: string; -}): ColumnHeaderOptions[] => { - const emptyFields: Record> = {}; - const timestamp = defaultHeaders.find(({ id }) => id === '@timestamp'); - const categoryFields: Array> = [ - ...Object.values(getOr(emptyFields, `${category}.fields`, browserFields)), - ]; - - return timestamp != null - ? uniqBy('id', [ - timestamp, - ...categoryFields.map((f) => getColumnHeaderFromBrowserField({ browserField: f })), - ]) - : []; -}; - -export const getIconFromType = (type: string | null | undefined) => { - switch (type) { - case 'string': // fall through - case 'keyword': - return 'string'; - case 'number': // fall through - case 'long': - return 'number'; - case 'date': - return 'clock'; - case 'ip': - case 'geo_point': - return 'globe'; - case 'object': - return 'questionInCircle'; - case 'float': - return 'number'; - default: - return 'questionInCircle'; - } -}; - -/** Returns example text, or an empty string if the field does not have an example */ -export const getExampleText = (example: string | number | null | undefined): string => - !isEmpty(example) ? `Example: ${example}` : ''; diff --git a/x-pack/plugins/timelines/public/components/utils/keury/index.test.ts b/x-pack/plugins/timelines/public/components/utils/keury/index.test.ts deleted file mode 100644 index 5f5147999b872..0000000000000 --- a/x-pack/plugins/timelines/public/components/utils/keury/index.test.ts +++ /dev/null @@ -1,335 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import expect from '@kbn/expect'; -import { convertToBuildEsQuery, escapeKuery } from '.'; -import { mockIndexPattern } from '../../../mock/index_pattern'; - -describe('Kuery escape', () => { - it('should not remove white spaces quotes', () => { - const value = ' netcat'; - const expected = ' netcat'; - expect(escapeKuery(value)).to.be(expected); - }); - - it('should escape quotes', () => { - const value = 'I said, "Hello."'; - const expected = 'I said, \\"Hello.\\"'; - expect(escapeKuery(value)).to.be(expected); - }); - - it('should escape special characters', () => { - const value = `This \\ has (a lot of) characters, don't you *think*? "Yes."`; - const expected = `This \\ has (a lot of) characters, don't you *think*? \\"Yes.\\"`; - expect(escapeKuery(value)).to.be(expected); - }); - - it('should NOT escape keywords', () => { - const value = 'foo and bar or baz not qux'; - const expected = 'foo and bar or baz not qux'; - expect(escapeKuery(value)).to.be(expected); - }); - - it('should NOT escape keywords next to each other', () => { - const value = 'foo and bar or not baz'; - const expected = 'foo and bar or not baz'; - expect(escapeKuery(value)).to.be(expected); - }); - - it('should not escape keywords without surrounding spaces', () => { - const value = 'And this has keywords, or does it not?'; - const expected = 'And this has keywords, or does it not?'; - expect(escapeKuery(value)).to.be(expected); - }); - - it('should NOT escape uppercase keywords', () => { - const value = 'foo AND bar'; - const expected = 'foo AND bar'; - expect(escapeKuery(value)).to.be(expected); - }); - - it('should escape special characters and NOT keywords', () => { - const value = 'Hello, "world", and to meet you!'; - const expected = 'Hello, \\"world\\", and to meet you!'; - expect(escapeKuery(value)).to.be(expected); - }); - - it('should escape newlines and tabs', () => { - const value = 'This\nhas\tnewlines\r\nwith\ttabs'; - const expected = 'This\\nhas\\tnewlines\\r\\nwith\\ttabs'; - expect(escapeKuery(value)).to.be(expected); - }); -}); - -describe('convertToBuildEsQuery', () => { - /** - * All the fields in this query, except for `@timestamp`, - * are nested fields https://www.elastic.co/guide/en/elasticsearch/reference/current/nested.html - * - * This mix of nested and non-nested fields will be used to verify that: - * ✅ Nested fields are converted to use the `nested` query syntax - * ✅ The `nested` query syntax includes the `ignore_unmapped` option - * ✅ Non-nested fields are NOT converted to the `nested` query syntax - * ✅ Non-nested fields do NOT include the `ignore_unmapped` option - */ - const queryWithNestedFields = [ - { - query: - '((threat.enrichments: { matched.atomic: a4f87cbcd2a4241da77b6bf0c5d9e8553fec991f } and threat.enrichments: { matched.type: indicator_match_rule } and threat.enrichments: { matched.field: file.hash.md5 }) and (@timestamp : *))', - language: 'kuery', - }, - ]; - - /** A search bar filter (displayed below the KQL / Lucene search bar ) */ - const filters = [ - { - meta: { - alias: null, - negate: false, - disabled: false, - type: 'exists', - key: '_id', - value: 'exists', - }, - query: { - exists: { - field: '_id', - }, - }, - }, - ]; - - const config = { - allowLeadingWildcards: true, - queryStringOptions: { - analyze_wildcard: true, - }, - ignoreFilterIfFieldNotInIndex: false, - dateFormatTZ: 'Browser', - }; - - it('should, by default, build a query where the `nested` fields syntax includes the `"ignore_unmapped":true` option', () => { - const [converted, _] = convertToBuildEsQuery({ - config, - queries: queryWithNestedFields, - indexPattern: mockIndexPattern, - filters, - }); - - expect(JSON.parse(converted ?? '')).to.eql({ - bool: { - must: [], - filter: [ - { - bool: { - filter: [ - { - bool: { - filter: [ - { - // ✅ Nested fields are converted to use the `nested` query syntax - nested: { - path: 'threat.enrichments', - query: { - bool: { - should: [ - { - match: { - 'threat.enrichments.matched.atomic': - 'a4f87cbcd2a4241da77b6bf0c5d9e8553fec991f', - }, - }, - ], - minimum_should_match: 1, - }, - }, - score_mode: 'none', - // ✅ The `nested` query syntax includes the `ignore_unmapped` option - ignore_unmapped: true, - }, - }, - { - nested: { - path: 'threat.enrichments', - query: { - bool: { - should: [ - { - match: { - 'threat.enrichments.matched.type': 'indicator_match_rule', - }, - }, - ], - minimum_should_match: 1, - }, - }, - score_mode: 'none', - ignore_unmapped: true, - }, - }, - { - nested: { - path: 'threat.enrichments', - query: { - bool: { - should: [ - { - match: { - 'threat.enrichments.matched.field': 'file.hash.md5', - }, - }, - ], - minimum_should_match: 1, - }, - }, - score_mode: 'none', - ignore_unmapped: true, - }, - }, - ], - }, - }, - { - bool: { - should: [ - { - exists: { - // ✅ Non-nested fields are NOT converted to the `nested` query syntax - // ✅ Non-nested fields do NOT include the `ignore_unmapped` option - field: '@timestamp', - }, - }, - ], - minimum_should_match: 1, - }, - }, - ], - }, - }, - { - exists: { - field: '_id', - }, - }, - ], - should: [], - must_not: [], - }, - }); - }); - - it('should, when the default is overridden, build a query where `nested` fields include the `"ignore_unmapped":false` option', () => { - const configWithOverride = { - ...config, - nestedIgnoreUnmapped: false, // <-- override the default - }; - - const [converted, _] = convertToBuildEsQuery({ - config: configWithOverride, - queries: queryWithNestedFields, - indexPattern: mockIndexPattern, - filters, - }); - - expect(JSON.parse(converted ?? '')).to.eql({ - bool: { - must: [], - filter: [ - { - bool: { - filter: [ - { - bool: { - filter: [ - { - nested: { - path: 'threat.enrichments', - query: { - bool: { - should: [ - { - match: { - 'threat.enrichments.matched.atomic': - 'a4f87cbcd2a4241da77b6bf0c5d9e8553fec991f', - }, - }, - ], - minimum_should_match: 1, - }, - }, - score_mode: 'none', - ignore_unmapped: false, // <-- overridden by the config to be false - }, - }, - { - nested: { - path: 'threat.enrichments', - query: { - bool: { - should: [ - { - match: { - 'threat.enrichments.matched.type': 'indicator_match_rule', - }, - }, - ], - minimum_should_match: 1, - }, - }, - score_mode: 'none', - ignore_unmapped: false, - }, - }, - { - nested: { - path: 'threat.enrichments', - query: { - bool: { - should: [ - { - match: { - 'threat.enrichments.matched.field': 'file.hash.md5', - }, - }, - ], - minimum_should_match: 1, - }, - }, - score_mode: 'none', - ignore_unmapped: false, - }, - }, - ], - }, - }, - { - bool: { - should: [ - { - exists: { - field: '@timestamp', - }, - }, - ], - minimum_should_match: 1, - }, - }, - ], - }, - }, - { - exists: { - field: '_id', - }, - }, - ], - should: [], - must_not: [], - }, - }); - }); -}); diff --git a/x-pack/plugins/timelines/public/components/utils/keury/index.ts b/x-pack/plugins/timelines/public/components/utils/keury/index.ts deleted file mode 100644 index 8f03c2f9ef20e..0000000000000 --- a/x-pack/plugins/timelines/public/components/utils/keury/index.ts +++ /dev/null @@ -1,97 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { isEmpty, isString, flow } from 'lodash/fp'; -import { - buildEsQuery, - EsQueryConfig, - Filter, - fromKueryExpression, - DataViewBase, - Query, - toElasticsearchQuery, -} from '@kbn/es-query'; - -export const convertKueryToElasticSearchQuery = ( - kueryExpression: string, - indexPattern?: DataViewBase -) => { - try { - return kueryExpression - ? JSON.stringify(toElasticsearchQuery(fromKueryExpression(kueryExpression), indexPattern)) - : ''; - } catch (err) { - return ''; - } -}; - -export const convertKueryToDslFilter = (kueryExpression: string, indexPattern: DataViewBase) => { - try { - return kueryExpression - ? toElasticsearchQuery(fromKueryExpression(kueryExpression), indexPattern) - : {}; - } catch (err) { - return {}; - } -}; - -export const escapeQueryValue = (val: number | string = ''): string | number => { - if (isString(val)) { - if (isEmpty(val)) { - return '""'; - } - return `"${escapeKuery(val)}"`; - } - - return val; -}; - -const escapeWhitespace = (val: string) => - val.replace(/\t/g, '\\t').replace(/\r/g, '\\r').replace(/\n/g, '\\n'); - -// See the SpecialCharacter rule in kuery.peg -const escapeSpecialCharacters = (val: string) => val.replace(/["]/g, '\\$&'); // $& means the whole matched string - -// See the Keyword rule in kuery.peg -// I do not think that we need that anymore since we are doing a full match_phrase all the time now => return `"${escapeKuery(val)}"`; -// const escapeAndOr = (val: string) => val.replace(/(\s+)(and|or)(\s+)/gi, '$1\\$2$3'); - -// const escapeNot = (val: string) => val.replace(/not(\s+)/gi, '\\$&'); - -export const escapeKuery = flow(escapeSpecialCharacters, escapeWhitespace); - -export const convertToBuildEsQuery = ({ - config, - indexPattern, - queries, - filters, -}: { - config: EsQueryConfig; - indexPattern: DataViewBase | undefined; - queries: Query[]; - filters: Filter[]; -}): [string, undefined] | [undefined, Error] => { - try { - return [ - JSON.stringify( - buildEsQuery( - indexPattern, - queries, - filters.filter((f) => f.meta.disabled === false), - { - nestedIgnoreUnmapped: true, // by default, prevent shard failures when unmapped `nested` fields are queried: https://github.com/elastic/kibana/issues/130340 - ...config, - dateFormatTZ: undefined, - } - ) - ), - undefined, - ]; - } catch (error) { - return [undefined, error]; - } -}; diff --git a/x-pack/plugins/timelines/public/components/utils/use_mount_appended.ts b/x-pack/plugins/timelines/public/components/utils/use_mount_appended.ts deleted file mode 100644 index 6acc21b5e4763..0000000000000 --- a/x-pack/plugins/timelines/public/components/utils/use_mount_appended.ts +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -// eslint-disable-next-line import/no-extraneous-dependencies -import { mount } from 'enzyme'; - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -type WrapperOf any> = (...args: Parameters) => ReturnType; -export type MountAppended = WrapperOf; - -export const useMountAppended = () => { - let root: HTMLElement; - - beforeEach(() => { - root = document.createElement('div'); - root.id = 'root'; - document.body.appendChild(root); - }); - - afterEach(() => { - document.body.removeChild(root); - }); - - const mountAppended: MountAppended = (node, options) => - mount(node, { ...options, attachTo: root }); - - return mountAppended; -}; diff --git a/x-pack/plugins/timelines/public/container/index.tsx b/x-pack/plugins/timelines/public/container/index.tsx deleted file mode 100644 index 8acc00d6d0004..0000000000000 --- a/x-pack/plugins/timelines/public/container/index.tsx +++ /dev/null @@ -1,491 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ -import type { AlertConsumers } from '@kbn/rule-data-utils'; -import deepEqual from 'fast-deep-equal'; -import { isEmpty, isString, noop } from 'lodash/fp'; -import { useCallback, useEffect, useRef, useState } from 'react'; -import { useDispatch } from 'react-redux'; -import { Subscription } from 'rxjs'; -import { MappingRuntimeFields } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; -import type { DataView } from '@kbn/data-views-plugin/public'; -import type { DataPublicPluginStart } from '@kbn/data-plugin/public'; -import { isCompleteResponse, isErrorResponse } from '@kbn/data-plugin/common'; -import { - clearEventsLoading, - clearEventsDeleted, - setTableUpdatedAt, - updateGraphEventId, - updateTotalCount, -} from '../store/t_grid/actions'; -import { - Direction, - TimelineFactoryQueryTypes, - TimelineEventsQueries, - EntityType, -} from '../../common/search_strategy'; -import type { - Inspect, - PaginationInputPaginated, - TimelineStrategyResponseType, - TimelineEdges, - TimelineEventsAllRequestOptions, - TimelineEventsAllStrategyResponse, - TimelineItem, - TimelineRequestSortField, -} from '../../common/search_strategy'; -import type { ESQuery } from '../../common/typed_json'; -import type { KueryFilterQueryKind, AlertStatus } from '../../common/types/timeline'; -import { useAppToasts } from '../hooks/use_app_toasts'; -import { TableId } from '../store/t_grid/types'; -import * as i18n from './translations'; -import { getSearchTransactionName, useStartTransaction } from '../lib/apm/use_start_transaction'; - -export type InspectResponse = Inspect & { response: string[] }; - -export const detectionsTimelineIds = [TableId.alertsOnAlertsPage, TableId.alertsOnRuleDetailsPage]; - -export type Refetch = () => void; - -export interface TimelineArgs { - consumers: Record; - events: TimelineItem[]; - id: string; - inspect: InspectResponse; - loadPage: LoadPage; - pageInfo: Pick; - refetch: Refetch; - totalCount: number; - updatedAt: number; -} - -type OnNextResponseHandler = (response: TimelineArgs) => Promise | void; - -type TimelineEventsSearchHandler = (onNextResponse?: OnNextResponseHandler) => void; - -type LoadPage = (newActivePage: number) => void; - -type TimelineRequest = TimelineEventsAllRequestOptions; - -type TimelineResponse = TimelineEventsAllStrategyResponse; - -export interface UseTimelineEventsProps { - alertConsumers?: AlertConsumers[]; - data?: DataPublicPluginStart; - dataViewId: string | null; - endDate: string; - entityType: EntityType; - excludeEcsData?: boolean; - fields: string[]; - filterQuery?: ESQuery | string; - id: string; - indexNames: string[]; - language?: KueryFilterQueryKind; - limit: number; - runtimeMappings: MappingRuntimeFields; - skip?: boolean; - sort?: TimelineRequestSortField[]; - startDate: string; - timerangeKind?: 'absolute' | 'relative'; - filterStatus?: AlertStatus; -} - -const createFilter = (filterQuery: ESQuery | string | undefined) => - isString(filterQuery) ? filterQuery : JSON.stringify(filterQuery); - -const getTimelineEvents = (timelineEdges: TimelineEdges[]): TimelineItem[] => - timelineEdges.map((e: TimelineEdges) => e.node); - -const getInspectResponse = ( - response: TimelineStrategyResponseType, - prevResponse: InspectResponse -): InspectResponse => ({ - dsl: response?.inspect?.dsl ?? prevResponse?.dsl ?? [], - response: - response != null ? [JSON.stringify(response.rawResponse, null, 2)] : prevResponse?.response, -}); - -const ID = 'timelineEventsQuery'; -export const initSortDefault = [ - { - direction: Direction.desc, - esTypes: ['date'], - field: '@timestamp', - type: 'date', - }, -]; - -const useApmTracking = (timelineId: string) => { - const { startTransaction } = useStartTransaction(); - - const startTracking = useCallback(() => { - // Create the transaction, the managed flag is turned off to prevent it from being polluted by non-related automatic spans. - // The managed flag can be turned on to investigate high latency requests in APM. - // However, note that by enabling the managed flag, the transaction trace may be distorted by other requests information. - const transaction = startTransaction({ - name: getSearchTransactionName(timelineId), - type: 'http-request', - options: { managed: false }, - }); - // Create a blocking span to control the transaction time and prevent it from closing automatically with partial batch responses. - // The blocking span needs to be ended manually when the batched request finishes. - const span = transaction?.startSpan('batched search', 'http-request', { blocking: true }); - return { - endTracking: (result: 'success' | 'error' | 'aborted' | 'invalid') => { - transaction?.addLabels({ result }); - span?.end(); - }, - }; - }, [startTransaction, timelineId]); - - return { startTracking }; -}; - -const NO_CONSUMERS: AlertConsumers[] = []; -export const useTimelineEventsHandler = ({ - alertConsumers = NO_CONSUMERS, - dataViewId, - endDate, - entityType, - excludeEcsData = false, - id = ID, - indexNames, - fields, - filterQuery, - startDate, - language = 'kuery', - limit, - runtimeMappings, - sort = initSortDefault, - skip = false, - data, - filterStatus, -}: UseTimelineEventsProps): [boolean, TimelineArgs, TimelineEventsSearchHandler] => { - const dispatch = useDispatch(); - const { startTracking } = useApmTracking(id); - const refetch = useRef(noop); - const abortCtrl = useRef(new AbortController()); - const searchSubscription$ = useRef(new Subscription()); - const [loading, setLoading] = useState(true); - const [activePage, setActivePage] = useState(0); - const [timelineRequest, setTimelineRequest] = useState | null>( - null - ); - const [prevFilterStatus, setFilterStatus] = useState(filterStatus); - const prevTimelineRequest = useRef | null>(null); - - const clearSignalsState = useCallback(() => { - if (id != null && detectionsTimelineIds.some((timelineId) => timelineId === id)) { - dispatch(clearEventsLoading({ id })); - dispatch(clearEventsDeleted({ id })); - } - }, [dispatch, id]); - - const wrappedLoadPage = useCallback( - (newActivePage: number) => { - clearSignalsState(); - setActivePage(newActivePage); - }, - [clearSignalsState] - ); - - const refetchGrid = useCallback(() => { - if (refetch.current != null) { - refetch.current(); - } - wrappedLoadPage(0); - }, [wrappedLoadPage]); - - const setUpdated = useCallback( - (updatedAt: number) => { - dispatch(setTableUpdatedAt({ id, updated: updatedAt })); - }, - [dispatch, id] - ); - - const setTotalCount = useCallback( - (totalCount: number) => dispatch(updateTotalCount({ id, totalCount })), - [dispatch, id] - ); - - const [timelineResponse, setTimelineResponse] = useState({ - consumers: {}, - id, - inspect: { - dsl: [], - response: [], - }, - refetch: refetchGrid, - totalCount: -1, - pageInfo: { - activePage: 0, - querySize: 0, - }, - events: [], - loadPage: wrappedLoadPage, - updatedAt: 0, - }); - const { addWarning } = useAppToasts(); - - const timelineSearch = useCallback( - (request: TimelineRequest | null, onNextHandler?: OnNextResponseHandler) => { - if (request == null || skip) { - return; - } - - const asyncSearch = async () => { - prevTimelineRequest.current = request; - abortCtrl.current = new AbortController(); - setLoading(true); - if (data && data.search) { - const { endTracking } = startTracking(); - const abortSignal = abortCtrl.current.signal; - - searchSubscription$.current = data.search - .search, TimelineResponse>( - { ...request, entityType }, - { - strategy: - request.language === 'eql' - ? 'timelineEqlSearchStrategy' - : 'timelineSearchStrategy', - abortSignal, - // we only need the id to throw better errors - indexPattern: { id: dataViewId } as unknown as DataView, - } - ) - .subscribe({ - next: (response) => { - if (isCompleteResponse(response)) { - endTracking('success'); - setTimelineResponse((prevResponse) => { - const newTimelineResponse = { - ...prevResponse, - consumers: response.consumers, - events: getTimelineEvents(response.edges), - inspect: getInspectResponse(response, prevResponse.inspect), - pageInfo: response.pageInfo, - totalCount: response.totalCount, - updatedAt: Date.now(), - }; - setUpdated(newTimelineResponse.updatedAt); - setTotalCount(newTimelineResponse.totalCount); - if (onNextHandler) onNextHandler(newTimelineResponse); - return newTimelineResponse; - }); - if (prevFilterStatus !== request.filterStatus) { - dispatch(updateGraphEventId({ id, graphEventId: '' })); - } - setFilterStatus(request.filterStatus); - setLoading(false); - - searchSubscription$.current.unsubscribe(); - } else if (isErrorResponse(response)) { - endTracking('invalid'); - setLoading(false); - addWarning(i18n.ERROR_TIMELINE_EVENTS); - searchSubscription$.current.unsubscribe(); - } - }, - error: (msg) => { - endTracking(abortSignal.aborted ? 'aborted' : 'error'); - setLoading(false); - data.search.showError(msg); - searchSubscription$.current.unsubscribe(); - }, - }); - } - }; - - searchSubscription$.current.unsubscribe(); - abortCtrl.current.abort(); - asyncSearch(); - refetch.current = asyncSearch; - }, - [ - skip, - data, - setTotalCount, - entityType, - dataViewId, - setUpdated, - addWarning, - startTracking, - dispatch, - id, - prevFilterStatus, - ] - ); - - useEffect(() => { - if (indexNames.length === 0) { - return; - } - - setTimelineRequest((prevRequest) => { - const prevSearchParameters = { - defaultIndex: prevRequest?.defaultIndex ?? [], - filterQuery: prevRequest?.filterQuery ?? '', - querySize: prevRequest?.pagination.querySize ?? 0, - sort: prevRequest?.sort ?? initSortDefault, - timerange: prevRequest?.timerange ?? {}, - runtimeMappings: prevRequest?.runtimeMappings ?? {}, - filterStatus: prevRequest?.filterStatus, - }; - - const currentSearchParameters = { - defaultIndex: indexNames, - filterQuery: createFilter(filterQuery), - querySize: limit, - sort, - runtimeMappings, - timerange: { - interval: '12h', - from: startDate, - to: endDate, - }, - filterStatus, - }; - - const newActivePage = deepEqual(prevSearchParameters, currentSearchParameters) - ? activePage - : 0; - - const currentRequest = { - alertConsumers, - defaultIndex: indexNames, - excludeEcsData, - factoryQueryType: TimelineEventsQueries.all, - fieldRequested: fields, - fields: [], - filterQuery: createFilter(filterQuery), - pagination: { - activePage: newActivePage, - querySize: limit, - }, - language, - runtimeMappings, - sort, - timerange: { - interval: '12h', - from: startDate, - to: endDate, - }, - filterStatus, - }; - - if (activePage !== newActivePage) { - setActivePage(newActivePage); - } - if (!deepEqual(prevRequest, currentRequest)) { - return currentRequest; - } - return prevRequest; - }); - }, [ - alertConsumers, - dispatch, - indexNames, - activePage, - endDate, - excludeEcsData, - filterQuery, - id, - language, - limit, - startDate, - sort, - fields, - runtimeMappings, - filterStatus, - ]); - - const timelineEventsSearchHandler = useCallback( - (onNextHandler?: OnNextResponseHandler) => { - if (!deepEqual(prevTimelineRequest.current, timelineRequest)) { - timelineSearch(timelineRequest, onNextHandler); - } - }, - [timelineRequest, timelineSearch] - ); - - /* - cleanup timeline events response when the filters were removed completely - to avoid displaying previous query results - */ - useEffect(() => { - if (isEmpty(filterQuery)) { - setTimelineResponse({ - consumers: {}, - id, - inspect: { - dsl: [], - response: [], - }, - refetch: refetchGrid, - totalCount: -1, - pageInfo: { - activePage: 0, - querySize: 0, - }, - events: [], - loadPage: wrappedLoadPage, - updatedAt: 0, - }); - } - }, [filterQuery, id, refetchGrid, wrappedLoadPage]); - - return [loading, timelineResponse, timelineEventsSearchHandler]; -}; - -export const useTimelineEvents = ({ - alertConsumers = NO_CONSUMERS, - dataViewId, - endDate, - entityType, - excludeEcsData = false, - id = ID, - indexNames, - fields, - filterQuery, - filterStatus, - startDate, - language = 'kuery', - limit, - runtimeMappings, - sort = initSortDefault, - skip = false, - timerangeKind, - data, -}: UseTimelineEventsProps): [boolean, TimelineArgs] => { - const [loading, timelineResponse, timelineSearchHandler] = useTimelineEventsHandler({ - alertConsumers, - dataViewId, - endDate, - entityType, - excludeEcsData, - filterStatus, - id, - indexNames, - fields, - filterQuery, - startDate, - language, - limit, - runtimeMappings, - sort, - skip, - timerangeKind, - data, - }); - - useEffect(() => { - if (!timelineSearchHandler) return; - timelineSearchHandler(); - }, [timelineSearchHandler]); - - return [loading, timelineResponse]; -}; diff --git a/x-pack/plugins/timelines/public/container/source/index.tsx b/x-pack/plugins/timelines/public/container/source/index.tsx deleted file mode 100644 index dfb995d66be98..0000000000000 --- a/x-pack/plugins/timelines/public/container/source/index.tsx +++ /dev/null @@ -1,160 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { useCallback, useEffect, useRef, useState } from 'react'; -import { isEmpty, isEqual, pick } from 'lodash/fp'; -import { Subscription } from 'rxjs'; - -import memoizeOne from 'memoize-one'; -import { DataViewBase } from '@kbn/es-query'; - -import type { DataPublicPluginStart } from '@kbn/data-plugin/public'; -import { isCompleteResponse, isErrorResponse } from '@kbn/data-plugin/common'; - -import { useKibana } from '@kbn/kibana-react-plugin/public'; -import * as i18n from './translations'; -import { - BrowserField, - BrowserFields, - IndexField, - IndexFieldsStrategyRequest, - IndexFieldsStrategyResponse, -} from '../../../common/search_strategy'; -import { useAppToasts } from '../../hooks/use_app_toasts'; - -const DEFAULT_BROWSER_FIELDS = {}; -const DEFAULT_INDEX_PATTERNS = { fields: [], title: '' }; -interface FetchIndexReturn { - browserFields: BrowserFields; - indexes: string[]; - indexExists: boolean; - indexPatterns: DataViewBase; -} - -/** - * HOT Code path where the fields can be 16087 in length or larger. This is - * VERY mutatious on purpose to improve the performance of the transform. - */ -export const getBrowserFields = memoizeOne( - (_title: string, fields: IndexField[]): BrowserFields => { - // Adds two dangerous casts to allow for mutations within this function - type DangerCastForMutation = Record; - type DangerCastForBrowserFieldsMutation = Record< - string, - Omit & { fields: Record } - >; - - // We mutate this instead of using lodash/set to keep this as fast as possible - return fields.reduce((accumulator, field) => { - if (accumulator[field.category] == null) { - (accumulator as DangerCastForMutation)[field.category] = {}; - } - if (accumulator[field.category].fields == null) { - accumulator[field.category].fields = {}; - } - accumulator[field.category].fields[field.name] = field as unknown as BrowserField; - return accumulator; - }, {}); - }, - // Update the value only if _title has changed - (newArgs, lastArgs) => newArgs[0] === lastArgs[0] -); - -export const getIndexFields = memoizeOne( - (title: string, fields: IndexField[]): DataViewBase => - fields && fields.length > 0 - ? { - fields: fields.map((field) => - pick(['name', 'searchable', 'type', 'aggregatable', 'esTypes', 'subType'], field) - ), - title, - } - : { fields: [], title }, - (newArgs, lastArgs) => newArgs[0] === lastArgs[0] && newArgs[1].length === lastArgs[1].length -); - -export const useFetchIndex = ( - indexNames: string[], - onlyCheckIfIndicesExist: boolean = false -): [boolean, FetchIndexReturn] => { - const { data } = useKibana<{ data: DataPublicPluginStart }>().services; - const abortCtrl = useRef(new AbortController()); - const searchSubscription$ = useRef(new Subscription()); - const previousIndexesName = useRef([]); - const [isLoading, setLoading] = useState(false); - - const [state, setState] = useState({ - browserFields: DEFAULT_BROWSER_FIELDS, - indexes: indexNames, - indexExists: true, - indexPatterns: DEFAULT_INDEX_PATTERNS, - }); - const { addError, addWarning } = useAppToasts(); - - const indexFieldsSearch = useCallback( - (iNames) => { - const asyncSearch = async () => { - abortCtrl.current = new AbortController(); - setLoading(true); - searchSubscription$.current = data.search - .search, IndexFieldsStrategyResponse>( - { indices: iNames, onlyCheckIfIndicesExist }, - { - abortSignal: abortCtrl.current.signal, - strategy: 'indexFields', - } - ) - .subscribe({ - next: (response) => { - if (isCompleteResponse(response)) { - const stringifyIndices = response.indicesExist.sort().join(); - - previousIndexesName.current = response.indicesExist; - setLoading(false); - - setState({ - browserFields: getBrowserFields(stringifyIndices, response.indexFields), - indexes: response.indicesExist, - indexExists: response.indicesExist.length > 0, - indexPatterns: getIndexFields(stringifyIndices, response.indexFields), - }); - - searchSubscription$.current.unsubscribe(); - } else if (isErrorResponse(response)) { - setLoading(false); - addWarning(i18n.ERROR_BEAT_FIELDS); - searchSubscription$.current.unsubscribe(); - } - }, - error: (msg) => { - setLoading(false); - addError(msg, { - title: i18n.FAIL_BEAT_FIELDS, - }); - searchSubscription$.current.unsubscribe(); - }, - }); - }; - searchSubscription$.current.unsubscribe(); - abortCtrl.current.abort(); - asyncSearch(); - }, - [data.search, addError, addWarning, onlyCheckIfIndicesExist, setLoading, setState] - ); - - useEffect(() => { - if (!isEmpty(indexNames) && !isEqual(previousIndexesName.current, indexNames)) { - indexFieldsSearch(indexNames); - } - return () => { - searchSubscription$.current.unsubscribe(); - abortCtrl.current.abort(); - }; - }, [indexNames, indexFieldsSearch, previousIndexesName]); - - return [isLoading, state]; -}; diff --git a/x-pack/plugins/timelines/public/container/source/translations.ts b/x-pack/plugins/timelines/public/container/source/translations.ts deleted file mode 100644 index f1fa461983116..0000000000000 --- a/x-pack/plugins/timelines/public/container/source/translations.ts +++ /dev/null @@ -1,19 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { i18n } from '@kbn/i18n'; - -export const ERROR_BEAT_FIELDS = i18n.translate( - 'xpack.timelines.beatFields.errorSearchDescription', - { - defaultMessage: `An error has occurred on getting beat fields`, - } -); - -export const FAIL_BEAT_FIELDS = i18n.translate('xpack.timelines.beatFields.failSearchDescription', { - defaultMessage: `Failed to run search on beat fields`, -}); diff --git a/x-pack/plugins/timelines/public/container/translations.ts b/x-pack/plugins/timelines/public/container/translations.ts deleted file mode 100644 index 757c936a93f72..0000000000000 --- a/x-pack/plugins/timelines/public/container/translations.ts +++ /dev/null @@ -1,15 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { i18n } from '@kbn/i18n'; - -export const ERROR_TIMELINE_EVENTS = i18n.translate( - 'xpack.timelines.timelineEvents.errorSearchDescription', - { - defaultMessage: `An error has occurred on timeline events search`, - } -); diff --git a/x-pack/plugins/timelines/public/container/use_update_alerts.ts b/x-pack/plugins/timelines/public/container/use_update_alerts.ts deleted file mode 100644 index 373490f8a014f..0000000000000 --- a/x-pack/plugins/timelines/public/container/use_update_alerts.ts +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; -import { CoreStart } from '@kbn/core/public'; - -import { useKibana } from '@kbn/kibana-react-plugin/public'; -import type { AlertStatus } from '../../common/types'; -import { - DETECTION_ENGINE_SIGNALS_STATUS_URL, - RAC_ALERTS_BULK_UPDATE_URL, -} from '../../common/constants'; - -/** - * Update alert status by query - * - * @param useDetectionEngine logic flag for using the regular Detection Engine URL or the RAC URL - * - * @param status to update to('open' / 'closed' / 'acknowledged') - * @param index index to be updated - * @param query optional query object to update alerts by query. - - * - * @throws An error if response is not OK - */ -export const useUpdateAlertsStatus = ( - useDetectionEngine: boolean = false -): { - updateAlertStatus: (params: { - status: AlertStatus; - index: string; - query: object; - }) => Promise; -} => { - const { http } = useKibana().services; - return { - updateAlertStatus: async ({ status, index, query }) => { - if (useDetectionEngine) { - return http.fetch(DETECTION_ENGINE_SIGNALS_STATUS_URL, { - method: 'POST', - body: JSON.stringify({ status, query }), - }); - } else { - const response = await http.post( - RAC_ALERTS_BULK_UPDATE_URL, - { body: JSON.stringify({ index, status, query }) } - ); - return response; - } - }, - }; -}; diff --git a/x-pack/plugins/timelines/public/index.ts b/x-pack/plugins/timelines/public/index.ts index 9568b31fa17b1..2efefc82f18da 100644 --- a/x-pack/plugins/timelines/public/index.ts +++ b/x-pack/plugins/timelines/public/index.ts @@ -16,35 +16,7 @@ import { TimelinesPlugin } from './plugin'; -export { - upsertColumn, - applyDeltaToColumnWidth, - updateColumnOrder, - updateColumnWidth, - toggleDetailPanel, - removeColumn, - updateIsLoading, - updateColumns, - updateItemsPerPage, - updateItemsPerPageOptions, - updateSort, - setSelected, - clearSelected, - setEventsLoading, - clearEventsLoading, - setEventsDeleted, - clearEventsDeleted, - initializeTGridSettings, - setTGridSelectAll, - updateGraphEventId, - updateSessionViewConfig, - createTGrid, - updateTotalCount, -} from './store/t_grid/actions'; - -export { tGridReducer } from './store/t_grid/reducer'; -export type { TimelinesUIStart, TableState, TableById, SubsetTGridModel } from './types'; -export type { TGridType, SortDirection, State as TGridState, TGridModel } from './types'; +export type { TimelinesUIStart } from './types'; export type { OnColumnFocused } from '../common/utils/accessibility'; export { @@ -70,32 +42,10 @@ export { stopPropagationAndPreventDefault, } from '../common/utils/accessibility'; -export { - addFieldToTimelineColumns, - getTimelineIdFromColumnDroppableId, -} from './components/drag_and_drop/helpers'; - -export { getActionsColumnWidth } from './components/t_grid/body/column_headers/helpers'; -export { DEFAULT_ACTION_BUTTON_WIDTH } from './components/t_grid/body/constants'; -export { useBulkActionItems } from './hooks/use_bulk_action_items'; -export { getPageRowIndex } from '../common/utils/pagination'; -export { - convertKueryToDslFilter, - convertKueryToElasticSearchQuery, - convertToBuildEsQuery, - escapeKuery, - escapeQueryValue, -} from './components/utils/keury'; - // This exports static code and TypeScript types, // as well as, Kibana Platform `plugin()` initializer. export function plugin() { return new TimelinesPlugin(); } -export { StatefulEventContext } from './components/stateful_event_context'; -export { TableContext } from './components/t_grid/shared'; - export type { AddToTimelineButtonProps } from './components/hover_actions/actions/add_to_timeline'; - -export { combineQueries } from './components/t_grid/helpers'; diff --git a/x-pack/plugins/timelines/public/lib/apm/constants.ts b/x-pack/plugins/timelines/public/lib/apm/constants.ts deleted file mode 100644 index 6b8036f2d2393..0000000000000 --- a/x-pack/plugins/timelines/public/lib/apm/constants.ts +++ /dev/null @@ -1,12 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -export const APM_USER_INTERACTIONS = { - BULK_QUERY_STATUS_UPDATE: 'Timeline bulkQueryStatusUpdate', - BULK_STATUS_UPDATE: 'Timeline bulkStatusUpdate', - STATUS_UPDATE: 'Timeline statusUpdate', -} as const; diff --git a/x-pack/plugins/timelines/public/lib/apm/types.ts b/x-pack/plugins/timelines/public/lib/apm/types.ts deleted file mode 100644 index eb52ab17b2f94..0000000000000 --- a/x-pack/plugins/timelines/public/lib/apm/types.ts +++ /dev/null @@ -1,15 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { APM_USER_INTERACTIONS } from './constants'; - -export type ApmUserInteractionName = - typeof APM_USER_INTERACTIONS[keyof typeof APM_USER_INTERACTIONS]; - -export type ApmSearchRequestName = `Timeline search ${string}`; - -export type ApmTransactionName = ApmSearchRequestName | ApmUserInteractionName; diff --git a/x-pack/plugins/timelines/public/lib/apm/use_start_transaction.ts b/x-pack/plugins/timelines/public/lib/apm/use_start_transaction.ts deleted file mode 100644 index fa47db412e467..0000000000000 --- a/x-pack/plugins/timelines/public/lib/apm/use_start_transaction.ts +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { useCallback } from 'react'; -import type { TransactionOptions } from '@elastic/apm-rum'; -import { useKibana } from '@kbn/kibana-react-plugin/public'; -import { TimelinesStartPlugins } from '../../types'; -import type { ApmSearchRequestName, ApmTransactionName } from './types'; - -const DEFAULT_TRANSACTION_OPTIONS: TransactionOptions = { managed: true }; - -interface StartTransactionOptions { - name: ApmTransactionName; - type?: string; - options?: TransactionOptions; -} - -export const useStartTransaction = () => { - const { apm } = useKibana().services; - - const startTransaction = useCallback( - ({ name, type = 'user-interaction', options }: StartTransactionOptions) => { - return apm?.startTransaction(name, type, options ?? DEFAULT_TRANSACTION_OPTIONS); - }, - [apm] - ); - - return { startTransaction }; -}; - -export const getSearchTransactionName = (timelineId: string): ApmSearchRequestName => - `Timeline search ${timelineId}`; diff --git a/x-pack/plugins/timelines/public/methods/index.tsx b/x-pack/plugins/timelines/public/methods/index.tsx index 92d6bc5a8bd3b..dfd68e0e2361a 100644 --- a/x-pack/plugins/timelines/public/methods/index.tsx +++ b/x-pack/plugins/timelines/public/methods/index.tsx @@ -7,53 +7,7 @@ import React, { lazy, Suspense } from 'react'; import { EuiLoadingSpinner } from '@elastic/eui'; -import type { Store } from 'redux'; -import type { Storage } from '@kbn/kibana-utils-plugin/public'; -import type { DataPublicPluginStart } from '@kbn/data-plugin/public'; -import type { TGridProps } from '../types'; -import type { LastUpdatedAtProps, LoadingPanelProps } from '../components'; -import { initialTGridState } from '../store/t_grid/reducer'; -import { createStore } from '../store/t_grid'; -import { TGridLoading } from '../components/t_grid/shared'; - -const initializeStore = ({ - store, - storage, - setStore, -}: { - store?: Store; - storage: Storage; - setStore: (store: Store) => void; -}) => { - let tGridStore = store; - if (!tGridStore) { - tGridStore = createStore(initialTGridState, storage); - setStore(tGridStore); - } -}; - -const TimelineLazy = lazy(() => import('../components')); -export const getTGridLazy = ( - props: TGridProps, - { - store, - storage, - data, - setStore, - }: { - store?: Store; - storage: Storage; - data: DataPublicPluginStart; - setStore: (store: Store) => void; - } -) => { - initializeStore({ store, storage, setStore }); - return ( - }> - - - ); -}; +import { LastUpdatedAtProps, LoadingPanelProps } from '../components'; const LastUpdatedLazy = lazy(() => import('../components/last_updated')); export const getLastUpdatedLazy = (props: LastUpdatedAtProps) => { diff --git a/x-pack/plugins/timelines/public/mock/cell_renderer.tsx b/x-pack/plugins/timelines/public/mock/cell_renderer.tsx deleted file mode 100644 index 74a20026cf2ab..0000000000000 --- a/x-pack/plugins/timelines/public/mock/cell_renderer.tsx +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; - -import { getMappedNonEcsValue } from '../components/t_grid/body/data_driven_columns'; -import type { CellValueElementProps } from '../../common/types/timeline'; - -export const TestCellRenderer: React.FC = ({ columnId, data }) => ( - <> - {getMappedNonEcsValue({ - data, - fieldName: columnId, - })?.reduce((x) => x[0]) ?? ''} - -); diff --git a/x-pack/plugins/timelines/public/mock/global_state.ts b/x-pack/plugins/timelines/public/mock/global_state.ts deleted file mode 100644 index 4ab0f883850f5..0000000000000 --- a/x-pack/plugins/timelines/public/mock/global_state.ts +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { Direction } from '../../common/search_strategy'; -import { TableId, TableState } from '../types'; -import { defaultHeaders } from './header'; - -export const mockGlobalState: TableState = { - tableById: { - [TableId.test]: { - columns: defaultHeaders, - dataViewId: null, - deletedEventIds: [], - expandedDetail: {}, - id: 'test', - indexNames: [ - 'apm-*-transaction*', - 'traces-apm*', - 'auditbeat-*', - 'endgame-*', - 'filebeat-*', - 'logs-*', - 'packetbeat-*', - 'winlogbeat-*', - ], - isLoading: false, - isSelectAllChecked: false, - itemsPerPage: 5, - itemsPerPageOptions: [5, 10, 20], - loadingEventIds: [], - showCheckboxes: false, - sort: [ - { - columnId: '@timestamp', - columnType: 'date', - esTypes: ['date'], - sortDirection: Direction.desc, - }, - ], - selectedEventIds: {}, - defaultColumns: defaultHeaders, - loadingText: 'loading events', - queryFields: [], - title: 'Events', - sessionViewConfig: null, - selectAll: false, - totalCount: 0, - }, - }, -}; diff --git a/x-pack/plugins/timelines/public/mock/header.ts b/x-pack/plugins/timelines/public/mock/header.ts deleted file mode 100644 index b52807d8aa958..0000000000000 --- a/x-pack/plugins/timelines/public/mock/header.ts +++ /dev/null @@ -1,144 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type { ColumnHeaderOptions } from '../../common/types/timeline'; -import { defaultColumnHeaderType } from '../components/t_grid/body/column_headers/default_headers'; -import { - DEFAULT_COLUMN_MIN_WIDTH, - DEFAULT_DATE_COLUMN_MIN_WIDTH, -} from '../components/t_grid/body/constants'; - -export const defaultHeaders: ColumnHeaderOptions[] = [ - { - category: 'base', - columnHeaderType: defaultColumnHeaderType, - description: - 'Date/time when the event originated.\nFor log events this is the date/time when the event was generated, and not when it was read.\nRequired field for all events.', - example: '2016-05-23T08:05:34.853Z', - id: '@timestamp', - type: 'date', - esTypes: ['date'], - aggregatable: true, - initialWidth: DEFAULT_DATE_COLUMN_MIN_WIDTH, - }, - { - category: 'event', - columnHeaderType: defaultColumnHeaderType, - description: - "Severity describes the severity of the event. What the different severity values mean can very different between use cases. It's up to the implementer to make sure severities are consistent across events.", - example: '7', - id: 'event.severity', - type: 'number', - esTypes: ['long'], - aggregatable: true, - initialWidth: DEFAULT_COLUMN_MIN_WIDTH, - }, - { - category: 'event', - columnHeaderType: defaultColumnHeaderType, - description: - 'Event category.\nThis contains high-level information about the contents of the event. It is more generic than `event.action`, in the sense that typically a category contains multiple actions. Warning: In future versions of ECS, we plan to provide a list of acceptable values for this field, please use with caution.', - example: 'user-management', - id: 'event.category', - type: 'string', - esTypes: ['keyword'], - aggregatable: true, - initialWidth: DEFAULT_COLUMN_MIN_WIDTH, - }, - { - category: 'event', - columnHeaderType: defaultColumnHeaderType, - description: - 'The action captured by the event.\nThis describes the information in the event. It is more specific than `event.category`. Examples are `group-add`, `process-started`, `file-created`. The value is normally defined by the implementer.', - example: 'user-password-change', - id: 'event.action', - type: 'string', - esTypes: ['keyword'], - aggregatable: true, - initialWidth: DEFAULT_COLUMN_MIN_WIDTH, - }, - { - category: 'host', - columnHeaderType: defaultColumnHeaderType, - description: - 'Name of the host.\nIt can contain what `hostname` returns on Unix systems, the fully qualified domain name, or a name specified by the user. The sender decides which value to use.', - example: '', - id: 'host.name', - type: 'string', - esTypes: ['keyword'], - aggregatable: true, - initialWidth: DEFAULT_COLUMN_MIN_WIDTH, - }, - { - category: 'source', - columnHeaderType: defaultColumnHeaderType, - description: 'IP address of the source.\nCan be one or multiple IPv4 or IPv6 addresses.', - example: '', - id: 'source.ip', - type: 'ip', - esTypes: ['ip'], - aggregatable: true, - initialWidth: DEFAULT_COLUMN_MIN_WIDTH, - }, - { - category: 'destination', - columnHeaderType: defaultColumnHeaderType, - description: 'IP address of the destination.\nCan be one or multiple IPv4 or IPv6 addresses.', - example: '', - id: 'destination.ip', - type: 'ip', - esTypes: ['ip'], - aggregatable: true, - initialWidth: DEFAULT_COLUMN_MIN_WIDTH, - }, - { - aggregatable: true, - category: 'destination', - columnHeaderType: defaultColumnHeaderType, - description: 'Bytes sent from the source to the destination', - example: '123', - format: 'bytes', - id: 'destination.bytes', - type: 'number', - esTypes: ['long'], - initialWidth: DEFAULT_COLUMN_MIN_WIDTH, - }, - { - category: 'user', - columnHeaderType: defaultColumnHeaderType, - description: 'Short name or login of the user.', - example: 'albert', - id: 'user.name', - type: 'string', - esTypes: ['keyword'], - aggregatable: true, - initialWidth: DEFAULT_COLUMN_MIN_WIDTH, - }, - { - category: 'base', - columnHeaderType: defaultColumnHeaderType, - description: 'Each document has an _id that uniquely identifies it', - example: 'Y-6TfmcB0WOhS6qyMv3s', - id: '_id', - type: 'string', - esTypes: [], // empty for _id - aggregatable: false, - initialWidth: DEFAULT_COLUMN_MIN_WIDTH, - }, - { - category: 'base', - columnHeaderType: defaultColumnHeaderType, - description: - 'For log events the message field contains the log message.\nIn other use cases the message field can be used to concatenate different values which are then freely searchable. If multiple messages exist, they can be combined into one message.', - example: 'Hello World', - id: 'message', - type: 'string', - esTypes: ['text'], - aggregatable: false, - initialWidth: DEFAULT_COLUMN_MIN_WIDTH, - }, -]; diff --git a/x-pack/plugins/timelines/public/mock/index.ts b/x-pack/plugins/timelines/public/mock/index.ts index 305413f0f17af..dc6befef94396 100644 --- a/x-pack/plugins/timelines/public/mock/index.ts +++ b/x-pack/plugins/timelines/public/mock/index.ts @@ -6,13 +6,9 @@ */ export * from './browser_fields'; -export * from './header'; export * from './index_pattern'; export * from './kibana_react.mock'; export * from './mock_and_providers'; export * from './mock_data_providers'; -export * from './mock_timeline_control_columns'; -export * from './mock_timeline_data'; -export * from './test_providers'; export * from './plugin_mock'; -export * from './t_grid'; +export * from './test_providers'; diff --git a/x-pack/plugins/timelines/public/mock/mock_timeline_control_columns.tsx b/x-pack/plugins/timelines/public/mock/mock_timeline_control_columns.tsx deleted file mode 100644 index 8a670a6cf90ab..0000000000000 --- a/x-pack/plugins/timelines/public/mock/mock_timeline_control_columns.tsx +++ /dev/null @@ -1,117 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React, { useState } from 'react'; -import { - EuiCheckbox, - EuiButtonIcon, - EuiPopover, - EuiFlexGroup, - EuiFlexItem, - EuiPopoverTitle, - EuiSpacer, -} from '@elastic/eui'; -import type { ControlColumnProps } from '../../common/types/timeline'; - -const SelectionHeaderCell = () => { - return ( -
    - null} /> -
    - ); -}; - -const SimpleHeaderCell = () => { - return ( -
    - {'Additional Actions'} -
    - ); -}; - -const SelectionRowCell = ({ rowIndex }: { rowIndex: number }) => { - return ( -
    - null} - /> -
    - ); -}; - -const TestTrailingColumn = () => { - const [isPopoverOpen, setIsPopoverOpen] = useState(false); - return ( - setIsPopoverOpen(!isPopoverOpen)} - /> - } - data-test-subj="test-trailing-column-popover-button" - closePopover={() => setIsPopoverOpen(false)} - > - {'Actions'} -
    - - - -
    -
    - ); -}; - -export const testTrailingControlColumns = [ - { - id: 'actions', - width: 96, - headerCellRender: SimpleHeaderCell, - rowCellRender: TestTrailingColumn, - }, -]; - -export const testLeadingControlColumn: ControlColumnProps = { - id: 'test-leading-control', - headerCellRender: SelectionHeaderCell, - rowCellRender: SelectionRowCell, - width: 100, -}; diff --git a/x-pack/plugins/timelines/public/mock/mock_timeline_data.ts b/x-pack/plugins/timelines/public/mock/mock_timeline_data.ts deleted file mode 100644 index 10d8673a2832c..0000000000000 --- a/x-pack/plugins/timelines/public/mock/mock_timeline_data.ts +++ /dev/null @@ -1,1580 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type { Ecs } from '../../common/ecs'; -import { TimelineItem, Direction } from '../../common/search_strategy'; -import type { TGridModel } from '../store/t_grid/model'; - -export const mockTimelineData: TimelineItem[] = [ - { - _id: '1', - data: [ - { field: '@timestamp', value: ['2018-11-05T19:03:25.937Z'] }, - { field: 'event.severity', value: ['3'] }, - { field: 'event.category', value: ['Access'] }, - { field: 'event.action', value: ['Action'] }, - { field: 'host.name', value: ['apache'] }, - { field: 'source.ip', value: ['192.168.0.1'] }, - { field: 'destination.ip', value: ['192.168.0.3'] }, - { field: 'destination.bytes', value: ['123456'] }, - { field: 'user.name', value: ['john.dee'] }, - ], - ecs: { - _id: '1', - timestamp: '2018-11-05T19:03:25.937Z', - host: { name: ['apache'], ip: ['192.168.0.1'] }, - event: { - id: ['1'], - action: ['Action'], - category: ['Access'], - module: ['nginx'], - severity: [3], - }, - source: { ip: ['192.168.0.1'], port: [80] }, - destination: { ip: ['192.168.0.3'], port: [6343] }, - user: { id: ['1'], name: ['john.dee'] }, - geo: { region_name: ['xx'], country_iso_code: ['xx'] }, - }, - }, - { - _id: '3', - data: [ - { field: '@timestamp', value: ['2018-11-07T19:03:25.937Z'] }, - { field: 'event.severity', value: ['1'] }, - { field: 'event.category', value: ['Access'] }, - { field: 'host.name', value: ['nginx'] }, - { field: 'source.ip', value: ['192.168.0.3'] }, - { field: 'destination.ip', value: ['192.168.0.3'] }, - { field: 'destination.bytes', value: ['123456'] }, - { field: 'user.name', value: ['evan.davis'] }, - ], - ecs: { - _id: '3', - timestamp: '2018-11-07T19:03:25.937Z', - host: { name: ['nginx'], ip: ['192.168.0.1'] }, - event: { - id: ['3'], - category: ['Access'], - type: ['HTTP Request'], - module: ['nginx'], - severity: [1], - }, - source: { ip: ['192.168.0.3'], port: [443] }, - destination: { ip: ['192.168.0.3'], port: [6343] }, - user: { id: ['3'], name: ['evan.davis'] }, - geo: { region_name: ['xx'], country_iso_code: ['xx'] }, - }, - }, - { - _id: '4', - data: [ - { field: '@timestamp', value: ['2018-11-08T19:03:25.937Z'] }, - { field: 'event.severity', value: ['1'] }, - { field: 'event.category', value: ['Attempted Administrator Privilege Gain'] }, - { field: 'host.name', value: ['suricata'] }, - { field: 'source.ip', value: ['192.168.0.3'] }, - { field: 'destination.ip', value: ['192.168.0.3'] }, - { field: 'destination.bytes', value: ['123456'] }, - { field: 'user.name', value: ['jenny.jones'] }, - ], - ecs: { - _id: '4', - timestamp: '2018-11-08T19:03:25.937Z', - host: { name: ['suricata'], ip: ['192.168.0.1'] }, - event: { - id: ['4'], - category: ['Attempted Administrator Privilege Gain'], - type: ['Alert'], - module: ['suricata'], - severity: [1], - }, - source: { ip: ['192.168.0.3'], port: [53] }, - destination: { ip: ['192.168.0.3'], port: [6343] }, - suricata: { - eve: { - flow_id: [4], - proto: [''], - alert: { - signature: [ - 'ET EXPLOIT NETGEAR WNR2000v5 hidden_lang_avi Stack Overflow (CVE-2016-10174)', - ], - signature_id: [4], - }, - }, - }, - user: { id: ['4'], name: ['jenny.jones'] }, - geo: { region_name: ['xx'], country_iso_code: ['xx'] }, - }, - }, - { - _id: '5', - data: [ - { field: '@timestamp', value: ['2018-11-09T19:03:25.937Z'] }, - { field: 'event.severity', value: ['3'] }, - { field: 'event.category', value: ['Access'] }, - { field: 'host.name', value: ['joe.computer'] }, - { field: 'source.ip', value: ['192.168.0.3'] }, - { field: 'destination.ip', value: ['192.168.0.3'] }, - { field: 'destination.bytes', value: ['123456'] }, - { field: 'user.name', value: ['becky.davis'] }, - ], - ecs: { - _id: '5', - timestamp: '2018-11-09T19:03:25.937Z', - host: { name: ['joe.computer'], ip: ['192.168.0.1'] }, - event: { - id: ['5'], - category: ['Access'], - type: ['HTTP Request'], - module: ['nginx'], - severity: [3], - }, - source: { ip: ['192.168.0.3'], port: [80] }, - destination: { ip: ['192.168.0.3'], port: [6343] }, - user: { id: ['5'], name: ['becky.davis'] }, - geo: { region_name: ['xx'], country_iso_code: ['xx'] }, - }, - }, - { - _id: '6', - data: [ - { field: '@timestamp', value: ['2018-11-10T19:03:25.937Z'] }, - { field: 'event.severity', value: ['3'] }, - { field: 'event.category', value: ['Access'] }, - { field: 'host.name', value: ['braden.davis'] }, - { field: 'source.ip', value: ['192.168.0.6'] }, - { field: 'destination.ip', value: ['192.168.0.3'] }, - { field: 'destination.bytes', value: ['123456'] }, - ], - ecs: { - _id: '6', - timestamp: '2018-11-10T19:03:25.937Z', - host: { name: ['braden.davis'], ip: ['192.168.0.1'] }, - event: { - id: ['6'], - category: ['Access'], - type: ['HTTP Request'], - module: ['nginx'], - severity: [3], - }, - source: { ip: ['192.168.0.6'], port: [80] }, - destination: { ip: ['192.168.0.3'], port: [6343] }, - geo: { region_name: ['xx'], country_iso_code: ['xx'] }, - }, - }, - { - _id: '8', - data: [ - { field: '@timestamp', value: ['2018-11-12T19:03:25.937Z'] }, - { field: 'event.severity', value: ['2'] }, - { field: 'event.category', value: ['Web Application Attack'] }, - { field: 'host.name', value: ['joe.computer'] }, - { field: 'source.ip', value: ['192.168.0.8'] }, - { field: 'destination.ip', value: ['192.168.0.3'] }, - { field: 'destination.bytes', value: ['123456'] }, - { field: 'user.name', value: ['jone.doe'] }, - ], - ecs: { - _id: '8', - timestamp: '2018-11-12T19:03:25.937Z', - host: { name: ['joe.computer'], ip: ['192.168.0.1'] }, - event: { - id: ['8'], - category: ['Web Application Attack'], - type: ['Alert'], - module: ['suricata'], - severity: [2], - }, - suricata: { - eve: { - flow_id: [8], - proto: [''], - alert: { - signature: ['ET WEB_SERVER Possible CVE-2014-6271 Attempt in HTTP Cookie'], - signature_id: [8], - }, - }, - }, - source: { ip: ['192.168.0.8'], port: [80] }, - destination: { ip: ['192.168.0.3'], port: [6343] }, - user: { id: ['8'], name: ['jone.doe'] }, - geo: { region_name: ['xx'], country_iso_code: ['xx'] }, - }, - }, - { - _id: '7', - data: [ - { field: '@timestamp', value: ['2018-11-11T19:03:25.937Z'] }, - { field: 'event.severity', value: ['3'] }, - { field: 'event.category', value: ['Access'] }, - { field: 'host.name', value: ['joe.computer'] }, - { field: 'source.ip', value: ['192.168.0.7'] }, - { field: 'destination.ip', value: ['192.168.0.3'] }, - { field: 'destination.bytes', value: ['123456'] }, - { field: 'user.name', value: ['jone.doe'] }, - ], - ecs: { - _id: '7', - timestamp: '2018-11-11T19:03:25.937Z', - host: { name: ['joe.computer'], ip: ['192.168.0.1'] }, - event: { - id: ['7'], - category: ['Access'], - type: ['HTTP Request'], - module: ['apache'], - severity: [3], - }, - source: { ip: ['192.168.0.7'], port: [80] }, - destination: { ip: ['192.168.0.3'], port: [6343] }, - user: { id: ['7'], name: ['jone.doe'] }, - geo: { region_name: ['xx'], country_iso_code: ['xx'] }, - }, - }, - { - _id: '9', - data: [ - { field: '@timestamp', value: ['2018-11-13T19:03:25.937Z'] }, - { field: 'event.severity', value: ['3'] }, - { field: 'event.category', value: ['Access'] }, - { field: 'host.name', value: ['joe.computer'] }, - { field: 'source.ip', value: ['192.168.0.9'] }, - { field: 'destination.ip', value: ['192.168.0.3'] }, - { field: 'destination.bytes', value: ['123456'] }, - { field: 'user.name', value: ['jone.doe'] }, - ], - ecs: { - _id: '9', - timestamp: '2018-11-13T19:03:25.937Z', - host: { name: ['joe.computer'], ip: ['192.168.0.1'] }, - event: { - id: ['9'], - category: ['Access'], - type: ['HTTP Request'], - module: ['nginx'], - severity: [3], - }, - source: { ip: ['192.168.0.9'], port: [80] }, - destination: { ip: ['192.168.0.3'], port: [6343] }, - user: { id: ['9'], name: ['jone.doe'] }, - geo: { region_name: ['xx'], country_iso_code: ['xx'] }, - }, - }, - { - _id: '10', - data: [ - { field: '@timestamp', value: ['2018-11-14T19:03:25.937Z'] }, - { field: 'event.severity', value: ['3'] }, - { field: 'event.category', value: ['Access'] }, - { field: 'host.name', value: ['joe.computer'] }, - { field: 'source.ip', value: ['192.168.0.10'] }, - { field: 'destination.ip', value: ['192.168.0.3'] }, - { field: 'destination.bytes', value: ['123456'] }, - { field: 'user.name', value: ['jone.doe'] }, - ], - ecs: { - _id: '10', - timestamp: '2018-11-14T19:03:25.937Z', - host: { name: ['joe.computer'], ip: ['192.168.0.1'] }, - event: { - id: ['10'], - category: ['Access'], - type: ['HTTP Request'], - module: ['nginx'], - severity: [3], - }, - source: { ip: ['192.168.0.10'], port: [80] }, - destination: { ip: ['192.168.0.3'], port: [6343] }, - user: { id: ['10'], name: ['jone.doe'] }, - geo: { region_name: ['xx'], country_iso_code: ['xx'] }, - }, - }, - { - _id: '11', - data: [ - { field: '@timestamp', value: ['2018-11-15T19:03:25.937Z'] }, - { field: 'event.severity', value: ['3'] }, - { field: 'event.category', value: ['Access'] }, - { field: 'host.name', value: ['joe.computer'] }, - { field: 'source.ip', value: ['192.168.0.11'] }, - { field: 'destination.ip', value: ['192.168.0.3'] }, - { field: 'destination.bytes', value: ['123456'] }, - { field: 'user.name', value: ['jone.doe'] }, - ], - ecs: { - _id: '11', - timestamp: '2018-11-15T19:03:25.937Z', - host: { name: ['joe.computer'], ip: ['192.168.0.1'] }, - event: { - id: ['11'], - category: ['Access'], - type: ['HTTP Request'], - module: ['nginx'], - severity: [3], - }, - source: { ip: ['192.168.0.11'], port: [80] }, - destination: { ip: ['192.168.0.3'], port: [6343] }, - user: { id: ['11'], name: ['jone.doe'] }, - geo: { region_name: ['xx'], country_iso_code: ['xx'] }, - }, - }, - { - _id: '12', - data: [ - { field: '@timestamp', value: ['2018-11-16T19:03:25.937Z'] }, - { field: 'event.severity', value: ['3'] }, - { field: 'event.category', value: ['Access'] }, - { field: 'host.name', value: ['joe.computer'] }, - { field: 'source.ip', value: ['192.168.0.12'] }, - { field: 'destination.ip', value: ['192.168.0.3'] }, - { field: 'destination.bytes', value: ['123456'] }, - { field: 'user.name', value: ['jone.doe'] }, - ], - ecs: { - _id: '12', - timestamp: '2018-11-16T19:03:25.937Z', - host: { name: ['joe.computer'], ip: ['192.168.0.1'] }, - event: { - id: ['12'], - category: ['Access'], - type: ['HTTP Request'], - module: ['nginx'], - severity: [3], - }, - source: { ip: ['192.168.0.12'], port: [80] }, - destination: { ip: ['192.168.0.3'], port: [6343] }, - user: { id: ['12'], name: ['jone.doe'] }, - geo: { region_name: ['xx'], country_iso_code: ['xx'] }, - }, - }, - { - _id: '2', - data: [ - { field: '@timestamp', value: ['2018-11-06T19:03:25.937Z'] }, - { field: 'event.severity', value: ['3'] }, - { field: 'event.category', value: ['Authentication'] }, - { field: 'host.name', value: ['joe.computer'] }, - { field: 'source.ip', value: ['192.168.0.2'] }, - { field: 'destination.ip', value: ['192.168.0.3'] }, - { field: 'destination.bytes', value: ['123456'] }, - { field: 'user.name', value: ['joe.bob'] }, - ], - ecs: { - _id: '2', - timestamp: '2018-11-06T19:03:25.937Z', - host: { name: ['joe.computer'], ip: ['192.168.0.1'] }, - event: { - id: ['2'], - category: ['Authentication'], - type: ['Authentication Success'], - module: ['authlog'], - severity: [3], - }, - source: { ip: ['192.168.0.2'], port: [80] }, - destination: { ip: ['192.168.0.3'], port: [6343] }, - user: { id: ['1'], name: ['joe.bob'] }, - geo: { region_name: ['xx'], country_iso_code: ['xx'] }, - }, - }, - { - _id: '13', - data: [ - { field: '@timestamp', value: ['2018-13-12T19:03:25.937Z'] }, - { field: 'event.severity', value: ['1'] }, - { field: 'event.category', value: ['Web Application Attack'] }, - { field: 'host.name', value: ['joe.computer'] }, - { field: 'source.ip', value: ['192.168.0.8'] }, - { field: 'destination.ip', value: ['192.168.0.3'] }, - { field: 'destination.bytes', value: ['123456'] }, - ], - ecs: { - _id: '13', - timestamp: '2018-13-12T19:03:25.937Z', - host: { name: ['joe.computer'], ip: ['192.168.0.1'] }, - event: { - id: ['13'], - category: ['Web Application Attack'], - type: ['Alert'], - module: ['suricata'], - severity: [1], - }, - suricata: { - eve: { - flow_id: [13], - proto: [''], - alert: { - signature: ['ET WEB_SERVER Possible Attempt in HTTP Cookie'], - signature_id: [13], - }, - }, - }, - source: { ip: ['192.168.0.8'], port: [80] }, - destination: { ip: ['192.168.0.3'], port: [6343] }, - geo: { region_name: ['xx'], country_iso_code: ['xx'] }, - }, - }, - { - _id: '14', - data: [ - { field: '@timestamp', value: ['2019-03-07T05:06:51.000Z'] }, - { field: 'host.name', value: ['zeek-franfurt'] }, - { field: 'source.ip', value: ['192.168.26.101'] }, - { field: 'destination.ip', value: ['192.168.238.205'] }, - ], - ecs: { - _id: '14', - timestamp: '2019-03-07T05:06:51.000Z', - event: { - module: ['zeek'], - dataset: ['zeek.connection'], - }, - host: { - id: ['37c81253e0fc4c46839c19b981be5177'], - name: ['zeek-franfurt'], - ip: ['207.154.238.205', '10.19.0.5', 'fe80::d82b:9aff:fe0d:1e12'], - }, - source: { ip: ['185.176.26.101'], port: [44059] }, - destination: { ip: ['207.154.238.205'], port: [11568] }, - geo: { region_name: ['New York'], country_iso_code: ['US'] }, - network: { transport: ['tcp'] }, - zeek: { - session_id: ['C8DRTq362Fios6hw16'], - connection: { - local_resp: [false], - local_orig: [false], - missed_bytes: [0], - state: ['REJ'], - history: ['Sr'], - }, - }, - }, - }, - { - _id: '15', - data: [ - { field: '@timestamp', value: ['2019-03-07T00:51:28.000Z'] }, - { field: 'host.name', value: ['suricata-zeek-singapore'] }, - { field: 'source.ip', value: ['192.168.35.240'] }, - { field: 'destination.ip', value: ['192.168.67.3'] }, - ], - ecs: { - _id: '15', - timestamp: '2019-03-07T00:51:28.000Z', - event: { - module: ['zeek'], - dataset: ['zeek.dns'], - }, - host: { - id: ['af3fddf15f1d47979ce817ba0df10c6e'], - name: ['suricata-zeek-singapore'], - ip: ['206.189.35.240', '10.15.0.5', 'fe80::98c7:eff:fe29:4455'], - }, - source: { ip: ['206.189.35.240'], port: [57475] }, - destination: { ip: ['67.207.67.3'], port: [53] }, - geo: { region_name: ['New York'], country_iso_code: ['US'] }, - network: { transport: ['udp'] }, - zeek: { - session_id: ['CyIrMA1L1JtLqdIuol'], - dns: { - AA: [false], - RD: [false], - trans_id: [65252], - RA: [false], - TC: [false], - }, - }, - }, - }, - { - _id: '16', - data: [ - { field: '@timestamp', value: ['2019-03-05T07:00:20.000Z'] }, - { field: 'host.name', value: ['suricata-zeek-singapore'] }, - { field: 'source.ip', value: ['192.168.35.240'] }, - { field: 'destination.ip', value: ['192.168.164.26'] }, - ], - ecs: { - _id: '16', - timestamp: '2019-03-05T07:00:20.000Z', - event: { - module: ['zeek'], - dataset: ['zeek.http'], - }, - host: { - id: ['af3fddf15f1d47979ce817ba0df10c6e'], - name: ['suricata-zeek-singapore'], - ip: ['206.189.35.240', '10.15.0.5', 'fe80::98c7:eff:fe29:4455'], - }, - source: { ip: ['206.189.35.240'], port: [36220] }, - destination: { ip: ['192.241.164.26'], port: [80] }, - geo: { region_name: ['New York'], country_iso_code: ['US'] }, - http: { - version: ['1.1'], - request: { body: { bytes: [0] } }, - response: { status_code: [302], body: { bytes: [154] } }, - }, - zeek: { - session_id: ['CZLkpC22NquQJOpkwe'], - - http: { - resp_mime_types: ['text/html'], - trans_depth: ['3'], - status_msg: ['Moved Temporarily'], - resp_fuids: ['FzeujEPP7GTHmYPsc'], - tags: [], - }, - }, - }, - }, - { - _id: '17', - data: [ - { field: '@timestamp', value: ['2019-02-28T22:36:28.000Z'] }, - { field: 'host.name', value: ['zeek-franfurt'] }, - { field: 'source.ip', value: ['192.168.77.171'] }, - ], - ecs: { - _id: '17', - timestamp: '2019-02-28T22:36:28.000Z', - event: { - module: ['zeek'], - dataset: ['zeek.notice'], - }, - host: { - id: ['37c81253e0fc4c46839c19b981be5177'], - name: ['zeek-franfurt'], - ip: ['207.154.238.205', '10.19.0.5', 'fe80::d82b:9aff:fe0d:1e12'], - }, - source: { ip: ['8.42.77.171'] }, - zeek: { - notice: { - suppress_for: [3600], - msg: ['8.42.77.171 scanned at least 15 unique ports of host 207.154.238.205 in 0m0s'], - note: ['Scan::Port_Scan'], - sub: ['remote'], - dst: ['207.154.238.205'], - dropped: [false], - peer_descr: ['bro'], - }, - }, - }, - }, - { - _id: '18', - data: [ - { field: '@timestamp', value: ['2019-02-22T21:12:13.000Z'] }, - { field: 'host.name', value: ['zeek-sensor-amsterdam'] }, - { field: 'source.ip', value: ['192.168.66.184'] }, - { field: 'destination.ip', value: ['192.168.95.15'] }, - ], - ecs: { - _id: '18', - timestamp: '2019-02-22T21:12:13.000Z', - event: { - module: ['zeek'], - dataset: ['zeek.ssl'], - }, - host: { id: ['2ce8b1e7d69e4a1d9c6bcddc473da9d9'], name: ['zeek-sensor-amsterdam'] }, - source: { ip: ['188.166.66.184'], port: [34514] }, - destination: { ip: ['91.189.95.15'], port: [443] }, - geo: { region_name: ['England'], country_iso_code: ['GB'] }, - zeek: { - session_id: ['CmTxzt2OVXZLkGDaRe'], - ssl: { - cipher: ['TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256'], - established: [false], - resumed: [false], - version: ['TLSv12'], - }, - }, - }, - }, - { - _id: '19', - data: [ - { field: '@timestamp', value: ['2019-03-03T04:26:38.000Z'] }, - { field: 'host.name', value: ['suricata-zeek-singapore'] }, - ], - ecs: { - _id: '19', - timestamp: '2019-03-03T04:26:38.000Z', - event: { - module: ['zeek'], - dataset: ['zeek.files'], - }, - host: { - id: ['af3fddf15f1d47979ce817ba0df10c6e'], - name: ['suricata-zeek-singapore'], - ip: ['206.189.35.240', '10.15.0.5', 'fe80::98c7:eff:fe29:4455'], - }, - zeek: { - session_id: ['Cu0n232QMyvNtzb75j'], - files: { - session_ids: ['Cu0n232QMyvNtzb75j'], - timedout: [false], - local_orig: [false], - tx_host: ['5.101.111.50'], - source: ['HTTP'], - is_orig: [false], - overflow_bytes: [0], - sha1: ['fa5195a5dfacc9d1c68d43600f0e0262cad14dde'], - duration: [0], - depth: [0], - analyzers: ['MD5', 'SHA1'], - mime_type: ['text/plain'], - rx_host: ['206.189.35.240'], - total_bytes: [88722], - fuid: ['FePz1uVEVCZ3I0FQi'], - seen_bytes: [1198], - missing_bytes: [0], - md5: ['f7653f1951693021daa9e6be61226e32'], - }, - }, - }, - }, - { - _id: '20', - data: [ - { field: '@timestamp', value: ['2019-03-13T05:42:11.815Z'] }, - { field: 'event.category', value: ['audit-rule'] }, - { field: 'host.name', value: ['zeek-sanfran'] }, - ], - ecs: { - _id: '20', - timestamp: '2019-03-13T05:42:11.815Z', - event: { - action: ['executed'], - module: ['auditd'], - category: ['audit-rule'], - }, - host: { - id: ['f896741c3b3b44bdb8e351a4ab6d2d7c'], - name: ['zeek-sanfran'], - ip: ['134.209.63.134', '10.46.0.5', 'fe80::a0d9:16ff:fecf:e70b'], - }, - user: { name: ['alice'] }, - process: { - pid: [5402], - name: ['gpgconf'], - ppid: [5401], - args: ['gpgconf', '--list-dirs', 'agent-socket'], - executable: ['/usr/bin/gpgconf'], - title: ['gpgconf --list-dirs agent-socket'], - working_directory: ['/'], - }, - }, - }, - { - _id: '21', - data: [ - { field: '@timestamp', value: ['2019-03-14T22:30:25.527Z'] }, - { field: 'event.category', value: ['user-login'] }, - { field: 'host.name', value: ['zeek-london'] }, - { field: 'source.ip', value: ['192.168.77.171'] }, - { field: 'user.name', value: ['root'] }, - ], - ecs: { - _id: '21', - timestamp: '2019-03-14T22:30:25.527Z', - event: { - action: ['logged-in'], - module: ['auditd'], - category: ['user-login'], - }, - auditd: { - result: ['success'], - session: ['14'], - data: { terminal: ['/dev/pts/0'], op: ['login'] }, - summary: { - actor: { primary: ['alice'], secondary: ['alice'] }, - object: { primary: ['/dev/pts/0'], secondary: ['8.42.77.171'], type: ['user-session'] }, - how: ['/usr/sbin/sshd'], - }, - }, - host: { - id: ['7c21f5ed03b04d0299569d221fe18bbc'], - name: ['zeek-london'], - ip: ['46.101.3.136', '10.16.0.5', 'fe80::4066:42ff:fe19:b3b9'], - }, - source: { ip: ['8.42.77.171'] }, - user: { name: ['root'] }, - process: { - pid: [17471], - executable: ['/usr/sbin/sshd'], - }, - }, - }, - { - _id: '22', - data: [ - { field: '@timestamp', value: ['2019-03-13T03:35:21.614Z'] }, - { field: 'event.category', value: ['user-login'] }, - { field: 'host.name', value: ['suricata-bangalore'] }, - { field: 'user.name', value: ['root'] }, - ], - ecs: { - _id: '22', - timestamp: '2019-03-13T03:35:21.614Z', - event: { - action: ['disposed-credentials'], - module: ['auditd'], - category: ['user-login'], - }, - auditd: { - result: ['success'], - session: ['340'], - data: { acct: ['alice'], terminal: ['ssh'], op: ['PAM:setcred'] }, - summary: { - actor: { primary: ['alice'], secondary: ['alice'] }, - object: { primary: ['ssh'], secondary: ['8.42.77.171'], type: ['user-session'] }, - how: ['/usr/sbin/sshd'], - }, - }, - host: { - id: ['0a63559c1acf4c419d979c4b4d8b83ff'], - name: ['suricata-bangalore'], - ip: ['139.59.11.147', '10.47.0.5', 'fe80::ec0b:1bff:fe29:80bd'], - }, - user: { name: ['root'] }, - process: { - pid: [21202], - executable: ['/usr/sbin/sshd'], - }, - }, - }, - { - _id: '23', - data: [ - { field: '@timestamp', value: ['2019-03-13T03:35:21.614Z'] }, - { field: 'event.category', value: ['user-login'] }, - { field: 'host.name', value: ['suricata-bangalore'] }, - { field: 'user.name', value: ['root'] }, - ], - ecs: { - _id: '23', - timestamp: '2019-03-13T03:35:21.614Z', - event: { - action: ['ended-session'], - module: ['auditd'], - category: ['user-login'], - }, - auditd: { - result: ['success'], - session: ['340'], - data: { acct: ['alice'], terminal: ['ssh'], op: ['PAM:session_close'] }, - summary: { - actor: { primary: ['alice'], secondary: ['alice'] }, - object: { primary: ['ssh'], secondary: ['8.42.77.171'], type: ['user-session'] }, - how: ['/usr/sbin/sshd'], - }, - }, - host: { - id: ['0a63559c1acf4c419d979c4b4d8b83ff'], - name: ['suricata-bangalore'], - ip: ['139.59.11.147', '10.47.0.5', 'fe80::ec0b:1bff:fe29:80bd'], - }, - user: { name: ['root'] }, - process: { - pid: [21202], - executable: ['/usr/sbin/sshd'], - }, - }, - }, - { - _id: '24', - data: [ - { field: '@timestamp', value: ['2019-03-18T23:17:01.645Z'] }, - { field: 'event.category', value: ['user-login'] }, - { field: 'host.name', value: ['zeek-london'] }, - { field: 'user.name', value: ['root'] }, - ], - ecs: { - _id: '24', - timestamp: '2019-03-18T23:17:01.645Z', - event: { - action: ['acquired-credentials'], - module: ['auditd'], - category: ['user-login'], - }, - auditd: { - result: ['success'], - session: ['unset'], - data: { acct: ['root'], terminal: ['cron'], op: ['PAM:setcred'] }, - summary: { - actor: { primary: ['unset'], secondary: ['root'] }, - object: { primary: ['cron'], type: ['user-session'] }, - how: ['/usr/sbin/cron'], - }, - }, - host: { - id: ['7c21f5ed03b04d0299569d221fe18bbc'], - name: ['zeek-london'], - ip: ['46.101.3.136', '10.16.0.5', 'fe80::4066:42ff:fe19:b3b9'], - }, - user: { name: ['root'] }, - process: { - pid: [9592], - executable: ['/usr/sbin/cron'], - }, - }, - }, - { - _id: '25', - data: [ - { field: '@timestamp', value: ['2019-03-19T01:17:01.336Z'] }, - { field: 'event.category', value: ['user-login'] }, - { field: 'host.name', value: ['siem-kibana'] }, - { field: 'user.name', value: ['root'] }, - ], - ecs: { - _id: '25', - timestamp: '2019-03-19T01:17:01.336Z', - event: { - action: ['started-session'], - module: ['auditd'], - category: ['user-login'], - }, - auditd: { - result: ['success'], - session: ['2908'], - data: { acct: ['root'], terminal: ['cron'], op: ['PAM:session_open'] }, - summary: { - actor: { primary: ['root'], secondary: ['root'] }, - object: { primary: ['cron'], type: ['user-session'] }, - how: ['/usr/sbin/cron'], - }, - }, - host: { id: ['aa7ca589f1b8220002f2fc61c64cfbf1'], name: ['siem-kibana'] }, - user: { name: ['root'] }, - process: { - pid: [725], - executable: ['/usr/sbin/cron'], - }, - }, - }, - { - _id: '26', - data: [ - { field: '@timestamp', value: ['2019-03-13T03:34:08.890Z'] }, - { field: 'event.category', value: ['user-login'] }, - { field: 'host.name', value: ['suricata-bangalore'] }, - { field: 'user.name', value: ['alice'] }, - ], - ecs: { - _id: '26', - timestamp: '2019-03-13T03:34:08.890Z', - event: { - action: ['was-authorized'], - module: ['auditd'], - category: ['user-login'], - }, - auditd: { - result: ['success'], - session: ['338'], - data: { terminal: ['/dev/pts/0'] }, - summary: { - actor: { primary: ['root'], secondary: ['alice'] }, - object: { primary: ['/dev/pts/0'], type: ['user-session'] }, - how: ['/sbin/pam_tally2'], - }, - }, - host: { - id: ['0a63559c1acf4c419d979c4b4d8b83ff'], - name: ['suricata-bangalore'], - ip: ['139.59.11.147', '10.47.0.5', 'fe80::ec0b:1bff:fe29:80bd'], - }, - user: { name: ['alice'] }, - process: { - pid: [21170], - executable: ['/sbin/pam_tally2'], - }, - }, - }, - { - _id: '27', - data: [ - { field: '@timestamp', value: ['2019-03-22T19:13:11.026Z'] }, - { field: 'event.action', value: ['connected-to'] }, - { field: 'event.category', value: ['audit-rule'] }, - { field: 'host.name', value: ['zeek-london'] }, - { field: 'destination.ip', value: ['192.168.216.34'] }, - { field: 'user.name', value: ['alice'] }, - ], - ecs: { - _id: '27', - timestamp: '2019-03-22T19:13:11.026Z', - event: { - action: ['connected-to'], - module: ['auditd'], - category: ['audit-rule'], - }, - auditd: { - result: ['success'], - session: ['246'], - summary: { - actor: { primary: ['alice'], secondary: ['alice'] }, - object: { primary: ['192.168.216.34'], secondary: ['80'], type: ['socket'] }, - how: ['/usr/bin/wget'], - }, - }, - host: { - id: ['7c21f5ed03b04d0299569d221fe18bbc'], - name: ['zeek-london'], - ip: ['46.101.3.136', '10.16.0.5', 'fe80::4066:42ff:fe19:b3b9'], - }, - destination: { ip: ['192.168.216.34'], port: [80] }, - user: { name: ['alice'] }, - process: { - pid: [1490], - name: ['wget'], - ppid: [1476], - executable: ['/usr/bin/wget'], - title: ['wget www.example.com'], - }, - }, - }, - { - _id: '28', - data: [ - { field: '@timestamp', value: ['2019-03-26T22:12:18.609Z'] }, - { field: 'event.action', value: ['opened-file'] }, - { field: 'event.category', value: ['audit-rule'] }, - { field: 'host.name', value: ['zeek-london'] }, - { field: 'user.name', value: ['root'] }, - ], - ecs: { - _id: '28', - timestamp: '2019-03-26T22:12:18.609Z', - event: { - action: ['opened-file'], - module: ['auditd'], - category: ['audit-rule'], - }, - auditd: { - result: ['success'], - session: ['242'], - summary: { - actor: { primary: ['unset'], secondary: ['root'] }, - object: { primary: ['/proc/15990/attr/current'], type: ['file'] }, - how: ['/lib/systemd/systemd-journald'], - }, - }, - file: { - path: ['/proc/15990/attr/current'], - device: ['00:00'], - inode: ['27672309'], - uid: ['0'], - owner: ['root'], - gid: ['0'], - group: ['root'], - mode: ['0666'], - }, - host: { - id: ['7c21f5ed03b04d0299569d221fe18bbc'], - name: ['zeek-london'], - ip: ['46.101.3.136', '10.16.0.5', 'fe80::4066:42ff:fe19:b3b9'], - }, - - user: { name: ['root'] }, - process: { - pid: [27244], - name: ['systemd-journal'], - ppid: [1], - executable: ['/lib/systemd/systemd-journald'], - title: ['/lib/systemd/systemd-journald'], - working_directory: ['/'], - }, - }, - }, - { - _id: '29', - data: [ - { field: '@timestamp', value: ['2019-04-08T21:18:57.000Z'] }, - { field: 'event.action', value: ['user_login'] }, - { field: 'event.category', value: null }, - { field: 'host.name', value: ['zeek-london'] }, - { field: 'user.name', value: ['Braden'] }, - ], - ecs: { - _id: '29', - event: { - action: ['user_login'], - dataset: ['login'], - kind: ['event'], - module: ['system'], - outcome: ['failure'], - }, - host: { - id: ['7c21f5ed03b04d0299569d221fe18bbc'], - name: ['zeek-london'], - ip: ['46.101.3.136', '10.16.0.5', 'fe80::4066:42ff:fe19:b3b9'], - }, - source: { - ip: ['128.199.212.120'], - }, - user: { - name: ['Braden'], - }, - process: { - pid: [6278], - }, - }, - }, - { - _id: '30', - data: [ - { field: '@timestamp', value: ['2019-04-08T22:27:14.814Z'] }, - { field: 'event.action', value: ['process_started'] }, - { field: 'event.category', value: null }, - { field: 'host.name', value: ['zeek-london'] }, - { field: 'user.name', value: ['Evan'] }, - ], - ecs: { - _id: '30', - event: { - action: ['process_started'], - dataset: ['login'], - kind: ['event'], - module: ['system'], - outcome: ['failure'], - }, - host: { - id: ['7c21f5ed03b04d0299569d221fe18bbc'], - name: ['zeek-london'], - ip: ['46.101.3.136', '10.16.0.5', 'fe80::4066:42ff:fe19:b3b9'], - }, - source: { - ip: ['128.199.212.120'], - }, - user: { - name: ['Evan'], - }, - process: { - pid: [6278], - }, - }, - }, - { - _id: '31', - data: [ - { field: '@timestamp', value: ['2018-11-05T19:03:25.937Z'] }, - { field: 'message', value: ['I am a log file message'] }, - { field: 'event.severity', value: ['3'] }, - { field: 'event.category', value: ['Access'] }, - { field: 'event.action', value: ['Action'] }, - { field: 'host.name', value: ['apache'] }, - { field: 'source.ip', value: ['192.168.0.1'] }, - { field: 'destination.ip', value: ['192.168.0.3'] }, - { field: 'destination.bytes', value: ['123456'] }, - { field: 'user.name', value: ['john.dee'] }, - ], - ecs: { - _id: '1', - timestamp: '2018-11-05T19:03:25.937Z', - host: { name: ['apache'], ip: ['192.168.0.1'] }, - event: { - id: ['1'], - action: ['Action'], - category: ['Access'], - module: ['nginx'], - severity: [3], - }, - message: ['I am a log file message'], - source: { ip: ['192.168.0.1'], port: [80] }, - destination: { ip: ['192.168.0.3'], port: [6343] }, - user: { id: ['1'], name: ['john.dee'] }, - geo: { region_name: ['xx'], country_iso_code: ['xx'] }, - }, - }, - { - _id: '32', - data: [], - ecs: { - _id: 'BuBP4W0BOpWiDweSoYSg', - timestamp: '2019-10-18T23:59:15.091Z', - threat: { - enrichments: [ - { - indicator: { - provider: ['indicator_provider'], - reference: ['https://example.com'], - }, - matched: { - atomic: ['192.168.1.1'], - field: ['source.ip'], - type: ['ip'], - }, - feed: { - name: ['feed_name'], - }, - }, - ], - }, - }, - }, -]; - -export const mockFimFileCreatedEvent: Ecs = { - _id: 'WuBP4W0BOpWiDweSoYSg', - timestamp: '2019-10-18T23:59:15.091Z', - host: { - architecture: ['x86_64'], - os: { - family: ['debian'], - name: ['Ubuntu'], - kernel: ['4.15.0-1046-gcp'], - platform: ['ubuntu'], - version: ['16.04.6 LTS (Xenial Xerus)'], - }, - id: ['host-id-123'], - name: ['foohost'], - }, - file: { - path: ['/etc/subgid'], - size: [4445], - owner: ['root'], - inode: ['90027'], - ctime: ['2019-10-18T23:59:14.872Z'], - gid: ['0'], - type: ['file'], - mode: ['0644'], - mtime: ['2019-10-18T23:59:14.872Z'], - uid: ['0'], - group: ['root'], - }, - event: { - module: ['file_integrity'], - dataset: ['file'], - action: ['created'], - }, -}; - -export const mockFimFileDeletedEvent: Ecs = { - _id: 'M-BP4W0BOpWiDweSo4cm', - timestamp: '2019-10-18T23:59:16.247Z', - host: { - name: ['foohost'], - os: { - platform: ['ubuntu'], - version: ['16.04.6 LTS (Xenial Xerus)'], - family: ['debian'], - name: ['Ubuntu'], - kernel: ['4.15.0-1046-gcp'], - }, - id: ['host-id-123'], - architecture: ['x86_64'], - }, - event: { - module: ['file_integrity'], - dataset: ['file'], - action: ['deleted'], - }, - file: { - path: ['/etc/gshadow.lock'], - }, -}; - -export const mockSocketOpenedEvent: Ecs = { - _id: 'Vusu4m0BOpWiDweSLkXY', - timestamp: '2019-10-19T04:02:19.473Z', - network: { - direction: ['outbound'], - transport: ['tcp'], - community_id: ['1:network-community_id'], - }, - host: { - name: ['foohost'], - architecture: ['x86_64'], - os: { - platform: ['centos'], - version: ['7 (Core)'], - family: ['redhat'], - name: ['CentOS Linux'], - kernel: ['3.10.0-1062.1.2.el7.x86_64'], - }, - id: ['host-id-123'], - }, - process: { - pid: [2166], - name: ['google_accounts'], - }, - destination: { - ip: ['10.1.2.3'], - port: [80], - }, - user: { - name: ['root'], - }, - source: { - port: [59554], - ip: ['10.4.20.1'], - }, - event: { - action: ['socket_opened'], - module: ['system'], - dataset: ['socket'], - kind: ['event'], - }, - message: [ - 'Outbound socket (10.4.20.1:59554 -> 10.1.2.3:80) OPENED by process google_accounts (PID: 2166) and user root (UID: 0)', - ], -}; - -export const mockSocketClosedEvent: Ecs = { - _id: 'V-su4m0BOpWiDweSLkXY', - timestamp: '2019-10-19T04:02:19.473Z', - process: { - pid: [2166], - name: ['google_accounts'], - }, - user: { - name: ['root'], - }, - source: { - port: [59508], - ip: ['10.4.20.1'], - }, - event: { - dataset: ['socket'], - kind: ['event'], - action: ['socket_closed'], - module: ['system'], - }, - message: [ - 'Outbound socket (10.4.20.1:59508 -> 10.1.2.3:80) CLOSED by process google_accounts (PID: 2166) and user root (UID: 0)', - ], - network: { - community_id: ['1:network-community_id'], - direction: ['outbound'], - transport: ['tcp'], - }, - destination: { - ip: ['10.1.2.3'], - port: [80], - }, - host: { - name: ['foohost'], - architecture: ['x86_64'], - os: { - version: ['7 (Core)'], - family: ['redhat'], - name: ['CentOS Linux'], - kernel: ['3.10.0-1062.1.2.el7.x86_64'], - platform: ['centos'], - }, - id: ['host-id-123'], - }, -}; - -export const mockDnsEvent: Ecs = { - _id: 'VUTUqm0BgJt5sZM7nd5g', - destination: { - domain: ['ten.one.one.one'], - port: [53], - bytes: [137], - ip: ['10.1.1.1'], - geo: { - continent_name: ['Oceania'], - location: { - lat: [-33.494], - lon: [143.2104], - }, - country_iso_code: ['AU'], - country_name: ['Australia'], - city_name: [''], - }, - }, - host: { - architecture: ['armv7l'], - id: ['host-id'], - os: { - family: ['debian'], - platform: ['raspbian'], - version: ['9 (stretch)'], - name: ['Raspbian GNU/Linux'], - kernel: ['4.19.57-v7+'], - }, - name: ['iot.example.com'], - }, - dns: { - question: { - name: ['lookup.example.com'], - type: ['A'], - }, - response_code: ['NOERROR'], - resolved_ip: ['10.1.2.3'], - }, - timestamp: '2019-10-08T10:05:23.241Z', - network: { - community_id: ['1:network-community_id'], - direction: ['outbound'], - bytes: [177], - transport: ['udp'], - protocol: ['dns'], - }, - event: { - duration: [6937500], - category: ['network_traffic'], - dataset: ['dns'], - kind: ['event'], - end: ['2019-10-08T10:05:23.248Z'], - start: ['2019-10-08T10:05:23.241Z'], - }, - source: { - port: [58732], - bytes: [40], - ip: ['10.9.9.9'], - }, -}; - -export const mockEndpointProcessExecutionMalwarePreventionAlert: Ecs = { - process: { - hash: { - md5: ['177afc1eb0be88eb9983fb74111260c4'], - sha256: ['3be13acde2f4dcded4fd8d518a513bfc9882407a6e384ffb17d12710db7d76fb'], - sha1: ['f573b85e9beb32121f1949217947b2adc6749e3d'], - }, - entity_id: [ - 'MWQxNWNmOWUtM2RjNy01Yjk3LWY1ODYtNzQzZjdjMjUxOGIyLTY5MjAtMTMyNDg5OTk2OTAuNDgzMzA3NzAw', - ], - executable: [ - 'C:\\Users\\sean\\Downloads\\3be13acde2f4dcded4fd8d518a513bfc9882407a6e384ffb17d12710db7d76fb.exe', - ], - name: [ - 'C:\\Users\\sean\\Downloads\\3be13acde2f4dcded4fd8d518a513bfc9882407a6e384ffb17d12710db7d76fb.exe', - ], - pid: [6920], - args: [ - 'C:\\Users\\sean\\Downloads\\3be13acde2f4dcded4fd8d518a513bfc9882407a6e384ffb17d12710db7d76fb.exe', - ], - }, - host: { - os: { - full: ['Windows Server 2019 Datacenter 1809 (10.0.17763.1518)'], - name: ['Windows'], - version: ['1809 (10.0.17763.1518)'], - platform: ['windows'], - family: ['windows'], - kernel: ['1809 (10.0.17763.1518)'], - }, - mac: ['aa:bb:cc:dd:ee:ff'], - architecture: ['x86_64'], - ip: ['10.1.2.3'], - id: ['d8ad572e-d224-4044-a57d-f5a84c0dfe5d'], - name: ['win2019-endpoint-1'], - }, - file: { - mtime: ['2020-11-04T21:40:51.494Z'], - path: [ - 'C:\\Users\\sean\\Downloads\\3be13acde2f4dcded4fd8d518a513bfc9882407a6e384ffb17d12710db7d76fb.exe', - ], - owner: ['sean'], - hash: { - md5: ['177afc1eb0be88eb9983fb74111260c4'], - sha256: ['3be13acde2f4dcded4fd8d518a513bfc9882407a6e384ffb17d12710db7d76fb'], - sha1: ['f573b85e9beb32121f1949217947b2adc6749e3d'], - }, - name: ['3be13acde2f4dcded4fd8d518a513bfc9882407a6e384ffb17d12710db7d76fb.exe'], - extension: ['exe'], - size: [1604112], - }, - event: { - category: ['malware', 'intrusion_detection', 'process'], - outcome: ['success'], - severity: [73], - code: ['malicious_file'], - action: ['execution'], - id: ['LsuMZVr+sdhvehVM++++Gp2Y'], - kind: ['alert'], - created: ['2020-11-04T21:41:30.533Z'], - module: ['endpoint'], - type: ['info', 'start', 'denied'], - dataset: ['endpoint.alerts'], - }, - agent: { - type: ['endpoint'], - }, - timestamp: '2020-11-04T21:41:30.533Z', - message: ['Malware Prevention Alert'], - _id: '0dA2lXUBn9bLIbfPkY7d', -}; - -export const mockEndpointLibraryLoadEvent: Ecs = { - file: { - path: ['C:\\Windows\\System32\\bcrypt.dll'], - hash: { - md5: ['00439016776de367bad087d739a03797'], - sha1: ['2c4ba5c1482987d50a182bad915f52cd6611ee63'], - sha256: ['e70f5d8f87aab14e3160227d38387889befbe37fa4f8f5adc59eff52804b35fd'], - }, - name: ['bcrypt.dll'], - }, - host: { - os: { - full: ['Windows Server 2019 Datacenter 1809 (10.0.17763.1697)'], - name: ['Windows'], - version: ['1809 (10.0.17763.1697)'], - family: ['windows'], - kernel: ['1809 (10.0.17763.1697)'], - platform: ['windows'], - }, - mac: ['aa:bb:cc:dd:ee:ff'], - name: ['win2019-endpoint-1'], - architecture: ['x86_64'], - ip: ['10.1.2.3'], - id: ['d8ad572e-d224-4044-a57d-f5a84c0dfe5d'], - }, - event: { - category: ['library'], - kind: ['event'], - created: ['2021-02-05T21:27:23.921Z'], - module: ['endpoint'], - action: ['load'], - type: ['start'], - id: ['LzzWB9jjGmCwGMvk++++Da5H'], - dataset: ['endpoint.events.library'], - }, - process: { - name: ['sshd.exe'], - pid: [9644], - entity_id: [ - 'MWQxNWNmOWUtM2RjNy01Yjk3LWY1ODYtNzQzZjdjMjUxOGIyLTk2NDQtMTMyNTcwMzQwNDEuNzgyMTczODAw', - ], - executable: ['C:\\Program Files\\OpenSSH-Win64\\sshd.exe'], - }, - agent: { - type: ['endpoint'], - }, - user: { - name: ['SYSTEM'], - domain: ['NT AUTHORITY'], - }, - message: ['Endpoint DLL load event'], - timestamp: '2021-02-05T21:27:23.921Z', - _id: 'IAUYdHcBGrBB52F2zo8Q', -}; - -export const mockEndpointRegistryModificationEvent: Ecs = { - host: { - os: { - full: ['Windows Server 2019 Datacenter 1809 (10.0.17763.1697)'], - name: ['Windows'], - version: ['1809 (10.0.17763.1697)'], - family: ['windows'], - kernel: ['1809 (10.0.17763.1697)'], - platform: ['windows'], - }, - mac: ['aa:bb:cc:dd:ee:ff'], - name: ['win2019-endpoint-1'], - architecture: ['x86_64'], - ip: ['10.1.2.3'], - id: ['d8ad572e-d224-4044-a57d-f5a84c0dfe5d'], - }, - event: { - category: ['registry'], - kind: ['event'], - created: ['2021-02-04T13:44:31.559Z'], - module: ['endpoint'], - action: ['modification'], - type: ['change'], - id: ['LzzWB9jjGmCwGMvk++++CbOn'], - dataset: ['endpoint.events.registry'], - }, - process: { - name: ['GoogleUpdate.exe'], - pid: [7408], - entity_id: [ - 'MWQxNWNmOWUtM2RjNy01Yjk3LWY1ODYtNzQzZjdjMjUxOGIyLTc0MDgtMTMyNTY5MTk4NDguODY4NTI0ODAw', - ], - executable: ['C:\\Program Files (x86)\\Google\\Update\\GoogleUpdate.exe'], - }, - registry: { - hive: ['HKLM'], - key: [ - 'SOFTWARE\\WOW6432Node\\Google\\Update\\ClientState\\{430FD4D0-B729-4F61-AA34-91526481799D}\\CurrentState', - ], - path: [ - 'HKLM\\SOFTWARE\\WOW6432Node\\Google\\Update\\ClientState\\{430FD4D0-B729-4F61-AA34-91526481799D}\\CurrentState\\StateValue', - ], - value: ['StateValue'], - }, - agent: { - type: ['endpoint'], - }, - user: { - name: ['SYSTEM'], - domain: ['NT AUTHORITY'], - }, - message: ['Endpoint registry event'], - timestamp: '2021-02-04T13:44:31.559Z', - _id: '4cxLbXcBGrBB52F2uOfF', -}; - -export const mockTgridModel: TGridModel = { - columns: [ - { - columnHeaderType: 'not-filtered', - id: '@timestamp', - initialWidth: 190, - }, - { - columnHeaderType: 'not-filtered', - id: 'message', - initialWidth: 180, - }, - { - columnHeaderType: 'not-filtered', - id: 'event.category', - initialWidth: 180, - }, - { - columnHeaderType: 'not-filtered', - id: 'host.name', - initialWidth: 180, - }, - { - columnHeaderType: 'not-filtered', - id: 'source.ip', - initialWidth: 180, - }, - { - columnHeaderType: 'not-filtered', - id: 'destination.ip', - initialWidth: 180, - }, - { - columnHeaderType: 'not-filtered', - id: 'user.name', - initialWidth: 180, - }, - ], - dataViewId: null, - selectAll: false, - totalCount: 0, - defaultColumns: [], - queryFields: [], - deletedEventIds: [], - expandedDetail: {}, - id: 'ef579e40-jibber-jabber', - indexNames: [], - isLoading: false, - isSelectAllChecked: false, - itemsPerPage: 25, - itemsPerPageOptions: [10, 25, 50, 100], - loadingEventIds: [], - selectedEventIds: {}, - showCheckboxes: false, - sort: [ - { - columnId: '@timestamp', - columnType: 'date', - esTypes: ['date'], - sortDirection: Direction.desc, - }, - ], - title: 'Test rule', - sessionViewConfig: null, -}; diff --git a/x-pack/plugins/timelines/public/mock/plugin_mock.tsx b/x-pack/plugins/timelines/public/mock/plugin_mock.tsx index 450aae63a1988..2813acb840966 100644 --- a/x-pack/plugins/timelines/public/mock/plugin_mock.tsx +++ b/x-pack/plugins/timelines/public/mock/plugin_mock.tsx @@ -5,22 +5,14 @@ * 2.0. */ import React from 'react'; -import { - LastUpdatedAt, - LastUpdatedAtProps, - LoadingPanelProps, - LoadingPanel, - useDraggableKeyboardWrapper, -} from '../components'; +import { LastUpdatedAt, LastUpdatedAtProps, LoadingPanel, LoadingPanelProps } from '../components'; import { useAddToTimeline, useAddToTimelineSensor } from '../hooks/use_add_to_timeline'; import { mockHoverActions } from './mock_hover_actions'; export const createTGridMocks = () => ({ getHoverActions: () => mockHoverActions, - getTGrid: () => <>{'hello grid'}, getLastUpdated: (props: LastUpdatedAtProps) => , getLoadingPanel: (props: LoadingPanelProps) => , getUseAddToTimeline: () => useAddToTimeline, getUseAddToTimelineSensor: () => useAddToTimelineSensor, - getUseDraggableKeyboardWrapper: () => useDraggableKeyboardWrapper, }); diff --git a/x-pack/plugins/timelines/public/mock/t_grid.tsx b/x-pack/plugins/timelines/public/mock/t_grid.tsx deleted file mode 100644 index 75d23fccd7c75..0000000000000 --- a/x-pack/plugins/timelines/public/mock/t_grid.tsx +++ /dev/null @@ -1,144 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { ALERT_START, ALERT_STATUS } from '@kbn/rule-data-utils'; -import { TGridIntegratedProps } from '../components/t_grid/integrated'; -import { mockBrowserFields, mockRuntimeMappings } from './browser_fields'; -import { mockTimelineData } from './mock_timeline_data'; -import { ColumnHeaderOptions } from '../../common/types'; -import { mockIndexNames, mockIndexPattern } from './index_pattern'; -import { EventRenderedViewProps } from '../components/t_grid/event_rendered_view'; -import { TableId } from '../types'; - -const columnHeaders: ColumnHeaderOptions[] = [ - { - columnHeaderType: 'not-filtered', - displayAsText: 'Status', - id: ALERT_STATUS, - initialWidth: 79, - category: 'kibana', - type: 'string', - aggregatable: true, - actions: { - showSortAsc: { - label: 'Sort A-Z', - }, - showSortDesc: { - label: 'Sort Z-A', - }, - }, - defaultSortDirection: 'desc', - display: { - key: null, - ref: null, - props: { - children: { - key: null, - ref: null, - props: { - children: 'Status', - }, - _owner: null, - }, - }, - _owner: null, - }, - isSortable: true, - }, - { - columnHeaderType: 'not-filtered', - displayAsText: 'Triggered', - id: ALERT_START, - initialWidth: 176, - category: 'kibana', - type: 'date', - aggregatable: true, - actions: { - showSortAsc: { - label: 'Sort A-Z', - }, - showSortDesc: { - label: 'Sort Z-A', - }, - }, - defaultSortDirection: 'desc', - display: { - key: null, - ref: null, - props: { - children: { - key: null, - ref: null, - props: { - children: 'Triggered', - }, - _owner: null, - }, - }, - _owner: null, - }, - isSortable: true, - }, -]; - -export const tGridIntegratedProps: TGridIntegratedProps = { - additionalFilters: null, - appId: '', - browserFields: mockBrowserFields, - columns: columnHeaders, - dataViewId: 'data-view-id', - deletedEventIds: [], - disabledCellActions: [], - end: '2021-08-19T00:30:00.000Z', - entityType: 'alerts', - filterStatus: 'open', - filters: [], - globalFullScreen: false, - graphEventId: undefined, - id: '', - indexNames: mockIndexNames, - indexPattern: mockIndexPattern, - isLive: false, - isLoadingIndexPattern: false, - itemsPerPage: 25, - itemsPerPageOptions: [10, 25, 50, 100], - query: { - query: '_id: "28bf94ad5d16b5fded1b258127aa88792f119d7e018c35869121613385619e1e"', - language: 'kuery', - }, - renderCellValue: () => null, - rowRenderers: [], - runtimeMappings: mockRuntimeMappings, - setQuery: () => null, - sort: [ - { - columnId: '@timestamp', - columnType: 'date', - esTypes: ['date'], - sortDirection: 'desc', - }, - ], - start: '2021-05-01T18:14:07.522Z', - tGridEventRenderedViewEnabled: true, - trailingControlColumns: [], -}; - -export const eventRenderedProps: EventRenderedViewProps = { - alertToolbar: <>, - appId: '', - events: mockTimelineData, - leadingControlColumns: [], - onChangePage: () => null, - onChangeItemsPerPage: () => null, - pageIndex: 0, - pageSize: 10, - pageSizeOptions: [10, 25, 50, 100], - rowRenderers: [], - timelineId: TableId.alertsOnAlertsPage, - totalItemCount: 100, -}; diff --git a/x-pack/plugins/timelines/public/mock/test_providers.tsx b/x-pack/plugins/timelines/public/mock/test_providers.tsx index 6d9f1823bb316..08085a7fbf333 100644 --- a/x-pack/plugins/timelines/public/mock/test_providers.tsx +++ b/x-pack/plugins/timelines/public/mock/test_providers.tsx @@ -9,45 +9,48 @@ import { euiDarkVars } from '@kbn/ui-theme'; import { I18nProvider } from '@kbn/i18n-react'; import React from 'react'; -import { DragDropContext, DropResult, ResponderProvided } from 'react-beautiful-dnd'; import { Provider as ReduxStoreProvider } from 'react-redux'; import { Store } from 'redux'; import { BehaviorSubject } from 'rxjs'; import { ThemeProvider } from 'styled-components'; -import { createStore, TableState } from '../types'; -import { mockGlobalState } from './global_state'; +import { configureStore } from '@reduxjs/toolkit'; import { createKibanaContextProviderMock, createStartServicesMock } from './kibana_react.mock'; -import { createSecuritySolutionStorageMock, localStorageMock } from './mock_local_storage'; - -const state: TableState = mockGlobalState; +import { timelineReducer } from '../store/timeline/reducer'; interface Props { children: React.ReactNode; store?: Store; - onDragEnd?: (result: DropResult, provided: ResponderProvided) => void; } export const kibanaObservable = new BehaviorSubject(createStartServicesMock()); -Object.defineProperty(window, 'localStorage', { - value: localStorageMock(), -}); +interface State { + timelineById: Record; +} + +const state: State = { + timelineById: { + test: {}, + }, +}; + window.scrollTo = jest.fn(); const MockKibanaContextProvider = createKibanaContextProviderMock(); -const { storage } = createSecuritySolutionStorageMock(); /** A utility for wrapping children in the providers required to run most tests */ const TestProvidersComponent: React.FC = ({ children, - store = createStore(state, storage), - onDragEnd = jest.fn(), + store = configureStore({ + preloadedState: state, + reducer: timelineReducer, + }), }) => ( ({ eui: euiDarkVars, darkMode: true })}> - {children} + {children} diff --git a/x-pack/plugins/timelines/public/plugin.ts b/x-pack/plugins/timelines/public/plugin.ts index 27ca9de0cf034..8de44309769da 100644 --- a/x-pack/plugins/timelines/public/plugin.ts +++ b/x-pack/plugins/timelines/public/plugin.ts @@ -6,20 +6,16 @@ */ import { Store, Unsubscribe } from 'redux'; -import { Storage } from '@kbn/kibana-utils-plugin/public'; import type { CoreSetup, Plugin, CoreStart } from '@kbn/core/public'; -import type { LastUpdatedAtProps, LoadingPanelProps } from './components'; -import { getLastUpdatedLazy, getLoadingPanelLazy, getTGridLazy } from './methods'; -import type { TimelinesUIStart, TGridProps, TimelinesStartPlugins } from './types'; -import { tGridReducer } from './store/t_grid/reducer'; -import { useDraggableKeyboardWrapper } from './components/drag_and_drop/draggable_keyboard_wrapper_hook'; +import { getLastUpdatedLazy, getLoadingPanelLazy } from './methods'; +import type { TimelinesUIStart, TimelinesStartPlugins } from './types'; import { useAddToTimeline, useAddToTimelineSensor } from './hooks/use_add_to_timeline'; import { getHoverActions, HoverActionsConfig } from './components/hover_actions'; import { timelineReducer } from './store/timeline/reducer'; +import { LastUpdatedAtProps, LoadingPanelProps } from './components'; export class TimelinesPlugin implements Plugin { private _store: Store | undefined; - private _storage = new Storage(localStorage); private _storeUnsubscribe: Unsubscribe | undefined; private _hoverActions: HoverActionsConfig | undefined; @@ -37,17 +33,6 @@ export class TimelinesPlugin implements Plugin { return this._hoverActions; } }, - getTGrid: (props: TGridProps) => { - return getTGridLazy(props, { - store: this._store, - storage: this._storage, - setStore: this.setStore.bind(this), - data, - }); - }, - getTGridReducer: () => { - return tGridReducer; - }, getTimelineReducer: () => { return timelineReducer; }, @@ -63,10 +48,7 @@ export class TimelinesPlugin implements Plugin { getUseAddToTimelineSensor: () => { return useAddToTimelineSensor; }, - getUseDraggableKeyboardWrapper: () => { - return useDraggableKeyboardWrapper; - }, - setTGridEmbeddedStore: (store: Store) => { + setTimelineEmbeddedStore: (store: Store) => { this.setStore(store); }, }; diff --git a/x-pack/plugins/timelines/public/store/t_grid/actions.ts b/x-pack/plugins/timelines/public/store/t_grid/actions.ts deleted file mode 100644 index c9d420f3610e9..0000000000000 --- a/x-pack/plugins/timelines/public/store/t_grid/actions.ts +++ /dev/null @@ -1,130 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import actionCreatorFactory from 'typescript-fsa'; -import type { TimelineNonEcsData } from '../../../common/search_strategy'; -import type { - ColumnHeaderOptions, - SortColumnTable, - DataExpandedDetailType, - SessionViewConfig, -} from '../../../common/types/timeline'; -import { InitialyzeTGridSettings, TGridPersistInput } from './types'; - -const actionCreator = actionCreatorFactory('x-pack/timelines/t-grid'); - -export const createTGrid = actionCreator('CREATE_TGRID'); - -export const upsertColumn = actionCreator<{ - column: ColumnHeaderOptions; - id: string; - index: number; -}>('UPSERT_COLUMN'); - -export const applyDeltaToColumnWidth = actionCreator<{ - id: string; - columnId: string; - delta: number; -}>('APPLY_DELTA_TO_COLUMN_WIDTH'); - -export const updateColumnOrder = actionCreator<{ - columnIds: string[]; - id: string; -}>('UPDATE_COLUMN_ORDER'); - -export const updateColumnWidth = actionCreator<{ - columnId: string; - id: string; - width: number; -}>('UPDATE_COLUMN_WIDTH'); - -export type TableToggleDetailPanel = DataExpandedDetailType & { - tabType?: string; - id: string; -}; - -export const toggleDetailPanel = actionCreator('TOGGLE_DETAIL_PANEL'); - -export const removeColumn = actionCreator<{ - id: string; - columnId: string; -}>('REMOVE_COLUMN'); - -export const updateIsLoading = actionCreator<{ - id: string; - isLoading: boolean; -}>('UPDATE_LOADING'); - -export const updateColumns = actionCreator<{ - id: string; - columns: ColumnHeaderOptions[]; -}>('UPDATE_COLUMNS'); - -export const updateItemsPerPage = actionCreator<{ id: string; itemsPerPage: number }>( - 'UPDATE_ITEMS_PER_PAGE' -); - -export const updateItemsPerPageOptions = actionCreator<{ - id: string; - itemsPerPageOptions: number[]; -}>('UPDATE_ITEMS_PER_PAGE_OPTIONS'); - -export const updateSort = actionCreator<{ id: string; sort: SortColumnTable[] }>('UPDATE_SORT'); - -export const setSelected = actionCreator<{ - id: string; - eventIds: Readonly>; - isSelected: boolean; - isSelectAllChecked: boolean; -}>('SET_TGRID_SELECTED'); - -export const clearSelected = actionCreator<{ - id: string; -}>('CLEAR_TGRID_SELECTED'); - -export const setEventsLoading = actionCreator<{ - id: string; - eventIds: string[]; - isLoading: boolean; -}>('SET_TGRID_EVENTS_LOADING'); - -export const clearEventsLoading = actionCreator<{ - id: string; -}>('CLEAR_TGRID_EVENTS_LOADING'); - -export const setEventsDeleted = actionCreator<{ - id: string; - eventIds: string[]; - isDeleted: boolean; -}>('SET_TGRID_EVENTS_DELETED'); - -export const clearEventsDeleted = actionCreator<{ - id: string; -}>('CLEAR_TGRID_EVENTS_DELETED'); - -export const initializeTGridSettings = actionCreator('INITIALIZE_TGRID'); - -export const setTGridSelectAll = actionCreator<{ id: string; selectAll: boolean }>( - 'SET_TGRID_SELECT_ALL' -); - -export const updateGraphEventId = actionCreator<{ id: string; graphEventId: string }>( - 'UPDATE_TGRID_GRAPH_EVENT_ID' -); - -export const updateSessionViewConfig = actionCreator<{ - id: string; - sessionViewConfig: SessionViewConfig | null; -}>('UPDATE_TGRID_SESSION_VIEW_CONFIG'); - -export const setTableUpdatedAt = actionCreator<{ id: string; updated: number }>( - 'SET_TABLE_UPDATED_AT' -); - -export const updateTotalCount = actionCreator<{ id: string; totalCount: number }>( - 'UPDATE_TOTAL_COUNT' -); diff --git a/x-pack/plugins/timelines/public/store/t_grid/defaults.ts b/x-pack/plugins/timelines/public/store/t_grid/defaults.ts deleted file mode 100644 index 96b407ed72152..0000000000000 --- a/x-pack/plugins/timelines/public/store/t_grid/defaults.ts +++ /dev/null @@ -1,105 +0,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 { Direction } from '../../../common/search_strategy'; -import type { ColumnHeaderOptions, ColumnHeaderType } from '../../../common/types/timeline'; -import { - DEFAULT_COLUMN_MIN_WIDTH, - DEFAULT_DATE_COLUMN_MIN_WIDTH, -} from '../../components/t_grid/body/constants'; -import type { SubsetTGridModel } from './model'; -import * as i18n from './translations'; - -export const defaultColumnHeaderType: ColumnHeaderType = 'not-filtered'; - -export const defaultHeaders: ColumnHeaderOptions[] = [ - { - columnHeaderType: defaultColumnHeaderType, - id: '@timestamp', - initialWidth: DEFAULT_DATE_COLUMN_MIN_WIDTH, - esTypes: ['date'], - type: 'date', - }, - { - columnHeaderType: defaultColumnHeaderType, - id: 'message', - initialWidth: DEFAULT_COLUMN_MIN_WIDTH, - }, - { - columnHeaderType: defaultColumnHeaderType, - id: 'event.category', - initialWidth: DEFAULT_COLUMN_MIN_WIDTH, - }, - { - columnHeaderType: defaultColumnHeaderType, - id: 'event.action', - initialWidth: DEFAULT_COLUMN_MIN_WIDTH, - }, - { - columnHeaderType: defaultColumnHeaderType, - id: 'host.name', - initialWidth: DEFAULT_COLUMN_MIN_WIDTH, - }, - { - columnHeaderType: defaultColumnHeaderType, - id: 'source.ip', - initialWidth: DEFAULT_COLUMN_MIN_WIDTH, - }, - { - columnHeaderType: defaultColumnHeaderType, - id: 'destination.ip', - initialWidth: DEFAULT_COLUMN_MIN_WIDTH, - }, - { - columnHeaderType: defaultColumnHeaderType, - id: 'user.name', - initialWidth: DEFAULT_COLUMN_MIN_WIDTH, - }, -]; - -export const tGridDefaults: SubsetTGridModel = { - columns: defaultHeaders, - defaultColumns: defaultHeaders, - dataViewId: null, - deletedEventIds: [], - expandedDetail: {}, - selectAll: false, - totalCount: 0, - filters: [], - indexNames: [], - isLoading: false, - isSelectAllChecked: false, - itemsPerPage: 50, - itemsPerPageOptions: [10, 25, 50, 100], - loadingEventIds: [], - selectedEventIds: {}, - showCheckboxes: false, - sort: [ - { - columnId: '@timestamp', - columnType: 'date', - esTypes: ['date'], - sortDirection: Direction.desc, - }, - ], - graphEventId: '', - sessionViewConfig: null, - queryFields: [], -}; - -export const getTGridManageDefaults = (id: string) => ({ - defaultColumns: defaultHeaders, - loadingText: i18n.LOADING_EVENTS, - documentType: '', - selectAll: false, - id, - isLoading: false, - queryFields: [], - title: '', - unit: (n: number) => i18n.UNIT(n), - graphEventId: '', -}); diff --git a/x-pack/plugins/timelines/public/store/t_grid/helpers.test.tsx b/x-pack/plugins/timelines/public/store/t_grid/helpers.test.tsx deleted file mode 100644 index 509ae15b0a8e1..0000000000000 --- a/x-pack/plugins/timelines/public/store/t_grid/helpers.test.tsx +++ /dev/null @@ -1,173 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { SortColumnTable } from '../../../common/types'; -import { tGridDefaults } from './defaults'; -import { - setInitializeTgridSettings, - updateTGridColumnOrder, - updateTGridColumnWidth, -} from './helpers'; -import { mockGlobalState } from '../../mock/global_state'; - -import { TableId, TGridModelSettings } from '.'; - -const id = 'foo'; -const defaultTableById = { - ...mockGlobalState.tableById, -}; - -describe('setInitializeTgridSettings', () => { - test('it returns the expected sort when tGridSettingsProps has an override', () => { - const sort: SortColumnTable[] = [ - { columnId: 'foozle', columnType: 'date', esTypes: ['date'], sortDirection: 'asc' }, - ]; - - const tGridSettingsProps: Partial = { - sort, // <-- override - }; - - expect( - setInitializeTgridSettings({ id, tableById: defaultTableById, tGridSettingsProps })[id].sort - ).toEqual(sort); - }); - - test('it returns the default sort when tGridSettingsProps does NOT contain an override', () => { - const tGridSettingsProps = {}; // <-- no `sort` override - - expect( - setInitializeTgridSettings({ id, tableById: defaultTableById, tGridSettingsProps })[id].sort - ).toEqual(tGridDefaults.sort); - }); - - test('it doesn`t overwrite the timeline if it is initialized', () => { - const tGridSettingsProps = { title: 'testTitle' }; - - const tableById = { - [id]: { - ...defaultTableById[TableId.test], - initialized: true, - }, - }; - - const result = setInitializeTgridSettings({ - id, - tableById: { ...defaultTableById, ...tableById }, - tGridSettingsProps, - }); - expect(result[id]).toBe(tableById[id]); - }); -}); - -describe('updateTGridColumnOrder', () => { - test('it returns the columns in the new expected order', () => { - const originalIdOrder = defaultTableById[TableId.test].columns.map((x) => x.id); // ['@timestamp', 'event.severity', 'event.category', '...'] - - // the new order swaps the positions of the first and second columns: - const newIdOrder = [originalIdOrder[1], originalIdOrder[0], ...originalIdOrder.slice(2)]; // ['event.severity', '@timestamp', 'event.category', '...'] - - expect( - updateTGridColumnOrder({ - columnIds: newIdOrder, - id: TableId.test, - tableById: defaultTableById, - }) - ).toEqual({ - ...defaultTableById, - [TableId.test]: { - ...defaultTableById[TableId.test], - columns: [ - defaultTableById[TableId.test].columns[1], // event.severity - defaultTableById[TableId.test].columns[0], // @timestamp - ...defaultTableById[TableId.test].columns.slice(2), // all remaining columns - ], - }, - }); - }); - - test('it omits unknown column IDs when re-ordering columns', () => { - const originalIdOrder = defaultTableById[TableId.test].columns.map((x) => x.id); // ['@timestamp', 'event.severity', 'event.category', '...'] - const unknownColumId = 'does.not.exist'; - const newIdOrder = [originalIdOrder[0], unknownColumId, ...originalIdOrder.slice(1)]; // ['@timestamp', 'does.not.exist', 'event.severity', 'event.category', '...'] - - expect( - updateTGridColumnOrder({ - columnIds: newIdOrder, - id: TableId.test, - tableById: defaultTableById, - }) - ).toEqual({ - ...defaultTableById, - [TableId.test]: { - ...defaultTableById[TableId.test], - }, - }); - }); - - test('it returns an empty collection of columns if none of the new column IDs are found', () => { - const newIdOrder = ['this.id.does.NOT.exist', 'this.id.also.does.NOT.exist']; // all unknown IDs - - expect( - updateTGridColumnOrder({ - columnIds: newIdOrder, - id: TableId.test, - tableById: defaultTableById, - }) - ).toEqual({ - ...defaultTableById, - [TableId.test]: { - ...defaultTableById[TableId.test], - columns: [], // <-- empty, because none of the new column IDs match the old IDs - }, - }); - }); -}); - -describe('updateTGridColumnWidth', () => { - test("it updates (only) the specified column's width", () => { - const columnId = '@timestamp'; - const width = 1234; - - const expectedUpdatedColumn = { - ...defaultTableById[TableId.test].columns[0], // @timestamp - initialWidth: width, - }; - - expect( - updateTGridColumnWidth({ - columnId, - id: TableId.test, - tableById: defaultTableById, - width, - }) - ).toEqual({ - ...defaultTableById, - [TableId.test]: { - ...defaultTableById[TableId.test], - columns: [expectedUpdatedColumn, ...defaultTableById[TableId.test].columns.slice(1)], - }, - }); - }); - - test('it is a noop if the the specified column is unknown', () => { - const unknownColumId = 'does.not.exist'; - - expect( - updateTGridColumnWidth({ - columnId: unknownColumId, - id: TableId.test, - tableById: defaultTableById, - width: 90210, - }) - ).toEqual({ - ...defaultTableById, - [TableId.test]: { - ...defaultTableById[TableId.test], - }, - }); - }); -}); diff --git a/x-pack/plugins/timelines/public/store/t_grid/helpers.ts b/x-pack/plugins/timelines/public/store/t_grid/helpers.ts deleted file mode 100644 index 0e1c8e1217125..0000000000000 --- a/x-pack/plugins/timelines/public/store/t_grid/helpers.ts +++ /dev/null @@ -1,500 +0,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 { omit, union } from 'lodash/fp'; - -import { isEmpty } from 'lodash'; -import { EuiDataGridColumn } from '@elastic/eui'; -import type { TableToggleDetailPanel } from './actions'; -import { TGridPersistInput, TableById } from './types'; -import type { TGridModelSettings } from './model'; - -import { - ColumnHeaderOptions, - SortColumnTable, - DataExpandedDetail, - DataExpandedDetailType, - SessionViewConfig, -} from '../../../common/types/timeline'; -import { getTGridManageDefaults, tGridDefaults } from './defaults'; - -export const isNotNull = (value: T | null): value is T => value !== null; -export type Maybe = T | null; - -/** The default minimum width of a column (when a width for the column type is not specified) */ -export const DEFAULT_COLUMN_MIN_WIDTH = 180; // px - -/** The minimum width of a resized column */ -export const RESIZED_COLUMN_MIN_WITH = 70; // px - -interface AddTableColumnParams { - column: ColumnHeaderOptions; - id: string; - index: number; - tableById: TableById; -} - -interface TableNonEcsData { - field: string; - value?: Maybe; -} - -interface CreateTGridParams extends TGridPersistInput { - tableById: TableById; -} - -/** Adds a new `Table` to the provided collection of `TableById` */ -export const createInitTGrid = ({ id, tableById, ...tGridProps }: CreateTGridParams): TableById => { - const dataTable = tableById[id]; - return { - ...tableById, - [id]: { - ...dataTable, - ...tGridDefaults, - ...tGridProps, - isLoading: false, - }, - }; -}; - -/** - * Adds or updates a column. When updating a column, it will be moved to the - * new index - */ -export const upsertTableColumn = ({ - column, - id, - index, - tableById, -}: AddTableColumnParams): TableById => { - const dataTable = tableById[id]; - const alreadyExistsAtIndex = dataTable.columns.findIndex((c) => c.id === column.id); - - if (alreadyExistsAtIndex !== -1) { - // remove the existing entry and add the new one at the specified index - const reordered = dataTable.columns.filter((c) => c.id !== column.id); - reordered.splice(index, 0, column); // ⚠️ mutation - - return { - ...tableById, - [id]: { - ...dataTable, - columns: reordered, - }, - }; - } - - // add the new entry at the specified index - const columns = [...dataTable.columns]; - columns.splice(index, 0, column); // ⚠️ mutation - - return { - ...tableById, - [id]: { - ...dataTable, - columns, - }, - }; -}; - -interface RemoveTableColumnParams { - id: string; - columnId: string; - tableById: TableById; -} - -export const removeTableColumn = ({ - id, - columnId, - tableById, -}: RemoveTableColumnParams): TableById => { - const dataTable = tableById[id]; - - const columns = dataTable.columns.filter((c) => c.id !== columnId); - - return { - ...tableById, - [id]: { - ...dataTable, - columns, - }, - }; -}; - -interface InitializeTgridParams { - id: string; - tableById: TableById; - tGridSettingsProps: Partial; -} - -export const setInitializeTgridSettings = ({ - id, - tableById, - tGridSettingsProps, -}: InitializeTgridParams): TableById => { - const dataTable = tableById[id]; - - return !dataTable?.initialized - ? { - ...tableById, - [id]: { - ...tGridDefaults, - ...getTGridManageDefaults(id), - ...dataTable, - ...tGridSettingsProps, - ...(!dataTable || - (isEmpty(dataTable.columns) && !isEmpty(tGridSettingsProps.defaultColumns)) - ? { columns: tGridSettingsProps.defaultColumns } - : {}), - sort: tGridSettingsProps.sort ?? tGridDefaults.sort, - loadingEventIds: tGridDefaults.loadingEventIds, - initialized: true, - }, - } - : tableById; -}; - -interface ApplyDeltaToTableColumnWidth { - id: string; - columnId: string; - delta: number; - tableById: TableById; -} - -export const applyDeltaToTableColumnWidth = ({ - id, - columnId, - delta, - tableById, -}: ApplyDeltaToTableColumnWidth): TableById => { - const dataTable = tableById[id]; - - const columnIndex = dataTable.columns.findIndex((c) => c.id === columnId); - if (columnIndex === -1) { - // the column was not found - return { - ...tableById, - [id]: { - ...dataTable, - }, - }; - } - - const requestedWidth = - (dataTable.columns[columnIndex].initialWidth ?? DEFAULT_COLUMN_MIN_WIDTH) + delta; // raw change in width - const initialWidth = Math.max(RESIZED_COLUMN_MIN_WITH, requestedWidth); // if the requested width is smaller than the min, use the min - - const columnWithNewWidth = { - ...dataTable.columns[columnIndex], - initialWidth, - }; - - const columns = [ - ...dataTable.columns.slice(0, columnIndex), - columnWithNewWidth, - ...dataTable.columns.slice(columnIndex + 1), - ]; - - return { - ...tableById, - [id]: { - ...dataTable, - columns, - }, - }; -}; - -type Columns = Array< - Pick & ColumnHeaderOptions ->; - -export const updateTGridColumnOrder = ({ - columnIds, - id, - tableById, -}: { - columnIds: string[]; - id: string; - tableById: TableById; -}): TableById => { - const dataTable = tableById[id]; - - const columns = columnIds.reduce((acc, cid) => { - const columnIndex = dataTable.columns.findIndex((c) => c.id === cid); - - return columnIndex !== -1 ? [...acc, dataTable.columns[columnIndex]] : acc; - }, []); - - return { - ...tableById, - [id]: { - ...dataTable, - columns, - }, - }; -}; - -export const updateTGridColumnWidth = ({ - columnId, - id, - tableById, - width, -}: { - columnId: string; - id: string; - tableById: TableById; - width: number; -}): TableById => { - const dataTable = tableById[id]; - - const columns = dataTable.columns.map((x) => ({ - ...x, - initialWidth: x.id === columnId ? width : x.initialWidth, - })); - - return { - ...tableById, - [id]: { - ...dataTable, - columns, - }, - }; -}; - -interface UpdateTableColumnsParams { - id: string; - columns: ColumnHeaderOptions[]; - tableById: TableById; -} - -export const updateTableColumns = ({ - id, - columns, - tableById, -}: UpdateTableColumnsParams): TableById => { - const dataTable = tableById[id]; - return { - ...tableById, - [id]: { - ...dataTable, - columns, - }, - }; -}; - -interface UpdateTableSortParams { - id: string; - sort: SortColumnTable[]; - tableById: TableById; -} - -export const updateTableSort = ({ id, sort, tableById }: UpdateTableSortParams): TableById => { - const dataTable = tableById[id]; - return { - ...tableById, - [id]: { - ...dataTable, - sort, - }, - }; -}; - -interface UpdateTableItemsPerPageParams { - id: string; - itemsPerPage: number; - tableById: TableById; -} - -export const updateTableItemsPerPage = ({ - id, - itemsPerPage, - tableById, -}: UpdateTableItemsPerPageParams) => { - const dataTable = tableById[id]; - return { - ...tableById, - [id]: { - ...dataTable, - itemsPerPage, - }, - }; -}; - -interface UpdateTablePerPageOptionsParams { - id: string; - itemsPerPageOptions: number[]; - tableById: TableById; -} - -export const updateTablePerPageOptions = ({ - id, - itemsPerPageOptions, - tableById, -}: UpdateTablePerPageOptionsParams) => { - const dataTable = tableById[id]; - return { - ...tableById, - [id]: { - ...dataTable, - itemsPerPageOptions, - }, - }; -}; - -interface SetDeletedTableEventsParams { - id: string; - eventIds: string[]; - isDeleted: boolean; - tableById: TableById; -} - -export const setDeletedTableEvents = ({ - id, - eventIds, - isDeleted, - tableById, -}: SetDeletedTableEventsParams): TableById => { - const dataTable = tableById[id]; - - const deletedEventIds = isDeleted - ? union(dataTable.deletedEventIds, eventIds) - : dataTable.deletedEventIds.filter((currentEventId) => !eventIds.includes(currentEventId)); - - const selectedEventIds = Object.fromEntries( - Object.entries(dataTable.selectedEventIds).filter( - ([selectedEventId]) => !deletedEventIds.includes(selectedEventId) - ) - ); - - const isSelectAllChecked = - Object.keys(selectedEventIds).length > 0 ? dataTable.isSelectAllChecked : false; - - return { - ...tableById, - [id]: { - ...dataTable, - deletedEventIds, - selectedEventIds, - isSelectAllChecked, - }, - }; -}; - -interface SetLoadingTableEventsParams { - id: string; - eventIds: string[]; - isLoading: boolean; - tableById: TableById; -} - -export const setLoadingTableEvents = ({ - id, - eventIds, - isLoading, - tableById, -}: SetLoadingTableEventsParams): TableById => { - const dataTable = tableById[id]; - - const loadingEventIds = isLoading - ? union(dataTable.loadingEventIds, eventIds) - : dataTable.loadingEventIds.filter((currentEventId) => !eventIds.includes(currentEventId)); - - return { - ...tableById, - [id]: { - ...dataTable, - loadingEventIds, - }, - }; -}; - -interface SetSelectedTableEventsParams { - id: string; - eventIds: Record; - isSelectAllChecked: boolean; - isSelected: boolean; - tableById: TableById; -} - -export const setSelectedTableEvents = ({ - id, - eventIds, - isSelectAllChecked = false, - isSelected, - tableById, -}: SetSelectedTableEventsParams): TableById => { - const dataTable = tableById[id]; - - const selectedEventIds = isSelected - ? { ...dataTable.selectedEventIds, ...eventIds } - : omit(Object.keys(eventIds), dataTable.selectedEventIds); - - return { - ...tableById, - [id]: { - ...dataTable, - selectedEventIds, - isSelectAllChecked, - }, - }; -}; - -export const updateTableDetailsPanel = (action: TableToggleDetailPanel): DataExpandedDetail => { - const { tabType, id, ...expandedDetails } = action; - - const panelViewOptions = new Set(['eventDetail', 'hostDetail', 'networkDetail', 'userDetail']); - const expandedTabType = tabType ?? 'query'; - const newExpandDetails = { - params: expandedDetails.params ? { ...expandedDetails.params } : {}, - panelView: expandedDetails.panelView, - } as DataExpandedDetailType; - return { - [expandedTabType]: panelViewOptions.has(expandedDetails.panelView ?? '') - ? newExpandDetails - : {}, - }; -}; - -export const updateTableGraphEventId = ({ - id, - graphEventId, - tableById, -}: { - id: string; - graphEventId: string; - tableById: TableById; -}): TableById => { - const table = tableById[id]; - - return { - ...tableById, - [id]: { - ...table, - graphEventId, - }, - }; -}; - -export const updateTableSessionViewConfig = ({ - id, - sessionViewConfig, - tableById, -}: { - id: string; - sessionViewConfig: SessionViewConfig | null; - tableById: TableById; -}): TableById => { - const table = tableById[id]; - - return { - ...tableById, - [id]: { - ...table, - sessionViewConfig, - }, - }; -}; diff --git a/x-pack/plugins/timelines/public/store/t_grid/index.ts b/x-pack/plugins/timelines/public/store/t_grid/index.ts deleted file mode 100644 index f81916c75ef47..0000000000000 --- a/x-pack/plugins/timelines/public/store/t_grid/index.ts +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { - Action, - applyMiddleware, - CombinedState, - compose, - createStore as createReduxStore, - PreloadedState, - Store, -} from 'redux'; - -import { createEpicMiddleware } from 'redux-observable'; -import { Storage } from '@kbn/kibana-utils-plugin/public'; -import { TableState, TGridEpicDependencies } from '../../types'; -import { tGridReducer } from './reducer'; -import { getTGridByIdSelector } from './selectors'; - -export * from './model'; -export * as tGridActions from './actions'; -export * as tGridSelectors from './selectors'; -export * from './types'; -export { tGridReducer }; - -export type State = CombinedState; -type ComposeType = typeof compose; -declare global { - interface Window { - __REDUX_DEVTOOLS_EXTENSION_COMPOSE__: ComposeType; - } -} - -/** - * Factory for Security App's redux store. - */ -export const createStore = ( - state: PreloadedState, - storage: Storage -): Store => { - const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose; - - const middlewareDependencies: TGridEpicDependencies = { - tGridByIdSelector: getTGridByIdSelector, - storage, - }; - - const epicMiddleware = createEpicMiddleware( - { - dependencies: middlewareDependencies, - } - ); - - const store: Store = createReduxStore( - tGridReducer, - state, - composeEnhancers(applyMiddleware(epicMiddleware)) - ); - - return store; -}; diff --git a/x-pack/plugins/timelines/public/store/t_grid/model.ts b/x-pack/plugins/timelines/public/store/t_grid/model.ts deleted file mode 100644 index eb46c5ec5c423..0000000000000 --- a/x-pack/plugins/timelines/public/store/t_grid/model.ts +++ /dev/null @@ -1,94 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type { EuiDataGridColumn } from '@elastic/eui'; -import type { Filter } from '@kbn/es-query'; -import type { TimelineNonEcsData } from '../../../common/search_strategy'; -import type { - ColumnHeaderOptions, - DataExpandedDetail, - SortColumnTable, - SessionViewConfig, -} from '../../../common/types/timeline'; -export interface TGridModelSettings { - defaultColumns: Array< - Pick & - ColumnHeaderOptions - >; - loadingText?: string | React.ReactNode; - queryFields: string[]; - selectAll: boolean; - /** When true, shows checkboxes enabling selection. Selected events store in selectedEventIds **/ - showCheckboxes: boolean; - /** Specifies which column the timeline is sorted on, and the direction (ascending / descending) */ - sort: SortColumnTable[]; - title?: string; - unit?: (n: number) => string | React.ReactNode; -} -export interface TGridModel extends TGridModelSettings { - /** The columns displayed in the data table */ - columns: Array< - Pick & - ColumnHeaderOptions - >; - /** Kibana data view id **/ - dataViewId: string | null; // null if legacy pre-8.0 data table - /** Events to not be rendered **/ - deletedEventIds: string[]; - /** This holds the view information for the flyout when viewing data in a consuming view (i.e. hosts page) or the side panel in the primary data view */ - expandedDetail: DataExpandedDetail; - filters?: Filter[]; - /** When non-empty, display a graph view for this event */ - graphEventId?: string; - /** Uniquely identifies the data table */ - id: string; - indexNames: string[]; - isLoading: boolean; - /** If selectAll checkbox in header is checked **/ - isSelectAllChecked: boolean; - /** The number of items to show in a single page of results */ - itemsPerPage: number; - /** Displays a series of choices that when selected, become the value of `itemsPerPage` */ - itemsPerPageOptions: number[]; - /** Events to be rendered as loading **/ - loadingEventIds: string[]; - /** Events selected on this timeline -- eventId to TimelineNonEcsData[] mapping of data required for bulk actions **/ - selectedEventIds: Record; - initialized?: boolean; - sessionViewConfig: SessionViewConfig | null; - /** updated saved object timestamp */ - updated?: number; - /** Total number of fetched events/alerts */ - totalCount: number; -} - -export type SubsetTGridModel = Readonly< - Pick< - TGridModel, - | 'columns' - | 'selectAll' - | 'defaultColumns' - | 'dataViewId' - | 'deletedEventIds' - | 'expandedDetail' - | 'filters' - | 'indexNames' - | 'isLoading' - | 'isSelectAllChecked' - | 'itemsPerPage' - | 'itemsPerPageOptions' - | 'loadingEventIds' - | 'showCheckboxes' - | 'sort' - | 'selectedEventIds' - | 'graphEventId' - | 'sessionViewConfig' - | 'queryFields' - | 'title' - | 'totalCount' - > ->; diff --git a/x-pack/plugins/timelines/public/store/t_grid/selectors.ts b/x-pack/plugins/timelines/public/store/t_grid/selectors.ts deleted file mode 100644 index 6c876d5b4923e..0000000000000 --- a/x-pack/plugins/timelines/public/store/t_grid/selectors.ts +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ -import { getOr } from 'lodash/fp'; -import { createSelector } from 'reselect'; -import { TGridModel } from '.'; -import { tGridDefaults, getTGridManageDefaults } from './defaults'; - -const getDefaultTgrid = (id: string) => ({ ...tGridDefaults, ...getTGridManageDefaults(id) }); - -const selectTGridById = (state: unknown, tableId: string): TGridModel => { - return getOr( - getOr(getDefaultTgrid(tableId), ['tableById', tableId], state), - ['dataTable', 'tableById', tableId], - state - ); -}; - -export const getTGridByIdSelector = () => createSelector(selectTGridById, (tGrid) => tGrid); - -export const getManageDataTableById = () => - createSelector( - selectTGridById, - ({ - dataViewId, - defaultColumns, - isLoading, - loadingText, - queryFields, - title, - selectAll, - graphEventId, - }) => ({ - dataViewId, - defaultColumns, - isLoading, - loadingText, - queryFields, - title, - selectAll, - graphEventId, - }) - ); diff --git a/x-pack/plugins/timelines/public/store/t_grid/translations.ts b/x-pack/plugins/timelines/public/store/t_grid/translations.ts deleted file mode 100644 index ee7279cecf8a9..0000000000000 --- a/x-pack/plugins/timelines/public/store/t_grid/translations.ts +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { i18n } from '@kbn/i18n'; - -export const EVENTS = i18n.translate('xpack.timelines.tGrid.eventsLabel', { - defaultMessage: 'Events', -}); - -export const LOADING_EVENTS = i18n.translate( - 'xpack.timelines.tGrid.footer.loadingEventsDataLabel', - { - defaultMessage: 'Loading Events', - } -); - -export const UNIT = (totalCount: number) => - i18n.translate('xpack.timelines.tGrid.unit', { - values: { totalCount }, - defaultMessage: `{totalCount, plural, =1 {alert} other {alerts}}`, - }); - -export const TOTAL_COUNT_OF_EVENTS = i18n.translate( - 'xpack.timelines.tGrid.footer.totalCountOfEvents', - { - defaultMessage: 'events', - } -); diff --git a/x-pack/plugins/timelines/public/store/t_grid/types.ts b/x-pack/plugins/timelines/public/store/t_grid/types.ts deleted file mode 100644 index b0c82c2abf637..0000000000000 --- a/x-pack/plugins/timelines/public/store/t_grid/types.ts +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { EuiDataGridColumn } from '@elastic/eui'; -import { Storage } from '@kbn/kibana-utils-plugin/public'; -import type { ColumnHeaderOptions, SortColumnTable } from '../../../common/types'; -import type { TGridModel, TGridModelSettings } from './model'; - -export type { TGridModel }; - -/** A map of id to data table */ -export interface TableById { - [id: string]: TGridModel; -} - -export const EMPTY_TABLE_BY_ID: TableById = {}; // stable reference - -export interface TGridEpicDependencies { - // kibana$: Observable; - storage: Storage; - tGridByIdSelector: () => (state: State, timelineId: string) => TGridModel; -} - -/** The state of all data tables is stored here */ -export interface TableState { - tableById: TableById; -} - -export enum TableId { - usersPageEvents = 'users-page-events', - hostsPageEvents = 'hosts-page-events', - networkPageEvents = 'network-page-events', - hostsPageSessions = 'hosts-page-sessions-v2', - alertsOnRuleDetailsPage = 'alerts-rules-details-page', - alertsOnAlertsPage = 'alerts-page', - casePage = 'timeline-case', - test = 'table-test', // Reserved for testing purposes - alternateTest = 'alternateTest', - kubernetesPageSessions = 'kubernetes-page-sessions', -} - -export enum TimelineId { - active = 'timeline-1', - casePage = 'timeline-case', - detectionsAlertDetailsPage = 'detections-alert-details-page', - test = 'timeline-test', // Reserved for testing purposes -} - -export interface InitialyzeTGridSettings extends Partial { - id: string; -} - -export interface TGridPersistInput extends Partial> { - id: string; - columns: ColumnHeaderOptions[]; - indexNames: string[]; - showCheckboxes?: boolean; - defaultColumns: Array< - Pick & - ColumnHeaderOptions - >; - sort: SortColumnTable[]; -} diff --git a/x-pack/plugins/timelines/public/store/timeline/index.ts b/x-pack/plugins/timelines/public/store/timeline/index.ts new file mode 100644 index 0000000000000..a84e069988300 --- /dev/null +++ b/x-pack/plugins/timelines/public/store/timeline/index.ts @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +/* + * Timeline IDs + */ + +export enum TimelineId { + active = 'timeline-1', + casePage = 'timeline-case', + test = 'timeline-test', // Reserved for testing purposes +} diff --git a/x-pack/plugins/timelines/public/types.ts b/x-pack/plugins/timelines/public/types.ts index aec647934c2ad..4933161f54e23 100644 --- a/x-pack/plugins/timelines/public/types.ts +++ b/x-pack/plugins/timelines/public/types.ts @@ -11,58 +11,26 @@ import { Store } from 'redux'; import { CoreStart } from '@kbn/core/public'; import type { DataPublicPluginStart } from '@kbn/data-plugin/public'; import { CasesUiStart } from '@kbn/cases-plugin/public'; -import type { TriggersAndActionsUIPublicPluginStart as TriggersActionsStart } from '@kbn/triggers-actions-ui-plugin/public'; import { ApmBase } from '@elastic/apm-rum'; -import type { - LastUpdatedAtProps, - LoadingPanelProps, - UseDraggableKeyboardWrapper, - UseDraggableKeyboardWrapperProps, -} from './components'; -export type { SortDirection } from '../common/types'; -import type { TGridIntegratedProps } from './components/t_grid/integrated'; import type { UseAddToTimelineProps, UseAddToTimeline } from './hooks/use_add_to_timeline'; import { HoverActionsConfig } from './components/hover_actions'; -export * from './store/t_grid'; +import { LastUpdatedAtProps } from './components/last_updated'; +import { LoadingPanelProps } from './components/loading'; export interface TimelinesUIStart { getHoverActions: () => HoverActionsConfig; - getTGrid: ( - props: GetTGridProps - ) => ReactElement>; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - getTGridReducer: () => any; // eslint-disable-next-line @typescript-eslint/no-explicit-any getTimelineReducer: () => any; getLoadingPanel: (props: LoadingPanelProps) => ReactElement; getLastUpdated: (props: LastUpdatedAtProps) => ReactElement; getUseAddToTimeline: () => (props: UseAddToTimelineProps) => UseAddToTimeline; getUseAddToTimelineSensor: () => (api: SensorAPI) => void; - getUseDraggableKeyboardWrapper: () => ( - props: UseDraggableKeyboardWrapperProps - ) => UseDraggableKeyboardWrapper; - setTGridEmbeddedStore: (store: Store) => void; + setTimelineEmbeddedStore: (store: Store) => void; } export interface TimelinesStartPlugins { data: DataPublicPluginStart; cases: CasesUiStart; - triggersActionsUi: TriggersActionsStart; apm?: ApmBase; } export type TimelinesStartServices = CoreStart & TimelinesStartPlugins; -interface TGridIntegratedCompProps extends TGridIntegratedProps { - type: 'embedded'; -} -export type TGridType = 'embedded'; -export type GetTGridProps = T extends 'embedded' - ? TGridIntegratedCompProps - : TGridIntegratedCompProps; -export type TGridProps = TGridIntegratedCompProps; - -export interface StatefulEventContextType { - tabType: string | undefined; - timelineID: string; - enableHostDetailsFlyout: boolean; - enableIpDetailsFlyout: boolean; -} diff --git a/x-pack/plugins/timelines/server/search_strategy/index_fields/index.test.ts b/x-pack/plugins/timelines/server/search_strategy/index_fields/index.test.ts index 801f45f8489f2..6146a73fddb08 100644 --- a/x-pack/plugins/timelines/server/search_strategy/index_fields/index.test.ts +++ b/x-pack/plugins/timelines/server/search_strategy/index_fields/index.test.ts @@ -7,7 +7,7 @@ import { sortBy } from 'lodash/fp'; -import { formatIndexFields, createFieldItem, requestIndexFieldSearch } from '.'; +import { formatIndexFields, createFieldItem, requestIndexFieldSearchHandler } from '.'; import { mockAuditbeatIndexField, mockFilebeatIndexField, mockPacketbeatIndexField } from './mock'; import { fieldsBeat as beatFields } from '../../utils/beat_schema/fields'; import { IndexPatternsFetcher, SearchStrategyDependencies } from '@kbn/data-plugin/server'; @@ -231,146 +231,199 @@ describe('Fields Provider', () => { }, ]); - const deps = { + const depsCurrentESUser = { esClient: { asCurrentUser: { search: esClientSearchMock, fieldCaps: esClientFieldCapsMock } }, } as unknown as SearchStrategyDependencies; - beforeAll(() => { - getFieldsForWildcardMock.mockResolvedValue([]); - - esClientSearchMock.mockResolvedValue({ hits: { total: { value: 123 } } }); - esClientFieldCapsMock.mockResolvedValue({ indices: ['value'] }); - IndexPatternsFetcher.prototype.getFieldsForWildcard = getFieldsForWildcardMock; - }); - - beforeEach(() => { - getFieldsForWildcardMock.mockClear(); - esClientSearchMock.mockClear(); - esClientFieldCapsMock.mockClear(); - }); - - afterAll(() => { - getFieldsForWildcardMock.mockRestore(); - }); - - it('should check index exists', async () => { - const indices = ['some-index-pattern-*']; - const request = { - indices, - onlyCheckIfIndicesExist: true, - }; - - const response = await requestIndexFieldSearch(request, deps, beatFields, getStartServices); - expect(response.indexFields).toHaveLength(0); - expect(response.indicesExist).toEqual(indices); - }); - - it('should search index fields', async () => { - const indices = ['some-index-pattern-*']; - const request = { - indices, - onlyCheckIfIndicesExist: false, - }; - - const response = await requestIndexFieldSearch(request, deps, beatFields, getStartServices); - - expect(getFieldsForWildcardMock).toHaveBeenCalledWith({ pattern: indices[0] }); - - expect(response.indexFields).not.toHaveLength(0); - expect(response.indicesExist).toEqual(indices); - }); - - it('should search index fields by data view id', async () => { - const dataViewId = 'id'; - const request = { - dataViewId, - onlyCheckIfIndicesExist: false, - }; - - const response = await requestIndexFieldSearch(request, deps, beatFields, getStartServices); - - expect(getFieldsForWildcardMock).not.toHaveBeenCalled(); - - expect(response.indexFields).not.toHaveLength(0); - expect(response.indicesExist).toEqual(['coolbro']); - }); - - it('onlyCheckIfIndicesExist by data view id', async () => { - const dataViewId = 'id'; - const request = { - dataViewId, - onlyCheckIfIndicesExist: true, - }; - - const response = await requestIndexFieldSearch(request, deps, beatFields, getStartServices); - - expect(response.indexFields).toHaveLength(0); - expect(response.indicesExist).toEqual(['coolbro']); - }); - - it('should search apm index fields', async () => { - const indices = ['apm-*-transaction*', 'traces-apm*']; - const request = { - indices, - onlyCheckIfIndicesExist: false, - }; - - const response = await requestIndexFieldSearch(request, deps, beatFields, getStartServices); - - expect(getFieldsForWildcardMock).toHaveBeenCalledWith({ pattern: indices[0] }); - expect(response.indexFields).not.toHaveLength(0); - expect(response.indicesExist).toEqual(indices); - }); + const depsInternalESUser = { + esClient: { + asInternalUser: { search: esClientSearchMock, fieldCaps: esClientFieldCapsMock }, + }, + } as unknown as SearchStrategyDependencies; - it('should check apm index exists with data', async () => { - const indices = ['apm-*-transaction*', 'traces-apm*']; - const request = { - indices, - onlyCheckIfIndicesExist: true, - }; + describe.each([ + ['currentESUser', depsCurrentESUser, false], + ['internalESUser', depsInternalESUser, true], + ])(`Using %s`, (_, deps, useInternalUser) => { + beforeAll(() => { + getFieldsForWildcardMock.mockResolvedValue([]); - esClientSearchMock.mockResolvedValue({ hits: { total: { value: 1 } } }); - const response = await requestIndexFieldSearch(request, deps, beatFields, getStartServices); + esClientSearchMock.mockResolvedValue({ hits: { total: { value: 123 } } }); + esClientFieldCapsMock.mockResolvedValue({ indices: ['value'] }); + IndexPatternsFetcher.prototype.getFieldsForWildcard = getFieldsForWildcardMock; + }); - expect(esClientSearchMock).toHaveBeenCalledWith({ - index: indices[0], - body: { query: { match_all: {} }, size: 0 }, + beforeEach(() => { + getFieldsForWildcardMock.mockClear(); + esClientSearchMock.mockClear(); + esClientFieldCapsMock.mockClear(); }); - expect(esClientSearchMock).toHaveBeenCalledWith({ - index: indices[1], - body: { query: { match_all: {} }, size: 0 }, + + afterAll(() => { + getFieldsForWildcardMock.mockRestore(); }); - expect(getFieldsForWildcardMock).not.toHaveBeenCalled(); - expect(response.indexFields).toHaveLength(0); - expect(response.indicesExist).toEqual(indices); - }); + it('should check index exists', async () => { + const indices = ['some-index-pattern-*']; + const request = { + indices, + onlyCheckIfIndicesExist: true, + }; + + const response = await requestIndexFieldSearchHandler( + request, + deps, + beatFields, + getStartServices, + useInternalUser + ); + expect(response.indexFields).toHaveLength(0); + expect(response.indicesExist).toEqual(indices); + }); - it('should check apm index exists with no data', async () => { - const indices = ['apm-*-transaction*', 'traces-apm*']; - const request = { - indices, - onlyCheckIfIndicesExist: true, - }; + it('should search index fields', async () => { + const indices = ['some-index-pattern-*']; + const request = { + indices, + onlyCheckIfIndicesExist: false, + }; + + const response = await requestIndexFieldSearchHandler( + request, + deps, + beatFields, + getStartServices, + useInternalUser + ); + + expect(getFieldsForWildcardMock).toHaveBeenCalledWith({ pattern: indices[0] }); + + expect(response.indexFields).not.toHaveLength(0); + expect(response.indicesExist).toEqual(indices); + }); - esClientSearchMock.mockResolvedValue({ - body: { hits: { total: { value: 0 } } }, + it('should search index fields by data view id', async () => { + const dataViewId = 'id'; + const request = { + dataViewId, + onlyCheckIfIndicesExist: false, + }; + + const response = await requestIndexFieldSearchHandler( + request, + deps, + beatFields, + getStartServices, + useInternalUser + ); + + expect(getFieldsForWildcardMock).not.toHaveBeenCalled(); + + expect(response.indexFields).not.toHaveLength(0); + expect(response.indicesExist).toEqual(['coolbro']); }); - const response = await requestIndexFieldSearch(request, deps, beatFields, getStartServices); + it('onlyCheckIfIndicesExist by data view id', async () => { + const dataViewId = 'id'; + const request = { + dataViewId, + onlyCheckIfIndicesExist: true, + }; + + const response = await requestIndexFieldSearchHandler( + request, + deps, + beatFields, + getStartServices, + useInternalUser + ); + + expect(response.indexFields).toHaveLength(0); + expect(response.indicesExist).toEqual(['coolbro']); + }); - expect(esClientSearchMock).toHaveBeenCalledWith({ - index: indices[0], - body: { query: { match_all: {} }, size: 0 }, + it('should search apm index fields', async () => { + const indices = ['apm-*-transaction*', 'traces-apm*']; + const request = { + indices, + onlyCheckIfIndicesExist: false, + }; + + const response = await requestIndexFieldSearchHandler( + request, + deps, + beatFields, + getStartServices, + useInternalUser + ); + + expect(getFieldsForWildcardMock).toHaveBeenCalledWith({ pattern: indices[0] }); + expect(response.indexFields).not.toHaveLength(0); + expect(response.indicesExist).toEqual(indices); }); - expect(esClientSearchMock).toHaveBeenCalledWith({ - index: indices[1], - body: { query: { match_all: {} }, size: 0 }, + + it('should check apm index exists with data', async () => { + const indices = ['apm-*-transaction*', 'traces-apm*']; + const request = { + indices, + onlyCheckIfIndicesExist: true, + }; + + esClientSearchMock.mockResolvedValue({ hits: { total: { value: 1 } } }); + const response = await requestIndexFieldSearchHandler( + request, + deps, + beatFields, + getStartServices, + useInternalUser + ); + + expect(esClientSearchMock).toHaveBeenCalledWith({ + index: indices[0], + body: { query: { match_all: {} }, size: 0 }, + }); + expect(esClientSearchMock).toHaveBeenCalledWith({ + index: indices[1], + body: { query: { match_all: {} }, size: 0 }, + }); + expect(getFieldsForWildcardMock).not.toHaveBeenCalled(); + + expect(response.indexFields).toHaveLength(0); + expect(response.indicesExist).toEqual(indices); }); - expect(getFieldsForWildcardMock).not.toHaveBeenCalled(); - expect(response.indexFields).toHaveLength(0); - expect(response.indicesExist).toEqual([]); + it('should check apm index exists with no data', async () => { + const indices = ['apm-*-transaction*', 'traces-apm*']; + const request = { + indices, + onlyCheckIfIndicesExist: true, + }; + + esClientSearchMock.mockResolvedValue({ + body: { hits: { total: { value: 0 } } }, + }); + + const response = await requestIndexFieldSearchHandler( + request, + deps, + beatFields, + getStartServices, + useInternalUser + ); + + expect(esClientSearchMock).toHaveBeenCalledWith({ + index: indices[0], + body: { query: { match_all: {} }, size: 0 }, + }); + expect(esClientSearchMock).toHaveBeenCalledWith({ + index: indices[1], + body: { query: { match_all: {} }, size: 0 }, + }); + expect(getFieldsForWildcardMock).not.toHaveBeenCalled(); + + expect(response.indexFields).toHaveLength(0); + expect(response.indicesExist).toEqual([]); + }); }); }); }); diff --git a/x-pack/plugins/timelines/server/search_strategy/index_fields/index.ts b/x-pack/plugins/timelines/server/search_strategy/index_fields/index.ts index bf433179f7ddb..c035e9b382d52 100644 --- a/x-pack/plugins/timelines/server/search_strategy/index_fields/index.ts +++ b/x-pack/plugins/timelines/server/search_strategy/index_fields/index.ts @@ -10,6 +10,7 @@ import isEmpty from 'lodash/isEmpty'; import get from 'lodash/get'; import { ElasticsearchClient, StartServicesAccessor } from '@kbn/core/server'; import { + DataViewsServerPluginStart, IndexPatternsFetcher, ISearchStrategy, SearchStrategyDependencies, @@ -41,7 +42,7 @@ export const indexFieldsProvider = ( return { search: (request, options, deps) => - from(requestIndexFieldSearch(request, deps, beatFields, getStartServices)), + from(requestIndexFieldSearchHandler(request, deps, beatFields, getStartServices)), }; }; @@ -70,27 +71,40 @@ export const findExistingIndices = async ( .map((p) => p.catch((e) => false)) ); +export const requestIndexFieldSearchHandler = async ( + request: IndexFieldsStrategyRequest<'indices' | 'dataView'>, + deps: SearchStrategyDependencies, + beatFields: BeatFields, + getStartServices: StartServicesAccessor, + useInternalUser?: boolean +): Promise => { + const [ + , + { + data: { indexPatterns }, + }, + ] = await getStartServices(); + return requestIndexFieldSearch(request, deps, beatFields, indexPatterns, useInternalUser); +}; + export const requestIndexFieldSearch = async ( request: IndexFieldsStrategyRequest<'indices' | 'dataView'>, { savedObjectsClient, esClient, request: kRequest }: SearchStrategyDependencies, beatFields: BeatFields, - getStartServices: StartServicesAccessor + indexPatterns: DataViewsServerPluginStart, + useInternalUser?: boolean ): Promise => { const indexPatternsFetcherAsCurrentUser = new IndexPatternsFetcher(esClient.asCurrentUser); const indexPatternsFetcherAsInternalUser = new IndexPatternsFetcher(esClient.asInternalUser); if ('dataViewId' in request && 'indices' in request) { throw new Error('Provide index field search with either `dataViewId` or `indices`, not both'); } - const [ - , - { - data: { indexPatterns }, - }, - ] = await getStartServices(); + + const esUser = useInternalUser ? esClient.asInternalUser : esClient.asCurrentUser; const dataViewService = await indexPatterns.dataViewsServiceFactory( savedObjectsClient, - esClient.asCurrentUser, + esUser, kRequest, true ); @@ -118,7 +132,7 @@ export const requestIndexFieldSearch = async ( } const patternList = dataView.title.split(','); - indicesExist = (await findExistingIndices(patternList, esClient.asCurrentUser)).reduce( + indicesExist = (await findExistingIndices(patternList, esUser)).reduce( (acc: string[], doesIndexExist, i) => (doesIndexExist ? [...acc, patternList[i]] : acc), [] ); @@ -131,7 +145,7 @@ export const requestIndexFieldSearch = async ( } } else if ('indices' in request) { const patternList = dedupeIndexName(request.indices); - indicesExist = (await findExistingIndices(patternList, esClient.asCurrentUser)).reduce( + indicesExist = (await findExistingIndices(patternList, esUser)).reduce( (acc: string[], doesIndexExist, i) => (doesIndexExist ? [...acc, patternList[i]] : acc), [] ); @@ -139,7 +153,7 @@ export const requestIndexFieldSearch = async ( const fieldDescriptor = ( await Promise.all( indicesExist.map(async (index, n) => { - if (index.startsWith('.alerts-observability')) { + if (index.startsWith('.alerts-observability') || useInternalUser) { return indexPatternsFetcherAsInternalUser.getFieldsForWildcard({ pattern: index, }); diff --git a/x-pack/plugins/timelines/server/search_strategy/timeline/factory/helpers/constants.ts b/x-pack/plugins/timelines/server/search_strategy/timeline/factory/helpers/constants.ts index 09cd43a5dad34..3aec85af09261 100644 --- a/x-pack/plugins/timelines/server/search_strategy/timeline/factory/helpers/constants.ts +++ b/x-pack/plugins/timelines/server/search_strategy/timeline/factory/helpers/constants.ts @@ -5,7 +5,12 @@ * 2.0. */ -import { ALERT_RULE_CONSUMER, ALERT_RISK_SCORE, ALERT_SEVERITY } from '@kbn/rule-data-utils'; +import { + ALERT_RULE_CONSUMER, + ALERT_RISK_SCORE, + ALERT_SEVERITY, + ALERT_RULE_PARAMETERS, +} from '@kbn/rule-data-utils'; import { ENRICHMENT_DESTINATION_PATH } from '../../../../../common/constants'; export const MATCHED_ATOMIC = 'matched.atomic'; @@ -40,6 +45,7 @@ export const CTI_ROW_RENDERER_FIELDS = [ FEED_NAME_REFERENCE, ]; +// TODO: update all of these fields to use the constants from technical field names export const TIMELINE_EVENTS_FIELDS = [ ALERT_RULE_CONSUMER, '@timestamp', @@ -58,6 +64,7 @@ export const TIMELINE_EVENTS_FIELDS = [ 'kibana.alert.rule.version', ALERT_SEVERITY, ALERT_RISK_SCORE, + ALERT_RULE_PARAMETERS, 'kibana.alert.threshold_result', 'kibana.alert.building_block_type', 'kibana.alert.suppression.docs_count', diff --git a/x-pack/plugins/timelines/server/search_strategy/timeline/factory/helpers/format_timeline_data.test.ts b/x-pack/plugins/timelines/server/search_strategy/timeline/factory/helpers/format_timeline_data.test.ts index ff2ee23643190..5117f8dc889ed 100644 --- a/x-pack/plugins/timelines/server/search_strategy/timeline/factory/helpers/format_timeline_data.test.ts +++ b/x-pack/plugins/timelines/server/search_strategy/timeline/factory/helpers/format_timeline_data.test.ts @@ -414,6 +414,50 @@ describe('formatTimelineData', () => { field: 'kibana.alert.rule.uuid', value: ['15d82f10-0926-11ed-bece-6b0c033d0075'], }, + { + field: 'kibana.alert.rule.parameters.sourceId', + value: ['default'], + }, + { + field: 'kibana.alert.rule.parameters.nodeType', + value: ['host'], + }, + { + field: 'kibana.alert.rule.parameters.criteria.comparator', + value: ['>'], + }, + { + field: 'kibana.alert.rule.parameters.criteria.timeSize', + value: ['1'], + }, + { + field: 'kibana.alert.rule.parameters.criteria.metric', + value: ['cpu'], + }, + { + field: 'kibana.alert.rule.parameters.criteria.threshold', + value: ['10'], + }, + { + field: 'kibana.alert.rule.parameters.criteria.customMetric.aggregation', + value: ['avg'], + }, + { + field: 'kibana.alert.rule.parameters.criteria.customMetric.id', + value: ['alert-custom-metric'], + }, + { + field: 'kibana.alert.rule.parameters.criteria.customMetric.field', + value: [''], + }, + { + field: 'kibana.alert.rule.parameters.criteria.customMetric.type', + value: ['custom'], + }, + { + field: 'kibana.alert.rule.parameters.criteria.timeUnit', + value: ['d'], + }, { field: 'event.action', value: ['active'], @@ -466,50 +510,6 @@ describe('formatTimelineData', () => { field: 'kibana.version', value: ['8.4.0'], }, - { - field: 'kibana.alert.rule.parameters.sourceId', - value: ['default'], - }, - { - field: 'kibana.alert.rule.parameters.nodeType', - value: ['host'], - }, - { - field: 'kibana.alert.rule.parameters.criteria.comparator', - value: ['>'], - }, - { - field: 'kibana.alert.rule.parameters.criteria.timeSize', - value: ['1'], - }, - { - field: 'kibana.alert.rule.parameters.criteria.metric', - value: ['cpu'], - }, - { - field: 'kibana.alert.rule.parameters.criteria.threshold', - value: ['10'], - }, - { - field: 'kibana.alert.rule.parameters.criteria.customMetric.aggregation', - value: ['avg'], - }, - { - field: 'kibana.alert.rule.parameters.criteria.customMetric.id', - value: ['alert-custom-metric'], - }, - { - field: 'kibana.alert.rule.parameters.criteria.customMetric.field', - value: [''], - }, - { - field: 'kibana.alert.rule.parameters.criteria.customMetric.type', - value: ['custom'], - }, - { - field: 'kibana.alert.rule.parameters.criteria.timeUnit', - value: ['d'], - }, ], ecs: { '@timestamp': ['2022-07-21T22:38:57.888Z'], @@ -528,6 +528,9 @@ describe('formatTimelineData', () => { consumer: ['infrastructure'], name: ['test 1212'], uuid: ['15d82f10-0926-11ed-bece-6b0c033d0075'], + parameters: [ + '{"sourceId":"default","nodeType":"host","criteria":[{"comparator":">","timeSize":1,"metric":"cpu","threshold":[10],"customMetric":{"aggregation":"avg","id":"alert-custom-metric","field":"","type":"custom"},"timeUnit":"d"}]}', + ], }, workflow_status: ['open'], }, diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/use_columns.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/use_columns.tsx index faf7bf60edcf6..01bec7cadb729 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/use_columns.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/use_columns.tsx @@ -133,7 +133,7 @@ export const useColumns = ( values: { transformId: item.config.id }, }) } - iconType={expandedRowItemIds.includes(item.config.id) ? 'arrowUp' : 'arrowDown'} + iconType={expandedRowItemIds.includes(item.config.id) ? 'arrowDown' : 'arrowRight'} data-test-subj="transformListRowDetailsToggle" /> ), diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index 25a9efc435dc1..fece10374f8cb 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -2133,13 +2133,9 @@ "discover.fieldChooser.fieldFilterButtonLabel": "Filtrer par type", "discover.fieldChooser.fieldsMobileButtonLabel": "Champs", "discover.fieldChooser.filter.aggregatableLabel": "Regroupable", - "discover.fieldChooser.filter.availableFieldsTitle": "Champs disponibles", "discover.fieldChooser.filter.filterByTypeLabel": "Filtrer par type", - "discover.fieldChooser.filter.hideEmptyFieldsLabel": "Masquer les champs vides", "discover.fieldChooser.filter.indexAndFieldsSectionAriaLabel": "Index et champs", - "discover.fieldChooser.filter.popularTitle": "Populaire", "discover.fieldChooser.filter.searchableLabel": "Interrogeable", - "discover.fieldChooser.filter.selectedFieldsTitle": "Champs sélectionnés", "discover.fieldChooser.filter.toggleButton.any": "tout", "discover.fieldChooser.filter.toggleButton.no": "non", "discover.fieldChooser.filter.toggleButton.yes": "oui", @@ -2218,12 +2214,8 @@ "discover.grid.viewDoc": "Afficher/Masquer les détails de la boîte de dialogue", "discover.gridSampleSize.advancedSettingsLinkLabel": "Paramètres avancés", "discover.helpMenu.appName": "Découverte", - "discover.inspectorRequestDataTitleChart": "Données du graphique", "discover.inspectorRequestDataTitleDocuments": "Documents", - "discover.inspectorRequestDataTitleTotalHits": "Nombre total de résultats", - "discover.inspectorRequestDescriptionChart": "Cette requête interroge Elasticsearch afin de récupérer les données d'agrégation pour le graphique.", "discover.inspectorRequestDescriptionDocument": "Cette requête interroge Elasticsearch afin de récupérer les documents.", - "discover.inspectorRequestDescriptionTotalHits": "Cette requête interroge Elasticsearch afin de récupérer le nombre total de résultats.", "discover.json.codeEditorAriaLabel": "Affichage JSON en lecture seule d’un document Elasticsearch", "discover.json.copyToClipboardLabel": "Copier dans le presse-papiers", "discover.loadingDocuments": "Chargement des documents", @@ -2271,7 +2263,6 @@ "discover.sampleData.viewLinkLabel": "Découverte", "discover.savedSearch.savedObjectName": "Recherche enregistrée", "discover.savedSearchEmbeddable.action.viewSavedSearch.displayName": "Ouvrir dans Discover", - "discover.searchingTitle": "Recherche", "discover.selectColumnHeader": "Sélectionner la colonne", "discover.showAllDocuments": "Afficher tous les documents", "discover.showErrorMessageAgain": "Afficher le message d'erreur", @@ -7150,12 +7141,6 @@ "xpack.apm.dependencyDetailThroughputChartTitle": "Rendement", "xpack.apm.dependencyErrorRateChart.chartTitle": "Taux de transactions ayant échoué", "xpack.apm.dependencyLatencyChart.chartTitle": "Latence", - "xpack.apm.dependencyOperationDetailTraceList": "Traces", - "xpack.apm.dependencyOperationDetailTraceListDurationColumn": "Durée", - "xpack.apm.dependencyOperationDetailTraceListOutcomeColumn": "Résultat", - "xpack.apm.dependencyOperationDetailTraceListServiceNameColumn": "Service d'origine", - "xpack.apm.dependencyOperationDetailTraceListTimestampColumn": "Horodatage", - "xpack.apm.dependencyOperationDetailTraceListTransactionNameColumn": "Nom de la transaction", "xpack.apm.dependencyOperationDistributionChart.allSpansLegendLabel": "Tous les intervalles", "xpack.apm.dependencyOperationDistributionChart.failedSpansLegendLabel": "Intervalles ayant échoué", "xpack.apm.dependencyThroughputChart.chartTitle": "Rendement", @@ -9293,7 +9278,6 @@ "xpack.cases.caseView.comment": "commentaire", "xpack.cases.caseView.comment.addComment": "Ajouter un commentaire", "xpack.cases.caseView.comment.addCommentHelpText": "Ajouter un nouveau commentaire...", - "xpack.cases.caseView.commentFieldRequiredError": "Un commentaire est requis.", "xpack.cases.caseView.connectors": "Système de gestion des incidents externes", "xpack.cases.caseView.copyCommentLinkAria": "Copier le lien de référence", "xpack.cases.caseView.create": "Créer un cas", @@ -17267,8 +17251,6 @@ "xpack.lens.indexPattern.valueCountOf": "Nombre de {name}", "xpack.lens.indexPatternSuggestion.removeLayerLabel": "Afficher uniquement {indexPatternTitle}", "xpack.lens.indexPatternSuggestion.removeLayerPositionLabel": "Afficher uniquement le calque {layerNumber}", - "xpack.lens.modalTitle.title.clear": "Effacer le calque {layerType} ?", - "xpack.lens.modalTitle.title.delete": "Supprimer le calque {layerType} ?", "xpack.lens.pie.arrayValues": "{label} contient des valeurs de tableau. Le rendu de votre visualisation peut ne pas se présenter comme attendu.", "xpack.lens.pie.suggestionLabel": "Comme {chartName}", "xpack.lens.shared.legend.filterOptionsLegend": "{legendDataLabel}, options de filtre", @@ -17671,7 +17653,6 @@ "xpack.lens.indexPattern.min": "Minimum", "xpack.lens.indexPattern.min.description": "Agrégation d'indicateurs à valeur unique qui renvoie la valeur minimale des valeurs numériques extraites des documents agrégés.", "xpack.lens.indexPattern.missingFieldLabel": "Champ manquant", - "xpack.lens.indexPattern.moveToWorkspaceDisabled": "Ce champ ne peut pas être ajouté automatiquement à l'espace de travail. Vous pouvez toujours l'utiliser directement dans le panneau de configuration.", "xpack.lens.indexPattern.moving_average.signature": "indicateur : nombre, [window] : nombre", "xpack.lens.indexPattern.movingAverage": "Moyenne mobile", "xpack.lens.indexPattern.movingAverage.basicExplanation": "La moyenne mobile fait glisser une fenêtre sur les données et affiche la valeur moyenne. La moyenne mobile est prise en charge uniquement par les histogrammes des dates.", @@ -17856,9 +17837,6 @@ "xpack.lens.metric.progressDirectionLabel": "Direction des barres", "xpack.lens.metric.secondaryMetric": "Indicateur secondaire", "xpack.lens.metric.subtitleLabel": "Sous-titre", - "xpack.lens.modalTitle.layerType.annotation": "annotations", - "xpack.lens.modalTitle.layerType.data": "visualisation", - "xpack.lens.modalTitle.layerType.refLines": "lignes de référence", "xpack.lens.pageTitle": "Lens", "xpack.lens.paletteHeatmapGradient.customize": "Modifier", "xpack.lens.paletteHeatmapGradient.customizeLong": "Modifier la palette", @@ -17901,7 +17879,6 @@ "xpack.lens.pieChart.valuesLabel": "Étiquettes", "xpack.lens.pieChart.visualOptionsLabel": "Options visuelles", "xpack.lens.primaryMetric.label": "Indicateur principal", - "xpack.lens.resetVisualizationAriaLabel": "Réinitialiser la visualisation", "xpack.lens.saveDuplicateRejectedDescription": "La confirmation d'enregistrement avec un doublon de titre a été rejetée.", "xpack.lens.searchTitle": "Lens : créer des visualisations", "xpack.lens.section.configPanelLabel": "Panneau de configuration", @@ -20877,7 +20854,6 @@ "xpack.ml.newJob.fromLens.createJob.error.colsNoSourceField": "Certaines colonnes ne contiennent pas de champ source.", "xpack.ml.newJob.fromLens.createJob.error.colsUsingFilterTimeSift": "Les colonnes contenant des paramètres incompatibles avec les détecteurs de ML, le décalage temporel et la fonction Filtrer par ne sont pas prises en charge.", "xpack.ml.newJob.fromLens.createJob.error.incompatibleLayerType": "Le calque n'est pas compatible. Seuls les calques de graphique peuvent être utilisés.", - "xpack.ml.newJob.fromLens.createJob.error.noCompatibleLayers": "La visualisation ne contient aucun calque pouvant être utilisé pour la création d'une tâche de détection des anomalies.", "xpack.ml.newJob.fromLens.createJob.error.noDataViews": "Aucune vue de données n'a été trouvée dans la visualisation.", "xpack.ml.newJob.fromLens.createJob.error.noDateField": "Impossible de trouver un champ de date.", "xpack.ml.newJob.fromLens.createJob.error.noTimeRange": "Plage temporelle non spécifiée.", @@ -23765,7 +23741,6 @@ "xpack.osquery.fleetIntegration.osqueryConfig.packConfigFilesErrorMessage": "Les fichiers de configuration de pack ne sont pas pris en charge. Les packs suivants doivent être supprimés : {packNames}.", "xpack.osquery.fleetIntegration.osqueryConfig.restrictedOptionsErrorMessage": "Les options Osquery suivantes ne sont pas prises en charge et doivent être supprimées : {restrictedFlags}.", "xpack.osquery.liveQuery.permissionDeniedPromptBody": "Pour pouvoir consulter les résultats de requête, demandez à votre administrateur de mettre à jour votre rôle utilisateur de sorte à disposer des privilèges de {read} pour les index {logs}.", - "xpack.osquery.liveQuery.queryForm.largeQueryError": "La recherche est trop volumineuse ({maxLength} caractères maxi)", "xpack.osquery.newPack.successToastMessageText": "Le pack \"{packName}\" a bien été créé.", "xpack.osquery.newSavedQuery.successToastMessageText": "Enregistrement réussi de la recherche \"{savedQueryId}\"", "xpack.osquery.pack.queriesTable.deleteActionAriaLabel": "Supprimer {queryName}", @@ -24175,7 +24150,6 @@ "xpack.reporting.exportTypes.csv.generateCsv.esErrorMessage": "Réponse {statusCode} reçue d'Elasticsearch : {message}", "xpack.reporting.exportTypes.csv.generateCsv.unknownErrorMessage": "Une erreur inconnue est survenue : {message}", "xpack.reporting.jobsQuery.deleteError": "Impossible de supprimer le rapport : {error}", - "xpack.reporting.jobsQuery.infoError.unauthorizedErrorMessage": "Désolé, vous n’êtes pas autorisé à voir les informations {jobType}.", "xpack.reporting.jobStatusDetail.attemptXofY": "Tentative {attempts} sur {max_attempts}.", "xpack.reporting.jobStatusDetail.timeoutSeconds": "{timeout} secondes", "xpack.reporting.listing.diagnosticApiCallFailure": "Un problème est survenu lors de l'exécution du diagnostic : {error}", @@ -25672,12 +25646,7 @@ "xpack.securitySolution.searchStrategy.error": "Impossible d'exécuter la recherche : {factoryQueryType}", "xpack.securitySolution.some_page.flyoutCreateSubmitSuccess": "\"{name}\" a été ajouté.", "xpack.securitySolution.tables.rowItemHelper.overflowButtonDescription": "+ {count} de plus", - "xpack.securitySolution.timeline.body.actions.addNotesForRowAriaLabel": "Ajouter des notes pour l'événement de la ligne {ariaRowindex} à la chronologie, avec les colonnes {columnValues}", "xpack.securitySolution.timeline.body.actions.attachAlertToCaseForRowAriaLabel": "Attacher l'alerte ou l'événement de la ligne {ariaRowindex} à un cas, avec les colonnes {columnValues}", - "xpack.securitySolution.timeline.body.actions.investigateInResolverForRowAriaLabel": "Analyser l'alerte ou l'événement de la ligne {ariaRowindex}, avec les colonnes {columnValues}", - "xpack.securitySolution.timeline.body.actions.moreActionsForRowAriaLabel": "Sélectionner davantage d'actions pour l'alerte ou l'événement de la ligne {ariaRowindex}, avec les colonnes {columnValues}", - "xpack.securitySolution.timeline.body.actions.sendAlertToTimelineForRowAriaLabel": "Envoyer l'alerte de la ligne {ariaRowindex} à la chronologie, avec les colonnes {columnValues}", - "xpack.securitySolution.timeline.body.actions.viewDetailsForRowAriaLabel": "Afficher les détails pour l'alerte ou l'événement de la ligne {ariaRowindex}, avec les colonnes {columnValues}", "xpack.securitySolution.timeline.body.pinning.disablePinnnedTooltip": "Impossible d’épingler{isAlert, select, true{cette alerte} other{cet événement}} lors de la modification d'une chronologie de modèle", "xpack.securitySolution.timeline.body.pinning.pinnnedWithNotesTooltip": "Impossible de désépingler{isAlert, select, true{cette alerte} other{cet événement}} en raison des notes", "xpack.securitySolution.timeline.body.pinning.pinTooltip": "Épingler {isAlert, select, true{l'alerte} other{l'événement}}", @@ -28880,10 +28849,6 @@ "xpack.securitySolution.timeline.autosave.warning.description": "Un autre utilisateur a effectué des modifications dans cette chronologie. Toutes les modifications que vous effectuez ne seront pas enregistrées automatiquement tant que vous n'aurez pas actualisé cette chronologie pour absorber ces modifications.", "xpack.securitySolution.timeline.autosave.warning.refresh.title": "Actualiser la chronologie", "xpack.securitySolution.timeline.autosave.warning.title": "Sauvegarde automatique désactivée jusqu'à l'actualisation", - "xpack.securitySolution.timeline.body.actions.checkboxForRowAriaLabel": "Case {checked, select, false {non cochée} true {cochée}} pour l'alerte ou l'événement de la ligne {ariaRowindex}, avec les colonnes {columnValues}", - "xpack.securitySolution.timeline.body.actions.investigateInResolverTooltip": "Analyser l'événement", - "xpack.securitySolution.timeline.body.actions.pinEventForRowAriaLabel": "{isEventPinned, select, false {Épingler} true {Désépingler}} l'événement de la ligne {ariaRowindex} {isEventPinned, select, false{dans} true {de}} la chronologie, avec les colonnes {columnValues}", - "xpack.securitySolution.timeline.body.actions.viewDetailsAriaLabel": "Afficher les détails", "xpack.securitySolution.timeline.body.notes.addNoteTooltip": "Ajouter la note", "xpack.securitySolution.timeline.body.notes.disableEventTooltip": "Les notes ne peuvent pas être ajoutées ici lors de la modification d'une chronologie de modèle", "xpack.securitySolution.timeline.body.openSessionViewLabel": "Ouvrir la vue de session", @@ -30883,7 +30848,6 @@ "xpack.synthetics.keyValuePairsField.value.ariaLabel": "Valeur", "xpack.synthetics.keyValuePairsField.value.label": "Valeur", "xpack.synthetics.kueryBar.searchPlaceholder.kql": "Rechercher à l'aide de la syntaxe KQL des ID, noms et types etc. de moniteurs (par ex. monitor.type: \"http\" AND tags: \"dev\")", - "xpack.synthetics.kueryBar.searchPlaceholder.simple": "Rechercher par ID, nom ou URL de moniteur (par ex. http:// )", "xpack.synthetics.locationName.helpLinkAnnotation": "Ajouter un emplacement", "xpack.synthetics.management.confirmDescriptionLabel": "Cette action supprimera le moniteur mais conservera toute donnée collectée. Cette action ne peut pas être annulée.", "xpack.synthetics.management.deleteLabel": "Supprimer", @@ -31108,7 +31072,6 @@ "xpack.synthetics.monitorManagement.monitorAdvancedOptions.monitorNamespaceFieldLabel": "Espace de nom", "xpack.synthetics.monitorManagement.monitorAdvancedOptions.namespaceHelpLearnMoreLabel": "En savoir plus", "xpack.synthetics.monitorManagement.monitorDeleteFailureMessage": "Impossible de supprimer le moniteur. Réessayez plus tard.", - "xpack.synthetics.monitorManagement.monitorDeleteLoadingMessage": "Suppression du moniteur...", "xpack.synthetics.monitorManagement.monitorEditedSuccessMessage": "Moniteur mis à jour.", "xpack.synthetics.monitorManagement.monitorFailureMessage": "Impossible d'enregistrer le moniteur. Réessayez plus tard.", "xpack.synthetics.monitorManagement.monitorList.actions": "Actions", @@ -31413,49 +31376,12 @@ "xpack.threatIntelligence.field.threat.indicator.last_seen": "Vu en dernier", "xpack.threatIntelligence.indicator.table.viewDetailsButton": "Afficher les détails", "xpack.timelines.clipboard.copy.successToastTitle": "Champ {field} copié dans le presse-papiers", - "xpack.timelines.footer.autoRefreshActiveTooltip": "Lorsque l'actualisation automatique est activée, la chronologie vous montrera les {numberOfItems} derniers événements correspondant à votre recherche.", - "xpack.timelines.footer.rowsPerPageLabel": "Lignes par page : {rowsPerPage}", "xpack.timelines.hoverActions.columnToggleLabel": "Basculer la vue {field} dans le tableau", "xpack.timelines.hoverActions.nestedColumnToggleLabel": "Le champ {field} est un objet, et il est composé de champs imbriqués qui peuvent être ajoutés en tant que colonnes", - "xpack.timelines.tGrid.unit": "{totalCount, plural, =1 {alerte} other {alertes}}", - "xpack.timelines.timeline.acknowledgedAlertSuccessToastMessage": "Marquage réussi de {totalAlerts} {totalAlerts, plural, =1 {alerte comme reconnue} other {alertes comme reconnues}}.", - "xpack.timelines.timeline.alertsUnit": "{totalCount, plural, =1 {alerte} other {alertes}}", - "xpack.timelines.timeline.body.actions.addNotesForRowAriaLabel": "Ajouter des notes pour l'événement de la ligne {ariaRowindex} à la chronologie, avec les colonnes {columnValues}", - "xpack.timelines.timeline.body.actions.attachAlertToCaseForRowAriaLabel": "Attacher l'alerte ou l'événement de la ligne {ariaRowindex} à un cas, avec les colonnes {columnValues}", - "xpack.timelines.timeline.body.actions.investigateInResolverForRowAriaLabel": "Analyser l'alerte ou l'événement de la ligne {ariaRowindex}, avec les colonnes {columnValues}", - "xpack.timelines.timeline.body.actions.moreActionsForRowAriaLabel": "Sélectionner davantage d'actions pour l'alerte ou l'événement de la ligne {ariaRowindex}, avec les colonnes {columnValues}", - "xpack.timelines.timeline.body.actions.sendAlertToTimelineForRowAriaLabel": "Envoyer l'alerte de la ligne {ariaRowindex} à la chronologie, avec les colonnes {columnValues}", - "xpack.timelines.timeline.body.actions.viewDetailsForRowAriaLabel": "Afficher les détails pour l'alerte ou l'événement de la ligne {ariaRowindex}, avec les colonnes {columnValues}", - "xpack.timelines.timeline.closedAlertSuccessToastMessage": "Fermeture réussie de {totalAlerts} {totalAlerts, plural, =1 {alerte} other {alertes}}.", - "xpack.timelines.timeline.eventsTableAriaLabel": "événements ; page {activePage} sur {totalPages}", - "xpack.timelines.timeline.openedAlertSuccessToastMessage": "Ouverture réussie de {totalAlerts} {totalAlerts, plural, =1 {alerte} other {alertes}}.", - "xpack.timelines.timeline.properties.timelineToggleButtonAriaLabel": "{isOpen, select, false {Ouvrir} true {Fermer} other {Basculer}} la chronologie {title}", - "xpack.timelines.timeline.updateAlertStatusFailed": "Impossible de mettre à jour { conflicts } {conflicts, plural, =1 {alerte} other {alertes}}.", - "xpack.timelines.timeline.updateAlertStatusFailedDetailed": "{ updated } {updated, plural, =1 {alerte a été mise à jour} other {alertes ont été mises à jour}} correctement, mais { conflicts } n'ont pas pu être mis à jour\n car { conflicts, plural, =1 {elle était} other {elles étaient}} déjà en cours de modification.", - "xpack.timelines.timeline.youAreInAnEventRendererScreenReaderOnly": "Vous êtes dans un outil de rendu d'événement pour la ligne : {row}. Appuyez sur la touche fléchée vers le haut pour quitter et revenir à la ligne en cours, ou sur la touche fléchée vers le bas pour quitter et passer à la ligne suivante.", - "xpack.timelines.toolbar.bulkActions.selectAllAlertsTitle": "Sélectionner un total de {totalAlertsFormatted} {totalAlerts, plural, =1 {alerte} other {alertes}}", - "xpack.timelines.toolbar.bulkActions.selectedAlertsTitle": "{selectedAlertsFormatted} {selectedAlerts, plural, =1 {alerte sélectionnée} other {alertes sélectionnées}}", - "xpack.timelines.alerts.EventRenderedView.eventSummary.column": "Résumé des événements", - "xpack.timelines.alerts.EventRenderedView.rule.column": "Règle", - "xpack.timelines.alerts.EventRenderedView.timestamp.column": "Horodatage", - "xpack.timelines.alerts.summaryView.eventRendererView.label": "Vue rendue des événements", - "xpack.timelines.alerts.summaryView.gridView.label": "Vue Grille", - "xpack.timelines.alerts.summaryView.options.default.description": "Afficher sous forme de données tabulaires avec la possibilité de regrouper et de trier selon des champs spécifiques", - "xpack.timelines.alerts.summaryView.options.summaryView.description": "Afficher un rendu du flux d'événements pour chaque alerte", - "xpack.timelines.beatFields.errorSearchDescription": "Une erreur s'est produite lors de l'obtention des champs d'agents Beats", - "xpack.timelines.beatFields.failSearchDescription": "Impossible de lancer une recherche sur les champs d'agents Beats", "xpack.timelines.clipboard.copied": "Copié", "xpack.timelines.clipboard.copy": "Copier", "xpack.timelines.clipboard.copy.to.the.clipboard": "Copier dans le presse-papiers", "xpack.timelines.clipboard.to.the.clipboard": "dans le presse-papiers", - "xpack.timelines.copyToClipboardTooltip": "Copier dans le Presse-papiers", - "xpack.timelines.footer.autoRefreshActiveDescription": "Actualisation automatique active", - "xpack.timelines.footer.events": "Événements", - "xpack.timelines.footer.loadingLabel": "Chargement", - "xpack.timelines.footer.loadingTimelineData": "Chargement des données de la chronologie", - "xpack.timelines.footer.of": "sur", - "xpack.timelines.footer.rows": "lignes", - "xpack.timelines.footer.totalCountOfEvents": "événements", "xpack.timelines.hoverActions.addToTimeline": "Ajouter à l'investigation de chronologie", "xpack.timelines.hoverActions.addToTimeline.addedFieldMessage": "Ajout effectué de {fieldOrValue} {isTimeline, select, true {à la chronologie} false {au modèle}}", "xpack.timelines.hoverActions.fieldLabel": "Champ", @@ -31463,58 +31389,6 @@ "xpack.timelines.hoverActions.filterOut": "Exclure", "xpack.timelines.hoverActions.moreActions": "Plus d'actions", "xpack.timelines.hoverActions.tooltipWithKeyboardShortcut.pressTooltipLabel": "Appuyer", - "xpack.timelines.inspect.modal.closeTitle": "Fermer", - "xpack.timelines.inspect.modal.indexPatternDescription": "Modèle d'indexation qui se connecte aux index Elasticsearch. Ces index peuvent être configurés dans Kibana > Paramètres avancés.", - "xpack.timelines.inspect.modal.indexPatternLabel": "Modèle d'indexation", - "xpack.timelines.inspect.modal.noAlertIndexFound": "Aucun index d'alerte n'a été trouvé", - "xpack.timelines.inspect.modal.queryTimeDescription": "Le temps qu'il a fallu pour traiter la requête. Ne comprend pas le temps nécessaire pour envoyer la requête ni l'analyser dans le navigateur.", - "xpack.timelines.inspect.modal.queryTimeLabel": "Heure de la requête", - "xpack.timelines.inspect.modal.reqTimestampDescription": "Heure à laquelle le début de la requête a été enregistré", - "xpack.timelines.inspect.modal.reqTimestampLabel": "Horodatage de la requête", - "xpack.timelines.inspect.modal.somethingWentWrongDescription": "Désolé, un problème est survenu.", - "xpack.timelines.inspectDescription": "Inspection", - "xpack.timelines.lastUpdated.updated": "Mis à jour", - "xpack.timelines.lastUpdated.updating": "Mise à jour...", - "xpack.timelines.tgrid.body.ariaLabel": "Alertes", - "xpack.timelines.tgrid.empty.description": "Essayer de rechercher sur une période plus longue ou de modifier votre recherche", - "xpack.timelines.tgrid.empty.title": "Aucun résultat ne correspond à vos critères de recherche.", - "xpack.timelines.tGrid.eventsLabel": "Événements", - "xpack.timelines.tGrid.footer.loadingEventsDataLabel": "Chargement des événements", - "xpack.timelines.tGrid.footer.totalCountOfEvents": "événements", - "xpack.timelines.timeline.acknowledgedAlertFailedToastMessage": "Impossible de marquer l'alerte ou les alertes comme reconnues", - "xpack.timelines.timeline.acknowledgedSelectedTitle": "Marquer comme reconnue", - "xpack.timelines.timeline.attachExistingCase": "Attacher à un cas existant", - "xpack.timelines.timeline.attachNewCase": "Attacher au nouveau cas", - "xpack.timelines.timeline.body.actions.checkboxForRowAriaLabel": "Case {checked, select, false {non cochée} true {cochée}} pour l'alerte ou l'événement de la ligne {ariaRowindex}, avec les colonnes {columnValues}", - "xpack.timelines.timeline.body.actions.collapseAriaLabel": "Réduire", - "xpack.timelines.timeline.body.actions.expandEventTooltip": "Afficher les détails", - "xpack.timelines.timeline.body.actions.investigateInResolverDisabledTooltip": "Cet événement ne peut pas être analysé, car il a des mappings de champs incompatibles", - "xpack.timelines.timeline.body.actions.investigateInResolverTooltip": "Analyser l'événement", - "xpack.timelines.timeline.body.actions.investigateLabel": "Examiner", - "xpack.timelines.timeline.body.actions.viewDetailsAriaLabel": "Afficher les détails", - "xpack.timelines.timeline.body.actions.viewSummaryLabel": "Afficher le résumé", - "xpack.timelines.timeline.body.copyToClipboardButtonLabel": "Copier dans le Presse-papiers", - "xpack.timelines.timeline.body.notes.disableEventTooltip": "Les notes ne peuvent pas être ajoutées ici lors de la modification d'une chronologie de modèle", - "xpack.timelines.timeline.body.pinning.disablePinnnedTooltip": "Cet événement ne peut pas être épinglé lors de la modification d'une chronologie de modèle", - "xpack.timelines.timeline.body.pinning.pinnnedWithNotesTooltip": "Cet événement ne peut pas être désépinglé, car il contient des notes", - "xpack.timelines.timeline.body.sort.sortedAscendingTooltip": "Trié par ordre croissant", - "xpack.timelines.timeline.body.sort.sortedDescendingTooltip": "Trié par ordre décroissant", - "xpack.timelines.timeline.categoryTooltip": "Catégorie", - "xpack.timelines.timeline.closedAlertFailedToastMessage": "Impossible de fermer l'alerte ou les alertes.", - "xpack.timelines.timeline.closeSelectedTitle": "Marquer comme fermé", - "xpack.timelines.timeline.descriptionTooltip": "Description", - "xpack.timelines.timeline.fieldTooltip": "Champ", - "xpack.timelines.timeline.flyout.pane.removeColumnButtonLabel": "Retirer une colonne", - "xpack.timelines.timeline.fullScreenButton": "Plein écran", - "xpack.timelines.timeline.openedAlertFailedToastMessage": "Impossible d'ouvrir l'alerte/les alertes", - "xpack.timelines.timeline.openSelectedTitle": "Marquer comme ouvert", - "xpack.timelines.timeline.sortAZLabel": "Trier A-Z", - "xpack.timelines.timeline.sortFieldsButton": "Trier les champs", - "xpack.timelines.timeline.sortZALabel": "Trier Z-A", - "xpack.timelines.timeline.typeTooltip": "Type", - "xpack.timelines.timeline.updateAlertStatusFailedSingleAlert": "Impossible de mettre à jour l'alerte, car elle était déjà en cours de modification.", - "xpack.timelines.timelineEvents.errorSearchDescription": "Une erreur s'est produite lors de la recherche d'événements de la chronologie", - "xpack.timelines.toolbar.bulkActions.clearSelectionTitle": "Effacer la sélection", "xpack.transform.actionDeleteTransform.deleteDestDataViewTitle": "Supprimer la vue de données {destinationIndex}", "xpack.transform.actionDeleteTransform.deleteDestinationIndexTitle": "Supprimer l'index de destination {destinationIndex}", "xpack.transform.alertTypes.transformHealth.errorMessagesMessage": "{count, plural, one {La transformation} other {Les transformations}} {transformsString} {count, plural, one {contient} other {contiennent}} des messages d'erreur.", @@ -32395,8 +32269,6 @@ "xpack.triggersActionsUI.sections.rulesList.bulkActionPopover.disableAllTitle": "Désactiver", "xpack.triggersActionsUI.sections.rulesList.bulkActionPopover.enableAllTitle": "Activer", "xpack.triggersActionsUI.sections.rulesList.bulkActionPopover.failedToDeleteRulesMessage": "Impossible de supprimer la ou les règles", - "xpack.triggersActionsUI.sections.rulesList.bulkActionPopover.failedToDisableRulesMessage": "Impossible de désactiver la ou les règles", - "xpack.triggersActionsUI.sections.rulesList.bulkActionPopover.failedToEnableRulesMessage": "Impossible d'activer la ou les règles", "xpack.triggersActionsUI.sections.rulesList.cancelSnooze": "Annuler la répétition", "xpack.triggersActionsUI.sections.rulesList.cancelSnoozeConfirmCallout": "Seule l'occurrence actuelle d'un calendrier sera annulée.", "xpack.triggersActionsUI.sections.rulesList.cancelSnoozeConfirmText": "Reprenez la notification lorsque des alertes sont générées comme défini dans les actions de la règle.", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 4d2d52309f229..5cf72f934ec56 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -2129,13 +2129,9 @@ "discover.fieldChooser.fieldFilterButtonLabel": "タイプでフィルタリング", "discover.fieldChooser.fieldsMobileButtonLabel": "フィールド", "discover.fieldChooser.filter.aggregatableLabel": "集約可能", - "discover.fieldChooser.filter.availableFieldsTitle": "利用可能なフィールド", "discover.fieldChooser.filter.filterByTypeLabel": "タイプでフィルタリング", - "discover.fieldChooser.filter.hideEmptyFieldsLabel": "空のフィールドを非表示", "discover.fieldChooser.filter.indexAndFieldsSectionAriaLabel": "インデックスとフィールド", - "discover.fieldChooser.filter.popularTitle": "人気", "discover.fieldChooser.filter.searchableLabel": "検索可能", - "discover.fieldChooser.filter.selectedFieldsTitle": "スクリプトフィールド", "discover.fieldChooser.filter.toggleButton.any": "すべて", "discover.fieldChooser.filter.toggleButton.no": "いいえ", "discover.fieldChooser.filter.toggleButton.yes": "はい", @@ -2214,12 +2210,8 @@ "discover.grid.viewDoc": "詳細ダイアログを切り替え", "discover.gridSampleSize.advancedSettingsLinkLabel": "高度な設定", "discover.helpMenu.appName": "Discover", - "discover.inspectorRequestDataTitleChart": "グラフデータ", "discover.inspectorRequestDataTitleDocuments": "ドキュメント", - "discover.inspectorRequestDataTitleTotalHits": "総ヒット数", - "discover.inspectorRequestDescriptionChart": "このリクエストはElasticsearchにクエリをかけ、グラフの集計データを取得します。", "discover.inspectorRequestDescriptionDocument": "このリクエストはElasticsearchにクエリをかけ、ドキュメントを取得します。", - "discover.inspectorRequestDescriptionTotalHits": "このリクエストはElasticsearchにクエリをかけ、合計一致数を取得します。", "discover.json.codeEditorAriaLabel": "Elasticsearch ドキュメントの JSON ビューのみを読み込む", "discover.json.copyToClipboardLabel": "クリップボードにコピー", "discover.loadingDocuments": "ドキュメントを読み込み中", @@ -2267,7 +2259,6 @@ "discover.sampleData.viewLinkLabel": "Discover", "discover.savedSearch.savedObjectName": "保存検索", "discover.savedSearchEmbeddable.action.viewSavedSearch.displayName": "Discoverで開く", - "discover.searchingTitle": "検索中", "discover.selectColumnHeader": "列を選択", "discover.showAllDocuments": "すべてのドキュメントを表示", "discover.showErrorMessageAgain": "エラーメッセージを表示", @@ -7138,12 +7129,6 @@ "xpack.apm.dependencyDetailThroughputChartTitle": "スループット", "xpack.apm.dependencyErrorRateChart.chartTitle": "失敗したトランザクション率", "xpack.apm.dependencyLatencyChart.chartTitle": "レイテンシ", - "xpack.apm.dependencyOperationDetailTraceList": "トレース", - "xpack.apm.dependencyOperationDetailTraceListDurationColumn": "期間", - "xpack.apm.dependencyOperationDetailTraceListOutcomeColumn": "成果", - "xpack.apm.dependencyOperationDetailTraceListServiceNameColumn": "発生元サービス", - "xpack.apm.dependencyOperationDetailTraceListTimestampColumn": "タイムスタンプ", - "xpack.apm.dependencyOperationDetailTraceListTransactionNameColumn": "トランザクション名", "xpack.apm.dependencyOperationDistributionChart.allSpansLegendLabel": "すべてのスパン", "xpack.apm.dependencyOperationDistributionChart.failedSpansLegendLabel": "失敗したスパン", "xpack.apm.dependencyThroughputChart.chartTitle": "スループット", @@ -9280,7 +9265,6 @@ "xpack.cases.caseView.comment": "コメント", "xpack.cases.caseView.comment.addComment": "コメントを追加", "xpack.cases.caseView.comment.addCommentHelpText": "新しいコメントを追加...", - "xpack.cases.caseView.commentFieldRequiredError": "コメントが必要です。", "xpack.cases.caseView.connectors": "外部インシデント管理システム", "xpack.cases.caseView.copyCommentLinkAria": "参照リンクをコピー", "xpack.cases.caseView.create": "ケースを作成", @@ -17248,8 +17232,6 @@ "xpack.lens.indexPattern.valueCountOf": "{name}のカウント", "xpack.lens.indexPatternSuggestion.removeLayerLabel": "{indexPatternTitle}のみを表示", "xpack.lens.indexPatternSuggestion.removeLayerPositionLabel": "レイヤー{layerNumber}のみを表示", - "xpack.lens.modalTitle.title.clear": "{layerType}レイヤーをクリアしますか?", - "xpack.lens.modalTitle.title.delete": "{layerType}レイヤーを削除しますか?", "xpack.lens.pie.arrayValues": "{label}には配列値が含まれます。可視化が想定通りに表示されない場合があります。", "xpack.lens.pie.suggestionLabel": "{chartName}として", "xpack.lens.shared.legend.filterOptionsLegend": "{legendDataLabel}、フィルターオプション", @@ -17654,7 +17636,6 @@ "xpack.lens.indexPattern.min": "最低", "xpack.lens.indexPattern.min.description": "集約されたドキュメントから抽出された数値の最小値を返す単一値メトリック集約。", "xpack.lens.indexPattern.missingFieldLabel": "見つからないフィールド", - "xpack.lens.indexPattern.moveToWorkspaceDisabled": "このフィールドは自動的にワークスペースに追加できません。構成パネルで直接使用することはできます。", "xpack.lens.indexPattern.moving_average.signature": "メトリック:数値、[window]:数値", "xpack.lens.indexPattern.movingAverage": "移動平均", "xpack.lens.indexPattern.movingAverage.basicExplanation": "移動平均はデータ全体でウィンドウをスライドし、平均値を表示します。移動平均は日付ヒストグラムでのみサポートされています。", @@ -17839,9 +17820,6 @@ "xpack.lens.metric.progressDirectionLabel": "バーの方向", "xpack.lens.metric.secondaryMetric": "副メトリック", "xpack.lens.metric.subtitleLabel": "サブタイトル", - "xpack.lens.modalTitle.layerType.annotation": "注釈", - "xpack.lens.modalTitle.layerType.data": "ビジュアライゼーション", - "xpack.lens.modalTitle.layerType.refLines": "基準線", "xpack.lens.pageTitle": "レンズ", "xpack.lens.paletteHeatmapGradient.customize": "編集", "xpack.lens.paletteHeatmapGradient.customizeLong": "パレットを編集", @@ -17884,7 +17862,6 @@ "xpack.lens.pieChart.valuesLabel": "ラベル", "xpack.lens.pieChart.visualOptionsLabel": "視覚オプション", "xpack.lens.primaryMetric.label": "主メトリック", - "xpack.lens.resetVisualizationAriaLabel": "ビジュアライゼーションをリセット", "xpack.lens.saveDuplicateRejectedDescription": "重複ファイルの保存確認が拒否されました", "xpack.lens.searchTitle": "Lens:ビジュアライゼーションを作成", "xpack.lens.section.configPanelLabel": "構成パネル", @@ -20858,7 +20835,6 @@ "xpack.ml.newJob.fromLens.createJob.error.colsNoSourceField": "一部の列にはソースフィールドがありません。", "xpack.ml.newJob.fromLens.createJob.error.colsUsingFilterTimeSift": "ML検知器に対応していない設定が列に含まれています。時間シフトとフィルター条件はサポートされていません。", "xpack.ml.newJob.fromLens.createJob.error.incompatibleLayerType": "レイヤーに互換性がありません。グラフレイヤーのみを使用できます。", - "xpack.ml.newJob.fromLens.createJob.error.noCompatibleLayers": "ビジュアライゼーションには、異常検知ジョブを作成するために使用できるレイヤーがありません。", "xpack.ml.newJob.fromLens.createJob.error.noDataViews": "ビジュアライゼーションでデータビューが見つかりません。", "xpack.ml.newJob.fromLens.createJob.error.noDateField": "日付フィールドが見つかりません。", "xpack.ml.newJob.fromLens.createJob.error.noTimeRange": "時間範囲が指定されていません。", @@ -23743,7 +23719,6 @@ "xpack.osquery.fleetIntegration.osqueryConfig.packConfigFilesErrorMessage": "パック構成ファイルはサポートされていません。これらのパックを削除する必要があります:{packNames}。", "xpack.osquery.fleetIntegration.osqueryConfig.restrictedOptionsErrorMessage": "次のosqueryオプションはサポートされていないため、削除する必要があります:{restrictedFlags}。", "xpack.osquery.liveQuery.permissionDeniedPromptBody": "クエリ結果を表示するには、ユーザーロールを更新して、{logs}インデックスに対する{read}権限を付与するように、管理者に依頼してください。", - "xpack.osquery.liveQuery.queryForm.largeQueryError": "クエリが大きすぎます(最大{maxLength}文字)", "xpack.osquery.newPack.successToastMessageText": "\"{packName}\"パックが正常に作成されました", "xpack.osquery.newSavedQuery.successToastMessageText": "\"{savedQueryId}\"クエリが正常に保存されました", "xpack.osquery.pack.queriesTable.deleteActionAriaLabel": "{queryName}を削除", @@ -24152,7 +24127,6 @@ "xpack.reporting.exportTypes.csv.generateCsv.esErrorMessage": "Elasticsearchから{statusCode}応答を受信しました:{message}", "xpack.reporting.exportTypes.csv.generateCsv.unknownErrorMessage": "不明なエラーが発生しました:{message}", "xpack.reporting.jobsQuery.deleteError": "レポートを削除できません:{error}", - "xpack.reporting.jobsQuery.infoError.unauthorizedErrorMessage": "{jobType}情報を表示する権限がありません", "xpack.reporting.jobStatusDetail.attemptXofY": "{attempts}/{max_attempts}回試行します。", "xpack.reporting.jobStatusDetail.timeoutSeconds": "{timeout}秒", "xpack.reporting.listing.diagnosticApiCallFailure": "診断の実行中に問題が発生しました:{error}", @@ -25647,12 +25621,7 @@ "xpack.securitySolution.searchStrategy.error": "検索を実行できませんでした:{factoryQueryType}", "xpack.securitySolution.some_page.flyoutCreateSubmitSuccess": "\"{name}\"が追加されました。", "xpack.securitySolution.tables.rowItemHelper.overflowButtonDescription": "他{count}件", - "xpack.securitySolution.timeline.body.actions.addNotesForRowAriaLabel": "行 {ariaRowindex}、列 {columnValues} のイベントのメモをタイムラインに追加", "xpack.securitySolution.timeline.body.actions.attachAlertToCaseForRowAriaLabel": "行 {ariaRowindex}、列 {columnValues} のアラートまたはイベントをケースに追加", - "xpack.securitySolution.timeline.body.actions.investigateInResolverForRowAriaLabel": "行 {ariaRowindex}、列 {columnValues} のアラートまたはイベントを分析", - "xpack.securitySolution.timeline.body.actions.moreActionsForRowAriaLabel": "行 {ariaRowindex}、列 {columnValues} のアラートまたはイベントのその他のアクションを選択", - "xpack.securitySolution.timeline.body.actions.sendAlertToTimelineForRowAriaLabel": "行 {ariaRowindex}、列 {columnValues} のイベントのアラートを送信", - "xpack.securitySolution.timeline.body.actions.viewDetailsForRowAriaLabel": "行 {ariaRowindex}、列 {columnValues} のアラートまたはイベントの詳細を表示", "xpack.securitySolution.timeline.body.pinning.disablePinnnedTooltip": "テンプレートタイムラインの編集中には、この{isAlert, select, true{アラート} other{イベント}}がピン留めされない場合があります", "xpack.securitySolution.timeline.body.pinning.pinnnedWithNotesTooltip": "この{isAlert, select, true{アラート} other{イベント}}にはメモがあるためピン留めできません", "xpack.securitySolution.timeline.body.pinning.pinTooltip": "{isAlert, select, true{アラート} other{イベント}}をピン留め", @@ -28855,10 +28824,6 @@ "xpack.securitySolution.timeline.autosave.warning.description": "別のユーザーがこのタイムラインに変更を加えました。このタイムラインを更新してこれらの変更を反映させるまで、ユーザーによる変更は自動的に保存されません。", "xpack.securitySolution.timeline.autosave.warning.refresh.title": "タイムラインを更新", "xpack.securitySolution.timeline.autosave.warning.title": "更新されるまで自動保存は無効です", - "xpack.securitySolution.timeline.body.actions.checkboxForRowAriaLabel": "行 {ariaRowindex}、列 {columnValues} のアラートまたはイベントのチェックボックスを{checked, select, false {オフ} true {オン}}", - "xpack.securitySolution.timeline.body.actions.investigateInResolverTooltip": "イベントを分析します", - "xpack.securitySolution.timeline.body.actions.pinEventForRowAriaLabel": "行 {ariaRowindex}、列 {columnValues} のイベントを{isEventPinned, select, false {固定} true {固定解除}}", - "xpack.securitySolution.timeline.body.actions.viewDetailsAriaLabel": "詳細を表示", "xpack.securitySolution.timeline.body.notes.addNoteTooltip": "メモを追加", "xpack.securitySolution.timeline.body.notes.disableEventTooltip": "テンプレートタイムラインの編集中には、メモが追加されない場合があります", "xpack.securitySolution.timeline.body.openSessionViewLabel": "セッションビューを開く", @@ -30859,7 +30824,6 @@ "xpack.synthetics.keyValuePairsField.value.ariaLabel": "値", "xpack.synthetics.keyValuePairsField.value.label": "値", "xpack.synthetics.kueryBar.searchPlaceholder.kql": "KQL構文を使用して、モニターID、名前、タイプ(例:monitor.type: \"http\" AND tags: \"dev\")などを検索", - "xpack.synthetics.kueryBar.searchPlaceholder.simple": "モニターID、名前、またはURL(例:http://)で検索", "xpack.synthetics.locationName.helpLinkAnnotation": "場所を追加", "xpack.synthetics.management.confirmDescriptionLabel": "このアクションにより、モニターが削除されますが、収集されたデータはすべて保持されます。この操作は元に戻すことができません。", "xpack.synthetics.management.deleteLabel": "削除", @@ -31084,7 +31048,6 @@ "xpack.synthetics.monitorManagement.monitorAdvancedOptions.monitorNamespaceFieldLabel": "名前空間", "xpack.synthetics.monitorManagement.monitorAdvancedOptions.namespaceHelpLearnMoreLabel": "詳細情報", "xpack.synthetics.monitorManagement.monitorDeleteFailureMessage": "モニターを削除できませんでした。しばらくたってから再試行してください。", - "xpack.synthetics.monitorManagement.monitorDeleteLoadingMessage": "モニターを削除しています...", "xpack.synthetics.monitorManagement.monitorEditedSuccessMessage": "モニターは正常に更新されました。", "xpack.synthetics.monitorManagement.monitorFailureMessage": "モニターを保存できませんでした。しばらくたってから再試行してください。", "xpack.synthetics.monitorManagement.monitorList.actions": "アクション", @@ -31389,49 +31352,12 @@ "xpack.threatIntelligence.field.threat.indicator.last_seen": "前回の認識", "xpack.threatIntelligence.indicator.table.viewDetailsButton": "詳細を表示", "xpack.timelines.clipboard.copy.successToastTitle": "フィールド{field}をクリップボードにコピーしました", - "xpack.timelines.footer.autoRefreshActiveTooltip": "自動更新が有効な間、タイムラインはクエリに一致する最新の {numberOfItems} 件のイベントを表示します。", - "xpack.timelines.footer.rowsPerPageLabel": "ページごとの行:{rowsPerPage}", "xpack.timelines.hoverActions.columnToggleLabel": "表の{field}列を切り替える", "xpack.timelines.hoverActions.nestedColumnToggleLabel": "{field}フィールドはオブジェクトであり、列として追加できるネストされたフィールドに分解されます", - "xpack.timelines.tGrid.unit": "{totalCount, plural, other {アラート}}", - "xpack.timelines.timeline.acknowledgedAlertSuccessToastMessage": "{totalAlerts} {totalAlerts, plural, other {件のアラート}}を確認済みに設定しました。", - "xpack.timelines.timeline.alertsUnit": "{totalCount, plural, other {アラート}}", - "xpack.timelines.timeline.body.actions.addNotesForRowAriaLabel": "行 {ariaRowindex}、列 {columnValues} のイベントのメモをタイムラインに追加", - "xpack.timelines.timeline.body.actions.attachAlertToCaseForRowAriaLabel": "行 {ariaRowindex}、列 {columnValues} のアラートまたはイベントをケースに追加", - "xpack.timelines.timeline.body.actions.investigateInResolverForRowAriaLabel": "行 {ariaRowindex}、列 {columnValues} のアラートまたはイベントを分析", - "xpack.timelines.timeline.body.actions.moreActionsForRowAriaLabel": "行 {ariaRowindex}、列 {columnValues} のアラートまたはイベントのその他のアクションを選択", - "xpack.timelines.timeline.body.actions.sendAlertToTimelineForRowAriaLabel": "行 {ariaRowindex}、列 {columnValues} のイベントのアラートを送信", - "xpack.timelines.timeline.body.actions.viewDetailsForRowAriaLabel": "行 {ariaRowindex}、列 {columnValues} のアラートまたはイベントの詳細を表示", - "xpack.timelines.timeline.closedAlertSuccessToastMessage": "{totalAlerts} {totalAlerts, plural, other {件のアラート}}を正常にクローズしました。", - "xpack.timelines.timeline.eventsTableAriaLabel": "イベント; {activePage}/{totalPages} ページ", - "xpack.timelines.timeline.openedAlertSuccessToastMessage": "{totalAlerts} {totalAlerts, plural, other {件のアラート}}を正常に開きました。", - "xpack.timelines.timeline.properties.timelineToggleButtonAriaLabel": "タイムライン {title} を{isOpen, select, false {開く} true {閉じる} other {切り替える}}", - "xpack.timelines.timeline.updateAlertStatusFailed": "{ conflicts } {conflicts, plural, other {アラート}}を更新できませんでした。", - "xpack.timelines.timeline.updateAlertStatusFailedDetailed": "{ updated } {updated, plural, other {アラート}}が正常に更新されましたが、{ conflicts }は更新できませんでした。\n { conflicts, plural, other {}}すでに修正されています。", - "xpack.timelines.timeline.youAreInAnEventRendererScreenReaderOnly": "行 {row} のイベントレンダラーを表示しています。上矢印キーを押すと、終了して現在の行に戻ります。下矢印キーを押すと、終了して次の行に進みます。", - "xpack.timelines.toolbar.bulkActions.selectAllAlertsTitle": "すべての{totalAlertsFormatted} {totalAlerts, plural, other {件のアラート}}を選択", - "xpack.timelines.toolbar.bulkActions.selectedAlertsTitle": "Selected {selectedAlertsFormatted} {selectedAlerts, plural, other {件のアラート}}", - "xpack.timelines.alerts.EventRenderedView.eventSummary.column": "イベント概要", - "xpack.timelines.alerts.EventRenderedView.rule.column": "ルール", - "xpack.timelines.alerts.EventRenderedView.timestamp.column": "タイムスタンプ", - "xpack.timelines.alerts.summaryView.eventRendererView.label": "イベント表示ビュー", - "xpack.timelines.alerts.summaryView.gridView.label": "グリッドビュー", - "xpack.timelines.alerts.summaryView.options.default.description": "特定のフィールドでグループ化および並べ替えることができるタブ形式のデータとして表示", - "xpack.timelines.alerts.summaryView.options.summaryView.description": "各アラートのイベントフローのレンダリングを表示", - "xpack.timelines.beatFields.errorSearchDescription": "Beatフィールドの取得でエラーが発生しました", - "xpack.timelines.beatFields.failSearchDescription": "Beat フィールドで検索を実行できませんでした", "xpack.timelines.clipboard.copied": "コピー完了", "xpack.timelines.clipboard.copy": "コピー", "xpack.timelines.clipboard.copy.to.the.clipboard": "クリップボードにコピー", "xpack.timelines.clipboard.to.the.clipboard": "クリップボードに", - "xpack.timelines.copyToClipboardTooltip": "クリップボードにコピー", - "xpack.timelines.footer.autoRefreshActiveDescription": "自動更新アクション", - "xpack.timelines.footer.events": "イベント", - "xpack.timelines.footer.loadingLabel": "読み込み中", - "xpack.timelines.footer.loadingTimelineData": "タイムラインデータを読み込み中", - "xpack.timelines.footer.of": "/", - "xpack.timelines.footer.rows": "行", - "xpack.timelines.footer.totalCountOfEvents": "イベント", "xpack.timelines.hoverActions.addToTimeline": "タイムライン調査に追加", "xpack.timelines.hoverActions.addToTimeline.addedFieldMessage": "{fieldOrValue}を{isTimeline, select, true {タイムライン} false {テンプレート}}に追加しました", "xpack.timelines.hoverActions.fieldLabel": "フィールド", @@ -31439,58 +31365,6 @@ "xpack.timelines.hoverActions.filterOut": "除外", "xpack.timelines.hoverActions.moreActions": "さらにアクションを表示", "xpack.timelines.hoverActions.tooltipWithKeyboardShortcut.pressTooltipLabel": "プレス", - "xpack.timelines.inspect.modal.closeTitle": "閉じる", - "xpack.timelines.inspect.modal.indexPatternDescription": "Elasticsearchインデックスに接続したインデックスパターンです。これらのインデックスは Kibana > 高度な設定で構成できます。", - "xpack.timelines.inspect.modal.indexPatternLabel": "インデックスパターン", - "xpack.timelines.inspect.modal.noAlertIndexFound": "アラートインデックスが見つかりません", - "xpack.timelines.inspect.modal.queryTimeDescription": "クエリの処理の所要時間です。リクエストの送信やブラウザーでのパースの時間は含まれません。", - "xpack.timelines.inspect.modal.queryTimeLabel": "クエリ時間", - "xpack.timelines.inspect.modal.reqTimestampDescription": "リクエストの開始が記録された時刻です", - "xpack.timelines.inspect.modal.reqTimestampLabel": "リクエストのタイムスタンプ", - "xpack.timelines.inspect.modal.somethingWentWrongDescription": "申し訳ございませんが、何か問題が発生しました。", - "xpack.timelines.inspectDescription": "検査", - "xpack.timelines.lastUpdated.updated": "更新しました", - "xpack.timelines.lastUpdated.updating": "更新中...", - "xpack.timelines.tgrid.body.ariaLabel": "アラート", - "xpack.timelines.tgrid.empty.description": "期間を長くして検索するか、検索を変更してください", - "xpack.timelines.tgrid.empty.title": "検索条件と一致する結果がありません。", - "xpack.timelines.tGrid.eventsLabel": "イベント", - "xpack.timelines.tGrid.footer.loadingEventsDataLabel": "イベントを読み込み中", - "xpack.timelines.tGrid.footer.totalCountOfEvents": "イベント", - "xpack.timelines.timeline.acknowledgedAlertFailedToastMessage": "アラートを確認済みに設定できませんでした", - "xpack.timelines.timeline.acknowledgedSelectedTitle": "確認済みに設定", - "xpack.timelines.timeline.attachExistingCase": "既存のケースに添付", - "xpack.timelines.timeline.attachNewCase": "新しいケースに添付", - "xpack.timelines.timeline.body.actions.checkboxForRowAriaLabel": "行 {ariaRowindex}、列 {columnValues} のアラートまたはイベントのチェックボックスを{checked, select, false {オフ} true {オン}}", - "xpack.timelines.timeline.body.actions.collapseAriaLabel": "縮小", - "xpack.timelines.timeline.body.actions.expandEventTooltip": "詳細を表示", - "xpack.timelines.timeline.body.actions.investigateInResolverDisabledTooltip": "このイベントを分析できません。フィールドマッピングの互換性がありません", - "xpack.timelines.timeline.body.actions.investigateInResolverTooltip": "イベントを分析します", - "xpack.timelines.timeline.body.actions.investigateLabel": "調査", - "xpack.timelines.timeline.body.actions.viewDetailsAriaLabel": "詳細を表示", - "xpack.timelines.timeline.body.actions.viewSummaryLabel": "概要を表示", - "xpack.timelines.timeline.body.copyToClipboardButtonLabel": "クリップボードにコピー", - "xpack.timelines.timeline.body.notes.disableEventTooltip": "テンプレートタイムラインの編集中には、メモが追加されない場合があります", - "xpack.timelines.timeline.body.pinning.disablePinnnedTooltip": "テンプレートタイムラインの編集中には、このイベントがピン留めされない場合があります", - "xpack.timelines.timeline.body.pinning.pinnnedWithNotesTooltip": "イベントにメモがあり、ピンを外すことができません", - "xpack.timelines.timeline.body.sort.sortedAscendingTooltip": "昇順で並べ替えます", - "xpack.timelines.timeline.body.sort.sortedDescendingTooltip": "降順で並べ替えます", - "xpack.timelines.timeline.categoryTooltip": "カテゴリー", - "xpack.timelines.timeline.closedAlertFailedToastMessage": "アラートをクローズできませんでした。", - "xpack.timelines.timeline.closeSelectedTitle": "クローズ済みに設定", - "xpack.timelines.timeline.descriptionTooltip": "説明", - "xpack.timelines.timeline.fieldTooltip": "フィールド", - "xpack.timelines.timeline.flyout.pane.removeColumnButtonLabel": "列を削除", - "xpack.timelines.timeline.fullScreenButton": "全画面", - "xpack.timelines.timeline.openedAlertFailedToastMessage": "アラートを開けませんでした", - "xpack.timelines.timeline.openSelectedTitle": "開封済みに設定", - "xpack.timelines.timeline.sortAZLabel": "A-Zの昇順で並べ替え", - "xpack.timelines.timeline.sortFieldsButton": "フィールドの並べ替え", - "xpack.timelines.timeline.sortZALabel": "ZーAの降順で並べ替え", - "xpack.timelines.timeline.typeTooltip": "型", - "xpack.timelines.timeline.updateAlertStatusFailedSingleAlert": "アラートを更新できませんでした。アラートはすでに修正されています。", - "xpack.timelines.timelineEvents.errorSearchDescription": "タイムラインイベント検索でエラーが発生しました", - "xpack.timelines.toolbar.bulkActions.clearSelectionTitle": "選択した項目をクリア", "xpack.transform.actionDeleteTransform.deleteDestDataViewTitle": "データビュー{destinationIndex}を削除", "xpack.transform.actionDeleteTransform.deleteDestinationIndexTitle": "ディスティネーションインデックス{destinationIndex}の削除", "xpack.transform.alertTypes.transformHealth.errorMessagesMessage": "{count, plural, other {個の変換}} {transformsString} {count, plural, other {}}にエラーメッセージがあります。", @@ -32369,8 +32243,6 @@ "xpack.triggersActionsUI.sections.rulesList.bulkActionPopover.disableAllTitle": "無効にする", "xpack.triggersActionsUI.sections.rulesList.bulkActionPopover.enableAllTitle": "有効にする", "xpack.triggersActionsUI.sections.rulesList.bulkActionPopover.failedToDeleteRulesMessage": "ルールを削除できませんでした", - "xpack.triggersActionsUI.sections.rulesList.bulkActionPopover.failedToDisableRulesMessage": "ルールを無効にできませんでした", - "xpack.triggersActionsUI.sections.rulesList.bulkActionPopover.failedToEnableRulesMessage": "ルールを有効にできませんでした", "xpack.triggersActionsUI.sections.rulesList.cancelSnooze": "スヌーズをキャンセル", "xpack.triggersActionsUI.sections.rulesList.cancelSnoozeConfirmCallout": "スケジュールの最新の発生のみがキャンセルされます。", "xpack.triggersActionsUI.sections.rulesList.cancelSnoozeConfirmText": "ルールアクションの定義に従ってアラートが生成されるときに通知を再開します。", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index be1b4b83c922a..4330b9d74fbab 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -2133,13 +2133,9 @@ "discover.fieldChooser.fieldFilterButtonLabel": "按类型筛选", "discover.fieldChooser.fieldsMobileButtonLabel": "字段", "discover.fieldChooser.filter.aggregatableLabel": "可聚合", - "discover.fieldChooser.filter.availableFieldsTitle": "可用字段", "discover.fieldChooser.filter.filterByTypeLabel": "按类型筛选", - "discover.fieldChooser.filter.hideEmptyFieldsLabel": "隐藏空字段", "discover.fieldChooser.filter.indexAndFieldsSectionAriaLabel": "索引和字段", - "discover.fieldChooser.filter.popularTitle": "常见", "discover.fieldChooser.filter.searchableLabel": "可搜索", - "discover.fieldChooser.filter.selectedFieldsTitle": "选定字段", "discover.fieldChooser.filter.toggleButton.any": "任意", "discover.fieldChooser.filter.toggleButton.no": "否", "discover.fieldChooser.filter.toggleButton.yes": "是", @@ -2218,12 +2214,8 @@ "discover.grid.viewDoc": "切换具有详情的对话框", "discover.gridSampleSize.advancedSettingsLinkLabel": "高级设置", "discover.helpMenu.appName": "Discover", - "discover.inspectorRequestDataTitleChart": "图表数据", "discover.inspectorRequestDataTitleDocuments": "文档", - "discover.inspectorRequestDataTitleTotalHits": "总命中数", - "discover.inspectorRequestDescriptionChart": "此请求将查询 Elasticsearch 以获取图表的聚合数据。", "discover.inspectorRequestDescriptionDocument": "此请求将查询 Elasticsearch 以获取文档。", - "discover.inspectorRequestDescriptionTotalHits": "此请求将查询 Elasticsearch 以获取总命中数。", "discover.json.codeEditorAriaLabel": "Elasticsearch 文档的只读 JSON 视图", "discover.json.copyToClipboardLabel": "复制到剪贴板", "discover.loadingDocuments": "正在加载文档", @@ -2271,7 +2263,6 @@ "discover.sampleData.viewLinkLabel": "Discover", "discover.savedSearch.savedObjectName": "已保存搜索", "discover.savedSearchEmbeddable.action.viewSavedSearch.displayName": "在 Discover 中打开", - "discover.searchingTitle": "正在搜索", "discover.selectColumnHeader": "选择列", "discover.showAllDocuments": "显示所有文档", "discover.showErrorMessageAgain": "显示错误消息", @@ -7154,12 +7145,6 @@ "xpack.apm.dependencyDetailThroughputChartTitle": "吞吐量", "xpack.apm.dependencyErrorRateChart.chartTitle": "失败事务率", "xpack.apm.dependencyLatencyChart.chartTitle": "延迟", - "xpack.apm.dependencyOperationDetailTraceList": "追溯", - "xpack.apm.dependencyOperationDetailTraceListDurationColumn": "持续时间", - "xpack.apm.dependencyOperationDetailTraceListOutcomeColumn": "结果", - "xpack.apm.dependencyOperationDetailTraceListServiceNameColumn": "发起服务", - "xpack.apm.dependencyOperationDetailTraceListTimestampColumn": "时间戳", - "xpack.apm.dependencyOperationDetailTraceListTransactionNameColumn": "事务名称", "xpack.apm.dependencyOperationDistributionChart.allSpansLegendLabel": "所有跨度", "xpack.apm.dependencyOperationDistributionChart.failedSpansLegendLabel": "失败的跨度", "xpack.apm.dependencyThroughputChart.chartTitle": "吞吐量", @@ -9298,7 +9283,6 @@ "xpack.cases.caseView.comment": "注释", "xpack.cases.caseView.comment.addComment": "添加注释", "xpack.cases.caseView.comment.addCommentHelpText": "添加新注释......", - "xpack.cases.caseView.commentFieldRequiredError": "注释必填。", "xpack.cases.caseView.connectors": "外部事件管理系统", "xpack.cases.caseView.copyCommentLinkAria": "复制引用链接", "xpack.cases.caseView.create": "创建案例", @@ -17273,8 +17257,6 @@ "xpack.lens.indexPattern.valueCountOf": "{name} 的计数", "xpack.lens.indexPatternSuggestion.removeLayerLabel": "仅显示 {indexPatternTitle}", "xpack.lens.indexPatternSuggestion.removeLayerPositionLabel": "仅显示图层 {layerNumber}", - "xpack.lens.modalTitle.title.clear": "清除 {layerType} 图层?", - "xpack.lens.modalTitle.title.delete": "删除 {layerType} 图层?", "xpack.lens.pie.arrayValues": "{label} 包含数组值。您的可视化可能无法正常渲染。", "xpack.lens.pie.suggestionLabel": "为 {chartName}", "xpack.lens.shared.legend.filterOptionsLegend": "{legendDataLabel}, 筛选选项", @@ -17679,7 +17661,6 @@ "xpack.lens.indexPattern.min": "最小值", "xpack.lens.indexPattern.min.description": "单值指标聚合,返回从聚合文档提取的数值中的最小值。", "xpack.lens.indexPattern.missingFieldLabel": "缺失字段", - "xpack.lens.indexPattern.moveToWorkspaceDisabled": "此字段无法自动添加到工作区。您仍可以在配置面板中直接使用它。", "xpack.lens.indexPattern.moving_average.signature": "指标:数字,[window]:数字", "xpack.lens.indexPattern.movingAverage": "移动平均值", "xpack.lens.indexPattern.movingAverage.basicExplanation": "移动平均值在数据上滑动时间窗并显示平均值。仅日期直方图支持移动平均值。", @@ -17864,9 +17845,6 @@ "xpack.lens.metric.progressDirectionLabel": "条形图方向", "xpack.lens.metric.secondaryMetric": "次级指标", "xpack.lens.metric.subtitleLabel": "子标题", - "xpack.lens.modalTitle.layerType.annotation": "标注", - "xpack.lens.modalTitle.layerType.data": "可视化", - "xpack.lens.modalTitle.layerType.refLines": "参考线", "xpack.lens.pageTitle": "Lens", "xpack.lens.paletteHeatmapGradient.customize": "编辑", "xpack.lens.paletteHeatmapGradient.customizeLong": "编辑调色板", @@ -17909,7 +17887,6 @@ "xpack.lens.pieChart.valuesLabel": "标签", "xpack.lens.pieChart.visualOptionsLabel": "视觉选项", "xpack.lens.primaryMetric.label": "主要指标", - "xpack.lens.resetVisualizationAriaLabel": "重置可视化", "xpack.lens.saveDuplicateRejectedDescription": "已拒绝使用重复标题保存确认", "xpack.lens.searchTitle": "Lens:创建可视化", "xpack.lens.section.configPanelLabel": "配置面板", @@ -20888,7 +20865,6 @@ "xpack.ml.newJob.fromLens.createJob.error.colsNoSourceField": "某些列不包含源字段。", "xpack.ml.newJob.fromLens.createJob.error.colsUsingFilterTimeSift": "列包含与 ML 检测工具不兼容的设置,不支持时间偏移和筛选依据。", "xpack.ml.newJob.fromLens.createJob.error.incompatibleLayerType": "图层不兼容。只可以使用图表图层。", - "xpack.ml.newJob.fromLens.createJob.error.noCompatibleLayers": "可视化不包含任何可用于创建异常检测作业的图层。", "xpack.ml.newJob.fromLens.createJob.error.noDataViews": "在可视化中找不到数据视图。", "xpack.ml.newJob.fromLens.createJob.error.noDateField": "找不到日期字段。", "xpack.ml.newJob.fromLens.createJob.error.noTimeRange": "未指定时间范围。", @@ -23774,7 +23750,6 @@ "xpack.osquery.fleetIntegration.osqueryConfig.packConfigFilesErrorMessage": "不支持包配置文件。必须移除这些包:{packNames}。", "xpack.osquery.fleetIntegration.osqueryConfig.restrictedOptionsErrorMessage": "不支持以下 osquery 选项,必须将其移除:{restrictedFlags}。", "xpack.osquery.liveQuery.permissionDeniedPromptBody": "要查看查询结果,请要求管理员将您的角色更新为具有 {logs} 索引的索引 {read} 权限。", - "xpack.osquery.liveQuery.queryForm.largeQueryError": "查询过大(最多 {maxLength} 个字符)", "xpack.osquery.newPack.successToastMessageText": "已成功创建“{packName}”包", "xpack.osquery.newSavedQuery.successToastMessageText": "已成功保存“{savedQueryId}”查询", "xpack.osquery.pack.queriesTable.deleteActionAriaLabel": "删除 {queryName}", @@ -24184,7 +24159,6 @@ "xpack.reporting.exportTypes.csv.generateCsv.esErrorMessage": "从 Elasticsearch 收到 {statusCode} 响应:{message}", "xpack.reporting.exportTypes.csv.generateCsv.unknownErrorMessage": "出现未知错误:{message}", "xpack.reporting.jobsQuery.deleteError": "无法删除报告:{error}", - "xpack.reporting.jobsQuery.infoError.unauthorizedErrorMessage": "抱歉,您无权查看 {jobType} 信息", "xpack.reporting.jobStatusDetail.attemptXofY": "尝试 {attempts} 次,最多可尝试 {max_attempts} 次。", "xpack.reporting.jobStatusDetail.timeoutSeconds": "{timeout} 秒", "xpack.reporting.listing.diagnosticApiCallFailure": "运行诊断时出现问题:{error}", @@ -25681,12 +25655,7 @@ "xpack.securitySolution.searchStrategy.error": "无法运行搜索:{factoryQueryType}", "xpack.securitySolution.some_page.flyoutCreateSubmitSuccess": "已添加“{name}”。", "xpack.securitySolution.tables.rowItemHelper.overflowButtonDescription": "另外 {count} 个", - "xpack.securitySolution.timeline.body.actions.addNotesForRowAriaLabel": "将事件第 {ariaRowindex} 行的备注添加到时间线,其中列为 {columnValues}", "xpack.securitySolution.timeline.body.actions.attachAlertToCaseForRowAriaLabel": "将第 {ariaRowindex} 行的告警或事件附加到案例,其中列为 {columnValues}", - "xpack.securitySolution.timeline.body.actions.investigateInResolverForRowAriaLabel": "分析第 {ariaRowindex} 行的告警或事件,其中列为 {columnValues}", - "xpack.securitySolution.timeline.body.actions.moreActionsForRowAriaLabel": "为第 {ariaRowindex} 行中的告警或事件选择更多操作,其中列为 {columnValues}", - "xpack.securitySolution.timeline.body.actions.sendAlertToTimelineForRowAriaLabel": "将第 {ariaRowindex} 行的告警发送到时间线,其中列为 {columnValues}", - "xpack.securitySolution.timeline.body.actions.viewDetailsForRowAriaLabel": "查看第 {ariaRowindex} 行的告警或事件的详细信息,其中列为 {columnValues}", "xpack.securitySolution.timeline.body.pinning.disablePinnnedTooltip": "编辑模板时间线时无法置顶此{isAlert, select, true{告警} other{事件}}", "xpack.securitySolution.timeline.body.pinning.pinnnedWithNotesTooltip": "无法取消置顶此{isAlert, select, true{告警} other{事件}},因为它具有备注", "xpack.securitySolution.timeline.body.pinning.pinTooltip": "置顶{isAlert, select, true{告警} other{事件}}", @@ -28889,10 +28858,6 @@ "xpack.securitySolution.timeline.autosave.warning.description": "其他用户已更改此时间线。您所做的任何更改不会自动保存,直至您刷新了此时间线以吸收这些更改。", "xpack.securitySolution.timeline.autosave.warning.refresh.title": "刷新时间线", "xpack.securitySolution.timeline.autosave.warning.title": "刷新后才会启用自动保存", - "xpack.securitySolution.timeline.body.actions.checkboxForRowAriaLabel": "告警或事件第 {ariaRowindex} 行的{checked, select, false {已取消选中} true {已选中}}复选框,其中列为 {columnValues}", - "xpack.securitySolution.timeline.body.actions.investigateInResolverTooltip": "分析事件", - "xpack.securitySolution.timeline.body.actions.pinEventForRowAriaLabel": "将第 {ariaRowindex} 行的事件{isEventPinned, select, false {固定} true {取消固定}}到时间线,其中列为 {columnValues}", - "xpack.securitySolution.timeline.body.actions.viewDetailsAriaLabel": "查看详情", "xpack.securitySolution.timeline.body.notes.addNoteTooltip": "添加备注", "xpack.securitySolution.timeline.body.notes.disableEventTooltip": "编辑模板时间线时无法在此处添加备注", "xpack.securitySolution.timeline.body.openSessionViewLabel": "打开会话视图", @@ -30894,7 +30859,6 @@ "xpack.synthetics.keyValuePairsField.value.ariaLabel": "值", "xpack.synthetics.keyValuePairsField.value.label": "值", "xpack.synthetics.kueryBar.searchPlaceholder.kql": "使用 kql 语法搜索监测 ID、名称和类型等(例如 monitor.type: \"http\" AND tags: \"dev\")", - "xpack.synthetics.kueryBar.searchPlaceholder.simple": "按监测 ID、名称或 url(例如 http://)搜索", "xpack.synthetics.locationName.helpLinkAnnotation": "添加位置", "xpack.synthetics.management.confirmDescriptionLabel": "此操作将删除监测,但会保留收集的任何数据。此操作无法撤消。", "xpack.synthetics.management.deleteLabel": "删除", @@ -31119,7 +31083,6 @@ "xpack.synthetics.monitorManagement.monitorAdvancedOptions.monitorNamespaceFieldLabel": "命名空间", "xpack.synthetics.monitorManagement.monitorAdvancedOptions.namespaceHelpLearnMoreLabel": "了解详情", "xpack.synthetics.monitorManagement.monitorDeleteFailureMessage": "无法删除监测。请稍后重试。", - "xpack.synthetics.monitorManagement.monitorDeleteLoadingMessage": "正在删除监测......", "xpack.synthetics.monitorManagement.monitorEditedSuccessMessage": "已成功更新监测。", "xpack.synthetics.monitorManagement.monitorFailureMessage": "无法保存监测。请稍后重试。", "xpack.synthetics.monitorManagement.monitorList.actions": "操作", @@ -31424,49 +31387,12 @@ "xpack.threatIntelligence.field.threat.indicator.last_seen": "最后看到时间", "xpack.threatIntelligence.indicator.table.viewDetailsButton": "查看详情", "xpack.timelines.clipboard.copy.successToastTitle": "已将字段 {field} 复制到剪贴板", - "xpack.timelines.footer.autoRefreshActiveTooltip": "自动刷新已启用时,时间线将显示匹配查询的最近 {numberOfItems} 个事件。", - "xpack.timelines.footer.rowsPerPageLabel": "每页行数:{rowsPerPage}", "xpack.timelines.hoverActions.columnToggleLabel": "在表中切换 {field} 列", "xpack.timelines.hoverActions.nestedColumnToggleLabel": "{field} 字段是对象,并分解为可以添加为列的嵌套字段", - "xpack.timelines.tGrid.unit": "{totalCount, plural, other {告警}}", - "xpack.timelines.timeline.acknowledgedAlertSuccessToastMessage": "已成功将 {totalAlerts} 个{totalAlerts, plural, other {告警}}标记为已确认。", - "xpack.timelines.timeline.alertsUnit": "{totalCount, plural, other {告警}}", - "xpack.timelines.timeline.body.actions.addNotesForRowAriaLabel": "将事件第 {ariaRowindex} 行的备注添加到时间线,其中列为 {columnValues}", - "xpack.timelines.timeline.body.actions.attachAlertToCaseForRowAriaLabel": "将第 {ariaRowindex} 行的告警或事件附加到案例,其中列为 {columnValues}", - "xpack.timelines.timeline.body.actions.investigateInResolverForRowAriaLabel": "分析第 {ariaRowindex} 行的告警或事件,其中列为 {columnValues}", - "xpack.timelines.timeline.body.actions.moreActionsForRowAriaLabel": "为第 {ariaRowindex} 行中的告警或事件选择更多操作,其中列为 {columnValues}", - "xpack.timelines.timeline.body.actions.sendAlertToTimelineForRowAriaLabel": "将第 {ariaRowindex} 行的告警发送到时间线,其中列为 {columnValues}", - "xpack.timelines.timeline.body.actions.viewDetailsForRowAriaLabel": "查看第 {ariaRowindex} 行的告警或事件的详细信息,其中列为 {columnValues}", - "xpack.timelines.timeline.closedAlertSuccessToastMessage": "已成功关闭 {totalAlerts} 个{totalAlerts, plural, other {告警}}。", - "xpack.timelines.timeline.eventsTableAriaLabel": "事件;第 {activePage} 页,共 {totalPages} 页", - "xpack.timelines.timeline.openedAlertSuccessToastMessage": "已成功打开 {totalAlerts} 个{totalAlerts, plural, other {告警}}。", - "xpack.timelines.timeline.properties.timelineToggleButtonAriaLabel": "{isOpen, select, false {打开} true {关闭} other {切换}}时间线 {title}", - "xpack.timelines.timeline.updateAlertStatusFailed": "无法更新{ conflicts } 个{conflicts, plural, other {告警}}。", - "xpack.timelines.timeline.updateAlertStatusFailedDetailed": "{ updated } 个{updated, plural, other {告警}}已成功更新,但是 { conflicts } 个无法更新,\n 因为{ conflicts, plural, other {其}}已被修改。", - "xpack.timelines.timeline.youAreInAnEventRendererScreenReaderOnly": "您正处于第 {row} 行的事件呈现器中。按向上箭头键退出并返回当前行,或按向下箭头键退出并前进到下一行。", - "xpack.timelines.toolbar.bulkActions.selectAllAlertsTitle": "选择全部 {totalAlertsFormatted} 个{totalAlerts, plural, other {告警}}", - "xpack.timelines.toolbar.bulkActions.selectedAlertsTitle": "已选择 {selectedAlertsFormatted} 个{selectedAlerts, plural, other {告警}}", - "xpack.timelines.alerts.EventRenderedView.eventSummary.column": "事件摘要", - "xpack.timelines.alerts.EventRenderedView.rule.column": "规则", - "xpack.timelines.alerts.EventRenderedView.timestamp.column": "时间戳", - "xpack.timelines.alerts.summaryView.eventRendererView.label": "事件渲染视图", - "xpack.timelines.alerts.summaryView.gridView.label": "网格视图", - "xpack.timelines.alerts.summaryView.options.default.description": "以表格数据方式查看,这样可以按特定字段分组和排序", - "xpack.timelines.alerts.summaryView.options.summaryView.description": "查看每个告警的事件渲染", - "xpack.timelines.beatFields.errorSearchDescription": "获取 Beat 字段时发生错误", - "xpack.timelines.beatFields.failSearchDescription": "无法对 Beat 字段执行搜索", "xpack.timelines.clipboard.copied": "已复制", "xpack.timelines.clipboard.copy": "复制", "xpack.timelines.clipboard.copy.to.the.clipboard": "复制到剪贴板", "xpack.timelines.clipboard.to.the.clipboard": "至剪贴板", - "xpack.timelines.copyToClipboardTooltip": "复制到剪贴板", - "xpack.timelines.footer.autoRefreshActiveDescription": "自动刷新已启用", - "xpack.timelines.footer.events": "事件", - "xpack.timelines.footer.loadingLabel": "正在加载", - "xpack.timelines.footer.loadingTimelineData": "正在加载时间线数据", - "xpack.timelines.footer.of": "/", - "xpack.timelines.footer.rows": "行", - "xpack.timelines.footer.totalCountOfEvents": "事件", "xpack.timelines.hoverActions.addToTimeline": "添加到时间线调查", "xpack.timelines.hoverActions.addToTimeline.addedFieldMessage": "已将 {fieldOrValue} 添加到{isTimeline, select, true {时间线} false {模板}}", "xpack.timelines.hoverActions.fieldLabel": "字段", @@ -31474,58 +31400,6 @@ "xpack.timelines.hoverActions.filterOut": "筛除", "xpack.timelines.hoverActions.moreActions": "更多操作", "xpack.timelines.hoverActions.tooltipWithKeyboardShortcut.pressTooltipLabel": "按", - "xpack.timelines.inspect.modal.closeTitle": "关闭", - "xpack.timelines.inspect.modal.indexPatternDescription": "连接到 Elasticsearch 索引的索引模式。可以在“Kibana”>“高级设置”中配置这些索引。", - "xpack.timelines.inspect.modal.indexPatternLabel": "索引模式", - "xpack.timelines.inspect.modal.noAlertIndexFound": "未找到告警索引", - "xpack.timelines.inspect.modal.queryTimeDescription": "处理查询所花费的时间。不包括发送请求或在浏览器中解析它的时间。", - "xpack.timelines.inspect.modal.queryTimeLabel": "查询时间", - "xpack.timelines.inspect.modal.reqTimestampDescription": "记录请求启动的时间", - "xpack.timelines.inspect.modal.reqTimestampLabel": "请求时间戳", - "xpack.timelines.inspect.modal.somethingWentWrongDescription": "抱歉,出现问题。", - "xpack.timelines.inspectDescription": "检查", - "xpack.timelines.lastUpdated.updated": "已更新", - "xpack.timelines.lastUpdated.updating": "正在更新......", - "xpack.timelines.tgrid.body.ariaLabel": "告警", - "xpack.timelines.tgrid.empty.description": "尝试搜索更长的时间段或修改您的搜索", - "xpack.timelines.tgrid.empty.title": "没有任何结果匹配您的搜索条件", - "xpack.timelines.tGrid.eventsLabel": "事件", - "xpack.timelines.tGrid.footer.loadingEventsDataLabel": "正在加载事件", - "xpack.timelines.tGrid.footer.totalCountOfEvents": "事件", - "xpack.timelines.timeline.acknowledgedAlertFailedToastMessage": "无法将告警标记为已确认", - "xpack.timelines.timeline.acknowledgedSelectedTitle": "标记为已确认", - "xpack.timelines.timeline.attachExistingCase": "附加到现有案例", - "xpack.timelines.timeline.attachNewCase": "附加到新案例", - "xpack.timelines.timeline.body.actions.checkboxForRowAriaLabel": "告警或事件第 {ariaRowindex} 行的{checked, select, false {已取消选中} true {已选中}}复选框,其中列为 {columnValues}", - "xpack.timelines.timeline.body.actions.collapseAriaLabel": "折叠", - "xpack.timelines.timeline.body.actions.expandEventTooltip": "查看详情", - "xpack.timelines.timeline.body.actions.investigateInResolverDisabledTooltip": "无法分析此事件,因为其包含不兼容的字段映射", - "xpack.timelines.timeline.body.actions.investigateInResolverTooltip": "分析事件", - "xpack.timelines.timeline.body.actions.investigateLabel": "调查", - "xpack.timelines.timeline.body.actions.viewDetailsAriaLabel": "查看详情", - "xpack.timelines.timeline.body.actions.viewSummaryLabel": "查看摘要", - "xpack.timelines.timeline.body.copyToClipboardButtonLabel": "复制到剪贴板", - "xpack.timelines.timeline.body.notes.disableEventTooltip": "编辑模板时间线时无法在此处添加备注", - "xpack.timelines.timeline.body.pinning.disablePinnnedTooltip": "编辑模板时间线时无法置顶此事件", - "xpack.timelines.timeline.body.pinning.pinnnedWithNotesTooltip": "此事件无法取消置顶,因为其有备注", - "xpack.timelines.timeline.body.sort.sortedAscendingTooltip": "已升序", - "xpack.timelines.timeline.body.sort.sortedDescendingTooltip": "已降序", - "xpack.timelines.timeline.categoryTooltip": "类别", - "xpack.timelines.timeline.closedAlertFailedToastMessage": "无法关闭告警。", - "xpack.timelines.timeline.closeSelectedTitle": "标记为已关闭", - "xpack.timelines.timeline.descriptionTooltip": "描述", - "xpack.timelines.timeline.fieldTooltip": "字段", - "xpack.timelines.timeline.flyout.pane.removeColumnButtonLabel": "移除列", - "xpack.timelines.timeline.fullScreenButton": "全屏", - "xpack.timelines.timeline.openedAlertFailedToastMessage": "无法打开告警", - "xpack.timelines.timeline.openSelectedTitle": "标记为打开", - "xpack.timelines.timeline.sortAZLabel": "A-Z 排序", - "xpack.timelines.timeline.sortFieldsButton": "排序字段", - "xpack.timelines.timeline.sortZALabel": "Z-A 排序", - "xpack.timelines.timeline.typeTooltip": "类型", - "xpack.timelines.timeline.updateAlertStatusFailedSingleAlert": "无法更新告警,因为它已被修改。", - "xpack.timelines.timelineEvents.errorSearchDescription": "搜索时间线事件时发生错误", - "xpack.timelines.toolbar.bulkActions.clearSelectionTitle": "清除所选内容", "xpack.transform.actionDeleteTransform.deleteDestDataViewTitle": "删除数据视图 {destinationIndex}", "xpack.transform.actionDeleteTransform.deleteDestinationIndexTitle": "删除目标索引 {destinationIndex}", "xpack.transform.alertTypes.transformHealth.errorMessagesMessage": "{count, plural, other {转换}} {transformsString} {count, plural, other {包含}}错误消息。", @@ -32406,8 +32280,6 @@ "xpack.triggersActionsUI.sections.rulesList.bulkActionPopover.disableAllTitle": "禁用", "xpack.triggersActionsUI.sections.rulesList.bulkActionPopover.enableAllTitle": "启用", "xpack.triggersActionsUI.sections.rulesList.bulkActionPopover.failedToDeleteRulesMessage": "无法删除规则", - "xpack.triggersActionsUI.sections.rulesList.bulkActionPopover.failedToDisableRulesMessage": "无法禁用规则", - "xpack.triggersActionsUI.sections.rulesList.bulkActionPopover.failedToEnableRulesMessage": "无法启用规则", "xpack.triggersActionsUI.sections.rulesList.cancelSnooze": "取消暂停", "xpack.triggersActionsUI.sections.rulesList.cancelSnoozeConfirmCallout": "只会取消当前发生的计划。", "xpack.triggersActionsUI.sections.rulesList.cancelSnoozeConfirmText": "根据规则操作中的定义生成告警时恢复通知。", diff --git a/x-pack/plugins/triggers_actions_ui/public/application/hooks/use_bulk_delete_response.tsx b/x-pack/plugins/triggers_actions_ui/public/application/hooks/use_bulk_delete_response.tsx deleted file mode 100644 index df2d2b8e3067b..0000000000000 --- a/x-pack/plugins/triggers_actions_ui/public/application/hooks/use_bulk_delete_response.tsx +++ /dev/null @@ -1,117 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ -import React, { useCallback, useMemo } from 'react'; -import { FormattedMessage } from '@kbn/i18n-react'; -import { EuiFlexGroup, EuiFlexItem, EuiButton } from '@elastic/eui'; -import { toMountPoint } from '@kbn/kibana-react-plugin/public'; -import { useKibana } from '../../common/lib/kibana'; -import { BulkDeleteResponse } from '../../types'; -import { - getSuccessfulDeletionNotificationText, - getFailedDeletionNotificationText, - getPartialSuccessDeletionNotificationText, - SINGLE_RULE_TITLE, - MULTIPLE_RULE_TITLE, -} from '../sections/rules_list/translations'; - -export const useBulkDeleteResponse = ({ - onSearchPopulate, -}: { - onSearchPopulate?: (filter: string) => void; -}) => { - const { - notifications: { toasts }, - } = useKibana().services; - - const onSearchPopulateInternal = useCallback( - (response: BulkDeleteResponse) => { - if (!onSearchPopulate) { - return; - } - const filter = response.errors.map((error) => error.rule.name).join(','); - onSearchPopulate(filter); - }, - [onSearchPopulate] - ); - - const renderToastErrorBody = useCallback( - (response: BulkDeleteResponse, messageType: 'warning' | 'danger') => { - return ( - - {onSearchPopulate && ( - - onSearchPopulateInternal(response)} - data-test-subj="bulkDeleteResponseFilterErrors" - > - - - - )} - - ); - }, - [onSearchPopulate, onSearchPopulateInternal] - ); - - const showToast = useCallback( - (response: BulkDeleteResponse) => { - const { errors, total } = response; - - const numberOfSuccess = total - errors.length; - const numberOfErrors = errors.length; - - // All success - if (!numberOfErrors) { - toasts.addSuccess( - getSuccessfulDeletionNotificationText( - numberOfSuccess, - SINGLE_RULE_TITLE, - MULTIPLE_RULE_TITLE - ) - ); - return; - } - - // All failure - if (numberOfErrors === total) { - toasts.addDanger({ - title: getFailedDeletionNotificationText( - numberOfErrors, - SINGLE_RULE_TITLE, - MULTIPLE_RULE_TITLE - ), - text: toMountPoint(renderToastErrorBody(response, 'danger')), - }); - return; - } - - // Some failure - toasts.addWarning({ - title: getPartialSuccessDeletionNotificationText( - numberOfSuccess, - numberOfErrors, - SINGLE_RULE_TITLE, - MULTIPLE_RULE_TITLE - ), - text: toMountPoint(renderToastErrorBody(response, 'warning')), - }); - }, - [toasts, renderToastErrorBody] - ); - - return useMemo(() => { - return { - showToast, - }; - }, [showToast]); -}; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/hooks/use_bulk_edit_select.tsx b/x-pack/plugins/triggers_actions_ui/public/application/hooks/use_bulk_edit_select.tsx index 468e9a0cda4c7..e2410801dd5b4 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/hooks/use_bulk_edit_select.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/hooks/use_bulk_edit_select.tsx @@ -235,7 +235,7 @@ export function useBulkEditSelect(props: UseBulkEditSelectProps) { return useMemo(() => { return { - selectedIds: state.selectedIds, + selectedIds: [...state.selectedIds], isAllSelected: state.isAllSelected, isPageSelected, numberOfSelectedItems, diff --git a/x-pack/plugins/triggers_actions_ui/public/application/hooks/use_bulk_operation_toast.tsx b/x-pack/plugins/triggers_actions_ui/public/application/hooks/use_bulk_operation_toast.tsx new file mode 100644 index 0000000000000..b71b0abaec7a3 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/hooks/use_bulk_operation_toast.tsx @@ -0,0 +1,147 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React, { useCallback, useMemo } from 'react'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { EuiFlexGroup, EuiFlexItem, EuiButton } from '@elastic/eui'; +import { toMountPoint } from '@kbn/kibana-react-plugin/public'; +import type { BulkOperationError } from '@kbn/alerting-plugin/server'; +import { useKibana } from '../../common/lib/kibana'; +import { + getSuccessfulDeletionNotificationText, + getFailedDeletionNotificationText, + getPartialSuccessDeletionNotificationText, + getPartialSuccessEnablingNotificationText, + getPartialSuccessDisablingNotificationText, + getFailedEnablingNotificationText, + getFailedDisablingNotificationText, + getSuccessfulEnablingNotificationText, + getSuccessfulDisablingNotificationText, + SINGLE_RULE_TITLE, + MULTIPLE_RULE_TITLE, +} from '../sections/rules_list/translations'; + +const actionToToastMapping = { + DELETE: { + getSuccessfulNotificationText: getSuccessfulDeletionNotificationText, + getFailedNotificationText: getFailedDeletionNotificationText, + getPartialSuccessNotificationText: getPartialSuccessDeletionNotificationText, + }, + ENABLE: { + getSuccessfulNotificationText: getSuccessfulEnablingNotificationText, + getFailedNotificationText: getFailedEnablingNotificationText, + getPartialSuccessNotificationText: getPartialSuccessEnablingNotificationText, + }, + DISABLE: { + getSuccessfulNotificationText: getSuccessfulDisablingNotificationText, + getFailedNotificationText: getFailedDisablingNotificationText, + getPartialSuccessNotificationText: getPartialSuccessDisablingNotificationText, + }, +}; + +export const useBulkOperationToast = ({ + onSearchPopulate, +}: { + onSearchPopulate?: (filter: string) => void; +}) => { + const { + notifications: { toasts }, + } = useKibana().services; + + const onSearchPopulateInternal = useCallback( + (errors: BulkOperationError[]) => { + if (!onSearchPopulate) { + return; + } + const filter = errors.map((error) => error.rule.name).join(','); + onSearchPopulate(filter); + }, + [onSearchPopulate] + ); + + const renderToastErrorBody = useCallback( + (errors: BulkOperationError[], messageType: 'warning' | 'danger') => { + return ( + + {onSearchPopulate && ( + + onSearchPopulateInternal(errors)} + data-test-subj="bulkDeleteResponseFilterErrors" + > + + + + )} + + ); + }, + [onSearchPopulate, onSearchPopulateInternal] + ); + + const showToast = useCallback( + ({ + action, + errors, + total, + }: { + action: 'DELETE' | 'ENABLE' | 'DISABLE'; + errors: BulkOperationError[]; + total: number; + }) => { + const numberOfSuccess = total - errors.length; + const numberOfErrors = errors.length; + + // All success + if (!numberOfErrors) { + toasts.addSuccess( + actionToToastMapping[action].getSuccessfulNotificationText( + numberOfSuccess, + SINGLE_RULE_TITLE, + MULTIPLE_RULE_TITLE + ) + ); + return; + } + + // All failure + if (numberOfErrors === total) { + toasts.addDanger({ + title: actionToToastMapping[action].getFailedNotificationText( + numberOfErrors, + SINGLE_RULE_TITLE, + MULTIPLE_RULE_TITLE + ), + text: toMountPoint(renderToastErrorBody(errors, 'danger')), + }); + return; + } + + // Some failure + toasts.addWarning({ + title: actionToToastMapping[action].getPartialSuccessNotificationText( + numberOfSuccess, + numberOfErrors, + SINGLE_RULE_TITLE, + MULTIPLE_RULE_TITLE + ), + text: toMountPoint(renderToastErrorBody(errors, 'warning')), + }); + }, + [toasts, renderToastErrorBody] + ); + + return useMemo(() => { + return { + showToast, + }; + }, [showToast]); +}; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/bulk_disable.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/bulk_disable.ts new file mode 100644 index 0000000000000..800a7267163da --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/bulk_disable.ts @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { HttpSetup } from '@kbn/core/public'; +import { KueryNode } from '@kbn/es-query'; +import { INTERNAL_BASE_ALERTING_API_PATH } from '../../constants'; +import { BulkDisableResponse } from '../../../types'; + +export const bulkDisableRules = async ({ + filter, + ids, + http, +}: { + filter?: KueryNode | null; + ids?: string[]; + http: HttpSetup; +}): Promise => { + try { + const body = JSON.stringify({ + ids: ids?.length ? ids : undefined, + ...(filter ? { filter: JSON.stringify(filter) } : {}), + }); + + return http.patch(`${INTERNAL_BASE_ALERTING_API_PATH}/rules/_bulk_disable`, { body }); + } catch (e) { + throw new Error(`Unable to parse bulk disable params: ${e}`); + } +}; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/bulk_enable.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/bulk_enable.ts new file mode 100644 index 0000000000000..d7644d9b598e7 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/bulk_enable.ts @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { HttpSetup } from '@kbn/core/public'; +import { KueryNode } from '@kbn/es-query'; +import { INTERNAL_BASE_ALERTING_API_PATH } from '../../constants'; +import { BulkEnableResponse } from '../../../types'; + +export const bulkEnableRules = async ({ + filter, + ids, + http, +}: { + filter?: KueryNode | null; + ids?: string[]; + http: HttpSetup; +}): Promise => { + try { + const body = JSON.stringify({ + ids: ids?.length ? ids : undefined, + ...(filter ? { filter: JSON.stringify(filter) } : {}), + }); + + return http.patch(`${INTERNAL_BASE_ALERTING_API_PATH}/rules/_bulk_enable`, { body }); + } catch (e) { + throw new Error(`Unable to parse bulk enable params: ${e}`); + } +}; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/index.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/index.ts index 09b053608542a..39f38b5cb0eeb 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/index.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/index.ts @@ -47,3 +47,5 @@ export type { BulkUpdateAPIKeyProps } from './update_api_key'; export { updateAPIKey, bulkUpdateAPIKey } from './update_api_key'; export { runSoon } from './run_soon'; export { bulkDeleteRules } from './bulk_delete'; +export { bulkEnableRules } from './bulk_enable'; +export { bulkDisableRules } from './bulk_disable'; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_search_bar/types.ts b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_search_bar/types.ts index 86e3e60ef059e..e75dd56168687 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_search_bar/types.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_search_bar/types.ts @@ -15,8 +15,8 @@ export interface AlertsSearchBarProps { rangeFrom?: string; rangeTo?: string; query?: string; - onQueryChange: ({}: { + onQueryChange: (query: { dateRange: { from: string; to: string; mode?: 'absolute' | 'relative' }; - query?: string; + query: string; }) => void; } diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/common/components/rule_quick_edit_buttons.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/common/components/rule_quick_edit_buttons.test.tsx index 510f74dfa66cf..a1a41d0a87186 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/common/components/rule_quick_edit_buttons.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/common/components/rule_quick_edit_buttons.test.tsx @@ -47,6 +47,8 @@ describe('rule_quick_edit_buttons', () => { selectedItems={[mockRule]} onPerformingAction={() => {}} onActionPerformed={() => {}} + onEnable={async () => {}} + onDisable={async () => {}} setRulesToDelete={() => {}} setRulesToDeleteFilter={() => {}} setRulesToUpdateAPIKey={() => {}} @@ -62,8 +64,8 @@ describe('rule_quick_edit_buttons', () => { /> ); - expect(wrapper.find('[data-test-subj="enableAll"]').exists()).toBeFalsy(); - expect(wrapper.find('[data-test-subj="disableAll"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="bulkEnable"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="bulkDisable"]').exists()).toBeTruthy(); expect(wrapper.find('[data-test-subj="updateAPIKeys"]').exists()).toBeTruthy(); expect(wrapper.find('[data-test-subj="bulkDelete"]').exists()).toBeTruthy(); expect(wrapper.find('[data-test-subj="bulkSnooze"]').exists()).toBeTruthy(); @@ -85,6 +87,8 @@ describe('rule_quick_edit_buttons', () => { selectedItems={[mockRule]} onPerformingAction={() => {}} onActionPerformed={() => {}} + onEnable={async () => {}} + onDisable={async () => {}} setRulesToDelete={() => {}} setRulesToDeleteFilter={() => {}} setRulesToUpdateAPIKey={() => {}} @@ -100,8 +104,8 @@ describe('rule_quick_edit_buttons', () => { /> ); - expect(wrapper.find('[data-test-subj="enableAll"]').exists()).toBeTruthy(); - expect(wrapper.find('[data-test-subj="disableAll"]').exists()).toBeFalsy(); + expect(wrapper.find('[data-test-subj="bulkEnable"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="bulkDisable"]').exists()).toBeTruthy(); }); it('disables the disable/enable/delete bulk actions if in select all mode', async () => { @@ -117,6 +121,8 @@ describe('rule_quick_edit_buttons', () => { selectedItems={[mockRule]} onPerformingAction={() => {}} onActionPerformed={() => {}} + onEnable={async () => {}} + onDisable={async () => {}} setRulesToDelete={() => {}} setRulesToDeleteFilter={() => {}} setRulesToUpdateAPIKey={() => {}} @@ -132,7 +138,7 @@ describe('rule_quick_edit_buttons', () => { /> ); - expect(wrapper.find('[data-test-subj="disableAll"]').first().prop('isDisabled')).toBeTruthy(); + expect(wrapper.find('[data-test-subj="bulkEnable"]').first().prop('isDisabled')).toBeFalsy(); expect(wrapper.find('[data-test-subj="bulkDelete"]').first().prop('isDisabled')).toBeFalsy(); expect(wrapper.find('[data-test-subj="updateAPIKeys"]').first().prop('isDisabled')).toBeFalsy(); expect(wrapper.find('[data-test-subj="bulkSnooze"]').first().prop('isDisabled')).toBeFalsy(); @@ -159,6 +165,8 @@ describe('rule_quick_edit_buttons', () => { selectedItems={[mockRule]} onPerformingAction={() => {}} onActionPerformed={() => {}} + onEnable={async () => {}} + onDisable={async () => {}} setRulesToDelete={() => {}} setRulesToDeleteFilter={() => {}} setRulesToSnooze={setRulesToSnooze} @@ -210,6 +218,8 @@ describe('rule_quick_edit_buttons', () => { selectedItems={[mockRule]} onPerformingAction={() => {}} onActionPerformed={() => {}} + onEnable={async () => {}} + onDisable={async () => {}} setRulesToDelete={() => {}} setRulesToDeleteFilter={() => {}} setRulesToSnooze={setRulesToSnooze} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/common/components/rule_quick_edit_buttons.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/common/components/rule_quick_edit_buttons.tsx index 2410e839d630f..f454ed78116b9 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/common/components/rule_quick_edit_buttons.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/common/components/rule_quick_edit_buttons.tsx @@ -7,9 +7,9 @@ import { i18n } from '@kbn/i18n'; import { KueryNode } from '@kbn/es-query'; -import React, { useState, useMemo } from 'react'; +import React, { useMemo } from 'react'; import { FormattedMessage } from '@kbn/i18n-react'; -import { EuiButtonEmpty, EuiFlexItem, EuiFlexGroup, EuiIconTip } from '@elastic/eui'; +import { EuiButtonEmpty, EuiFlexItem, EuiFlexGroup } from '@elastic/eui'; import { RuleTableItem } from '../../../../types'; import { @@ -26,6 +26,8 @@ export type ComponentOpts = { onPerformingAction?: () => void; onActionPerformed?: () => void; isDeletingRules?: boolean; + isEnablingRules?: boolean; + isDisablingRules?: boolean; isSnoozingRules?: boolean; isUnsnoozingRules?: boolean; isSchedulingRules?: boolean; @@ -43,30 +45,10 @@ export type ComponentOpts = { setRulesToScheduleFilter: React.Dispatch>; setRulesToUnscheduleFilter: React.Dispatch>; setRulesToUpdateAPIKeyFilter: React.Dispatch>; + onDisable: () => Promise; + onEnable: () => Promise; } & BulkOperationsComponentOpts; -const ButtonWithTooltip = ({ - showTooltip, - tooltip, - children, -}: { - showTooltip: boolean; - tooltip: string; - children: JSX.Element; -}) => { - if (!showTooltip) { - return children; - } - return ( - - {children} - - - - - ); -}; - export const RuleQuickEditButtons: React.FunctionComponent = ({ selectedItems, isAllSelected = false, @@ -74,13 +56,13 @@ export const RuleQuickEditButtons: React.FunctionComponent = ({ onPerformingAction = noop, onActionPerformed = noop, isDeletingRules = false, + isEnablingRules = false, + isDisablingRules = false, isSnoozingRules = false, isUnsnoozingRules = false, isSchedulingRules = false, isUnschedulingRules = false, isUpdatingRuleAPIKeys = false, - enableRules, - disableRules, setRulesToDelete, setRulesToDeleteFilter, setRulesToUpdateAPIKey, @@ -93,14 +75,13 @@ export const RuleQuickEditButtons: React.FunctionComponent = ({ setRulesToScheduleFilter, setRulesToUnscheduleFilter, setRulesToUpdateAPIKeyFilter, + onEnable, + onDisable, }: ComponentOpts) => { const { notifications: { toasts }, } = useKibana().services; - const [isEnablingRules, setIsEnablingRules] = useState(false); - const [isDisablingRules, setIsDisablingRules] = useState(false); - const isPerformingAction = isEnablingRules || isDisablingRules || @@ -111,13 +92,6 @@ export const RuleQuickEditButtons: React.FunctionComponent = ({ isUnschedulingRules || isUpdatingRuleAPIKeys; - const allRulesDisabled = useMemo(() => { - if (isAllSelected) { - return false; - } - return selectedItems.every(isRuleDisabled); - }, [selectedItems, isAllSelected]); - const hasDisabledByLicenseRuleTypes = useMemo(() => { if (isAllSelected) { return false; @@ -125,52 +99,6 @@ export const RuleQuickEditButtons: React.FunctionComponent = ({ return !!selectedItems.find((alertItem) => !alertItem.enabledInLicense); }, [selectedItems, isAllSelected]); - async function onEnableAllClick() { - if (isAllSelected) { - return; - } - onPerformingAction(); - setIsEnablingRules(true); - try { - await enableRules(selectedItems); - } catch (e) { - toasts.addDanger({ - title: i18n.translate( - 'xpack.triggersActionsUI.sections.rulesList.bulkActionPopover.failedToEnableRulesMessage', - { - defaultMessage: 'Failed to enable rules', - } - ), - }); - } finally { - setIsEnablingRules(false); - onActionPerformed(); - } - } - - async function onDisableAllClick() { - if (isAllSelected) { - return; - } - onPerformingAction(); - setIsDisablingRules(true); - try { - await disableRules(selectedItems); - } catch (e) { - toasts.addDanger({ - title: i18n.translate( - 'xpack.triggersActionsUI.sections.rulesList.bulkActionPopover.failedToDisableRulesMessage', - { - defaultMessage: 'Failed to disable rules', - } - ), - }); - } finally { - setIsDisablingRules(false); - onActionPerformed(); - } - } - async function deleteSelectedItems() { onPerformingAction(); try { @@ -362,48 +290,32 @@ export const RuleQuickEditButtons: React.FunctionComponent = ({ /> - - <> - {allRulesDisabled && ( - - - - - - )} - {!allRulesDisabled && ( - - - - - - )} - - + + + + + + + + + + = ({ export const RuleQuickEditButtonsWithApi = withBulkRuleOperations(RuleQuickEditButtons); -function isRuleDisabled(alert: RuleTableItem) { - return alert.enabled === false; -} - function noop() {} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_form.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_form.tsx index d37e41ff26555..16dc1642c584c 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_form.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_form.tsx @@ -386,7 +386,7 @@ export const RuleForm = ({
    - + {items .sort((a, b) => ruleTypeCompare(a, b)) .map((item, index) => { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list.tsx index 791bfa5e76574..6664aff57b0b3 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list.tsx @@ -67,6 +67,8 @@ import { snoozeRule, unsnoozeRule, bulkUpdateAPIKey, + bulkDisableRules, + bulkEnableRules, cloneRule, } from '../../../lib/rule_api'; import { loadActionTypes } from '../../../lib/action_connector_api'; @@ -101,7 +103,7 @@ import { SINGLE_RULE_TITLE, MULTIPLE_RULE_TITLE, } from '../translations'; -import { useBulkDeleteResponse } from '../../../hooks/use_bulk_delete_response'; +import { useBulkOperationToast } from '../../../hooks/use_bulk_operation_toast'; const ENTER_KEY = 13; @@ -228,6 +230,8 @@ export const RulesList = ({ const [rulesToDelete, setRulesToDelete] = useState([]); const [rulesToDeleteFilter, setRulesToDeleteFilter] = useState(); const [isDeletingRules, setIsDeletingRules] = useState(false); + const [isEnablingRules, setIsEnablingRules] = useState(false); + const [isDisablingRules, setIsDisablingRules] = useState(false); // TODO - tech debt: Right now we're using null and undefined to determine if we should // render the bulk edit modal. Refactor this to only keep track of 1 set of rules and types @@ -678,9 +682,9 @@ export const RulesList = ({ if (isAllSelected) { return true; } - const selectedIdsArray = [...selectedIds]; - return selectedIdsArray.length - ? filterRulesById(rulesState.data, selectedIdsArray).every((selectedRule) => + + return selectedIds.length + ? filterRulesById(rulesState.data, selectedIds).every((selectedRule) => hasAllPrivilege(selectedRule, ruleTypesState.data.get(selectedRule.ruleTypeId)) ) : false; @@ -723,6 +727,8 @@ export const RulesList = ({ isPerformingAction || isDeletingRules || isSnoozingRules || + isEnablingRules || + isDisablingRules || isUnsnoozingRules || isSchedulingRules || isUnschedulingRules || @@ -734,6 +740,8 @@ export const RulesList = ({ ruleTypesState, isPerformingAction, isDeletingRules, + isEnablingRules, + isDisablingRules, isSnoozingRules, isUnsnoozingRules, isSchedulingRules, @@ -944,7 +952,7 @@ export const RulesList = ({ > ); @@ -1020,7 +1032,38 @@ export const RulesList = ({ useEffect(() => { setIsDeleteModalVisibility(rulesToDelete.length > 0 || Boolean(rulesToDeleteFilter)); }, [rulesToDelete, rulesToDeleteFilter]); - const { showToast } = useBulkDeleteResponse({ onSearchPopulate }); + + const { showToast } = useBulkOperationToast({ onSearchPopulate }); + + const onEnable = useCallback(async () => { + setIsEnablingRules(true); + + const { errors, total } = await bulkEnableRules({ + ...(isAllSelected ? { filter: getFilter() } : {}), + ...(isAllSelected ? {} : { ids: selectedIds }), + http, + }); + + setIsEnablingRules(false); + showToast({ action: 'ENABLE', errors, total }); + await refreshRules(); + onClearSelection(); + }, [http, selectedIds, getFilter, setIsEnablingRules, showToast]); + + const onDisable = useCallback(async () => { + setIsDisablingRules(true); + + const { errors, total } = await bulkDisableRules({ + ...(isAllSelected ? { filter: getFilter() } : {}), + ...(isAllSelected ? {} : { ids: selectedIds }), + http, + }); + + setIsDisablingRules(false); + showToast({ action: 'DISABLE', errors, total }); + await refreshRules(); + onClearSelection(); + }, [http, selectedIds, getFilter, setIsDisablingRules, showToast]); const onDeleteCancel = () => { setIsDeleteModalVisibility(false); @@ -1037,7 +1080,7 @@ export const RulesList = ({ }); setIsDeletingRules(false); - showToast({ errors, total }); + showToast({ action: 'DELETE', errors, total }); await refreshRules(); clearRulesToDelete(); onClearSelection(); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list_bulk_disable.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list_bulk_disable.test.tsx new file mode 100644 index 0000000000000..19fe6c68f2646 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list_bulk_disable.test.tsx @@ -0,0 +1,257 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import * as React from 'react'; +import { ReactWrapper } from 'enzyme'; +import { act } from '@testing-library/react'; +import { mountWithIntl, nextTick } from '@kbn/test-jest-helpers'; +import { actionTypeRegistryMock } from '../../../action_type_registry.mock'; +import { ruleTypeRegistryMock } from '../../../rule_type_registry.mock'; +import { RulesList } from './rules_list'; +import { getIsExperimentalFeatureEnabled } from '../../../../common/get_experimental_features'; +import { useKibana } from '../../../../common/lib/kibana'; +import { + mockedRulesData, + ruleTypeFromApi, + getDisabledByLicenseRuleTypeFromApi, + ruleType, +} from './test_helpers'; +import { IToasts } from '@kbn/core/public'; + +jest.mock('../../../../common/lib/kibana'); +jest.mock('@kbn/kibana-react-plugin/public/ui_settings/use_ui_setting', () => ({ + useUiSetting: jest.fn(() => false), + useUiSetting$: jest.fn((value: string) => ['0,0']), +})); +jest.mock('../../../lib/action_connector_api', () => ({ + loadActionTypes: jest.fn(), + loadAllActions: jest.fn(), +})); + +jest.mock('../../../lib/rule_api', () => ({ + loadRulesWithKueryFilter: jest.fn(), + loadRuleTypes: jest.fn(), + loadRuleAggregationsWithKueryFilter: jest.fn(), + updateAPIKey: jest.fn(), + loadRuleTags: jest.fn(), + bulkDisableRules: jest.fn().mockResolvedValue({ errors: [], total: 10 }), + alertingFrameworkHealth: jest.fn(() => ({ + isSufficientlySecure: true, + hasPermanentEncryptionKey: true, + })), +})); + +jest.mock('../../../lib/rule_api/aggregate_kuery_filter'); +jest.mock('../../../lib/rule_api/rules_kuery_filter'); + +jest.mock('../../../../common/lib/health_api', () => ({ + triggersActionsUiHealth: jest.fn(() => ({ isRulesAvailable: true })), +})); +jest.mock('../../../../common/lib/config_api', () => ({ + triggersActionsUiConfig: jest + .fn() + .mockResolvedValue({ minimumScheduleInterval: { value: '1m', enforce: false } }), +})); +jest.mock('react-router-dom', () => ({ + useHistory: () => ({ + push: jest.fn(), + }), + useLocation: () => ({ + pathname: '/triggersActions/rules/', + }), +})); + +jest.mock('../../../lib/capabilities', () => ({ + hasAllPrivilege: jest.fn(() => true), + hasSaveRulesCapability: jest.fn(() => true), + hasShowActionsCapability: jest.fn(() => true), + hasExecuteActionsCapability: jest.fn(() => true), +})); +jest.mock('../../../../common/get_experimental_features', () => ({ + getIsExperimentalFeatureEnabled: jest.fn(), +})); + +const { loadRuleTypes, bulkDisableRules } = jest.requireMock('../../../lib/rule_api'); + +const { loadRulesWithKueryFilter } = jest.requireMock('../../../lib/rule_api/rules_kuery_filter'); +const { loadActionTypes, loadAllActions } = jest.requireMock('../../../lib/action_connector_api'); + +const actionTypeRegistry = actionTypeRegistryMock.create(); +const ruleTypeRegistry = ruleTypeRegistryMock.create(); + +ruleTypeRegistry.list.mockReturnValue([ruleType]); +actionTypeRegistry.list.mockReturnValue([]); + +const useKibanaMock = useKibana as jest.Mocked; + +beforeEach(() => { + (getIsExperimentalFeatureEnabled as jest.Mock).mockImplementation(() => false); +}); + +// Test are too slow. It's breaking the build. So we skipp it now and waiting for improvment according this ticket: +// https://github.com/elastic/kibana/issues/145122 +describe.skip('Rules list bulk disable', () => { + let wrapper: ReactWrapper; + + const setup = async (authorized: boolean = true) => { + loadRulesWithKueryFilter.mockResolvedValue({ + page: 1, + perPage: 10000, + total: 6, + data: mockedRulesData, + }); + + loadActionTypes.mockResolvedValue([ + { + id: 'test', + name: 'Test', + }, + { + id: 'test2', + name: 'Test2', + }, + ]); + loadRuleTypes.mockResolvedValue([ + ruleTypeFromApi, + getDisabledByLicenseRuleTypeFromApi(authorized), + ]); + loadAllActions.mockResolvedValue([]); + // eslint-disable-next-line react-hooks/rules-of-hooks + useKibanaMock().services.ruleTypeRegistry = ruleTypeRegistry; + + // eslint-disable-next-line react-hooks/rules-of-hooks + useKibanaMock().services.actionTypeRegistry = actionTypeRegistry; + wrapper = mountWithIntl(); + + await act(async () => { + await nextTick(); + wrapper.update(); + }); + }; + + afterEach(() => { + jest.clearAllMocks(); + }); + + beforeAll(async () => { + await setup(); + useKibanaMock().services.notifications.toasts = { + addSuccess: jest.fn(), + addError: jest.fn(), + addDanger: jest.fn(), + addWarning: jest.fn(), + } as unknown as IToasts; + }); + + beforeEach(() => { + wrapper.find('[data-test-subj="checkboxSelectRow-1"]').at(1).simulate('change'); + wrapper.find('[data-test-subj="selectAllRulesButton"]').at(1).simulate('click'); + // Unselect something to test filtering + wrapper.find('[data-test-subj="checkboxSelectRow-2"]').at(1).simulate('change'); + wrapper.find('[data-test-subj="showBulkActionButton"]').first().simulate('click'); + }); + + it.skip('can bulk disable', async () => { + wrapper.find('button[data-test-subj="bulkDisable"]').first().simulate('click'); + + await act(async () => { + await nextTick(); + wrapper.update(); + }); + + const filter = bulkDisableRules.mock.calls[0][0].filter; + + expect(filter.function).toEqual('and'); + expect(filter.arguments[0].function).toEqual('or'); + expect(filter.arguments[1].function).toEqual('not'); + expect(filter.arguments[1].arguments[0].arguments[0].value).toEqual('alert.id'); + expect(filter.arguments[1].arguments[0].arguments[1].value).toEqual('alert:2'); + + expect(bulkDisableRules).toHaveBeenCalledWith( + expect.not.objectContaining({ + ids: [], + }) + ); + expect( + wrapper.find('[data-test-subj="checkboxSelectRow-1"]').first().prop('checked') + ).toBeFalsy(); + expect(wrapper.find('button[data-test-subj="bulkDisable"]').exists()).toBeFalsy(); + }); + + describe('Toast', () => { + it('should have success toast message', async () => { + wrapper.find('button[data-test-subj="bulkDisable"]').first().simulate('click'); + + await act(async () => { + await nextTick(); + wrapper.update(); + }); + + expect(useKibanaMock().services.notifications.toasts.addSuccess).toHaveBeenCalledTimes(1); + expect(useKibanaMock().services.notifications.toasts.addSuccess).toHaveBeenCalledWith( + 'Disabled 10 rules' + ); + }); + + it('should have warning toast message', async () => { + bulkDisableRules.mockResolvedValue({ + errors: [ + { + message: 'string', + rule: { + id: 'string', + name: 'string', + }, + }, + ], + total: 10, + }); + + wrapper.find('button[data-test-subj="bulkDisable"]').first().simulate('click'); + + await act(async () => { + await nextTick(); + wrapper.update(); + }); + + expect(useKibanaMock().services.notifications.toasts.addWarning).toHaveBeenCalledTimes(1); + expect(useKibanaMock().services.notifications.toasts.addWarning).toHaveBeenCalledWith( + expect.objectContaining({ + title: 'Disabled 9 rules, 1 rule encountered errors', + }) + ); + }); + + it('should have danger toast message', async () => { + bulkDisableRules.mockResolvedValue({ + errors: [ + { + message: 'string', + rule: { + id: 'string', + name: 'string', + }, + }, + ], + total: 1, + }); + + wrapper.find('button[data-test-subj="bulkDisable"]').first().simulate('click'); + + await act(async () => { + await nextTick(); + wrapper.update(); + }); + + expect(useKibanaMock().services.notifications.toasts.addDanger).toHaveBeenCalledTimes(1); + expect(useKibanaMock().services.notifications.toasts.addDanger).toHaveBeenCalledWith( + expect.objectContaining({ + title: 'Failed to disable 1 rule', + }) + ); + }); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list_bulk_enable.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list_bulk_enable.test.tsx new file mode 100644 index 0000000000000..688e0ae50ec46 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list_bulk_enable.test.tsx @@ -0,0 +1,257 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import * as React from 'react'; +import { ReactWrapper } from 'enzyme'; +import { act } from '@testing-library/react'; +import { mountWithIntl, nextTick } from '@kbn/test-jest-helpers'; +import { actionTypeRegistryMock } from '../../../action_type_registry.mock'; +import { ruleTypeRegistryMock } from '../../../rule_type_registry.mock'; +import { RulesList } from './rules_list'; +import { getIsExperimentalFeatureEnabled } from '../../../../common/get_experimental_features'; +import { useKibana } from '../../../../common/lib/kibana'; +import { + mockedRulesData, + ruleTypeFromApi, + getDisabledByLicenseRuleTypeFromApi, + ruleType, +} from './test_helpers'; +import { IToasts } from '@kbn/core/public'; + +jest.mock('../../../../common/lib/kibana'); +jest.mock('@kbn/kibana-react-plugin/public/ui_settings/use_ui_setting', () => ({ + useUiSetting: jest.fn(() => false), + useUiSetting$: jest.fn((value: string) => ['0,0']), +})); +jest.mock('../../../lib/action_connector_api', () => ({ + loadActionTypes: jest.fn(), + loadAllActions: jest.fn(), +})); + +jest.mock('../../../lib/rule_api', () => ({ + loadRulesWithKueryFilter: jest.fn(), + loadRuleTypes: jest.fn(), + loadRuleAggregationsWithKueryFilter: jest.fn(), + updateAPIKey: jest.fn(), + loadRuleTags: jest.fn(), + bulkEnableRules: jest.fn().mockResolvedValue({ errors: [], total: 10 }), + alertingFrameworkHealth: jest.fn(() => ({ + isSufficientlySecure: true, + hasPermanentEncryptionKey: true, + })), +})); + +jest.mock('../../../lib/rule_api/aggregate_kuery_filter'); +jest.mock('../../../lib/rule_api/rules_kuery_filter'); + +jest.mock('../../../../common/lib/health_api', () => ({ + triggersActionsUiHealth: jest.fn(() => ({ isRulesAvailable: true })), +})); +jest.mock('../../../../common/lib/config_api', () => ({ + triggersActionsUiConfig: jest + .fn() + .mockResolvedValue({ minimumScheduleInterval: { value: '1m', enforce: false } }), +})); +jest.mock('react-router-dom', () => ({ + useHistory: () => ({ + push: jest.fn(), + }), + useLocation: () => ({ + pathname: '/triggersActions/rules/', + }), +})); + +jest.mock('../../../lib/capabilities', () => ({ + hasAllPrivilege: jest.fn(() => true), + hasSaveRulesCapability: jest.fn(() => true), + hasShowActionsCapability: jest.fn(() => true), + hasExecuteActionsCapability: jest.fn(() => true), +})); +jest.mock('../../../../common/get_experimental_features', () => ({ + getIsExperimentalFeatureEnabled: jest.fn(), +})); + +const { loadRuleTypes, bulkEnableRules } = jest.requireMock('../../../lib/rule_api'); + +const { loadRulesWithKueryFilter } = jest.requireMock('../../../lib/rule_api/rules_kuery_filter'); +const { loadActionTypes, loadAllActions } = jest.requireMock('../../../lib/action_connector_api'); + +const actionTypeRegistry = actionTypeRegistryMock.create(); +const ruleTypeRegistry = ruleTypeRegistryMock.create(); + +ruleTypeRegistry.list.mockReturnValue([ruleType]); +actionTypeRegistry.list.mockReturnValue([]); + +const useKibanaMock = useKibana as jest.Mocked; + +beforeEach(() => { + (getIsExperimentalFeatureEnabled as jest.Mock).mockImplementation(() => false); +}); + +// Test are too slow. It's breaking the build. So we skipp it now and waiting for improvment according this ticket: +// https://github.com/elastic/kibana/issues/145122 +describe.skip('Rules list bulk enable', () => { + let wrapper: ReactWrapper; + + const setup = async (authorized: boolean = true) => { + loadRulesWithKueryFilter.mockResolvedValue({ + page: 1, + perPage: 10000, + total: 6, + data: mockedRulesData.map((rule) => ({ ...rule, enabled: false })), + }); + + loadActionTypes.mockResolvedValue([ + { + id: 'test', + name: 'Test', + }, + { + id: 'test2', + name: 'Test2', + }, + ]); + loadRuleTypes.mockResolvedValue([ + ruleTypeFromApi, + getDisabledByLicenseRuleTypeFromApi(authorized), + ]); + loadAllActions.mockResolvedValue([]); + // eslint-disable-next-line react-hooks/rules-of-hooks + useKibanaMock().services.ruleTypeRegistry = ruleTypeRegistry; + + // eslint-disable-next-line react-hooks/rules-of-hooks + useKibanaMock().services.actionTypeRegistry = actionTypeRegistry; + wrapper = mountWithIntl(); + + await act(async () => { + await nextTick(); + wrapper.update(); + }); + }; + + afterEach(() => { + jest.clearAllMocks(); + }); + + beforeAll(async () => { + await setup(); + useKibanaMock().services.notifications.toasts = { + addSuccess: jest.fn(), + addError: jest.fn(), + addDanger: jest.fn(), + addWarning: jest.fn(), + } as unknown as IToasts; + }); + + beforeEach(() => { + wrapper.find('[data-test-subj="checkboxSelectRow-1"]').at(1).simulate('change'); + wrapper.find('[data-test-subj="selectAllRulesButton"]').at(1).simulate('click'); + // Unselect something to test filtering + wrapper.find('[data-test-subj="checkboxSelectRow-2"]').at(1).simulate('change'); + wrapper.find('[data-test-subj="showBulkActionButton"]').first().simulate('click'); + }); + + it('can bulk enable', async () => { + wrapper.find('button[data-test-subj="bulkEnable"]').first().simulate('click'); + + await act(async () => { + await nextTick(); + wrapper.update(); + }); + + const filter = bulkEnableRules.mock.calls[0][0].filter; + + expect(filter.function).toEqual('and'); + expect(filter.arguments[0].function).toEqual('or'); + expect(filter.arguments[1].function).toEqual('not'); + expect(filter.arguments[1].arguments[0].arguments[0].value).toEqual('alert.id'); + expect(filter.arguments[1].arguments[0].arguments[1].value).toEqual('alert:2'); + + expect(bulkEnableRules).toHaveBeenCalledWith( + expect.not.objectContaining({ + ids: [], + }) + ); + expect( + wrapper.find('[data-test-subj="checkboxSelectRow-1"]').first().prop('checked') + ).toBeFalsy(); + expect(wrapper.find('button[data-test-subj="bulkEnable"]').exists()).toBeFalsy(); + }); + + describe('Toast', () => { + it('should have success toast message', async () => { + wrapper.find('button[data-test-subj="bulkEnable"]').first().simulate('click'); + + await act(async () => { + await nextTick(); + wrapper.update(); + }); + + expect(useKibanaMock().services.notifications.toasts.addSuccess).toHaveBeenCalledTimes(1); + expect(useKibanaMock().services.notifications.toasts.addSuccess).toHaveBeenCalledWith( + 'Enabled 10 rules' + ); + }); + + it('should have warning toast message', async () => { + bulkEnableRules.mockResolvedValue({ + errors: [ + { + message: 'string', + rule: { + id: 'string', + name: 'string', + }, + }, + ], + total: 10, + }); + + wrapper.find('button[data-test-subj="bulkEnable"]').first().simulate('click'); + + await act(async () => { + await nextTick(); + wrapper.update(); + }); + + expect(useKibanaMock().services.notifications.toasts.addWarning).toHaveBeenCalledTimes(1); + expect(useKibanaMock().services.notifications.toasts.addWarning).toHaveBeenCalledWith( + expect.objectContaining({ + title: 'Enabled 9 rules, 1 rule encountered errors', + }) + ); + }); + + it('should have danger toast message', async () => { + bulkEnableRules.mockResolvedValue({ + errors: [ + { + message: 'string', + rule: { + id: 'string', + name: 'string', + }, + }, + ], + total: 1, + }); + + wrapper.find('button[data-test-subj="bulkEnable"]').first().simulate('click'); + + await act(async () => { + await nextTick(); + wrapper.update(); + }); + + expect(useKibanaMock().services.notifications.toasts.addDanger).toHaveBeenCalledTimes(1); + expect(useKibanaMock().services.notifications.toasts.addDanger).toHaveBeenCalledWith( + expect.objectContaining({ + title: 'Failed to enable 1 rule', + }) + ); + }); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/translations.ts b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/translations.ts index 07675befd8d4f..dd7a2b8cec3a2 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/translations.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/translations.ts @@ -379,6 +379,43 @@ export const getSuccessfulDeletionNotificationText = ( }, } ); + +export const getSuccessfulEnablingNotificationText = ( + numSuccesses: number, + singleTitle: string, + multipleTitle: string +) => + i18n.translate( + 'xpack.triggersActionsUI.components.enableSelectedIdsSuccessNotification.descriptionText', + { + defaultMessage: + 'Enabled {numSuccesses, number} {numSuccesses, plural, one {{singleTitle}} other {{multipleTitle}}}', + values: { + numSuccesses, + singleTitle, + multipleTitle, + }, + } + ); + +export const getSuccessfulDisablingNotificationText = ( + numSuccesses: number, + singleTitle: string, + multipleTitle: string +) => + i18n.translate( + 'xpack.triggersActionsUI.components.disableSelectedIdsSuccessNotification.descriptionText', + { + defaultMessage: + 'Disabled {numSuccesses, number} {numSuccesses, plural, one {{singleTitle}} other {{multipleTitle}}}', + values: { + numSuccesses, + singleTitle, + multipleTitle, + }, + } + ); + export const getFailedDeletionNotificationText = ( numErrors: number, singleTitle: string, @@ -397,6 +434,42 @@ export const getFailedDeletionNotificationText = ( } ); +export const getFailedEnablingNotificationText = ( + numErrors: number, + singleTitle: string, + multipleTitle: string +) => + i18n.translate( + 'xpack.triggersActionsUI.components.enableSelectedIdsErrorNotification.descriptionText', + { + defaultMessage: + 'Failed to enable {numErrors, number} {numErrors, plural, one {{singleTitle}} other {{multipleTitle}}}', + values: { + numErrors, + singleTitle, + multipleTitle, + }, + } + ); + +export const getFailedDisablingNotificationText = ( + numErrors: number, + singleTitle: string, + multipleTitle: string +) => + i18n.translate( + 'xpack.triggersActionsUI.components.disableSelectedIdsErrorNotification.descriptionText', + { + defaultMessage: + 'Failed to disable {numErrors, number} {numErrors, plural, one {{singleTitle}} other {{multipleTitle}}}', + values: { + numErrors, + singleTitle, + multipleTitle, + }, + } + ); + export const getPartialSuccessDeletionNotificationText = ( numberOfSuccess: number, numberOfErrors: number, @@ -416,3 +489,43 @@ export const getPartialSuccessDeletionNotificationText = ( }, } ); + +export const getPartialSuccessEnablingNotificationText = ( + numberOfSuccess: number, + numberOfErrors: number, + singleTitle: string, + multipleTitle: string +) => + i18n.translate( + 'xpack.triggersActionsUI.components.enableSelectedIdsPartialSuccessNotification.descriptionText', + { + defaultMessage: + 'Enabled {numberOfSuccess, number} {numberOfSuccess, plural, one {{singleTitle}} other {{multipleTitle}}}, {numberOfErrors, number} {numberOfErrors, plural, one {{singleTitle}} other {{multipleTitle}}} encountered errors', + values: { + numberOfSuccess, + numberOfErrors, + singleTitle, + multipleTitle, + }, + } + ); + +export const getPartialSuccessDisablingNotificationText = ( + numberOfSuccess: number, + numberOfErrors: number, + singleTitle: string, + multipleTitle: string +) => + i18n.translate( + 'xpack.triggersActionsUI.components.disableSelectedIdsPartialSuccessNotification.descriptionText', + { + defaultMessage: + 'Disabled {numberOfSuccess, number} {numberOfSuccess, plural, one {{singleTitle}} other {{multipleTitle}}}, {numberOfErrors, number} {numberOfErrors, plural, one {{singleTitle}} other {{multipleTitle}}} encountered errors', + values: { + numberOfSuccess, + numberOfErrors, + singleTitle, + multipleTitle, + }, + } + ); diff --git a/x-pack/plugins/triggers_actions_ui/public/types.ts b/x-pack/plugins/triggers_actions_ui/public/types.ts index 415672324e648..f0d2b8e2e2aea 100644 --- a/x-pack/plugins/triggers_actions_ui/public/types.ts +++ b/x-pack/plugins/triggers_actions_ui/public/types.ts @@ -174,6 +174,18 @@ export interface BulkDeleteResponse { total: number; } +export interface BulkEnableResponse { + rules: Rule[]; + errors: BulkOperationError[]; + total: number; +} + +export interface BulkDisableResponse { + rules: Rule[]; + errors: BulkOperationError[]; + total: number; +} + export interface ActionParamsProps { actionParams: Partial; index: number; @@ -352,6 +364,9 @@ export interface RuleTypeModel { requiresAppContext: boolean; defaultActionMessage?: string; defaultRecoveryMessage?: string; + alertDetailsAppSection?: + | React.FunctionComponent + | React.LazyExoticComponent>; } export interface IErrorObject { diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecation_logs/fix_deprecation_logs/external_links.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecation_logs/fix_deprecation_logs/external_links.tsx index 37bb16a9ba4d7..37bcf0e0c8a95 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecation_logs/fix_deprecation_logs/external_links.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecation_logs/fix_deprecation_logs/external_links.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { encode } from 'rison-node'; +import { encode } from '@kbn/rison'; import React, { FunctionComponent, useState, useEffect } from 'react'; import { buildPhrasesFilter, PhraseFilter } from '@kbn/es-query'; diff --git a/x-pack/plugins/ux/e2e/journeys/index.ts b/x-pack/plugins/ux/e2e/journeys/index.ts index 1377dda843dc4..cb6a5b4145932 100644 --- a/x-pack/plugins/ux/e2e/journeys/index.ts +++ b/x-pack/plugins/ux/e2e/journeys/index.ts @@ -6,7 +6,7 @@ */ export * from './core_web_vitals'; -export * from './page_views'; +// export * from './page_views'; export * from './url_ux_query.journey'; export * from './ux_js_errors.journey'; export * from './ux_client_metrics.journey'; diff --git a/x-pack/plugins/ux/public/components/app/rum_dashboard/visitor_breakdown_map/__mocks__/regions_layer.mock.ts b/x-pack/plugins/ux/public/components/app/rum_dashboard/visitor_breakdown_map/__mocks__/regions_layer.mock.ts index a7882e7d21d33..beef30007ffce 100644 --- a/x-pack/plugins/ux/public/components/app/rum_dashboard/visitor_breakdown_map/__mocks__/regions_layer.mock.ts +++ b/x-pack/plugins/ux/public/components/app/rum_dashboard/visitor_breakdown_map/__mocks__/regions_layer.mock.ts @@ -65,6 +65,7 @@ export const mockLayerList = [ iconOrientation: { type: 'STATIC', options: { orientation: 0 } }, labelText: { type: 'STATIC', options: { value: '' } }, labelColor: { type: 'STATIC', options: { color: '#000000' } }, + labelPosition: { options: { position: 'CENTER' } }, labelSize: { type: 'STATIC', options: { size: 14 } }, labelBorderColor: { type: 'STATIC', options: { color: '#FFFFFF' } }, labelZoomRange: { @@ -136,6 +137,7 @@ export const mockLayerList = [ iconOrientation: { type: 'STATIC', options: { orientation: 0 } }, labelText: { type: 'STATIC', options: { value: '' } }, labelColor: { type: 'STATIC', options: { color: '#000000' } }, + labelPosition: { options: { position: 'CENTER' } }, labelSize: { type: 'STATIC', options: { size: 14 } }, labelBorderColor: { type: 'STATIC', options: { color: '#FFFFFF' } }, labelZoomRange: { diff --git a/x-pack/plugins/ux/public/components/app/rum_dashboard/visitor_breakdown_map/use_layer_list.ts b/x-pack/plugins/ux/public/components/app/rum_dashboard/visitor_breakdown_map/use_layer_list.ts index eca4df56ff6b4..4ec159b8dcab4 100644 --- a/x-pack/plugins/ux/public/components/app/rum_dashboard/visitor_breakdown_map/use_layer_list.ts +++ b/x-pack/plugins/ux/public/components/app/rum_dashboard/visitor_breakdown_map/use_layer_list.ts @@ -15,6 +15,7 @@ import { COLOR_MAP_TYPE, FIELD_ORIGIN, LABEL_BORDER_SIZES, + LABEL_POSITIONS, LAYER_TYPE, SOURCE_TYPES, STYLE_TYPE, @@ -118,6 +119,11 @@ export function useLayerList() { options: { orientation: 0 }, }, labelText: { type: STYLE_TYPE.STATIC, options: { value: '' } }, + labelPosition: { + options: { + position: LABEL_POSITIONS.CENTER, + }, + }, labelZoomRange: { options: { useLayerZoomRange: true, diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/clone.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/clone.ts deleted file mode 100644 index 329782a79c3c3..0000000000000 --- a/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/clone.ts +++ /dev/null @@ -1,231 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import expect from '@kbn/expect'; -import { Spaces, UserAtSpaceScenarios } from '../../../scenarios'; -import { - checkAAD, - getTestRuleData, - getConsumerUnauthorizedErrorMessage, - getUrlPrefix, - ObjectRemover, - TaskManagerDoc, -} from '../../../../common/lib'; -import { FtrProviderContext } from '../../../../common/ftr_provider_context'; - -interface RuleSpace { - body: any; -} - -// eslint-disable-next-line import/no-default-export -export default function createAlertTests({ getService }: FtrProviderContext) { - const supertest = getService('supertest'); - const es = getService('es'); - const supertestWithoutAuth = getService('supertestWithoutAuth'); - - describe('clone', async () => { - const objectRemover = new ObjectRemover(supertest); - const space1 = Spaces[0].id; - const space2 = Spaces[1].id; - let ruleSpace1: RuleSpace = { body: {} }; - let ruleSpace2: RuleSpace = { body: {} }; - after(() => objectRemover.removeAll()); - before(async () => { - const { body: createdActionSpace1 } = await supertest - .post(`${getUrlPrefix(space1)}/api/actions/connector`) - .set('kbn-xsrf', 'foo') - .send({ - name: 'MY action', - connector_type_id: 'test.noop', - config: {}, - secrets: {}, - }) - .expect(200); - - const { body: createdActionSpace2 } = await supertest - .post(`${getUrlPrefix(space2)}/api/actions/connector`) - .set('kbn-xsrf', 'foo') - .send({ - name: 'MY action', - connector_type_id: 'test.noop', - config: {}, - secrets: {}, - }) - .expect(200); - - ruleSpace1 = await supertest - .post(`${getUrlPrefix(space1)}/api/alerting/rule`) - .set('kbn-xsrf', 'foo') - .send( - getTestRuleData({ - actions: [ - { - id: createdActionSpace1.id, - group: 'default', - params: {}, - }, - ], - }) - ); - objectRemover.add(space1, ruleSpace1.body.id, 'rule', 'alerting'); - - ruleSpace2 = await supertest - .post(`${getUrlPrefix(space2)}/api/alerting/rule`) - .set('kbn-xsrf', 'foo') - .send( - getTestRuleData({ - actions: [ - { - id: createdActionSpace2.id, - group: 'default', - params: {}, - }, - ], - }) - ); - objectRemover.add(space2, ruleSpace2.body.id, 'rule', 'alerting'); - }); - - async function getScheduledTask(id: string): Promise { - const scheduledTask = await es.get({ - id: `task:${id}`, - index: '.kibana_task_manager', - }); - return scheduledTask._source!; - } - - for (const scenario of UserAtSpaceScenarios) { - const { user, space } = scenario; - describe(scenario.id, () => { - it('should handle clone rule request appropriately', async () => { - const ruleIdToClone = - space.id === space1 - ? ruleSpace1.body.id - : space.id === space2 - ? ruleSpace2.body.id - : null; - const response = await supertestWithoutAuth - .post(`${getUrlPrefix(space.id)}/internal/alerting/rule/${ruleIdToClone}/_clone`) - .set('kbn-xsrf', 'foo') - .auth(user.username, user.password) - .send(); - - switch (scenario.id) { - case 'no_kibana_privileges at space1': - case 'global_read at space1': - case 'space_1_all at space2': - expect(response.statusCode).to.eql(403); - expect(response.body).to.eql({ - error: 'Forbidden', - message: getConsumerUnauthorizedErrorMessage( - 'create', - 'test.noop', - 'alertsFixture' - ), - statusCode: 403, - }); - break; - case 'space_1_all_alerts_none_actions at space1': - case 'superuser at space1': - case 'space_1_all at space1': - case 'space_1_all_with_restricted_fixture at space1': - expect(response.statusCode).to.eql(200); - objectRemover.add(space.id, response.body.id, 'rule', 'alerting'); - expect(response.body).to.eql({ - id: response.body.id, - name: 'abc [Clone]', - tags: ['foo'], - actions: [ - { - id: response.body.actions[0].id, - connector_type_id: response.body.actions[0].connector_type_id, - group: 'default', - params: {}, - }, - ], - enabled: true, - rule_type_id: 'test.noop', - consumer: 'alertsFixture', - params: {}, - created_by: user.username, - schedule: { interval: '1m' }, - scheduled_task_id: response.body.scheduled_task_id, - created_at: response.body.created_at, - updated_at: response.body.updated_at, - throttle: '1m', - notify_when: 'onThrottleInterval', - updated_by: user.username, - api_key_owner: user.username, - mute_all: false, - muted_alert_ids: [], - execution_status: response.body.execution_status, - last_run: { - alerts_count: { - active: 0, - ignored: 0, - new: 0, - recovered: 0, - }, - outcome: 'succeeded', - outcome_msg: null, - warning: null, - }, - next_run: response.body.next_run, - }); - expect(typeof response.body.scheduled_task_id).to.be('string'); - expect(Date.parse(response.body.created_at)).to.be.greaterThan(0); - expect(Date.parse(response.body.updated_at)).to.be.greaterThan(0); - - const taskRecord = await getScheduledTask(response.body.scheduled_task_id); - expect(taskRecord.type).to.eql('task'); - expect(taskRecord.task.taskType).to.eql('alerting:test.noop'); - expect(JSON.parse(taskRecord.task.params)).to.eql({ - alertId: response.body.id, - spaceId: space.id, - consumer: 'alertsFixture', - }); - expect(taskRecord.task.enabled).to.eql(true); - // Ensure AAD isn't broken - await checkAAD({ - supertest, - spaceId: space.id, - type: 'alert', - id: response.body.id, - }); - break; - default: - throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`); - } - }); - }); - } - - it('should throw an error when trying to duplicate a rule who belongs to security solution', async () => { - const ruleCreated = await supertest - .post(`${getUrlPrefix(space1)}/api/alerting/rule`) - .set('kbn-xsrf', 'foo') - .send( - getTestRuleData({ - rule_type_id: 'test.unrestricted-noop', - consumer: 'siem', - }) - ); - objectRemover.add(space1, ruleCreated.body.id, 'rule', 'alerting'); - - const cloneRuleResponse = await supertest - .post(`${getUrlPrefix(space1)}/internal/alerting/rule/${ruleCreated.body.id}/_clone`) - .set('kbn-xsrf', 'foo') - .send(); - - expect(cloneRuleResponse.body).to.eql({ - error: 'Bad Request', - message: 'The clone functionality is not enable for rule who belongs to security solution', - statusCode: 400, - }); - }); - }); -} diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/index.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/index.ts index 0c8e0c6741656..86b249dc6bc79 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/index.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/index.ts @@ -30,12 +30,7 @@ export default function alertingTests({ loadTestFile, getService }: FtrProviderC loadTestFile(require.resolve('./get_alert_state')); loadTestFile(require.resolve('./get_alert_summary')); loadTestFile(require.resolve('./rule_types')); - loadTestFile(require.resolve('./bulk_edit')); - loadTestFile(require.resolve('./bulk_delete')); - loadTestFile(require.resolve('./bulk_enable')); - loadTestFile(require.resolve('./bulk_disable')); loadTestFile(require.resolve('./retain_api_key')); - loadTestFile(require.resolve('./clone')); }); }); } diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/rbac_legacy.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/rbac_legacy.ts index 7528c6e77acd3..30b8fc600e15a 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/rbac_legacy.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/rbac_legacy.ts @@ -55,7 +55,8 @@ export default function alertTests({ getService }: FtrProviderContext) { ), }; - describe('alerts', () => { + // FLAKY: https://github.com/elastic/kibana/issues/140867 + describe.skip('alerts', () => { const authorizationIndex = '.kibana-test-authorization'; const objectRemover = new ObjectRemover(supertest); diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group3/config.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group3/config.ts new file mode 100644 index 0000000000000..f999da061b90b --- /dev/null +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group3/config.ts @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { createTestConfig } from '../../common/config'; + +// eslint-disable-next-line import/no-default-export +export default createTestConfig('security_and_spaces', { + disabledPlugins: [], + license: 'trial', + ssl: true, + enableActionsProxy: true, + publicBaseUrl: true, + testFiles: [require.resolve('./tests')], + useDedicatedTaskRunner: true, +}); diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/bulk_delete.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group3/tests/alerting/bulk_delete.ts similarity index 100% rename from x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/bulk_delete.ts rename to x-pack/test/alerting_api_integration/security_and_spaces/group3/tests/alerting/bulk_delete.ts diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/bulk_disable.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group3/tests/alerting/bulk_disable.ts similarity index 100% rename from x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/bulk_disable.ts rename to x-pack/test/alerting_api_integration/security_and_spaces/group3/tests/alerting/bulk_disable.ts diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/bulk_edit.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group3/tests/alerting/bulk_edit.ts similarity index 100% rename from x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/bulk_edit.ts rename to x-pack/test/alerting_api_integration/security_and_spaces/group3/tests/alerting/bulk_edit.ts diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/bulk_enable.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group3/tests/alerting/bulk_enable.ts similarity index 100% rename from x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/bulk_enable.ts rename to x-pack/test/alerting_api_integration/security_and_spaces/group3/tests/alerting/bulk_enable.ts diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group3/tests/alerting/clone.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group3/tests/alerting/clone.ts new file mode 100644 index 0000000000000..e99aa177d8a78 --- /dev/null +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group3/tests/alerting/clone.ts @@ -0,0 +1,251 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import { Spaces, UserAtSpaceScenarios } from '../../../scenarios'; +import { + checkAAD, + getTestRuleData, + getConsumerUnauthorizedErrorMessage, + getUrlPrefix, + ObjectRemover, + TaskManagerDoc, +} from '../../../../common/lib'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; + +interface RuleSpace { + body: any; +} + +// eslint-disable-next-line import/no-default-export +export default function createAlertTests({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const es = getService('es'); + const supertestWithoutAuth = getService('supertestWithoutAuth'); + + describe('clone', async () => { + const objectRemover = new ObjectRemover(supertest); + const space1 = Spaces[0].id; + const space2 = Spaces[1].id; + let ruleSpace1: RuleSpace = { body: {} }; + let ruleSpace2: RuleSpace = { body: {} }; + after(() => objectRemover.removeAll()); + before(async () => { + const { body: createdActionSpace1 } = await supertest + .post(`${getUrlPrefix(space1)}/api/actions/connector`) + .set('kbn-xsrf', 'foo') + .send({ + name: 'MY action', + connector_type_id: 'test.noop', + config: {}, + secrets: {}, + }) + .expect(200); + + const { body: createdActionSpace2 } = await supertest + .post(`${getUrlPrefix(space2)}/api/actions/connector`) + .set('kbn-xsrf', 'foo') + .send({ + name: 'MY action', + connector_type_id: 'test.noop', + config: {}, + secrets: {}, + }) + .expect(200); + + ruleSpace1 = await supertest + .post(`${getUrlPrefix(space1)}/api/alerting/rule`) + .set('kbn-xsrf', 'foo') + .send( + getTestRuleData({ + actions: [ + { + id: createdActionSpace1.id, + group: 'default', + params: {}, + }, + ], + }) + ); + objectRemover.add(space1, ruleSpace1.body.id, 'rule', 'alerting'); + + ruleSpace2 = await supertest + .post(`${getUrlPrefix(space2)}/api/alerting/rule`) + .set('kbn-xsrf', 'foo') + .send( + getTestRuleData({ + actions: [ + { + id: createdActionSpace2.id, + group: 'default', + params: {}, + }, + ], + }) + ); + objectRemover.add(space2, ruleSpace2.body.id, 'rule', 'alerting'); + }); + + async function getScheduledTask(id: string): Promise { + const scheduledTask = await es.get({ + id: `task:${id}`, + index: '.kibana_task_manager', + }); + return scheduledTask._source!; + } + + for (const scenario of UserAtSpaceScenarios) { + const { user, space } = scenario; + describe(scenario.id, () => { + it('should handle clone rule request appropriately', async () => { + const ruleIdToClone = + space.id === space1 + ? ruleSpace1.body.id + : space.id === space2 + ? ruleSpace2.body.id + : null; + const response = await supertestWithoutAuth + .post(`${getUrlPrefix(space.id)}/internal/alerting/rule/${ruleIdToClone}/_clone`) + .set('kbn-xsrf', 'foo') + .auth(user.username, user.password) + .send(); + + switch (scenario.id) { + case 'no_kibana_privileges at space1': + case 'global_read at space1': + case 'space_1_all at space2': + expect(response.statusCode).to.eql(403); + expect(response.body).to.eql({ + error: 'Forbidden', + message: getConsumerUnauthorizedErrorMessage( + 'create', + 'test.noop', + 'alertsFixture' + ), + statusCode: 403, + }); + break; + case 'space_1_all_alerts_none_actions at space1': + case 'superuser at space1': + case 'space_1_all at space1': + case 'space_1_all_with_restricted_fixture at space1': + expect(response.statusCode).to.eql(200); + objectRemover.add(space.id, response.body.id, 'rule', 'alerting'); + expect(response.body).to.eql({ + id: response.body.id, + name: 'abc [Clone]', + tags: ['foo'], + actions: [ + { + id: response.body.actions[0].id, + connector_type_id: response.body.actions[0].connector_type_id, + group: 'default', + params: {}, + }, + ], + enabled: true, + rule_type_id: 'test.noop', + consumer: 'alertsFixture', + params: {}, + created_by: user.username, + schedule: { interval: '1m' }, + scheduled_task_id: response.body.scheduled_task_id, + created_at: response.body.created_at, + updated_at: response.body.updated_at, + throttle: '1m', + notify_when: 'onThrottleInterval', + updated_by: user.username, + api_key_owner: user.username, + mute_all: false, + muted_alert_ids: [], + execution_status: response.body.execution_status, + last_run: { + alerts_count: { + active: 0, + ignored: 0, + new: 0, + recovered: 0, + }, + outcome: 'succeeded', + outcome_msg: null, + warning: null, + }, + next_run: response.body.next_run, + }); + expect(typeof response.body.scheduled_task_id).to.be('string'); + expect(Date.parse(response.body.created_at)).to.be.greaterThan(0); + expect(Date.parse(response.body.updated_at)).to.be.greaterThan(0); + + const taskRecord = await getScheduledTask(response.body.scheduled_task_id); + expect(taskRecord.type).to.eql('task'); + expect(taskRecord.task.taskType).to.eql('alerting:test.noop'); + expect(JSON.parse(taskRecord.task.params)).to.eql({ + alertId: response.body.id, + spaceId: space.id, + consumer: 'alertsFixture', + }); + expect(taskRecord.task.enabled).to.eql(true); + // Ensure AAD isn't broken + await checkAAD({ + supertest, + spaceId: space.id, + type: 'alert', + id: response.body.id, + }); + break; + default: + throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`); + } + }); + }); + } + + it('should throw an error when trying to duplicate a rule who belongs to security solution', async () => { + const ruleCreated = await supertest + .post(`${getUrlPrefix(space1)}/api/alerting/rule`) + .set('kbn-xsrf', 'foo') + .send( + getTestRuleData({ + rule_type_id: 'test.unrestricted-noop', + consumer: 'siem', + }) + ); + objectRemover.add(space1, ruleCreated.body.id, 'rule', 'alerting'); + + const cloneRuleResponse = await supertest + .post(`${getUrlPrefix(space1)}/internal/alerting/rule/${ruleCreated.body.id}/_clone`) + .set('kbn-xsrf', 'foo') + .send(); + + expect(cloneRuleResponse.body).to.eql({ + error: 'Bad Request', + message: 'The clone functionality is not enable for rule who belongs to security solution', + statusCode: 400, + }); + }); + + it('should set scheduled_task_id to null when the rule cloned is disable', async () => { + const disableRuleCreated = await supertest + .post(`${getUrlPrefix(space1)}/api/alerting/rule`) + .set('kbn-xsrf', 'foo') + .send( + getTestRuleData({ + enabled: false, + }) + ); + objectRemover.add(space1, disableRuleCreated.body.id, 'rule', 'alerting'); + + const cloneRuleResponse = await supertest + .post(`${getUrlPrefix(space1)}/internal/alerting/rule/${disableRuleCreated.body.id}/_clone`) + .set('kbn-xsrf', 'foo') + .send(); + objectRemover.add(space1, cloneRuleResponse.body.id, 'rule', 'alerting'); + + expect(cloneRuleResponse.body.scheduled_task_id).to.eql(undefined); + }); + }); +} diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group3/tests/alerting/index.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group3/tests/alerting/index.ts new file mode 100644 index 0000000000000..0dd1ec2531733 --- /dev/null +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group3/tests/alerting/index.ts @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; +import { setupSpacesAndUsers, tearDown } from '../../../setup'; + +// eslint-disable-next-line import/no-default-export +export default function alertingTests({ loadTestFile, getService }: FtrProviderContext) { + describe('Alerts - Group 3', () => { + describe('alerts', () => { + before(async () => { + await setupSpacesAndUsers(getService); + }); + + after(async () => { + await tearDown(getService); + }); + + loadTestFile(require.resolve('./bulk_edit')); + loadTestFile(require.resolve('./bulk_delete')); + loadTestFile(require.resolve('./bulk_enable')); + loadTestFile(require.resolve('./bulk_disable')); + loadTestFile(require.resolve('./clone')); + }); + }); +} diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group3/tests/index.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group3/tests/index.ts new file mode 100644 index 0000000000000..c6b0d233a6041 --- /dev/null +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group3/tests/index.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; + +// eslint-disable-next-line import/no-default-export +export default function alertingApiIntegrationTests({ loadTestFile }: FtrProviderContext) { + describe('alerting api integration security and spaces enabled - Group 3', function () { + loadTestFile(require.resolve('./alerting')); + }); +} diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/event_log.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/event_log.ts index b5509096727fb..6de49d3d44915 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/event_log.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/event_log.ts @@ -530,6 +530,153 @@ export default function eventLogTests({ getService }: FtrProviderContext) { numRecoveredAlerts: 0, }); }); + + it('should generate expected events for flapping alerts that are mainly active', async () => { + const { body: createdAction } = await supertest + .post(`${getUrlPrefix(space.id)}/api/actions/connector`) + .set('kbn-xsrf', 'foo') + .send({ + name: 'MY action', + connector_type_id: 'test.noop', + config: {}, + secrets: {}, + }) + .expect(200); + + // pattern of when the alert should fire + const instance = [true, false, true, false].concat(new Array(22).fill(true)); + const pattern = { + instance, + }; + + const response = await supertest + .post(`${getUrlPrefix(space.id)}/api/alerting/rule`) + .set('kbn-xsrf', 'foo') + .send( + getTestRuleData({ + rule_type_id: 'test.patternFiring', + schedule: { interval: '1s' }, + throttle: null, + params: { + pattern, + }, + actions: [ + { + id: createdAction.id, + group: 'default', + params: {}, + }, + ], + }) + ); + + expect(response.status).to.eql(200); + const alertId = response.body.id; + objectRemover.add(space.id, alertId, 'rule', 'alerting'); + + // get the events we're expecting + const events = await retry.try(async () => { + return await getEventLog({ + getService, + spaceId: space.id, + type: 'alert', + id: alertId, + provider: 'alerting', + actions: new Map([ + // make sure the counts of the # of events per type are as expected + ['execute-start', { gte: 25 }], + ['execute', { gte: 25 }], + ['execute-action', { equal: 23 }], + ['new-instance', { equal: 3 }], + ['active-instance', { gte: 23 }], + ['recovered-instance', { equal: 2 }], + ]), + }); + }); + + const flapping = events + .filter( + (event) => + event?.event?.action === 'active-instance' || + event?.event?.action === 'recovered-instance' + ) + .map((event) => event?.kibana?.alert?.flapping); + const result = [false, false, false].concat(new Array(21).fill(true)).concat([false]); + expect(flapping).to.eql(result); + }); + + it('should generate expected events for flapping alerts that are mainly recovered', async () => { + const { body: createdAction } = await supertest + .post(`${getUrlPrefix(space.id)}/api/actions/connector`) + .set('kbn-xsrf', 'foo') + .send({ + name: 'MY action', + connector_type_id: 'test.noop', + config: {}, + secrets: {}, + }) + .expect(200); + + // pattern of when the alert should fire + const instance = [true, false, true].concat(new Array(18).fill(false)).concat(true); + const pattern = { + instance, + }; + + const response = await supertest + .post(`${getUrlPrefix(space.id)}/api/alerting/rule`) + .set('kbn-xsrf', 'foo') + .send( + getTestRuleData({ + rule_type_id: 'test.patternFiring', + schedule: { interval: '1s' }, + throttle: null, + params: { + pattern, + }, + actions: [ + { + id: createdAction.id, + group: 'default', + params: {}, + }, + ], + }) + ); + + expect(response.status).to.eql(200); + const alertId = response.body.id; + objectRemover.add(space.id, alertId, 'rule', 'alerting'); + + // get the events we're expecting + const events = await retry.try(async () => { + return await getEventLog({ + getService, + spaceId: space.id, + type: 'alert', + id: alertId, + provider: 'alerting', + actions: new Map([ + // make sure the counts of the # of events per type are as expected + ['execute-start', { gte: 20 }], + ['execute', { gte: 20 }], + ['execute-action', { equal: 3 }], + ['new-instance', { equal: 3 }], + ['active-instance', { gte: 3 }], + ['recovered-instance', { equal: 3 }], + ]), + }); + }); + + const flapping = events + .filter( + (event) => + event?.event?.action === 'active-instance' || + event?.event?.action === 'recovered-instance' + ) + .map((event) => event?.kibana?.alert?.flapping); + expect(flapping).to.eql([false, false, false, true, false, false]); + }); }); } }); diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/event_log_alerts.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/event_log_alerts.ts index 29046ae028f6a..86754ee31d14f 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/event_log_alerts.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/event_log_alerts.ts @@ -93,12 +93,12 @@ export default function eventLogAlertTests({ getService }: FtrProviderContext) { start?: string; durationToDate?: string; } = {}; - + const flapping = []; for (let i = 0; i < instanceEvents.length; ++i) { + flapping.push(instanceEvents[i]?.kibana?.alert?.flapping); switch (instanceEvents[i]?.event?.action) { case 'new-instance': expect(instanceEvents[i]?.kibana?.alerting?.instance_id).to.equal('instance'); - expect(instanceEvents[i]?.kibana?.alert?.flapping).to.equal(false); // a new alert should generate a unique UUID for the duration of its activeness expect(instanceEvents[i]?.event?.end).to.be(undefined); @@ -109,7 +109,6 @@ export default function eventLogAlertTests({ getService }: FtrProviderContext) { case 'active-instance': expect(instanceEvents[i]?.kibana?.alerting?.instance_id).to.equal('instance'); - expect(instanceEvents[i]?.kibana?.alert?.flapping).to.equal(false); expect(instanceEvents[i]?.event?.start).to.equal(currentAlertSpan.start); expect(instanceEvents[i]?.event?.end).to.be(undefined); @@ -124,7 +123,6 @@ export default function eventLogAlertTests({ getService }: FtrProviderContext) { case 'recovered-instance': expect(instanceEvents[i]?.kibana?.alerting?.instance_id).to.equal('instance'); - expect(instanceEvents[i]?.kibana?.alert?.flapping).to.equal(false); expect(instanceEvents[i]?.event?.start).to.equal(currentAlertSpan.start); expect(instanceEvents[i]?.event?.end).not.to.be(undefined); expect( @@ -134,6 +132,7 @@ export default function eventLogAlertTests({ getService }: FtrProviderContext) { break; } } + expect(flapping).to.eql(new Array(instanceEvents.length - 1).fill(false).concat(true)); }); }); } diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/flapping_history.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/flapping_history.ts new file mode 100644 index 0000000000000..ac09451e862fb --- /dev/null +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/flapping_history.ts @@ -0,0 +1,226 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import { get } from 'lodash'; +import { FtrProviderContext } from '../../../common/ftr_provider_context'; +import { getUrlPrefix, getTestRuleData, ObjectRemover } from '../../../common/lib'; +import { Spaces } from '../../scenarios'; + +// eslint-disable-next-line import/no-default-export +export default function createFlappingHistoryTests({ getService }: FtrProviderContext) { + const es = getService('es'); + const supertestWithoutAuth = getService('supertestWithoutAuth'); + const retry = getService('retry'); + const supertest = getService('supertest'); + const space = Spaces.default; + + const ACTIVE_PATH = 'alertInstances.instance.meta.flappingHistory'; + const RECOVERED_PATH = 'alertRecoveredInstances.instance.meta.flappingHistory'; + + describe('Flapping History', () => { + let actionId: string; + const objectRemover = new ObjectRemover(supertestWithoutAuth); + + before(async () => { + actionId = await createAction(); + }); + + after(async () => { + objectRemover.add(space.id, actionId, 'connector', 'actions'); + await objectRemover.removeAll(); + }); + + afterEach(() => objectRemover.removeAll()); + + it('should update flappingHistory when the alert flaps states', async () => { + let start = new Date().toISOString(); + const pattern = { + instance: [true, true, false, true], + }; + + const alertId = await createAlert(pattern, actionId); + objectRemover.add(space.id, alertId, 'rule', 'alerting'); + + let state = await getRuleState(start, alertId); + expect(get(state, ACTIVE_PATH)).to.eql([true]); + expect(state.alertRecoveredInstances).to.eql({}); + + start = new Date().toISOString(); + state = await getRuleState(start, alertId, 2, true); + expect(get(state, ACTIVE_PATH)).to.eql([true, false]); + expect(state.alertRecoveredInstances).to.eql({}); + + start = new Date().toISOString(); + state = await getRuleState(start, alertId, 3, true, true); + expect(get(state, RECOVERED_PATH)).to.eql([true, false, true]); + expect(state.alertInstances).to.eql({}); + + start = new Date().toISOString(); + state = await getRuleState(start, alertId, 4, true); + expect(get(state, ACTIVE_PATH)).to.eql([true, false, true, true]); + expect(state.alertRecoveredInstances).to.eql({}); + }); + + it('should update flappingHistory when the alert remains active', async () => { + let start = new Date().toISOString(); + const pattern = { + instance: [true, true, true, true], + }; + + const alertId = await createAlert(pattern, actionId); + objectRemover.add(space.id, alertId, 'rule', 'alerting'); + + let state = await getRuleState(start, alertId); + expect(get(state, ACTIVE_PATH)).to.eql([true]); + expect(state.alertRecoveredInstances).to.eql({}); + + start = new Date().toISOString(); + state = await getRuleState(start, alertId, 2, true); + expect(get(state, ACTIVE_PATH)).to.eql([true, false]); + expect(state.alertRecoveredInstances).to.eql({}); + + start = new Date().toISOString(); + state = await getRuleState(start, alertId, 3, true); + expect(get(state, ACTIVE_PATH)).to.eql([true, false, false]); + expect(state.alertRecoveredInstances).to.eql({}); + + start = new Date().toISOString(); + state = await getRuleState(start, alertId, 4, true); + expect(get(state, ACTIVE_PATH)).to.eql([true, false, false, false]); + expect(state.alertRecoveredInstances).to.eql({}); + }); + + it('should update flappingHistory when the alert remains recovered', async () => { + let start = new Date().toISOString(); + const pattern = { + instance: [true, false, false, false, true], + }; + + const alertId = await createAlert(pattern, actionId); + objectRemover.add(space.id, alertId, 'rule', 'alerting'); + + let state = await getRuleState(start, alertId); + expect(get(state, ACTIVE_PATH)).to.eql([true]); + expect(state.alertRecoveredInstances).to.eql({}); + + start = new Date().toISOString(); + state = await getRuleState(start, alertId, 2, true, true); + expect(get(state, RECOVERED_PATH)).to.eql([true, true]); + expect(state.alertInstances).to.eql({}); + + start = new Date().toISOString(); + state = await getRuleState(start, alertId, 3, true, true); + expect(get(state, RECOVERED_PATH)).to.eql([true, true, false]); + expect(state.alertInstances).to.eql({}); + + start = new Date().toISOString(); + state = await getRuleState(start, alertId, 4, true, true); + expect(get(state, RECOVERED_PATH)).to.eql([true, true, false, false]); + expect(state.alertInstances).to.eql({}); + + start = new Date().toISOString(); + state = await getRuleState(start, alertId, 5, true, false); + expect(get(state, ACTIVE_PATH)).to.eql([true, true, false, false, true]); + expect(state.alertRecoveredInstances).to.eql({}); + }); + }); + + async function getState(start: string, runs: number, recovered: boolean) { + const result: any = await retry.try(async () => { + const searchResult = await es.search({ + index: '.kibana_task_manager', + body: { + query: { + bool: { + must: [ + { + term: { + 'task.taskType': 'alerting:test.patternFiring', + }, + }, + { + range: { + 'task.scheduledAt': { + gte: start, + }, + }, + }, + ], + }, + }, + }, + }); + + const taskDoc: any = searchResult.hits.hits[0]; + const state = JSON.parse(taskDoc._source.task.state); + const flappingHistory = recovered + ? get(state, RECOVERED_PATH, []) + : get(state, ACTIVE_PATH, []); + if (flappingHistory.length !== runs) { + throw new Error(`Expected ${runs} rule executions but received ${flappingHistory.length}.`); + } + + return state; + }); + + return result; + } + + async function getRuleState( + start: string, + alertId: string, + runs: number = 1, + runRule: boolean = false, + recovered: boolean = false + ) { + if (runRule) { + const response = await supertest + .post(`${getUrlPrefix(space.id)}/internal/alerting/rule/${alertId}/_run_soon`) + .set('kbn-xsrf', 'foo'); + expect(response.status).to.eql(204); + } + return await getState(start, runs, recovered); + } + + async function createAlert(pattern: { instance: boolean[] }, actionId: string) { + const { body: createdAlert } = await supertest + .post(`${getUrlPrefix(space.id)}/api/alerting/rule`) + .set('kbn-xsrf', 'foo') + .send( + getTestRuleData({ + rule_type_id: 'test.patternFiring', + schedule: { interval: '24h' }, + throttle: null, + params: { + pattern, + }, + actions: [], + }) + ) + .expect(200); + return createdAlert.id; + } + + async function createAction() { + const { body: createdAction } = await supertest + .post(`${getUrlPrefix(space.id)}/api/actions/connector`) + .set('kbn-xsrf', 'foo') + .send({ + name: 'My action', + connector_type_id: 'test.index-record', + config: { + unencrypted: `This value shouldn't get encrypted`, + }, + secrets: { + encrypted: 'This value should be encrypted', + }, + }) + .expect(200); + return createdAction.id; + } +} diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/index.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/index.ts index d0981fbc1e93a..c887a10aa14aa 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/index.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/index.ts @@ -50,6 +50,7 @@ export default function alertingTests({ loadTestFile, getService }: FtrProviderC loadTestFile(require.resolve('./capped_action_type')); loadTestFile(require.resolve('./scheduled_task_id')); loadTestFile(require.resolve('./run_soon')); + loadTestFile(require.resolve('./flapping_history')); loadTestFile(require.resolve('./check_registered_rule_types')); // Do not place test files here, due to https://github.com/elastic/kibana/issues/123059 diff --git a/x-pack/test/api_integration/apis/logs_ui/log_threshold_alert.ts b/x-pack/test/api_integration/apis/logs_ui/log_threshold_alert.ts index 2ada34d4dcd5f..0847b650762b5 100644 --- a/x-pack/test/api_integration/apis/logs_ui/log_threshold_alert.ts +++ b/x-pack/test/api_integration/apis/logs_ui/log_threshold_alert.ts @@ -135,6 +135,9 @@ export default function ({ getService }: FtrProviderContext) { context: { conditions: 'env does not equal test', group: 'dev', + groupByKeys: { + env: 'dev', + }, isRatio: false, matchingDocuments: 2, reason: '2 log entries in the last 5 mins for dev. Alert when ≥ 1.', @@ -192,6 +195,9 @@ export default function ({ getService }: FtrProviderContext) { context: { conditions: 'env does not equal test', group: 'dev', + groupByKeys: { + env: 'dev', + }, isRatio: false, matchingDocuments: 2, reason: '2 log entries in the last 5 mins for dev. Alert when ≥ 1.', @@ -306,6 +312,11 @@ export default function ({ getService }: FtrProviderContext) { context: { denominatorConditions: 'event.dataset does not equal nginx.error', group: 'web', + groupByKeys: { + event: { + category: 'web', + }, + }, isRatio: true, numeratorConditions: 'event.dataset equals nginx.error', ratio: 0.5526081141328578, diff --git a/x-pack/test/api_integration/apis/metrics_ui/constants.ts b/x-pack/test/api_integration/apis/metrics_ui/constants.ts index b37b5b1ddd5da..7fd25f09875b4 100644 --- a/x-pack/test/api_integration/apis/metrics_ui/constants.ts +++ b/x-pack/test/api_integration/apis/metrics_ui/constants.ts @@ -23,6 +23,10 @@ export const DATES = { min: new Date('2022-01-20T17:09:55.124Z').getTime(), max: new Date('2022-01-20T17:14:57.378Z').getTime(), }, + hosts_and_netowrk: { + min: new Date('2022-11-23T14:13:19.534Z').getTime(), + max: new Date('2022-11-25T14:13:19.534Z').getTime(), + }, hosts_only: { min: new Date('2022-01-18T19:57:47.534Z').getTime(), max: new Date('2022-01-18T20:02:50.043Z').getTime(), diff --git a/x-pack/test/api_integration/apis/metrics_ui/metrics_overview_top.ts b/x-pack/test/api_integration/apis/metrics_ui/metrics_overview_top.ts index 0d88e78841f8e..f6a4e0e0ccdea 100644 --- a/x-pack/test/api_integration/apis/metrics_ui/metrics_overview_top.ts +++ b/x-pack/test/api_integration/apis/metrics_ui/metrics_overview_top.ts @@ -18,13 +18,12 @@ export default function ({ getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const supertest = getService('supertest'); - const { min, max } = DATES['7.0.0'].hosts; - describe('API /metrics/overview/top', () => { before(() => esArchiver.load('x-pack/test/functional/es_archives/infra/7.0.0/hosts')); after(() => esArchiver.unload('x-pack/test/functional/es_archives/infra/7.0.0/hosts')); it('works', async () => { + const { min, max } = DATES['7.0.0'].hosts; const response = await supertest .post('/api/metrics/overview/top') .set({ @@ -49,5 +48,49 @@ export default function ({ getService }: FtrProviderContext) { expect(series[0].id).to.be('demo-stack-mysql-01'); expect(series[0].timeseries[1].timestamp - series[0].timeseries[0].timestamp).to.be(300_000); }); + + describe('Runtime fields calculation', () => { + before(() => + esArchiver.load('x-pack/test/functional/es_archives/infra/8.0.0/hosts_and_network') + ); + after(() => + esArchiver.unload('x-pack/test/functional/es_archives/infra/8.0.0/hosts_and_network') + ); + + it('should return correct sorted calculations', async () => { + const { min, max } = DATES['8.0.0'].hosts_and_netowrk; + const response = await supertest + .post('/api/metrics/overview/top') + .set({ + 'kbn-xsrf': 'some-xsrf-token', + }) + .send( + TopNodesRequestRT.encode({ + sourceId: 'default', + bucketSize: '300s', + size: 5, + timerange: { + from: min, + to: max, + }, + sort: 'rx', + sortDirection: 'asc', + }) + ) + .expect(200); + const { series } = decodeOrThrow(TopNodesResponseRT)(response.body); + + const hosts = series.map((s) => ({ + name: s.name, + rx: s.rx, + tx: s.tx, + })); + + expect(hosts.length).to.be(3); + expect(hosts[0]).to.eql({ name: 'metricbeat-2', rx: 8000, tx: 16860 }); + expect(hosts[1]).to.eql({ name: 'metricbeat-1', rx: 11250, tx: 25290.5 }); + expect(hosts[2]).to.eql({ name: 'metricbeat-3', rx: null, tx: null }); + }); + }); }); } diff --git a/x-pack/test/api_integration/apis/ml/results/get_anomaly_search.ts b/x-pack/test/api_integration/apis/ml/results/get_anomaly_search.ts index ce4b5b600e579..67202d6d903fa 100644 --- a/x-pack/test/api_integration/apis/ml/results/get_anomaly_search.ts +++ b/x-pack/test/api_integration/apis/ml/results/get_anomaly_search.ts @@ -62,7 +62,7 @@ export default ({ getService }: FtrProviderContext) => { await ml.api.createAndRunAnomalyDetectionLookbackJob( ml.commonConfig.getADFqSingleMetricJobConfig(adJobId), ml.commonConfig.getADFqDatafeedConfig(adJobId), - idSpace1 + { space: idSpace1 } ); await ml.api.assertJobSpaces(adJobId, 'anomaly-detector', [idSpace1]); }); diff --git a/x-pack/test/api_integration/apis/ml/results/get_category_definition.ts b/x-pack/test/api_integration/apis/ml/results/get_category_definition.ts index 3606677fc0a3e..a349c799e1763 100644 --- a/x-pack/test/api_integration/apis/ml/results/get_category_definition.ts +++ b/x-pack/test/api_integration/apis/ml/results/get_category_definition.ts @@ -87,7 +87,7 @@ export default ({ getService }: FtrProviderContext) => { // @ts-expect-error not full interface testJobConfig, testDatafeedConfig, - idSpace1 + { space: idSpace1 } ); }); diff --git a/x-pack/test/api_integration/apis/ml/results/get_category_examples.ts b/x-pack/test/api_integration/apis/ml/results/get_category_examples.ts index ddcdf6244629d..6b0a0823faaca 100644 --- a/x-pack/test/api_integration/apis/ml/results/get_category_examples.ts +++ b/x-pack/test/api_integration/apis/ml/results/get_category_examples.ts @@ -88,7 +88,7 @@ export default ({ getService }: FtrProviderContext) => { // @ts-expect-error not full interface testJobConfig, testDatafeedConfig, - idSpace1 + { space: idSpace1 } ); }); diff --git a/x-pack/test/api_integration/apis/synthetics/add_monitor_private_location.ts b/x-pack/test/api_integration/apis/synthetics/add_monitor_private_location.ts index 75e8434136f52..1855770a19525 100644 --- a/x-pack/test/api_integration/apis/synthetics/add_monitor_private_location.ts +++ b/x-pack/test/api_integration/apis/synthetics/add_monitor_private_location.ts @@ -4,6 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ +import semver from 'semver'; import uuid from 'uuid'; import { ConfigKey, HTTPFields } from '@kbn/synthetics-plugin/common/runtime_types'; import { API_URLS } from '@kbn/synthetics-plugin/common/constants'; @@ -18,8 +19,7 @@ import { comparePolicies, getTestSyntheticsPolicy } from '../uptime/rest/sample_ import { PrivateLocationTestService } from './services/private_location_test_service'; export default function ({ getService }: FtrProviderContext) { - // Failing: See https://github.com/elastic/kibana/issues/145639 - describe.skip('PrivateLocationMonitor', function () { + describe('PrivateLocationMonitor', function () { this.tags('skipCloud'); const kibanaServer = getService('kibanaServer'); const supertestAPI = getService('supertest'); @@ -364,11 +364,6 @@ export default function ({ getService }: FtrProviderContext) { expect(packagePolicy.package.version).eql('0.10.3'); - await supertestAPI - .post('/api/fleet/epm/packages/synthetics/0.11.2') - .set('kbn-xsrf', 'true') - .send({ force: true }); - await supertestAPI.post('/api/fleet/setup').set('kbn-xsrf', 'true').send().expect(200); const policyResponseAfterUpgrade = await supertestAPI.get( '/api/fleet/package_policies?page=1&perPage=2000&kuery=ingest-package-policies.package.name%3A%20synthetics' @@ -377,7 +372,7 @@ export default function ({ getService }: FtrProviderContext) { (pkgPolicy: PackagePolicy) => pkgPolicy.id === monitorId + '-' + testFleetPolicyID + `-default` ); - expect(packagePolicyAfterUpgrade.package.version).eql('0.11.2'); + expect(semver.gt(packagePolicyAfterUpgrade.package.version, '0.10.3')).eql(true); } finally { await supertestAPI .delete(API_URLS.SYNTHETICS_MONITORS + '/' + monitorId) diff --git a/x-pack/test/api_integration/apis/synthetics/add_monitor_project.ts b/x-pack/test/api_integration/apis/synthetics/add_monitor_project.ts index 7911beea80a6e..168da6d75bb63 100644 --- a/x-pack/test/api_integration/apis/synthetics/add_monitor_project.ts +++ b/x-pack/test/api_integration/apis/synthetics/add_monitor_project.ts @@ -152,11 +152,8 @@ export default function ({ getService }: FtrProviderContext) { lon: 0, }, id: 'localhost', - isInvalid: false, isServiceManaged: true, label: 'Local Synthetics Service', - status: 'experimental', - url: 'mockDevUrl', }, ], name: 'check if title is present', @@ -282,11 +279,8 @@ export default function ({ getService }: FtrProviderContext) { lon: 0, }, id: 'localhost', - isInvalid: false, isServiceManaged: true, label: 'Local Synthetics Service', - status: 'experimental', - url: 'mockDevUrl', }, ], max_redirects: '0', @@ -390,11 +384,8 @@ export default function ({ getService }: FtrProviderContext) { lon: 0, }, id: 'localhost', - isInvalid: false, isServiceManaged: true, label: 'Local Synthetics Service', - status: 'experimental', - url: 'mockDevUrl', }, ], name: monitor.name, @@ -489,21 +480,15 @@ export default function ({ getService }: FtrProviderContext) { lon: 0, }, id: 'localhost', - isInvalid: false, isServiceManaged: true, label: 'Local Synthetics Service', - status: 'experimental', - url: 'mockDevUrl', }, { - agentPolicyId: testPolicyId, - concurrentMonitors: 1, geo: { lat: '', lon: '', }, id: testPolicyId, - isInvalid: false, isServiceManaged: false, label: 'Test private location 0', }, @@ -1317,22 +1302,16 @@ export default function ({ getService }: FtrProviderContext) { id: 'localhost', label: 'Local Synthetics Service', geo: { lat: 0, lon: 0 }, - url: 'mockDevUrl', isServiceManaged: true, - status: 'experimental', - isInvalid: false, }, { label: 'Test private location 0', isServiceManaged: false, - isInvalid: false, - agentPolicyId: testPolicyId, id: testPolicyId, geo: { lat: '', lon: '', }, - concurrentMonitors: 1, }, ]); }); diff --git a/x-pack/test/api_integration/apis/synthetics/add_monitor_project_legacy.ts b/x-pack/test/api_integration/apis/synthetics/add_monitor_project_legacy.ts index 9d5e4b45f4e7f..fcaee57dd8166 100644 --- a/x-pack/test/api_integration/apis/synthetics/add_monitor_project_legacy.ts +++ b/x-pack/test/api_integration/apis/synthetics/add_monitor_project_legacy.ts @@ -152,11 +152,8 @@ export default function ({ getService }: FtrProviderContext) { lon: 0, }, id: 'localhost', - isInvalid: false, isServiceManaged: true, label: 'Local Synthetics Service', - status: 'experimental', - url: 'mockDevUrl', }, ], name: 'check if title is present', @@ -279,11 +276,8 @@ export default function ({ getService }: FtrProviderContext) { lon: 0, }, id: 'localhost', - isInvalid: false, isServiceManaged: true, label: 'Local Synthetics Service', - status: 'experimental', - url: 'mockDevUrl', }, ], max_redirects: '0', @@ -384,11 +378,8 @@ export default function ({ getService }: FtrProviderContext) { lon: 0, }, id: 'localhost', - isInvalid: false, isServiceManaged: true, label: 'Local Synthetics Service', - status: 'experimental', - url: 'mockDevUrl', }, ], name: monitor.name, @@ -481,21 +472,15 @@ export default function ({ getService }: FtrProviderContext) { lon: 0, }, id: 'localhost', - isInvalid: false, isServiceManaged: true, label: 'Local Synthetics Service', - status: 'experimental', - url: 'mockDevUrl', }, { - agentPolicyId: testPolicyId, - concurrentMonitors: 1, geo: { lat: '', lon: '', }, id: testPolicyId, - isInvalid: false, isServiceManaged: false, label: 'Test private location 0', }, @@ -2012,22 +1997,16 @@ export default function ({ getService }: FtrProviderContext) { id: 'localhost', label: 'Local Synthetics Service', geo: { lat: 0, lon: 0 }, - url: 'mockDevUrl', isServiceManaged: true, - status: 'experimental', - isInvalid: false, }, { label: 'Test private location 0', isServiceManaged: false, - isInvalid: false, - agentPolicyId: testPolicyId, id: testPolicyId, geo: { lat: '', lon: '', }, - concurrentMonitors: 1, }, ]); }); diff --git a/x-pack/test/api_integration/apis/synthetics/edit_monitor.ts b/x-pack/test/api_integration/apis/synthetics/edit_monitor.ts index 209ba0c4d9a38..0ed7f83de204d 100644 --- a/x-pack/test/api_integration/apis/synthetics/edit_monitor.ts +++ b/x-pack/test/api_integration/apis/synthetics/edit_monitor.ts @@ -43,6 +43,7 @@ export default function ({ getService }: FtrProviderContext) { before(async () => { _httpMonitorJson = getFixtureJson('http_monitor'); await supertest.post('/api/fleet/setup').set('kbn-xsrf', 'true').send().expect(200); + await supertest.post(API_URLS.SYNTHETICS_ENABLEMENT).set('kbn-xsrf', 'true').expect(200); const testPolicyName = 'Fleet test server policy' + Date.now(); const apiResponse = await testPrivateLocations.addFleetPolicy(testPolicyName); @@ -238,6 +239,61 @@ export default function ({ getService }: FtrProviderContext) { expect(apiResponse.body.message).eql('Monitor type is invalid'); }); + it('sets config hash to empty string on edits', async () => { + const newMonitor = httpMonitorJson; + const configHash = 'djrhefje'; + + const { id: monitorId, attributes: savedMonitor } = await saveMonitor({ + ...(newMonitor as MonitorFields), + [ConfigKey.CONFIG_HASH]: configHash, + }); + + expect(savedMonitor).eql( + omit( + { + ...newMonitor, + [ConfigKey.CONFIG_ID]: monitorId, + [ConfigKey.MONITOR_QUERY_ID]: monitorId, + [ConfigKey.CONFIG_HASH]: configHash, + }, + secretKeys + ) + ); + + const updates: Partial = { + [ConfigKey.URLS]: 'https://modified-host.com', + } as Partial; + + const modifiedMonitor = { + ...newMonitor, + ...updates, + [ConfigKey.METADATA]: { + ...newMonitor[ConfigKey.METADATA], + ...updates[ConfigKey.METADATA], + }, + }; + + const editResponse = await supertest + .put(API_URLS.SYNTHETICS_MONITORS + '/' + monitorId) + .set('kbn-xsrf', 'true') + .send(modifiedMonitor) + .expect(200); + + expect(editResponse.body.attributes).eql( + omit( + { + ...modifiedMonitor, + [ConfigKey.CONFIG_ID]: monitorId, + [ConfigKey.MONITOR_QUERY_ID]: monitorId, + [ConfigKey.CONFIG_HASH]: '', + revision: 2, + }, + secretKeys + ) + ); + expect(editResponse.body.attributes).not.to.have.keys('unknownkey'); + }); + it('handles private location errors and does not update the monitor if integration policy is unable to be updated', async () => { const name = 'Monitor with private location'; const newMonitor = { diff --git a/x-pack/test/api_integration/apis/synthetics/get_monitor_overview.ts b/x-pack/test/api_integration/apis/synthetics/get_monitor_overview.ts index 89bbb5f46108a..0aa940941f98a 100644 --- a/x-pack/test/api_integration/apis/synthetics/get_monitor_overview.ts +++ b/x-pack/test/api_integration/apis/synthetics/get_monitor_overview.ts @@ -102,151 +102,151 @@ export default function ({ getService }: FtrProviderContext) { await security.role.delete(roleName); }); - describe('returns total number of monitor combinations', () => { - it('returns the correct response', async () => { - let savedMonitors: SimpleSavedObject[] = []; - try { - const savedResponse = await Promise.all(monitors.map(saveMonitor)); - savedMonitors = savedResponse; - - const apiResponse = await supertest.get( - `/s/${SPACE_ID}${SYNTHETICS_API_URLS.SYNTHETICS_OVERVIEW}` - ); - - expect(apiResponse.body.total).eql(monitors.length * 2); - expect(apiResponse.body.allMonitorIds.sort()).eql( - savedMonitors.map((monitor) => monitor.id).sort() - ); - expect(apiResponse.body.monitors.length).eql(40); - } finally { - await Promise.all( - savedMonitors.map((monitor) => { - return deleteMonitor(monitor.id); - }) - ); - } - }); + it('returns the correct response', async () => { + let savedMonitors: SimpleSavedObject[] = []; + try { + const savedResponse = await Promise.all(monitors.map(saveMonitor)); + savedMonitors = savedResponse; + + const apiResponse = await supertest.get( + `/s/${SPACE_ID}${SYNTHETICS_API_URLS.SYNTHETICS_OVERVIEW}` + ); + + expect(apiResponse.body.total).eql(monitors.length * 2); + expect(apiResponse.body.allMonitorIds.sort()).eql( + savedMonitors.map((monitor) => monitor.id).sort() + ); + expect(apiResponse.body.monitors.length).eql(40); + } finally { + await Promise.all( + savedMonitors.map((monitor) => { + return deleteMonitor(monitor.id); + }) + ); + } + }); - it('accepts search queries', async () => { - let savedMonitors: Array> = []; - try { - const savedResponse = await Promise.all(monitors.map(saveMonitor)); - savedMonitors = savedResponse; - - const apiResponse = await supertest - .get(`/s/${SPACE_ID}${SYNTHETICS_API_URLS.SYNTHETICS_OVERVIEW}`) - .query({ - query: '19', - }); - - expect(apiResponse.body.total).eql(2); - expect(apiResponse.body.allMonitorIds.sort()).eql( - savedMonitors - .filter((monitor) => monitor.attributes.name.includes('19')) - .map((monitor) => monitor.id) - ); - expect(apiResponse.body.monitors.length).eql(2); - } finally { - await Promise.all( - savedMonitors.map((monitor) => { - return deleteMonitor(monitor.id); - }) - ); - } - }); + it('accepts search queries', async () => { + let savedMonitors: Array> = []; + try { + const savedResponse = await Promise.all(monitors.map(saveMonitor)); + savedMonitors = savedResponse; + + const apiResponse = await supertest + .get(`/s/${SPACE_ID}${SYNTHETICS_API_URLS.SYNTHETICS_OVERVIEW}`) + .query({ + query: '19', + }); + + expect(apiResponse.body.total).eql(2); + expect(apiResponse.body.allMonitorIds.sort()).eql( + savedMonitors + .filter((monitor) => monitor.attributes.name.includes('19')) + .map((monitor) => monitor.id) + ); + expect(apiResponse.body.monitors.length).eql(2); + } finally { + await Promise.all( + savedMonitors.map((monitor) => { + return deleteMonitor(monitor.id); + }) + ); + } }); - describe('Overview Item', () => { - it('returns the correct response', async () => { - let savedMonitors: Array> = []; - const customHeartbeatId = 'example_custom_heartbeat_id'; - try { - const savedResponse = await Promise.all( - [ - monitors[0], - { ...monitors[1], custom_heartbeat_id: 'example_custom_heartbeat_id' }, - ].map(saveMonitor) - ); - savedMonitors = savedResponse; - - const apiResponse = await supertest.get( - `/s/${SPACE_ID}${SYNTHETICS_API_URLS.SYNTHETICS_OVERVIEW}` - ); - expect(apiResponse.body.monitors).eql([ + it('returns the correct response', async () => { + let savedMonitors: Array> = []; + const customHeartbeatId = 'example_custom_heartbeat_id'; + try { + const savedResponse = await Promise.all( + [ + { ...monitors[0], name: 'test monitor a' }, { - id: savedMonitors[0].attributes[ConfigKey.MONITOR_QUERY_ID], - configId: savedMonitors[0].id, - name: 'test-monitor-name 0', - location: { - id: 'eu-west-01', - label: 'Europe West', - geo: { - lat: 33.2343132435, - lon: 73.2342343434, - }, - url: 'https://example-url.com', - isServiceManaged: true, + ...monitors[1], + custom_heartbeat_id: 'example_custom_heartbeat_id', + name: 'test monitor b', + }, + ].map(saveMonitor) + ); + savedMonitors = savedResponse; + + const apiResponse = await supertest.get( + `/s/${SPACE_ID}${SYNTHETICS_API_URLS.SYNTHETICS_OVERVIEW}` + ); + expect(apiResponse.body.monitors).eql([ + { + id: savedMonitors[0].attributes[ConfigKey.MONITOR_QUERY_ID], + configId: savedMonitors[0].id, + name: 'test monitor a', + location: { + id: 'eu-west-01', + label: 'Europe West', + geo: { + lat: 33.2343132435, + lon: 73.2342343434, }, - isEnabled: true, + url: 'https://example-url.com', + isServiceManaged: true, }, - { - id: savedMonitors[0].attributes[ConfigKey.MONITOR_QUERY_ID], - configId: savedMonitors[0].id, - name: 'test-monitor-name 0', - location: { - id: 'eu-west-02', - label: 'Europe West', - geo: { - lat: 33.2343132435, - lon: 73.2342343434, - }, - url: 'https://example-url.com', - isServiceManaged: true, + isEnabled: true, + }, + { + id: savedMonitors[0].attributes[ConfigKey.MONITOR_QUERY_ID], + configId: savedMonitors[0].id, + name: 'test monitor a', + location: { + id: 'eu-west-02', + label: 'Europe West', + geo: { + lat: 33.2343132435, + lon: 73.2342343434, }, - isEnabled: true, + url: 'https://example-url.com', + isServiceManaged: true, }, - { - id: savedMonitors[1].attributes[ConfigKey.MONITOR_QUERY_ID], - configId: savedMonitors[1].id, - name: 'test-monitor-name 1', - location: { - id: 'eu-west-01', - label: 'Europe West', - geo: { - lat: 33.2343132435, - lon: 73.2342343434, - }, - url: 'https://example-url.com', - isServiceManaged: true, + isEnabled: true, + }, + { + id: savedMonitors[1].attributes[ConfigKey.MONITOR_QUERY_ID], + configId: savedMonitors[1].id, + name: 'test monitor b', + location: { + id: 'eu-west-01', + label: 'Europe West', + geo: { + lat: 33.2343132435, + lon: 73.2342343434, }, - isEnabled: true, + url: 'https://example-url.com', + isServiceManaged: true, }, - { - id: savedMonitors[1].attributes[ConfigKey.MONITOR_QUERY_ID], - configId: savedMonitors[1].id, - name: 'test-monitor-name 1', - location: { - id: 'eu-west-02', - label: 'Europe West', - geo: { - lat: 33.2343132435, - lon: 73.2342343434, - }, - url: 'https://example-url.com', - isServiceManaged: true, + isEnabled: true, + }, + { + id: savedMonitors[1].attributes[ConfigKey.MONITOR_QUERY_ID], + configId: savedMonitors[1].id, + name: 'test monitor b', + location: { + id: 'eu-west-02', + label: 'Europe West', + geo: { + lat: 33.2343132435, + lon: 73.2342343434, }, - isEnabled: true, + url: 'https://example-url.com', + isServiceManaged: true, }, - ]); - expect(savedMonitors[1].attributes[ConfigKey.MONITOR_QUERY_ID]).eql(customHeartbeatId); - } finally { - await Promise.all( - savedMonitors.map((monitor) => { - return deleteMonitor(monitor.id); - }) - ); - } - }); + isEnabled: true, + }, + ]); + expect(savedMonitors[1].attributes[ConfigKey.MONITOR_QUERY_ID]).eql(customHeartbeatId); + } finally { + await Promise.all( + savedMonitors.map((monitor) => { + return deleteMonitor(monitor.id); + }) + ); + } }); }); } diff --git a/x-pack/test/apm_api_integration/tests/sourcemaps/sourcemaps.ts b/x-pack/test/apm_api_integration/tests/sourcemaps/sourcemaps.ts index 44a9eae45b5f1..0b3dbf2c6ebfc 100644 --- a/x-pack/test/apm_api_integration/tests/sourcemaps/sourcemaps.ts +++ b/x-pack/test/apm_api_integration/tests/sourcemaps/sourcemaps.ts @@ -7,7 +7,7 @@ import { APIReturnType } from '@kbn/apm-plugin/public/services/rest/create_call_apm_api'; import type { SourceMap } from '@kbn/apm-plugin/server/routes/source_maps/route'; import expect from '@kbn/expect'; -import { times } from 'lodash'; +import { first, last, times } from 'lodash'; import { FtrProviderContext } from '../../common/ftr_provider_context'; export default function ApiTest({ getService }: FtrProviderContext) { @@ -47,11 +47,14 @@ export default function ApiTest({ getService }: FtrProviderContext) { }); } - async function listSourcemaps() { + async function listSourcemaps({ page, perPage }: { page?: number; perPage?: number } = {}) { + const query = page && perPage ? { page, perPage } : {}; + const response = await apmApiClient.readUser({ endpoint: 'GET /api/apm/sourcemaps', + params: { query }, }); - return response.body.artifacts; + return response.body; } registry.when('source maps', { config: 'basic', archives: [] }, () => { @@ -66,7 +69,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { it('can upload a source map', async () => { resp = await uploadSourcemap({ - serviceName: 'foo', + serviceName: 'my_service', serviceVersion: '1.0.0', bundleFilePath: 'bar', sourcemap: { @@ -79,13 +82,13 @@ export default function ApiTest({ getService }: FtrProviderContext) { }); }); - describe('list source maps', () => { + describe('list source maps', async () => { const uploadedSourcemapIds: string[] = []; before(async () => { - const sourcemapCount = times(2); + const sourcemapCount = times(15); for (const i of sourcemapCount) { const sourcemap = await uploadSourcemap({ - serviceName: 'foo', + serviceName: 'my_service', serviceVersion: `1.0.${i}`, bundleFilePath: 'bar', sourcemap: { @@ -95,7 +98,6 @@ export default function ApiTest({ getService }: FtrProviderContext) { }, }); uploadedSourcemapIds.push(sourcemap.id); - await sleep(100); } }); @@ -103,17 +105,41 @@ export default function ApiTest({ getService }: FtrProviderContext) { await Promise.all(uploadedSourcemapIds.map((id) => deleteSourcemap(id))); }); + describe('pagination', () => { + it('can retrieve the first page', async () => { + const firstPageItems = await listSourcemaps({ page: 1, perPage: 5 }); + expect(first(firstPageItems.artifacts)?.identifier).to.eql('my_service-1.0.14'); + expect(last(firstPageItems.artifacts)?.identifier).to.eql('my_service-1.0.10'); + expect(firstPageItems.artifacts.length).to.be(5); + expect(firstPageItems.total).to.be(15); + }); + + it('can retrieve the second page', async () => { + const secondPageItems = await listSourcemaps({ page: 2, perPage: 5 }); + expect(first(secondPageItems.artifacts)?.identifier).to.eql('my_service-1.0.9'); + expect(last(secondPageItems.artifacts)?.identifier).to.eql('my_service-1.0.5'); + expect(secondPageItems.artifacts.length).to.be(5); + expect(secondPageItems.total).to.be(15); + }); + + it('can retrieve the third page', async () => { + const thirdPageItems = await listSourcemaps({ page: 3, perPage: 5 }); + expect(first(thirdPageItems.artifacts)?.identifier).to.eql('my_service-1.0.4'); + expect(last(thirdPageItems.artifacts)?.identifier).to.eql('my_service-1.0.0'); + expect(thirdPageItems.artifacts.length).to.be(5); + expect(thirdPageItems.total).to.be(15); + }); + }); + it('can list source maps', async () => { const sourcemaps = await listSourcemaps(); - expect(sourcemaps).to.not.empty(); + expect(sourcemaps.artifacts.length).to.be(15); + expect(sourcemaps.total).to.be(15); }); it('returns newest source maps first', async () => { - const response = await apmApiClient.readUser({ - endpoint: 'GET /api/apm/sourcemaps', - }); - - const timestamps = response.body.artifacts.map((a) => new Date(a.created).getTime()); + const { artifacts } = await listSourcemaps(); + const timestamps = artifacts.map((a) => new Date(a.created).getTime()); expect(timestamps[0]).to.be.greaterThan(timestamps[1]); }); }); @@ -121,7 +147,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { describe('delete source maps', () => { it('can delete a source map', async () => { const sourcemap = await uploadSourcemap({ - serviceName: 'foo', + serviceName: 'my_service', serviceVersion: '1.0.0', bundleFilePath: 'bar', sourcemap: { @@ -132,13 +158,10 @@ export default function ApiTest({ getService }: FtrProviderContext) { }); await deleteSourcemap(sourcemap.id); - const sourcemaps = await listSourcemaps(); - expect(sourcemaps).to.be.empty(); + const { artifacts, total } = await listSourcemaps(); + expect(artifacts).to.be.empty(); + expect(total).to.be(0); }); }); }); } - -function sleep(ms: number) { - return new Promise((resolve) => setTimeout(resolve, ms)); -} diff --git a/x-pack/test/cloud_security_posture_functional/config.ts b/x-pack/test/cloud_security_posture_functional/config.ts new file mode 100644 index 0000000000000..99aa5cf8893ea --- /dev/null +++ b/x-pack/test/cloud_security_posture_functional/config.ts @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { resolve } from 'path'; +import type { FtrConfigProviderContext } from '@kbn/test'; +import { pageObjects } from './page_objects'; +import { getPreConfiguredFleetPackages, getPreConfiguredAgentPolicies } from './helpers'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const xpackFunctionalConfig = await readConfigFile( + require.resolve('../functional/config.base.js') + ); + + return { + ...xpackFunctionalConfig.getAll(), + pageObjects, + testFiles: [resolve(__dirname, './pages')], + junit: { + reportName: 'X-Pack Cloud Security Posture Functional Tests', + }, + kbnTestServer: { + ...xpackFunctionalConfig.get('kbnTestServer'), + serverArgs: [ + ...xpackFunctionalConfig.get('kbnTestServer.serverArgs'), + ...getPreConfiguredFleetPackages(), + ...getPreConfiguredAgentPolicies(), + ], + }, + }; +} diff --git a/x-pack/test/cloud_security_posture_functional/ftr_provider_context.d.ts b/x-pack/test/cloud_security_posture_functional/ftr_provider_context.d.ts new file mode 100644 index 0000000000000..368a4b380602d --- /dev/null +++ b/x-pack/test/cloud_security_posture_functional/ftr_provider_context.d.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { GenericFtrProviderContext } from '@kbn/test'; + +import { pageObjects } from './page_objects'; +import { services } from '../functional/services'; + +export type FtrProviderContext = GenericFtrProviderContext; diff --git a/x-pack/test/cloud_security_posture_functional/helpers.ts b/x-pack/test/cloud_security_posture_functional/helpers.ts new file mode 100644 index 0000000000000..4c7f0ef633594 --- /dev/null +++ b/x-pack/test/cloud_security_posture_functional/helpers.ts @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +const CLOUD_SECURITY_POSTURE_PACKAGE_NAME = 'cloud_security_posture'; + +/** + * flags to load kibana with fleet pre-configured to have 'cloud_security_posture' integration installed + */ +export const getPreConfiguredFleetPackages = () => [ + `--xpack.fleet.packages.0.name=${CLOUD_SECURITY_POSTURE_PACKAGE_NAME}`, + `--xpack.fleet.packages.0.version=latest`, +]; + +/** + * flags to load kibana with pre-configured agent policy with a 'cloud_security_posture' package policy + */ +export const getPreConfiguredAgentPolicies = () => [ + `--xpack.fleet.agentPolicies.0.id=agent-policy-csp`, + `--xpack.fleet.agentPolicies.0.name=example-agent-policy-csp`, + `--xpack.fleet.agentPolicies.0.package_policies.0.id=integration-policy-csp`, + `--xpack.fleet.agentPolicies.0.package_policies.0.name=example-integration-csp`, + `--xpack.fleet.agentPolicies.0.package_policies.0.package.name=${CLOUD_SECURITY_POSTURE_PACKAGE_NAME}`, +]; diff --git a/x-pack/test/cloud_security_posture_functional/page_objects/findings_page.ts b/x-pack/test/cloud_security_posture_functional/page_objects/findings_page.ts new file mode 100644 index 0000000000000..5c3477e521176 --- /dev/null +++ b/x-pack/test/cloud_security_posture_functional/page_objects/findings_page.ts @@ -0,0 +1,176 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import type { FtrProviderContext } from '../ftr_provider_context'; + +// Defined in CSP plugin +const FINDINGS_INDEX = 'logs-cloud_security_posture.findings_latest-default'; + +export function FindingsPageProvider({ getService, getPageObjects }: FtrProviderContext) { + const testSubjects = getService('testSubjects'); + const PageObjects = getPageObjects(['common']); + const retry = getService('retry'); + const es = getService('es'); + const supertest = getService('supertest'); + const log = getService('log'); + + /** + * required before indexing findings + */ + const waitForPluginInitialized = (): Promise => + retry.try(async () => { + log.debug('Check CSP plugin is initialized'); + const response = await supertest + .get('/internal/cloud_security_posture/status?check=init') + .expect(200); + expect(response.body).to.eql({ isPluginInitialized: true }); + log.debug('CSP plugin is initialized'); + }); + + const index = { + remove: () => es.indices.delete({ index: FINDINGS_INDEX, ignore_unavailable: true }), + add: async (findingsMock: T[]) => { + await waitForPluginInitialized(); + await Promise.all( + findingsMock.map((finding) => + es.index({ + index: FINDINGS_INDEX, + body: finding, + }) + ) + ); + }, + }; + + const distributionBar = { + filterBy: async (type: 'passed' | 'failed') => + testSubjects.click(type === 'failed' ? 'distribution_bar_failed' : 'distribution_bar_passed'), + }; + + const table = { + getElement: () => testSubjects.find('findings_table'), + + getHeaders: async () => { + const element = await table.getElement(); + return await element.findAllByCssSelector('thead tr :is(th,td)'); + }, + + getColumnIndex: async (columnName: string) => { + const headers = await table.getHeaders(); + const texts = await Promise.all(headers.map((header) => header.getVisibleText())); + const columnIndex = texts.findIndex((i) => i === columnName); + expect(columnIndex).to.be.greaterThan(-1); + return columnIndex + 1; + }, + + getColumnHeaderCell: async (columnName: string) => { + const headers = await table.getHeaders(); + const headerIndexes = await Promise.all(headers.map((header) => header.getVisibleText())); + const columnIndex = headerIndexes.findIndex((i) => i === columnName); + return headers[columnIndex]; + }, + + getRowsCount: async () => { + const element = await table.getElement(); + const rows = await element.findAllByCssSelector('tbody tr'); + return rows.length; + }, + + getFindingsCount: async (type: 'passed' | 'failed') => { + const element = await table.getElement(); + const items = await element.findAllByCssSelector(`span[data-test-subj="${type}_finding"]`); + return items.length; + }, + + getRowIndexForValue: async (columnName: string, value: string) => { + const values = await table.getColumnValues(columnName); + const rowIndex = values.indexOf(value); + expect(rowIndex).to.be.greaterThan(-1); + return rowIndex + 1; + }, + + getFilterElementButton: async (rowIndex: number, columnIndex: number, negated = false) => { + const tableElement = await table.getElement(); + const button = negated + ? 'findings_table_cell_add_negated_filter' + : 'findings_table_cell_add_filter'; + const selector = `tbody tr:nth-child(${rowIndex}) td:nth-child(${columnIndex}) button[data-test-subj="${button}"]`; + return tableElement.findByCssSelector(selector); + }, + + addCellFilter: async (columnName: string, cellValue: string, negated = false) => { + const columnIndex = await table.getColumnIndex(columnName); + const rowIndex = await table.getRowIndexForValue(columnName, cellValue); + const filterElement = await table.getFilterElementButton(rowIndex, columnIndex, negated); + await filterElement.click(); + }, + + getColumnValues: async (columnName: string) => { + const tableElement = await table.getElement(); + const columnIndex = await table.getColumnIndex(columnName); + const columnCells = await tableElement.findAllByCssSelector( + `tbody tr td:nth-child(${columnIndex}) div[data-test-subj="filter_cell_value"]` + ); + + return await Promise.all(columnCells.map((cell) => cell.getVisibleText())); + }, + + hasColumnValue: async (columnName: string, value: string) => { + const values = await table.getColumnValues(columnName); + return values.includes(value); + }, + + assertColumnSort: async (columnName: string, direction: 'asc' | 'desc') => { + const values = (await table.getColumnValues(columnName)).filter(Boolean); + expect(values).to.not.be.empty(); + const sorted = values + .slice() + .sort((a, b) => (direction === 'asc' ? a.localeCompare(b) : b.localeCompare(a))); + values.forEach((value, i) => expect(value).to.be(sorted[i])); + }, + + toggleColumnSortOrFail: async (columnName: string, direction: 'asc' | 'desc') => { + const element = await table.getColumnHeaderCell(columnName); + const currentSort = await element.getAttribute('aria-sort'); + if (currentSort === 'none') { + // a click is needed to focus on Eui column header + await element.click(); + + // default is ascending + if (direction === 'desc') { + const nonStaleElement = await table.getColumnHeaderCell(columnName); + await nonStaleElement.click(); + } + } + if ( + (currentSort === 'ascending' && direction === 'desc') || + (currentSort === 'descending' && direction === 'asc') + ) { + // Without getting the element again, the click throws an error (stale element reference) + const nonStaleElement = await table.getColumnHeaderCell(columnName); + await nonStaleElement.click(); + } + await table.assertColumnSort(columnName, direction); + }, + }; + + const navigateToFindingsPage = async () => { + await PageObjects.common.navigateToUrl( + 'securitySolution', // Defined in Security Solution plugin + 'cloud_security_posture/findings', + { shouldUseHashForSubUrl: false } + ); + }; + + return { + navigateToFindingsPage, + table, + index, + distributionBar, + }; +} diff --git a/x-pack/test/cloud_security_posture_functional/page_objects/index.ts b/x-pack/test/cloud_security_posture_functional/page_objects/index.ts new file mode 100644 index 0000000000000..e5738873edc51 --- /dev/null +++ b/x-pack/test/cloud_security_posture_functional/page_objects/index.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { pageObjects as xpackFunctionalPageObjects } from '../../functional/page_objects'; +import { FindingsPageProvider } from './findings_page'; + +export const pageObjects = { + ...xpackFunctionalPageObjects, + findings: FindingsPageProvider, +}; diff --git a/x-pack/test/cloud_security_posture_functional/pages/findings.ts b/x-pack/test/cloud_security_posture_functional/pages/findings.ts new file mode 100644 index 0000000000000..a2114bac5d5be --- /dev/null +++ b/x-pack/test/cloud_security_posture_functional/pages/findings.ts @@ -0,0 +1,127 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import type { FtrProviderContext } from '../ftr_provider_context'; + +// eslint-disable-next-line import/no-default-export +export default function ({ getPageObjects, getService }: FtrProviderContext) { + const queryBar = getService('queryBar'); + const filterBar = getService('filterBar'); + const retry = getService('retry'); + const pageObjects = getPageObjects(['common', 'findings']); + + const data = Array.from({ length: 2 }, (_, id) => { + return { + resource: { id, name: `Resource ${id}` }, + result: { evaluation: id === 0 ? 'passed' : 'failed' }, + rule: { + name: `Rule ${id}`, + section: 'Kubelet', + tags: ['Kubernetes'], + type: 'process', + }, + }; + }); + + const ruleName1 = data[0].rule.name; + const ruleName2 = data[1].rule.name; + + describe('Findings Page', () => { + let findings: typeof pageObjects.findings; + let table: typeof pageObjects.findings.table; + let distributionBar: typeof pageObjects.findings.distributionBar; + + before(async () => { + findings = pageObjects.findings; + table = pageObjects.findings.table; + distributionBar = pageObjects.findings.distributionBar; + + await findings.index.add(data); + await findings.navigateToFindingsPage(); + await retry.waitFor( + 'Findings table to be loaded', + async () => (await table.getRowsCount()) === data.length + ); + }); + + after(async () => { + await findings.index.remove(); + }); + + describe('SearchBar', () => { + it('add filter', async () => { + await filterBar.addFilter('rule.name', 'is', ruleName1); + + expect(await filterBar.hasFilter('rule.name', ruleName1)).to.be(true); + expect(await table.hasColumnValue('Rule', ruleName1)).to.be(true); + }); + + it('remove filter', async () => { + await filterBar.removeFilter('rule.name'); + + expect(await filterBar.hasFilter('rule.name', ruleName1)).to.be(false); + expect(await table.getRowsCount()).to.be(data.length); + }); + + it('set search query', async () => { + await queryBar.setQuery(ruleName1); + await queryBar.submitQuery(); + + expect(await table.hasColumnValue('Rule', ruleName1)).to.be(true); + expect(await table.hasColumnValue('Rule', ruleName2)).to.be(false); + + await queryBar.setQuery(''); + await queryBar.submitQuery(); + + expect(await table.getRowsCount()).to.be(data.length); + }); + }); + + describe('Table Filters', () => { + it('add cell value filter', async () => { + await table.addCellFilter('Rule', ruleName1, false); + + expect(await filterBar.hasFilter('rule.name', ruleName1)).to.be(true); + expect(await table.hasColumnValue('Rule', ruleName1)).to.be(true); + }); + + it('add negated cell value filter', async () => { + await table.addCellFilter('Rule', ruleName1, true); + + expect(await filterBar.hasFilter('rule.name', ruleName1, true, false, true)).to.be(true); + expect(await table.hasColumnValue('Rule', ruleName1)).to.be(false); + expect(await table.hasColumnValue('Rule', ruleName2)).to.be(true); + + await filterBar.removeFilter('rule.name'); + }); + }); + + describe('Table Sort', () => { + it('sorts by rule name', async () => { + await table.toggleColumnSortOrFail('Rule', 'asc'); + }); + + it('sorts by resource name', async () => { + await table.toggleColumnSortOrFail('Resource Name', 'desc'); + }); + }); + + describe('DistributionBar', () => { + (['passed', 'failed'] as const).forEach((type) => { + it(`filters by ${type} findings`, async () => { + await distributionBar.filterBy(type); + + const items = data.filter(({ result }) => result.evaluation === type); + expect(await table.getFindingsCount(type)).to.eql(items.length); + + await filterBar.removeFilter('result.evaluation'); + }); + }); + }); + }); +} diff --git a/x-pack/test/cloud_security_posture_functional/pages/index.ts b/x-pack/test/cloud_security_posture_functional/pages/index.ts new file mode 100644 index 0000000000000..80e96b8b17ce9 --- /dev/null +++ b/x-pack/test/cloud_security_posture_functional/pages/index.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrProviderContext } from '../ftr_provider_context'; + +// eslint-disable-next-line import/no-default-export +export default function ({ loadTestFile }: FtrProviderContext) { + describe('Cloud Security Posture', function () { + loadTestFile(require.resolve('./findings')); + }); +} diff --git a/x-pack/test/detection_engine_api_integration/basic/tests/add_prepackaged_rules.ts b/x-pack/test/detection_engine_api_integration/basic/tests/add_prepackaged_rules.ts index 7f7d023fe2727..b9e4d3de355de 100644 --- a/x-pack/test/detection_engine_api_integration/basic/tests/add_prepackaged_rules.ts +++ b/x-pack/test/detection_engine_api_integration/basic/tests/add_prepackaged_rules.ts @@ -5,13 +5,8 @@ * 2.0. */ -import expect from '@kbn/expect'; -import { - PREBUILT_RULES_STATUS_URL, - PREBUILT_RULES_URL, - InstallPrebuiltRulesAndTimelinesResponse, -} from '@kbn/security-solution-plugin/common/detection_engine/prebuilt_rules'; - +import { PREBUILT_RULES_STATUS_URL } from '@kbn/security-solution-plugin/common/detection_engine/prebuilt_rules'; +import expect from 'expect'; import { FtrProviderContext } from '../../common/ftr_provider_context'; import { createSignalsIndex, @@ -42,35 +37,22 @@ export default ({ getService }: FtrProviderContext): void => { }); it('should create the prepackaged rules and return a count greater than zero, rules_updated to be zero, and contain the correct keys', async () => { - let responseBody: unknown; - await waitFor( - async () => { - const { body, status } = await supertest - .put(PREBUILT_RULES_URL) - .set('kbn-xsrf', 'true') - .send(); - if (status === 200) { - responseBody = body; - } - return status === 200; - }, - PREBUILT_RULES_URL, - log - ); + const response = await installPrePackagedRules(supertest, es, log); - const prepackagedRules = responseBody as InstallPrebuiltRulesAndTimelinesResponse; - expect(prepackagedRules.rules_installed).to.be.greaterThan(0); - expect(prepackagedRules.rules_updated).to.eql(0); - expect(Object.keys(prepackagedRules)).to.eql([ - 'rules_installed', - 'rules_updated', - 'timelines_installed', - 'timelines_updated', - ]); + expect(response?.rules_installed).toBeGreaterThan(0); + expect(response?.rules_updated).toBe(0); + expect(response).toEqual( + expect.objectContaining({ + rules_installed: expect.any(Number), + rules_updated: expect.any(Number), + timelines_installed: expect.any(Number), + timelines_updated: expect.any(Number), + }) + ); }); it('should be possible to call the API twice and the second time the number of rules installed should be zero as well as timeline', async () => { - await installPrePackagedRules(supertest, log); + await installPrePackagedRules(supertest, es, log); // NOTE: I call the GET call until eventually it becomes consistent and that the number of rules to install are zero. // This is to reduce flakiness where it can for a short period of time try to install the same rule twice. @@ -86,25 +68,10 @@ export default ({ getService }: FtrProviderContext): void => { log ); - let responseBody: unknown; - await waitFor( - async () => { - const { body, status } = await supertest - .put(PREBUILT_RULES_URL) - .set('kbn-xsrf', 'true') - .send(); - if (status === 200) { - responseBody = body; - } - return status === 200; - }, - PREBUILT_RULES_URL, - log - ); + const response = await installPrePackagedRules(supertest, es, log); - const prepackagedRules = responseBody as InstallPrebuiltRulesAndTimelinesResponse; - expect(prepackagedRules.rules_installed).to.eql(0); - expect(prepackagedRules.timelines_installed).to.eql(0); + expect(response?.rules_installed).toBe(0); + expect(response?.timelines_installed).toBe(0); }); }); }); diff --git a/x-pack/test/detection_engine_api_integration/basic/tests/get_prepackaged_rules_status.ts b/x-pack/test/detection_engine_api_integration/basic/tests/get_prepackaged_rules_status.ts index ce75086a2deea..244d8d677ae14 100644 --- a/x-pack/test/detection_engine_api_integration/basic/tests/get_prepackaged_rules_status.ts +++ b/x-pack/test/detection_engine_api_integration/basic/tests/get_prepackaged_rules_status.ts @@ -21,6 +21,8 @@ import { getSimpleRule, deleteAllTimelines, } from '../../utils'; +import { createPrebuiltRuleAssetSavedObjects } from '../../utils/create_prebuilt_rule_saved_objects'; +import { deleteAllPrebuiltRules } from '../../utils/delete_all_prebuilt_rules'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { @@ -32,12 +34,14 @@ export default ({ getService }: FtrProviderContext): void => { describe('getting prepackaged rules status', () => { beforeEach(async () => { await createSignalsIndex(supertest, log); + await createPrebuiltRuleAssetSavedObjects(es); }); afterEach(async () => { await deleteSignalsIndex(supertest, log); await deleteAllAlerts(supertest, log); await deleteAllTimelines(es); + await deleteAllPrebuiltRules(es); }); it('should return expected JSON keys of the pre-packaged rules and pre-packaged timelines status', async () => { diff --git a/x-pack/test/detection_engine_api_integration/basic/tests/index.ts b/x-pack/test/detection_engine_api_integration/basic/tests/index.ts index 349d33c89fdf3..c70d7520dd425 100644 --- a/x-pack/test/detection_engine_api_integration/basic/tests/index.ts +++ b/x-pack/test/detection_engine_api_integration/basic/tests/index.ts @@ -27,6 +27,5 @@ export default ({ loadTestFile }: FtrProviderContext): void => { loadTestFile(require.resolve('./query_signals')); loadTestFile(require.resolve('./open_close_signals')); loadTestFile(require.resolve('./import_timelines')); - loadTestFile(require.resolve('./update_rac_alerts')); }); }; diff --git a/x-pack/test/detection_engine_api_integration/basic/tests/update_rac_alerts.ts b/x-pack/test/detection_engine_api_integration/basic/tests/update_rac_alerts.ts deleted file mode 100644 index 983fdc4c8a285..0000000000000 --- a/x-pack/test/detection_engine_api_integration/basic/tests/update_rac_alerts.ts +++ /dev/null @@ -1,159 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import expect from '@kbn/expect'; -import { ALERT_WORKFLOW_STATUS } from '@kbn/rule-data-utils'; - -import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; -import { DETECTION_ENGINE_QUERY_SIGNALS_URL } from '@kbn/security-solution-plugin/common/constants'; -import { RAC_ALERTS_BULK_UPDATE_URL } from '@kbn/timelines-plugin/common/constants'; -import { DetectionAlert } from '@kbn/security-solution-plugin/common/detection_engine/schemas/alerts'; -import { FtrProviderContext } from '../../common/ftr_provider_context'; -import { - createSignalsIndex, - deleteSignalsIndex, - getQuerySignalIds, - deleteAllAlerts, - createRule, - waitForSignalsToBePresent, - getSignalsByIds, - waitForRuleSuccessOrStatus, - getRuleForSignalTesting, -} from '../../utils'; - -// eslint-disable-next-line import/no-default-export -export default ({ getService }: FtrProviderContext) => { - const supertest = getService('supertest'); - const esArchiver = getService('esArchiver'); - const log = getService('log'); - - describe('open_close_signals', () => { - describe('tests with auditbeat data', () => { - before(async () => { - await esArchiver.load('x-pack/test/functional/es_archives/auditbeat/hosts'); - }); - after(async () => { - await esArchiver.unload('x-pack/test/functional/es_archives/auditbeat/hosts'); - }); - beforeEach(async () => { - await deleteAllAlerts(supertest, log); - await createSignalsIndex(supertest, log); - }); - afterEach(async () => { - await deleteSignalsIndex(supertest, log); - await deleteAllAlerts(supertest, log); - }); - - it('should be able to execute and get 10 signals', async () => { - const rule = getRuleForSignalTesting(['auditbeat-*']); - const { id } = await createRule(supertest, log, rule); - await waitForRuleSuccessOrStatus(supertest, log, id); - await waitForSignalsToBePresent(supertest, log, 10, [id]); - const signalsOpen = await getSignalsByIds(supertest, log, [id]); - expect(signalsOpen.hits.hits.length).equal(10); - }); - - it('should be have set the signals in an open state initially', async () => { - const rule = getRuleForSignalTesting(['auditbeat-*']); - const { id } = await createRule(supertest, log, rule); - await waitForRuleSuccessOrStatus(supertest, log, id); - await waitForSignalsToBePresent(supertest, log, 10, [id]); - const signalsOpen = await getSignalsByIds(supertest, log, [id]); - const everySignalOpen = signalsOpen.hits.hits.every( - (hit) => hit._source?.[ALERT_WORKFLOW_STATUS] === 'open' - ); - expect(everySignalOpen).to.eql(true); - }); - - it('should be able to get a count of 10 closed signals when closing 10', async () => { - const rule = getRuleForSignalTesting(['auditbeat-*']); - const { id } = await createRule(supertest, log, rule); - await waitForRuleSuccessOrStatus(supertest, log, id); - await waitForSignalsToBePresent(supertest, log, 10, [id]); - const signalsOpen = await getSignalsByIds(supertest, log, [id]); - const signalIds = signalsOpen.hits.hits.map((signal) => signal._id); - - // set all of the signals to the state of closed. There is no reason to use a waitUntil here - // as this route intentionally has a waitFor within it and should only return when the query has - // the data. - await supertest - .post(RAC_ALERTS_BULK_UPDATE_URL) - .set('kbn-xsrf', 'true') - .send({ ids: signalIds, status: 'closed', index: '.siem-signals-default' }) - .expect(200); - - const { body: signalsClosed }: { body: estypes.SearchResponse } = - await supertest - .post(DETECTION_ENGINE_QUERY_SIGNALS_URL) - .set('kbn-xsrf', 'true') - .send(getQuerySignalIds(signalIds)) - .expect(200); - expect(signalsClosed.hits.hits.length).to.equal(10); - }); - - it('should be able close 10 signals immediately and they all should be closed', async () => { - const rule = getRuleForSignalTesting(['auditbeat-*']); - const { id } = await createRule(supertest, log, rule); - await waitForRuleSuccessOrStatus(supertest, log, id); - await waitForSignalsToBePresent(supertest, log, 10, [id]); - const signalsOpen = await getSignalsByIds(supertest, log, [id]); - const signalIds = signalsOpen.hits.hits.map((signal) => signal._id); - - // set all of the signals to the state of closed. There is no reason to use a waitUntil here - // as this route intentionally has a waitFor within it and should only return when the query has - // the data. - await supertest - .post(RAC_ALERTS_BULK_UPDATE_URL) - .set('kbn-xsrf', 'true') - .send({ ids: signalIds, status: 'closed', index: '.siem-signals-default' }) - .expect(200); - - const { body: signalsClosed }: { body: estypes.SearchResponse } = - await supertest - .post(DETECTION_ENGINE_QUERY_SIGNALS_URL) - .set('kbn-xsrf', 'true') - .send(getQuerySignalIds(signalIds)) - .expect(200); - - const everySignalClosed = signalsClosed.hits.hits.every( - (hit) => hit._source?.['kibana.alert.workflow_status'] === 'closed' - ); - expect(everySignalClosed).to.eql(true); - }); - - it('should be able mark 10 signals as acknowledged immediately and they all should be acknowledged', async () => { - const rule = getRuleForSignalTesting(['auditbeat-*']); - const { id } = await createRule(supertest, log, rule); - await waitForRuleSuccessOrStatus(supertest, log, id); - await waitForSignalsToBePresent(supertest, log, 10, [id]); - const signalsOpen = await getSignalsByIds(supertest, log, [id]); - const signalIds = signalsOpen.hits.hits.map((signal) => signal._id); - - // set all of the signals to the state of acknowledged. There is no reason to use a waitUntil here - // as this route intentionally has a waitFor within it and should only return when the query has - // the data. - await supertest - .post(RAC_ALERTS_BULK_UPDATE_URL) - .set('kbn-xsrf', 'true') - .send({ ids: signalIds, status: 'acknowledged', index: '.siem-signals-default' }) - .expect(200); - - const { body: acknowledgedSignals }: { body: estypes.SearchResponse } = - await supertest - .post(DETECTION_ENGINE_QUERY_SIGNALS_URL) - .set('kbn-xsrf', 'true') - .send(getQuerySignalIds(signalIds)) - .expect(200); - - const everyAcknowledgedSignal = acknowledgedSignals.hits.hits.every( - (hit) => hit._source?.['kibana.alert.workflow_status'] === 'acknowledged' - ); - expect(everyAcknowledgedSignal).to.eql(true); - }); - }); - }); -}; diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/add_prepackaged_rules.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/add_prepackaged_rules.ts index f90b1f8c8949b..9b2d593ff8c56 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/add_prepackaged_rules.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/add_prepackaged_rules.ts @@ -21,6 +21,8 @@ import { installPrePackagedRules, waitFor, } from '../../utils'; +import { createPrebuiltRuleAssetSavedObjects } from '../../utils/create_prebuilt_rule_saved_objects'; +import { deleteAllPrebuiltRules } from '../../utils/delete_all_prebuilt_rules'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { @@ -32,12 +34,14 @@ export default ({ getService }: FtrProviderContext): void => { describe('creating prepackaged rules', () => { beforeEach(async () => { await createSignalsIndex(supertest, log); + await createPrebuiltRuleAssetSavedObjects(es); }); afterEach(async () => { await deleteSignalsIndex(supertest, log); await deleteAllAlerts(supertest, log); await deleteAllTimelines(es); + await deleteAllPrebuiltRules(es); }); it('should create the prepackaged rules and return a count greater than zero, rules_updated to be zero, and contain the correct keys', async () => { @@ -69,7 +73,7 @@ export default ({ getService }: FtrProviderContext): void => { }); it('should be possible to call the API twice and the second time the number of rules installed should be zero as well as timeline', async () => { - await installPrePackagedRules(supertest, log); + await installPrePackagedRules(supertest, es, log); // NOTE: I call the GET call until eventually it becomes consistent and that the number of rules to install are zero. // This is to reduce flakiness where it can for a short period of time try to install the same rule twice. diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/create_rules.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/create_rules.ts index a1da5d5697b1e..6ddbe82e7d5cf 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/create_rules.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/create_rules.ts @@ -272,41 +272,114 @@ export default ({ getService }: FtrProviderContext) => { }); }); - it('should not create a rule if trying to add more than one default rule exception list', async () => { - const rule: RuleCreateProps = { - name: 'Simple Rule Query', - description: 'Simple Rule Query', - enabled: true, - risk_score: 1, - rule_id: 'rule-1', - severity: 'high', - type: 'query', - query: 'user.name: root or user.name: admin', - exceptions_list: [ - { - id: '2', - list_id: '123', - namespace_type: 'single', - type: ExceptionListTypeEnum.RULE_DEFAULT, - }, - { - id: '1', - list_id: '456', - namespace_type: 'single', - type: ExceptionListTypeEnum.RULE_DEFAULT, - }, - ], - }; + describe('exception', () => { + it('should not create a rule if trying to add more than one default rule exception list', async () => { + const rule: RuleCreateProps = { + name: 'Simple Rule Query', + description: 'Simple Rule Query', + enabled: true, + risk_score: 1, + rule_id: 'rule-1', + severity: 'high', + type: 'query', + query: 'user.name: root or user.name: admin', + exceptions_list: [ + { + id: '2', + list_id: '123', + namespace_type: 'single', + type: ExceptionListTypeEnum.RULE_DEFAULT, + }, + { + id: '1', + list_id: '456', + namespace_type: 'single', + type: ExceptionListTypeEnum.RULE_DEFAULT, + }, + ], + }; - const { body } = await supertest - .post(DETECTION_ENGINE_RULES_URL) - .set('kbn-xsrf', 'true') - .send(rule) - .expect(500); + const { body } = await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(rule) + .expect(500); + + expect(body).to.eql({ + message: 'More than one default exception list found on rule', + status_code: 500, + }); + }); + + it('should not create a rule if trying to add default rule exception list which attached to another', async () => { + const rule: RuleCreateProps = { + name: 'Simple Rule Query', + description: 'Simple Rule Query', + enabled: true, + risk_score: 1, + rule_id: 'rule-1', + severity: 'high', + type: 'query', + query: 'user.name: root or user.name: admin', + exceptions_list: [ + { + id: '2', + list_id: '123', + namespace_type: 'single', + type: ExceptionListTypeEnum.RULE_DEFAULT, + }, + ], + }; + + const { body: ruleWithException } = await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(rule) + .expect(200); + + const { body } = await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send({ ...rule, rule_id: 'rule-2' }) + .expect(409); + + expect(body).to.eql({ + message: `default exception list already exists in rule(s): ${ruleWithException.id}`, + status_code: 409, + }); + }); + + it('allow to create a rule if trying to add shared rule exception list which attached to another', async () => { + const rule: RuleCreateProps = { + name: 'Simple Rule Query', + description: 'Simple Rule Query', + enabled: true, + risk_score: 1, + rule_id: 'rule-1', + severity: 'high', + type: 'query', + query: 'user.name: root or user.name: admin', + exceptions_list: [ + { + id: '2', + list_id: '123', + namespace_type: 'single', + type: ExceptionListTypeEnum.DETECTION, + }, + ], + }; + + await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(rule) + .expect(200); - expect(body).to.eql({ - message: 'More than one default exception list found on rule', - status_code: 500, + await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send({ ...rule, rule_id: 'rule-2' }) + .expect(200); }); }); diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/create_rules_bulk.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/create_rules_bulk.ts index da5d54fdf2d7a..aa1cb11642255 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/create_rules_bulk.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/create_rules_bulk.ts @@ -7,7 +7,12 @@ import expect from '@kbn/expect'; -import { DETECTION_ENGINE_RULES_BULK_CREATE } from '@kbn/security-solution-plugin/common/constants'; +import { + DETECTION_ENGINE_RULES_BULK_CREATE, + DETECTION_ENGINE_RULES_URL, +} from '@kbn/security-solution-plugin/common/constants'; +import { ExceptionListTypeEnum } from '@kbn/securitysolution-io-ts-list-types'; + import { FtrProviderContext } from '../../common/ftr_provider_context'; import { createSignalsIndex, @@ -159,6 +164,118 @@ export default ({ getService }: FtrProviderContext): void => { }, ]); }); + + it('should return a 200 ok but have a 409 conflict if we attempt to create the rule, which use existing attached rule defult list', async () => { + const { body: ruleWithException } = await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send({ + ...getSimpleRuleWithoutRuleId(), + exceptions_list: [ + { + id: '2', + list_id: '123', + namespace_type: 'single', + type: ExceptionListTypeEnum.RULE_DEFAULT, + }, + ], + }) + .expect(200); + + const { body } = await supertest + .post(DETECTION_ENGINE_RULES_BULK_CREATE) + .set('kbn-xsrf', 'true') + .send([ + { + ...getSimpleRule(), + exceptions_list: [ + { + id: '2', + list_id: '123', + namespace_type: 'single', + type: ExceptionListTypeEnum.RULE_DEFAULT, + }, + ], + }, + ]) + .expect(200); + + expect(body).to.eql([ + { + error: { + message: `default exception list for rule: rule-1 already exists in rule(s): ${ruleWithException.id}`, + status_code: 409, + }, + rule_id: 'rule-1', + }, + ]); + }); + + it('should return a 409 if several rules has the same exception rule default list', async () => { + const { body } = await supertest + .post(DETECTION_ENGINE_RULES_BULK_CREATE) + .set('kbn-xsrf', 'true') + .send([ + { + ...getSimpleRule(), + exceptions_list: [ + { + id: '2', + list_id: '123', + namespace_type: 'single', + type: ExceptionListTypeEnum.RULE_DEFAULT, + }, + ], + }, + { + ...getSimpleRule('rule-2'), + exceptions_list: [ + { + id: '2', + list_id: '123', + namespace_type: 'single', + type: ExceptionListTypeEnum.RULE_DEFAULT, + }, + ], + }, + { + ...getSimpleRuleWithoutRuleId(), + exceptions_list: [ + { + id: '2', + list_id: '123', + namespace_type: 'single', + type: ExceptionListTypeEnum.RULE_DEFAULT, + }, + ], + }, + ]) + .expect(200); + + expect(body).to.eql([ + { + error: { + message: 'default exceptions list 2 for rule rule-1 is duplicated', + status_code: 409, + }, + rule_id: 'rule-1', + }, + { + error: { + message: 'default exceptions list 2 for rule rule-2 is duplicated', + status_code: 409, + }, + rule_id: 'rule-2', + }, + { + error: { + message: 'default exceptions list 2 is duplicated', + status_code: 409, + }, + rule_id: '(unknown id)', + }, + ]); + }); }); }); }; diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/get_prepackaged_rules_status.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/get_prepackaged_rules_status.ts index ce75086a2deea..244d8d677ae14 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/get_prepackaged_rules_status.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/get_prepackaged_rules_status.ts @@ -21,6 +21,8 @@ import { getSimpleRule, deleteAllTimelines, } from '../../utils'; +import { createPrebuiltRuleAssetSavedObjects } from '../../utils/create_prebuilt_rule_saved_objects'; +import { deleteAllPrebuiltRules } from '../../utils/delete_all_prebuilt_rules'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { @@ -32,12 +34,14 @@ export default ({ getService }: FtrProviderContext): void => { describe('getting prepackaged rules status', () => { beforeEach(async () => { await createSignalsIndex(supertest, log); + await createPrebuiltRuleAssetSavedObjects(es); }); afterEach(async () => { await deleteSignalsIndex(supertest, log); await deleteAllAlerts(supertest, log); await deleteAllTimelines(es); + await deleteAllPrebuiltRules(es); }); it('should return expected JSON keys of the pre-packaged rules and pre-packaged timelines status', async () => { diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/update_actions.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/update_actions.ts index 11ad72b505f1f..4897805e09eb2 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/update_actions.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/update_actions.ts @@ -29,20 +29,18 @@ import { getSimpleRuleOutput, ruleToUpdateSchema, } from '../../utils'; - -// Rule id of "9a1a2dae-0b5f-4c3d-8305-a268d404c306" is from the file: -// x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/content/prepackaged_rules/elastic_endpoint_security.json -const RULE_ID = '9a1a2dae-0b5f-4c3d-8305-a268d404c306'; +import { ELASTIC_SECURITY_RULE_ID } from '../../utils/create_prebuilt_rule_saved_objects'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext) => { + const es = getService('es'); const supertest = getService('supertest'); const esArchiver = getService('esArchiver'); const log = getService('log'); const getImmutableRule = async () => { - await installPrePackagedRules(supertest, log); - return getRule(supertest, log, RULE_ID); + await installPrePackagedRules(supertest, es, log); + return getRule(supertest, log, ELASTIC_SECURITY_RULE_ID); }; describe('update_actions', () => { @@ -178,7 +176,7 @@ export default ({ getService }: FtrProviderContext) => { ruleToUpdateSchema(immutableRule) ); await updateRule(supertest, log, ruleToUpdate); - const body = await findImmutableRuleById(supertest, log, RULE_ID); + const body = await findImmutableRuleById(supertest, log, ELASTIC_SECURITY_RULE_ID); expect(body.data.length).to.eql(1); // should have only one length to the data set, otherwise we have duplicates or the tags were removed and that is incredibly bad. const bodyToCompare = removeServerGeneratedProperties(body.data[0]); diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/patch_rules.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/patch_rules.ts index 030e92abf1f73..2ec13365eb1d1 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/patch_rules.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/patch_rules.ts @@ -264,6 +264,78 @@ export default ({ getService }: FtrProviderContext) => { }); }); + it('should not patch a rule if trying to add default rule exception list which attached to another', async () => { + const ruleWithException = await createRule(supertest, log, { + ...getSimpleRule('rule-1'), + exceptions_list: [ + { + id: '2', + list_id: '123', + namespace_type: 'single', + type: ExceptionListTypeEnum.RULE_DEFAULT, + }, + ], + }); + await createRule(supertest, log, getSimpleRule('rule-2')); + + const { body } = await supertest + .patch(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send({ + rule_id: 'rule-2', + exceptions_list: [ + { + id: '2', + list_id: '123', + namespace_type: 'single', + type: ExceptionListTypeEnum.RULE_DEFAULT, + }, + ], + }) + .expect(409); + + expect(body).to.eql({ + message: `default exception list for rule: rule-2 already exists in rule(s): ${ruleWithException.id}`, + status_code: 409, + }); + }); + + it('should not update a rule if trying to add default rule exception list which attached to another using rule.id', async () => { + const ruleWithException = await createRule(supertest, log, { + ...getSimpleRule('rule-1'), + exceptions_list: [ + { + id: '2', + list_id: '123', + namespace_type: 'single', + type: ExceptionListTypeEnum.RULE_DEFAULT, + }, + ], + }); + const createdBody = await createRule(supertest, log, getSimpleRule('rule-2')); + + const { body } = await supertest + .patch(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send({ + id: createdBody.id, + exceptions_list: [ + { + id: '2', + list_id: '123', + namespace_type: 'single', + type: ExceptionListTypeEnum.RULE_DEFAULT, + }, + ], + }) + .expect(409); + + expect(body).to.eql({ + message: `default exception list for rule: ${createdBody.id} already exists in rule(s): ${ruleWithException.id}`, + status_code: 409, + }); + }); + it('should return the rule with migrated actions after the enable patch', async () => { const [connector, rule] = await Promise.all([ supertest diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/patch_rules_bulk.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/patch_rules_bulk.ts index 41f8066e50779..b844e9500e126 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/patch_rules_bulk.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/patch_rules_bulk.ts @@ -8,6 +8,8 @@ import expect from '@kbn/expect'; import { DETECTION_ENGINE_RULES_BULK_UPDATE } from '@kbn/security-solution-plugin/common/constants'; +import { ExceptionListTypeEnum } from '@kbn/securitysolution-io-ts-list-types'; + import { FtrProviderContext } from '../../common/ftr_provider_context'; import { createSignalsIndex, @@ -369,6 +371,100 @@ export default ({ getService }: FtrProviderContext) => { }, ]); }); + + it('should return a 200 ok but have a 409 conflict if we attempt to patch the rule, which use existing attached rule defult list', async () => { + await createRule(supertest, log, getSimpleRule('rule-1')); + const ruleWithException = await createRule(supertest, log, { + ...getSimpleRule('rule-2'), + exceptions_list: [ + { + id: '2', + list_id: '123', + namespace_type: 'single', + type: ExceptionListTypeEnum.RULE_DEFAULT, + }, + ], + }); + + const { body } = await supertest + .patch(DETECTION_ENGINE_RULES_BULK_UPDATE) + .set('kbn-xsrf', 'true') + .send([ + { + rule_id: 'rule-1', + exceptions_list: [ + { + id: '2', + list_id: '123', + namespace_type: 'single', + type: ExceptionListTypeEnum.RULE_DEFAULT, + }, + ], + }, + ]) + .expect(200); + + expect(body).to.eql([ + { + error: { + message: `default exception list for rule: rule-1 already exists in rule(s): ${ruleWithException.id}`, + status_code: 409, + }, + rule_id: 'rule-1', + }, + ]); + }); + + it('should return a 409 if several rules has the same exception rule default list', async () => { + await createRule(supertest, log, getSimpleRule('rule-1')); + await createRule(supertest, log, getSimpleRule('rule-2')); + + const { body } = await supertest + .patch(DETECTION_ENGINE_RULES_BULK_UPDATE) + .set('kbn-xsrf', 'true') + .send([ + { + rule_id: 'rule-1', + exceptions_list: [ + { + id: '2', + list_id: '123', + namespace_type: 'single', + type: ExceptionListTypeEnum.RULE_DEFAULT, + }, + ], + }, + { + rule_id: 'rule-2', + exceptions_list: [ + { + id: '2', + list_id: '123', + namespace_type: 'single', + type: ExceptionListTypeEnum.RULE_DEFAULT, + }, + ], + }, + ]) + .expect(200); + + expect(body).to.eql([ + { + error: { + message: 'default exceptions list 2 for rule rule-1 is duplicated', + status_code: 409, + }, + rule_id: 'rule-1', + }, + { + error: { + message: 'default exceptions list 2 for rule rule-2 is duplicated', + status_code: 409, + }, + rule_id: 'rule-2', + }, + ]); + }); }); }); }; diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/perform_bulk_action.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/perform_bulk_action.ts index 5a9777e7f2e79..48117cc08ecf6 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/perform_bulk_action.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/perform_bulk_action.ts @@ -1062,7 +1062,7 @@ export default ({ getService }: FtrProviderContext): void => { ]; cases.forEach(({ type, value }) => { it(`should return error when trying to apply "${type}" edit action to prebuilt rule`, async () => { - await installPrePackagedRules(supertest, log); + await installPrePackagedRules(supertest, es, log); const prebuiltRule = await fetchPrebuiltRule(); const { body } = await postBulkAction() @@ -1523,7 +1523,7 @@ export default ({ getService }: FtrProviderContext): void => { ]; cases.forEach(({ type }) => { it(`should apply "${type}" rule action to prebuilt rule`, async () => { - await installPrePackagedRules(supertest, log); + await installPrePackagedRules(supertest, es, log); const prebuiltRule = await fetchPrebuiltRule(); const webHookConnector = await createWebHookConnector(); @@ -1577,7 +1577,7 @@ export default ({ getService }: FtrProviderContext): void => { // if rule action is applied together with another edit action, that can't be applied to prebuilt rule (for example: tags action) // bulk edit request should return error it(`should return error if one of edit action is not eligible for prebuilt rule`, async () => { - await installPrePackagedRules(supertest, log); + await installPrePackagedRules(supertest, es, log); const prebuiltRule = await fetchPrebuiltRule(); const webHookConnector = await createWebHookConnector(); diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/perform_bulk_action_dry_run.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/perform_bulk_action_dry_run.ts index 741839e707ea9..7914edf10247a 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/perform_bulk_action_dry_run.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/perform_bulk_action_dry_run.ts @@ -26,6 +26,7 @@ import { // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { + const es = getService('es'); const supertest = getService('supertest'); const log = getService('log'); @@ -162,7 +163,7 @@ export default ({ getService }: FtrProviderContext): void => { }); it('should validate immutable rule edit', async () => { - await installPrePackagedRules(supertest, log); + await installPrePackagedRules(supertest, es, log); const { body: findBody } = await findRules() .query({ per_page: 1, filter: 'alert.attributes.params.immutable: true' }) .send() diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/update_rules.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/update_rules.ts index 705714ec3ba50..a33aacd26bb8a 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/update_rules.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/update_rules.ts @@ -428,6 +428,83 @@ export default ({ getService }: FtrProviderContext) => { }); }); + it('should not update a rule if trying to add default rule exception list which attached to another', async () => { + const ruleWithException = await createRule(supertest, log, { + ...getSimpleRule('rule-1'), + exceptions_list: [ + { + id: '2', + list_id: '123', + namespace_type: 'single', + type: ExceptionListTypeEnum.RULE_DEFAULT, + }, + ], + }); + await createRule(supertest, log, getSimpleRule('rule-2')); + + const { body } = await supertest + .put(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send({ + ...getSimpleRule('rule-2'), + exceptions_list: [ + { + id: '2', + list_id: '123', + namespace_type: 'single', + type: ExceptionListTypeEnum.RULE_DEFAULT, + }, + ], + }) + .expect(409); + + expect(body).to.eql({ + message: `default exception list for rule: rule-2 already exists in rule(s): ${ruleWithException.id}`, + status_code: 409, + }); + }); + + it('should not update a rule if trying to add default rule exception list which attached to another using rule.id', async () => { + const ruleWithException = await createRule(supertest, log, { + ...getSimpleRule('rule-1'), + exceptions_list: [ + { + id: '2', + list_id: '123', + namespace_type: 'single', + type: ExceptionListTypeEnum.RULE_DEFAULT, + }, + ], + }); + const createdBody = await createRule(supertest, log, getSimpleRule('rule-2')); + + // update a simple rule's name + const updatedRule = getSimpleRuleUpdate('rule-2'); + updatedRule.id = createdBody.id; + delete updatedRule.rule_id; + + const { body } = await supertest + .put(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send({ + ...updatedRule, + exceptions_list: [ + { + id: '2', + list_id: '123', + namespace_type: 'single', + type: ExceptionListTypeEnum.RULE_DEFAULT, + }, + ], + }) + .expect(409); + + expect(body).to.eql({ + message: `default exception list for rule: ${updatedRule.id} already exists in rule(s): ${ruleWithException.id}`, + status_code: 409, + }); + }); + describe('threshold validation', () => { it('should result in 400 error if no threshold-specific fields are provided', async () => { const existingRule = getThresholdRuleForSignalTesting(['*']); diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/update_rules_bulk.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/update_rules_bulk.ts index a3f6812f6a0b0..04f3eb2536a41 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/update_rules_bulk.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/update_rules_bulk.ts @@ -12,6 +12,7 @@ import { DETECTION_ENGINE_RULES_URL, DETECTION_ENGINE_RULES_BULK_UPDATE, } from '@kbn/security-solution-plugin/common/constants'; +import { ExceptionListTypeEnum } from '@kbn/securitysolution-io-ts-list-types'; import { FtrProviderContext } from '../../common/ftr_provider_context'; import { createSignalsIndex, @@ -483,6 +484,109 @@ export default ({ getService }: FtrProviderContext) => { }, ]); }); + + it('should return a 200 ok but have a 409 conflict if we attempt to update the rule, which use existing attached rule defult list', async () => { + await createRule(supertest, log, getSimpleRule('rule-1')); + const ruleWithException = await createRule(supertest, log, { + ...getSimpleRule('rule-2'), + exceptions_list: [ + { + id: '2', + list_id: '123', + namespace_type: 'single', + type: ExceptionListTypeEnum.RULE_DEFAULT, + }, + ], + }); + + const rule1 = getSimpleRuleUpdate('rule-1'); + rule1.name = 'some other name'; + + const { body } = await supertest + .put(DETECTION_ENGINE_RULES_BULK_UPDATE) + .set('kbn-xsrf', 'true') + .send([ + { + ...rule1, + exceptions_list: [ + { + id: '2', + list_id: '123', + namespace_type: 'single', + type: ExceptionListTypeEnum.RULE_DEFAULT, + }, + ], + }, + ]) + .expect(200); + + expect(body).to.eql([ + { + error: { + message: `default exception list for rule: rule-1 already exists in rule(s): ${ruleWithException.id}`, + status_code: 409, + }, + rule_id: 'rule-1', + }, + ]); + }); + + it('should return a 409 if several rules has the same exception rule default list', async () => { + await createRule(supertest, log, getSimpleRule('rule-1')); + await createRule(supertest, log, getSimpleRule('rule-2')); + + const rule1 = getSimpleRuleUpdate('rule-1'); + rule1.name = 'some other name'; + + const rule2 = getSimpleRuleUpdate('rule-2'); + rule2.name = 'some other name'; + + const { body } = await supertest + .put(DETECTION_ENGINE_RULES_BULK_UPDATE) + .set('kbn-xsrf', 'true') + .send([ + { + ...rule1, + exceptions_list: [ + { + id: '2', + list_id: '123', + namespace_type: 'single', + type: ExceptionListTypeEnum.RULE_DEFAULT, + }, + ], + }, + { + ...rule2, + exceptions_list: [ + { + id: '2', + list_id: '123', + namespace_type: 'single', + type: ExceptionListTypeEnum.RULE_DEFAULT, + }, + ], + }, + ]) + .expect(200); + + expect(body).to.eql([ + { + error: { + message: 'default exceptions list 2 for rule rule-1 is duplicated', + status_code: 409, + }, + rule_id: 'rule-1', + }, + { + error: { + message: 'default exceptions list 2 for rule rule-2 is duplicated', + status_code: 409, + }, + rule_id: 'rule-2', + }, + ]); + }); }); }); }; diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/group3/create_exceptions.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group3/create_exceptions.ts index f6ea0fc02747b..af653c9740afd 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/group3/create_exceptions.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/group3/create_exceptions.ts @@ -53,6 +53,10 @@ import { importFile, } from '../../../lists_api_integration/utils'; import { createUserAndRole, deleteUserAndRole } from '../../../common/services/security_solution'; +import { + ELASTIC_SECURITY_RULE_ID, + SAMPLE_PREBUILT_RULES, +} from '../../utils/create_prebuilt_rule_saved_objects'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext) => { @@ -162,35 +166,25 @@ export default ({ getService }: FtrProviderContext) => { }); it('should allow removing an exception list from an immutable rule through patch', async () => { - await installPrePackagedRules(supertest, log); + await installPrePackagedRules(supertest, es, log); - // Rule id of "9a1a2dae-0b5f-4c3d-8305-a268d404c306" is from the file: - // x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/content/prepackaged_rules/elastic_endpoint.json // This rule has an existing exceptions_list that we are going to use - const immutableRule = await getRule( - supertest, - log, - '9a1a2dae-0b5f-4c3d-8305-a268d404c306' - ); + const immutableRule = await getRule(supertest, log, ELASTIC_SECURITY_RULE_ID); expect(immutableRule.exceptions_list.length).greaterThan(0); // make sure we have at least one exceptions_list // remove the exceptions list as a user is allowed to remove it from an immutable rule await supertest .patch(DETECTION_ENGINE_RULES_URL) .set('kbn-xsrf', 'true') - .send({ rule_id: '9a1a2dae-0b5f-4c3d-8305-a268d404c306', exceptions_list: [] }) + .send({ rule_id: ELASTIC_SECURITY_RULE_ID, exceptions_list: [] }) .expect(200); - const immutableRuleSecondTime = await getRule( - supertest, - log, - '9a1a2dae-0b5f-4c3d-8305-a268d404c306' - ); + const immutableRuleSecondTime = await getRule(supertest, log, ELASTIC_SECURITY_RULE_ID); expect(immutableRuleSecondTime.exceptions_list.length).to.eql(0); }); it('should allow adding a second exception list to an immutable rule through patch', async () => { - await installPrePackagedRules(supertest, log); + await installPrePackagedRules(supertest, es, log); const { id, list_id, namespace_type, type } = await createExceptionList( supertest, @@ -198,14 +192,8 @@ export default ({ getService }: FtrProviderContext) => { getCreateExceptionListMinimalSchemaMock() ); - // Rule id of "9a1a2dae-0b5f-4c3d-8305-a268d404c306" is from the file: - // x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/content/prepackaged_rules/elastic_endpoint.json // This rule has an existing exceptions_list that we are going to use - const immutableRule = await getRule( - supertest, - log, - '9a1a2dae-0b5f-4c3d-8305-a268d404c306' - ); + const immutableRule = await getRule(supertest, log, ELASTIC_SECURITY_RULE_ID); expect(immutableRule.exceptions_list.length).greaterThan(0); // make sure we have at least one // add a second exceptions list as a user is allowed to add a second list to an immutable rule @@ -213,7 +201,7 @@ export default ({ getService }: FtrProviderContext) => { .patch(DETECTION_ENGINE_RULES_URL) .set('kbn-xsrf', 'true') .send({ - rule_id: '9a1a2dae-0b5f-4c3d-8305-a268d404c306', + rule_id: ELASTIC_SECURITY_RULE_ID, exceptions_list: [ ...immutableRule.exceptions_list, { @@ -226,41 +214,27 @@ export default ({ getService }: FtrProviderContext) => { }) .expect(200); - const immutableRuleSecondTime = await getRule( - supertest, - log, - '9a1a2dae-0b5f-4c3d-8305-a268d404c306' - ); + const immutableRuleSecondTime = await getRule(supertest, log, ELASTIC_SECURITY_RULE_ID); expect(immutableRuleSecondTime.exceptions_list.length).to.eql(2); }); it('should override any updates to pre-packaged rules if the user removes the exception list through the API but the new version of a rule has an exception list again', async () => { - await installPrePackagedRules(supertest, log); + await installPrePackagedRules(supertest, es, log); - // Rule id of "9a1a2dae-0b5f-4c3d-8305-a268d404c306" is from the file: - // x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/content/prepackaged_rules/elastic_endpoint.json // This rule has an existing exceptions_list that we are going to use - const immutableRule = await getRule( - supertest, - log, - '9a1a2dae-0b5f-4c3d-8305-a268d404c306' - ); + const immutableRule = await getRule(supertest, log, ELASTIC_SECURITY_RULE_ID); expect(immutableRule.exceptions_list.length).greaterThan(0); // make sure we have at least one await supertest .patch(DETECTION_ENGINE_RULES_URL) .set('kbn-xsrf', 'true') - .send({ rule_id: '9a1a2dae-0b5f-4c3d-8305-a268d404c306', exceptions_list: [] }) + .send({ rule_id: ELASTIC_SECURITY_RULE_ID, exceptions_list: [] }) .expect(200); - await downgradeImmutableRule(es, log, '9a1a2dae-0b5f-4c3d-8305-a268d404c306'); - await installPrePackagedRules(supertest, log); - const immutableRuleSecondTime = await getRule( - supertest, - log, - '9a1a2dae-0b5f-4c3d-8305-a268d404c306' - ); + await downgradeImmutableRule(es, log, ELASTIC_SECURITY_RULE_ID); + await installPrePackagedRules(supertest, es, log); + const immutableRuleSecondTime = await getRule(supertest, log, ELASTIC_SECURITY_RULE_ID); // We should have a length of 1 and it should be the same as our original before we tried to remove it using patch expect(immutableRuleSecondTime.exceptions_list.length).to.eql(1); @@ -268,7 +242,7 @@ export default ({ getService }: FtrProviderContext) => { }); it('should merge back an exceptions_list if it was removed from the immutable rule through PATCH', async () => { - await installPrePackagedRules(supertest, log); + await installPrePackagedRules(supertest, es, log); const { id, list_id, namespace_type, type } = await createExceptionList( supertest, @@ -276,14 +250,8 @@ export default ({ getService }: FtrProviderContext) => { getCreateExceptionListMinimalSchemaMock() ); - // Rule id of "9a1a2dae-0b5f-4c3d-8305-a268d404c306" is from the file: - // x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/content/prepackaged_rules/elastic_endpoint.json // This rule has an existing exceptions_list that we are going to ensure does not stomp on our existing rule - const immutableRule = await getRule( - supertest, - log, - '9a1a2dae-0b5f-4c3d-8305-a268d404c306' - ); + const immutableRule = await getRule(supertest, log, ELASTIC_SECURITY_RULE_ID); expect(immutableRule.exceptions_list.length).greaterThan(0); // make sure we have at least one // remove the exception list and only have a single list that is not an endpoint_list @@ -291,7 +259,7 @@ export default ({ getService }: FtrProviderContext) => { .patch(DETECTION_ENGINE_RULES_URL) .set('kbn-xsrf', 'true') .send({ - rule_id: '9a1a2dae-0b5f-4c3d-8305-a268d404c306', + rule_id: ELASTIC_SECURITY_RULE_ID, exceptions_list: [ { id, @@ -303,13 +271,9 @@ export default ({ getService }: FtrProviderContext) => { }) .expect(200); - await downgradeImmutableRule(es, log, '9a1a2dae-0b5f-4c3d-8305-a268d404c306'); - await installPrePackagedRules(supertest, log); - const immutableRuleSecondTime = await getRule( - supertest, - log, - '9a1a2dae-0b5f-4c3d-8305-a268d404c306' - ); + await downgradeImmutableRule(es, log, ELASTIC_SECURITY_RULE_ID); + await installPrePackagedRules(supertest, es, log); + const immutableRuleSecondTime = await getRule(supertest, log, ELASTIC_SECURITY_RULE_ID); expect(immutableRuleSecondTime.exceptions_list).to.eql([ ...immutableRule.exceptions_list, @@ -323,26 +287,16 @@ export default ({ getService }: FtrProviderContext) => { }); it('should NOT add an extra exceptions_list that already exists on a rule during an upgrade', async () => { - await installPrePackagedRules(supertest, log); + await installPrePackagedRules(supertest, es, log); - // Rule id of "9a1a2dae-0b5f-4c3d-8305-a268d404c306" is from the file: - // x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/content/prepackaged_rules/elastic_endpoint.json // This rule has an existing exceptions_list that we are going to ensure does not stomp on our existing rule - const immutableRule = await getRule( - supertest, - log, - '9a1a2dae-0b5f-4c3d-8305-a268d404c306' - ); + const immutableRule = await getRule(supertest, log, ELASTIC_SECURITY_RULE_ID); expect(immutableRule.exceptions_list.length).greaterThan(0); // make sure we have at least one - await downgradeImmutableRule(es, log, '9a1a2dae-0b5f-4c3d-8305-a268d404c306'); - await installPrePackagedRules(supertest, log); + await downgradeImmutableRule(es, log, ELASTIC_SECURITY_RULE_ID); + await installPrePackagedRules(supertest, es, log); - const immutableRuleSecondTime = await getRule( - supertest, - log, - '9a1a2dae-0b5f-4c3d-8305-a268d404c306' - ); + const immutableRuleSecondTime = await getRule(supertest, log, ELASTIC_SECURITY_RULE_ID); // The installed rule should have both the original immutable exceptions list back and the // new list the user added. @@ -352,7 +306,7 @@ export default ({ getService }: FtrProviderContext) => { }); it('should NOT allow updates to pre-packaged rules to overwrite existing exception based rules when the user adds an additional exception list', async () => { - await installPrePackagedRules(supertest, log); + await installPrePackagedRules(supertest, es, log); const { id, list_id, namespace_type, type } = await createExceptionList( supertest, @@ -360,21 +314,15 @@ export default ({ getService }: FtrProviderContext) => { getCreateExceptionListMinimalSchemaMock() ); - // Rule id of "9a1a2dae-0b5f-4c3d-8305-a268d404c306" is from the file: - // x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/content/prepackaged_rules/elastic_endpoint.json // This rule has an existing exceptions_list that we are going to ensure does not stomp on our existing rule - const immutableRule = await getRule( - supertest, - log, - '9a1a2dae-0b5f-4c3d-8305-a268d404c306' - ); + const immutableRule = await getRule(supertest, log, ELASTIC_SECURITY_RULE_ID); // add a second exceptions list as a user is allowed to add a second list to an immutable rule await supertest .patch(DETECTION_ENGINE_RULES_URL) .set('kbn-xsrf', 'true') .send({ - rule_id: '9a1a2dae-0b5f-4c3d-8305-a268d404c306', + rule_id: ELASTIC_SECURITY_RULE_ID, exceptions_list: [ ...immutableRule.exceptions_list, { @@ -387,13 +335,9 @@ export default ({ getService }: FtrProviderContext) => { }) .expect(200); - await downgradeImmutableRule(es, log, '9a1a2dae-0b5f-4c3d-8305-a268d404c306'); - await installPrePackagedRules(supertest, log); - const immutableRuleSecondTime = await getRule( - supertest, - log, - '9a1a2dae-0b5f-4c3d-8305-a268d404c306' - ); + await downgradeImmutableRule(es, log, ELASTIC_SECURITY_RULE_ID); + await installPrePackagedRules(supertest, es, log); + const immutableRuleSecondTime = await getRule(supertest, log, ELASTIC_SECURITY_RULE_ID); // It should be the same as what the user added originally expect(immutableRuleSecondTime.exceptions_list).to.eql([ @@ -408,7 +352,7 @@ export default ({ getService }: FtrProviderContext) => { }); it('should not remove any exceptions added to a pre-packaged/immutable rule during an update if that rule has no existing exception lists', async () => { - await installPrePackagedRules(supertest, log); + await installPrePackagedRules(supertest, es, log); // Create a new exception list const { id, list_id, namespace_type, type } = await createExceptionList( @@ -417,14 +361,16 @@ export default ({ getService }: FtrProviderContext) => { getCreateExceptionListMinimalSchemaMock() ); - // Rule id of "eb079c62-4481-4d6e-9643-3ca499df7aaa" is from the file: - // x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/content/prepackaged_rules/external_alerts.json - // since this rule does not have existing exceptions_list that we are going to use for tests - const immutableRule = await getRule( - supertest, - log, - 'eb079c62-4481-4d6e-9643-3ca499df7aaa' + // Find a rule without exceptions_list + const ruleWithoutExceptionList = SAMPLE_PREBUILT_RULES.find( + (rule) => !rule['security-rule'].exceptions_list ); + const ruleId = ruleWithoutExceptionList?.['security-rule'].rule_id; + if (!ruleId) { + throw new Error('Cannot find a rule without exceptions_list in the sample data'); + } + + const immutableRule = await getRule(supertest, log, ruleId); expect(immutableRule.exceptions_list.length).eql(0); // make sure we have no exceptions_list // add a second exceptions list as a user is allowed to add a second list to an immutable rule @@ -432,7 +378,7 @@ export default ({ getService }: FtrProviderContext) => { .patch(DETECTION_ENGINE_RULES_URL) .set('kbn-xsrf', 'true') .send({ - rule_id: 'eb079c62-4481-4d6e-9643-3ca499df7aaa', + rule_id: ruleId, exceptions_list: [ { id, @@ -444,13 +390,9 @@ export default ({ getService }: FtrProviderContext) => { }) .expect(200); - await downgradeImmutableRule(es, log, 'eb079c62-4481-4d6e-9643-3ca499df7aaa'); - await installPrePackagedRules(supertest, log); - const immutableRuleSecondTime = await getRule( - supertest, - log, - 'eb079c62-4481-4d6e-9643-3ca499df7aaa' - ); + await downgradeImmutableRule(es, log, ruleId); + await installPrePackagedRules(supertest, es, log); + const immutableRuleSecondTime = await getRule(supertest, log, ruleId); expect(immutableRuleSecondTime.exceptions_list).to.eql([ { @@ -463,7 +405,7 @@ export default ({ getService }: FtrProviderContext) => { }); it('should not change the immutable tags when adding a second exception list to an immutable rule through patch', async () => { - await installPrePackagedRules(supertest, log); + await installPrePackagedRules(supertest, es, log); const { id, list_id, namespace_type, type } = await createExceptionList( supertest, @@ -471,14 +413,8 @@ export default ({ getService }: FtrProviderContext) => { getCreateExceptionListMinimalSchemaMock() ); - // Rule id of "9a1a2dae-0b5f-4c3d-8305-a268d404c306" is from the file: - // x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/content/prepackaged_rules/elastic_endpoint.json // This rule has an existing exceptions_list that we are going to use - const immutableRule = await getRule( - supertest, - log, - '9a1a2dae-0b5f-4c3d-8305-a268d404c306' - ); + const immutableRule = await getRule(supertest, log, ELASTIC_SECURITY_RULE_ID); expect(immutableRule.exceptions_list.length).greaterThan(0); // make sure we have at least one // add a second exceptions list as a user is allowed to add a second list to an immutable rule @@ -486,7 +422,7 @@ export default ({ getService }: FtrProviderContext) => { .patch(DETECTION_ENGINE_RULES_URL) .set('kbn-xsrf', 'true') .send({ - rule_id: '9a1a2dae-0b5f-4c3d-8305-a268d404c306', + rule_id: ELASTIC_SECURITY_RULE_ID, exceptions_list: [ ...immutableRule.exceptions_list, { @@ -499,11 +435,7 @@ export default ({ getService }: FtrProviderContext) => { }) .expect(200); - const body = await findImmutableRuleById( - supertest, - log, - '9a1a2dae-0b5f-4c3d-8305-a268d404c306' - ); + const body = await findImmutableRuleById(supertest, log, ELASTIC_SECURITY_RULE_ID); expect(body.data.length).to.eql(1); // should have only one length to the data set, otherwise we have duplicates or the tags were removed and that is incredibly bad. const bodyToCompare = removeServerGeneratedProperties(body.data[0]); @@ -513,7 +445,7 @@ export default ({ getService }: FtrProviderContext) => { }); it('should not change count of prepacked rules when adding a second exception list to an immutable rule through patch. If this fails, suspect the immutable tags are not staying on the rule correctly.', async () => { - await installPrePackagedRules(supertest, log); + await installPrePackagedRules(supertest, es, log); const { id, list_id, namespace_type, type } = await createExceptionList( supertest, @@ -521,14 +453,8 @@ export default ({ getService }: FtrProviderContext) => { getCreateExceptionListMinimalSchemaMock() ); - // Rule id of "9a1a2dae-0b5f-4c3d-8305-a268d404c306" is from the file: - // x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/content/prepackaged_rules/elastic_endpoint.json // This rule has an existing exceptions_list that we are going to use - const immutableRule = await getRule( - supertest, - log, - '9a1a2dae-0b5f-4c3d-8305-a268d404c306' - ); + const immutableRule = await getRule(supertest, log, ELASTIC_SECURITY_RULE_ID); expect(immutableRule.exceptions_list.length).greaterThan(0); // make sure we have at least one // add a second exceptions list as a user is allowed to add a second list to an immutable rule @@ -536,7 +462,7 @@ export default ({ getService }: FtrProviderContext) => { .patch(DETECTION_ENGINE_RULES_URL) .set('kbn-xsrf', 'true') .send({ - rule_id: '9a1a2dae-0b5f-4c3d-8305-a268d404c306', + rule_id: ELASTIC_SECURITY_RULE_ID, exceptions_list: [ ...immutableRule.exceptions_list, { diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/group4/telemetry/task_based/detection_rules.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group4/telemetry/task_based/detection_rules.ts index 36f5b84a50c3d..50e79a447e48a 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/group4/telemetry/task_based/detection_rules.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/group4/telemetry/task_based/detection_rules.ts @@ -24,19 +24,16 @@ import { removeTimeFieldsFromTelemetryStats, } from '../../../../utils'; import { deleteAllExceptions } from '../../../../../lists_api_integration/utils'; +import { ELASTIC_SECURITY_RULE_ID } from '../../../../utils/create_prebuilt_rule_saved_objects'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext) => { + const es = getService('es'); const supertest = getService('supertest'); const esArchiver = getService('esArchiver'); const log = getService('log'); const retry = getService('retry'); - // Rule id of "9a1a2dae-0b5f-4c3d-8305-a268d404c306" is from the file: - // x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/content/prepackaged_rules/elastic_endpoint_security.json - // This rule has an existing exceptions_list that we are going to use. - const IMMUTABLE_RULE_ID = '9a1a2dae-0b5f-4c3d-8305-a268d404c306'; - describe('Detection rule task telemetry', async () => { before(async () => { await esArchiver.load('x-pack/test/functional/es_archives/security_solution/telemetry'); @@ -341,7 +338,7 @@ export default ({ getService }: FtrProviderContext) => { describe('pre-built/immutable/elastic rules should show detection_rules telemetry data for each list type', () => { beforeEach(async () => { // install prepackaged rules to get immutable rules for testing - await installPrePackagedRules(supertest, log); + await installPrePackagedRules(supertest, es, log); }); it('should return mutating types such as "id", "@timestamp", etc... for list of type "detection"', async () => { @@ -371,12 +368,12 @@ export default ({ getService }: FtrProviderContext) => { }); // add the exception list to the pre-built/immutable/elastic rule using "PATCH" endpoint - const { exceptions_list } = await getRule(supertest, log, IMMUTABLE_RULE_ID); + const { exceptions_list } = await getRule(supertest, log, ELASTIC_SECURITY_RULE_ID); await supertest .patch(DETECTION_ENGINE_RULES_URL) .set('kbn-xsrf', 'true') .send({ - rule_id: IMMUTABLE_RULE_ID, + rule_id: ELASTIC_SECURITY_RULE_ID, exceptions_list: [ ...exceptions_list, { @@ -429,12 +426,12 @@ export default ({ getService }: FtrProviderContext) => { }); // add the exception list to the pre-built/immutable/elastic rule - const immutableRule = await getRule(supertest, log, IMMUTABLE_RULE_ID); + const immutableRule = await getRule(supertest, log, ELASTIC_SECURITY_RULE_ID); await supertest .patch(DETECTION_ENGINE_RULES_URL) .set('kbn-xsrf', 'true') .send({ - rule_id: IMMUTABLE_RULE_ID, + rule_id: ELASTIC_SECURITY_RULE_ID, exceptions_list: [ ...immutableRule.exceptions_list, { @@ -505,12 +502,12 @@ export default ({ getService }: FtrProviderContext) => { }); // add the exception list to the pre-built/immutable/elastic rule - const immutableRule = await getRule(supertest, log, IMMUTABLE_RULE_ID); + const immutableRule = await getRule(supertest, log, ELASTIC_SECURITY_RULE_ID); await supertest .patch(DETECTION_ENGINE_RULES_URL) .set('kbn-xsrf', 'true') .send({ - rule_id: IMMUTABLE_RULE_ID, + rule_id: ELASTIC_SECURITY_RULE_ID, exceptions_list: [ ...immutableRule.exceptions_list, { @@ -581,12 +578,12 @@ export default ({ getService }: FtrProviderContext) => { }); // add the exception list to the pre-built/immutable/elastic rule - const immutableRule = await getRule(supertest, log, IMMUTABLE_RULE_ID); + const immutableRule = await getRule(supertest, log, ELASTIC_SECURITY_RULE_ID); await supertest .patch(DETECTION_ENGINE_RULES_URL) .set('kbn-xsrf', 'true') .send({ - rule_id: IMMUTABLE_RULE_ID, + rule_id: ELASTIC_SECURITY_RULE_ID, exceptions_list: [ ...immutableRule.exceptions_list, { @@ -657,12 +654,12 @@ export default ({ getService }: FtrProviderContext) => { }); // add the exception list to the pre-built/immutable/elastic rule - const immutableRule = await getRule(supertest, log, IMMUTABLE_RULE_ID); + const immutableRule = await getRule(supertest, log, ELASTIC_SECURITY_RULE_ID); await supertest .patch(DETECTION_ENGINE_RULES_URL) .set('kbn-xsrf', 'true') .send({ - rule_id: IMMUTABLE_RULE_ID, + rule_id: ELASTIC_SECURITY_RULE_ID, exceptions_list: [ ...immutableRule.exceptions_list, { @@ -733,12 +730,12 @@ export default ({ getService }: FtrProviderContext) => { }); // add the exception list to the pre-built/immutable/elastic rule - const immutableRule = await getRule(supertest, log, IMMUTABLE_RULE_ID); + const immutableRule = await getRule(supertest, log, ELASTIC_SECURITY_RULE_ID); await supertest .patch(DETECTION_ENGINE_RULES_URL) .set('kbn-xsrf', 'true') .send({ - rule_id: IMMUTABLE_RULE_ID, + rule_id: ELASTIC_SECURITY_RULE_ID, exceptions_list: [ ...immutableRule.exceptions_list, { @@ -786,7 +783,7 @@ export default ({ getService }: FtrProviderContext) => { describe('pre-built/immutable/elastic rules should show detection_rules telemetry data for multiple list items and types', () => { beforeEach(async () => { // install prepackaged rules to get immutable rules for testing - await installPrePackagedRules(supertest, log); + await installPrePackagedRules(supertest, es, log); }); it('should give telemetry/stats for 2 exception lists to the type of "detection"', async () => { @@ -833,12 +830,12 @@ export default ({ getService }: FtrProviderContext) => { }); // add the exception list to the pre-built/immutable/elastic rule - const immutableRule = await getRule(supertest, log, IMMUTABLE_RULE_ID); + const immutableRule = await getRule(supertest, log, ELASTIC_SECURITY_RULE_ID); await supertest .patch(DETECTION_ENGINE_RULES_URL) .set('kbn-xsrf', 'true') .send({ - rule_id: IMMUTABLE_RULE_ID, + rule_id: ELASTIC_SECURITY_RULE_ID, exceptions_list: [ ...immutableRule.exceptions_list, { diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/group4/telemetry/usage_collector/detection_rules.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group4/telemetry/usage_collector/detection_rules.ts index 1d059e02ed5cc..f1de2682deba6 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/group4/telemetry/usage_collector/detection_rules.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/group4/telemetry/usage_collector/detection_rules.ts @@ -36,6 +36,7 @@ import { updateRule, deleteAllEventLogExecutionEvents, } from '../../../../utils'; +import { ELASTIC_SECURITY_RULE_ID } from '../../../../utils/create_prebuilt_rule_saved_objects'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext) => { @@ -1266,12 +1267,11 @@ export default ({ getService }: FtrProviderContext) => { describe('"pre-packaged"/"immutable" rules', async () => { it('should show stats for totals for in-active pre-packaged rules', async () => { - await installPrePackagedRules(supertest, log); + await installPrePackagedRules(supertest, es, log); await retry.try(async () => { const stats = await getStats(supertest, log); expect(stats.detection_rules.detection_rule_usage.elastic_total.enabled).above(0); expect(stats.detection_rules.detection_rule_usage.elastic_total.disabled).above(0); - expect(stats.detection_rules.detection_rule_usage.elastic_total.enabled).above(0); expect( stats.detection_rules.detection_rule_usage.elastic_total.legacy_notifications_enabled ).to.eql(0); @@ -1299,14 +1299,11 @@ export default ({ getService }: FtrProviderContext) => { }); it('should show stats for the detection_rule_details for a specific pre-packaged rule', async () => { - await installPrePackagedRules(supertest, log); + await installPrePackagedRules(supertest, es, log); await retry.try(async () => { const stats = await getStats(supertest, log); - // Rule id of "9a1a2dae-0b5f-4c3d-8305-a268d404c306" is from the file: - // x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/content/prepackaged_rules/elastic_endpoint_security.json - // We have to search by "rule_name" since the "rule_id" it is storing is the Saved Object ID and not the rule_id const foundRule = stats.detection_rules.detection_rule_detail.find( - (rule) => rule.rule_id === '9a1a2dae-0b5f-4c3d-8305-a268d404c306' + (rule) => rule.rule_id === ELASTIC_SECURITY_RULE_ID ); if (foundRule == null) { throw new Error('Found rule should not be null. Please change this end to end test.'); @@ -1319,23 +1316,21 @@ export default ({ getService }: FtrProviderContext) => { ...omittedFields } = foundRule; expect(omittedFields).to.eql({ - rule_name: 'Endpoint Security', + rule_name: 'A rule with an exception list', rule_type: 'query', enabled: true, elastic_rule: true, alert_count_daily: 0, cases_count_total: 0, - has_notification: false, has_legacy_notification: false, + has_notification: false, }); }); }); it('should show "notifications_disabled" to be "1", "has_notification" to be "true, "has_legacy_notification" to be "false" for rule that has at least "1" action(s) and the alert is "disabled"/"in-active"', async () => { - await installPrePackagedRules(supertest, log); - // Rule id of "9a1a2dae-0b5f-4c3d-8305-a268d404c306" is from the file: - // x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/content/prepackaged_rules/elastic_endpoint_security.json - const immutableRule = await getRule(supertest, log, '9a1a2dae-0b5f-4c3d-8305-a268d404c306'); + await installPrePackagedRules(supertest, es, log); + const immutableRule = await getRule(supertest, log, ELASTIC_SECURITY_RULE_ID); const hookAction = await createNewAction(supertest, log); const newRuleToUpdate = getSimpleRule(immutableRule.rule_id); const ruleToUpdate = getRuleWithWebHookAction(hookAction.id, false, newRuleToUpdate); @@ -1345,7 +1340,7 @@ export default ({ getService }: FtrProviderContext) => { const stats = await getStats(supertest, log); // We have to search by "rule_name" since the "rule_id" it is storing is the Saved Object ID and not the rule_id const foundRule = stats.detection_rules.detection_rule_detail.find( - (rule) => rule.rule_id === '9a1a2dae-0b5f-4c3d-8305-a268d404c306' + (rule) => rule.rule_id === ELASTIC_SECURITY_RULE_ID ); if (foundRule == null) { throw new Error('Found rule should not be null. Please change this end to end test.'); @@ -1386,10 +1381,8 @@ export default ({ getService }: FtrProviderContext) => { }); it('should show "notifications_enabled" to be "1", "has_notification" to be "true, "has_legacy_notification" to be "false" for rule that has at least "1" action(s) and the alert is "enabled"/"active"', async () => { - await installPrePackagedRules(supertest, log); - // Rule id of "9a1a2dae-0b5f-4c3d-8305-a268d404c306" is from the file: - // x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/content/prepackaged_rules/elastic_endpoint_security.json - const immutableRule = await getRule(supertest, log, '9a1a2dae-0b5f-4c3d-8305-a268d404c306'); + await installPrePackagedRules(supertest, es, log); + const immutableRule = await getRule(supertest, log, ELASTIC_SECURITY_RULE_ID); const hookAction = await createNewAction(supertest, log); const newRuleToUpdate = getSimpleRule(immutableRule.rule_id); const ruleToUpdate = getRuleWithWebHookAction(hookAction.id, true, newRuleToUpdate); @@ -1399,7 +1392,7 @@ export default ({ getService }: FtrProviderContext) => { const stats = await getStats(supertest, log); // We have to search by "rule_name" since the "rule_id" it is storing is the Saved Object ID and not the rule_id const foundRule = stats.detection_rules.detection_rule_detail.find( - (rule) => rule.rule_id === '9a1a2dae-0b5f-4c3d-8305-a268d404c306' + (rule) => rule.rule_id === ELASTIC_SECURITY_RULE_ID ); if (foundRule == null) { throw new Error('Found rule should not be null. Please change this end to end test.'); @@ -1440,10 +1433,8 @@ export default ({ getService }: FtrProviderContext) => { }); it('should show "legacy_notifications_disabled" to be "1", "has_notification" to be "false, "has_legacy_notification" to be "true" for rule that has at least "1" action(s) and the alert is "disabled"/"in-active"', async () => { - await installPrePackagedRules(supertest, log); - // Rule id of "9a1a2dae-0b5f-4c3d-8305-a268d404c306" is from the file: - // x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/content/prepackaged_rules/elastic_endpoint_security.json - const immutableRule = await getRule(supertest, log, '9a1a2dae-0b5f-4c3d-8305-a268d404c306'); + await installPrePackagedRules(supertest, es, log); + const immutableRule = await getRule(supertest, log, ELASTIC_SECURITY_RULE_ID); const hookAction = await createNewAction(supertest, log); const newRuleToUpdate = getSimpleRule(immutableRule.rule_id, false); await updateRule(supertest, log, newRuleToUpdate); @@ -1453,7 +1444,7 @@ export default ({ getService }: FtrProviderContext) => { const stats = await getStats(supertest, log); // We have to search by "rule_name" since the "rule_id" it is storing is the Saved Object ID and not the rule_id const foundRule = stats.detection_rules.detection_rule_detail.find( - (rule) => rule.rule_id === '9a1a2dae-0b5f-4c3d-8305-a268d404c306' + (rule) => rule.rule_id === ELASTIC_SECURITY_RULE_ID ); if (foundRule == null) { throw new Error('Found rule should not be null. Please change this end to end test.'); @@ -1494,10 +1485,8 @@ export default ({ getService }: FtrProviderContext) => { }); it('should show "legacy_notifications_enabled" to be "1", "has_notification" to be "false, "has_legacy_notification" to be "true" for rule that has at least "1" action(s) and the alert is "enabled"/"active"', async () => { - await installPrePackagedRules(supertest, log); - // Rule id of "9a1a2dae-0b5f-4c3d-8305-a268d404c306" is from the file: - // x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/content/prepackaged_rules/elastic_endpoint_security.json - const immutableRule = await getRule(supertest, log, '9a1a2dae-0b5f-4c3d-8305-a268d404c306'); + await installPrePackagedRules(supertest, es, log); + const immutableRule = await getRule(supertest, log, ELASTIC_SECURITY_RULE_ID); const hookAction = await createNewAction(supertest, log); const newRuleToUpdate = getSimpleRule(immutableRule.rule_id, true); await updateRule(supertest, log, newRuleToUpdate); @@ -1507,7 +1496,7 @@ export default ({ getService }: FtrProviderContext) => { const stats = await getStats(supertest, log); // We have to search by "rule_name" since the "rule_id" it is storing is the Saved Object ID and not the rule_id const foundRule = stats.detection_rules.detection_rule_detail.find( - (rule) => rule.rule_id === '9a1a2dae-0b5f-4c3d-8305-a268d404c306' + (rule) => rule.rule_id === ELASTIC_SECURITY_RULE_ID ); if (foundRule == null) { throw new Error('Found rule should not be null. Please change this end to end test.'); diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/rule_execution_logic/query.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/rule_execution_logic/query.ts index 8ef0bf6b736dd..074989e424cfe 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/rule_execution_logic/query.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/rule_execution_logic/query.ts @@ -623,7 +623,7 @@ export default ({ getService }: FtrProviderContext) => { it('should generate multiple alerts for a single doc in multiple groups', async () => { const rule: QueryRuleCreateProps = { ...getRuleForSignalTesting(['suppression-data']), - query: `destination.ip: *`, + query: `*:*`, alert_suppression: { group_by: ['destination.ip'], }, @@ -642,7 +642,7 @@ export default ({ getService }: FtrProviderContext) => { size: 1000, sort: ['destination.ip'], }); - expect(previewAlerts.length).to.eql(2); + expect(previewAlerts.length).to.eql(3); expect(previewAlerts[0]._source).to.eql({ ...previewAlerts[0]._source, @@ -657,6 +657,21 @@ export default ({ getService }: FtrProviderContext) => { [ALERT_SUPPRESSION_END]: '2020-10-28T05:00:00.000Z', [ALERT_SUPPRESSION_DOCS_COUNT]: 0, }); + + // We also expect to have a separate group for documents that don't populate the groupBy field + expect(previewAlerts[2]._source).to.eql({ + ...previewAlerts[2]._source, + [ALERT_SUPPRESSION_TERMS]: [ + { + field: 'destination.ip', + value: null, + }, + ], + [ALERT_ORIGINAL_TIME]: '2020-10-28T05:00:00.000Z', + [ALERT_SUPPRESSION_START]: '2020-10-28T05:00:00.000Z', + [ALERT_SUPPRESSION_END]: '2020-10-28T05:00:02.000Z', + [ALERT_SUPPRESSION_DOCS_COUNT]: 16, + }); }); }); }); diff --git a/x-pack/test/detection_engine_api_integration/utils/create_prebuilt_rule_saved_objects.ts b/x-pack/test/detection_engine_api_integration/utils/create_prebuilt_rule_saved_objects.ts new file mode 100644 index 0000000000000..15778e41a3be6 --- /dev/null +++ b/x-pack/test/detection_engine_api_integration/utils/create_prebuilt_rule_saved_objects.ts @@ -0,0 +1,69 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { Client } from '@elastic/elasticsearch'; +import { + getPrebuiltRuleMock, + getPrebuiltRuleWithExceptionsMock, +} from '@kbn/security-solution-plugin/common/detection_engine/prebuilt_rules/model/prebuilt_rule.mock'; + +/** + * Rule signature id (`rule.rule_id`) of the prebuilt "Endpoint Security" rule. + */ +export const ELASTIC_SECURITY_RULE_ID = '9a1a2dae-0b5f-4c3d-8305-a268d404c306'; + +export const SAMPLE_PREBUILT_RULES = [ + { + 'security-rule': { + ...getPrebuiltRuleWithExceptionsMock(), + rule_id: ELASTIC_SECURITY_RULE_ID, + enabled: true, + }, + type: 'security-rule', + references: [], + coreMigrationVersion: '8.6.0', + updated_at: '2022-11-01T12:56:39.717Z', + created_at: '2022-11-01T12:56:39.717Z', + }, + { + 'security-rule': { + ...getPrebuiltRuleMock(), + rule_id: '000047bb-b27a-47ec-8b62-ef1a5d2c9e19', + }, + type: 'security-rule', + references: [], + coreMigrationVersion: '8.6.0', + updated_at: '2022-11-01T12:56:39.717Z', + created_at: '2022-11-01T12:56:39.717Z', + }, + { + 'security-rule': { + ...getPrebuiltRuleMock(), + rule_id: '00140285-b827-4aee-aa09-8113f58a08f3', + }, + type: 'security-rule', + references: [], + coreMigrationVersion: '8.6.0', + updated_at: '2022-11-01T12:56:39.717Z', + created_at: '2022-11-01T12:56:39.717Z', + }, +]; + +/** + * Creates saved objects with prebuilt rule assets which can be used for installing actual prebuilt rules after that. + * + * @param es Elasticsearch client + */ +export const createPrebuiltRuleAssetSavedObjects = async (es: Client): Promise => { + await es.bulk({ + refresh: 'wait_for', + body: SAMPLE_PREBUILT_RULES.flatMap((doc) => [ + { index: { _index: '.kibana', _id: `security-rule:${doc['security-rule'].rule_id}` } }, + doc, + ]), + }); +}; diff --git a/x-pack/test/detection_engine_api_integration/utils/delete_all_prebuilt_rules.ts b/x-pack/test/detection_engine_api_integration/utils/delete_all_prebuilt_rules.ts new file mode 100644 index 0000000000000..de89f80667aff --- /dev/null +++ b/x-pack/test/detection_engine_api_integration/utils/delete_all_prebuilt_rules.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 type { Client } from '@elastic/elasticsearch'; + +/** + * Remove all prebuilt rules from the .kibana index + * @param es The ElasticSearch handle + * @param log The tooling logger + */ +export const deleteAllPrebuiltRules = async (es: Client): Promise => { + await es.deleteByQuery({ + index: '.kibana', + q: 'type:security-rule', + wait_for_completion: true, + refresh: true, + body: {}, + }); +}; diff --git a/x-pack/test/detection_engine_api_integration/utils/install_prepackaged_rules.ts b/x-pack/test/detection_engine_api_integration/utils/install_prepackaged_rules.ts index 53fb592e84d13..9f48a378832f0 100644 --- a/x-pack/test/detection_engine_api_integration/utils/install_prepackaged_rules.ts +++ b/x-pack/test/detection_engine_api_integration/utils/install_prepackaged_rules.ts @@ -5,34 +5,29 @@ * 2.0. */ +import { Client } from '@elastic/elasticsearch'; +import { + InstallPrebuiltRulesAndTimelinesResponse, + PREBUILT_RULES_URL, +} from '@kbn/security-solution-plugin/common/detection_engine/prebuilt_rules'; import type { ToolingLog } from '@kbn/tooling-log'; import type SuperTest from 'supertest'; - -import { PREBUILT_RULES_URL } from '@kbn/security-solution-plugin/common/detection_engine/prebuilt_rules'; -import { countDownTest } from './count_down_test'; +import { createPrebuiltRuleAssetSavedObjects } from './create_prebuilt_rule_saved_objects'; export const installPrePackagedRules = async ( supertest: SuperTest.SuperTest, + es: Client, log: ToolingLog -): Promise => { - await countDownTest( - async () => { - const { status, body } = await supertest - .put(PREBUILT_RULES_URL) - .set('kbn-xsrf', 'true') - .send(); - if (status !== 200) { - return { - passed: false, - errorMessage: `Did not get an expected 200 "ok" when installing pre-packaged rules (installPrePackagedRules) yet. Retrying until we get a 200 "ok". body: ${JSON.stringify( - body - )}, status: ${JSON.stringify(status)}`, - }; - } else { - return { passed: true }; - } - }, - 'installPrePackagedRules', - log - ); +): Promise => { + // Ensure there are prebuilt rule saved objects before installing rules + await createPrebuiltRuleAssetSavedObjects(es); + const response = await supertest.put(PREBUILT_RULES_URL).set('kbn-xsrf', 'true').send(); + if (response.status !== 200) { + log.error( + `Did not get an expected 200 "ok" when installing prebuilt rules. body: ${JSON.stringify( + response.body + )}, status: ${JSON.stringify(response.status)}` + ); + } + return response.body; }; diff --git a/x-pack/test/fleet_api_integration/apis/agents/upgrade.ts b/x-pack/test/fleet_api_integration/apis/agents/upgrade.ts index e8dad8624021f..b648c5ae70acb 100644 --- a/x-pack/test/fleet_api_integration/apis/agents/upgrade.ts +++ b/x-pack/test/fleet_api_integration/apis/agents/upgrade.ts @@ -217,7 +217,7 @@ export default function (providerContext: FtrProviderContext) { }, }); const action: any = actionsRes.hits.hits[0]._source; - expect(action.data.source_uri).contain('http://path/to/download'); + expect(action.data.sourceURI).contain('http://path/to/download'); }); it('should respond 400 if trying to upgrade to a version that does not match installed kibana version', async () => { const kibanaVersion = await kibanaServer.version.get(); @@ -976,7 +976,7 @@ export default function (providerContext: FtrProviderContext) { }); const action: any = actionsRes.hits.hits[0]._source; - expect(action.data.source_uri).contain('http://path/to/download'); + expect(action.data.sourceURI).contain('http://path/to/download'); }); it('enrolled in a hosted agent policy bulk upgrade should respond with 200 and object of results. Should not update the hosted agent SOs', async () => { diff --git a/x-pack/test/fleet_api_integration/config.ts b/x-pack/test/fleet_api_integration/config.ts index d06707706c692..2d5f5be792a88 100644 --- a/x-pack/test/fleet_api_integration/config.ts +++ b/x-pack/test/fleet_api_integration/config.ts @@ -15,11 +15,10 @@ import { const getFullPath = (relativePath: string) => path.join(path.dirname(__filename), relativePath); // Docker image to use for Fleet API integration tests. -// This hash comes from the latest successful build of the Snapshot Distribution of the Package Registry, for -// example: https://beats-ci.elastic.co/blue/organizations/jenkins/Ingest-manager%2Fpackage-storage/detail/snapshot/74/pipeline/257#step-302-log-1. -// It should be updated any time there is a new Docker image published for the Snapshot Distribution of the Package Registry. -export const dockerImage = - 'docker.elastic.co/package-registry/distribution:production-v2-experimental'; +// This hash comes from the latest successful build of the Production Distribution of the Package Registry, for +// example: https://internal-ci.elastic.co/blue/organizations/jenkins/package_storage%2Findexing-job/detail/main/1884/pipeline/147. +// It should be updated any time there is a new package published. +export const dockerImage = 'docker.elastic.co/package-registry/distribution:production'; export const BUNDLED_PACKAGE_DIR = '/tmp/fleet_bundled_packages'; diff --git a/x-pack/test/functional/apps/canvas/expression.ts b/x-pack/test/functional/apps/canvas/expression.ts index f346615b771a6..875a9deb2ebe4 100644 --- a/x-pack/test/functional/apps/canvas/expression.ts +++ b/x-pack/test/functional/apps/canvas/expression.ts @@ -12,13 +12,12 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function canvasExpressionTest({ getService, getPageObjects }: FtrProviderContext) { const testSubjects = getService('testSubjects'); const retry = getService('retry'); + const monacoEditor = getService('monacoEditor'); const PageObjects = getPageObjects(['canvas', 'common']); - const find = getService('find'); const kibanaServer = getService('kibanaServer'); const archive = 'x-pack/test/functional/fixtures/kbn_archiver/canvas/default'; - // FLAKY: https://github.com/elastic/kibana/issues/115883 - describe.skip('expression editor', function () { + describe('expression editor', function () { // there is an issue with FF not properly clicking on workpad elements this.tags('skipFirefox'); @@ -44,31 +43,31 @@ export default function canvasExpressionTest({ getService, getPageObjects }: Ftr expect(elements).to.have.length(4); }); + const codeEditorSubj = 'canvasCodeEditorField'; + // find the first workpad element (a markdown element) and click it to select it await testSubjects.click('canvasWorkpadPage > canvasWorkpadPageElementContent', 20000); + await monacoEditor.waitCodeEditorReady(codeEditorSubj); // open the expression editor await PageObjects.canvas.openExpressionEditor(); + await monacoEditor.waitCodeEditorReady('canvasExpressionInput'); // select markdown content and clear it - const mdBox = await find.byCssSelector('.canvasSidebar__panel .canvasTextArea__code'); - const oldMd = await mdBox.getVisibleText(); - await mdBox.clearValueWithKeyboard(); + const oldMd = await monacoEditor.getCodeEditorValue(0); + await monacoEditor.setCodeEditorValue('', 0); // type the new text const newMd = `${oldMd} and this is a test`; - await mdBox.type(newMd); - await find.clickByCssSelector('.canvasArg--controls .euiButton'); + await monacoEditor.setCodeEditorValue(newMd, 0); // make sure the open expression editor also has the changes - const editor = await find.byCssSelector('.monaco-editor .view-lines'); - const editorText = await editor.getVisibleText(); - expect(editorText).to.contain('Orange: Timelion, Server function and this is a test'); - + await retry.try(async () => { + const editorText = await monacoEditor.getCodeEditorValue(1); + expect(editorText).to.contain('Orange: Timelion, Server function and this is a test'); + }); // reset the markdown - await mdBox.clearValueWithKeyboard(); - await mdBox.type(oldMd); - await find.clickByCssSelector('.canvasArg--controls .euiButton'); + await monacoEditor.setCodeEditorValue(oldMd, 0); }); }); } diff --git a/x-pack/test/functional/apps/dashboard/group2/sync_colors.ts b/x-pack/test/functional/apps/dashboard/group2/sync_colors.ts index 093318bb8b5cd..7e31cbcdc425a 100644 --- a/x-pack/test/functional/apps/dashboard/group2/sync_colors.ts +++ b/x-pack/test/functional/apps/dashboard/group2/sync_colors.ts @@ -33,9 +33,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { return colorMapping; } - - // FLAKY: https://github.com/elastic/kibana/issues/97403 - describe.skip('sync colors', function () { + describe('sync colors', function () { before(async function () { await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/logstash_functional'); await kibanaServer.importExport.load( @@ -48,6 +46,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await kibanaServer.importExport.unload( 'x-pack/test/functional/fixtures/kbn_archiver/lens/lens_basic.json' ); + await kibanaServer.savedObjects.cleanStandardList(); }); it('should sync colors on dashboard by default', async function () { @@ -90,9 +89,13 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await filterBar.addFilter('geo.src', 'is not', 'CN'); await PageObjects.lens.save('vis2', false, true); + await PageObjects.dashboard.useColorSync(true); await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.dashboard.waitForRenderComplete(); + const colorMapping1 = getColorMapping(await PageObjects.dashboard.getPanelChartDebugState(0)); const colorMapping2 = getColorMapping(await PageObjects.dashboard.getPanelChartDebugState(1)); + expect(Object.keys(colorMapping1)).to.have.length(6); expect(Object.keys(colorMapping1)).to.have.length(6); const panel1Keys = ['CN']; diff --git a/x-pack/test/functional/apps/discover/feature_controls/discover_security.ts b/x-pack/test/functional/apps/discover/feature_controls/discover_security.ts index 66b870f42ade1..aa798a0cd7d98 100644 --- a/x-pack/test/functional/apps/discover/feature_controls/discover_security.ts +++ b/x-pack/test/functional/apps/discover/feature_controls/discover_security.ts @@ -58,6 +58,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await kibanaServer.importExport.unload( 'x-pack/test/functional/fixtures/kbn_archiver/discover/feature_controls/security' ); + await kibanaServer.savedObjects.cleanStandardList(); }); describe('global discover all privileges', () => { @@ -444,12 +445,14 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await security.user.delete('no_discover_privileges_user'); }); - it(`shows 403`, async () => { + it('shows 403', async () => { await PageObjects.common.navigateToUrl('discover', '', { ensureCurrentUrl: false, shouldLoginIfPrompted: false, }); - await PageObjects.error.expectForbidden(); + await retry.try(async () => { + await PageObjects.error.expectForbidden(); + }); }); }); @@ -505,6 +508,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { }); after(async () => { + await kibanaServer.uiSettings.unset('defaultIndex'); await esSupertest .post('/_aliases') .send({ diff --git a/x-pack/test/functional/apps/discover/visualize_field.ts b/x-pack/test/functional/apps/discover/visualize_field.ts index 453c3467c923a..8c3ebd7fd06a2 100644 --- a/x-pack/test/functional/apps/discover/visualize_field.ts +++ b/x-pack/test/functional/apps/discover/visualize_field.ts @@ -6,6 +6,7 @@ */ import expect from '@kbn/expect'; +import { WebElementWrapper } from '../../../../../test/functional/services/lib/web_element_wrapper'; import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getPageObjects, getService }: FtrProviderContext) { @@ -91,6 +92,27 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { expect(await queryBar.getQueryString()).to.equal('machine.os : ios'); }); + it('should visualize correctly using breakdown field', async () => { + await PageObjects.discover.chooseBreakdownField('extension.raw'); + await PageObjects.header.waitUntilLoadingHasFinished(); + await testSubjects.click('unifiedHistogramEditVisualization'); + await PageObjects.header.waitUntilLoadingHasFinished(); + await retry.try(async () => { + const breakdownLabel = await testSubjects.find( + 'lnsDragDrop_draggable-Top 3 values of extension.raw' + ); + + const lnsWorkspace = await testSubjects.find('lnsWorkspace'); + const list = await lnsWorkspace.findAllByClassName('echLegendItem__label'); + const values = await Promise.all( + list.map((elem: WebElementWrapper) => elem.getVisibleText()) + ); + + expect(await breakdownLabel.getVisibleText()).to.eql('Top 3 values of extension.raw'); + expect(values).to.eql(['Other', 'png', 'css', 'jpg']); + }); + }); + it('should visualize correctly using adhoc data view', async () => { await PageObjects.discover.createAdHocDataView('logst', true); await PageObjects.header.waitUntilLoadingHasFinished(); diff --git a/x-pack/test/functional/apps/lens/group1/index.ts b/x-pack/test/functional/apps/lens/group1/index.ts index 622953098b725..03f4a4032154e 100644 --- a/x-pack/test/functional/apps/lens/group1/index.ts +++ b/x-pack/test/functional/apps/lens/group1/index.ts @@ -39,6 +39,7 @@ export default ({ getService, loadTestFile, getPageObjects }: FtrProviderContext before(async () => { await log.debug('Starting lens before method'); await browser.setWindowSize(1280, 1200); + await kibanaServer.savedObjects.cleanStandardList(); try { config.get('esTestCluster.ccs'); remoteEsArchiver = getService('remoteEsArchiver' as 'esArchiver'); @@ -67,6 +68,7 @@ export default ({ getService, loadTestFile, getPageObjects }: FtrProviderContext await PageObjects.timePicker.resetDefaultAbsoluteRangeViaUiSettings(); await kibanaServer.importExport.unload(fixtureDirs.lensBasic); await kibanaServer.importExport.unload(fixtureDirs.lensDefault); + await kibanaServer.savedObjects.cleanStandardList(); }); if (config.get('esTestCluster.ccs')) { diff --git a/x-pack/test/functional/apps/lens/group1/text_based_languages.ts b/x-pack/test/functional/apps/lens/group1/text_based_languages.ts index d4766916d0da8..138c5fea4b898 100644 --- a/x-pack/test/functional/apps/lens/group1/text_based_languages.ts +++ b/x-pack/test/functional/apps/lens/group1/text_based_languages.ts @@ -30,6 +30,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const defaultSettings = { 'discover:enableSql': true, + defaultIndex: 'log*', }; async function switchToTextBasedLanguage(language: string) { @@ -43,8 +44,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { describe('lens text based language tests', () => { before(async () => { - await kibanaServer.uiSettings.replace(defaultSettings); + await kibanaServer.uiSettings.update(defaultSettings); }); + it('should navigate to text based languages mode correctly', async () => { await switchToTextBasedLanguage('SQL'); expect(await testSubjects.exists('showQueryBarMenu')).to.be(false); diff --git a/x-pack/test/functional/apps/lens/group2/index.ts b/x-pack/test/functional/apps/lens/group2/index.ts index 477ad2f8561dd..20cb355735666 100644 --- a/x-pack/test/functional/apps/lens/group2/index.ts +++ b/x-pack/test/functional/apps/lens/group2/index.ts @@ -39,6 +39,7 @@ export default ({ getService, loadTestFile, getPageObjects }: FtrProviderContext before(async () => { await log.debug('Starting lens before method'); await browser.setWindowSize(1280, 1200); + await kibanaServer.savedObjects.cleanStandardList(); try { config.get('esTestCluster.ccs'); remoteEsArchiver = getService('remoteEsArchiver' as 'esArchiver'); @@ -67,6 +68,7 @@ export default ({ getService, loadTestFile, getPageObjects }: FtrProviderContext await PageObjects.timePicker.resetDefaultAbsoluteRangeViaUiSettings(); await kibanaServer.importExport.unload(fixtureDirs.lensBasic); await kibanaServer.importExport.unload(fixtureDirs.lensDefault); + await kibanaServer.savedObjects.cleanStandardList(); }); loadTestFile(require.resolve('./add_to_dashboard')); diff --git a/x-pack/test/functional/apps/lens/group2/show_underlying_data.ts b/x-pack/test/functional/apps/lens/group2/show_underlying_data.ts index 29f684c1ebf37..41e327234009b 100644 --- a/x-pack/test/functional/apps/lens/group2/show_underlying_data.ts +++ b/x-pack/test/functional/apps/lens/group2/show_underlying_data.ts @@ -44,7 +44,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await testSubjects.existOrFail('unifiedHistogramChart'); // check the table columns const columns = await PageObjects.discover.getColumnHeaders(); - expect(columns).to.eql(['extension.raw', '@timestamp', 'bytes']); + expect(columns).to.eql(['@timestamp', 'extension.raw', 'bytes']); await browser.closeCurrentWindow(); await browser.switchToWindow(lensWindowHandler); }); @@ -142,7 +142,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await testSubjects.existOrFail('unifiedHistogramChart'); // check the columns const columns = await PageObjects.discover.getColumnHeaders(); - expect(columns).to.eql(['extension.raw', '@timestamp', 'memory']); + expect(columns).to.eql(['@timestamp', 'extension.raw', 'memory']); // check the query expect(await queryBar.getQueryString()).be.eql( '( ( bytes > 2000 ) AND ( ( extension.raw: "css" ) OR ( extension.raw: "gif" ) OR ( extension.raw: "jpg" ) ) )' @@ -175,7 +175,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await browser.switchToWindow(discoverWindowHandle); await PageObjects.header.waitUntilLoadingHasFinished(); await testSubjects.existOrFail('unifiedHistogramChart'); - // check the query expect(await queryBar.getQueryString()).be.eql( '( ( bytes > 4000 ) AND ( ( extension.raw: "css" ) OR ( extension.raw: "gif" ) OR ( extension.raw: "jpg" ) ) )' diff --git a/x-pack/test/functional/apps/lens/group2/show_underlying_data_dashboard.ts b/x-pack/test/functional/apps/lens/group2/show_underlying_data_dashboard.ts index a89b4c8727bc1..46fdc94879d03 100644 --- a/x-pack/test/functional/apps/lens/group2/show_underlying_data_dashboard.ts +++ b/x-pack/test/functional/apps/lens/group2/show_underlying_data_dashboard.ts @@ -50,7 +50,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await testSubjects.existOrFail('unifiedHistogramChart'); // check the table columns const columns = await PageObjects.discover.getColumnHeaders(); - expect(columns).to.eql(['ip', '@timestamp', 'bytes']); + expect(columns).to.eql(['@timestamp', 'ip', 'bytes']); await browser.closeCurrentWindow(); await browser.switchToWindow(dashboardWindowHandle); diff --git a/x-pack/test/functional/apps/lens/group3/index.ts b/x-pack/test/functional/apps/lens/group3/index.ts index 30c8624d876b5..315d905f300e9 100644 --- a/x-pack/test/functional/apps/lens/group3/index.ts +++ b/x-pack/test/functional/apps/lens/group3/index.ts @@ -39,6 +39,7 @@ export default ({ getService, loadTestFile, getPageObjects }: FtrProviderContext before(async () => { log.debug('Starting lens before method'); await browser.setWindowSize(1280, 1200); + await kibanaServer.savedObjects.cleanStandardList(); try { config.get('esTestCluster.ccs'); remoteEsArchiver = getService('remoteEsArchiver' as 'esArchiver'); @@ -67,6 +68,7 @@ export default ({ getService, loadTestFile, getPageObjects }: FtrProviderContext await PageObjects.timePicker.resetDefaultAbsoluteRangeViaUiSettings(); await kibanaServer.importExport.unload(fixtureDirs.lensBasic); await kibanaServer.importExport.unload(fixtureDirs.lensDefault); + await kibanaServer.savedObjects.cleanStandardList(); }); loadTestFile(require.resolve('./colors')); diff --git a/x-pack/test/functional/apps/lens/open_in_lens/agg_based/heatmap.ts b/x-pack/test/functional/apps/lens/open_in_lens/agg_based/heatmap.ts index 8556ae601daf9..4f43bf466f892 100644 --- a/x-pack/test/functional/apps/lens/open_in_lens/agg_based/heatmap.ts +++ b/x-pack/test/functional/apps/lens/open_in_lens/agg_based/heatmap.ts @@ -67,19 +67,19 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { expect(debugState.legend!.items).to.eql([ { color: '#006837', - key: '0 - 25', - name: '0 - 25', + key: '1,322 - 1,717.5', + name: '1,322 - 1,717.5', }, - { color: '#86CB66', key: '25 - 50', name: '25 - 50' }, + { color: '#86CB66', key: '1,717.5 - 2,113', name: '1,717.5 - 2,113' }, { color: '#FEFEBD', - key: '50 - 75', - name: '50 - 75', + key: '2,113 - 2,508.5', + name: '2,113 - 2,508.5', }, { color: '#F88D52', - key: '75 - 100', - name: '75 - 100', + key: '2,508.5 - 2,904', + name: '2,508.5 - 2,904', }, ]); }); @@ -125,33 +125,33 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { expect(debugState.legend!.items).to.eql([ { color: '#006837', - key: '0 - 16.67', - name: '0 - 16.67', + key: '1,322 - 1,585.67', + name: '1,322 - 1,585.67', }, { color: '#4CB15D', - key: '16.67 - 33.33', - name: '16.67 - 33.33', + key: '1,585.67 - 1,849.33', + name: '1,585.67 - 1,849.33', }, { color: '#B7E075', - key: '33.33 - 50', - name: '33.33 - 50', + key: '1,849.33 - 2,113', + name: '1,849.33 - 2,113', }, { color: '#FEFEBD', - key: '50 - 66.67', - name: '50 - 66.67', + key: '2,113 - 2,376.67', + name: '2,113 - 2,376.67', }, { color: '#FDBF6F', - key: '66.67 - 83.33', - name: '66.67 - 83.33', + key: '2,376.67 - 2,640.33', + name: '2,376.67 - 2,640.33', }, { color: '#EA5839', - key: '83.33 - 100', - name: '83.33 - 100', + key: '2,640.33 - 2,904', + name: '2,640.33 - 2,904', }, ]); }); diff --git a/x-pack/test/functional/apps/lens/open_in_lens/agg_based/index.ts b/x-pack/test/functional/apps/lens/open_in_lens/agg_based/index.ts index c7380d2388a35..787b7bf986b8f 100644 --- a/x-pack/test/functional/apps/lens/open_in_lens/agg_based/index.ts +++ b/x-pack/test/functional/apps/lens/open_in_lens/agg_based/index.ts @@ -76,5 +76,6 @@ export default function ({ loadTestFile, getService, getPageObjects }: FtrProvid loadTestFile(require.resolve('./goal')); loadTestFile(require.resolve('./table')); loadTestFile(require.resolve('./heatmap')); + loadTestFile(require.resolve('./navigation')); }); } diff --git a/x-pack/test/functional/apps/lens/open_in_lens/agg_based/navigation.ts b/x-pack/test/functional/apps/lens/open_in_lens/agg_based/navigation.ts new file mode 100644 index 0000000000000..791edb26888b2 --- /dev/null +++ b/x-pack/test/functional/apps/lens/open_in_lens/agg_based/navigation.ts @@ -0,0 +1,98 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; + +export default function ({ getPageObjects, getService }: FtrProviderContext) { + const { visualize, lens, timePicker } = getPageObjects(['visualize', 'lens', 'timePicker']); + + const testSubjects = getService('testSubjects'); + const retry = getService('retry'); + + describe('Visualize to Lens and back', function describeIndexTests() { + before(async () => { + await visualize.initTests(); + }); + + before(async () => { + await visualize.navigateToNewAggBasedVisualization(); + await visualize.clickLineChart(); + await visualize.clickNewSearch(); + await timePicker.setDefaultAbsoluteRange(); + }); + + it('should let the user return back to Visualize if no changes were made', async () => { + await visualize.navigateToLensFromAnotherVisulization(); + await lens.waitForVisualization('xyVisChart'); + + await retry.try(async () => { + expect(await lens.getLayerCount()).to.be(1); + }); + + await testSubjects.click('lnsApp_goBackToAppButton'); + + // it should be back to visualize now + await retry.try(async () => { + expect(await visualize.hasNavigateToLensButton()).to.eql(true); + }); + }); + + it('should let the user return back to Visualize but show a warning modal if changes happened in Lens', async () => { + await visualize.navigateToLensFromAnotherVisulization(); + await lens.waitForVisualization('xyVisChart'); + + await retry.try(async () => { + expect(await lens.getLayerCount()).to.be(1); + }); + + // Make a change in Lens + await lens.configureDimension({ + dimension: 'lnsXY_xDimensionPanel > lns-empty-dimension', + operation: 'date_histogram', + field: '@timestamp', + }); + + await testSubjects.click('lnsApp_goBackToAppButton'); + + // check that there's a modal + await retry.try(async () => { + await testSubjects.existOrFail('lnsApp_discardChangesModalOrigin'); + }); + // click on discard + await testSubjects.click('confirmModalConfirmButton'); + + await retry.try(async () => { + expect(await visualize.hasNavigateToLensButton()).to.eql(true); + }); + }); + + it('should let the user return back to Visualize with no modal if changes have been saved in Lens', async () => { + await visualize.navigateToLensFromAnotherVisulization(); + await lens.waitForVisualization('xyVisChart'); + + await retry.try(async () => { + expect(await lens.getLayerCount()).to.be(1); + }); + + // Make a change in Lens + await lens.configureDimension({ + dimension: 'lnsXY_xDimensionPanel > lns-empty-dimension', + operation: 'date_histogram', + field: '@timestamp', + }); + + // save in Lens new changes + await lens.save('Migrated Viz saved in Lens'); + + await testSubjects.click('lnsApp_goBackToAppButton'); + await retry.try(async () => { + expect(await visualize.hasNavigateToLensButton()).to.eql(true); + }); + }); + }); +} diff --git a/x-pack/test/functional/apps/ml/short_tests/model_management/model_list.ts b/x-pack/test/functional/apps/ml/short_tests/model_management/model_list.ts index 2743df8a81949..04a23cd33e1c7 100644 --- a/x-pack/test/functional/apps/ml/short_tests/model_management/model_list.ts +++ b/x-pack/test/functional/apps/ml/short_tests/model_management/model_list.ts @@ -93,7 +93,7 @@ export default function ({ getService }: FtrProviderContext) { await ml.trainedModelsTable.assertPipelinesTabContent(false); }); - it('displays the built-in model and no actions are enabled', async () => { + it('displays the built-in model with only Test action enabled', async () => { await ml.testExecution.logTestStep('should display the model in the table'); await ml.trainedModelsTable.filterWithSearchString(builtInModelData.modelId, 1); @@ -121,6 +121,21 @@ export default function ({ getService }: FtrProviderContext) { builtInModelData.modelId, false ); + + await ml.testExecution.logTestStep('should have enabled the button that opens Test flyout'); + await ml.trainedModelsTable.assertModelTestButtonExists(builtInModelData.modelId, true); + + await ml.trainedModelsTable.testModel( + 'lang_ident', + builtInModelData.modelId, + { + inputText: 'Goedemorgen! Ik ben een appel.', + }, + { + title: 'This looks like Dutch,Flemish', + topLang: { code: 'nl', minProbability: 0.9 }, + } + ); }); it('displays a model with an ingest pipeline and delete action is disabled', async () => { diff --git a/x-pack/test/functional/apps/transform/creation_index_pattern.ts b/x-pack/test/functional/apps/transform/creation_index_pattern.ts index 4dee8e3af8262..5c240b2c0403c 100644 --- a/x-pack/test/functional/apps/transform/creation_index_pattern.ts +++ b/x-pack/test/functional/apps/transform/creation_index_pattern.ts @@ -16,10 +16,11 @@ import { PivotTransformTestData, } from '.'; -export default function ({ getService }: FtrProviderContext) { +export default function ({ getService, getPageObjects }: FtrProviderContext) { const canvasElement = getService('canvasElement'); const esArchiver = getService('esArchiver'); const transform = getService('transform'); + const PageObjects = getPageObjects(['discover']); describe('creation_index_pattern', function () { before(async () => { @@ -698,6 +699,7 @@ export default function ({ getService }: FtrProviderContext) { await transform.testExecution.logTestStep('should navigate to discover'); await transform.table.clickTransformRowAction(testData.transformId, 'Discover'); + await PageObjects.discover.waitUntilSearchingHasFinished(); if (testData.discoverAdjustSuperDatePicker) { await transform.discover.assertNoResults(testData.destinationIndex); diff --git a/x-pack/test/functional/apps/upgrade_assistant/deprecation_pages.ts b/x-pack/test/functional/apps/upgrade_assistant/deprecation_pages.ts index 756fc43055f17..bf1794863a2d0 100644 --- a/x-pack/test/functional/apps/upgrade_assistant/deprecation_pages.ts +++ b/x-pack/test/functional/apps/upgrade_assistant/deprecation_pages.ts @@ -20,10 +20,8 @@ export default function upgradeAssistantFunctionalTests({ const security = getService('security'); const log = getService('log'); - // FLAKY: https://github.com/elastic/kibana/issues/144885 - describe.skip('Deprecation pages', function () { + describe('Deprecation pages', function () { this.tags('skipFirefox'); - this.timeout(32000); before(async () => { await security.testUser.setRoles(['superuser']); @@ -64,7 +62,7 @@ export default function upgradeAssistantFunctionalTests({ }); // Wait for the cluster settings to be reflected to the ES nodes - await setTimeout(30000); + await setTimeout(12000); } catch (e) { log.debug('[Setup error] Error updating cluster settings'); throw e; diff --git a/x-pack/test/functional/config.base.js b/x-pack/test/functional/config.base.js index 7ed28068485b3..ea6a865ecbc58 100644 --- a/x-pack/test/functional/config.base.js +++ b/x-pack/test/functional/config.base.js @@ -11,11 +11,10 @@ import { services } from './services'; import { pageObjects } from './page_objects'; // Docker image to use for Fleet API integration tests. -// This hash comes from the latest successful build of the Snapshot Distribution of the Package Registry, for -// example: https://beats-ci.elastic.co/blue/organizations/jenkins/Ingest-manager%2Fpackage-storage/detail/snapshot/74/pipeline/257#step-302-log-1. -// It should be updated any time there is a new Docker image published for the Snapshot Distribution of the Package Registry. -export const dockerImage = - 'docker.elastic.co/package-registry/distribution:production-v2-experimental'; +// This hash comes from the latest successful build of the Production Distribution of the Package Registry, for +// example: https://internal-ci.elastic.co/blue/organizations/jenkins/package_storage%2Findexing-job/detail/main/1884/pipeline/147. +// It should be updated any time there is a new package published. +export const dockerImage = 'docker.elastic.co/package-registry/distribution:production'; // the default export of config files must be a config provider // that returns an object with the projects config values @@ -36,7 +35,11 @@ export default async function ({ readConfigFile }) { esTestCluster: { license: 'trial', from: 'snapshot', - serverArgs: ['path.repo=/tmp/', 'xpack.security.authc.api_key.enabled=true'], + serverArgs: [ + 'path.repo=/tmp/', + 'xpack.security.authc.api_key.enabled=true', + 'cluster.routing.allocation.disk.threshold_enabled=true', // make sure disk thresholds are enabled for UA cluster testing + ], }, kbnTestServer: { @@ -172,6 +175,9 @@ export default async function ({ readConfigFile }) { connectors: { pathname: '/app/management/insightsAndAlerting/triggersActionsConnectors/', }, + triggersActions: { + pathname: '/app/management/insightsAndAlerting/triggersActions', + }, }, // choose where screenshots should be saved diff --git a/x-pack/test/functional/es_archives/infra/8.0.0/hosts_and_network/data.json.gz b/x-pack/test/functional/es_archives/infra/8.0.0/hosts_and_network/data.json.gz new file mode 100644 index 0000000000000..3a7f6b2d48b98 Binary files /dev/null and b/x-pack/test/functional/es_archives/infra/8.0.0/hosts_and_network/data.json.gz differ diff --git a/x-pack/test/functional/es_archives/infra/8.0.0/hosts_and_network/mappings.json b/x-pack/test/functional/es_archives/infra/8.0.0/hosts_and_network/mappings.json new file mode 100644 index 0000000000000..a5747ad5df17e --- /dev/null +++ b/x-pack/test/functional/es_archives/infra/8.0.0/hosts_and_network/mappings.json @@ -0,0 +1,325 @@ +{ + "type": "index", + "value": { + "aliases": { + }, + "index": "metricbeat-8.7.0", + "mappings": { + "date_detection": false, + "dynamic_templates": [ + { + "labels": { + "mapping": { + "type": "keyword" + }, + "match_mapping_type": "string", + "path_match": "labels.*" + } + }, + { + "strings_as_keyword": { + "mapping": { + "ignore_above": 1024, + "type": "keyword" + }, + "match_mapping_type": "string" + } + } + ], + "properties": { + "@timestamp": { + "type": "date" + }, + "event": { + "properties": { + "dataset": { + "ignore_above": 256, + "type": "keyword" + }, + "module": { + "ignore_above": 256, + "type": "keyword" + } + } + }, + "host": { + "properties": { + "architecture": { + "type": "keyword", + "ignore_above": 1024 + }, + "containerized": { + "type": "boolean" + }, + "cpu": { + "properties": { + "usage": { + "type": "scaled_float", + "scaling_factor": 1000 + } + } + }, + "disk": { + "properties": { + "read": { + "properties": { + "bytes": { + "type": "long" + } + } + }, + "write": { + "properties": { + "bytes": { + "type": "long" + } + } + } + } + }, + "domain": { + "type": "keyword", + "ignore_above": 1024 + }, + "geo": { + "properties": { + "city_name": { + "type": "keyword", + "ignore_above": 1024 + }, + "continent_code": { + "type": "keyword", + "ignore_above": 1024 + }, + "continent_name": { + "type": "keyword", + "ignore_above": 1024 + }, + "country_iso_code": { + "type": "keyword", + "ignore_above": 1024 + }, + "country_name": { + "type": "keyword", + "ignore_above": 1024 + }, + "location": { + "type": "geo_point" + }, + "name": { + "type": "keyword", + "ignore_above": 1024 + }, + "postal_code": { + "type": "keyword", + "ignore_above": 1024 + }, + "region_iso_code": { + "type": "keyword", + "ignore_above": 1024 + }, + "region_name": { + "type": "keyword", + "ignore_above": 1024 + }, + "timezone": { + "type": "keyword", + "ignore_above": 1024 + } + } + }, + "hostname": { + "type": "keyword", + "ignore_above": 1024 + }, + "id": { + "type": "keyword", + "ignore_above": 1024 + }, + "ip": { + "type": "ip" + }, + "mac": { + "type": "keyword", + "ignore_above": 1024 + }, + "name": { + "type": "keyword", + "ignore_above": 1024 + }, + "network": { + "properties": { + "egress": { + "properties": { + "bytes": { + "type": "long" + }, + "packets": { + "type": "long" + } + } + }, + "ingress": { + "properties": { + "bytes": { + "type": "long" + }, + "packets": { + "type": "long" + } + } + } + } + }, + "os": { + "properties": { + "build": { + "type": "keyword", + "ignore_above": 1024 + }, + "codename": { + "type": "keyword", + "ignore_above": 1024 + }, + "family": { + "type": "keyword", + "ignore_above": 1024 + }, + "full": { + "type": "keyword", + "ignore_above": 1024, + "fields": { + "text": { + "type": "match_only_text" + } + } + }, + "kernel": { + "type": "keyword", + "ignore_above": 1024 + }, + "name": { + "type": "keyword", + "ignore_above": 1024, + "fields": { + "text": { + "type": "match_only_text" + } + } + }, + "platform": { + "type": "keyword", + "ignore_above": 1024 + }, + "type": { + "type": "keyword", + "ignore_above": 1024 + }, + "version": { + "type": "keyword", + "ignore_above": 1024 + } + } + }, + "type": { + "type": "keyword", + "ignore_above": 1024 + }, + "uptime": { + "type": "long" + } + } + }, + "labels": { + "properties": { + "eventId": { + "type": "keyword" + }, + "groupId": { + "type": "keyword" + } + } + }, + "metricset": { + "properties": { + "period": { + "type": "long" + } + } + }, + "system": { + "properties": { + "cpu": { + "properties": { + "cores": { + "type": "long" + }, + "system": { + "properties": { + "pct": { + "scaling_factor": 1000, + "type": "scaled_float" + } + } + }, + "total": { + "properties": { + "norm": { + "properties": { + "pct": { + "scaling_factor": 1000, + "type": "scaled_float" + } + } + } + } + }, + "user": { + "properties": { + "pct": { + "scaling_factor": 1000, + "type": "scaled_float" + } + } + } + } + }, + "network": { + "properties": { + "in": { + "properties": { + "bytes": { + "type": "long" + } + } + }, + "name": { + "ignore_above": 256, + "type": "keyword" + }, + "out": { + "properties": { + "bytes": { + "type": "long" + } + } + } + } + } + } + } + } + }, + "settings": { + "index": { + "mapping": { + "total_fields": { + "limit": "10000" + } + }, + "number_of_replicas": "0", + "number_of_shards": "1" + } + } + } +} diff --git a/x-pack/test/functional/page_objects/infra_logs_page.ts b/x-pack/test/functional/page_objects/infra_logs_page.ts index 0bcbff031005c..040495cd754c3 100644 --- a/x-pack/test/functional/page_objects/infra_logs_page.ts +++ b/x-pack/test/functional/page_objects/infra_logs_page.ts @@ -8,7 +8,7 @@ import { FlyoutOptionsUrlState } from '@kbn/infra-plugin/public/containers/logs/log_flyout'; import { LogPositionUrlState } from '@kbn/infra-plugin/public/containers/logs/log_position'; import querystring from 'querystring'; -import { encode, RisonValue } from 'rison-node'; +import { encode } from '@kbn/rison'; import { FtrProviderContext } from '../ftr_provider_context'; export interface TabsParams { @@ -37,7 +37,7 @@ export function InfraLogsPageProvider({ getPageObjects, getService }: FtrProvide for (const key in params) { if (params.hasOwnProperty(key)) { - const value = params[key] as unknown as RisonValue; + const value = params[key]; parsedParams[key] = encode(value); } } diff --git a/x-pack/test/functional/page_objects/observability_page.ts b/x-pack/test/functional/page_objects/observability_page.ts index 0177939ec3d15..bfbd1741d9f11 100644 --- a/x-pack/test/functional/page_objects/observability_page.ts +++ b/x-pack/test/functional/page_objects/observability_page.ts @@ -11,6 +11,7 @@ import { FtrProviderContext } from '../ftr_provider_context'; export function ObservabilityPageProvider({ getService, getPageObjects }: FtrProviderContext) { const testSubjects = getService('testSubjects'); + const textValue = 'Foobar'; return { async clickSolutionNavigationEntry(appId: string, navId: string) { @@ -44,6 +45,7 @@ export function ObservabilityPageProvider({ getService, getPageObjects }: FtrPro }, async expectAddCommentButton() { + await testSubjects.setValue('add-comment', textValue); const button = await testSubjects.find('submit-comment', 20000); const disabledAttr = await button.getAttribute('disabled'); expect(disabledAttr).to.be(null); diff --git a/x-pack/test/functional/services/actions/api.ts b/x-pack/test/functional/services/actions/api.ts index a6ea0a2666119..89eef12cb3370 100644 --- a/x-pack/test/functional/services/actions/api.ts +++ b/x-pack/test/functional/services/actions/api.ts @@ -9,6 +9,7 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export function ActionsAPIServiceProvider({ getService }: FtrProviderContext) { const kbnSupertest = getService('supertest'); + const log = getService('log'); return { async createConnector({ @@ -37,10 +38,24 @@ export function ActionsAPIServiceProvider({ getService }: FtrProviderContext) { }, async deleteConnector(id: string) { - return kbnSupertest + log.debug(`Deleting connector with id '${id}'...`); + const rsp = kbnSupertest .delete(`/api/actions/connector/${id}`) .set('kbn-xsrf', 'foo') .expect(204, ''); + log.debug('> Connector deleted.'); + return rsp; + }, + + async deleteAllConnectors() { + const { body } = await kbnSupertest + .get(`/api/actions/connectors`) + .set('kbn-xsrf', 'foo') + .expect(200); + + for (const connector of body) { + await this.deleteConnector(connector.id); + } }, }; } diff --git a/x-pack/test/functional/services/ml/alerting.ts b/x-pack/test/functional/services/ml/alerting.ts index 7007f0d00f284..300e009d256fb 100644 --- a/x-pack/test/functional/services/ml/alerting.ts +++ b/x-pack/test/functional/services/ml/alerting.ts @@ -26,9 +26,16 @@ export function MachineLearningAlertingProvider( return { async selectAnomalyDetectionAlertType() { - await testSubjects.click('xpack.ml.anomaly_detection_alert-SelectOption'); await retry.tryForTime(5000, async () => { - await testSubjects.existOrFail(`mlAnomalyAlertForm`); + await testSubjects.click('xpack.ml.anomaly_detection_alert-SelectOption'); + await testSubjects.existOrFail(`mlAnomalyAlertForm`, { timeout: 1000 }); + }); + }, + + async selectAnomalyDetectionJobHealthAlertType() { + await retry.tryForTime(5000, async () => { + await testSubjects.click('xpack.ml.anomaly_detection_jobs_health-SelectOption'); + await testSubjects.existOrFail(`mlJobsHealthAlertingRuleForm`, { timeout: 1000 }); }); }, @@ -50,8 +57,14 @@ export function MachineLearningAlertingProvider( }, async selectResultType(resultType: string) { - await testSubjects.click(`mlAnomalyAlertResult_${resultType}`); - await this.assertResultTypeSelection(resultType); + if ( + (await testSubjects.exists(`mlAnomalyAlertResult_${resultType}_selected`, { + timeout: 1000, + })) === false + ) { + await testSubjects.click(`mlAnomalyAlertResult_${resultType}`); + await this.assertResultTypeSelection(resultType); + } }, async assertResultTypeSelection(resultType: string) { @@ -168,5 +181,64 @@ export function MachineLearningAlertingProvider( mlApi.assertResponseStatusCode(204, status, body); } }, + + async openNotifySelection() { + await retry.tryForTime(5000, async () => { + await testSubjects.click('notifyWhenSelect'); + await testSubjects.existOrFail('onActionGroupChange', { timeout: 1000 }); + }); + }, + + async setRuleName(rulename: string) { + await testSubjects.setValue('ruleNameInput', rulename); + }, + + async scrollRuleNameIntoView() { + await testSubjects.scrollIntoView('ruleNameInput'); + }, + + async selectSlackConnectorType() { + await retry.tryForTime(5000, async () => { + await testSubjects.click('.slack-alerting-ActionTypeSelectOption'); + await testSubjects.existOrFail('createActionConnectorButton-0', { timeout: 1000 }); + }); + }, + + async clickCreateConnectorButton() { + await retry.tryForTime(5000, async () => { + await testSubjects.click('createActionConnectorButton-0'); + await testSubjects.existOrFail('connectorAddModal', { timeout: 1000 }); + }); + }, + + async setConnectorName(connectorname: string) { + await testSubjects.setValue('nameInput', connectorname); + }, + + async setWebhookUrl(webhookurl: string) { + await testSubjects.setValue('slackWebhookUrlInput', webhookurl); + }, + + async clickSaveActionButton() { + await retry.tryForTime(5000, async () => { + await testSubjects.click('saveActionButtonModal'); + await testSubjects.existOrFail('addNewActionConnectorActionGroup-0', { timeout: 1000 }); + }); + }, + + async clickCancelSaveRuleButton() { + await retry.tryForTime(5000, async () => { + await testSubjects.click('cancelSaveRuleButton'); + await testSubjects.existOrFail('confirmModalTitleText', { timeout: 1000 }); + await testSubjects.click('confirmModalConfirmButton'); + }); + }, + + async openAddRuleVariable() { + await retry.tryForTime(5000, async () => { + await testSubjects.click('messageAddVariableButton'); + await testSubjects.existOrFail('variableMenuButton-alert.actionGroup', { timeout: 1000 }); + }); + }, }; } diff --git a/x-pack/test/functional/services/ml/api.ts b/x-pack/test/functional/services/ml/api.ts index 2e61af5cc60a1..2eea4aeee484e 100644 --- a/x-pack/test/functional/services/ml/api.ts +++ b/x-pack/test/functional/services/ml/api.ts @@ -860,12 +860,13 @@ export function MachineLearningAPIProvider({ getService }: FtrProviderContext) { async createAndRunAnomalyDetectionLookbackJob( jobConfig: Job, datafeedConfig: Datafeed, - space?: string + options: { space?: string; end?: string } = {} ) { + const { space = undefined, end = `${Date.now()}` } = options; await this.createAnomalyDetectionJob(jobConfig, space); await this.createDatafeed(datafeedConfig, space); await this.openAnomalyDetectionJob(jobConfig.job_id); - await this.startDatafeed(datafeedConfig.datafeed_id, { start: '0', end: `${Date.now()}` }); + await this.startDatafeed(datafeedConfig.datafeed_id, { start: '0', end }); await this.waitForDatafeedState(datafeedConfig.datafeed_id, DATAFEED_STATE.STOPPED); await this.waitForJobState(jobConfig.job_id, JOB_STATE.CLOSED); }, diff --git a/x-pack/test/functional/services/ml/common_ui.ts b/x-pack/test/functional/services/ml/common_ui.ts index ef69f909437c1..138450e56a426 100644 --- a/x-pack/test/functional/services/ml/common_ui.ts +++ b/x-pack/test/functional/services/ml/common_ui.ts @@ -402,17 +402,28 @@ export function MachineLearningCommonUIProvider({ }); }, - async invokeTableRowAction(rowSelector: string, actionTestSubject: string) { + async invokeTableRowAction( + rowSelector: string, + actionTestSubject: string, + fromContextMenu: boolean = true + ) { await retry.tryForTime(30 * 1000, async () => { - await this.ensureAllMenuPopoversClosed(); - await testSubjects.click(`${rowSelector} > euiCollapsedItemActionsButton`); - await find.existsByCssSelector('euiContextMenuPanel'); + if (fromContextMenu) { + await this.ensureAllMenuPopoversClosed(); + + await testSubjects.click(`${rowSelector} > euiCollapsedItemActionsButton`); + await find.existsByCssSelector('euiContextMenuPanel'); - const isEnabled = await testSubjects.isEnabled(actionTestSubject); + const isEnabled = await testSubjects.isEnabled(actionTestSubject); - expect(isEnabled).to.eql(true, `Expected action "${actionTestSubject}" to be enabled.`); + expect(isEnabled).to.eql(true, `Expected action "${actionTestSubject}" to be enabled.`); - await testSubjects.click(actionTestSubject); + await testSubjects.click(actionTestSubject); + } else { + const isEnabled = await testSubjects.isEnabled(`${rowSelector} > ${actionTestSubject}`); + expect(isEnabled).to.eql(true, `Expected action "${actionTestSubject}" to be enabled.`); + await testSubjects.click(`${rowSelector} > ${actionTestSubject}`); + } }); }, }; diff --git a/x-pack/test/functional/services/ml/index.ts b/x-pack/test/functional/services/ml/index.ts index 9452baa324898..e360bfecf3a32 100644 --- a/x-pack/test/functional/services/ml/index.ts +++ b/x-pack/test/functional/services/ml/index.ts @@ -131,7 +131,7 @@ export function MachineLearningProvider(context: FtrProviderContext) { const alerting = MachineLearningAlertingProvider(context, api, commonUI); const swimLane = SwimLaneProvider(context); const trainedModels = TrainedModelsProvider(context, commonUI); - const trainedModelsTable = TrainedModelsTableProvider(context, commonUI); + const trainedModelsTable = TrainedModelsTableProvider(context, commonUI, trainedModels); const mlNodesPanel = MlNodesPanelProvider(context); const notifications = NotificationsProvider(context, commonUI, tableService); diff --git a/x-pack/test/functional/services/ml/trained_models.ts b/x-pack/test/functional/services/ml/trained_models.ts index c0fb549f97864..09503f7415ed4 100644 --- a/x-pack/test/functional/services/ml/trained_models.ts +++ b/x-pack/test/functional/services/ml/trained_models.ts @@ -5,13 +5,76 @@ * 2.0. */ +// eslint-disable-next-line max-classes-per-file import expect from '@kbn/expect'; +import { ProvidedType } from '@kbn/test'; import { FtrProviderContext } from '../../ftr_provider_context'; import { MlCommonUI } from './common_ui'; +export type TrainedModelsActions = ProvidedType; + +export type ModelType = 'lang_ident'; + +export interface MappedInputParams { + lang_ident: LangIdentInput; +} + +export interface MappedOutput { + lang_ident: LangIdentOutput; +} + export function TrainedModelsProvider({ getService }: FtrProviderContext, mlCommonUI: MlCommonUI) { const testSubjects = getService('testSubjects'); const retry = getService('retry'); + const browser = getService('browser'); + + class TestModelFactory { + public static createAssertionInstance(modelType: ModelType) { + switch (modelType) { + case 'lang_ident': + return new TestLangIdentModel(); + default: + throw new Error(`Testing class for ${modelType} is not implemented`); + } + } + } + + class TestModelBase implements TestTrainedModel { + async setRequiredInput(input: BaseInput): Promise { + await testSubjects.setValue('mlTestModelInputText', input.inputText); + await this.assertTestInputText(input.inputText); + } + + async assertTestInputText(expectedText: string) { + const actualValue = await testSubjects.getAttribute('mlTestModelInputText', 'value'); + expect(actualValue).to.eql( + expectedText, + `Expected input text to equal ${expectedText}, got ${actualValue}` + ); + } + + assertModelOutput(expectedOutput: unknown): Promise { + throw new Error('assertModelOutput has to be implemented per model type'); + } + } + + class TestLangIdentModel + extends TestModelBase + implements TestTrainedModel + { + async assertModelOutput(expectedOutput: LangIdentOutput) { + const title = await testSubjects.getVisibleText('mlTestModelLangIdentTitle'); + expect(title).to.eql(expectedOutput.title); + + const values = await testSubjects.findAll('mlTestModelLangIdentInputValue'); + const topValue = await values[0].getVisibleText(); + expect(topValue).to.eql(expectedOutput.topLang.code); + + const probabilities = await testSubjects.findAll('mlTestModelLangIdentInputProbability'); + const topProbability = Number(await probabilities[0].getVisibleText()); + expect(topProbability).to.above(expectedOutput.topLang.minProbability); + } + } return { async assertStats(expectedTotalCount: number) { @@ -28,5 +91,80 @@ export function TrainedModelsProvider({ getService }: FtrProviderContext, mlComm async assertRowsNumberPerPage(rowsNumber: 10 | 25 | 100) { await mlCommonUI.assertRowsNumberPerPage('mlModelsTableContainer', rowsNumber); }, + + async assertTestButtonEnabled(expectedValue: boolean = false) { + const isEnabled = await testSubjects.isEnabled('mlTestModelTestButton'); + expect(isEnabled).to.eql( + expectedValue, + `Expected trained model "Test" button to be '${ + expectedValue ? 'enabled' : 'disabled' + }' (got '${isEnabled ? 'enabled' : 'disabled'}')` + ); + }, + + async testModel() { + await testSubjects.click('mlTestModelTestButton'); + }, + + async assertTestInputText(expectedText: string) { + const actualValue = await testSubjects.getAttribute('mlTestModelInputText', 'value'); + expect(actualValue).to.eql( + expectedText, + `Expected input text to equal ${expectedText}, got ${actualValue}` + ); + }, + + async waitForResultsToLoad() { + await testSubjects.waitForEnabled('mlTestModelTestButton'); + await retry.tryForTime(5000, async () => { + await testSubjects.existOrFail(`mlTestModelOutput`); + }); + }, + + async testModelOutput( + modelType: ModelType, + inputParams: MappedInputParams[typeof modelType], + expectedOutput: MappedOutput[typeof modelType] + ) { + await this.assertTestButtonEnabled(false); + + const modelTest = TestModelFactory.createAssertionInstance(modelType); + await modelTest.setRequiredInput(inputParams); + + await this.assertTestButtonEnabled(true); + await this.testModel(); + await this.waitForResultsToLoad(); + + await modelTest.assertModelOutput(expectedOutput); + + await this.ensureTestFlyoutClosed(); + }, + + async ensureTestFlyoutClosed() { + await retry.tryForTime(5000, async () => { + await browser.pressKeys(browser.keys.ESCAPE); + await testSubjects.missingOrFail('mlTestModelsFlyout'); + }); + }, }; } + +export interface BaseInput { + inputText: string; +} + +export type LangIdentInput = BaseInput; + +export interface LangIdentOutput { + title: string; + topLang: { code: string; minProbability: number }; +} + +/** + * Interface that needed to be implemented by all model types + */ +interface TestTrainedModel { + setRequiredInput(input: Input): Promise; + assertTestInputText(inputText: Input['inputText']): Promise; + assertModelOutput(expectedOutput: Output): Promise; +} diff --git a/x-pack/test/functional/services/ml/trained_models_table.ts b/x-pack/test/functional/services/ml/trained_models_table.ts index 6f2b76be1bdb1..8179dc7a5986b 100644 --- a/x-pack/test/functional/services/ml/trained_models_table.ts +++ b/x-pack/test/functional/services/ml/trained_models_table.ts @@ -12,6 +12,7 @@ import { upperFirst } from 'lodash'; import { WebElementWrapper } from '../../../../../test/functional/services/lib/web_element_wrapper'; import type { FtrProviderContext } from '../../ftr_provider_context'; import type { MlCommonUI } from './common_ui'; +import { MappedInputParams, MappedOutput, ModelType, TrainedModelsActions } from './trained_models'; export interface TrainedModelRowData { id: string; @@ -23,7 +24,8 @@ export type MlTrainedModelsTable = ProvidedType { - describe('Uptime app', function () { - describe('with generated data', () => { - loadTestFile(require.resolve('./synthetics_integration')); - }); - }); -}; diff --git a/x-pack/test/functional_synthetics/apps/uptime/synthetics_integration.ts b/x-pack/test/functional_synthetics/apps/uptime/synthetics_integration.ts deleted file mode 100644 index b19d4fcb7668e..0000000000000 --- a/x-pack/test/functional_synthetics/apps/uptime/synthetics_integration.ts +++ /dev/null @@ -1,772 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import expect from '@kbn/expect'; -import { FullAgentPolicy } from '@kbn/fleet-plugin/common'; -import { FtrProviderContext } from '../../ftr_provider_context'; -import { skipIfNoDockerRegistry } from '../../helpers'; - -export default function (providerContext: FtrProviderContext) { - const { getPageObjects, getService } = providerContext; - const monitorName = 'Sample Synthetics integration'; - - const uptimePage = getPageObjects(['syntheticsIntegration']); - const testSubjects = getService('testSubjects'); - const uptimeService = getService('uptime'); - - const getSyntheticsPolicy = (agentFullPolicy: FullAgentPolicy) => - agentFullPolicy.inputs.find((input) => input.meta?.package?.name === 'synthetics'); - - const generatePolicy = ({ - agentFullPolicy, - version, - monitorType, - name, - config, - }: { - agentFullPolicy: FullAgentPolicy; - version: string; - monitorType: string; - name: string; - config: Record; - }) => ({ - data_stream: { - namespace: 'default', - }, - id: getSyntheticsPolicy(agentFullPolicy)?.id, - meta: { - package: { - name: 'synthetics', - version, - }, - }, - name, - package_policy_id: getSyntheticsPolicy(agentFullPolicy)?.package_policy_id, - revision: 1, - streams: [ - { - data_stream: { - dataset: monitorType, - elasticsearch: { - privileges: { - indices: ['auto_configure', 'create_doc', 'read'], - }, - }, - type: 'synthetics', - }, - id: `${getSyntheticsPolicy(agentFullPolicy)?.streams?.[0]?.id}`, - name, - type: monitorType, - enabled: true, - processors: [ - { - add_observer_metadata: { - geo: { - name: 'Fleet managed', - }, - }, - }, - { - add_fields: { - fields: { - 'monitor.fleet_managed': true, - }, - target: '', - }, - }, - ], - ...config, - }, - ...(monitorType === 'browser' - ? [ - { - data_stream: { - dataset: 'browser.network', - elasticsearch: { - privileges: { - indices: ['auto_configure', 'create_doc', 'read'], - }, - }, - type: 'synthetics', - }, - id: `${getSyntheticsPolicy(agentFullPolicy)?.streams?.[1]?.id}`, - processors: [ - { - add_observer_metadata: { - geo: { - name: 'Fleet managed', - }, - }, - }, - { - add_fields: { - fields: { - 'monitor.fleet_managed': true, - }, - target: '', - }, - }, - ], - }, - { - data_stream: { - dataset: 'browser.screenshot', - elasticsearch: { - privileges: { - indices: ['auto_configure', 'create_doc', 'read'], - }, - }, - type: 'synthetics', - }, - id: `${getSyntheticsPolicy(agentFullPolicy)?.streams?.[2]?.id}`, - processors: [ - { - add_observer_metadata: { - geo: { - name: 'Fleet managed', - }, - }, - }, - { - add_fields: { - fields: { - 'monitor.fleet_managed': true, - }, - target: '', - }, - }, - ], - }, - ] - : []), - ], - type: `synthetics/${monitorType}`, - use_output: 'default', - }); - - describe('When on the Synthetics Integration Policy Create Page', function () { - skipIfNoDockerRegistry(providerContext); - const basicConfig = { - name: monitorName, - apmServiceName: 'Sample APM Service', - tags: 'sample tag', - }; - - const generateHTTPConfig = (url: string) => ({ - ...basicConfig, - url, - }); - - const generateTCPorICMPConfig = (host: string) => ({ - ...basicConfig, - host, - }); - - const generateBrowserConfig = (config: Record): Record => ({ - ...basicConfig, - ...config, - }); - - describe('displays custom UI', () => { - before(async () => { - const version = await uptimeService.syntheticsPackage.getSyntheticsPackageVersion(); - await uptimePage.syntheticsIntegration.navigateToPackagePage(version!); - }); - - it('should display policy view', async () => { - await uptimePage.syntheticsIntegration.ensureIsOnPackagePage(); - }); - - it('prevent saving when integration name, url/host, or schedule is missing', async () => { - const saveButton = await uptimePage.syntheticsIntegration.findSaveButton(); - await saveButton.click(); - - await testSubjects.missingOrFail('postInstallAddAgentModal'); - }); - }); - - describe('create new policy', () => { - let version: string; - - beforeEach(async () => { - version = (await uptimeService.syntheticsPackage.getSyntheticsPackageVersion())!; - await uptimePage.syntheticsIntegration.navigateToPackagePage(version!); - await uptimeService.syntheticsPackage.deletePolicyByName(monitorName); - }); - - afterEach(async () => { - await uptimeService.syntheticsPackage.deletePolicyByName(monitorName); - }); - - it('allows saving when user enters a valid integration name and url/host', async () => { - // This test ensures that updates made to the Synthetics Policy are carried all the way through - // to the generated Agent Policy that is dispatch down to the Elastic Agent. - const config = generateHTTPConfig('http://elastic.co'); - await uptimePage.syntheticsIntegration.createBasicHTTPMonitorDetails(config); - await uptimePage.syntheticsIntegration.confirmAndSave(); - - await uptimePage.syntheticsIntegration.isPolicyCreatedSuccessfully(); - - const [agentPolicy] = await uptimeService.syntheticsPackage.getAgentPolicyList(); - const agentPolicyId = agentPolicy.id; - const agentFullPolicy = await uptimeService.syntheticsPackage.getFullAgentPolicy( - agentPolicyId - ); - - expect(getSyntheticsPolicy(agentFullPolicy)).to.eql( - generatePolicy({ - agentFullPolicy, - version, - name: monitorName, - monitorType: 'http', - config: { - max_redirects: 0, - 'response.include_body': 'on_error', - 'response.include_headers': true, - schedule: '@every 3m', - timeout: '16s', - urls: config.url, - 'service.name': config.apmServiceName, - tags: [config.tags], - 'check.request.method': 'GET', - __ui: { - is_tls_enabled: false, - is_zip_url_tls_enabled: false, - }, - }, - }) - ); - }); - - it('allows enabling tls with defaults', async () => { - // This test ensures that updates made to the Synthetics Policy are carried all the way through - // to the generated Agent Policy that is dispatch down to the Elastic Agent. - const config = generateHTTPConfig('http://elastic.co'); - - await uptimePage.syntheticsIntegration.createBasicHTTPMonitorDetails(config); - await uptimePage.syntheticsIntegration.enableTLS(); - await uptimePage.syntheticsIntegration.confirmAndSave(); - - await uptimePage.syntheticsIntegration.isPolicyCreatedSuccessfully(); - - const [agentPolicy] = await uptimeService.syntheticsPackage.getAgentPolicyList(); - const agentPolicyId = agentPolicy.id; - const agentFullPolicy = await uptimeService.syntheticsPackage.getFullAgentPolicy( - agentPolicyId - ); - - expect( - agentFullPolicy.inputs.find((input) => input.meta?.package?.name === 'synthetics') - ).to.eql( - generatePolicy({ - agentFullPolicy, - version, - name: monitorName, - monitorType: 'http', - config: { - max_redirects: 0, - 'check.request.method': 'GET', - 'response.include_body': 'on_error', - 'response.include_headers': true, - schedule: '@every 3m', - 'ssl.supported_protocols': ['TLSv1.1', 'TLSv1.2', 'TLSv1.3'], - 'ssl.verification_mode': 'full', - timeout: '16s', - urls: config.url, - 'service.name': config.apmServiceName, - tags: [config.tags], - __ui: { - is_tls_enabled: true, - is_zip_url_tls_enabled: false, - }, - }, - }) - ); - }); - - it('allows configuring tls', async () => { - // This test ensures that updates made to the Synthetics Policy are carried all the way through - // to the generated Agent Policy that is dispatch down to the Elastic Agent. - const config = generateHTTPConfig('http://elastic.co'); - - const tlsConfig = { - verificationMode: 'strict', - ca: 'ca', - cert: 'cert', - certKey: 'certKey', - certKeyPassphrase: 'certKeyPassphrase', - }; - await uptimePage.syntheticsIntegration.createBasicHTTPMonitorDetails(config); - await uptimePage.syntheticsIntegration.configureTLSOptions(tlsConfig); - await uptimePage.syntheticsIntegration.confirmAndSave(); - - await uptimePage.syntheticsIntegration.isPolicyCreatedSuccessfully(); - - const [agentPolicy] = await uptimeService.syntheticsPackage.getAgentPolicyList(); - const agentPolicyId = agentPolicy.id; - const agentFullPolicy = await uptimeService.syntheticsPackage.getFullAgentPolicy( - agentPolicyId - ); - - expect(getSyntheticsPolicy(agentFullPolicy)).to.eql( - generatePolicy({ - agentFullPolicy, - version, - name: monitorName, - monitorType: 'http', - config: { - max_redirects: 0, - 'check.request.method': 'GET', - 'response.include_body': 'on_error', - 'response.include_headers': true, - schedule: '@every 3m', - 'ssl.supported_protocols': ['TLSv1.1', 'TLSv1.2', 'TLSv1.3'], - 'ssl.verification_mode': tlsConfig.verificationMode, - 'ssl.certificate': tlsConfig.cert, - 'ssl.certificate_authorities': tlsConfig.ca, - 'ssl.key': tlsConfig.certKey, - 'ssl.key_passphrase': tlsConfig.certKeyPassphrase, - timeout: '16s', - urls: config.url, - 'service.name': config.apmServiceName, - tags: [config.tags], - __ui: { - is_tls_enabled: true, - is_zip_url_tls_enabled: false, - }, - }, - }) - ); - }); - - it('allows configuring http advanced options', async () => { - // This test ensures that updates made to the Synthetics Policy are carried all the way through - // to the generated Agent Policy that is dispatch down to the Elastic Agent. - const config = generateHTTPConfig('http://elastic.co'); - - await uptimePage.syntheticsIntegration.createBasicHTTPMonitorDetails(config); - const advancedConfig = { - username: 'username', - password: 'password', - proxyUrl: 'proxyUrl', - requestMethod: 'POST', - responseStatusCheck: '204', - responseBodyCheckPositive: 'success', - responseBodyCheckNegative: 'failure', - requestHeaders: { - sampleRequestHeader1: 'sampleRequestKey1', - sampleRequestHeader2: 'sampleRequestKey2', - }, - responseHeaders: { - sampleResponseHeader1: 'sampleResponseKey1', - sampleResponseHeader2: 'sampleResponseKey2', - }, - requestBody: { - type: 'xml', - value: 'samplexml', - }, - indexResponseBody: false, - indexResponseHeaders: false, - }; - await uptimePage.syntheticsIntegration.configureHTTPAdvancedOptions(advancedConfig); - await uptimePage.syntheticsIntegration.confirmAndSave(); - - await uptimePage.syntheticsIntegration.isPolicyCreatedSuccessfully(); - - const [agentPolicy] = await uptimeService.syntheticsPackage.getAgentPolicyList(); - const agentPolicyId = agentPolicy.id; - const agentFullPolicy = await uptimeService.syntheticsPackage.getFullAgentPolicy( - agentPolicyId - ); - - expect(getSyntheticsPolicy(agentFullPolicy)).to.eql( - generatePolicy({ - agentFullPolicy, - version, - name: monitorName, - monitorType: 'http', - config: { - max_redirects: 0, - 'check.request.method': advancedConfig.requestMethod, - 'check.request.headers': { - 'Content-Type': 'application/xml', - ...advancedConfig.requestHeaders, - }, - 'check.response.headers': advancedConfig.responseHeaders, - 'check.response.status': [advancedConfig.responseStatusCheck], - 'check.request.body': advancedConfig.requestBody.value, - 'check.response.body.positive': [advancedConfig.responseBodyCheckPositive], - 'check.response.body.negative': [advancedConfig.responseBodyCheckNegative], - 'response.include_body': advancedConfig.indexResponseBody ? 'on_error' : 'never', - 'response.include_headers': advancedConfig.indexResponseHeaders, - schedule: '@every 3m', - timeout: '16s', - urls: config.url, - proxy_url: advancedConfig.proxyUrl, - username: advancedConfig.username, - password: advancedConfig.password, - 'service.name': config.apmServiceName, - tags: [config.tags], - __ui: { - is_tls_enabled: false, - is_zip_url_tls_enabled: false, - }, - }, - }) - ); - }); - - it('allows saving tcp monitor when user enters a valid integration name and host+port', async () => { - // This test ensures that updates made to the Synthetics Policy are carried all the way through - // to the generated Agent Policy that is dispatch down to the Elastic Agent. - const config = generateTCPorICMPConfig('smtp.gmail.com:587'); - - await uptimePage.syntheticsIntegration.createBasicTCPMonitorDetails(config); - await uptimePage.syntheticsIntegration.confirmAndSave(); - - await uptimePage.syntheticsIntegration.isPolicyCreatedSuccessfully(); - - const [agentPolicy] = await uptimeService.syntheticsPackage.getAgentPolicyList(); - const agentPolicyId = agentPolicy.id; - const agentFullPolicy = await uptimeService.syntheticsPackage.getFullAgentPolicy( - agentPolicyId - ); - - expect(getSyntheticsPolicy(agentFullPolicy)).to.eql( - generatePolicy({ - agentFullPolicy, - version, - name: monitorName, - monitorType: 'tcp', - config: { - proxy_use_local_resolver: false, - schedule: '@every 3m', - timeout: '16s', - hosts: config.host, - tags: [config.tags], - 'service.name': config.apmServiceName, - __ui: { - is_tls_enabled: false, - is_zip_url_tls_enabled: false, - }, - }, - }) - ); - }); - - it('allows configuring tcp advanced options', async () => { - // This test ensures that updates made to the Synthetics Policy are carried all the way through - // to the generated Agent Policy that is dispatch down to the Elastic Agent. - const config = generateTCPorICMPConfig('smtp.gmail.com:587'); - - await uptimePage.syntheticsIntegration.createBasicTCPMonitorDetails(config); - const advancedConfig = { - proxyUrl: 'proxyUrl', - requestSendCheck: 'body', - responseReceiveCheck: 'success', - proxyUseLocalResolver: true, - }; - await uptimePage.syntheticsIntegration.configureTCPAdvancedOptions(advancedConfig); - await uptimePage.syntheticsIntegration.confirmAndSave(); - - await uptimePage.syntheticsIntegration.isPolicyCreatedSuccessfully(); - - const [agentPolicy] = await uptimeService.syntheticsPackage.getAgentPolicyList(); - const agentPolicyId = agentPolicy.id; - const agentFullPolicy = await uptimeService.syntheticsPackage.getFullAgentPolicy( - agentPolicyId - ); - - expect(getSyntheticsPolicy(agentFullPolicy)).to.eql( - generatePolicy({ - agentFullPolicy, - version, - name: monitorName, - monitorType: 'tcp', - config: { - schedule: '@every 3m', - timeout: '16s', - hosts: config.host, - proxy_url: advancedConfig.proxyUrl, - proxy_use_local_resolver: advancedConfig.proxyUseLocalResolver, - 'check.receive': advancedConfig.responseReceiveCheck, - 'check.send': advancedConfig.requestSendCheck, - 'service.name': config.apmServiceName, - tags: [config.tags], - __ui: { - is_tls_enabled: false, - is_zip_url_tls_enabled: false, - }, - }, - }) - ); - }); - - it('allows saving icmp monitor when user enters a valid integration name and host', async () => { - // This test ensures that updates made to the Synthetics Policy are carried all the way through - // to the generated Agent Policy that is dispatch down to the Elastic Agent. - const config = generateTCPorICMPConfig('1.1.1.1'); - - await uptimePage.syntheticsIntegration.createBasicICMPMonitorDetails(config); - await uptimePage.syntheticsIntegration.confirmAndSave(); - - await uptimePage.syntheticsIntegration.isPolicyCreatedSuccessfully(); - - const [agentPolicy] = await uptimeService.syntheticsPackage.getAgentPolicyList(); - const agentPolicyId = agentPolicy.id; - const agentFullPolicy = await uptimeService.syntheticsPackage.getFullAgentPolicy( - agentPolicyId - ); - - expect(getSyntheticsPolicy(agentFullPolicy)).to.eql( - generatePolicy({ - agentFullPolicy, - version, - name: monitorName, - monitorType: 'icmp', - config: { - schedule: '@every 3m', - timeout: '16s', - wait: '1s', - hosts: config.host, - 'service.name': config.apmServiceName, - tags: [config.tags], - __ui: null, - }, - }) - ); - }); - - it('allows saving browser monitor', async () => { - // This test ensures that updates made to the Synthetics Policy are carried all the way through - // to the generated Agent Policy that is dispatch down to the Elastic Agent. - const config = generateBrowserConfig({ - zipUrl: 'http://test.zip', - params: JSON.stringify({ url: 'http://localhost:8080' }), - folder: 'folder', - username: 'username', - password: 'password', - }); - - await uptimePage.syntheticsIntegration.createBasicBrowserMonitorDetails(config); - await uptimePage.syntheticsIntegration.confirmAndSave(); - - await uptimePage.syntheticsIntegration.isPolicyCreatedSuccessfully(); - - const [agentPolicy] = await uptimeService.syntheticsPackage.getAgentPolicyList(); - const agentPolicyId = agentPolicy.id; - const agentFullPolicy = await uptimeService.syntheticsPackage.getFullAgentPolicy( - agentPolicyId - ); - - expect(getSyntheticsPolicy(agentFullPolicy)).to.eql( - generatePolicy({ - agentFullPolicy, - version, - name: monitorName, - monitorType: 'browser', - config: { - screenshots: 'on', - schedule: '@every 10m', - timeout: null, - tags: [config.tags], - throttling: '5d/3u/20l', - 'service.name': config.apmServiceName, - 'source.zip_url.url': config.zipUrl, - 'source.zip_url.folder': config.folder, - 'source.zip_url.username': config.username, - 'source.zip_url.password': config.password, - params: JSON.parse(config.params), - __ui: { - is_tls_enabled: false, - is_zip_url_tls_enabled: false, - script_source: { - file_name: '', - is_generated_script: false, - }, - }, - }, - }) - ); - }); - - it('allows saving browser monitor with inline script', async () => { - // This test ensures that updates made to the Synthetics Policy are carried all the way through - // to the generated Agent Policy that is dispatch down to the Elastic Agent. - const config = generateBrowserConfig({ - inlineScript: - 'step("load homepage", async () => { await page.goto(\'https://www.elastic.co\'); });', - }); - - await uptimePage.syntheticsIntegration.createBasicBrowserMonitorDetails(config, true); - await uptimePage.syntheticsIntegration.confirmAndSave(); - - await uptimePage.syntheticsIntegration.isPolicyCreatedSuccessfully(); - - const [agentPolicy] = await uptimeService.syntheticsPackage.getAgentPolicyList(); - const agentPolicyId = agentPolicy.id; - const agentFullPolicy = await uptimeService.syntheticsPackage.getFullAgentPolicy( - agentPolicyId - ); - - expect(getSyntheticsPolicy(agentFullPolicy)).to.eql( - generatePolicy({ - agentFullPolicy, - version, - name: monitorName, - monitorType: 'browser', - config: { - screenshots: 'on', - schedule: '@every 10m', - timeout: null, - tags: [config.tags], - throttling: '5d/3u/20l', - 'service.name': config.apmServiceName, - 'source.inline.script': config.inlineScript, - __ui: { - is_tls_enabled: false, - is_zip_url_tls_enabled: false, - script_source: { - file_name: '', - is_generated_script: false, - }, - }, - }, - }) - ); - }); - - it('allows saving browser monitor advanced options', async () => { - // This test ensures that updates made to the Synthetics Policy are carried all the way through - // to the generated Agent Policy that is dispatch down to the Elastic Agent. - const config = generateBrowserConfig({ - zipUrl: 'http://test.zip', - params: JSON.stringify({ url: 'http://localhost:8080' }), - folder: 'folder', - username: 'username', - password: 'password', - }); - - const advancedConfig = { - screenshots: 'off', - syntheticsArgs: '-ssBlocks', - isThrottlingEnabled: true, - downloadSpeed: '1337', - uploadSpeed: '1338', - latency: '1339', - }; - - await uptimePage.syntheticsIntegration.createBasicBrowserMonitorDetails(config); - await uptimePage.syntheticsIntegration.configureBrowserAdvancedOptions(advancedConfig); - await uptimePage.syntheticsIntegration.confirmAndSave(); - - await uptimePage.syntheticsIntegration.isPolicyCreatedSuccessfully(); - - const [agentPolicy] = await uptimeService.syntheticsPackage.getAgentPolicyList(); - const agentPolicyId = agentPolicy.id; - const agentFullPolicy = await uptimeService.syntheticsPackage.getFullAgentPolicy( - agentPolicyId - ); - - expect(getSyntheticsPolicy(agentFullPolicy)).to.eql( - generatePolicy({ - agentFullPolicy, - version, - name: monitorName, - monitorType: 'browser', - config: { - screenshots: advancedConfig.screenshots, - schedule: '@every 10m', - timeout: null, - tags: [config.tags], - throttling: '1337d/1338u/1339l', - 'service.name': config.apmServiceName, - 'source.zip_url.url': config.zipUrl, - 'source.zip_url.folder': config.folder, - 'source.zip_url.username': config.username, - 'source.zip_url.password': config.password, - params: JSON.parse(config.params), - synthetics_args: [advancedConfig.syntheticsArgs], - __ui: { - is_tls_enabled: false, - is_zip_url_tls_enabled: false, - script_source: { - file_name: '', - is_generated_script: false, - }, - }, - }, - }) - ); - }); - - it('allows saving disabling throttling', async () => { - // This test ensures that updates made to the Synthetics Policy are carried all the way through - // to the generated Agent Policy that is dispatch down to the Elastic Agent. - const config = generateBrowserConfig({ - zipUrl: 'http://test.zip', - params: JSON.stringify({ url: 'http://localhost:8080' }), - folder: 'folder', - username: 'username', - password: 'password', - }); - - const advancedConfig = { - screenshots: 'off', - syntheticsArgs: '-ssBlocks', - isThrottlingEnabled: false, - downloadSpeed: '1337', - uploadSpeed: '1338', - latency: '1339', - }; - - await uptimePage.syntheticsIntegration.createBasicBrowserMonitorDetails(config); - await uptimePage.syntheticsIntegration.configureBrowserAdvancedOptions(advancedConfig); - await uptimePage.syntheticsIntegration.confirmAndSave(); - - await uptimePage.syntheticsIntegration.isPolicyCreatedSuccessfully(); - - const [agentPolicy] = await uptimeService.syntheticsPackage.getAgentPolicyList(); - const agentPolicyId = agentPolicy.id; - const agentFullPolicy = await uptimeService.syntheticsPackage.getFullAgentPolicy( - agentPolicyId - ); - - expect(getSyntheticsPolicy(agentFullPolicy)).to.eql( - generatePolicy({ - agentFullPolicy, - version, - name: monitorName, - monitorType: 'browser', - config: { - screenshots: advancedConfig.screenshots, - schedule: '@every 10m', - timeout: null, - tags: [config.tags], - 'service.name': config.apmServiceName, - 'source.zip_url.url': config.zipUrl, - 'source.zip_url.folder': config.folder, - 'source.zip_url.username': config.username, - 'source.zip_url.password': config.password, - params: JSON.parse(config.params), - synthetics_args: [advancedConfig.syntheticsArgs], - throttling: false, - __ui: { - is_tls_enabled: false, - is_zip_url_tls_enabled: false, - script_source: { - file_name: '', - is_generated_script: false, - }, - }, - }, - }) - ); - }); - }); - }); -} diff --git a/x-pack/test/functional_synthetics/config.js b/x-pack/test/functional_synthetics/config.js deleted file mode 100644 index d563911a9bebc..0000000000000 --- a/x-pack/test/functional_synthetics/config.js +++ /dev/null @@ -1,104 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import path, { resolve } from 'path'; - -import { defineDockerServersConfig } from '@kbn/test'; -import { dockerImage as fleetDockerImage } from '../fleet_api_integration/config'; - -import { services } from './services'; -import { pageObjects } from './page_objects'; - -// the default export of config files must be a config provider -// that returns an object with the projects config values -export default async function ({ readConfigFile }) { - const registryPort = process.env.FLEET_PACKAGE_REGISTRY_PORT; - - const kibanaCommonConfig = await readConfigFile( - require.resolve('../../../test/common/config.js') - ); - const kibanaFunctionalConfig = await readConfigFile( - require.resolve('../../../test/functional/config.base.js') - ); - - // mount the config file for the package registry as well as - // the directory containing additional packages into the container - const dockerArgs = [ - '-v', - `${path.join( - path.dirname(__filename), - './fixtures/package_registry_config.yml' - )}:/package-registry/config.yml`, - ]; - - return { - // list paths to the files that contain your plugins tests - testFiles: [resolve(__dirname, './apps/uptime')], - - services, - pageObjects, - - servers: kibanaFunctionalConfig.get('servers'), - - esTestCluster: { - license: 'trial', - from: 'snapshot', - serverArgs: ['path.repo=/tmp/', 'xpack.security.authc.api_key.enabled=true'], - }, - - kbnTestServer: { - ...kibanaCommonConfig.get('kbnTestServer'), - serverArgs: [ - ...kibanaCommonConfig.get('kbnTestServer.serverArgs'), - '--status.allowAnonymous=true', - '--server.uuid=5b2de169-2785-441b-ae8c-186a1936b17d', - '--xpack.maps.showMapsInspectorAdapter=true', - '--xpack.maps.preserveDrawingBuffer=true', - '--xpack.security.encryptionKey="wuGNaIhoMpk5sO4UBxgr3NyW1sFcLgIf"', // server restarts should not invalidate active sessions - '--xpack.encryptedSavedObjects.encryptionKey="DkdXazszSCYexXqz4YktBGHCRkV6hyNK"', - '--xpack.discoverEnhanced.actions.exploreDataInContextMenu.enabled=true', - '--savedObjects.maxImportPayloadBytes=10485760', // for OSS test management/_import_objects, - ...(registryPort ? [`--xpack.fleet.registryUrl=http://localhost:${registryPort}`] : []), - ], - }, - uiSettings: { - defaults: { - 'accessibility:disableAnimations': true, - 'dateFormat:tz': 'UTC', - }, - }, - // the apps section defines the urls that - // `PageObjects.common.navigateTo(appKey)` will use. - // Merge urls for your plugin with the urls defined in - // Kibana's config in order to use this helper - apps: { - ...kibanaFunctionalConfig.get('apps'), - fleet: { - pathname: '/app/fleet', - }, - }, - - // choose where screenshots should be saved - screenshots: { - directory: resolve(__dirname, 'screenshots'), - }, - - junit: { - reportName: 'Chrome Elastic Synthetics Integration UI Functional Tests', - }, - dockerServers: defineDockerServersConfig({ - registry: { - enabled: !!registryPort, - image: fleetDockerImage, - portInContainer: 8080, - port: registryPort, - args: dockerArgs, - waitForLogLine: 'package manifests loaded', - }, - }), - }; -} diff --git a/x-pack/test/functional_synthetics/fixtures/package_registry_config.yml b/x-pack/test/functional_synthetics/fixtures/package_registry_config.yml deleted file mode 100644 index a6c51976af986..0000000000000 --- a/x-pack/test/functional_synthetics/fixtures/package_registry_config.yml +++ /dev/null @@ -1,2 +0,0 @@ -package_paths: - - /packages/package-storage \ No newline at end of file diff --git a/x-pack/test/functional_synthetics/ftr_provider_context.ts b/x-pack/test/functional_synthetics/ftr_provider_context.ts deleted file mode 100644 index e757164fa1de9..0000000000000 --- a/x-pack/test/functional_synthetics/ftr_provider_context.ts +++ /dev/null @@ -1,14 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { GenericFtrProviderContext, GenericFtrService } from '@kbn/test'; - -import { pageObjects } from './page_objects'; -import { services } from './services'; - -export type FtrProviderContext = GenericFtrProviderContext; -export class FtrService extends GenericFtrService {} diff --git a/x-pack/test/functional_synthetics/helpers.ts b/x-pack/test/functional_synthetics/helpers.ts deleted file mode 100644 index 8635609cf35d9..0000000000000 --- a/x-pack/test/functional_synthetics/helpers.ts +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { Context } from 'mocha'; -import { ToolingLog } from '@kbn/tooling-log'; -import { FtrProviderContext } from './ftr_provider_context'; - -export function warnAndSkipTest(mochaContext: Context, log: ToolingLog) { - log.warning( - 'disabling tests because DockerServers service is not enabled, set FLEET_PACKAGE_REGISTRY_PORT to run them' - ); - mochaContext.skip(); -} - -export function skipIfNoDockerRegistry(providerContext: FtrProviderContext) { - const { getService } = providerContext; - const dockerServers = getService('dockerServers'); - - const server = dockerServers.get('registry'); - const log = getService('log'); - - beforeEach(function beforeSetupWithDockerRegistry() { - if (!server.enabled) { - warnAndSkipTest(this, log); - } - }); -} diff --git a/x-pack/test/functional_synthetics/page_objects/index.ts b/x-pack/test/functional_synthetics/page_objects/index.ts deleted file mode 100644 index 253157297713b..0000000000000 --- a/x-pack/test/functional_synthetics/page_objects/index.ts +++ /dev/null @@ -1,17 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { pageObjects as kibanaFunctionalPageObjects } from '../../../../test/functional/page_objects'; - -import { SyntheticsIntegrationPageProvider } from './synthetics_integration_page'; - -// just like services, PageObjects are defined as a map of -// names to Providers. Merge in Kibana's or pick specific ones -export const pageObjects = { - ...kibanaFunctionalPageObjects, - syntheticsIntegration: SyntheticsIntegrationPageProvider, -}; diff --git a/x-pack/test/functional_synthetics/page_objects/synthetics_integration_page.ts b/x-pack/test/functional_synthetics/page_objects/synthetics_integration_page.ts deleted file mode 100644 index 8650a2b352e78..0000000000000 --- a/x-pack/test/functional_synthetics/page_objects/synthetics_integration_page.ts +++ /dev/null @@ -1,483 +0,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 { WebElementWrapper } from '../../../../test/functional/services/lib/web_element_wrapper'; -import { FtrProviderContext } from '../ftr_provider_context'; - -export function SyntheticsIntegrationPageProvider({ - getService, - getPageObjects, -}: FtrProviderContext) { - const pageObjects = getPageObjects(['common', 'header']); - const testSubjects = getService('testSubjects'); - const comboBox = getService('comboBox'); - - const fixedFooterHeight = 72; // Size of EuiBottomBar more or less - - return { - /** - * Navigates to the Synthetics Integration page - * - */ - async navigateToPackagePage(packageVersion: string) { - await pageObjects.common.navigateToUrlWithBrowserHistory( - 'fleet', - `/integrations/synthetics-${packageVersion}/add-integration` - ); - await pageObjects.header.waitUntilLoadingHasFinished(); - }, - - async navigateToPackageEditPage(packageId: string, agentId: string) { - await pageObjects.common.navigateToUrlWithBrowserHistory( - 'fleet', - `/policies/${agentId}/edit-integration/${packageId}` - ); - await pageObjects.header.waitUntilLoadingHasFinished(); - }, - - /** - * Finds and returns the Policy Details Page Save button - */ - async findSaveButton(isEditPage?: boolean) { - await this.ensureIsOnPackagePage(); - return await testSubjects.find( - isEditPage ? 'saveIntegration' : 'createPackagePolicySaveButton' - ); - }, - - /** - * Finds and returns the Policy Details Page Cancel Button - */ - async findCancelButton() { - await this.ensureIsOnPackagePage(); - return await testSubjects.find('policyDetailsCancelButton'); - }, - - /** - * Determines if the policy was created successfully by looking for the creation success toast - */ - async isPolicyCreatedSuccessfully() { - await testSubjects.existOrFail('postInstallAddAgentModal'); - }, - - /** - * Selects the monitor type - * @params {monitorType} the type of monitor, tcp, http, or icmp - */ - async selectMonitorType(monitorType: string) { - await testSubjects.selectValue('syntheticsMonitorTypeField', monitorType); - }, - - /** - * Fills a text input - * @params {testSubj} the testSubj of the input to fill - * @params {value} the value of the input - */ - async fillTextInputByTestSubj(testSubj: string, value: string) { - const field = await testSubjects.find(testSubj); - await field.scrollIntoViewIfNecessary({ bottomOffset: fixedFooterHeight }); - await field.click(); - await field.clearValue(); - await field.type(value); - }, - - /** - * Fills a text input - * @params {testSubj} the testSubj of the input to fill - * @params {value} the value of the input - */ - async fillTextInput(field: WebElementWrapper, value: string) { - await field.scrollIntoViewIfNecessary({ bottomOffset: fixedFooterHeight }); - await field.click(); - await field.clearValue(); - await field.type(value); - }, - - /** - * Fills a text input - * @params {testSubj} the testSubj of the comboBox - */ - async setComboBox(testSubj: string, value: string) { - await comboBox.setCustom(`${testSubj} > comboBoxInput`, value); - }, - - /** - * Finds and returns the HTTP advanced options accordion trigger - */ - async findHTTPAdvancedOptionsAccordion() { - await this.ensureIsOnPackagePage(); - const accordion = await testSubjects.find('syntheticsHTTPAdvancedFieldsAccordion'); - return accordion; - }, - - /** - * Finds and returns the enable throttling checkbox - */ - async findThrottleSwitch() { - await this.ensureIsOnPackagePage(); - return await testSubjects.find('syntheticsBrowserIsThrottlingEnabled'); - }, - - /** - * Finds and returns the enable TLS checkbox - */ - async findEnableTLSSwitch() { - await this.ensureIsOnPackagePage(); - return await testSubjects.find('syntheticsIsTLSEnabled'); - }, - - /** - * ensures that the package page is the currently display view - */ - async ensureIsOnPackagePage() { - await testSubjects.existOrFail('monitorSettingsSection'); - }, - - /** - * Clicks save button and confirms update on the Policy Details page - */ - async confirmAndSave(isEditPage?: boolean) { - await this.ensureIsOnPackagePage(); - const saveButton = await this.findSaveButton(isEditPage); - await saveButton.click(); - await this.maybeForceInstall(); - }, - - /** - * If the force install modal opens, click force install - */ - async maybeForceInstall() { - const confirmForceInstallModalOpen = await testSubjects.exists('confirmForceInstallModal'); - - if (confirmForceInstallModalOpen) { - const forceInstallBtn = await testSubjects.find('confirmModalConfirmButton'); - return forceInstallBtn.click(); - } - }, - - /** - * Fills in the username and password field - * @params username {string} the value of the username - * @params password {string} the value of the password - */ - async configureUsernameAndPassword({ username, password }: Record) { - await this.fillTextInputByTestSubj('syntheticsUsername', username); - await this.fillTextInputByTestSubj('syntheticsPassword', password); - }, - - /** - * - * Configures request headers - * @params headers {string} an object containing desired headers - * - */ - async configureRequestHeaders(headers: Record) { - await this.configureHeaders('syntheticsRequestHeaders', headers); - }, - - /** - * - * Configures response headers - * @params headers {string} an object containing desired headers - * - */ - async configureResponseHeaders(headers: Record) { - await this.configureHeaders('syntheticsResponseHeaders', headers); - }, - - /** - * - * Configures headers - * @params testSubj {string} test subj - * @params headers {string} an object containing desired headers - * - */ - async configureHeaders(testSubj: string, headers: Record) { - const headersContainer = await testSubjects.find(testSubj); - const addHeaderButton = await testSubjects.find(`${testSubj}__button`); - const keys = Object.keys(headers); - - await Promise.all( - keys.map(async (key, index) => { - await addHeaderButton.click(); - const keyField = await headersContainer.findByCssSelector( - `[data-test-subj="keyValuePairsKey${index}"]` - ); - const valueField = await headersContainer.findByCssSelector( - `[data-test-subj="keyValuePairsValue${index}"]` - ); - await this.fillTextInput(keyField, key); - await this.fillTextInput(valueField, headers[key]); - }) - ); - }, - - /** - * - * Configures request body - * @params contentType {string} contentType of the request body - * @params value {string} value of the request body - * - */ - async configureRequestBody(testSubj: string, value: string) { - await testSubjects.click(`syntheticsRequestBodyTab__${testSubj}`); - await this.fillCodeEditor(value); - }, - - /** - * - * Fills the monaco code editor - * @params value {string} value of code input - * - */ - async fillCodeEditor(value: string) { - const codeEditorContainer = await testSubjects.find('codeEditorContainer'); - const textArea = await codeEditorContainer.findByCssSelector('textarea'); - await textArea.clearValue(); - await textArea.type(value); - }, - - /** - * Creates basic common monitor details - * @params name {string} the name of the monitor - * @params url {string} the url of the monitor - * - */ - async createBasicMonitorDetails({ name, apmServiceName, tags }: Record) { - await this.fillTextInputByTestSubj('packagePolicyNameInput', name); - await this.fillTextInputByTestSubj('syntheticsAPMServiceName', apmServiceName); - await this.setComboBox('syntheticsTags', tags); - }, - - /** - * Fills in the fields to create a basic HTTP monitor - * @params name {string} the name of the monitor - * @params url {string} the url of the monitor - * - */ - async createBasicHTTPMonitorDetails({ - name, - url, - apmServiceName, - tags, - }: Record) { - await this.createBasicMonitorDetails({ name, apmServiceName, tags }); - await this.fillTextInputByTestSubj('syntheticsUrlField', url); - }, - - /** - * Fills in the fields to create a basic TCP monitor - * @params name {string} the name of the monitor - * @params host {string} the host (and port) of the monitor - * - */ - async createBasicTCPMonitorDetails({ - name, - host, - apmServiceName, - tags, - }: Record) { - await this.selectMonitorType('tcp'); - await this.createBasicMonitorDetails({ name, apmServiceName, tags }); - await this.fillTextInputByTestSubj('syntheticsTCPHostField', host); - }, - - /** - * Creates a basic ICMP monitor - * @params name {string} the name of the monitor - * @params host {string} the host of the monitor - */ - async createBasicICMPMonitorDetails({ - name, - host, - apmServiceName, - tags, - }: Record) { - await this.selectMonitorType('icmp'); - await this.fillTextInputByTestSubj('packagePolicyNameInput', name); - await this.createBasicMonitorDetails({ name, apmServiceName, tags }); - await this.fillTextInputByTestSubj('syntheticsICMPHostField', host); - }, - - /** - * Creates a basic browser monitor - * @params name {string} the name of the monitor - * @params zipUrl {string} the zip url of the synthetics suites - */ - async createBasicBrowserMonitorDetails( - { - name, - inlineScript, - zipUrl, - folder, - params, - username, - password, - apmServiceName, - tags, - }: Record, - isInline: boolean = false - ) { - await this.selectMonitorType('browser'); - await this.fillTextInputByTestSubj('packagePolicyNameInput', name); - await this.createBasicMonitorDetails({ name, apmServiceName, tags }); - if (isInline) { - await testSubjects.click('syntheticsSourceTab__inline'); - await this.fillCodeEditor(inlineScript); - return; - } else { - await testSubjects.click('syntheticsSourceTab__zipUrl'); - } - await this.fillTextInputByTestSubj('syntheticsBrowserZipUrl', zipUrl); - await this.fillTextInputByTestSubj('syntheticsBrowserZipUrlFolder', folder); - await this.fillTextInputByTestSubj('syntheticsBrowserZipUrlUsername', username); - await this.fillTextInputByTestSubj('syntheticsBrowserZipUrlPassword', password); - await this.fillCodeEditor(params); - }, - - /** - * Enables TLS - */ - async enableTLS() { - const tlsSwitch = await this.findEnableTLSSwitch(); - await tlsSwitch.click(); - }, - - /** - * Configures TLS settings - * @params verificationMode {string} the name of the monitor - */ - async configureTLSOptions({ - verificationMode, - ca, - cert, - certKey, - certKeyPassphrase, - }: Record) { - await this.enableTLS(); - await testSubjects.selectValue('syntheticsTLSVerificationMode', verificationMode); - await this.fillTextInputByTestSubj('syntheticsTLSCA', ca); - await this.fillTextInputByTestSubj('syntheticsTLSCert', cert); - await this.fillTextInputByTestSubj('syntheticsTLSCertKey', certKey); - await this.fillTextInputByTestSubj('syntheticsTLSCertKeyPassphrase', certKeyPassphrase); - }, - - /** - * Configure http advanced settings - */ - async configureHTTPAdvancedOptions({ - username, - password, - proxyUrl, - requestMethod, - requestHeaders, - responseStatusCheck, - responseBodyCheckPositive, - responseBodyCheckNegative, - requestBody, - responseHeaders, - indexResponseBody, - indexResponseHeaders, - }: { - username: string; - password: string; - proxyUrl: string; - requestMethod: string; - responseStatusCheck: string; - responseBodyCheckPositive: string; - responseBodyCheckNegative: string; - requestBody: { value: string; type: string }; - requestHeaders: Record; - responseHeaders: Record; - indexResponseBody: boolean; - indexResponseHeaders: boolean; - }) { - await testSubjects.click('syntheticsHTTPAdvancedFieldsAccordion'); - await this.configureResponseHeaders(responseHeaders); - await this.configureRequestHeaders(requestHeaders); - await this.configureRequestBody(requestBody.type, requestBody.value); - await this.configureUsernameAndPassword({ username, password }); - await this.setComboBox('syntheticsResponseStatusCheck', responseStatusCheck); - await this.setComboBox('syntheticsResponseBodyCheckPositive', responseBodyCheckPositive); - await this.setComboBox('syntheticsResponseBodyCheckNegative', responseBodyCheckNegative); - await this.fillTextInputByTestSubj('syntheticsProxyUrl', proxyUrl); - await testSubjects.selectValue('syntheticsRequestMethod', requestMethod); - if (!indexResponseBody) { - const field = await testSubjects.find('syntheticsIndexResponseBody'); - const label = await field.findByCssSelector('label'); - await label.click(); - } - if (!indexResponseHeaders) { - const field = await testSubjects.find('syntheticsIndexResponseHeaders'); - const label = await field.findByCssSelector('label'); - await label.click(); - } - }, - - /** - * Configure tcp advanced settings - */ - async configureTCPAdvancedOptions({ - proxyUrl, - requestSendCheck, - responseReceiveCheck, - proxyUseLocalResolver, - }: { - proxyUrl: string; - requestSendCheck: string; - responseReceiveCheck: string; - proxyUseLocalResolver: boolean; - }) { - await testSubjects.click('syntheticsTCPAdvancedFieldsAccordion'); - await this.fillTextInputByTestSubj('syntheticsProxyUrl', proxyUrl); - await this.fillTextInputByTestSubj('syntheticsTCPRequestSendCheck', requestSendCheck); - await this.fillTextInputByTestSubj('syntheticsTCPResponseReceiveCheck', responseReceiveCheck); - if (proxyUseLocalResolver) { - const field = await testSubjects.find('syntheticsUseLocalResolver'); - const label = await field.findByCssSelector('label'); - await label.click(); - } - }, - - /** - * Configure browser advanced settings - * @params name {string} the name of the monitor - * @params zipUrl {string} the zip url of the synthetics suites - */ - async configureBrowserAdvancedOptions({ - screenshots, - syntheticsArgs, - isThrottlingEnabled, - downloadSpeed, - uploadSpeed, - latency, - }: { - screenshots: string; - syntheticsArgs: string; - isThrottlingEnabled: boolean; - downloadSpeed: string; - uploadSpeed: string; - latency: string; - }) { - await testSubjects.click('syntheticsBrowserAdvancedFieldsAccordion'); - - const throttleSwitch = await this.findThrottleSwitch(); - if (!isThrottlingEnabled) { - await throttleSwitch.click(); - } - - await testSubjects.selectValue('syntheticsBrowserScreenshots', screenshots); - await this.setComboBox('syntheticsBrowserSyntheticsArgs', syntheticsArgs); - - if (isThrottlingEnabled) { - await this.fillTextInputByTestSubj('syntheticsBrowserDownloadSpeed', downloadSpeed); - await this.fillTextInputByTestSubj('syntheticsBrowserUploadSpeed', uploadSpeed); - await this.fillTextInputByTestSubj('syntheticsBrowserLatency', latency); - } - }, - }; -} diff --git a/x-pack/test/functional_synthetics/services/index.ts b/x-pack/test/functional_synthetics/services/index.ts deleted file mode 100644 index b8340acccb512..0000000000000 --- a/x-pack/test/functional_synthetics/services/index.ts +++ /dev/null @@ -1,19 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { services as kibanaFunctionalServices } from '../../../../test/functional/services'; -import { services as commonServices } from '../../common/services'; -import { UptimeProvider } from './uptime'; - -// define the name and providers for services that should be -// available to your tests. If you don't specify anything here -// only the built-in services will be available -export const services = { - ...kibanaFunctionalServices, - ...commonServices, - uptime: UptimeProvider, -}; diff --git a/x-pack/test/functional_synthetics/services/uptime/index.ts b/x-pack/test/functional_synthetics/services/uptime/index.ts deleted file mode 100644 index 649408c03284d..0000000000000 --- a/x-pack/test/functional_synthetics/services/uptime/index.ts +++ /dev/null @@ -1,8 +0,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. - */ - -export { UptimeProvider } from './uptime'; diff --git a/x-pack/test/functional_synthetics/services/uptime/synthetics_package.ts b/x-pack/test/functional_synthetics/services/uptime/synthetics_package.ts deleted file mode 100644 index 5b9525bf2060b..0000000000000 --- a/x-pack/test/functional_synthetics/services/uptime/synthetics_package.ts +++ /dev/null @@ -1,176 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { - PACKAGE_POLICY_SAVED_OBJECT_TYPE, - DeletePackagePoliciesRequest, - GetPackagePoliciesResponse, - GetFullAgentPolicyResponse, - GetPackagesResponse, - GetAgentPoliciesResponse, -} from '@kbn/fleet-plugin/common'; -import { FtrProviderContext } from '../../ftr_provider_context'; - -const INGEST_API_ROOT = '/api/fleet'; -const INGEST_API_AGENT_POLICIES = `${INGEST_API_ROOT}/agent_policies`; -const INGEST_API_PACKAGE_POLICIES = `${INGEST_API_ROOT}/package_policies`; -const INGEST_API_PACKAGE_POLICIES_DELETE = `${INGEST_API_PACKAGE_POLICIES}/delete`; -const INGEST_API_EPM_PACKAGES = `${INGEST_API_ROOT}/epm/packages`; - -export function SyntheticsPackageProvider({ getService }: FtrProviderContext) { - const supertest = getService('supertest'); - const log = getService('log'); - const retry = getService('retry'); - - const logSupertestApiErrorAndThrow = (message: string, error: any): never => { - const responseBody = error?.response?.body; - const responseText = error?.response?.text; - log.error(`Error occurred at ${Date.now()} | ${new Date().toISOString()}`); - log.error(JSON.stringify(responseBody || responseText, null, 2)); - log.error(error); - throw new Error(message); - }; - const retrieveSyntheticsPackageInfo = (() => { - // Retrieve information about the Synthetics package - // EPM does not currently have an API to get the "lastest" information for a page given its name, - // so we'll retrieve a list of packages and then find the package info in the list. - let apiRequest: Promise; - - return () => { - if (!apiRequest) { - log.info(`Setting up call to retrieve Synthetics package`); - - // Currently (as of 2020-june) the package registry used in CI is the public one and - // at times it encounters network connection issues. We use `retry.try` below to see if - // subsequent requests get through. - apiRequest = retry.try(() => { - return supertest - .get(INGEST_API_EPM_PACKAGES) - .query({ prerelease: true }) - .set('kbn-xsrf', 'xxx') - .expect(200) - .catch((error) => { - return logSupertestApiErrorAndThrow(`Unable to retrieve packages via Ingest!`, error); - }) - .then((response: { body: GetPackagesResponse }) => { - const { body } = response; - const syntheticsPackageInfo = body.items.find( - (epmPackage) => epmPackage.name === 'synthetics' - ); - if (!syntheticsPackageInfo) { - throw new Error( - `Synthetics package was not in response from ${INGEST_API_EPM_PACKAGES}` - ); - } - return Promise.resolve(syntheticsPackageInfo); - }); - }); - } else { - log.info('Using cached retrieval of synthetics package'); - } - return apiRequest; - }; - })(); - - return { - /** - * Returns the synthetics package version for the currently installed package. This version can then - * be used to build URLs for Fleet pages or APIs - */ - async getSyntheticsPackageVersion() { - const syntheticsPackage = await retrieveSyntheticsPackageInfo()!; - - return syntheticsPackage?.version; - }, - - /** - * Retrieves the full Agent policy by id, which mirrors what the Elastic Agent would get - * once they checkin. - */ - async getFullAgentPolicy(agentPolicyId: string): Promise { - let fullAgentPolicy: GetFullAgentPolicyResponse['item']; - try { - const apiResponse: { body: GetFullAgentPolicyResponse } = await supertest - .get(`${INGEST_API_AGENT_POLICIES}/${agentPolicyId}/full`) - .expect(200); - - fullAgentPolicy = apiResponse.body.item; - } catch (error) { - return logSupertestApiErrorAndThrow('Unable to get full Agent policy', error); - } - - return fullAgentPolicy!; - }, - - /** - * Retrieves all the agent policies. - */ - async getAgentPolicyList(): Promise { - let agentPolicyList: GetAgentPoliciesResponse['items']; - try { - const apiResponse: { body: GetAgentPoliciesResponse } = await supertest - .get(INGEST_API_AGENT_POLICIES) - .expect(200); - - agentPolicyList = apiResponse.body.items; - } catch (error) { - return logSupertestApiErrorAndThrow('Unable to get full Agent policy list', error); - } - - return agentPolicyList!; - }, - - /** - * Deletes a policy (Package Policy) by using the policy name - * @param name - */ - async deletePolicyByName(name: string) { - const id = await this.getPackagePolicyIdByName(name); - - if (id) { - try { - const deletePackagePolicyData: DeletePackagePoliciesRequest['body'] = { - packagePolicyIds: [id], - }; - await supertest - .post(INGEST_API_PACKAGE_POLICIES_DELETE) - .set('kbn-xsrf', 'xxx') - .send(deletePackagePolicyData) - .expect(200); - } catch (error) { - logSupertestApiErrorAndThrow( - `Unable to delete Package Policy via Ingest! ${name}`, - error - ); - } - } - }, - - /** - * Gets the policy id (Package Policy) by using the policy name - * @param name - */ - async getPackagePolicyIdByName(name: string) { - const { body: packagePoliciesResponse }: { body: GetPackagePoliciesResponse } = - await supertest - .get(INGEST_API_PACKAGE_POLICIES) - .set('kbn-xsrf', 'xxx') - .query({ kuery: `${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.name: ${name}` }) - .send() - .expect(200); - const packagePolicyList: GetPackagePoliciesResponse['items'] = packagePoliciesResponse.items; - - if (packagePolicyList.length > 1) { - throw new Error(`Found ${packagePolicyList.length} Policies - was expecting only one!`); - } - - if (packagePolicyList.length) { - return packagePolicyList[0].id; - } - }, - }; -} diff --git a/x-pack/test/functional_synthetics/services/uptime/uptime.ts b/x-pack/test/functional_synthetics/services/uptime/uptime.ts deleted file mode 100644 index 24354f4ddae0d..0000000000000 --- a/x-pack/test/functional_synthetics/services/uptime/uptime.ts +++ /dev/null @@ -1,18 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { FtrProviderContext } from '../../ftr_provider_context'; - -import { SyntheticsPackageProvider } from './synthetics_package'; - -export function UptimeProvider(context: FtrProviderContext) { - const syntheticsPackage = SyntheticsPackageProvider(context); - - return { - syntheticsPackage, - }; -} diff --git a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alerts_list.ts b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alerts_list.ts index f4ae0f00a170d..bb6d8a46988a5 100644 --- a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alerts_list.ts +++ b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alerts_list.ts @@ -244,13 +244,13 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await pageObjects.triggersActionsUI.searchAlerts(createdAlert.name); await testSubjects.click(`checkboxSelectRow-${createdAlert.id}`); - await testSubjects.click('bulkAction'); + await testSubjects.click('bulkDisable'); - await testSubjects.click('disableAll'); - - // Enable all button shows after clicking disable all - await testSubjects.existOrFail('enableAll'); + await retry.try(async () => { + const toastTitle = await pageObjects.common.closeToast(); + expect(toastTitle).to.eql('Disabled 1 rule'); + }); await pageObjects.triggersActionsUI.ensureRuleActionStatusApplied( createdAlert.name, @@ -261,19 +261,14 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { it('should enable all selection', async () => { const createdAlert = await createAlert({ supertest, objectRemover }); + await disableAlert({ supertest, alertId: createdAlert.id }); + await refreshAlertsList(); await pageObjects.triggersActionsUI.searchAlerts(createdAlert.name); await testSubjects.click(`checkboxSelectRow-${createdAlert.id}`); - await testSubjects.click('bulkAction'); - - await testSubjects.click('disableAll'); - - await testSubjects.click('enableAll'); - - // Disable all button shows after clicking enable all - await testSubjects.existOrFail('disableAll'); + await testSubjects.click('bulkEnable'); await pageObjects.triggersActionsUI.ensureRuleActionStatusApplied( createdAlert.name, diff --git a/x-pack/test/localization/README.md b/x-pack/test/localization/README.md new file mode 100644 index 0000000000000..ffc2af1814fd7 --- /dev/null +++ b/x-pack/test/localization/README.md @@ -0,0 +1,3 @@ +# FTR tests for the `core.i18n` service + +Contains sanity checks to ensure that Kibana can load each supported locale without failures. diff --git a/x-pack/test/localization/config.base.ts b/x-pack/test/localization/config.base.ts new file mode 100644 index 0000000000000..bad0e6d5ad5bc --- /dev/null +++ b/x-pack/test/localization/config.base.ts @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { resolve } from 'path'; +import { FtrConfigProviderContext } from '@kbn/test'; +import { services, pageObjects } from './ftr_provider_context'; + +export async function withLocale({ readConfigFile }: FtrConfigProviderContext, locale: string) { + const functionalConfig = await readConfigFile(require.resolve('../functional/config.base.js')); + + return { + ...functionalConfig.getAll(), + testFiles: [require.resolve('./tests')], + services, + pageObjects, + junit: { + reportName: `Localization (${locale}) Integration Tests`, + }, + screenshots: { + directory: resolve(__dirname, 'screenshots'), + }, + kbnTestServer: { + ...functionalConfig.get('kbnTestServer'), + serverArgs: [...functionalConfig.get('kbnTestServer.serverArgs'), `--i18n.locale=${locale}`], + }, + }; +} diff --git a/x-pack/test/localization/config.fr_fr.ts b/x-pack/test/localization/config.fr_fr.ts new file mode 100644 index 0000000000000..7527aa3a51816 --- /dev/null +++ b/x-pack/test/localization/config.fr_fr.ts @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrConfigProviderContext } from '@kbn/test'; +import { withLocale } from './config.base'; + +/* + * These tests exist in a separate configuration because: + * 1) The FTR does not support building and installing plugins against built Kibana. + * This test must be run against source only in order to build the fixture plugins. + * 2) It provides a specific service to make EBT testing easier. + * 3) The intention is to grow this suite as more developers use this feature. + */ +export default async function (ftrConfigProviderContext: FtrConfigProviderContext) { + return withLocale(ftrConfigProviderContext, 'fr-FR'); +} diff --git a/x-pack/test/localization/config.ja_jp.ts b/x-pack/test/localization/config.ja_jp.ts new file mode 100644 index 0000000000000..ee2e8a7777046 --- /dev/null +++ b/x-pack/test/localization/config.ja_jp.ts @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrConfigProviderContext } from '@kbn/test'; +import { withLocale } from './config.base'; + +/* + * These tests exist in a separate configuration because: + * 1) The FTR does not support building and installing plugins against built Kibana. + * This test must be run against source only in order to build the fixture plugins. + * 2) It provides a specific service to make EBT testing easier. + * 3) The intention is to grow this suite as more developers use this feature. + */ +export default async function (ftrConfigProviderContext: FtrConfigProviderContext) { + return withLocale(ftrConfigProviderContext, 'ja-JP'); +} diff --git a/x-pack/test/localization/config.zh_cn.ts b/x-pack/test/localization/config.zh_cn.ts new file mode 100644 index 0000000000000..58418be757cbf --- /dev/null +++ b/x-pack/test/localization/config.zh_cn.ts @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrConfigProviderContext } from '@kbn/test'; +import { withLocale } from './config.base'; + +/* + * These tests exist in a separate configuration because: + * 1) The FTR does not support building and installing plugins against built Kibana. + * This test must be run against source only in order to build the fixture plugins. + * 2) It provides a specific service to make EBT testing easier. + * 3) The intention is to grow this suite as more developers use this feature. + */ +export default async function (ftrConfigProviderContext: FtrConfigProviderContext) { + return withLocale(ftrConfigProviderContext, 'zh-CN'); +} diff --git a/x-pack/test/localization/ftr_provider_context.ts b/x-pack/test/localization/ftr_provider_context.ts new file mode 100644 index 0000000000000..c641b4efcc493 --- /dev/null +++ b/x-pack/test/localization/ftr_provider_context.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { GenericFtrProviderContext } from '@kbn/test'; +import { services } from '../functional/services'; +import { pageObjects } from '../functional/page_objects'; + +export type FtrProviderContext = GenericFtrProviderContext; +export { services, pageObjects }; diff --git a/x-pack/test/localization/tests/index.ts b/x-pack/test/localization/tests/index.ts new file mode 100644 index 0000000000000..a5c844ed24843 --- /dev/null +++ b/x-pack/test/localization/tests/index.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { FtrProviderContext } from '../ftr_provider_context'; + +export default function ({ loadTestFile }: FtrProviderContext) { + describe('Sanity checks', () => { + loadTestFile(require.resolve('./login_page')); + }); +} diff --git a/x-pack/test/localization/tests/login_page.ts b/x-pack/test/localization/tests/login_page.ts new file mode 100644 index 0000000000000..6ff2b3a9b4757 --- /dev/null +++ b/x-pack/test/localization/tests/login_page.ts @@ -0,0 +1,81 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import type { FtrProviderContext } from '../ftr_provider_context'; + +/** + * Strings Needs to be hardcoded since getting it from the i18n.translate + * function will not actually test if the expected locale is being used. + * + * The alternative would be to read directly from the filesystem but this + * would add unnecessary ties between the test suite and the localization plugin. + */ +function getExpectedI18nTranslation(locale: string): string | undefined { + switch (locale) { + case 'ja-JP': + return 'Elasticへようこそ'; + case 'zh-CN': + return '欢迎使用 Elastic'; + case 'fr-FR': + return 'Bienvenue dans Elastic'; + default: + return; + } +} + +function getI18nLocaleFromServerArgs(kbnServerArgs: string[]): string { + const re = /--i18n\.locale=(?.*)/; + for (const serverArg of kbnServerArgs) { + const match = re.exec(serverArg); + const locale = match?.groups?.locale; + if (locale) { + return locale; + } + } + + throw Error('i18n.locale is not set in the server arguments'); +} + +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const testSubjects = getService('testSubjects'); + const config = getService('config'); + const log = getService('log'); + const retry = getService('retry'); + const PageObjects = getPageObjects(['common', 'security']); + + describe('Login Page', function () { + this.tags('includeFirefox'); + + before(async () => { + await PageObjects.security.forceLogout(); + }); + + afterEach(async () => { + // NOTE: Logout needs to happen before anything else to avoid flaky behavior + await PageObjects.security.forceLogout(); + }); + + it('login page meets i18n requirements', async () => { + await PageObjects.common.navigateToApp('login'); + const serverArgs: string[] = config.get('kbnTestServer.serverArgs'); + const kbnServerLocale = getI18nLocaleFromServerArgs(serverArgs); + + log.debug(`Expecting page to be using ${kbnServerLocale} Locale.`); + + const expectedWelcomeTitleText = getExpectedI18nTranslation(kbnServerLocale); + await retry.waitFor( + 'login page visible', + async () => await testSubjects.exists('loginSubmit') + ); + const welcomeTitleText = await testSubjects.getVisibleText('loginWelcomeTitle'); + + expect(welcomeTitleText).not.to.be(undefined); + expect(welcomeTitleText).to.be(expectedWelcomeTitleText); + }); + }); +} diff --git a/x-pack/test/observability_functional/apps/observability/pages/rules_page.ts b/x-pack/test/observability_functional/apps/observability/pages/rules_page.ts index 9ba2c69885b2a..89be0d8ccbd70 100644 --- a/x-pack/test/observability_functional/apps/observability/pages/rules_page.ts +++ b/x-pack/test/observability_functional/apps/observability/pages/rules_page.ts @@ -65,7 +65,8 @@ export default ({ getService }: FtrProviderContext) => { }); }); - describe('Create rule button', () => { + // FLAKY: https://github.com/elastic/kibana/issues/146450 + describe.skip('Create rule button', () => { it('Show Create Rule flyout when Create Rule button is clicked', async () => { await observability.alerts.common.navigateToRulesPage(); await retry.waitFor( diff --git a/x-pack/test/reporting_api_integration/reporting_and_security/usage/api_counters.ts b/x-pack/test/reporting_api_integration/reporting_and_security/usage/api_counters.ts index eeeba2130629f..93b7d4d71fafb 100644 --- a/x-pack/test/reporting_api_integration/reporting_and_security/usage/api_counters.ts +++ b/x-pack/test/reporting_api_integration/reporting_and_security/usage/api_counters.ts @@ -12,8 +12,7 @@ import { FtrProviderContext } from '../../ftr_provider_context'; // eslint-disable-next-line import/no-default-export export default function ({ getService }: FtrProviderContext) { const log = getService('log'); - const supertest = getService('supertest'); - const supertestUnauth = getService('supertestWithoutAuth'); + const supertest = getService('supertestWithoutAuth'); const esArchiver = getService('esArchiver'); const usageAPI = getService('usageAPI'); const reportingAPI = getService('reportingAPI'); @@ -22,11 +21,13 @@ export default function ({ getService }: FtrProviderContext) { before(async () => { await esArchiver.emptyKibanaIndex(); await reportingAPI.initEcommerce(); + await esArchiver.load('x-pack/test/functional/es_archives/reporting/archived_reports'); }); after(async () => { await reportingAPI.deleteAllReports(); await reportingAPI.teardownEcommerce(); + await esArchiver.unload('x-pack/test/functional/es_archives/reporting/archived_reports'); }); describe('server', function () { @@ -43,7 +44,7 @@ export default function ({ getService }: FtrProviderContext) { enum paths { LIST = '/api/reporting/jobs/list', COUNT = '/api/reporting/jobs/count', - INFO = '/api/reporting/jobs/info/{docId}', + INFO = '/api/reporting/jobs/info/kraz0qle154g0763b569zz83', ILM = '/api/reporting/ilm_policy_status', DIAG_BROWSER = '/api/reporting/diagnose/browser', DIAG_SCREENSHOT = '/api/reporting/diagnose/screenshot', @@ -58,7 +59,11 @@ export default function ({ getService }: FtrProviderContext) { await Promise.all( Object.keys(paths).map(async (key) => { - await Promise.all([...Array(CALL_COUNT)].map(() => supertest.get((paths as any)[key]))); + await Promise.all( + [...Array(CALL_COUNT)].map(() => + supertest.get(paths[key as keyof typeof paths]).auth('test_user', 'changeme') + ) + ); }) ); @@ -80,30 +85,26 @@ export default function ({ getService }: FtrProviderContext) { }); it('job info', async () => { - expect(getUsageCount(initialStats, `get ${paths.INFO}`)).to.be(0); - expect(getUsageCount(stats, `get ${paths.INFO}`)).to.be(CALL_COUNT); + expect( + getUsageCount(initialStats, `get /api/reporting/jobs/info/{docId}:printable_pdf`) + ).to.be(0); + expect(getUsageCount(stats, `get /api/reporting/jobs/info/{docId}:printable_pdf`)).to.be( + CALL_COUNT + ); }); }); describe('downloading and deleting', () => { - before(async () => { - await esArchiver.load('x-pack/test/functional/es_archives/reporting/archived_reports'); - }); - - after(async () => { - await esArchiver.unload('x-pack/test/functional/es_archives/reporting/archived_reports'); - }); - it('downloading', async () => { try { await Promise.all([ - supertestUnauth + supertest .get('/api/reporting/jobs/download/kraz0qle154g0763b569zz83') .auth('test_user', 'changeme'), - supertestUnauth + supertest .get('/api/reporting/jobs/download/kraz0vj4154g0763b5curq51') .auth('test_user', 'changeme'), - supertestUnauth + supertest .get('/api/reporting/jobs/download/k9a9rq1i0gpe1457b17s7yc6') .auth('test_user', 'changeme'), ]); @@ -117,7 +118,10 @@ export default function ({ getService }: FtrProviderContext) { log.info(`calling getUsageStats...`); expect( - getUsageCount(await usageAPI.getUsageStats(), `get /api/reporting/jobs/download/{docId}`) + getUsageCount( + await usageAPI.getUsageStats(), + `get /api/reporting/jobs/download/{docId}:printable_pdf` + ) ).to.be(3); }); @@ -125,7 +129,7 @@ export default function ({ getService }: FtrProviderContext) { log.info(`sending 1 delete request...`); try { - await supertestUnauth + await supertest .delete('/api/reporting/jobs/delete/krazcyw4156m0763b503j7f9') .auth('test_user', 'changeme') .set('kbn-xsrf', 'xxx'); @@ -140,7 +144,10 @@ export default function ({ getService }: FtrProviderContext) { log.info(`calling getUsageStats...`); expect( - getUsageCount(await usageAPI.getUsageStats(), `delete /api/reporting/jobs/delete/{docId}`) + getUsageCount( + await usageAPI.getUsageStats(), + `delete /api/reporting/jobs/delete/{docId}:csv_searchsource` + ) ).to.be(1); }); }); diff --git a/x-pack/test/reporting_api_integration/reporting_and_security/usage/index.ts b/x-pack/test/reporting_api_integration/reporting_and_security/usage/index.ts index 420ec0b1c7444..f43ca39136dcb 100644 --- a/x-pack/test/reporting_api_integration/reporting_and_security/usage/index.ts +++ b/x-pack/test/reporting_api_integration/reporting_and_security/usage/index.ts @@ -9,13 +9,7 @@ import { FtrProviderContext } from '../../ftr_provider_context'; // eslint-disable-next-line import/no-default-export export default function ({ getService, loadTestFile }: FtrProviderContext) { - const reportingAPI = getService('reportingAPI'); - describe('Usage', () => { - const deleteAllReports = () => reportingAPI.deleteAllReports(); - beforeEach(deleteAllReports); - after(deleteAllReports); - loadTestFile(require.resolve('./archived_data')); loadTestFile(require.resolve('./initial')); loadTestFile(require.resolve('./metrics')); diff --git a/x-pack/test/reporting_api_integration/reporting_and_security/usage/new_jobs.ts b/x-pack/test/reporting_api_integration/reporting_and_security/usage/new_jobs.ts index e702be05f9bd8..0377b277defbb 100644 --- a/x-pack/test/reporting_api_integration/reporting_and_security/usage/new_jobs.ts +++ b/x-pack/test/reporting_api_integration/reporting_and_security/usage/new_jobs.ts @@ -38,12 +38,7 @@ export default function ({ getService }: FtrProviderContext) { ); const usage = await usageAPI.getUsageStats(); - reportingAPI.expectRecentPdfAppStats(usage, 'visualization', 0); - reportingAPI.expectRecentPdfAppStats(usage, 'dashboard', 0); - reportingAPI.expectRecentPdfLayoutStats(usage, 'preserve_layout', 0); - reportingAPI.expectRecentPdfLayoutStats(usage, 'print', 0); reportingAPI.expectRecentJobTypeTotalStats(usage, 'csv_searchsource', 1); - reportingAPI.expectRecentJobTypeTotalStats(usage, 'printable_pdf', 0); }); it('should handle preserve_layout pdf', async () => { @@ -55,12 +50,8 @@ export default function ({ getService }: FtrProviderContext) { ); const usage = await usageAPI.getUsageStats(); - reportingAPI.expectRecentPdfAppStats(usage, 'visualization', 1); - reportingAPI.expectRecentPdfAppStats(usage, 'dashboard', 1); reportingAPI.expectRecentPdfLayoutStats(usage, 'preserve_layout', 2); - reportingAPI.expectRecentPdfLayoutStats(usage, 'print', 0); - reportingAPI.expectRecentJobTypeTotalStats(usage, 'csv_searchsource', 0); - reportingAPI.expectRecentJobTypeTotalStats(usage, 'printable_pdf', 2); + reportingAPI.expectAllTimePdfLayoutStats(usage, 'preserve_layout', 2); }); it('should handle print_layout pdf', async () => { @@ -72,19 +63,8 @@ export default function ({ getService }: FtrProviderContext) { ); const usage = await usageAPI.getUsageStats(); - reportingAPI.expectRecentPdfAppStats(usage, 'visualization', 1); - reportingAPI.expectRecentPdfAppStats(usage, 'dashboard', 1); - reportingAPI.expectRecentPdfLayoutStats(usage, 'preserve_layout', 1); reportingAPI.expectRecentPdfLayoutStats(usage, 'print', 1); - reportingAPI.expectRecentJobTypeTotalStats(usage, 'csv_searchsource', 0); - reportingAPI.expectRecentJobTypeTotalStats(usage, 'printable_pdf', 2); - - reportingAPI.expectAllTimePdfAppStats(usage, 'visualization', 1); - reportingAPI.expectAllTimePdfAppStats(usage, 'dashboard', 1); - reportingAPI.expectAllTimePdfLayoutStats(usage, 'preserve_layout', 1); reportingAPI.expectAllTimePdfLayoutStats(usage, 'print', 1); - reportingAPI.expectAllTimeJobTypeTotalStats(usage, 'csv_searchsource', 0); - reportingAPI.expectAllTimeJobTypeTotalStats(usage, 'printable_pdf', 2); }); }); } diff --git a/x-pack/test/reporting_api_integration/services/scenarios.ts b/x-pack/test/reporting_api_integration/services/scenarios.ts index c61bb809da3a4..181af370e42c6 100644 --- a/x-pack/test/reporting_api_integration/services/scenarios.ts +++ b/x-pack/test/reporting_api_integration/services/scenarios.ts @@ -5,7 +5,7 @@ * 2.0. */ -import rison, { RisonValue } from 'rison-node'; +import rison from '@kbn/rison'; import { API_GET_ILM_POLICY_STATUS, API_MIGRATE_ILM_POLICY_URL, @@ -143,7 +143,7 @@ export function createScenarios({ getService }: Pick { - const jobParams = rison.encode(job as object as RisonValue); + const jobParams = rison.encode(job); return await supertestWithoutAuth .post(`/api/reporting/generate/printablePdf`) .auth(username, password) @@ -151,7 +151,7 @@ export function createScenarios({ getService }: Pick { - const jobParams = rison.encode(job as object as RisonValue); + const jobParams = rison.encode(job); return await supertestWithoutAuth .post(`/api/reporting/generate/png`) .auth(username, password) @@ -163,7 +163,7 @@ export function createScenarios({ getService }: Pick { - const jobParams = rison.encode(job as object as RisonValue); + const jobParams = rison.encode(job); return await supertestWithoutAuth .post(`/api/reporting/generate/csv_searchsource`) diff --git a/x-pack/test/rule_registry/spaces_only/tests/trial/get_summarized_alerts.ts b/x-pack/test/rule_registry/spaces_only/tests/trial/get_summarized_alerts.ts index a5d6763c35c47..cc72f48d31613 100644 --- a/x-pack/test/rule_registry/spaces_only/tests/trial/get_summarized_alerts.ts +++ b/x-pack/test/rule_registry/spaces_only/tests/trial/get_summarized_alerts.ts @@ -180,7 +180,7 @@ export default function createGetSummarizedAlertsTest({ getService }: FtrProvide const getState = ( shouldTriggerAlert: boolean, alerts: Record - ) => ({ wrapped: { shouldTriggerAlert }, trackedAlerts: alerts }); + ) => ({ wrapped: { shouldTriggerAlert }, trackedAlerts: alerts, trackedAlertsRecovered: {} }); // Execute the rule the first time - this creates a new alert const preExecution1Start = new Date(); diff --git a/x-pack/test/scalability/config.ts b/x-pack/test/scalability/config.ts index 49bcfee2cf199..0d3303faf5970 100644 --- a/x-pack/test/scalability/config.ts +++ b/x-pack/test/scalability/config.ts @@ -62,8 +62,6 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { ...(!!AGGS_SHARD_DELAY ? ['--data.search.aggs.shardDelay.enabled=true'] : []), ...(!!DISABLE_PLUGINS ? ['--plugins.initialize=false'] : []), ], - // delay shutdown to ensure that APM can report the data it collects during test execution - delayShutdown: 90_000, }, }; } diff --git a/x-pack/test/scalability/runner.ts b/x-pack/test/scalability/runner.ts index e09a9d438b410..5882237ade467 100644 --- a/x-pack/test/scalability/runner.ts +++ b/x-pack/test/scalability/runner.ts @@ -6,6 +6,7 @@ */ import { withProcRunner } from '@kbn/dev-proc-runner'; +import path from 'path'; import { FtrProviderContext } from './ftr_provider_context'; /** @@ -19,6 +20,7 @@ export async function ScalabilityTestRunner( gatlingProjectRootPath: string ) { const log = getService('log'); + const gatlingReportBaseDir = path.parse(scalabilityJsonPath).name; log.info(`Running scalability test with json file: '${scalabilityJsonPath}'`); @@ -28,6 +30,7 @@ export async function ScalabilityTestRunner( args: [ 'gatling:test', '-q', + `-Dgatling.core.outputDirectoryBaseName=${gatlingReportBaseDir}`, '-Dgatling.simulationClass=org.kibanaLoadTest.simulation.generic.GenericJourney', `-DjourneyPath=${scalabilityJsonPath}`, ], diff --git a/x-pack/test/screenshot_creation/apps/ml_docs/anomaly_detection/custom_urls.ts b/x-pack/test/screenshot_creation/apps/ml_docs/anomaly_detection/custom_urls.ts index 285239abcac66..55f0bc88761a3 100644 --- a/x-pack/test/screenshot_creation/apps/ml_docs/anomaly_detection/custom_urls.ts +++ b/x-pack/test/screenshot_creation/apps/ml_docs/anomaly_detection/custom_urls.ts @@ -94,7 +94,7 @@ export default function ({ getService }: FtrProviderContext) { await ml.anomaliesTable.scrollTableIntoView(); await ml.anomaliesTable.ensureAnomalyActionsMenuOpen(0); - await commonScreenshots.takeScreenshot('ml-population-results', screenshotDirectories); + await commonScreenshots.takeScreenshot('ml-customurl', screenshotDirectories); }); }); } diff --git a/x-pack/test/screenshot_creation/apps/ml_docs/anomaly_detection/generate_anomaly_alerts.ts b/x-pack/test/screenshot_creation/apps/ml_docs/anomaly_detection/generate_anomaly_alerts.ts new file mode 100644 index 0000000000000..5246eee8c5ab3 --- /dev/null +++ b/x-pack/test/screenshot_creation/apps/ml_docs/anomaly_detection/generate_anomaly_alerts.ts @@ -0,0 +1,168 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import { DATAFEED_STATE } from '@kbn/ml-plugin/common/constants/states'; +import { FtrProviderContext } from '../../../ftr_provider_context'; + +import { ECOMMERCE_INDEX_PATTERN } from '..'; + +function createTestJobAndDatafeed() { + const timestamp = Date.now(); + const jobId = `high_sum_total_sales_${timestamp}`; + + return { + job: { + job_id: jobId, + description: 'test_job', + groups: ['ecommerce'], + analysis_config: { + bucket_span: '1h', + detectors: [ + { + detector_description: 'High total sales', + function: 'high_sum', + field_name: 'taxful_total_price', + over_field_name: 'customer_full_name.keyword', + detector_index: 0, + }, + ], + influencers: ['customer_full_name.keyword', 'category.keyword'], + }, + data_description: { + time_field: 'order_date', + time_format: 'epoch_ms', + }, + analysis_limits: { + model_memory_limit: '13mb', + categorization_examples_limit: 4, + }, + }, + datafeed: { + datafeed_id: `datafeed-${jobId}`, + job_id: jobId, + query: { + bool: { + must: [ + { + match_all: {}, + }, + ], + filter: [], + must_not: [], + }, + }, + query_delay: '120s', + indices: [ECOMMERCE_INDEX_PATTERN], + } as unknown as estypes.MlDatafeed, + }; +} + +export default ({ getPageObjects, getService }: FtrProviderContext) => { + const esArchiver = getService('esArchiver'); + const ml = getService('ml'); + const pageObjects = getPageObjects(['triggersActionsUI']); + const commonScreenshots = getService('commonScreenshots'); + const browser = getService('browser'); + const actions = getService('actions'); + + const screenshotDirectories = ['ml_docs', 'anomaly_detection']; + + let testJobId = ''; + + describe('anomaly detection alert', function () { + before(async () => { + await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/ml/ecommerce'); + await ml.testResources.createIndexPatternIfNeeded('ft_ecommerce', 'order_date'); + + const { job, datafeed } = createTestJobAndDatafeed(); + + testJobId = job.job_id; + + // Set up jobs + // @ts-expect-error not full interface + await ml.api.createAnomalyDetectionJob(job); + await ml.api.openAnomalyDetectionJob(job.job_id); + await ml.api.createDatafeed(datafeed); + await ml.api.startDatafeed(datafeed.datafeed_id); + await ml.api.waitForDatafeedState(datafeed.datafeed_id, DATAFEED_STATE.STARTED); + await ml.api.assertJobResultsExist(job.job_id); + }); + + after(async () => { + await ml.api.deleteAnomalyDetectionJobES(testJobId); + await ml.api.cleanMlIndices(); + await ml.alerting.cleanAnomalyDetectionRules(); + await actions.api.deleteAllConnectors(); + }); + + describe('overview page alert flyout controls', () => { + it('alert flyout screenshots', async () => { + await ml.navigation.navigateToAlertsAndAction(); + await pageObjects.triggersActionsUI.clickCreateAlertButton(); + await ml.alerting.setRuleName('test-ecommerce'); + + await ml.alerting.openNotifySelection(); + await commonScreenshots.takeScreenshot('ml-rule', screenshotDirectories, 1920, 1400); + + // close popover + await browser.pressKeys(browser.keys.ESCAPE); + + await ml.alerting.selectAnomalyDetectionJobHealthAlertType(); + await ml.alerting.selectJobs([testJobId]); + await ml.testExecution.logTestStep('take screenshot'); + await commonScreenshots.takeScreenshot( + 'ml-health-check-config', + screenshotDirectories, + 1920, + 1400 + ); + await ml.alerting.clickCancelSaveRuleButton(); + + await pageObjects.triggersActionsUI.clickCreateAlertButton(); + await ml.alerting.setRuleName('test-ecommerce'); + await ml.alerting.selectAnomalyDetectionAlertType(); + await ml.testExecution.logTestStep('should have correct default values'); + await ml.alerting.assertSeverity(75); + await ml.alerting.assertPreviewButtonState(false); + await ml.testExecution.logTestStep('should complete the alert params'); + await ml.alerting.selectJobs([testJobId]); + await ml.alerting.selectResultType('bucket'); + await ml.alerting.setSeverity(75); + await ml.testExecution.logTestStep('should populate advanced settings with default values'); + await ml.alerting.assertTopNBuckets(1); + await ml.alerting.assertLookbackInterval('123m'); + await ml.testExecution.logTestStep('should preview the alert condition'); + await ml.alerting.assertPreviewButtonState(false); + await ml.alerting.setTestInterval('1y'); + await ml.alerting.assertPreviewButtonState(true); + await ml.alerting.scrollRuleNameIntoView(); + await ml.testExecution.logTestStep('take screenshot'); + await commonScreenshots.takeScreenshot( + 'ml-anomaly-alert-severity', + screenshotDirectories, + 1920, + 1400 + ); + await ml.alerting.selectSlackConnectorType(); + await ml.testExecution.logTestStep('should open connectors'); + await ml.alerting.clickCreateConnectorButton(); + await ml.alerting.setConnectorName('test-connector'); + await ml.alerting.setWebhookUrl('https://www.elastic.co'); + await ml.alerting.clickSaveActionButton(); + await ml.alerting.openAddRuleVariable(); + await ml.testExecution.logTestStep('take screenshot'); + await commonScreenshots.takeScreenshot( + 'ml-anomaly-alert-messages', + screenshotDirectories, + 1920, + 1400 + ); + }); + }); + }); +}; diff --git a/x-pack/test/screenshot_creation/apps/ml_docs/anomaly_detection/geographic_data.ts b/x-pack/test/screenshot_creation/apps/ml_docs/anomaly_detection/geographic_data.ts index 8c7bb633a2fa2..66fa058e0868f 100644 --- a/x-pack/test/screenshot_creation/apps/ml_docs/anomaly_detection/geographic_data.ts +++ b/x-pack/test/screenshot_creation/apps/ml_docs/anomaly_detection/geographic_data.ts @@ -13,10 +13,11 @@ import { ECOMMERCE_INDEX_PATTERN, LOGS_INDEX_PATTERN } from '..'; export default function ({ getPageObject, getService }: FtrProviderContext) { const elasticChart = getService('elasticChart'); - const maps = getPageObject('maps'); const ml = getService('ml'); const commonScreenshots = getService('commonScreenshots'); const renderable = getService('renderable'); + const maps = getPageObject('maps'); + const timePicker = getPageObject('timePicker'); const screenshotDirectories = ['ml_docs', 'anomaly_detection']; @@ -76,9 +77,12 @@ export default function ({ getPageObject, getService }: FtrProviderContext) { describe('geographic data', function () { before(async () => { + // Stop the sample data feed about three months after the current date to capture anomaly + const dateStopString = new Date(Date.now() + 131400 * 60 * 1000).toISOString(); await ml.api.createAndRunAnomalyDetectionLookbackJob( ecommerceGeoJobConfig as Job, - ecommerceGeoDatafeedConfig as Datafeed + ecommerceGeoDatafeedConfig as Datafeed, + { end: dateStopString } ); await ml.api.createAndRunAnomalyDetectionLookbackJob( weblogGeoJobConfig as Job, @@ -222,8 +226,7 @@ export default function ({ getPageObject, getService }: FtrProviderContext) { ); }); - // the job stopped to produce an anomaly, needs investigation - it.skip('ecommerce anomaly explorer screenshots', async () => { + it('ecommerce anomaly explorer screenshots', async () => { await ml.testExecution.logTestStep('navigate to job list'); await ml.navigation.navigateToMl(); await ml.navigation.navigateToJobManagement(); @@ -233,6 +236,13 @@ export default function ({ getPageObject, getService }: FtrProviderContext) { await ml.jobTable.filterWithSearchString(ecommerceGeoJobConfig.job_id, 1); await ml.jobTable.clickOpenJobInAnomalyExplorerButton(ecommerceGeoJobConfig.job_id); await ml.commonUI.waitForMlLoadingIndicatorToDisappear(); + await ml.testExecution.logTestStep('Choose time range...'); + await timePicker.setCommonlyUsedTime('sample_data range'); + + await ml.testExecution.logTestStep('open anomaly list actions and take screenshot'); + await ml.anomaliesTable.scrollTableIntoView(); + await ml.anomaliesTable.ensureAnomalyActionsMenuOpen(0); + await commonScreenshots.takeScreenshot('view-in-maps', screenshotDirectories); await ml.testExecution.logTestStep('select swim lane tile'); const cells = await ml.swimLane.getCells(overallSwimLaneTestSubj); @@ -242,11 +252,9 @@ export default function ({ getPageObject, getService }: FtrProviderContext) { y: sampleCell.y + cellSize, }); await ml.swimLane.waitForSwimLanesToLoad(); - - await ml.testExecution.logTestStep('take screenshot'); + await ml.testExecution.logTestStep('open anomaly list details and take screenshot'); await ml.anomaliesTable.ensureDetailsOpen(0); await ml.anomalyExplorer.scrollChartsContainerIntoView(); - await commonScreenshots.takeScreenshot( 'ecommerce-anomaly-explorer-geopoint', screenshotDirectories diff --git a/x-pack/test/screenshot_creation/apps/ml_docs/anomaly_detection/index.ts b/x-pack/test/screenshot_creation/apps/ml_docs/anomaly_detection/index.ts index 389a240eaa464..4d092c36d6545 100644 --- a/x-pack/test/screenshot_creation/apps/ml_docs/anomaly_detection/index.ts +++ b/x-pack/test/screenshot_creation/apps/ml_docs/anomaly_detection/index.ts @@ -10,6 +10,7 @@ import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('anomaly detection', function () { loadTestFile(require.resolve('./geographic_data')); + loadTestFile(require.resolve('./generate_anomaly_alerts')); loadTestFile(require.resolve('./population_analysis')); loadTestFile(require.resolve('./custom_urls')); loadTestFile(require.resolve('./mapping_anomalies')); diff --git a/x-pack/test/screenshot_creation/apps/ml_docs/anomaly_detection/population_analysis.ts b/x-pack/test/screenshot_creation/apps/ml_docs/anomaly_detection/population_analysis.ts index 23fa9f047a5ce..caad3cc76ae5c 100644 --- a/x-pack/test/screenshot_creation/apps/ml_docs/anomaly_detection/population_analysis.ts +++ b/x-pack/test/screenshot_creation/apps/ml_docs/anomaly_detection/population_analysis.ts @@ -14,7 +14,6 @@ export default function ({ getService }: FtrProviderContext) { const elasticChart = getService('elasticChart'); const ml = getService('ml'); const commonScreenshots = getService('commonScreenshots'); - const testSubjects = getService('testSubjects'); const screenshotDirectories = ['ml_docs', 'anomaly_detection']; @@ -85,22 +84,12 @@ export default function ({ getService }: FtrProviderContext) { await ml.jobTable.filterWithSearchString(populationJobConfig.job_id, 1); await ml.jobTable.clickOpenJobInAnomalyExplorerButton(populationJobConfig.job_id); await ml.commonUI.waitForMlLoadingIndicatorToDisappear(); - - await ml.testExecution.logTestStep('open tooltip and take screenshot'); - const viewBySwimLanes = await testSubjects.find(viewBySwimLaneTestSubj); - const cells = await ml.swimLane.getCells(viewBySwimLaneTestSubj); - const sampleCell = cells[0]; - - await viewBySwimLanes.moveMouseTo({ - xOffset: Math.floor(cellSize / 2.0), - yOffset: Math.floor(cellSize / 2.0), - }); - await commonScreenshots.takeScreenshot('ml-population-results', screenshotDirectories); - await ml.testExecution.logTestStep( 'select swim lane tile, expand anomaly row and take screenshot' ); + const cells = await ml.swimLane.getCells(viewBySwimLaneTestSubj); + const sampleCell = cells[0]; await ml.swimLane.selectSingleCell(viewBySwimLaneTestSubj, { x: sampleCell.x + cellSize, y: sampleCell.y + cellSize, diff --git a/x-pack/test/screenshot_creation/config.ts b/x-pack/test/screenshot_creation/config.ts index c83e90658ff72..5464806c20726 100644 --- a/x-pack/test/screenshot_creation/config.ts +++ b/x-pack/test/screenshot_creation/config.ts @@ -8,6 +8,7 @@ import Fs from 'fs'; import { CA_CERT_PATH } from '@kbn/dev-utils'; import { FtrConfigProviderContext } from '@kbn/test'; +import { pageObjects } from './page_objects'; import { services } from './services'; export default async function ({ readConfigFile }: FtrConfigProviderContext) { @@ -28,6 +29,7 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { // default to the xpack functional config ...xpackFunctionalConfig.getAll(), servers, + pageObjects, services, testFiles: [require.resolve('./apps')], junit: { diff --git a/x-pack/test/security_solution_cypress/config.ts b/x-pack/test/security_solution_cypress/config.ts index 843d6457b55f1..81d18ce1cab5d 100644 --- a/x-pack/test/security_solution_cypress/config.ts +++ b/x-pack/test/security_solution_cypress/config.ts @@ -39,9 +39,6 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { '--usageCollection.uiCounters.enabled=false', // define custom kibana server args here `--elasticsearch.ssl.certificateAuthorities=${CA_CERT_PATH}`, - // retrieve rules from the filesystem but not from fleet for Cypress tests - '--xpack.securitySolution.prebuiltRulesFromFileSystem=true', - '--xpack.securitySolution.prebuiltRulesFromSavedObjects=false', '--xpack.ruleRegistry.write.enabled=true', '--xpack.ruleRegistry.write.cache.enabled=false', '--xpack.ruleRegistry.unsafe.indexUpgrade.enabled=true', diff --git a/x-pack/test/security_solution_endpoint/services/endpoint_policy.ts b/x-pack/test/security_solution_endpoint/services/endpoint_policy.ts index 9d90e2530c18c..db2f12694ac7c 100644 --- a/x-pack/test/security_solution_endpoint/services/endpoint_policy.ts +++ b/x-pack/test/security_solution_endpoint/services/endpoint_policy.ts @@ -33,7 +33,7 @@ const INGEST_API_PACKAGE_POLICIES = `${INGEST_API_ROOT}/package_policies`; const INGEST_API_PACKAGE_POLICIES_DELETE = `${INGEST_API_PACKAGE_POLICIES}/delete`; const INGEST_API_EPM_PACKAGES = `${INGEST_API_ROOT}/epm/packages`; -const SECURITY_PACKAGES_ROUTE = `${INGEST_API_EPM_PACKAGES}?category=security&experimental=true`; +const SECURITY_PACKAGES_ROUTE = `${INGEST_API_EPM_PACKAGES}?category=security&prerelease=true`; /** * Holds information about the test resources created to support an Endpoint Policy diff --git a/x-pack/test/security_solution_endpoint_api_int/apis/endpoint_authz.ts b/x-pack/test/security_solution_endpoint_api_int/apis/endpoint_authz.ts index 9b7212b20b833..8c820e95f83bf 100644 --- a/x-pack/test/security_solution_endpoint_api_int/apis/endpoint_authz.ts +++ b/x-pack/test/security_solution_endpoint_api_int/apis/endpoint_authz.ts @@ -17,26 +17,12 @@ import { UNISOLATE_HOST_ROUTE_V2, } from '@kbn/security-solution-plugin/common/endpoint/constants'; import { FtrProviderContext } from '../ftr_provider_context'; -import { - createUserAndRole, - deleteUserAndRole, - ROLES, -} from '../../common/services/security_solution'; +import { ROLE } from '../services/roles_users'; export default function ({ getService }: FtrProviderContext) { const supertestWithoutAuth = getService('supertestWithoutAuth'); describe('When attempting to call an endpoint api with no authz', () => { - before(async () => { - // create role/user - await createUserAndRole(getService, ROLES.t1_analyst); - }); - - after(async () => { - // delete role/user - await deleteUserAndRole(getService, ROLES.t1_analyst); - }); - const apiList = [ { method: 'get', @@ -90,7 +76,7 @@ export default function ({ getService }: FtrProviderContext) { apiListItem.path }]`, async () => { await supertestWithoutAuth[apiListItem.method](apiListItem.path) - .auth(ROLES.t1_analyst, 'changeme') + .auth(ROLE.t1_analyst, 'changeme') .set('kbn-xsrf', 'xxx') .send(apiListItem.body) .expect(403, { diff --git a/x-pack/test/security_solution_endpoint_api_int/apis/index.ts b/x-pack/test/security_solution_endpoint_api_int/apis/index.ts index 22a7a5d7567ef..f6530a76a18ab 100644 --- a/x-pack/test/security_solution_endpoint_api_int/apis/index.ts +++ b/x-pack/test/security_solution_endpoint_api_int/apis/index.ts @@ -8,12 +8,14 @@ import { getRegistryUrl as getRegistryUrlFromIngest } from '@kbn/fleet-plugin/server'; import { FtrProviderContext } from '../ftr_provider_context'; import { isRegistryEnabled, getRegistryUrlFromTestEnv } from '../registry'; +import { ROLE } from '../services/roles_users'; export default function endpointAPIIntegrationTests(providerContext: FtrProviderContext) { const { loadTestFile, getService } = providerContext; describe('Endpoint plugin', function () { const ingestManager = getService('ingestManager'); + const rolesUsersProvider = getService('rolesUsersProvider'); const log = getService('log'); @@ -24,9 +26,22 @@ export default function endpointAPIIntegrationTests(providerContext: FtrProvider const registryUrl = getRegistryUrlFromTestEnv() ?? getRegistryUrlFromIngest(); log.info(`Package registry URL for tests: ${registryUrl}`); + const roles = Object.values(ROLE); before(async () => { await ingestManager.setup(); + + // create role/user + for (const role of roles) { + await rolesUsersProvider.createRole({ predefinedRole: role }); + await rolesUsersProvider.createUser({ name: role, roles: [role] }); + } + }); + after(async () => { + // delete role/user + await rolesUsersProvider.deleteUsers(roles); + await rolesUsersProvider.deleteRoles(roles); }); + loadTestFile(require.resolve('./resolver')); loadTestFile(require.resolve('./metadata')); loadTestFile(require.resolve('./policy')); diff --git a/x-pack/test/security_solution_endpoint_api_int/services/index.ts b/x-pack/test/security_solution_endpoint_api_int/services/index.ts index eb66d738af1b0..e94e41f37b922 100644 --- a/x-pack/test/security_solution_endpoint_api_int/services/index.ts +++ b/x-pack/test/security_solution_endpoint_api_int/services/index.ts @@ -7,6 +7,7 @@ import { services as xPackAPIServices } from '../../api_integration/services'; import { ResolverGeneratorProvider } from './resolver'; +import { RolesUsersProvider } from './roles_users'; import { EndpointTestResources } from '../../security_solution_endpoint/services/endpoint'; import { EndpointPolicyTestResourcesProvider } from '../../security_solution_endpoint/services/endpoint_policy'; import { EndpointArtifactsTestResources } from '../../security_solution_endpoint/services/endpoint_artifacts'; @@ -17,4 +18,5 @@ export const services = { endpointTestResources: EndpointTestResources, endpointPolicyTestResources: EndpointPolicyTestResourcesProvider, endpointArtifactTestResources: EndpointArtifactsTestResources, + rolesUsersProvider: RolesUsersProvider, }; diff --git a/x-pack/test/security_solution_endpoint_api_int/services/roles_users.ts b/x-pack/test/security_solution_endpoint_api_int/services/roles_users.ts new file mode 100644 index 0000000000000..c10b6a64658eb --- /dev/null +++ b/x-pack/test/security_solution_endpoint_api_int/services/roles_users.ts @@ -0,0 +1,104 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { Role } from '@kbn/security-plugin/common'; + +import { getT1Analyst } from '@kbn/security-solution-plugin/scripts/endpoint/common/roles_users/t1_analyst'; +import { getT2Analyst } from '@kbn/security-solution-plugin/scripts/endpoint/common/roles_users/t2_analyst'; +import { getHunter } from '@kbn/security-solution-plugin/scripts/endpoint/common/roles_users/hunter'; +import { getThreadIntelligenceAnalyst } from '@kbn/security-solution-plugin/scripts/endpoint/common/roles_users/thread_intelligence_analyst'; +import { getSocManager } from '@kbn/security-solution-plugin/scripts/endpoint/common/roles_users/soc_manager'; +import { getPlatformEngineer } from '@kbn/security-solution-plugin/scripts/endpoint/common/roles_users/platform_engineer'; +import { getEndpointOperationsAnalyst } from '@kbn/security-solution-plugin/scripts/endpoint/common/roles_users/endpoint_operations_analyst'; +import { getEndpointSecurityPolicyManager } from '@kbn/security-solution-plugin/scripts/endpoint/common/roles_users/endpoint_security_policy_manager'; + +import { FtrProviderContext } from '../ftr_provider_context'; + +export enum ROLE { + t1_analyst = 't1Analyst', + t2_analyst = 't2Analyst', + analyst_hunter = 'hunter', + thread_intelligence_analyst = 'threadIntelligenceAnalyst', + detections_engineer = 'detectionsEngineer', + soc_manager = 'socManager', + platform_engineer = 'platformEngineer', + endpoint_operations_analyst = 'endpointOperationsAnalyst', + endpoint_security_policy_manager = 'endpointSecurityPolicyManager', +} + +const rolesMapping: { [id: string]: Omit } = { + t1Analyst: getT1Analyst(), + t2Analyst: getT2Analyst(), + hunter: getHunter(), + threadIntelligenceAnalyst: getThreadIntelligenceAnalyst(), + socManager: getSocManager(), + platformEngineer: getPlatformEngineer(), + endpointOperationsAnalyst: getEndpointOperationsAnalyst(), + endpointSecurityPolicyManager: getEndpointSecurityPolicyManager(), +}; + +export function RolesUsersProvider({ getService }: FtrProviderContext) { + const security = getService('security'); + return { + /** + * Creates an user with specific values + * @param user + */ + async createUser(user: { name: string; roles: string[]; password?: string }): Promise { + const { name, roles, password } = user; + await security.user.create(name, { roles, password: password ?? 'changeme' }); + }, + + /** + * Deletes specified users by username + * @param names[] + */ + async deleteUsers(names: string[]): Promise { + for (const name of names) { + await security.user.delete(name); + } + }, + + /** + * Creates a role using predefined role config if defined or a custom one. It also allows define extra privileges. + * @param options + */ + async createRole(options: { + predefinedRole?: ROLE; + extraPrivileges?: string[]; + customRole?: { roleName: string; extraPrivileges: string[] }; + }): Promise { + const { predefinedRole, customRole, extraPrivileges } = options; + if (predefinedRole) { + const roleConfig = rolesMapping[predefinedRole]; + if (extraPrivileges) { + roleConfig.kibana[0].feature.siem = [ + ...roleConfig.kibana[0].feature.siem, + ...extraPrivileges, + ]; + } + + await security.role.create(predefinedRole, rolesMapping[predefinedRole]); + } + if (customRole) { + await security.role.create(customRole.roleName, { + permissions: { feature: { siem: [...customRole.extraPrivileges] } }, + }); + } + }, + + /** + * Deletes specified roles by name + * @param roles[] + */ + async deleteRoles(roles: string[]): Promise { + for (const role of roles) { + await security.role.delete(role); + } + }, + }; +} diff --git a/x-pack/test/security_solution_ftr/services/detections/endpoint_rule_alert_generator.ts b/x-pack/test/security_solution_ftr/services/detections/endpoint_rule_alert_generator.ts index a02a1a2469578..87b5f9a29784d 100644 --- a/x-pack/test/security_solution_ftr/services/detections/endpoint_rule_alert_generator.ts +++ b/x-pack/test/security_solution_ftr/services/detections/endpoint_rule_alert_generator.ts @@ -6,7 +6,6 @@ */ import { BaseDataGenerator } from '@kbn/security-solution-plugin/common/endpoint/data_generators/base_data_generator'; -import endpointPrePackagedRule from '@kbn/security-solution-plugin/server/lib/detection_engine/prebuilt_rules/content/prepackaged_rules/elastic_endpoint_security.json'; import { kibanaPackageJson } from '@kbn/utils'; import { mergeWith } from 'lodash'; import { EndpointMetadataGenerator } from '@kbn/security-solution-plugin/common/endpoint/data_generators/endpoint_metadata_generator'; @@ -229,17 +228,90 @@ export class EndpointRuleAlertGenerator extends BaseDataGenerator { ], 'kibana.alert.rule.execution.uuid': this.seededUUIDv4(), 'kibana.alert.rule.false_positives': [], - 'kibana.alert.rule.from': endpointPrePackagedRule.from, + 'kibana.alert.rule.from': 'now-10m', 'kibana.alert.rule.immutable': true, - 'kibana.alert.rule.indices': endpointPrePackagedRule.index, + 'kibana.alert.rule.indices': ['logs-endpoint.alerts-*'], 'kibana.alert.rule.interval': '5m', 'kibana.alert.rule.license': 'Elastic License v2', 'kibana.alert.rule.max_signals': 10000, - 'kibana.alert.rule.name': endpointPrePackagedRule.name, - 'kibana.alert.rule.parameters': endpointPrePackagedRule, + 'kibana.alert.rule.name': 'Endpoint Security', + 'kibana.alert.rule.parameters': { + author: ['Elastic'], + description: + 'Generates a detection alert each time an Elastic Endpoint Security alert is received. Enabling this rule allows you to immediately begin investigating your Endpoint alerts.', + enabled: true, + exceptions_list: [ + { + id: 'endpoint_list', + list_id: 'endpoint_list', + namespace_type: 'agnostic', + type: 'endpoint', + }, + ], + from: 'now-10m', + index: ['logs-endpoint.alerts-*'], + language: 'kuery', + license: 'Elastic License v2', + max_signals: 10000, + name: 'Endpoint Security', + query: 'event.kind:alert and event.module:(endpoint and not endgame)\n', + required_fields: [ + { + ecs: true, + name: 'event.kind', + type: 'keyword', + }, + { + ecs: true, + name: 'event.module', + type: 'keyword', + }, + ], + risk_score: 47, + risk_score_mapping: [ + { + field: 'event.risk_score', + operator: 'equals', + value: '', + }, + ], + rule_id: '9a1a2dae-0b5f-4c3d-8305-a268d404c306', + rule_name_override: 'message', + severity: 'medium', + severity_mapping: [ + { + field: 'event.severity', + operator: 'equals', + severity: 'low', + value: '21', + }, + { + field: 'event.severity', + operator: 'equals', + severity: 'medium', + value: '47', + }, + { + field: 'event.severity', + operator: 'equals', + severity: 'high', + value: '73', + }, + { + field: 'event.severity', + operator: 'equals', + severity: 'critical', + value: '99', + }, + ], + tags: ['Elastic', 'Endpoint Security'], + timestamp_override: 'event.ingested', + type: 'query', + version: 100, + }, 'kibana.alert.rule.producer': 'siem', 'kibana.alert.rule.references': [], - 'kibana.alert.rule.risk_score': endpointPrePackagedRule.risk_score, + 'kibana.alert.rule.risk_score': 47, 'kibana.alert.rule.risk_score_mapping': [ { field: 'event.risk_score', @@ -247,14 +319,39 @@ export class EndpointRuleAlertGenerator extends BaseDataGenerator { value: '', }, ], - 'kibana.alert.rule.rule_id': endpointPrePackagedRule.rule_id, + 'kibana.alert.rule.rule_id': '9a1a2dae-0b5f-4c3d-8305-a268d404c306', 'kibana.alert.rule.rule_name_override': 'message', 'kibana.alert.rule.rule_type_id': 'siem.queryRule', 'kibana.alert.rule.severity': 'medium', - 'kibana.alert.rule.severity_mapping': endpointPrePackagedRule.severity_mapping, - 'kibana.alert.rule.tags': endpointPrePackagedRule.tags, + 'kibana.alert.rule.severity_mapping': [ + { + field: 'event.severity', + operator: 'equals', + severity: 'low', + value: '21', + }, + { + field: 'event.severity', + operator: 'equals', + severity: 'medium', + value: '47', + }, + { + field: 'event.severity', + operator: 'equals', + severity: 'high', + value: '73', + }, + { + field: 'event.severity', + operator: 'equals', + severity: 'critical', + value: '99', + }, + ], + 'kibana.alert.rule.tags': ['Elastic', 'Endpoint Security'], 'kibana.alert.rule.threat': [], - 'kibana.alert.rule.timestamp_override': endpointPrePackagedRule.timestamp_override, + 'kibana.alert.rule.timestamp_override': 'event.ingested', 'kibana.alert.rule.to': 'now', 'kibana.alert.rule.type': 'query', 'kibana.alert.rule.updated_at': '2022-10-26T21:02:00.237Z', diff --git a/x-pack/test/security_solution_ftr/services/detections/index.ts b/x-pack/test/security_solution_ftr/services/detections/index.ts index 2fbc8c158640c..e7cbc15f99305 100644 --- a/x-pack/test/security_solution_ftr/services/detections/index.ts +++ b/x-pack/test/security_solution_ftr/services/detections/index.ts @@ -14,13 +14,13 @@ import { DETECTION_ENGINE_RULES_URL, } from '@kbn/security-solution-plugin/common/constants'; import { estypes } from '@elastic/elasticsearch'; -import endpointPrePackagedRule from '@kbn/security-solution-plugin/server/lib/detection_engine/prebuilt_rules/content/prepackaged_rules/elastic_endpoint_security.json'; import { Rule } from '@kbn/security-solution-plugin/public/detection_engine/rule_management/logic/types'; import { kibanaPackageJson } from '@kbn/utils'; import { wrapErrorIfNeeded } from '@kbn/security-solution-plugin/common/endpoint/data_loaders/utils'; import { FtrService } from '../../../functional/ftr_provider_context'; import { EndpointRuleAlertGenerator } from './endpoint_rule_alert_generator'; import { getAlertsIndexMappings } from './alerts_security_index_mappings'; +import { ELASTIC_SECURITY_RULE_ID } from '../../../detection_engine_api_integration/utils/create_prebuilt_rule_saved_objects'; export interface IndexedEndpointRuleAlerts { alerts: estypes.WriteResponseBase[]; @@ -99,7 +99,7 @@ export class DetectionsTestService extends FtrService { return this.supertest .get(DETECTION_ENGINE_RULES_URL) .set('kbn-xsrf', 'true') - .query({ rule_id: endpointPrePackagedRule.rule_id }) + .query({ rule_id: ELASTIC_SECURITY_RULE_ID }) .send() .then(this.getHttpResponseFailureHandler()) .then((response) => response.body as Rule); diff --git a/x-pack/test/threat_intelligence_cypress/config.ts b/x-pack/test/threat_intelligence_cypress/config.ts index 3fc305ab35f74..1f67d5cf069ef 100644 --- a/x-pack/test/threat_intelligence_cypress/config.ts +++ b/x-pack/test/threat_intelligence_cypress/config.ts @@ -38,9 +38,6 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { '--csp.warnLegacyBrowsers=false', // define custom kibana server args here `--elasticsearch.ssl.certificateAuthorities=${CA_CERT_PATH}`, - // retrieve rules from the filesystem but not from fleet for Cypress tests - '--xpack.securitySolution.prebuiltRulesFromFileSystem=true', - '--xpack.securitySolution.prebuiltRulesFromSavedObjects=false', '--xpack.ruleRegistry.write.enabled=true', '--xpack.ruleRegistry.write.cache.enabled=false', '--xpack.ruleRegistry.unsafe.indexUpgrade.enabled=true', diff --git a/x-pack/test/upgrade/apps/dashboard/dashboard_smoke_tests.ts b/x-pack/test/upgrade/apps/dashboard/dashboard_smoke_tests.ts index 341d4af0e4c64..105620f90d6b7 100644 --- a/x-pack/test/upgrade/apps/dashboard/dashboard_smoke_tests.ts +++ b/x-pack/test/upgrade/apps/dashboard/dashboard_smoke_tests.ts @@ -51,6 +51,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { it('should render visualizations', async () => { await PageObjects.home.launchSampleDashboard('flights'); await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.timePicker.setCommonlyUsedTime('Last_1 year'); await renderable.waitForRender(); log.debug('Checking saved searches rendered'); await dashboardExpect.savedSearchRowCount(49); diff --git a/yarn.lock b/yarn.lock index a1e85461a3915..a18e013d8f967 100644 --- a/yarn.lock +++ b/yarn.lock @@ -113,21 +113,21 @@ semver "^5.4.1" source-map "^0.5.0" -"@babel/core@^7.1.0", "@babel/core@^7.11.6", "@babel/core@^7.12.10", "@babel/core@^7.12.3", "@babel/core@^7.20.2", "@babel/core@^7.7.2", "@babel/core@^7.7.5": - version "7.20.2" - resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.20.2.tgz#8dc9b1620a673f92d3624bd926dc49a52cf25b92" - integrity sha512-w7DbG8DtMrJcFOi4VrLm+8QM4az8Mo+PuLBKLp2zrYRCow8W/f9xiXm5sN53C8HksCyDQwCKha9JiDoIyPjT2g== +"@babel/core@^7.1.0", "@babel/core@^7.11.6", "@babel/core@^7.12.10", "@babel/core@^7.12.3", "@babel/core@^7.20.5", "@babel/core@^7.7.2", "@babel/core@^7.7.5": + version "7.20.5" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.20.5.tgz#45e2114dc6cd4ab167f81daf7820e8fa1250d113" + integrity sha512-UdOWmk4pNWTm/4DlPUl/Pt4Gz4rcEMb7CY0Y3eJl5Yz1vI8ZJGmHWaVE55LoxRjdpx0z259GE9U5STA9atUinQ== dependencies: "@ampproject/remapping" "^2.1.0" "@babel/code-frame" "^7.18.6" - "@babel/generator" "^7.20.2" + "@babel/generator" "^7.20.5" "@babel/helper-compilation-targets" "^7.20.0" "@babel/helper-module-transforms" "^7.20.2" - "@babel/helpers" "^7.20.1" - "@babel/parser" "^7.20.2" + "@babel/helpers" "^7.20.5" + "@babel/parser" "^7.20.5" "@babel/template" "^7.18.10" - "@babel/traverse" "^7.20.1" - "@babel/types" "^7.20.2" + "@babel/traverse" "^7.20.5" + "@babel/types" "^7.20.5" convert-source-map "^1.7.0" debug "^4.1.0" gensync "^1.0.0-beta.2" @@ -150,12 +150,12 @@ dependencies: eslint-rule-composer "^0.3.0" -"@babel/generator@^7.12.11", "@babel/generator@^7.12.5", "@babel/generator@^7.20.1", "@babel/generator@^7.20.2", "@babel/generator@^7.20.4", "@babel/generator@^7.7.2": - version "7.20.4" - resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.20.4.tgz#4d9f8f0c30be75fd90a0562099a26e5839602ab8" - integrity sha512-luCf7yk/cm7yab6CAW1aiFnmEfBJplb/JojV56MYEK7ziWfGmFlTfmL9Ehwfy4gFhbjBfWO1wj7/TuSbVNEEtA== +"@babel/generator@^7.12.11", "@babel/generator@^7.12.5", "@babel/generator@^7.20.5", "@babel/generator@^7.7.2": + version "7.20.5" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.20.5.tgz#cb25abee3178adf58d6814b68517c62bdbfdda95" + integrity sha512-jl7JY2Ykn9S0yj4DQP82sYvPU+T3g0HFcWTqDLqiuA9tGRNIj9VfbtXGAYTTkyNEnQk1jkMGOdYka8aG/lulCA== dependencies: - "@babel/types" "^7.20.2" + "@babel/types" "^7.20.5" "@jridgewell/gen-mapping" "^0.3.2" jsesc "^2.5.1" @@ -370,14 +370,14 @@ "@babel/traverse" "^7.18.9" "@babel/types" "^7.18.9" -"@babel/helpers@^7.12.5", "@babel/helpers@^7.20.1": - version "7.20.1" - resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.20.1.tgz#2ab7a0fcb0a03b5bf76629196ed63c2d7311f4c9" - integrity sha512-J77mUVaDTUJFZ5BpP6mMn6OIl3rEWymk2ZxDBQJUG3P+PbmyMcF3bYWvz0ma69Af1oobDqT/iAsvzhB58xhQUg== +"@babel/helpers@^7.12.5", "@babel/helpers@^7.20.5": + version "7.20.6" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.20.6.tgz#e64778046b70e04779dfbdf924e7ebb45992c763" + integrity sha512-Pf/OjgfgFRW5bApskEz5pvidpim7tEDPlFtKcNRXWmfHGn9IEI2W2flqRQXTFb7gIPTyK++N6rVHuwKut4XK6w== dependencies: "@babel/template" "^7.18.10" - "@babel/traverse" "^7.20.1" - "@babel/types" "^7.20.0" + "@babel/traverse" "^7.20.5" + "@babel/types" "^7.20.5" "@babel/highlight@^7.10.4", "@babel/highlight@^7.18.6": version "7.18.6" @@ -388,10 +388,10 @@ chalk "^2.0.0" js-tokens "^4.0.0" -"@babel/parser@^7.1.0", "@babel/parser@^7.10.3", "@babel/parser@^7.12.11", "@babel/parser@^7.12.7", "@babel/parser@^7.14.7", "@babel/parser@^7.18.10", "@babel/parser@^7.20.1", "@babel/parser@^7.20.2", "@babel/parser@^7.20.3": - version "7.20.3" - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.20.3.tgz#5358cf62e380cf69efcb87a7bb922ff88bfac6e2" - integrity sha512-OP/s5a94frIPXwjzEcv5S/tpQfc6XhxYUnmWpgdqMWGgYCuErA3SzozaRAMQgSZWKeTJxht9aWAkUY+0UzvOFg== +"@babel/parser@^7.1.0", "@babel/parser@^7.10.3", "@babel/parser@^7.12.11", "@babel/parser@^7.12.7", "@babel/parser@^7.14.7", "@babel/parser@^7.18.10", "@babel/parser@^7.20.5": + version "7.20.5" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.20.5.tgz#7f3c7335fe417665d929f34ae5dceae4c04015e8" + integrity sha512-r27t/cy/m9uKLXQNWWebeCUHgnAZq0CpG1OwKRxzJMP1vpSU4bSIK2hq+/cp0bQxetkXx38n09rNu8jVkcK/zA== "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@^7.18.6": version "7.18.6" @@ -1177,12 +1177,12 @@ core-js-pure "^3.0.0" regenerator-runtime "^0.13.4" -"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.10.2", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.0", "@babel/runtime@^7.12.1", "@babel/runtime@^7.12.13", "@babel/runtime@^7.12.5", "@babel/runtime@^7.15.4", "@babel/runtime@^7.17.8", "@babel/runtime@^7.18.3", "@babel/runtime@^7.20.1", "@babel/runtime@^7.3.1", "@babel/runtime@^7.4.4", "@babel/runtime@^7.4.5", "@babel/runtime@^7.5.0", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.3", "@babel/runtime@^7.7.2", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2": - version "7.20.1" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.20.1.tgz#1148bb33ab252b165a06698fde7576092a78b4a9" - integrity sha512-mrzLkl6U9YLF8qpqI7TB82PESyEGjm/0Ly91jG575eVxMMlb8fYfOXFZIJ8XfLrJZQbm7dlKry2bJmXBUEkdFg== +"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.10.2", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.0", "@babel/runtime@^7.12.1", "@babel/runtime@^7.12.13", "@babel/runtime@^7.12.5", "@babel/runtime@^7.15.4", "@babel/runtime@^7.17.8", "@babel/runtime@^7.18.3", "@babel/runtime@^7.20.6", "@babel/runtime@^7.3.1", "@babel/runtime@^7.4.4", "@babel/runtime@^7.4.5", "@babel/runtime@^7.5.0", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.3", "@babel/runtime@^7.7.2", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2": + version "7.20.6" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.20.6.tgz#facf4879bfed9b5326326273a64220f099b0fce3" + integrity sha512-Q+8MqP7TiHMWzSfwiJwXCjyf4GYA4Dgw3emg/7xmwsdLJOZUp+nMqcOwOzzYheuM1rhDu8FSj2l0aoMygEuXuA== dependencies: - regenerator-runtime "^0.13.10" + regenerator-runtime "^0.13.11" "@babel/template@^7.12.7", "@babel/template@^7.18.10", "@babel/template@^7.18.6", "@babel/template@^7.3.3": version "7.18.10" @@ -1193,26 +1193,26 @@ "@babel/parser" "^7.18.10" "@babel/types" "^7.18.10" -"@babel/traverse@^7.10.3", "@babel/traverse@^7.12.11", "@babel/traverse@^7.12.9", "@babel/traverse@^7.13.0", "@babel/traverse@^7.18.9", "@babel/traverse@^7.19.1", "@babel/traverse@^7.20.1", "@babel/traverse@^7.4.5", "@babel/traverse@^7.7.2": - version "7.20.1" - resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.20.1.tgz#9b15ccbf882f6d107eeeecf263fbcdd208777ec8" - integrity sha512-d3tN8fkVJwFLkHkBN479SOsw4DMZnz8cdbL/gvuDuzy3TS6Nfw80HuQqhw1pITbIruHyh7d1fMA47kWzmcUEGA== +"@babel/traverse@^7.10.3", "@babel/traverse@^7.12.11", "@babel/traverse@^7.12.9", "@babel/traverse@^7.13.0", "@babel/traverse@^7.18.9", "@babel/traverse@^7.19.1", "@babel/traverse@^7.20.1", "@babel/traverse@^7.20.5", "@babel/traverse@^7.4.5", "@babel/traverse@^7.7.2": + version "7.20.5" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.20.5.tgz#78eb244bea8270fdda1ef9af22a5d5e5b7e57133" + integrity sha512-WM5ZNN3JITQIq9tFZaw1ojLU3WgWdtkxnhM1AegMS+PvHjkM5IXjmYEGY7yukz5XS4sJyEf2VzWjI8uAavhxBQ== dependencies: "@babel/code-frame" "^7.18.6" - "@babel/generator" "^7.20.1" + "@babel/generator" "^7.20.5" "@babel/helper-environment-visitor" "^7.18.9" "@babel/helper-function-name" "^7.19.0" "@babel/helper-hoist-variables" "^7.18.6" "@babel/helper-split-export-declaration" "^7.18.6" - "@babel/parser" "^7.20.1" - "@babel/types" "^7.20.0" + "@babel/parser" "^7.20.5" + "@babel/types" "^7.20.5" debug "^4.1.0" globals "^11.1.0" -"@babel/types@^7.0.0", "@babel/types@^7.10.3", "@babel/types@^7.12.11", "@babel/types@^7.12.7", "@babel/types@^7.18.10", "@babel/types@^7.18.6", "@babel/types@^7.18.9", "@babel/types@^7.19.0", "@babel/types@^7.20.0", "@babel/types@^7.20.2", "@babel/types@^7.3.0", "@babel/types@^7.3.3", "@babel/types@^7.4.4": - version "7.20.2" - resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.20.2.tgz#67ac09266606190f496322dbaff360fdaa5e7842" - integrity sha512-FnnvsNWgZCr232sqtXggapvlkk/tuwR/qhGzcmxI0GXLCjmPYQPzio2FbdlWuY6y1sHFfQKk+rRbUZ9VStQMog== +"@babel/types@^7.0.0", "@babel/types@^7.10.3", "@babel/types@^7.12.11", "@babel/types@^7.12.7", "@babel/types@^7.18.10", "@babel/types@^7.18.6", "@babel/types@^7.18.9", "@babel/types@^7.19.0", "@babel/types@^7.20.2", "@babel/types@^7.20.5", "@babel/types@^7.3.0", "@babel/types@^7.3.3", "@babel/types@^7.4.4": + version "7.20.5" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.20.5.tgz#e206ae370b5393d94dfd1d04cd687cace53efa84" + integrity sha512-c9fst/h2/dcF7H+MJKZ2T0KjEQ8hY/BNnDk/H3XY8C4Aw/eWQXWn/lWntHF9ooUBnGmEvbfGrTgLWc+um0YDUg== dependencies: "@babel/helper-string-parser" "^7.19.4" "@babel/helper-validator-identifier" "^7.19.1" @@ -1452,10 +1452,10 @@ dependencies: object-hash "^1.3.0" -"@elastic/charts@50.2.1": - version "50.2.1" - resolved "https://registry.yarnpkg.com/@elastic/charts/-/charts-50.2.1.tgz#3dde69125fb181306129a48b73bce1f5823d0cad" - integrity sha512-LA9EcW1LGAIrlhro3P2Fgtj0BkHQV1ArHCEdJ43EjgaNpJsKaWnFSeYcbuXiUEyLtXbF4Sv6Di90qDppwThu6Q== +"@elastic/charts@51.1.1": + version "51.1.1" + resolved "https://registry.yarnpkg.com/@elastic/charts/-/charts-51.1.1.tgz#005c9fce7a2ef0345ac223d18115ad719c2c15e2" + integrity sha512-DJjhHBvovQ/EfAlF7cjAB1fhwjcrt4Iyy/Vxed0BAw6gqoppT7/3Kk8AmUzzj05BK7ZFHlaL/05fe3fUEWuQEw== dependencies: "@popperjs/core" "^2.4.0" bezier-easing "^2.1.0" @@ -1476,7 +1476,7 @@ resize-observer-polyfill "^1.5.1" ts-debounce "^4.0.0" utility-types "^3.10.0" - uuid "^8.3.2" + uuid "^9" "@elastic/datemath@5.0.3": version "5.0.3" @@ -1527,10 +1527,10 @@ resolved "https://registry.yarnpkg.com/@elastic/eslint-plugin-eui/-/eslint-plugin-eui-0.0.2.tgz#56b9ef03984a05cc213772ae3713ea8ef47b0314" integrity sha512-IoxURM5zraoQ7C8f+mJb9HYSENiZGgRVcG4tLQxE61yHNNRDXtGDWTZh8N1KIHcsqN1CEPETjuzBXkJYF/fDiQ== -"@elastic/eui@70.2.4": - version "70.2.4" - resolved "https://registry.yarnpkg.com/@elastic/eui/-/eui-70.2.4.tgz#341be8be182e2d96980de771d3fc0f2a43757e9f" - integrity sha512-rtjeJJCz7XGtuRP30kK6TZti6o5wmwA1BHQuStTH8mFmTIdUnfRgFaI1AJvbLwckgj4JuVkdfAYBUxqyC9V8UA== +"@elastic/eui@70.4.0": + version "70.4.0" + resolved "https://registry.yarnpkg.com/@elastic/eui/-/eui-70.4.0.tgz#0ce7520ac96e137f05861224a6cd0a029c4dc0bc" + integrity sha512-w/pMxC0drBtzy3RQzHBLLbKRgy4EUTSetej0eg7m87copRZOwWXqlrIt52uuUj9txenxmpSonnnvSB+1a7fCfg== dependencies: "@types/chroma-js" "^2.0.0" "@types/lodash" "^4.14.160" @@ -1551,7 +1551,7 @@ react-beautiful-dnd "^13.1.0" react-dropzone "^11.5.3" react-element-to-jsx-string "^14.3.4" - react-focus-on "^3.5.4" + react-focus-on "^3.7.0" react-input-autosize "^3.0.0" react-is "^17.0.2" react-virtualized-auto-sizer "^1.0.6" @@ -3785,6 +3785,10 @@ version "0.0.0" uid "" +"@kbn/rison@link:bazel-bin/packages/kbn-rison": + version "0.0.0" + uid "" + "@kbn/rule-data-utils@link:bazel-bin/packages/kbn-rule-data-utils": version "0.0.0" uid "" @@ -3901,11 +3905,11 @@ version "0.0.0" uid "" -"@kbn/shared-ux-file-image-mocks@link:bazel-bin/packages/shared-ux/file/image/mocks": +"@kbn/shared-ux-file-context@link:bazel-bin/packages/shared-ux/file/context": version "0.0.0" uid "" -"@kbn/shared-ux-file-image-types@link:bazel-bin/packages/shared-ux/file/image/types": +"@kbn/shared-ux-file-image-mocks@link:bazel-bin/packages/shared-ux/file/image/mocks": version "0.0.0" uid "" @@ -3913,6 +3917,22 @@ version "0.0.0" uid "" +"@kbn/shared-ux-file-mocks@link:bazel-bin/packages/shared-ux/file/mocks": + version "0.0.0" + uid "" + +"@kbn/shared-ux-file-picker@link:bazel-bin/packages/shared-ux/file/file_picker/impl": + version "0.0.0" + uid "" + +"@kbn/shared-ux-file-types@link:bazel-bin/packages/shared-ux/file/types": + version "0.0.0" + uid "" + +"@kbn/shared-ux-file-upload@link:bazel-bin/packages/shared-ux/file/file_upload/impl": + version "0.0.0" + uid "" + "@kbn/shared-ux-file-util@link:bazel-bin/packages/shared-ux/file/util": version "0.0.0" uid "" @@ -4017,6 +4037,10 @@ version "0.0.0" uid "" +"@kbn/shared-ux-prompt-not-found@link:bazel-bin/packages/shared-ux/prompt/not_found": + version "0.0.0" + uid "" + "@kbn/shared-ux-router-mocks@link:bazel-bin/packages/shared-ux/router/mocks": version "0.0.0" uid "" @@ -4162,6 +4186,36 @@ resolved "https://registry.yarnpkg.com/@leichtgewicht/ip-codec/-/ip-codec-2.0.4.tgz#b2ac626d6cb9c8718ab459166d4bb405b8ffa78b" integrity sha512-Hcv+nVC0kZnQ3tD9GVu5xSMR4VVYOteQIr/hwFPVEvPdlXqgGEuRjiheChHgdM+JyqdgNcmzZOX/tnl0JOiI7A== +"@lmdb/lmdb-darwin-arm64@2.6.9": + version "2.6.9" + resolved "https://registry.yarnpkg.com/@lmdb/lmdb-darwin-arm64/-/lmdb-darwin-arm64-2.6.9.tgz#4b84bb0ad71e78472332920c9cf8603ea3dad0bc" + integrity sha512-QxyheKfTP9k5ZVAiddCqGUtp2AD3/BMgYfski96iIbFH0skPFO+MYARMGZuemTgyM9uieT+5oKj4FjigWplpWg== + +"@lmdb/lmdb-darwin-x64@2.6.9": + version "2.6.9" + resolved "https://registry.yarnpkg.com/@lmdb/lmdb-darwin-x64/-/lmdb-darwin-x64-2.6.9.tgz#28b191a9f7a1f30462d8d179cd05598fa66ebbfc" + integrity sha512-zJ1oUepZMaqiujvWeWJRG5VHXBS3opJnjAzbd4vTVsQFT0t5rbPhHgAJ2ruR9rVrb2V1BINJZuwpjhIOg9fLCQ== + +"@lmdb/lmdb-linux-arm64@2.6.9": + version "2.6.9" + resolved "https://registry.yarnpkg.com/@lmdb/lmdb-linux-arm64/-/lmdb-linux-arm64-2.6.9.tgz#274dfe11209a70c059cb55c72026c24903dde3e1" + integrity sha512-KZRet8POwKowbYZqrRqdYJ+B6l+7cWG18vMCe2sgOSuE41sEMpfRQ1mKcolt3fsr0KVbuP63aPG+dwi0wGpX9w== + +"@lmdb/lmdb-linux-arm@2.6.9": + version "2.6.9" + resolved "https://registry.yarnpkg.com/@lmdb/lmdb-linux-arm/-/lmdb-linux-arm-2.6.9.tgz#7bacd104067e7dbb1bb67c907c1bc642e2d2ac96" + integrity sha512-Umw+ikxbsYZHHqr8eMycmApj6IIZCK4k1rp5/pqqx9FvAaPv4/Y63owiMLoKfipjel0YPaNyvSeXAJK3l/8Pbw== + +"@lmdb/lmdb-linux-x64@2.6.9": + version "2.6.9" + resolved "https://registry.yarnpkg.com/@lmdb/lmdb-linux-x64/-/lmdb-linux-x64-2.6.9.tgz#d37b25c9b553c5d5e66055a64d118e3fd42557d9" + integrity sha512-11xFQ4kCIPGnYULcfkW4SIMIY1sukA4DHez62DKvYn+tLr4AB1o9jm1Jk6bisKFh5Cql+JUr7klHxeIuxvGZdg== + +"@lmdb/lmdb-win32-x64@2.6.9": + version "2.6.9" + resolved "https://registry.yarnpkg.com/@lmdb/lmdb-win32-x64/-/lmdb-win32-x64-2.6.9.tgz#bf8e647dabd8b672744315f5df3e363b5987a463" + integrity sha512-qECZ+1j3PSarYeCmJlYlrxq3TB7S020ICrYmpxyQyphbRiMI9I1Bw4t+vPrMAEKsTqB8UaOzBp21YWUpsiCBfA== + "@loaders.gl/core@2.3.1": version "2.3.1" resolved "https://registry.yarnpkg.com/@loaders.gl/core/-/core-2.3.1.tgz#147037e17b014528dce00187aac0ec6ccb05938b" @@ -5981,33 +6035,33 @@ dependencies: defer-to-connect "^2.0.0" -"@tanstack/match-sorter-utils@8.1.1", "@tanstack/match-sorter-utils@^8.1.1": - version "8.1.1" - resolved "https://registry.yarnpkg.com/@tanstack/match-sorter-utils/-/match-sorter-utils-8.1.1.tgz#895f407813254a46082a6bbafad9b39b943dc834" - integrity sha512-IdmEekEYxQsoLOR0XQyw3jD1GujBpRRYaGJYQUw1eOT1eUugWxdc7jomh1VQ1EKHcdwDLpLaCz/8y4KraU4T9A== +"@tanstack/match-sorter-utils@^8.7.0": + version "8.7.0" + resolved "https://registry.yarnpkg.com/@tanstack/match-sorter-utils/-/match-sorter-utils-8.7.0.tgz#60b09a6d3d7974d5f86f1318053c1bd5a85fb0be" + integrity sha512-OgfIPMHTfuw9JGcXCCoEHWFP/eSP2eyhCYwkrFnWBM3NbUPAgOlFP11DbM7cozDRVB0XbPr1tD4pLAtWKlVUVg== dependencies: remove-accents "0.4.2" -"@tanstack/query-core@4.13.4": - version "4.13.4" - resolved "https://registry.yarnpkg.com/@tanstack/query-core/-/query-core-4.13.4.tgz#77043e066586359eca40859803acc4a44e2a2dc8" - integrity sha512-DMIy6tgGehYoRUFyoR186+pQspOicyZNSGvBWxPc2CinHjWOQ7DPnGr9zmn/kE9xK4Zd3GXd25Nj3X20+TF6Lw== +"@tanstack/query-core@4.19.1": + version "4.19.1" + resolved "https://registry.yarnpkg.com/@tanstack/query-core/-/query-core-4.19.1.tgz#2e92d9e8a50884eb231c5beb4386e131ebe34306" + integrity sha512-Zp0aIose5C8skBzqbVFGk9HJsPtUhRVDVNWIqVzFbGQQgYSeLZMd3Sdb4+EnA5wl1J7X+bre2PJGnQg9x/zHOA== -"@tanstack/react-query-devtools@^4.13.4": - version "4.13.4" - resolved "https://registry.yarnpkg.com/@tanstack/react-query-devtools/-/react-query-devtools-4.13.4.tgz#d631961fbb0803d2246cdf39dd2e35f443a88b6e" - integrity sha512-G0ZG+ZUk8ktJoi6Mzn4U7LnSOVbVFPyBJGB3dX4+SukkcKhWmErsYv2H1plRCL+V01Cg+dOg9RDfGYqsNbJszQ== +"@tanstack/react-query-devtools@^4.18.0": + version "4.19.1" + resolved "https://registry.yarnpkg.com/@tanstack/react-query-devtools/-/react-query-devtools-4.19.1.tgz#850058df8dba932362838c17f566bd717044449b" + integrity sha512-U63A+ly9JLPJj7ryR9omdXT3n+gS7jlExrHty4klsd/6xdUhC38CKZyZ0Gi3vctaVYRGTU8/vI+uKzBYdFqLaA== dependencies: - "@tanstack/match-sorter-utils" "^8.1.1" + "@tanstack/match-sorter-utils" "^8.7.0" superjson "^1.10.0" use-sync-external-store "^1.2.0" -"@tanstack/react-query@^4.13.4": - version "4.13.4" - resolved "https://registry.yarnpkg.com/@tanstack/react-query/-/react-query-4.13.4.tgz#6264e5513245a8cbec1195ba6ed9647d9230a520" - integrity sha512-OHkUulPorHDiWNcUrcSUNxedeZ28z9kCKRG3JY+aJ8dFH/o4fixtac4ys0lwCP/n/VL1XMPnu+/CXEhbXHyJZA== +"@tanstack/react-query@^4.18.0": + version "4.19.1" + resolved "https://registry.yarnpkg.com/@tanstack/react-query/-/react-query-4.19.1.tgz#43356dd537127e76d75f5a2769eb23dafd9a3690" + integrity sha512-5dvHvmc0vrWI03AJugzvKfirxCyCLe+qawrWFCXdu8t7dklIhJ7D5ZhgTypv7mMtIpdHPcECtCiT/+V74wCn2A== dependencies: - "@tanstack/query-core" "4.13.4" + "@tanstack/query-core" "4.19.1" use-sync-external-store "^1.2.0" "@testim/chrome-version@^1.1.3": @@ -7164,10 +7218,10 @@ "@types/node" "*" form-data "^2.3.3" -"@types/node-forge@^1.3.0": - version "1.3.0" - resolved "https://registry.yarnpkg.com/@types/node-forge/-/node-forge-1.3.0.tgz#c655e951e0fb5c4b53c9f4746c2128d4f93002fd" - integrity sha512-yUsIEHG3d81E2c+akGjZAMdVcjbfqMzpMjvpebnTO7pEGfqxCtBzpRV52kR1RETtwJ9fHkLdEjtaM+uMKWpwFA== +"@types/node-forge@^1.3.1": + version "1.3.1" + resolved "https://registry.yarnpkg.com/@types/node-forge/-/node-forge-1.3.1.tgz#49e44432c306970b4e900c3b214157c480af19fa" + integrity sha512-hvQ7Wav8I0j9amPXJtGqI/Yx70zeF62UKlAYq8JPm0nHzjKKzZvo9iR3YI2MiOghZRlOI+tQ2f6D+G6vVf4V2Q== dependencies: "@types/node" "*" @@ -8857,12 +8911,12 @@ argsplit@^1.0.5: resolved "https://registry.yarnpkg.com/argsplit/-/argsplit-1.0.5.tgz#9319a6ef63411716cfeb216c45ec1d13b35c5e99" integrity sha1-kxmm72NBFxbP6yFsRewdE7NcXpk= -aria-hidden@^1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/aria-hidden/-/aria-hidden-1.1.3.tgz#bb48de18dc84787a3c6eee113709c473c64ec254" - integrity sha512-RhVWFtKH5BiGMycI72q2RAFMLQi8JP9bLuQXgR5a8Znp7P5KOIADSJeyfI8PCVxLEp067B2HbP5JIiI/PXIZeA== +aria-hidden@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/aria-hidden/-/aria-hidden-1.2.2.tgz#8c4f7cc88d73ca42114106fdf6f47e68d31475b8" + integrity sha512-6y/ogyDTk/7YAe91T3E2PR1ALVKyM2QbTio5HwM+N1Q6CMlCKhvClyIjkckBswa0f2xJhjsfzIGa1yVSe1UMVA== dependencies: - tslib "^1.0.0" + tslib "^2.0.0" aria-query@^4.2.2: version "4.2.2" @@ -9241,7 +9295,7 @@ axobject-query@^2.2.0: resolved "https://registry.yarnpkg.com/axobject-query/-/axobject-query-2.2.0.tgz#943d47e10c0b704aa42275e20edf3722648989be" integrity sha512-Td525n+iPOOyUQIeBfcASuG6uJsDOITl7Mds5gFyerkWiX7qhUTdYUBlSgNMyVqtSJqwpt1kXGLdUt6SykLMRA== -babel-jest@^29.2.2, babel-jest@^29.3.1: +babel-jest@^29.3.1: version "29.3.1" resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-29.3.1.tgz#05c83e0d128cd48c453eea851482a38782249f44" integrity sha512-aard+xnMoxgjwV70t0L6wkW/3HQQtV+O0PEimxKgzNqCJnbYmroPojdP2tqKSOAt8QAKV/uSZU8851M7B5+fcA== @@ -12765,22 +12819,7 @@ ejs@^3.1.6, ejs@^3.1.8: dependencies: jake "^10.8.5" -elastic-apm-http-client@11.0.2, elastic-apm-http-client@^11.0.1: - version "11.0.2" - resolved "https://registry.yarnpkg.com/elastic-apm-http-client/-/elastic-apm-http-client-11.0.2.tgz#576521443d4f3c733b5220ae8175bf5538870cf5" - integrity sha512-Wiqwi4lnhjkILtP54wIbdY0X3Lv+x9JID42zYBI3g7BGRWUu4pPcTjJStWT/muMW57cdimHUektD3tOMFogprQ== - dependencies: - agentkeepalive "^4.2.1" - breadth-filter "^2.0.0" - end-of-stream "^1.4.4" - fast-safe-stringify "^2.0.7" - fast-stream-to-buffer "^1.0.0" - object-filter-sequence "^1.0.0" - readable-stream "^3.4.0" - semver "^6.3.0" - stream-chopper "^3.0.1" - -elastic-apm-http-client@11.0.3: +elastic-apm-http-client@11.0.3, elastic-apm-http-client@^11.0.1: version "11.0.3" resolved "https://registry.yarnpkg.com/elastic-apm-http-client/-/elastic-apm-http-client-11.0.3.tgz#1d357af449d66695ef10019c21efe6377ad8815e" integrity sha512-y+P9ByvfxjZbnLejgGaCAnwEe+FWMVshoMmjeLEEEVlQTLiFUHy7vhYyCQVqgbZzQ6zpaGPqPU2woKglKW4RHw== @@ -12795,45 +12834,7 @@ elastic-apm-http-client@11.0.3: semver "^6.3.0" stream-chopper "^3.0.1" -elastic-apm-node@^3.38.0: - version "3.40.0" - resolved "https://registry.yarnpkg.com/elastic-apm-node/-/elastic-apm-node-3.40.0.tgz#ed805ec817db7687ba9a77bcc0db6131e8cbc8cf" - integrity sha512-gs9Z7boZW2o3ZMVbdjoJKXv4F2AcfMh52DW1WxEE/FSFa6lymj6GmCEFywuP8SqdpRZbh6yohJoGOpl7sheNJg== - dependencies: - "@elastic/ecs-pino-format" "^1.2.0" - "@opentelemetry/api" "^1.1.0" - after-all-results "^2.0.0" - async-cache "^1.1.0" - async-value-promise "^1.1.1" - basic-auth "^2.0.1" - cookie "^0.5.0" - core-util-is "^1.0.2" - elastic-apm-http-client "11.0.2" - end-of-stream "^1.4.4" - error-callsites "^2.0.4" - error-stack-parser "^2.0.6" - escape-string-regexp "^4.0.0" - fast-safe-stringify "^2.0.7" - http-headers "^3.0.2" - is-native "^1.0.1" - lru-cache "^6.0.0" - measured-reporting "^1.51.1" - monitor-event-loop-delay "^1.0.0" - object-filter-sequence "^1.0.0" - object-identity-map "^1.0.2" - original-url "^1.2.3" - pino "^6.11.2" - relative-microtime "^2.0.0" - require-in-the-middle "^5.2.0" - semver "^6.3.0" - set-cookie-serde "^1.0.0" - shallow-clone-shim "^2.0.0" - source-map "^0.8.0-beta.0" - sql-summary "^1.0.1" - traverse "^0.6.6" - unicode-byte-truncate "^1.0.0" - -elastic-apm-node@^3.40.1: +elastic-apm-node@^3.38.0, elastic-apm-node@^3.40.1: version "3.40.1" resolved "https://registry.yarnpkg.com/elastic-apm-node/-/elastic-apm-node-3.40.1.tgz#ae3669d480fdacf62ace40d12a6f1a3c46b37940" integrity sha512-vdyEZ7BPKJP2a1PkCsg350XXGZj03bwOiGrZdqgflocYxns5QwFbhvMKaVq7hWWWS8/sACesrLLELyQgdOpFsw== @@ -18540,17 +18541,23 @@ listr@^0.14.1: p-map "^2.0.0" rxjs "^6.3.3" -lmdb-store@^1: - version "1.6.11" - resolved "https://registry.yarnpkg.com/lmdb-store/-/lmdb-store-1.6.11.tgz#801da597af8c7a01c81f87d5cc7a7497e381236d" - integrity sha512-hIvoGmHGsFhb2VRCmfhodA/837ULtJBwRHSHKIzhMB7WtPH6BRLPsvXp1MwD3avqGzuZfMyZDUp3tccLvr721Q== +lmdb@^2.6.9: + version "2.6.9" + resolved "https://registry.yarnpkg.com/lmdb/-/lmdb-2.6.9.tgz#aa782ec873bcf70333b251eede9e711819ef5765" + integrity sha512-rVA3OchNoKxoD2rYhtc9nooqbJmId+vvfPzTWhanRPhdVr0hbgnF9uB9ZEHFU2lEeYVdh83Pt2H6DudeWuz+JA== dependencies: - nan "^2.14.2" - node-gyp-build "^4.2.3" - ordered-binary "^1.0.0" - weak-lru-cache "^1.0.0" + msgpackr "1.7.2" + node-addon-api "^4.3.0" + node-gyp-build-optional-packages "5.0.3" + ordered-binary "^1.4.0" + weak-lru-cache "^1.2.2" optionalDependencies: - msgpackr "^1.4.7" + "@lmdb/lmdb-darwin-arm64" "2.6.9" + "@lmdb/lmdb-darwin-x64" "2.6.9" + "@lmdb/lmdb-linux-arm" "2.6.9" + "@lmdb/lmdb-linux-arm64" "2.6.9" + "@lmdb/lmdb-linux-x64" "2.6.9" + "@lmdb/lmdb-win32-x64" "2.6.9" load-json-file@^1.0.0: version "1.1.0" @@ -19896,7 +19903,7 @@ ms@2.1.3, ms@^2.0.0, ms@^2.1.1, ms@^2.1.3: resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== -msgpackr-extract@^2.2.0: +msgpackr-extract@^2.1.2: version "2.2.0" resolved "https://registry.yarnpkg.com/msgpackr-extract/-/msgpackr-extract-2.2.0.tgz#4bb749b58d9764cfdc0d91c7977a007b08e8f262" integrity sha512-0YcvWSv7ZOGl9Od6Y5iJ3XnPww8O7WLcpYMDwX+PAA/uXLDtyw94PJv9GLQV/nnp3cWlDhMoyKZIQLrx33sWog== @@ -19910,12 +19917,12 @@ msgpackr-extract@^2.2.0: "@msgpackr-extract/msgpackr-extract-linux-x64" "2.2.0" "@msgpackr-extract/msgpackr-extract-win32-x64" "2.2.0" -msgpackr@^1.4.7: - version "1.8.0" - resolved "https://registry.yarnpkg.com/msgpackr/-/msgpackr-1.8.0.tgz#6cf213e88f04c5a358c61085a42a4dbe5542de44" - integrity sha512-1Cos3r86XACdjLVY4CN8r72Cgs5lUzxSON6yb81sNZP9vC9nnBrEbu1/ldBhuR9BKejtoYV5C9UhmYUvZFJSNQ== +msgpackr@1.7.2: + version "1.7.2" + resolved "https://registry.yarnpkg.com/msgpackr/-/msgpackr-1.7.2.tgz#68d6debf5999d6b61abb6e7046a689991ebf7261" + integrity sha512-mWScyHTtG6TjivXX9vfIy2nBtRupaiAj0HQ2mtmpmYujAmqZmaaEVPaSZ1NKLMvicaMLFzEaMk0ManxMRg8rMQ== optionalDependencies: - msgpackr-extract "^2.2.0" + msgpackr-extract "^2.1.2" multicast-dns@^7.2.5: version "7.2.5" @@ -19953,7 +19960,7 @@ mute-stream@0.0.8: resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.8.tgz#1630c42b2251ff81e2a283de96a5497ea92e5e0d" integrity sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA== -nan@^2.13.2, nan@^2.14.2, nan@^2.15.0: +nan@^2.13.2, nan@^2.15.0: version "2.15.0" resolved "https://registry.yarnpkg.com/nan/-/nan-2.15.0.tgz#3f34a473ff18e15c1b5626b62903b5ad6e665fee" integrity sha512-8ZtvEnA2c5aYCZYd1cvgdnU6cqwixRoYg70xPLWUws5ORTa/lnw+u4amixRS/Ac5U5mQVgp9pnlSUnbNWFaWZQ== @@ -20123,6 +20130,11 @@ node-addon-api@^3.0.0: resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-3.2.1.tgz#81325e0a2117789c0128dab65e7e38f07ceba161" integrity sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A== +node-addon-api@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-4.3.0.tgz#52a1a0b475193e0928e98e0426a0d1254782b77f" + integrity sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ== + node-addon-api@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-5.0.0.tgz#7d7e6f9ef89043befdb20c1989c905ebde18c501" @@ -20171,7 +20183,7 @@ node-gyp-build-optional-packages@5.0.3: resolved "https://registry.yarnpkg.com/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.0.3.tgz#92a89d400352c44ad3975010368072b41ad66c17" integrity sha512-k75jcVzk5wnnc/FMxsf4udAoTEUv2jY3ycfdSd3yWu6Cnd1oee6/CfZJApyscA4FJOmdoixWwiwOyf16RzD5JA== -node-gyp-build@^4.2.2, node-gyp-build@^4.2.3: +node-gyp-build@^4.2.2: version "4.5.0" resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.5.0.tgz#7a64eefa0b21112f89f58379da128ac177f20e40" integrity sha512-2iGbaQBV+ITgCz76ZEjmhUKAKVf7xfY1sRl4UiKQspfZMH2h06SyhNsnSVy50cwkFQDGLyif6m/6uFXHkOZ6rg== @@ -20790,7 +20802,7 @@ ora@^5.4.1: strip-ansi "^6.0.0" wcwidth "^1.0.1" -ordered-binary@^1.0.0: +ordered-binary@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/ordered-binary/-/ordered-binary-1.4.0.tgz#6bb53d44925f3b8afc33d1eed0fa15693b211389" integrity sha512-EHQ/jk4/a9hLupIKxTfUsQRej1Yd/0QLQs3vGvIqg5ZtCYSzNhkzHoZc7Zf4e4kUlDaC3Uw8Q/1opOLNN2OKRQ== @@ -22595,10 +22607,10 @@ react-fast-compare@^3.0.1: resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-3.2.0.tgz#641a9da81b6a6320f270e89724fb45a0b39e43bb" integrity sha512-rtGImPZ0YyLrscKI9xTpV8psd6I8VAtjKCzQDlzyDvqJA8XOW78TXYQwNRNd8g8JZnDu8q9Fu/1v4HPAVwVdHA== -react-focus-lock@^2.9.0: - version "2.9.1" - resolved "https://registry.yarnpkg.com/react-focus-lock/-/react-focus-lock-2.9.1.tgz#094cfc19b4f334122c73bb0bff65d77a0c92dd16" - integrity sha512-pSWOQrUmiKLkffPO6BpMXN7SNKXMsuOakl652IBuALAu1esk+IcpJyM+ALcYzPTTFz1rD0R54aB9A4HuP5t1Wg== +react-focus-lock@^2.9.2: + version "2.9.2" + resolved "https://registry.yarnpkg.com/react-focus-lock/-/react-focus-lock-2.9.2.tgz#a57dfd7c493e5a030d87f161c96ffd082bd920f2" + integrity sha512-5JfrsOKyA5Zn3h958mk7bAcfphr24jPoMoznJ8vaJF6fUrPQ8zrtEd3ILLOK8P5jvGxdMd96OxWNjDzATfR2qw== dependencies: "@babel/runtime" "^7.0.0" focus-lock "^0.11.2" @@ -22607,14 +22619,14 @@ react-focus-lock@^2.9.0: use-callback-ref "^1.3.0" use-sidecar "^1.1.2" -react-focus-on@^3.5.4, react-focus-on@^3.6.0: - version "3.6.0" - resolved "https://registry.yarnpkg.com/react-focus-on/-/react-focus-on-3.6.0.tgz#159e13082dad4ea1f07abe11254f0e981d5a7b79" - integrity sha512-onIRjpd9trAUenXNdDcvjc8KJUSklty4X/Gr7hAm/MzM7ekSF2pg9D8KBKL7ipige22IAPxLRRf/EmJji9KD6Q== +react-focus-on@^3.7.0: + version "3.7.0" + resolved "https://registry.yarnpkg.com/react-focus-on/-/react-focus-on-3.7.0.tgz#bf782b51483d52d1d336b7b09cb864897af26cdf" + integrity sha512-TsCnbJr4qjqFatJ4U1N8qGSZH+FUzxJ5mJ5ta7TY2YnDmUbGGmcvZMTZgGjQ1fl6vlztsMyg6YyZlPAeeIhEUg== dependencies: - aria-hidden "^1.1.3" - react-focus-lock "^2.9.0" - react-remove-scroll "^2.5.2" + aria-hidden "^1.2.2" + react-focus-lock "^2.9.2" + react-remove-scroll "^2.5.5" react-style-singleton "^2.2.0" tslib "^2.3.1" use-callback-ref "^1.3.0" @@ -22631,10 +22643,10 @@ react-grid-layout@^1.3.4: react-draggable "^4.0.0" react-resizable "^3.0.4" -react-hook-form@^7.39.1: - version "7.39.1" - resolved "https://registry.yarnpkg.com/react-hook-form/-/react-hook-form-7.39.1.tgz#ded87d4b3f6692d1f9219515f78ca282b6e1ebf7" - integrity sha512-MiF9PCILN5KulhSGbnjohMiTOrB47GerDTichMNP0y2cPUu1GTRFqbunOxCE9N1499YTLMV/ne4gFzqCp1rxrQ== +react-hook-form@^7.39.7: + version "7.40.0" + resolved "https://registry.yarnpkg.com/react-hook-form/-/react-hook-form-7.40.0.tgz#62bc939dddca88522cd7f5135b6603192ccf7e17" + integrity sha512-0rokdxMPJs0k9bvFtY6dbcSydyNhnZNXCR49jgDr/aR03FDHFOK6gfh8ccqB3fl696Mk7lqh04xdm+agqWXKSw== react-input-autosize@^3.0.0: version "3.0.0" @@ -22769,7 +22781,7 @@ react-remove-scroll-bar@^2.3.3: react-style-singleton "^2.2.1" tslib "^2.0.0" -react-remove-scroll@^2.5.2: +react-remove-scroll@^2.5.5: version "2.5.5" resolved "https://registry.yarnpkg.com/react-remove-scroll/-/react-remove-scroll-2.5.5.tgz#1e31a1260df08887a8a0e46d09271b52b3a37e77" integrity sha512-ImKhrzJJsyXJfBZ4bzu8Bwpka14c/fQt0k+cyFp/PBhTfyDnU5hjOtM4AG/0AMyy8oKzOTR0lDgJIM7pYXI0kw== @@ -23329,10 +23341,10 @@ regenerator-runtime@^0.11.0: resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz#be05ad7f9bf7d22e056f9726cee5017fbf19e2e9" integrity sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg== -regenerator-runtime@^0.13.10, regenerator-runtime@^0.13.4, regenerator-runtime@^0.13.7: - version "0.13.10" - resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.10.tgz#ed07b19616bcbec5da6274ebc75ae95634bfc2ee" - integrity sha512-KepLsg4dU12hryUO7bp/axHAKvwGOCV0sGloQtpagJ12ai+ojVDqkeGSiRX1zlq+kjIMZ1t7gpze+26QqtdGqw== +regenerator-runtime@^0.13.11, regenerator-runtime@^0.13.4, regenerator-runtime@^0.13.7: + version "0.13.11" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz#f6dca3e7ceec20590d07ada785636a90cdca17f9" + integrity sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg== regenerator-transform@^0.15.0: version "0.15.0" @@ -26264,7 +26276,7 @@ tslib@2.3.1: resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.3.1.tgz#e8a335add5ceae51aa261d32a490158ef042ef01" integrity sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw== -tslib@^1.0.0, tslib@^1.10.0, tslib@^1.8.1, tslib@^1.9.0, tslib@^1.9.3: +tslib@^1.10.0, tslib@^1.8.1, tslib@^1.9.0, tslib@^1.9.3: version "1.13.0" resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.13.0.tgz#c881e13cc7015894ed914862d276436fa9a47043" integrity sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q== @@ -26946,6 +26958,11 @@ uuid@^8.3.0, uuid@^8.3.2: resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== +uuid@^9: + version "9.0.0" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-9.0.0.tgz#592f550650024a38ceb0c562f2f6aa435761efb5" + integrity sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg== + v8-compile-cache-lib@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz#6336e8d71965cb3d35a1bbb7868445a7c05264bf" @@ -27538,7 +27555,7 @@ wcwidth@^1.0.1: dependencies: defaults "^1.0.3" -weak-lru-cache@^1.0.0: +weak-lru-cache@^1.2.2: version "1.2.2" resolved "https://registry.yarnpkg.com/weak-lru-cache/-/weak-lru-cache-1.2.2.tgz#fdbb6741f36bae9540d12f480ce8254060dccd19" integrity sha512-DEAoo25RfSYMuTGc9vPJzZcZullwIqRDSI9LOy+fkCJPi6hykCnfKaXTuPBDuXAUcqHXyOgFtHNp/kB2FjYHbw==