diff --git a/.buildkite/pipeline-resource-definitions/kibana-chrome-forward-testing.yml b/.buildkite/pipeline-resource-definitions/kibana-chrome-forward-testing.yml index 3df81d900f8cc..beeb6152509b6 100644 --- a/.buildkite/pipeline-resource-definitions/kibana-chrome-forward-testing.yml +++ b/.buildkite/pipeline-resource-definitions/kibana-chrome-forward-testing.yml @@ -19,10 +19,6 @@ spec: description: 'Testing Kibana against upcoming versions of Chrome' spec: env: - # This is what will switch the FTRs pipeline to use Chrome Beta - USE_CHROME_BETA: 'true' - # Unit-tests don't depend on Chrome's versions, integration tests , so we don't need to run those - LIMIT_CONFIG_TYPE: 'functional' SLACK_NOTIFICATIONS_CHANNEL: '#kibana-operations-alerts' ELASTIC_SLACK_NOTIFICATIONS_ENABLED: 'true' @@ -30,7 +26,7 @@ spec: branch_configuration: main default_branch: main repository: elastic/kibana - pipeline_file: .buildkite/scripts/pipelines/pull_request/pipeline.sh + pipeline_file: .buildkite/pipelines/chrome_forward_testing.yml skip_intermediate_builds: true provider_settings: prefix_pull_request_fork_branch_names: false diff --git a/.buildkite/pipelines/chrome_forward_testing.yml b/.buildkite/pipelines/chrome_forward_testing.yml new file mode 100644 index 0000000000000..76069f6ad8070 --- /dev/null +++ b/.buildkite/pipelines/chrome_forward_testing.yml @@ -0,0 +1,366 @@ +env: + GITHUB_COMMIT_STATUS_ENABLED: 'false' + # This is what will switch the FTRs/cypress pipeline to use Chrome Beta + USE_CHROME_BETA: 'true' +agents: + image: family/kibana-ubuntu-2004 + imageProject: elastic-images-prod + provider: gcp + +steps: + - command: .buildkite/scripts/lifecycle/pre_build.sh + label: Pre-Build + timeout_in_minutes: 10 + agents: + machineType: n2-standard-2 + retry: + automatic: + - exit_status: '*' + limit: 1 + - wait + + - command: .buildkite/scripts/steps/build_kibana.sh + label: Build Kibana Distribution and Plugins + agents: + machineType: n2-standard-16 + preemptible: true + key: build + timeout_in_minutes: 60 + retry: + automatic: + - exit_status: '*' + limit: 1 + + - wait + + - command: .buildkite/scripts/steps/ci_stats_ready.sh + label: Mark CI Stats as ready + agents: + machineType: n2-standard-2 + timeout_in_minutes: 10 + depends_on: + - build + retry: + automatic: + - exit_status: '*' + limit: 1 + + - command: .buildkite/scripts/steps/test/pick_test_group_run_order.sh + label: 'Pick Test Group Run Order' + agents: + machineType: n2-standard-2 + timeout_in_minutes: 10 + env: + # Unit-tests don't depend on Chrome's versions, integration tests , so we don't need to run those + LIMIT_CONFIG_TYPE: 'functional' + JEST_UNIT_SCRIPT: '.buildkite/scripts/steps/test/jest.sh' + JEST_INTEGRATION_SCRIPT: '.buildkite/scripts/steps/test/jest_integration.sh' + FTR_CONFIGS_SCRIPT: '.buildkite/scripts/steps/test/ftr_configs.sh' + retry: + automatic: + - exit_status: '*' + limit: 1 + + - command: .buildkite/scripts/steps/functional/security_serverless_entity_analytics.sh + label: 'Serverless Entity Analytics - Security Cypress Tests' + agents: + machineType: n2-standard-4 + preemptible: true + depends_on: + - build + timeout_in_minutes: 60 + parallelism: 3 + retry: + automatic: + - exit_status: '-1' + limit: 1 + + - command: .buildkite/scripts/steps/functional/security_serverless_explore.sh + label: 'Serverless Explore - Security Solution Cypress Tests' + agents: + machineType: n2-standard-4 + preemptible: true + depends_on: + - build + timeout_in_minutes: 60 + parallelism: 2 + retry: + automatic: + - exit_status: '-1' + limit: 1 + + - command: .buildkite/scripts/steps/functional/security_serverless_investigations.sh + label: 'Serverless Investigations - Security Solution Cypress Tests' + agents: + machineType: n2-standard-4 + preemptible: true + depends_on: + - build + timeout_in_minutes: 60 + parallelism: 8 + retry: + automatic: + - exit_status: '-1' + limit: 1 + + - command: .buildkite/scripts/steps/functional/security_serverless_rule_management.sh + label: 'Serverless Rule Management - Security Solution Cypress Tests' + agents: + machineType: n2-standard-4 + preemptible: true + depends_on: + - build + timeout_in_minutes: 60 + parallelism: 5 + retry: + automatic: + - exit_status: '-1' + limit: 1 + + - command: .buildkite/scripts/steps/functional/security_serverless_rule_management_prebuilt_rules.sh + label: 'Serverless Rule Management - Prebuilt Rules - Security Solution Cypress Tests' + agents: + machineType: n2-standard-4 + preemptible: true + depends_on: + - build + timeout_in_minutes: 60 + parallelism: 1 + retry: + automatic: + - exit_status: '-1' + limit: 1 + + - command: .buildkite/scripts/steps/functional/security_solution_rule_management.sh + label: 'Rule Management - Security Solution Cypress Tests' + agents: + machineType: n2-standard-4 + preemptible: true + depends_on: + - build + timeout_in_minutes: 60 + parallelism: 4 + retry: + automatic: + - exit_status: '-1' + limit: 1 + + - command: .buildkite/scripts/steps/functional/security_solution_rule_management_prebuilt_rules.sh + label: 'Rule Management - Prebuilt Rules - Security Solution Cypress Tests' + agents: + machineType: n2-standard-4 + preemptible: true + depends_on: + - build + timeout_in_minutes: 60 + parallelism: 6 + retry: + automatic: + - exit_status: '-1' + limit: 1 + + - command: .buildkite/scripts/steps/functional/security_serverless_detection_engine.sh + label: 'Serverless Detection Engine - Security Solution Cypress Tests' + agents: + machineType: n2-standard-4 + preemptible: true + depends_on: + - build + timeout_in_minutes: 60 + parallelism: 5 + retry: + automatic: + - exit_status: '-1' + limit: 1 + + - command: .buildkite/scripts/steps/functional/security_serverless_detection_engine_exceptions.sh + label: 'Serverless Detection Engine - Exceptions - Security Solution Cypress Tests' + agents: + machineType: n2-standard-4 + preemptible: true + depends_on: + - build + timeout_in_minutes: 60 + parallelism: 6 + retry: + automatic: + - exit_status: '-1' + limit: 1 + + - command: .buildkite/scripts/steps/functional/security_solution_detection_engine.sh + label: 'Detection Engine - Security Solution Cypress Tests' + agents: + machineType: n2-standard-4 + preemptible: true + depends_on: + - build + timeout_in_minutes: 60 + parallelism: 5 + retry: + automatic: + - exit_status: '-1' + limit: 1 + + - command: .buildkite/scripts/steps/functional/security_solution_detection_engine_exceptions.sh + label: 'Detection Engine - Exceptions - Security Solution Cypress Tests' + agents: + machineType: n2-standard-4 + preemptible: true + depends_on: + - build + timeout_in_minutes: 60 + parallelism: 6 + retry: + automatic: + - exit_status: '-1' + limit: 1 + + - command: .buildkite/scripts/steps/functional/security_serverless_ai_assistant.sh + label: 'Serverless AI Assistant - Security Solution Cypress Tests' + agents: + machineType: n2-standard-4 + preemptible: true + depends_on: + - build + timeout_in_minutes: 60 + parallelism: 1 + retry: + automatic: + - exit_status: '-1' + limit: 1 + + - command: .buildkite/scripts/steps/functional/security_solution_ai_assistant.sh + label: 'AI Assistant - Security Solution Cypress Tests' + agents: + machineType: n2-standard-4 + preemptible: true + depends_on: + - build + timeout_in_minutes: 60 + parallelism: 1 + retry: + automatic: + - exit_status: '-1' + limit: 1 + + - command: .buildkite/scripts/steps/functional/security_solution_entity_analytics.sh + label: 'Entity Analytics - Security Solution Cypress Tests' + agents: + machineType: n2-standard-4 + preemptible: true + depends_on: + - build + timeout_in_minutes: 60 + parallelism: 2 + retry: + automatic: + - exit_status: '-1' + limit: 1 + + - command: .buildkite/scripts/steps/functional/security_solution_explore.sh + label: 'Explore - Security Solution Cypress Tests' + agents: + machineType: n2-standard-4 + preemptible: true + depends_on: + - build + timeout_in_minutes: 60 + parallelism: 2 + retry: + automatic: + - exit_status: '-1' + limit: 1 + + - command: .buildkite/scripts/steps/functional/security_solution_investigations.sh + label: 'Investigations - Security Solution Cypress Tests' + agents: + machineType: n2-standard-4 + preemptible: true + depends_on: + - build + timeout_in_minutes: 60 + parallelism: 6 + retry: + automatic: + - exit_status: '-1' + limit: 1 + + - command: .buildkite/scripts/steps/functional/osquery_cypress.sh + label: 'Osquery Cypress Tests' + agents: + machineType: n2-standard-4 + preemptible: true + depends_on: + - build + timeout_in_minutes: 60 + parallelism: 8 + retry: + automatic: + - exit_status: '-1' + limit: 1 + + - command: .buildkite/scripts/steps/functional/security_serverless_osquery.sh + label: 'Serverless Osquery Cypress Tests' + agents: + machineType: n2-standard-4 + preemptible: true + depends_on: + - build + timeout_in_minutes: 60 + parallelism: 8 + retry: + automatic: + - exit_status: '-1' + limit: 1 + + - command: .buildkite/scripts/steps/functional/defend_workflows.sh + label: 'Defend Workflows Cypress Tests' + agents: + enableNestedVirtualization: true + localSsds: 1 + localSsdInterface: nvme + machineType: n2-standard-4 + depends_on: + - build + timeout_in_minutes: 60 + parallelism: 20 + retry: + automatic: + - exit_status: '-1' + limit: 1 + + - command: .buildkite/scripts/steps/functional/defend_workflows_serverless.sh + label: 'Defend Workflows Cypress Tests on Serverless' + agents: + enableNestedVirtualization: true + localSsds: 1 + localSsdInterface: nvme + machineType: n2-standard-4 + depends_on: + - build + timeout_in_minutes: 60 + parallelism: 14 + retry: + automatic: + - exit_status: '-1' + limit: 1 + + - command: .buildkite/scripts/steps/checks.sh + label: 'Checks' + agents: + machineType: n2-standard-2 + preemptible: true + timeout_in_minutes: 60 + retry: + automatic: + - exit_status: '-1' + limit: 3 + + - wait: ~ + continue_on_failure: true + + - command: .buildkite/scripts/lifecycle/post_build.sh + label: Post-Build + timeout_in_minutes: 10 + agents: + machineType: n2-standard-2 diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 12eef80dea380..d4c8f52704e96 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1014,6 +1014,10 @@ packages/kbn-zod-helpers @elastic/security-detection-rule-management # used for the 'team' designator within Kibana Stats # Data Discovery +/x-pack/test_serverless/functional/es_archives/pre_calculated_histogram @elastic/kibana-data-discovery +/x-pack/test_serverless/functional/es_archives/kibana_sample_data_flights_index_pattern @elastic/kibana-data-discovery +/x-pack/test_serverless/functional/test_suites/security/config.examples.ts @elastic/kibana-data-discovery +/x-pack/test_serverless/functional/test_suites/security/config.context_awareness.ts @elastic/kibana-data-discovery /test/accessibility/apps/discover.ts @elastic/kibana-data-discovery /test/api_integration/apis/data_views @elastic/kibana-data-discovery /test/api_integration/apis/data_view_field_editor @elastic/kibana-data-discovery @@ -1062,6 +1066,10 @@ packages/kbn-zod-helpers @elastic/security-detection-rule-management /x-pack/test_serverless/functional/test_suites/common/management/data_views @elastic/kibana-data-discovery src/plugins/discover/public/context_awareness/profile_providers/security @elastic/kibana-data-discovery @elastic/security-threat-hunting-investigations +# Platform Docs +/x-pack/test_serverless/functional/test_suites/security/screenshot_creation/index.ts @elastic/platform-docs +/x-pack/test_serverless/functional/test_suites/security/config.screenshots.ts @elastic/platform-docs + # Visualizations /src/plugins/visualize/ @elastic/kibana-visualizations /x-pack/test/functional/apps/lens @elastic/kibana-visualizations @@ -1146,7 +1154,10 @@ x-pack/test_serverless/**/test_suites/observability/ai_assistant @elastic/obs-ai /x-pack/plugins/observability_solution/infra/server/services @elastic/obs-ux-infra_services-team /x-pack/plugins/observability_solution/infra/server/usage @elastic/obs-ux-infra_services-team /x-pack/plugins/observability_solution/infra/server/utils @elastic/obs-ux-infra_services-team + ## Logs UI code exceptions -> @elastic/obs-ux-logs-team +/x-pack/test_serverless/functional/page_objects/svl_oblt_onboarding_stream_log_file.ts @elastic/obs-ux-logs-team +/x-pack/test_serverless/functional/page_objects/svl_oblt_onboarding_page.ts @elastic/obs-ux-logs-team /x-pack/plugins/observability_solution/infra/common/http_api/log_alerts @elastic/obs-ux-logs-team /x-pack/plugins/observability_solution/infra/common/http_api/log_analysis @elastic/obs-ux-logs-team /x-pack/plugins/observability_solution/infra/common/log_analysis @elastic/obs-ux-logs-team @@ -1219,6 +1230,7 @@ x-pack/test_serverless/**/test_suites/observability/ai_assistant @elastic/obs-ai /x-pack/test_serverless/api_integration/test_suites/observability/synthetics @elastic/obs-ux-management-team # Logs +/x-pack/test_serverless/api_integration/test_suites/observability/index.feature_flags.ts @elastic/obs-ux-logs-team /x-pack/test/api_integration/apis/logs_ui @elastic/obs-ux-logs-team /x-pack/test/dataset_quality_api_integration @elastic/obs-ux-logs-team /x-pack/test_serverless/api_integration/test_suites/observability/dataset_quality_api_integration @elastic/obs-ux-logs-team @@ -1309,6 +1321,13 @@ x-pack/test_serverless/**/test_suites/observability/ai_assistant @elastic/obs-ai /.devcontainer/ @elastic/kibana-operations # Appex QA +/x-pack/test_serverless/tsconfig.json @elastic/appex-qa +/x-pack/test_serverless/kibana.jsonc @elastic/appex-qa +/x-pack/test_serverless/functional/test_suites/common/README.md @elastic/appex-qa +/x-pack/test_serverless/functional/page_objects/index.ts @elastic/appex-qa +/x-pack/test_serverless/functional/ftr_provider_context.d.ts @elastic/appex-qa +/x-pack/test_serverless/functional/test_suites/common/management/index.ts @elastic/appex-qa +/x-pack/test_serverless/functional/test_suites/common/examples/index.ts @elastic/appex-qa /x-pack/test_serverless/functional/page_objects/svl_common_page.ts @elastic/appex-qa /x-pack/test_serverless/README.md @elastic/appex-qa /x-pack/test_serverless/api_integration/ftr_provider_context.d.ts @elastic/appex-qa @@ -1335,6 +1354,7 @@ x-pack/test/api_integration/deployment_agnostic/services/ @elastic/appex-qa x-pack/test/**/deployment_agnostic/ @elastic/appex-qa #temporarily to monitor tests migration # Core +/x-pack/test_serverless/functional/test_suites/security/config.saved_objects_management.ts @elastic/kibana-core /config/ @elastic/kibana-core /config/serverless.yml @elastic/kibana-core @elastic/kibana-security /config/serverless.es.yml @elastic/kibana-core @elastic/kibana-security @@ -1411,7 +1431,11 @@ x-pack/plugins/cloud_integrations/cloud_full_story/server/config.ts @elastic/kib #CC# /x-pack/plugins/security/ @elastic/kibana-security # Response Ops team -/x-pack/test_serverless/functional/test_suites/security/screenshot_creation/index.ts @elastic/response-ops +/x-pack/test_serverless/api_integration/test_suites/observability/config.ts @elastic/response-ops +/x-pack/test_serverless/api_integration/test_suites/observability/index.ts @elastic/response-ops +/x-pack/test_serverless/functional/page_objects/svl_triggers_actions_ui_page.ts @elastic/response-ops +/x-pack/test_serverless/functional/page_objects/svl_rule_details_ui_page.ts @elastic/response-ops +/x-pack/test_serverless/functional/page_objects/svl_oblt_overview_page.ts @elastic/response-ops /x-pack/test/alerting_api_integration/ @elastic/response-ops /x-pack/test/alerting_api_integration/observability @elastic/obs-ux-management-team /x-pack/test/plugin_api_integration/test_suites/task_manager/ @elastic/response-ops @@ -1454,6 +1478,8 @@ x-pack/plugins/cloud_integrations/cloud_full_story/server/config.ts @elastic/kib /x-pack/test_serverless/functional/test_suites/search/ @elastic/search-kibana x-pack/test/api_integration/apis/management/index_management/inference_endpoints.ts @elastic/search-kibana /x-pack/test_serverless/api_integration/test_suites/search @elastic/search-kibana +/x-pack/test_serverless/functional/page_objects/svl_api_keys.ts @elastic/search-kibana +/x-pack/test_serverless/functional/page_objects/svl_search_* @elastic/search-kibana # Management Experience - Deployment Management /x-pack/test_serverless/**/test_suites/common/index_management/ @elastic/kibana-management @@ -1474,6 +1500,10 @@ x-pack/test/api_integration/apis/management/index_management/inference_endpoints #CC# /x-pack/plugins/cross_cluster_replication/ @elastic/kibana-management # Security Solution +/x-pack/test_serverless/functional/test_suites/security/config.ts @elastic/security-solution +/x-pack/test_serverless/functional/test_suites/security/config.feature_flags.ts @elastic/security-solution +/x-pack/test_serverless/api_integration/test_suites/observability/config.feature_flags.ts @elastic/security-solution +/x-pack/test_serverless/functional/test_suites/common/spaces/multiple_spaces_enabled.ts @elastic/security-solution /x-pack/test/functional/es_archives/endpoint/ @elastic/security-solution /x-pack/test/plugin_functional/test_suites/resolver/ @elastic/security-solution /x-pack/test/security_solution_api_integration @elastic/security-solution @@ -1783,6 +1813,7 @@ x-pack/plugins/osquery @elastic/security-defend-workflows /x-pack/plugins/security_solution/public/cloud_defend @elastic/kibana-cloud-security-posture # Cloud Security Posture +/x-pack/test_serverless/functional/test_suites/security/config.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 @@ -1842,9 +1873,12 @@ x-pack/test/profiling_api_integration @elastic/obs-ux-infra_services-team x-pack/plugins/observability_solution/observability_shared/public/components/profiling @elastic/obs-ux-infra_services-team # Shared UX +/x-pack/test_serverless/functional/test_suites/common/spaces/spaces_selection.ts @elastic/appex-sharedux +/x-pack/test_serverless/functional/test_suites/common/spaces/index.ts @elastic/appex-sharedux packages/react @elastic/appex-sharedux test/functional/page_objects/solution_navigation.ts @elastic/appex-sharedux /x-pack/test_serverless/functional/page_objects/svl_common_navigation.ts @elastic/appex-sharedux +/x-pack/test_serverless/functional/fixtures/kbn_archiver/reporting @elastic/appex-sharedux # OpenAPI spec files oas_docs/.spectral.yaml @elastic/platform-docs diff --git a/docs/search/playground/index.asciidoc b/docs/search/playground/index.asciidoc index e810767e57546..72d8eccab47c1 100644 --- a/docs/search/playground/index.asciidoc +++ b/docs/search/playground/index.asciidoc @@ -136,7 +136,7 @@ _You can skip this step if you already have data in one or more {es} indices._ There are many options for ingesting data into {es}, including: * The {enterprise-search-ref}/crawler.html[Elastic crawler] for web content (*NOTE*: Not yet available in _Serverless_) -* {enterprise-search-ref}/connectors.html[Elastic connectors] for data synced from third-party sources +* {ref}/es-connectors.html[Elastic connectors] for data synced from third-party sources * The {es} {ref}/docs-bulk.html[Bulk API] for JSON documents + .*Expand* for example diff --git a/docs/setup/connect-to-elasticsearch.asciidoc b/docs/setup/connect-to-elasticsearch.asciidoc index 3104e8f98ac42..16e8b8a22053c 100644 --- a/docs/setup/connect-to-elasticsearch.asciidoc +++ b/docs/setup/connect-to-elasticsearch.asciidoc @@ -26,7 +26,7 @@ A good place to start is with one of our Elastic solutions, which offer experiences for common use cases. * *Elastic connectors and crawler.* -** Create searchable mirrors of your data in Sharepoint Online, S3, Google Drive, and many other web services using our open code {enterprise-search-ref}/connectors.html[Elastic connectors]. +** Create searchable mirrors of your data in Sharepoint Online, S3, Google Drive, and many other web services using our open code {ref}/es-connectors.html[Elastic connectors]. ** Discover, extract, and index your web content into {es} using the {enterprise-search-ref}/crawler.html[Elastic web crawler]. diff --git a/packages/kbn-check-mappings-update-cli/current_fields.json b/packages/kbn-check-mappings-update-cli/current_fields.json index 87bf8c7104871..79c6216decb5a 100644 --- a/packages/kbn-check-mappings-update-cli/current_fields.json +++ b/packages/kbn-check-mappings-update-cli/current_fields.json @@ -861,6 +861,7 @@ "alertSampleSizePerShard", "dataViewId", "enabled", + "excludeAlertStatuses", "filter", "identifierType", "interval", diff --git a/packages/kbn-check-mappings-update-cli/current_mappings.json b/packages/kbn-check-mappings-update-cli/current_mappings.json index 7b315efcbcd4f..bfe5816a2f1b2 100644 --- a/packages/kbn-check-mappings-update-cli/current_mappings.json +++ b/packages/kbn-check-mappings-update-cli/current_mappings.json @@ -2847,6 +2847,9 @@ "enabled": { "type": "boolean" }, + "excludeAlertStatuses": { + "type": "keyword" + }, "filter": { "dynamic": false, "properties": {} diff --git a/packages/kbn-doc-links/src/get_doc_links.ts b/packages/kbn-doc-links/src/get_doc_links.ts index 8e0dc0d3e108f..62a6304945dab 100644 --- a/packages/kbn-doc-links/src/get_doc_links.ts +++ b/packages/kbn-doc-links/src/get_doc_links.ts @@ -157,7 +157,7 @@ export const getDocLinks = ({ kibanaBranch, buildFlavor }: GetDocLinkOptions): D behavioralAnalytics: `${ELASTICSEARCH_DOCS}behavioral-analytics-overview.html`, behavioralAnalyticsCORS: `${ELASTICSEARCH_DOCS}behavioral-analytics-cors.html`, behavioralAnalyticsEvents: `${ELASTICSEARCH_DOCS}behavioral-analytics-event.html`, - buildConnector: `${ENTERPRISE_SEARCH_DOCS}build-connector.html`, + buildConnector: `${ELASTICSEARCH_DOCS}es-build-connector.html`, bulkApi: `${ELASTICSEARCH_DOCS}docs-bulk.html`, configuration: `${ENTERPRISE_SEARCH_DOCS}configuration.html`, connectors: `${ELASTICSEARCH_DOCS}es-connectors.html`, diff --git a/packages/kbn-esql-ast/README.md b/packages/kbn-esql-ast/README.md index f7be5248f2ca0..dcb244af3c381 100644 --- a/packages/kbn-esql-ast/README.md +++ b/packages/kbn-esql-ast/README.md @@ -12,6 +12,7 @@ Contents of this package: - [`walker` — Contains the ES|QL AST `Walker` utility](./src/walker/README.md). - [`visitor` — Contains the ES|QL AST `Visitor` utility](./src/visitor/README.md). - [`pretty_print` — Contains code for formatting AST to text](./src/pretty_print/README.md). +- [`mutate` — Contains code for traversing and mutating the AST.](./src/mutate/README.md). ## Demo diff --git a/packages/kbn-esql-ast/index.ts b/packages/kbn-esql-ast/index.ts index 869d1aea7e0c6..1780b75f29237 100644 --- a/packages/kbn-esql-ast/index.ts +++ b/packages/kbn-esql-ast/index.ts @@ -56,3 +56,5 @@ export { } from './src/pretty_print'; export { EsqlQuery } from './src/query'; + +export * as mutate from './src/mutate'; diff --git a/packages/kbn-esql-ast/src/builder/builder.ts b/packages/kbn-esql-ast/src/builder/builder.ts index ecc375f885d8b..ece92fbcd7d5e 100644 --- a/packages/kbn-esql-ast/src/builder/builder.ts +++ b/packages/kbn-esql-ast/src/builder/builder.ts @@ -12,7 +12,9 @@ import { ESQLAstComment, ESQLAstQueryExpression, + ESQLColumn, ESQLCommand, + ESQLCommandOption, ESQLDecimalLiteral, ESQLInlineCast, ESQLIntegerLiteral, @@ -20,7 +22,7 @@ import { ESQLLocation, ESQLSource, } from '../types'; -import { AstNodeParserFields, AstNodeTemplate } from './types'; +import { AstNodeParserFields, AstNodeTemplate, PartialFields } from './types'; export namespace Builder { /** @@ -38,16 +40,29 @@ export namespace Builder { }); export const command = ( - template: AstNodeTemplate, + template: PartialFields, 'args'>, fromParser?: Partial ): ESQLCommand => { return { ...template, ...Builder.parserFields(fromParser), + args: template.args ?? [], type: 'command', }; }; + export const option = ( + template: PartialFields, 'args'>, + fromParser?: Partial + ): ESQLCommandOption => { + return { + ...template, + ...Builder.parserFields(fromParser), + args: template.args ?? [], + type: 'option', + }; + }; + export const comment = ( subtype: ESQLAstComment['subtype'], text: string, @@ -85,6 +100,19 @@ export namespace Builder { }; }; + export const column = ( + template: Omit, 'name' | 'quoted'>, + fromParser?: Partial + ): ESQLColumn => { + return { + ...template, + ...Builder.parserFields(fromParser), + quoted: false, + name: template.parts.join('.'), + type: 'column', + }; + }; + export const inlineCast = ( template: Omit, 'name'>, fromParser?: Partial diff --git a/packages/kbn-esql-ast/src/builder/types.ts b/packages/kbn-esql-ast/src/builder/types.ts index be1c1c0d6d458..2713a15fddc0f 100644 --- a/packages/kbn-esql-ast/src/builder/types.ts +++ b/packages/kbn-esql-ast/src/builder/types.ts @@ -29,3 +29,5 @@ export type AstNodeTemplate = Omit< 'type' | 'text' | 'location' | 'incomplete' > & Partial>; + +export type PartialFields = Omit & Partial>; diff --git a/packages/kbn-esql-ast/src/mutate/README.md b/packages/kbn-esql-ast/src/mutate/README.md new file mode 100644 index 0000000000000..8c38bb72ca226 --- /dev/null +++ b/packages/kbn-esql-ast/src/mutate/README.md @@ -0,0 +1,36 @@ +# Mutation API + +The ES|QL mutation API provides methods to navigate and modify the AST. + + +## Usage + +For example, insert a `FROM` command `METADATA` field: + +```typescript +import { parse, mutate, BasicPrettyPrinter } from '@elastic/esql'; + +const { root } = parse('FROM index METADATA _lang'); + +console.log([...mutate.commands.from.metadata.list(root)]); // [ '_lang' ] + +mutate.commands.from.metadata.upsert(root, '_id'); + +console.log([...mutate.commands.from.metadata.list(root)]); // [ '_lang', '_id' ] + +const src = BasicPrettyPrinter.print(root); + +console.log(src); // FROM index METADATA _lang, _id +``` + + +## API + +- `.commands.from.metadata.list()` — List all `METADATA` fields. +- `.commands.from.metadata.find()` — Find a `METADATA` field by name. +- `.commands.from.metadata.removeByPredicate()` — Remove a `METADATA` + field by matching a predicate. +- `.commands.from.metadata.remove()` — Remove a `METADATA` field by name. +- `.commands.from.metadata.insert()` — Insert a `METADATA` field. +- `.commands.from.metadata.upsert()` — Insert `METADATA` field, if it does + not exist. diff --git a/packages/kbn-esql-ast/src/mutate/commands/from/index.ts b/packages/kbn-esql-ast/src/mutate/commands/from/index.ts new file mode 100644 index 0000000000000..df76e072b346e --- /dev/null +++ b/packages/kbn-esql-ast/src/mutate/commands/from/index.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", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import * as metadata from './metadata'; + +export { metadata }; diff --git a/packages/kbn-esql-ast/src/mutate/commands/from/metadata.test.ts b/packages/kbn-esql-ast/src/mutate/commands/from/metadata.test.ts new file mode 100644 index 0000000000000..b6cb485395a6c --- /dev/null +++ b/packages/kbn-esql-ast/src/mutate/commands/from/metadata.test.ts @@ -0,0 +1,332 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { parse } from '../../../parser'; +import { BasicPrettyPrinter } from '../../../pretty_print'; +import * as commands from '..'; + +describe('commands.from.metadata', () => { + describe('.list()', () => { + it('returns empty array on no metadata in query', () => { + const src = 'FROM index | WHERE a = b | LIMIT 123'; + const { root } = parse(src); + const column = [...commands.from.metadata.list(root)]; + + expect(column.length).toBe(0); + }); + + it('returns a single METADATA field', () => { + const src = 'FROM index METADATA a'; + const { root } = parse(src); + const [column] = [...commands.from.metadata.list(root)][0]; + + expect(column).toMatchObject({ + type: 'column', + parts: ['a'], + }); + }); + + it('returns all METADATA fields', () => { + const src = 'FROM index METADATA a, b, _id, _lang | STATS avg(a) as avg_a | LIMIT 88'; + const { root } = parse(src); + const columns = [...commands.from.metadata.list(root)].map(([column]) => column); + + expect(columns).toMatchObject([ + { + type: 'column', + parts: ['a'], + }, + { + type: 'column', + parts: ['b'], + }, + { + type: 'column', + parts: ['_id'], + }, + { + type: 'column', + parts: ['_lang'], + }, + ]); + }); + }); + + describe('.find()', () => { + it('returns undefined if field is not found', () => { + const src = 'FROM index | WHERE a = b | LIMIT 123'; + const { root } = parse(src); + const column = commands.from.metadata.find(root, ['a']); + + expect(column).toBe(undefined); + }); + + it('can find a single field', () => { + const src = 'FROM index METADATA a'; + const { root } = parse(src); + const [column] = commands.from.metadata.find(root, ['a'])!; + + expect(column).toMatchObject({ + type: 'column', + name: 'a', + }); + }); + + it('can find a single METADATA field', () => { + const src = 'FROM index METADATA a, b, c, _lang, _id'; + const { root } = parse(src); + const [column1] = commands.from.metadata.find(root, 'c')!; + const [column2] = commands.from.metadata.find(root, '_id')!; + + expect(column1).toMatchObject({ + type: 'column', + name: 'c', + }); + expect(column2).toMatchObject({ + type: 'column', + name: '_id', + }); + }); + }); + + describe('.remove()', () => { + it('can remove a metadata field from a list', () => { + const src1 = 'FROM index METADATA a, b, c'; + const { root } = parse(src1); + const src2 = BasicPrettyPrinter.print(root); + + expect(src2).toBe('FROM index METADATA a, b, c'); + + commands.from.metadata.remove(root, 'b'); + + const src3 = BasicPrettyPrinter.print(root); + + expect(src3).toBe('FROM index METADATA a, c'); + }); + + it('does nothing if field-to-delete does not exist', () => { + const src1 = 'FROM index METADATA a, b, c'; + const { root } = parse(src1); + const src2 = BasicPrettyPrinter.print(root); + + expect(src2).toBe('FROM index METADATA a, b, c'); + + commands.from.metadata.remove(root, 'd'); + + const src3 = BasicPrettyPrinter.print(root); + + expect(src3).toBe('FROM index METADATA a, b, c'); + }); + + it('can remove all metadata fields one-by-one', () => { + const src1 = 'FROM index METADATA a, b, c'; + const { root } = parse(src1); + const src2 = BasicPrettyPrinter.print(root); + + expect(src2).toBe('FROM index METADATA a, b, c'); + + commands.from.metadata.remove(root, 'b'); + commands.from.metadata.remove(root, 'c'); + commands.from.metadata.remove(root, 'a'); + + const src3 = BasicPrettyPrinter.print(root); + + expect(src3).toBe('FROM index'); + }); + }); + + describe('.insert()', () => { + it('can append a METADATA field', () => { + const src1 = 'FROM index METADATA a'; + const { root } = parse(src1); + + commands.from.metadata.insert(root, 'b'); + + const src2 = BasicPrettyPrinter.print(root); + + expect(src2).toBe('FROM index METADATA a, b'); + }); + + it('return inserted `column` node, and parent `option` node', () => { + const src1 = 'FROM index METADATA a'; + const { root } = parse(src1); + + const tuple = commands.from.metadata.insert(root, 'b'); + + expect(tuple).toMatchObject([ + { + type: 'column', + name: 'b', + }, + { + type: 'option', + name: 'metadata', + }, + ]); + }); + + it('can insert at specified position', () => { + const src1 = 'FROM index METADATA a1, a2, a3'; + const { root } = parse(src1); + + commands.from.metadata.insert(root, 'x', 0); + + const src2 = BasicPrettyPrinter.print(root); + + expect(src2).toBe('FROM index METADATA x, a1, a2, a3'); + + commands.from.metadata.insert(root, 'y', 2); + + const src3 = BasicPrettyPrinter.print(root); + + expect(src3).toBe('FROM index METADATA x, a1, y, a2, a3'); + + commands.from.metadata.insert(root, 'z', 4); + + const src4 = BasicPrettyPrinter.print(root); + + expect(src4).toBe('FROM index METADATA x, a1, y, a2, z, a3'); + }); + + it('appends element, when insert position too high', () => { + const src1 = 'FROM index METADATA a1, a2, a3'; + const { root } = parse(src1); + + commands.from.metadata.insert(root, 'x', 999); + + const src2 = BasicPrettyPrinter.print(root); + + expect(src2).toBe('FROM index METADATA a1, a2, a3, x'); + }); + + it('can insert a field when no METADATA option present', () => { + const src1 = 'FROM index'; + const { root } = parse(src1); + + commands.from.metadata.insert(root, 'x', 999); + + const src2 = BasicPrettyPrinter.print(root); + + expect(src2).toBe('FROM index METADATA x'); + + commands.from.metadata.insert(root, 'y', 999); + + const src3 = BasicPrettyPrinter.print(root); + + expect(src3).toBe('FROM index METADATA x, y'); + }); + + it('can inset the same field twice', () => { + const src1 = 'FROM index'; + const { root } = parse(src1); + + commands.from.metadata.insert(root, 'x', 999); + commands.from.metadata.insert(root, 'x', 999); + + const src2 = BasicPrettyPrinter.print(root); + + expect(src2).toBe('FROM index METADATA x, x'); + }); + }); + + describe('.upsert()', () => { + it('can append a METADATA field', () => { + const src1 = 'FROM index METADATA a'; + const { root } = parse(src1); + + commands.from.metadata.upsert(root, 'b'); + + const src2 = BasicPrettyPrinter.print(root); + + expect(src2).toBe('FROM index METADATA a, b'); + }); + + it('return inserted `column` node, and parent `option` node', () => { + const src1 = 'FROM index METADATA a'; + const { root } = parse(src1); + + const tuple = commands.from.metadata.upsert(root, 'b'); + + expect(tuple).toMatchObject([ + { + type: 'column', + name: 'b', + }, + { + type: 'option', + name: 'metadata', + }, + ]); + }); + + it('can insert at specified position', () => { + const src1 = 'FROM index METADATA a1, a2, a3'; + const { root } = parse(src1); + + commands.from.metadata.upsert(root, 'x', 0); + + const src2 = BasicPrettyPrinter.print(root); + + expect(src2).toBe('FROM index METADATA x, a1, a2, a3'); + + commands.from.metadata.upsert(root, 'y', 2); + + const src3 = BasicPrettyPrinter.print(root); + + expect(src3).toBe('FROM index METADATA x, a1, y, a2, a3'); + + commands.from.metadata.upsert(root, 'z', 4); + + const src4 = BasicPrettyPrinter.print(root); + + expect(src4).toBe('FROM index METADATA x, a1, y, a2, z, a3'); + }); + + it('appends element, when insert position too high', () => { + const src1 = 'FROM index METADATA a1, a2, a3'; + const { root } = parse(src1); + + commands.from.metadata.upsert(root, 'x', 999); + + const src2 = BasicPrettyPrinter.print(root); + + expect(src2).toBe('FROM index METADATA a1, a2, a3, x'); + }); + + it('can insert a field when no METADATA option present', () => { + const src1 = 'FROM index'; + const { root } = parse(src1); + + commands.from.metadata.upsert(root, 'x', 999); + + const src2 = BasicPrettyPrinter.print(root); + + expect(src2).toBe('FROM index METADATA x'); + + commands.from.metadata.upsert(root, 'y', 999); + + const src3 = BasicPrettyPrinter.print(root); + + expect(src3).toBe('FROM index METADATA x, y'); + }); + + it('does not insert a field if it is already present', () => { + const src1 = 'FROM index'; + const { root } = parse(src1); + + commands.from.metadata.upsert(root, 'x', 999); + commands.from.metadata.upsert(root, 'x', 999); + commands.from.metadata.upsert(root, 'x', 999); + + const src2 = BasicPrettyPrinter.print(root); + + expect(src2).toBe('FROM index METADATA x'); + }); + }); +}); diff --git a/packages/kbn-esql-ast/src/mutate/commands/from/metadata.ts b/packages/kbn-esql-ast/src/mutate/commands/from/metadata.ts new file mode 100644 index 0000000000000..5892b028823aa --- /dev/null +++ b/packages/kbn-esql-ast/src/mutate/commands/from/metadata.ts @@ -0,0 +1,206 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { Walker } from '../../../walker'; +import { ESQLAstQueryExpression, ESQLColumn, ESQLCommandOption } from '../../../types'; +import { Visitor } from '../../../visitor'; +import { cmpArr, findByPredicate } from '../../util'; +import * as generic from '../../generic'; +import { Builder } from '../../../builder'; +import type { Predicate } from '../../types'; + +/** + * Returns all METADATA field AST nodes and their corresponding parent command + * option nodes. + * + * @param ast The root AST node to search for metadata fields. + * @returns A collection of [column, option] pairs for each metadata field found. + */ +export const list = ( + ast: ESQLAstQueryExpression +): IterableIterator<[ESQLColumn, ESQLCommandOption]> => { + type ReturnExpression = IterableIterator; + type ReturnCommand = IterableIterator<[ESQLColumn, ESQLCommandOption]>; + + return new Visitor() + .on('visitExpression', function* (): ReturnExpression {}) + .on('visitColumnExpression', function* (ctx): ReturnExpression { + yield ctx.node; + }) + .on('visitCommandOption', function* (ctx): ReturnCommand { + if (ctx.node.name !== 'metadata') { + return; + } + for (const args of ctx.visitArguments()) { + for (const column of args) { + yield [column, ctx.node]; + } + } + }) + .on('visitFromCommand', function* (ctx): ReturnCommand { + for (const options of ctx.visitOptions()) { + yield* options; + } + }) + .on('visitCommand', function* (): ReturnCommand {}) + .on('visitQuery', function* (ctx): ReturnCommand { + for (const command of ctx.visitCommands()) { + yield* command; + } + }) + .visitQuery(ast); +}; + +/** + * Find a METADATA field by its name or parts. + * + * @param ast The root AST node to search for metadata fields. + * @param fieldName The name or parts of the field to find. + * @returns A 2-tuple containing the column and the option it was found in, or + * `undefined` if the field was not found. + */ +export const find = ( + ast: ESQLAstQueryExpression, + fieldName: string | string[] +): [ESQLColumn, ESQLCommandOption] | undefined => { + if (typeof fieldName === 'string') { + fieldName = [fieldName]; + } + + const predicate: Predicate<[ESQLColumn, unknown]> = ([field]) => + cmpArr(field.parts, fieldName as string[]); + + return findByPredicate(list(ast), predicate); +}; + +/** + * Removes the first found METADATA field that satisfies the predicate. + * + * @param ast The root AST node to search for metadata fields. + * @param predicate The predicate function to filter fields. + * @returns The removed column and option, if any. + */ +export const removeByPredicate = ( + ast: ESQLAstQueryExpression, + predicate: Predicate +): [column: ESQLColumn, option: ESQLCommandOption] | undefined => { + const tuple = findByPredicate(list(ast), ([field]) => predicate(field)); + + if (!tuple) { + return; + } + + const [column, option] = tuple; + const index = option.args.indexOf(column); + + if (index === -1) { + return; + } + + option.args.splice(index, 1); + + if (option.args.length === 0) { + generic.removeCommandOption(ast, option); + } + + return tuple; +}; + +/** + * Removes the first METADATA field that matches the given name and returns + * a 2-tuple (the column and the option it was removed from). + * + * @param ast The root AST node to search for metadata fields. + * @param fieldName The name or parts of the field to remove. + * @returns The removed column and option, if any. + */ +export const remove = ( + ast: ESQLAstQueryExpression, + fieldName: string | string[] +): [column: ESQLColumn, option: ESQLCommandOption] | undefined => { + if (typeof fieldName === 'string') { + fieldName = [fieldName]; + } + + return removeByPredicate(ast, (field) => cmpArr(field.parts, fieldName as string[])); +}; + +/** + * Insert into a specific position or append a `METADATA` field to the `FROM` + * command. + * + * @param ast The root AST node. + * @param fieldName Field name or parts as an array, e.g. `['a', 'b']`. + * @param index Position to insert the field at. If `-1` or not specified, the + * field will be appended. + * @returns If the field was successfully inserted, returns a 2-tuple containing + * the column and the option it was inserted into. Otherwise, returns + * `undefined`. + */ +export const insert = ( + ast: ESQLAstQueryExpression, + fieldName: string | string[], + index: number = -1 +): [column: ESQLColumn, option: ESQLCommandOption] | undefined => { + let option = generic.findCommandOptionByName(ast, 'from', 'metadata'); + + if (!option) { + const command = generic.findCommandByName(ast, 'from'); + + if (!command) { + return; + } + + option = generic.insertCommandOption(command, 'metadata'); + } + + const parts: string[] = typeof fieldName === 'string' ? [fieldName] : fieldName; + const column = Builder.expression.column({ parts }); + + if (index === -1) { + option.args.push(column); + } else { + option.args.splice(index, 0, column); + } + + return [column, option]; +}; + +/** + * The `.upsert()` method works like `.insert()`, but will not insert a field + * if it already exists. + * + * @param ast The root AST node. + * @param fieldName The field name or parts as an array, e.g. `['a', 'b']`. + * @param index Position to insert the field at. If `-1` or not specified, the + * field will be appended. + * @returns If the field was successfully inserted, returns a 2-tuple containing + * the column and the option it was inserted into. Otherwise, returns + * `undefined`. + */ +export const upsert = ( + ast: ESQLAstQueryExpression, + fieldName: string | string[], + index: number = -1 +): [column: ESQLColumn, option: ESQLCommandOption] | undefined => { + const option = generic.findCommandOptionByName(ast, 'from', 'metadata'); + + if (option) { + const parts = Array.isArray(fieldName) ? fieldName : [fieldName]; + const existing = Walker.find( + option, + (node) => node.type === 'column' && cmpArr(node.parts, parts) + ); + if (existing) { + return undefined; + } + } + + return insert(ast, fieldName, index); +}; diff --git a/packages/kbn-esql-ast/src/mutate/commands/index.ts b/packages/kbn-esql-ast/src/mutate/commands/index.ts new file mode 100644 index 0000000000000..cc3b7f446fa88 --- /dev/null +++ b/packages/kbn-esql-ast/src/mutate/commands/index.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", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import * as from from './from'; + +export { from }; diff --git a/packages/kbn-esql-ast/src/mutate/generic.test.ts b/packages/kbn-esql-ast/src/mutate/generic.test.ts new file mode 100644 index 0000000000000..14d951db1bccb --- /dev/null +++ b/packages/kbn-esql-ast/src/mutate/generic.test.ts @@ -0,0 +1,113 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { parse } from '../parser'; +import { BasicPrettyPrinter } from '../pretty_print'; +import * as generic from './generic'; + +describe('generic', () => { + describe('.listCommands()', () => { + it('lists all commands', () => { + const src = 'FROM index | WHERE a == b | LIMIT 123'; + const { root } = parse(src); + const commands = [...generic.listCommands(root)].map((cmd) => cmd.name); + + expect(commands).toEqual(['from', 'where', 'limit']); + }); + }); + + describe('.findCommand()', () => { + it('can the first command', () => { + const src = 'FROM index | WHERE a == b | LIMIT 123'; + const { root } = parse(src); + const command = generic.findCommand(root, (cmd) => cmd.name === 'from'); + + expect(command).toMatchObject({ + type: 'command', + name: 'from', + args: [ + { + type: 'source', + }, + ], + }); + }); + + it('can the last command', () => { + const src = 'FROM index | WHERE a == b | LIMIT 123'; + const { root } = parse(src); + const command = generic.findCommand(root, (cmd) => cmd.name === 'limit'); + + expect(command).toMatchObject({ + type: 'command', + name: 'limit', + args: [ + { + type: 'literal', + }, + ], + }); + }); + + it('find the specific of multiple commands', () => { + const src = 'FROM index | WHERE a == b | LIMIT 1 | LIMIT 2 | LIMIT 3'; + const { root } = parse(src); + const command = generic.findCommand( + root, + (cmd) => cmd.name === 'limit' && (cmd.args?.[0] as any).value === 2 + ); + + expect(command).toMatchObject({ + type: 'command', + name: 'limit', + args: [ + { + type: 'literal', + value: 2, + }, + ], + }); + }); + }); + + describe('.findCommandOptionByName()', () => { + it('can the find a command option', () => { + const src = 'FROM index METADATA _score'; + const { root } = parse(src); + const option = generic.findCommandOptionByName(root, 'from', 'metadata'); + + expect(option).toMatchObject({ + type: 'option', + name: 'metadata', + }); + }); + + it('returns undefined if there is no option', () => { + const src = 'FROM index'; + const { root } = parse(src); + const option = generic.findCommandOptionByName(root, 'from', 'metadata'); + + expect(option).toBe(undefined); + }); + }); + + describe('.removeCommandOption()', () => { + it('can remove existing command option', () => { + const src = 'FROM index METADATA _score'; + const { root } = parse(src); + const option = generic.findCommandOptionByName(root, 'from', 'metadata'); + + generic.removeCommandOption(root, option!); + + const src2 = BasicPrettyPrinter.print(root); + + expect(src2).toBe('FROM index'); + }); + }); +}); diff --git a/packages/kbn-esql-ast/src/mutate/generic.ts b/packages/kbn-esql-ast/src/mutate/generic.ts new file mode 100644 index 0000000000000..968eaf84f4a46 --- /dev/null +++ b/packages/kbn-esql-ast/src/mutate/generic.ts @@ -0,0 +1,198 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { Builder } from '../builder'; +import { ESQLAstQueryExpression, ESQLCommand, ESQLCommandOption } from '../types'; +import { Visitor } from '../visitor'; +import { Predicate } from './types'; + +/** + * Returns an iterator for all command AST nodes in the query. If a predicate is + * provided, only commands that satisfy the predicate will be returned. + * + * @param ast Root AST node to search for commands. + * @param predicate Optional predicate to filter commands. + * @returns A list of commands found in the AST. + */ +export const listCommands = ( + ast: ESQLAstQueryExpression, + predicate?: Predicate +): IterableIterator => { + return new Visitor() + .on('visitQuery', function* (ctx): IterableIterator { + for (const cmd of ctx.commands()) { + if (!predicate || predicate(cmd)) { + yield cmd; + } + } + }) + .visitQuery(ast); +}; + +/** + * Returns the first command AST node at a given index in the query that + * satisfies the predicate. If no index is provided, the first command found + * will be returned. + * + * @param ast Root AST node to search for commands. + * @param predicate Optional predicate to filter commands. + * @param index The index of the command to return. + * @returns The command found in the AST, if any. + */ +export const findCommand = ( + ast: ESQLAstQueryExpression, + predicate?: Predicate, + index: number = 0 +): ESQLCommand | undefined => { + for (const cmd of listCommands(ast, predicate)) { + if (!index) { + return cmd; + } + + index--; + } + + return undefined; +}; + +/** + * Returns the first command option AST node that satisfies the predicate. + * + * @param command The command AST node to search for options. + * @param predicate The predicate to filter options. + * @returns The option found in the command, if any. + */ +export const findCommandOption = ( + command: ESQLCommand, + predicate: Predicate +): ESQLCommandOption | undefined => { + return new Visitor() + .on('visitCommand', (ctx): ESQLCommandOption | undefined => { + for (const opt of ctx.options()) { + if (predicate(opt)) { + return opt; + } + } + + return undefined; + }) + .visitCommand(command); +}; + +/** + * Returns the first command AST node at a given index with a given name in the + * query. If no index is provided, the first command found will be returned. + * + * @param ast Root AST node to search for commands. + * @param commandName The name of the command to find. + * @param index The index of the command to return. + * @returns The command found in the AST, if any. + */ +export const findCommandByName = ( + ast: ESQLAstQueryExpression, + commandName: string, + index: number = 0 +): ESQLCommand | undefined => { + return findCommand(ast, (cmd) => cmd.name === commandName, index); +}; + +/** + * Returns the first command option AST node with a given name in the query. + * + * @param ast The root AST node to search for command options. + * @param commandName Command name to search for. + * @param optionName Option name to search for. + * @returns The option found in the command, if any. + */ +export const findCommandOptionByName = ( + ast: ESQLAstQueryExpression, + commandName: string, + optionName: string +): ESQLCommandOption | undefined => { + const command = findCommand(ast, (cmd) => cmd.name === commandName); + + if (!command) { + return undefined; + } + + return findCommandOption(command, (opt) => opt.name === optionName); +}; + +/** + * Inserts a command option into the command's arguments list. The option can + * be specified as a string or an AST node. + * + * @param command The command AST node to insert the option into. + * @param option The option to insert. + * @returns The inserted option. + */ +export const insertCommandOption = ( + command: ESQLCommand, + option: string | ESQLCommandOption +): ESQLCommandOption => { + if (typeof option === 'string') { + option = Builder.option({ name: option }); + } + + command.args.push(option); + + return option; +}; + +/** + * Removes the first command option from the command's arguments list that + * satisfies the predicate. + * + * @param command The command AST node to remove the option from. + * @param predicate The predicate to filter options. + * @returns The removed option, if any. + */ +export const removeCommandOption = ( + ast: ESQLAstQueryExpression, + option: ESQLCommandOption +): boolean => { + return new Visitor() + .on('visitCommandOption', (ctx): boolean => { + return ctx.node === option; + }) + .on('visitCommand', (ctx): boolean => { + let target: undefined | ESQLCommandOption; + + for (const opt of ctx.options()) { + if (opt === option) { + target = opt; + break; + } + } + + if (!target) { + return false; + } + + const index = ctx.node.args.indexOf(target); + + if (index === -1) { + return false; + } + + ctx.node.args.splice(index, 1); + + return true; + }) + .on('visitQuery', (ctx): boolean => { + for (const success of ctx.visitCommands()) { + if (success) { + return true; + } + } + + return false; + }) + .visitQuery(ast); +}; diff --git a/packages/kbn-esql-ast/src/mutate/index.ts b/packages/kbn-esql-ast/src/mutate/index.ts new file mode 100644 index 0000000000000..da312eb79418a --- /dev/null +++ b/packages/kbn-esql-ast/src/mutate/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", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +export * from './types'; + +import * as generic from './generic'; +import * as commands from './commands'; + +export { generic, commands }; diff --git a/packages/kbn-esql-ast/src/mutate/types.ts b/packages/kbn-esql-ast/src/mutate/types.ts new file mode 100644 index 0000000000000..14cf5d5867d38 --- /dev/null +++ b/packages/kbn-esql-ast/src/mutate/types.ts @@ -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", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +export type Predicate = (item: T) => boolean; diff --git a/packages/kbn-esql-ast/src/mutate/util.ts b/packages/kbn-esql-ast/src/mutate/util.ts new file mode 100644 index 0000000000000..24f10c1b22f93 --- /dev/null +++ b/packages/kbn-esql-ast/src/mutate/util.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", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { Predicate } from './types'; + +/** + * Find the first item in an iterable (such as array) that matches a predicate. + * + * @param iterable List of items to search through. + * @param predicate Function to determine if an item is the one we are looking + * for. + * @returns The first item that matches the predicate, or undefined if no item + * matches. + */ +export const findByPredicate = ( + iterable: IterableIterator, + predicate: Predicate +): T | undefined => { + for (const item of iterable) { + if (predicate(item)) { + return item; + } + } + return undefined; +}; + +/** + * Shallowly compares two arrays for equality. + * + * @param a The first array to compare. + * @param b The second array to compare. + * @returns True if the arrays are equal, false otherwise. + */ +export const cmpArr = (a: T[], b: T[]): boolean => { + const length = a.length; + if (length !== b.length) { + return false; + } + + for (let i = 0; i < length; i++) { + if (a[i] !== b[i]) { + return false; + } + } + return true; +}; diff --git a/packages/kbn-esql-ast/src/visitor/visitor.ts b/packages/kbn-esql-ast/src/visitor/visitor.ts index 523325b49457c..8fc454ea5e3f7 100644 --- a/packages/kbn-esql-ast/src/visitor/visitor.ts +++ b/packages/kbn-esql-ast/src/visitor/visitor.ts @@ -247,6 +247,7 @@ export class Visitor< ? Builder.expression.query(nodeOrCommands) : nodeOrCommands; const queryContext = new QueryVisitorContext(this.ctx, node, null); + return this.visit(queryContext, input); } diff --git a/packages/kbn-esql-utils/index.ts b/packages/kbn-esql-utils/index.ts index 4221bced60d04..223181f2bd154 100644 --- a/packages/kbn-esql-utils/index.ts +++ b/packages/kbn-esql-utils/index.ts @@ -28,6 +28,7 @@ export { prettifyQuery, isQueryWrappedByPipes, retrieveMetadataColumns, + getQueryColumnsFromESQLQuery, TextBasedLanguages, } from './src'; diff --git a/packages/kbn-esql-utils/src/index.ts b/packages/kbn-esql-utils/src/index.ts index 95332bbd10328..e36283c7a9238 100644 --- a/packages/kbn-esql-utils/src/index.ts +++ b/packages/kbn-esql-utils/src/index.ts @@ -20,6 +20,7 @@ export { prettifyQuery, isQueryWrappedByPipes, retrieveMetadataColumns, + getQueryColumnsFromESQLQuery, } from './utils/query_parsing_helpers'; export { appendToESQLQuery, appendWhereClauseToESQLQuery } from './utils/append_to_query'; export { diff --git a/packages/kbn-esql-utils/src/utils/query_parsing_helpers.test.ts b/packages/kbn-esql-utils/src/utils/query_parsing_helpers.test.ts index 5479d6982e46a..9cd0052ca9882 100644 --- a/packages/kbn-esql-utils/src/utils/query_parsing_helpers.test.ts +++ b/packages/kbn-esql-utils/src/utils/query_parsing_helpers.test.ts @@ -16,6 +16,7 @@ import { prettifyQuery, isQueryWrappedByPipes, retrieveMetadataColumns, + getQueryColumnsFromESQLQuery, } from './query_parsing_helpers'; describe('esql query helpers', () => { @@ -229,4 +230,47 @@ describe('esql query helpers', () => { expect(retrieveMetadataColumns('from a | eval b = 1')).toStrictEqual([]); }); }); + + describe('getQueryColumnsFromESQLQuery', () => { + it('should return the columns used in stats', () => { + expect( + getQueryColumnsFromESQLQuery('from a | stats var0 = avg(bytes) by dest') + ).toStrictEqual(['var0', 'bytes', 'dest']); + }); + + it('should return the columns used in eval', () => { + expect( + getQueryColumnsFromESQLQuery('from a | eval dest = geo.dest, var1 = bytes') + ).toStrictEqual(['dest', 'geo.dest', 'var1', 'bytes']); + }); + + it('should return the columns used in eval and stats', () => { + expect( + getQueryColumnsFromESQLQuery('from a | stats var0 = avg(bytes) by dest | eval meow = var0') + ).toStrictEqual(['var0', 'bytes', 'dest', 'meow', 'var0']); + }); + + it('should return the metadata columns', () => { + expect( + getQueryColumnsFromESQLQuery('from a metadata _id, _ignored | eval b = 1') + ).toStrictEqual(['_id', '_ignored', 'b']); + }); + + it('should return the keep columns', () => { + expect(getQueryColumnsFromESQLQuery('from a | keep b, c, d')).toStrictEqual(['b', 'c', 'd']); + }); + + it('should return the where columns', () => { + expect( + getQueryColumnsFromESQLQuery('from a | where field > 1000 and abs(fieldb) < 20') + ).toStrictEqual(['field', 'fieldb']); + }); + + it('should return the rename columns', () => { + expect(getQueryColumnsFromESQLQuery('from a | rename field as fieldb')).toStrictEqual([ + 'field', + 'fieldb', + ]); + }); + }); }); diff --git a/packages/kbn-esql-utils/src/utils/query_parsing_helpers.ts b/packages/kbn-esql-utils/src/utils/query_parsing_helpers.ts index 5442f52c94b1b..ccad80f064a03 100644 --- a/packages/kbn-esql-utils/src/utils/query_parsing_helpers.ts +++ b/packages/kbn-esql-utils/src/utils/query_parsing_helpers.ts @@ -136,3 +136,14 @@ export const retrieveMetadataColumns = (esql: string): string[] => { const metadataOptions = options.find(({ name }) => name === 'metadata'); return metadataOptions?.args.map((column) => (column as ESQLColumn).name) ?? []; }; + +export const getQueryColumnsFromESQLQuery = (esql: string): string[] => { + const { root } = parse(esql); + const columns: ESQLColumn[] = []; + + walk(root, { + visitColumn: (node) => columns.push(node), + }); + + return columns.map((column) => column.name); +}; diff --git a/packages/kbn-search-index-documents/components/result/result.tsx b/packages/kbn-search-index-documents/components/result/result.tsx index 5e1c4db104116..0d0438a2275a5 100644 --- a/packages/kbn-search-index-documents/components/result/result.tsx +++ b/packages/kbn-search-index-documents/components/result/result.tsx @@ -36,6 +36,7 @@ export interface ResultProps { showScore?: boolean; compactCard?: boolean; onDocumentClick?: () => void; + onDocumentDelete?: () => void; } export const Result: React.FC = ({ @@ -45,6 +46,7 @@ export const Result: React.FC = ({ compactCard = true, showScore = false, onDocumentClick, + onDocumentDelete, }) => { const [isExpanded, setIsExpanded] = useState(false); const tooltipText = @@ -87,7 +89,10 @@ export const Result: React.FC = ({ values: { id: metaData.id }, }) } - metaData={metaData} + metaData={{ + ...metaData, + onDocumentDelete, + }} /> )} {!compactCard && ( @@ -101,7 +106,10 @@ export const Result: React.FC = ({ }) } onTitleClick={onDocumentClick} - metaData={metaData} + metaData={{ + ...metaData, + onDocumentDelete, + }} rightSideActions={ diff --git a/packages/kbn-search-index-documents/components/result/rich_result_header.tsx b/packages/kbn-search-index-documents/components/result/rich_result_header.tsx index 7caff8514871f..ff3f0df0ce7cd 100644 --- a/packages/kbn-search-index-documents/components/result/rich_result_header.tsx +++ b/packages/kbn-search-index-documents/components/result/rich_result_header.tsx @@ -70,6 +70,7 @@ const MetadataPopover: React.FC = ({ size="xs" iconType="iInCircle" color="primary" + data-test-subj="documentMetadataButton" onClick={(e: React.MouseEvent) => { e.stopPropagation(); setPopoverIsOpen(!popoverIsOpen); @@ -121,8 +122,10 @@ const MetadataPopover: React.FC = ({ iconType="trash" color="danger" size="s" + data-test-subj="deleteDocumentButton" onClick={(e: React.MouseEvent) => { e.stopPropagation(); + onDocumentDelete(); closePopover(); }} fullWidth diff --git a/src/core/server/integration_tests/ci_checks/saved_objects/check_registered_types.test.ts b/src/core/server/integration_tests/ci_checks/saved_objects/check_registered_types.test.ts index a8b31ffdd90fa..86eef03b1db26 100644 --- a/src/core/server/integration_tests/ci_checks/saved_objects/check_registered_types.test.ts +++ b/src/core/server/integration_tests/ci_checks/saved_objects/check_registered_types.test.ts @@ -146,7 +146,7 @@ describe('checking migration metadata changes on all registered SO types', () => "osquery-saved-query": "6095e288750aa3164dfe186c74bc5195c2bf2bd4", "policy-settings-protection-updates-note": "33924bb246f9e5bcb876109cc83e3c7a28308352", "query": "501bece68f26fe561286a488eabb1a8ab12f1137", - "risk-engine-configuration": "aea0c371a462e6d07c3ceb3aff11891b47feb09d", + "risk-engine-configuration": "bab237d09c2e7189dddddcb1b28f19af69755efb", "rules-settings": "892a2918ebaeba809a612b8d97cec0b07c800b5f", "sample-data-telemetry": "37441b12f5b0159c2d6d5138a494c9f440e950b5", "search": "0aa6eefb37edd3145be340a8b67779c2ca578b22", diff --git a/x-pack/plugins/cloud_security_posture/public/assets/illustrations/clouds.svg b/x-pack/plugins/cloud_security_posture/public/assets/illustrations/clouds.svg index 37561c8d5a340..50b4901752349 100644 --- a/x-pack/plugins/cloud_security_posture/public/assets/illustrations/clouds.svg +++ b/x-pack/plugins/cloud_security_posture/public/assets/illustrations/clouds.svg @@ -1,5822 +1,301 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/x-pack/plugins/cloud_security_posture/public/pages/compliance_dashboard/dashboard_sections/summary_section.tsx b/x-pack/plugins/cloud_security_posture/public/pages/compliance_dashboard/dashboard_sections/summary_section.tsx index 61632fcb4aa99..9cb41910f8f83 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/compliance_dashboard/dashboard_sections/summary_section.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/compliance_dashboard/dashboard_sections/summary_section.tsx @@ -40,13 +40,9 @@ export const dashboardColumnsGrow: Record = { third: 8, }; -export const getPolicyTemplateQuery = (policyTemplate: PosturePolicyTemplate): NavFilter => { - if (policyTemplate === CSPM_POLICY_TEMPLATE) { - return { 'rule.benchmark.posture_type': CSPM_POLICY_TEMPLATE }; - } - - return { 'rule.benchmark.posture_type': { value: CSPM_POLICY_TEMPLATE, negate: true } }; -}; +export const getPolicyTemplateQuery = (policyTemplate: PosturePolicyTemplate): NavFilter => ({ + 'rule.benchmark.posture_type': policyTemplate, +}); export const SummarySection = ({ dashboardType, diff --git a/x-pack/plugins/data_visualizer/kibana.jsonc b/x-pack/plugins/data_visualizer/kibana.jsonc index 84fc98d3fb22f..06d37106c2480 100644 --- a/x-pack/plugins/data_visualizer/kibana.jsonc +++ b/x-pack/plugins/data_visualizer/kibana.jsonc @@ -33,7 +33,6 @@ "kibanaReact", "kibanaUtils", "maps", - "esUiShared", "fieldFormats", "uiActions", "lens", diff --git a/x-pack/plugins/data_visualizer/public/application/file_data_visualizer/components/json_editor/json_editor.tsx b/x-pack/plugins/data_visualizer/public/application/file_data_visualizer/components/json_editor/json_editor.tsx index e9dc536ff7af4..54894dddf6c52 100644 --- a/x-pack/plugins/data_visualizer/public/application/file_data_visualizer/components/json_editor/json_editor.tsx +++ b/x-pack/plugins/data_visualizer/public/application/file_data_visualizer/components/json_editor/json_editor.tsx @@ -9,9 +9,8 @@ import type { FC } from 'react'; import React from 'react'; import { CodeEditor, type CodeEditorProps } from '@kbn/code-editor'; -import { expandLiteralStrings, XJsonMode } from '../../../shared_imports'; -export const EDITOR_MODE = { TEXT: 'text', JSON: 'json', XJSON: new XJsonMode() }; +export const EDITOR_MODE = { TEXT: 'text', JSON: 'json' }; interface JobEditorProps { value: string; @@ -30,10 +29,6 @@ export const JsonEditor: FC = ({ readOnly = false, onChange = () => {}, }) => { - if (mode === EDITOR_MODE.XJSON) { - value = expandLiteralStrings(value); - } - return ( ; interface Props { actions: CreateAnalyticsFormProps['actions']; @@ -176,7 +173,6 @@ export const RuntimeMappings: FC = ({ actions, state }) => { } setAdvancedRuntimeMappingsConfig={setAdvancedRuntimeMappingsConfig} convertToJson={convertToJson} - xJsonMode={xJsonMode} /> )} diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/runtime_mappings/runtime_mappings_editor.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/runtime_mappings/runtime_mappings_editor.tsx index dab89a3da3dd4..7315719a5009b 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/runtime_mappings/runtime_mappings_editor.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/runtime_mappings/runtime_mappings_editor.tsx @@ -12,7 +12,6 @@ import { i18n } from '@kbn/i18n'; import { CodeEditor } from '@kbn/code-editor'; import { isRuntimeMappings } from '@kbn/ml-runtime-field-utils'; -import type { XJsonModeType } from './runtime_mappings'; interface Props { convertToJson: (data: string) => string; @@ -20,13 +19,11 @@ interface Props { setIsRuntimeMappingsEditorApplyButtonEnabled: React.Dispatch>; advancedEditorRuntimeMappingsLastApplied: string | undefined; advancedRuntimeMappingsConfig: string; - xJsonMode: XJsonModeType; } export const RuntimeMappingsEditor: FC = memo( ({ convertToJson, - xJsonMode, setAdvancedRuntimeMappingsConfig, setIsRuntimeMappingsEditorApplyButtonEnabled, advancedEditorRuntimeMappingsLastApplied, diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/common/datafeed_preview_flyout/datafeed_preview.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/common/datafeed_preview_flyout/datafeed_preview.tsx index dc5bfc6003dd3..c81aa0a1bb127 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/common/datafeed_preview_flyout/datafeed_preview.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/common/datafeed_preview_flyout/datafeed_preview.tsx @@ -29,7 +29,7 @@ export const DatafeedPreview: FC<{ const { jobs: { datafeedPreview }, } = useMlApi(); - // the ace editor requires a fixed height + // the editor requires a fixed height const editorHeight = useMemo( () => `${window.innerHeight - 230 - heightOffset}px`, [heightOffset] diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/common/json_editor_flyout/json_editor_flyout.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/common/json_editor_flyout/json_editor_flyout.tsx index 028cbc0e54f2a..6c128bcef12a0 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/common/json_editor_flyout/json_editor_flyout.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/common/json_editor_flyout/json_editor_flyout.tsx @@ -298,7 +298,7 @@ const Contents: FC<{ heightOffset?: number; schema?: object; }> = ({ title, value, editJson, onChange, heightOffset = 0, schema }) => { - // the ace editor requires a fixed height + // the editor requires a fixed height const editorHeight = useMemo( () => `${window.innerHeight - 230 - heightOffset}px`, [heightOffset] diff --git a/x-pack/plugins/ml/tsconfig.json b/x-pack/plugins/ml/tsconfig.json index 4f1d63728ab33..24ed35277b93a 100644 --- a/x-pack/plugins/ml/tsconfig.json +++ b/x-pack/plugins/ml/tsconfig.json @@ -24,7 +24,6 @@ "path": "../../../src/setup_node_env/tsconfig.json" }, // add references to other TypeScript projects the plugin depends on - "@kbn/ace", "@kbn/actions-plugin", "@kbn/aiops-plugin", "@kbn/alerting-plugin", diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant/public/hooks/use_chat.ts b/x-pack/plugins/observability_solution/observability_ai_assistant/public/hooks/use_chat.ts index b51b33797e285..b621b3b151713 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant/public/hooks/use_chat.ts +++ b/x-pack/plugins/observability_solution/observability_ai_assistant/public/hooks/use_chat.ts @@ -104,10 +104,11 @@ function useChatWithoutContext({ (error: Error) => { if (error instanceof AbortError) { setChatState(ChatState.Aborted); - } else { - setChatState(ChatState.Error); + return; } + setChatState(ChatState.Error); + if (isTokenLimitReachedError(error)) { setMessages((msgs) => [ ...msgs, diff --git a/x-pack/plugins/search_indices/common/routes.ts b/x-pack/plugins/search_indices/common/routes.ts index 3be5f465de5ab..9ffe1d09d3db5 100644 --- a/x-pack/plugins/search_indices/common/routes.ts +++ b/x-pack/plugins/search_indices/common/routes.ts @@ -9,3 +9,5 @@ export const GET_STATUS_ROUTE = '/internal/search_indices/status'; export const GET_USER_PRIVILEGES_ROUTE = '/internal/search_indices/start_privileges'; export const POST_CREATE_INDEX_ROUTE = '/internal/search_indices/indices/create'; + +export const INDEX_DOCUMENT_ROUTE = '/internal/search_indices/{indexName}/documents/{id}'; diff --git a/x-pack/plugins/search_indices/public/components/index_documents/document_list.tsx b/x-pack/plugins/search_indices/public/components/index_documents/document_list.tsx index 22735cc86c05f..361bc83a5a044 100644 --- a/x-pack/plugins/search_indices/public/components/index_documents/document_list.tsx +++ b/x-pack/plugins/search_indices/public/components/index_documents/document_list.tsx @@ -13,6 +13,7 @@ import { Result, resultMetaData, resultToField } from '@kbn/search-index-documen import { EuiSpacer } from '@elastic/eui'; import { RecentDocsActionMessage } from './recent_docs_action_message'; +import { useDeleteDocument } from '../../hooks/api/use_delete_document'; export interface DocumentListProps { indexName: string; @@ -21,6 +22,8 @@ export interface DocumentListProps { } export const DocumentList = ({ indexName, docs, mappingProperties }: DocumentListProps) => { + const { mutate } = useDeleteDocument(indexName); + return ( <> @@ -28,7 +31,14 @@ export const DocumentList = ({ indexName, docs, mappingProperties }: DocumentLis {docs.map((doc) => { return ( - + { + mutate({ id: doc._id! }); + }} + compactCard={false} + /> ); diff --git a/x-pack/plugins/search_indices/public/components/indices/details_page.tsx b/x-pack/plugins/search_indices/public/components/indices/details_page.tsx index f12618f4a76bd..b2e1ab9f2c992 100644 --- a/x-pack/plugins/search_indices/public/components/indices/details_page.tsx +++ b/x-pack/plugins/search_indices/public/components/indices/details_page.tsx @@ -34,7 +34,6 @@ import { SearchIndexDetailsMappings } from './details_page_mappings'; import { SearchIndexDetailsSettings } from './details_page_settings'; import { SearchIndexDetailsPageMenuItemPopover } from './details_page_menu_item'; import { useIndexDocumentSearch } from '../../hooks/api/use_document_search'; -import { DEFAULT_PAGE_SIZE } from '../index_documents/constants'; export const SearchIndexDetailsPage = () => { const indexName = decodeURIComponent(useParams<{ indexName: string }>().indexName); @@ -55,10 +54,7 @@ export const SearchIndexDetailsPage = () => { error: mappingsError, } = useIndexMapping(indexName); const { data: indexDocuments, isInitialLoading: indexDocumentsIsInitialLoading } = - useIndexDocumentSearch(indexName, { - pageSize: DEFAULT_PAGE_SIZE, - pageIndex: 0, - }); + useIndexDocumentSearch(indexName); const navigateToPlayground = useCallback(async () => { const playgroundLocator = share.url.locators.get('PLAYGROUND_LOCATOR_ID'); diff --git a/x-pack/plugins/search_indices/public/constants.ts b/x-pack/plugins/search_indices/public/constants.ts index 54652954964aa..bf9cf14a4ea17 100644 --- a/x-pack/plugins/search_indices/public/constants.ts +++ b/x-pack/plugins/search_indices/public/constants.ts @@ -9,12 +9,16 @@ export enum QueryKeys { FetchIndex = 'fetchIndex', FetchSearchIndicesStatus = 'fetchSearchIndicesStatus', FetchUserStartPrivileges = 'fetchUserStartPrivileges', + SearchDocuments = 'searchDocuments', } export enum MutationKeys { SearchIndicesCreateIndex = 'searchIndicesCreateIndex', + SearchIndicesDeleteDocument = 'searchIndicesDeleteDocument', } export const ELASTICSEARCH_URL_PLACEHOLDER = 'https://your_deployment_url'; export const API_KEY_PLACEHOLDER = 'YOUR_API_KEY'; export const INDEX_PLACEHOLDER = 'my-index'; + +export const DEFAULT_DOCUMENT_PAGE_SIZE = 50; diff --git a/x-pack/plugins/search_indices/public/hooks/api/use_delete_document.ts b/x-pack/plugins/search_indices/public/hooks/api/use_delete_document.ts new file mode 100644 index 0000000000000..bbf43d684de56 --- /dev/null +++ b/x-pack/plugins/search_indices/public/hooks/api/use_delete_document.ts @@ -0,0 +1,72 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { AcknowledgedResponseBase } from '@elastic/elasticsearch/lib/api/types'; +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { SearchHit } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import { MutationKeys, QueryKeys } from '../../constants'; +import { useKibana } from '../use_kibana'; +import { INDEX_SEARCH_POLLING, IndexDocuments } from './use_document_search'; + +interface DeleteDocumentParams { + id: string; +} + +export const useDeleteDocument = (indexName: string) => { + const { http } = useKibana().services; + const queryClient = useQueryClient(); + + const result = useMutation( + async ({ id }: DeleteDocumentParams) => { + const response = await http.delete( + `/internal/search_indices/${indexName}/documents/${id}` + ); + return response.acknowledged; + }, + { + mutationKey: [MutationKeys.SearchIndicesDeleteDocument, indexName], + onMutate: async ({ id }: DeleteDocumentParams) => { + await queryClient.cancelQueries([QueryKeys.SearchDocuments, indexName]); + + const previousData = queryClient.getQueryData([ + QueryKeys.SearchDocuments, + indexName, + ]); + + queryClient.setQueryData( + [QueryKeys.SearchDocuments, indexName], + (snapshot: IndexDocuments | undefined) => { + const oldData = snapshot ?? { results: { data: [] } }; + return { + ...oldData, + results: { + ...oldData.results, + data: oldData.results.data.filter((doc: SearchHit) => doc._id !== id), + }, + } as IndexDocuments; + } + ); + + return { previousData }; + }, + + onSuccess: () => { + setTimeout(() => { + queryClient.invalidateQueries([QueryKeys.SearchDocuments, indexName]); + }, INDEX_SEARCH_POLLING); + }, + + onError: (error, _, context) => { + if (context?.previousData) { + queryClient.setQueryData([QueryKeys.SearchDocuments, indexName], context.previousData); + } + return error; + }, + } + ); + return { ...result }; +}; diff --git a/x-pack/plugins/search_indices/public/hooks/api/use_delete_index.ts b/x-pack/plugins/search_indices/public/hooks/api/use_delete_index.ts index 9e1162a6ac390..5dd2ccc579c39 100644 --- a/x-pack/plugins/search_indices/public/hooks/api/use_delete_index.ts +++ b/x-pack/plugins/search_indices/public/hooks/api/use_delete_index.ts @@ -28,8 +28,9 @@ export const useDeleteIndex = (indexName: string) => { ); return response.acknowledged; }, - onSuccess: () => { + onSettled: () => { queryClient.invalidateQueries([QueryKeys.FetchIndex, indexName]); + queryClient.invalidateQueries([QueryKeys.SearchDocuments, indexName]); }, }); return { ...result }; diff --git a/x-pack/plugins/search_indices/public/hooks/api/use_document_search.ts b/x-pack/plugins/search_indices/public/hooks/api/use_document_search.ts index c1ee54088bfe3..7a74391809f60 100644 --- a/x-pack/plugins/search_indices/public/hooks/api/use_document_search.ts +++ b/x-pack/plugins/search_indices/public/hooks/api/use_document_search.ts @@ -10,6 +10,7 @@ import { SearchHit } from '@kbn/es-types'; import { pageToPagination, Paginate } from '@kbn/search-index-documents'; import { useQuery } from '@tanstack/react-query'; import { useKibana } from '../use_kibana'; +import { QueryKeys, DEFAULT_DOCUMENT_PAGE_SIZE } from '../../constants'; export interface IndexDocuments { meta: Pagination; @@ -18,32 +19,29 @@ export interface IndexDocuments { const DEFAULT_PAGINATION = { from: 0, has_more_hits_than_total: false, - size: 10, + size: DEFAULT_DOCUMENT_PAGE_SIZE, total: 0, }; -const pollingInterval = 5 * 1000; -export const useIndexDocumentSearch = ( - indexName: string, - pagination: Omit, - searchQuery?: string -) => { +export const INDEX_SEARCH_POLLING = 5 * 1000; +export const useIndexDocumentSearch = (indexName: string) => { const { services: { http }, } = useKibana(); const response = useQuery({ - queryKey: ['fetchIndexDocuments', pagination, searchQuery], - refetchInterval: pollingInterval, + queryKey: [QueryKeys.SearchDocuments, indexName], + refetchInterval: INDEX_SEARCH_POLLING, refetchIntervalInBackground: true, refetchOnWindowFocus: 'always', - queryFn: async () => + queryFn: async ({ signal }) => http.post(`/internal/serverless_search/indices/${indexName}/search`, { body: JSON.stringify({ - searchQuery, + searchQuery: '', }), query: { - page: pagination.pageIndex, - size: pagination.pageSize, + page: 0, + size: DEFAULT_DOCUMENT_PAGE_SIZE, }, + signal, }), }); return { diff --git a/x-pack/plugins/search_indices/public/utils/errors.ts b/x-pack/plugins/search_indices/public/utils/errors.ts index 4625b2cf5240c..2e78ee6170015 100644 --- a/x-pack/plugins/search_indices/public/utils/errors.ts +++ b/x-pack/plugins/search_indices/public/utils/errors.ts @@ -11,6 +11,7 @@ export function getErrorMessage(error: unknown, defaultMessage?: string): string if (typeof error === 'string') { return error; } + if (isKibanaServerError(error)) { return error.body.message; } diff --git a/x-pack/plugins/search_indices/server/lib/documents.test.ts b/x-pack/plugins/search_indices/server/lib/documents.test.ts new file mode 100644 index 0000000000000..9b82b8757d0db --- /dev/null +++ b/x-pack/plugins/search_indices/server/lib/documents.test.ts @@ -0,0 +1,56 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { deleteDocument } from './documents'; +import { Logger } from '@kbn/logging'; +import type { ElasticsearchClient } from '@kbn/core-elasticsearch-server'; + +describe('deleteDocument', () => { + let mockClient: jest.Mocked; + let mockLogger: jest.Mocked; + + beforeEach(() => { + mockClient = { + delete: jest.fn().mockResolvedValue({}), + } as unknown as jest.Mocked; + + mockLogger = { + info: jest.fn(), + error: jest.fn(), + warn: jest.fn(), + debug: jest.fn(), + trace: jest.fn(), + fatal: jest.fn(), + log: jest.fn(), + } as unknown as jest.Mocked; + }); + + it('should delete a document and return true', async () => { + const index = 'test-index'; + const id = 'test-id'; + + const result = await deleteDocument(mockClient, mockLogger, index, id); + + expect(mockClient.delete).toHaveBeenCalledWith({ + index, + id, + }); + expect(result).toBe(true); + }); + + it('should log an error and throw when delete fails', async () => { + const index = 'test-index'; + const id = 'test-id'; + const error = new Error('Delete failed'); + + mockClient.delete.mockRejectedValue(error); + + await expect(deleteDocument(mockClient, mockLogger, index, id)).rejects.toThrow( + 'Delete failed' + ); + }); +}); diff --git a/x-pack/plugins/search_indices/server/lib/documents.ts b/x-pack/plugins/search_indices/server/lib/documents.ts new file mode 100644 index 0000000000000..efafdf464d5ef --- /dev/null +++ b/x-pack/plugins/search_indices/server/lib/documents.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 type { ElasticsearchClient } from '@kbn/core-elasticsearch-server'; +import type { Logger } from '@kbn/logging'; + +export async function deleteDocument( + client: ElasticsearchClient, + logger: Logger, + index: string, + id: string +): Promise { + await client.delete({ + index, + id, + }); + return true; +} diff --git a/x-pack/plugins/search_indices/server/routes/documents.ts b/x-pack/plugins/search_indices/server/routes/documents.ts new file mode 100644 index 0000000000000..6e6cf62f84295 --- /dev/null +++ b/x-pack/plugins/search_indices/server/routes/documents.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 { schema } from '@kbn/config-schema'; +import { i18n } from '@kbn/i18n'; +import type { IRouter } from '@kbn/core/server'; +import type { Logger } from '@kbn/logging'; + +import { INDEX_DOCUMENT_ROUTE } from '../../common/routes'; +import { deleteDocument } from '../lib/documents'; + +export function registerDocumentRoutes(router: IRouter, logger: Logger) { + router.delete( + { + path: INDEX_DOCUMENT_ROUTE, + validate: { + params: schema.object({ + indexName: schema.string(), + id: schema.string(), + }), + }, + options: { + access: 'internal', + }, + }, + async (context, request, response) => { + const core = await context.core; + const client = core.elasticsearch.client.asCurrentUser; + + const { indexName, id } = request.params; + + try { + await deleteDocument(client, logger, indexName, id); + return response.ok(); + } catch (e) { + return response.customError({ + statusCode: e?.meta && e.meta?.statusCode ? e.meta?.statusCode : 500, + body: { + message: i18n.translate('xpack.searchIndices.server.deleteDocument.errorMessage', { + defaultMessage: 'Failed to delete document', + }), + }, + }); + } + } + ); +} diff --git a/x-pack/plugins/search_indices/server/routes/index.ts b/x-pack/plugins/search_indices/server/routes/index.ts index 451647eb22774..a2e31d189affd 100644 --- a/x-pack/plugins/search_indices/server/routes/index.ts +++ b/x-pack/plugins/search_indices/server/routes/index.ts @@ -11,9 +11,11 @@ import type { Logger } from '@kbn/logging'; import { registerSearchApiKeysRoutes } from '@kbn/search-api-keys-server'; import { registerIndicesRoutes } from './indices'; import { registerStatusRoutes } from './status'; +import { registerDocumentRoutes } from './documents'; export function defineRoutes(router: IRouter, logger: Logger) { registerIndicesRoutes(router, logger); registerStatusRoutes(router, logger); registerSearchApiKeysRoutes(router, logger); + registerDocumentRoutes(router, logger); } diff --git a/x-pack/plugins/security_solution/common/api/entity_analytics/risk_engine/preview_route.gen.ts b/x-pack/plugins/security_solution/common/api/entity_analytics/risk_engine/preview_route.gen.ts index ab80ccb6e1b6e..c58d2c02d562f 100644 --- a/x-pack/plugins/security_solution/common/api/entity_analytics/risk_engine/preview_route.gen.ts +++ b/x-pack/plugins/security_solution/common/api/entity_analytics/risk_engine/preview_route.gen.ts @@ -55,6 +55,12 @@ export const RiskScoresPreviewRequest = z.object({ */ range: DateRange.optional(), weights: RiskScoreWeights.optional(), + /** + * A list of alert statuses to exclude from the risk score calculation. If unspecified, all alert statuses are included. + */ + excludeAlertStatuses: z + .array(z.enum(['open', 'closed', 'in-progress', 'acknowledged'])) + .optional(), }); export type RiskScoresPreviewResponse = z.infer; diff --git a/x-pack/plugins/security_solution/common/api/entity_analytics/risk_engine/preview_route.schema.yaml b/x-pack/plugins/security_solution/common/api/entity_analytics/risk_engine/preview_route.schema.yaml index 3d7013a30a4e4..a634a1a75975c 100644 --- a/x-pack/plugins/security_solution/common/api/entity_analytics/risk_engine/preview_route.schema.yaml +++ b/x-pack/plugins/security_solution/common/api/entity_analytics/risk_engine/preview_route.schema.yaml @@ -58,6 +58,17 @@ components: description: Defines the time period over which scores will be evaluated. If unspecified, a range of `[now, now-30d]` will be used. weights: $ref: '../common/common.schema.yaml#/components/schemas/RiskScoreWeights' + excludeAlertStatuses: + description: A list of alert statuses to exclude from the risk score calculation. If unspecified, all alert statuses are included. + type: array + items: + type: string + enum: + - open + - closed + - in-progress + - acknowledged + RiskScoresPreviewResponse: type: object diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/action.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/action.ts index 43db8f92929e8..c6b54dcb50beb 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/action.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/action.ts @@ -73,11 +73,6 @@ export interface ServerReturnedEndpointNonExistingPolicies { payload: EndpointState['nonExistingPolicies']; } -export interface ServerReturnedEndpointAgentPolicies { - type: 'serverReturnedEndpointAgentPolicies'; - payload: EndpointState['agentPolicies']; -} - export interface ServerFinishedInitialization { type: 'serverFinishedInitialization'; payload: boolean; @@ -162,7 +157,6 @@ export type EndpointAction = | AppRequestedEndpointList | ServerReturnedEndpointNonExistingPolicies | ServerReturnedAgenstWithEndpointsTotal - | ServerReturnedEndpointAgentPolicies | UserUpdatedEndpointListRefreshOptions | ServerReturnedEndpointsTotal | ServerFailedToReturnAgenstWithEndpointsTotal diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/builders.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/builders.ts index a47590023dbc3..b47583aa08b20 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/builders.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/builders.ts @@ -29,8 +29,7 @@ export const initialEndpointPageState = (): Immutable => { selectedPolicyId: undefined, policyItemsLoading: false, endpointPackageInfo: createUninitialisedResourceState(), - nonExistingPolicies: {}, - agentPolicies: {}, + nonExistingPolicies: new Set(), endpointsExist: true, patterns: [], patternsError: undefined, diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/index.test.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/index.test.ts index ea2e82ac864e1..241445e5fdd02 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/index.test.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/index.test.ts @@ -40,7 +40,7 @@ describe('EndpointList store concerns', () => { }); test('it creates default state', () => { - expect(store.getState()).toEqual({ + const expectedDefaultState: EndpointState = { hosts: [], isInitialized: false, pageSize: 10, @@ -57,8 +57,7 @@ describe('EndpointList store concerns', () => { endpointPackageInfo: { type: 'UninitialisedResourceState', }, - nonExistingPolicies: {}, - agentPolicies: {}, + nonExistingPolicies: new Set(), endpointsExist: true, patterns: [], patternsError: undefined, @@ -68,12 +67,13 @@ describe('EndpointList store concerns', () => { endpointsTotal: 0, agentsWithEndpointsTotalError: undefined, endpointsTotalError: undefined, - queryStrategyVersion: undefined, isolationRequestState: { type: 'UninitialisedResourceState', }, metadataTransformStats: createUninitialisedResourceState(), - }); + }; + + expect(store.getState()).toEqual(expectedDefaultState); }); test('it handles `serverReturnedEndpointList', () => { diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts index 56b92e4692edc..61a049d2dd99e 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts @@ -47,7 +47,12 @@ import { sendGetEndpointSecurityPackage, } from '../../../services/policies/ingest'; import type { GetPolicyListResponse } from '../../policy/types'; -import type { EndpointState, PolicyIds, TransformStats, TransformStatsResponse } from '../types'; +import type { + EndpointState, + NonExistingPolicies, + TransformStats, + TransformStatsResponse, +} from '../types'; import type { EndpointPackageInfoStateChanged } from './action'; import { endpointPackageInfo, @@ -130,26 +135,24 @@ export const endpointMiddlewareFactory: ImmutableMiddlewareFactory => { + currentNonExistingPolicies: Immutable +): Promise => { if (hosts.length === 0) { return; } // Create an array of unique policy IDs that are not yet known to be non-existing. const policyIdsToCheck = [ - ...new Set( - hosts.reduce((acc: string[], host) => { - const appliedPolicyId = host.metadata.Endpoint.policy.applied.id; - if (!currentNonExistingPolicies[appliedPolicyId]) { - acc.push(appliedPolicyId); - } - return acc; - }, []) - ), + ...hosts.reduce>((acc, host) => { + const appliedPolicyId = host.metadata.Endpoint.policy.applied.id; + if (!currentNonExistingPolicies.has(appliedPolicyId)) { + acc.add(appliedPolicyId); + } + return acc; + }, new Set()), ]; if (policyIdsToCheck.length === 0) { @@ -158,37 +161,24 @@ const getAgentAndPoliciesForEndpointsList = async ( const policiesFound = ( await sendBulkGetPackagePolicies(http, policyIdsToCheck) - ).items.reduce( - (list, packagePolicy) => { - list.packagePolicy[packagePolicy.id as string] = true; - list.agentPolicy[packagePolicy.id as string] = packagePolicy.policy_ids[0]; // TODO - - return list; - }, - { packagePolicy: {}, agentPolicy: {} } - ); + ).items.reduce((set, packagePolicy) => set.add(packagePolicy.id), new Set()); - // packagePolicy contains non-existing packagePolicy ids whereas agentPolicy contains existing agentPolicy ids - const nonExistingPackagePoliciesAndExistingAgentPolicies = policyIdsToCheck.reduce( - (list, policyId: string) => { - if (policiesFound.packagePolicy[policyId as string]) { - list.agentPolicy[policyId as string] = policiesFound.agentPolicy[policyId]; - return list; + const nonExistingPackagePolicies = policyIdsToCheck.reduce( + (set, policyId) => { + if (!policiesFound.has(policyId)) { + set.add(policyId); } - list.packagePolicy[policyId as string] = true; - return list; + + return set; }, - { packagePolicy: {}, agentPolicy: {} } + new Set() ); - if ( - Object.keys(nonExistingPackagePoliciesAndExistingAgentPolicies.packagePolicy).length === 0 && - Object.keys(nonExistingPackagePoliciesAndExistingAgentPolicies.agentPolicy).length === 0 - ) { + if (!nonExistingPackagePolicies.size) { return; } - return nonExistingPackagePoliciesAndExistingAgentPolicies; + return nonExistingPackagePolicies; }; const endpointsTotal = async (http: HttpStart): Promise => { @@ -330,7 +320,7 @@ async function endpointListMiddleware({ payload: endpointResponse, }); - dispatchIngestPolicies({ http: coreStart.http, hosts: endpointResponse.data, store }); + fetchNonExistingPolicies({ http: coreStart.http, hosts: endpointResponse.data, store }); } catch (error) { dispatch({ type: 'serverFailedToReturnEndpointList', @@ -447,7 +437,7 @@ export async function handleLoadMetadataTransformStats(http: HttpStart, store: E } } -async function dispatchIngestPolicies({ +async function fetchNonExistingPolicies({ store, hosts, http, @@ -458,21 +448,15 @@ async function dispatchIngestPolicies({ }) { const { getState, dispatch } = store; try { - const ingestPolicies = await getAgentAndPoliciesForEndpointsList( + const missingPolicies = await getNonExistingPoliciesForEndpointList( http, hosts, nonExistingPolicies(getState()) ); - if (ingestPolicies?.packagePolicy !== undefined) { + if (missingPolicies !== undefined) { dispatch({ type: 'serverReturnedEndpointNonExistingPolicies', - payload: ingestPolicies.packagePolicy, - }); - } - if (ingestPolicies?.agentPolicy !== undefined) { - dispatch({ - type: 'serverReturnedEndpointAgentPolicies', - payload: ingestPolicies.agentPolicy, + payload: missingPolicies, }); } } catch (error) { diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/reducer.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/reducer.ts index 806f66654ca6d..3471d5a373f12 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/reducer.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/reducer.ts @@ -67,18 +67,7 @@ export const endpointListReducer: StateReducer = (state = initialEndpointPageSta } else if (action.type === 'serverReturnedEndpointNonExistingPolicies') { return { ...state, - nonExistingPolicies: { - ...state.nonExistingPolicies, - ...action.payload, - }, - }; - } else if (action.type === 'serverReturnedEndpointAgentPolicies') { - return { - ...state, - agentPolicies: { - ...state.agentPolicies, - ...action.payload, - }, + nonExistingPolicies: new Set([...state.nonExistingPolicies, ...action.payload]), }; } else if (action.type === 'serverReturnedMetadataPatterns') { // handle an error case diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/selectors.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/selectors.ts index bd8cff1495be9..fea48a36493b2 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/selectors.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/selectors.ts @@ -167,13 +167,6 @@ export const nonExistingPolicies: ( state: Immutable ) => Immutable = (state) => state.nonExistingPolicies; -/** - * returns the list of known existing agent policies - */ -export const agentPolicies: ( - state: Immutable -) => Immutable = (state) => state.agentPolicies; - /** * Return boolean that indicates whether endpoints exist * @param state diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/types.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/types.ts index 4b46d772a155f..415f05c417ca8 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/types.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/types.ts @@ -48,9 +48,7 @@ export interface EndpointState { /** Endpoint package info */ endpointPackageInfo: AsyncResourceState; /** Tracks the list of policy IDs used in Host metadata that may no longer exist */ - nonExistingPolicies: PolicyIds['packagePolicy']; - /** List of Package Policy Ids mapped to an associated Fleet Parent Agent Policy Id*/ - agentPolicies: PolicyIds['agentPolicy']; + nonExistingPolicies: NonExistingPolicies; /** Tracks whether hosts exist and helps control if onboarding should be visible */ endpointsExist: boolean; /** index patterns for query bar */ @@ -79,13 +77,9 @@ export interface EndpointState { export type AgentIdsPendingActions = Map; /** - * packagePolicy contains a list of Package Policy IDs (received via Endpoint metadata policy response) mapped to a boolean whether they exist or not. - * agentPolicy contains a list of existing Package Policy Ids mapped to an associated Fleet parent Agent Config. + * Set containing Package Policy IDs which are used but do not exist anymore */ -export interface PolicyIds { - packagePolicy: Record; - agentPolicy: Record; -} +export type NonExistingPolicies = Set; /** * Query params on the host page parsed from the URL diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/components/table_row_actions.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/components/table_row_actions.tsx index 6ebf4eb4e3283..df8381539b683 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/components/table_row_actions.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/components/table_row_actions.tsx @@ -10,16 +10,16 @@ import type { EuiContextMenuPanelProps, EuiPopoverProps } from '@elastic/eui'; import { EuiButtonIcon, EuiContextMenuPanel, EuiPopover } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { ContextMenuItemNavByRouter } from '../../../../components/context_menu_with_router_support/context_menu_item_nav_by_router'; -import type { HostMetadata } from '../../../../../../common/endpoint/types'; +import type { HostInfo } from '../../../../../../common/endpoint/types'; import { useEndpointActionItems } from '../hooks'; export interface TableRowActionProps { - endpointMetadata: HostMetadata; + endpointInfo: HostInfo; } -export const TableRowActions = memo(({ endpointMetadata }) => { +export const TableRowActions = memo(({ endpointInfo }) => { const [isOpen, setIsOpen] = useState(false); - const endpointActions = useEndpointActionItems(endpointMetadata, { isEndpointList: true }); + const endpointActions = useEndpointActionItems(endpointInfo, { isEndpointList: true }); const handleCloseMenu = useCallback(() => setIsOpen(false), [setIsOpen]); const handleToggleMenu = useCallback(() => setIsOpen(!isOpen), [isOpen]); diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/actions_menu.test.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/actions_menu.test.tsx index 66123062dff92..6eb0c573dd5b9 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/actions_menu.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/actions_menu.test.tsx @@ -56,6 +56,8 @@ describe('When using the Endpoint Details Actions Menu', () => { endpointHost.metadata.host.os.name = 'Windows'; // @ts-expect-error TS2540 endpointHost.metadata.agent.version = '7.14.0'; + // @ts-expect-error TS2540 + endpointHost.policy_info = { agent: { applied: { id: '123' } } }; httpMocks.responseProvider.metadataDetails.mockReturnValue(endpointHost); }; @@ -87,7 +89,7 @@ describe('When using the Endpoint Details Actions Menu', () => { }); render = async () => { - renderResult = mockedContext.render(); + renderResult = mockedContext.render(); const endpointDetailsActionsButton = renderResult.getByTestId('endpointDetailsActionsButton'); endpointDetailsActionsButton.style.pointerEvents = 'all'; await user.click(endpointDetailsActionsButton); @@ -129,10 +131,6 @@ describe('When using the Endpoint Details Actions Menu', () => { 'should navigate via kibana `navigateToApp()` when %s is clicked', async (_, dataTestSubj) => { await render(); - // TODO middlewareSpy.waitForAction() times out after the upgrade to userEvent v14 https://github.com/elastic/kibana/pull/189949 - // await act(async () => { - // await middlewareSpy.waitForAction('serverReturnedEndpointAgentPolicies'); - // }); const takeActionMenuItem = renderResult.getByTestId(dataTestSubj); takeActionMenuItem.style.pointerEvents = 'all'; diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/actions_menu.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/actions_menu.tsx index dd0c16a5f5651..bdd2e626d244a 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/actions_menu.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/actions_menu.tsx @@ -8,12 +8,12 @@ import React, { memo, useCallback, useMemo, useState } from 'react'; import { EuiButton, EuiContextMenuPanel, EuiPopover } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; -import type { HostMetadata } from '../../../../../../../common/endpoint/types'; +import type { HostInfo } from '../../../../../../../common/endpoint/types'; import { useEndpointActionItems } from '../../hooks'; import { ContextMenuItemNavByRouter } from '../../../../../components/context_menu_with_router_support/context_menu_item_nav_by_router'; -export const ActionsMenu = memo<{ hostMetadata: HostMetadata }>(({ hostMetadata }) => { - const menuOptions = useEndpointActionItems(hostMetadata); +export const ActionsMenu = memo<{ hostInfo: HostInfo }>(({ hostInfo }) => { + const menuOptions = useEndpointActionItems(hostInfo); const [isPopoverOpen, setIsPopoverOpen] = useState(false); const closePopoverHandler = useCallback(() => { diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/endpoint_details.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/endpoint_details.tsx index d6ed904b0f24a..dca25055c1b7e 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/endpoint_details.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/endpoint_details.tsx @@ -141,7 +141,7 @@ export const EndpointDetails = memo(() => { {showFlyoutFooter && ( - + )} diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/endpoint_details_content.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/endpoint_details_content.tsx index 454b85ad32238..7f458953022c5 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/endpoint_details_content.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/endpoint_details_content.tsx @@ -116,7 +116,7 @@ export const EndpointDetailsContent = memo( policyId={hostInfo.metadata.Endpoint.policy.applied.id} revision={hostInfo.metadata.Endpoint.policy.applied.endpoint_policy_version} isOutdated={isPolicyOutOfDate(hostInfo.metadata.Endpoint.policy.applied, policyInfo)} - policyExists={!missingPolicies[hostInfo.metadata.Endpoint.policy.applied.id]} + policyExists={!missingPolicies.has(hostInfo.metadata.Endpoint.policy.applied.id)} data-test-subj="policyDetailsValue" > {hostInfo.metadata.Endpoint.policy.applied.name} diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/hooks/use_endpoint_action_items.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/hooks/use_endpoint_action_items.tsx index 9773e8f7eafb6..bb6102e8051c4 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/hooks/use_endpoint_action_items.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/hooks/use_endpoint_action_items.tsx @@ -13,9 +13,9 @@ import { useUserPrivileges } from '../../../../../common/components/user_privile import { useWithShowResponder } from '../../../../hooks'; import { APP_UI_ID } from '../../../../../../common/constants'; import { getEndpointDetailsPath, getEndpointListPath } from '../../../../common/routing'; -import type { HostMetadata, MaybeImmutable } from '../../../../../../common/endpoint/types'; +import type { HostInfo, MaybeImmutable } from '../../../../../../common/endpoint/types'; import { useEndpointSelector } from './hooks'; -import { agentPolicies, uiQueryParams } from '../../store/selectors'; +import { uiQueryParams } from '../../store/selectors'; import { useAppUrl } from '../../../../../common/lib/kibana/hooks'; import type { ContextMenuItemNavByRouterProps } from '../../../../components/context_menu_with_router_support/context_menu_item_nav_by_router'; import { isEndpointHostIsolated } from '../../../../../common/utils/validators'; @@ -27,15 +27,14 @@ interface Options { /** * Returns a list (array) of actions for an individual endpoint - * @param endpointMetadata + * @param endpointInfo * @param options */ export const useEndpointActionItems = ( - endpointMetadata: MaybeImmutable | undefined, + endpointInfo: MaybeImmutable | undefined, options?: Options ): ContextMenuItemNavByRouterProps[] => { const { getAppUrl } = useAppUrl(); - const fleetAgentPolicies = useEndpointSelector(agentPolicies); const allCurrentUrlParams = useEndpointSelector(uiQueryParams); const showEndpointResponseActionsConsole = useWithShowResponder(); const { @@ -49,13 +48,14 @@ export const useEndpointActionItems = ( } = useUserPrivileges().endpointPrivileges; return useMemo(() => { - if (!endpointMetadata) { + if (!endpointInfo) { return []; } + const endpointAgentPolicyId = endpointInfo.policy_info?.agent.applied.id; + const endpointMetadata = endpointInfo.metadata; const isIsolated = isEndpointHostIsolated(endpointMetadata); const endpointId = endpointMetadata.agent.id; - const endpointPolicyId = endpointMetadata.Endpoint.policy.applied.id; const endpointHostName = endpointMetadata.host.hostname; const fleetAgentId = endpointMetadata.elastic.agent.id; const { show, selected_endpoint: _selectedEndpoint, ...currentUrlParams } = allCurrentUrlParams; @@ -182,19 +182,23 @@ export const useEndpointActionItems = ( key: 'agentConfigLink', 'data-test-subj': 'agentPolicyLink', navigateAppId: 'fleet', - navigateOptions: { - path: `${ - pagePathGetters.policy_details({ - policyId: fleetAgentPolicies[endpointPolicyId], - })[1] - }`, - }, - href: `${getAppUrl({ appId: 'fleet' })}${ - pagePathGetters.policy_details({ - policyId: fleetAgentPolicies[endpointPolicyId], - })[1] - }`, - disabled: fleetAgentPolicies[endpointPolicyId] === undefined, + ...(endpointAgentPolicyId + ? { + navigateOptions: { + path: `${ + pagePathGetters.policy_details({ + policyId: endpointAgentPolicyId, + })[1] + }`, + }, + href: `${getAppUrl({ appId: 'fleet' })}${ + pagePathGetters.policy_details({ + policyId: endpointAgentPolicyId, + })[1] + }`, + } + : {}), + disabled: endpointAgentPolicyId === undefined, children: ( { const generator = new EndpointDocGenerator('seed'); let hostInfo: HostInfo[]; let agentId: string; - let agentPolicyId: string; + let agentPolicies: AgentPolicy[]; let endpointActionsButton: HTMLElement; // 2nd endpoint only has isolation capabilities const mockEndpointListApi = () => { + agentPolicies = [generator.generateAgentPolicy(), generator.generateAgentPolicy()]; + const { data: hosts } = mockEndpointResultList({ total: 2 }); hostInfo = [ { @@ -1180,6 +1183,13 @@ describe('when on the endpoint list page', () => { }, }, last_checkin: hosts[0].last_checkin, + policy_info: { + agent: { + applied: { id: agentPolicies[1].id, revision: 13 }, // host is assigned to the 2nd agent policy + configured: { id: 'dont-care', revision: 39 }, + }, + endpoint: { id: 'dont-care', revision: 3 }, + }, }, { host_status: hosts[1].host_status, @@ -1212,15 +1222,13 @@ describe('when on the endpoint list page', () => { const packagePolicy = docGenerator.generatePolicyPackagePolicy(); packagePolicy.id = hosts[0].metadata.Endpoint.policy.applied.id; - const agentPolicy = generator.generateAgentPolicy(); - agentPolicyId = agentPolicy.id; agentId = hosts[0].metadata.elastic.agent.id; - packagePolicy.policy_ids = [agentPolicyId]; + packagePolicy.policy_ids = [agentPolicies[0].id, agentPolicies[1].id]; // package is assigned to two agent policies setEndpointListApiMockImplementation(coreStart.http, { endpointsResults: hostInfo, endpointPackagePolicies: [packagePolicy], - agentPolicy, + agentPolicy: agentPolicies[0], }); }; @@ -1234,7 +1242,6 @@ describe('when on the endpoint list page', () => { render(); await middlewareSpy.waitForAction('serverReturnedEndpointList'); - await middlewareSpy.waitForAction('serverReturnedEndpointAgentPolicies'); endpointActionsButton = (await renderResult.findAllByTestId('endpointTableRowActions'))[0]; @@ -1302,9 +1309,11 @@ describe('when on the endpoint list page', () => { `${APP_PATH}/hosts/${hostInfo[0].metadata.host.hostname}` ); }); - it('navigates to the Ingest Agent Policy page', async () => { + it('navigates to the correct Ingest Agent Policy page', async () => { const agentPolicyLink = await renderResult.findByTestId('agentPolicyLink'); - expect(agentPolicyLink.getAttribute('href')).toEqual(`/app/fleet/policies/${agentPolicyId}`); + expect(agentPolicyLink.getAttribute('href')).toEqual( + `/app/fleet/policies/${agentPolicies[1].id}` + ); }); it('navigates to the Ingest Agent Details page', async () => { const agentDetailsLink = await renderResult.findByTestId('agentDetailsLink'); @@ -1482,7 +1491,6 @@ describe('when on the endpoint list page', () => { render(); await middlewareSpy.waitForAction('serverReturnedEndpointList'); - await middlewareSpy.waitForAction('serverReturnedEndpointAgentPolicies'); const endpointActionsButton: HTMLElement = ( await renderResult.findAllByTestId('endpointTableRowActions') diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.tsx index 162d05f54ec21..945ae41237416 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.tsx @@ -180,7 +180,7 @@ const getEndpointListColumns = ({ policyId={policy.id} revision={policy.endpoint_policy_version} isOutdated={isPolicyOutOfDate(policy, item.policy_info)} - policyExists={!missingPolicies[policy.id]} + policyExists={!missingPolicies.has(policy.id)} data-test-subj="policyNameCellLink" backLink={backToEndpointList} > @@ -301,7 +301,7 @@ const getEndpointListColumns = ({ actions: [ { render: (item: HostInfo) => { - return ; + return ; }, }, ], diff --git a/x-pack/plugins/security_solution/public/management/services/policies/hooks.ts b/x-pack/plugins/security_solution/public/management/services/policies/hooks.ts index 0e823c985c696..3a29063c2d14f 100644 --- a/x-pack/plugins/security_solution/public/management/services/policies/hooks.ts +++ b/x-pack/plugins/security_solution/public/management/services/policies/hooks.ts @@ -98,6 +98,10 @@ export function useBulkGetAgentPolicies({ ['agentPolicies', policyIds], async () => { + if (!policyIds.length) { + return []; + } + return (await sendBulkGetAgentPolicies({ http, requestBody: { ids: policyIds } }))?.items; }, diff --git a/x-pack/plugins/security_solution/scripts/run_cypress/parallel.ts b/x-pack/plugins/security_solution/scripts/run_cypress/parallel.ts index 5a6f9e2aa78de..85cbbbe0eb7fd 100644 --- a/x-pack/plugins/security_solution/scripts/run_cypress/parallel.ts +++ b/x-pack/plugins/security_solution/scripts/run_cypress/parallel.ts @@ -54,6 +54,8 @@ export const cli = () => { ) .boolean('inspect'); + const USE_CHROME_BETA = process.env.USE_CHROME_BETA?.match(/(1|true)/i); + _cliLogger.info(` ---------------------------------------------- Script arguments: @@ -438,7 +440,7 @@ ${JSON.stringify(cyCustomEnv, null, 2)} }); } else { result = await cypress.run({ - browser: 'chrome', + browser: USE_CHROME_BETA ? 'chrome:beta' : 'chrome', spec: filePath, configFile: cypressConfigFilePath, reporter: argv.reporter as string, diff --git a/x-pack/plugins/security_solution/scripts/run_cypress/parallel_serverless.ts b/x-pack/plugins/security_solution/scripts/run_cypress/parallel_serverless.ts index 1ebdd2b2afbd3..8e680e8ebc451 100644 --- a/x-pack/plugins/security_solution/scripts/run_cypress/parallel_serverless.ts +++ b/x-pack/plugins/security_solution/scripts/run_cypress/parallel_serverless.ts @@ -261,6 +261,7 @@ export const cli = () => { const PROXY_URL = process.env.PROXY_URL ? process.env.PROXY_URL : undefined; const PROXY_SECRET = process.env.PROXY_SECRET ? process.env.PROXY_SECRET : undefined; const PROXY_CLIENT_ID = process.env.PROXY_CLIENT_ID ? process.env.PROXY_CLIENT_ID : undefined; + const USE_CHROME_BETA = process.env.USE_CHROME_BETA?.match(/(1|true)/i); const API_KEY = process.env.CLOUD_QA_API_KEY ? process.env.CLOUD_QA_API_KEY @@ -270,6 +271,7 @@ export const cli = () => { log.info(`PROXY_CLIENT_ID is defined : ${PROXY_CLIENT_ID !== undefined}`); log.info(`PROXY_SECRET is defined : ${PROXY_SECRET !== undefined}`); log.info(`API_KEY is defined : ${API_KEY !== undefined}`); + log.info(`USE_CHROME_BETA is defined : ${USE_CHROME_BETA !== undefined}`); let cloudHandler: ProjectHandler; if (PROXY_URL && PROXY_CLIENT_ID && PROXY_SECRET && (await proxyHealthcheck(PROXY_URL))) { @@ -555,7 +557,7 @@ ${JSON.stringify(cypressConfigFile, null, 2)} } else { try { result = await cypress.run({ - browser: 'chrome', + browser: USE_CHROME_BETA ? 'chrome:beta' : 'chrome', spec: filePath, configFile: cypressConfigFilePath, reporter: argv.reporter as string, diff --git a/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_engine/saved_object/risk_engine_configuration_type.ts b/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_engine/saved_object/risk_engine_configuration_type.ts index 572c1b7ebef83..6c9b589dba511 100644 --- a/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_engine/saved_object/risk_engine_configuration_type.ts +++ b/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_engine/saved_object/risk_engine_configuration_type.ts @@ -45,6 +45,9 @@ export const riskEngineConfigurationTypeMappings: SavedObjectsType['mappings'] = }, }, }, + excludeAlertStatuses: { + type: 'keyword', + }, }, }; @@ -59,6 +62,28 @@ const version1: SavedObjectsModelVersion = { ], }; +const version2: SavedObjectsModelVersion = { + changes: [ + { + type: 'mappings_addition', + addedMappings: { + excludeAlertStatuses: { type: 'keyword' }, + }, + }, + { + type: 'data_backfill', + backfillFn: (document) => { + return { + attributes: { + ...document.attributes, + excludeAlertStatuses: document.attributes.excludeAlertStatuses || ['closed'], + }, + }; + }, + }, + ], +}; + export const riskEngineConfigurationType: SavedObjectsType = { name: riskEngineConfigurationTypeName, indexPattern: SECURITY_SOLUTION_SAVED_OBJECT_INDEX, @@ -67,5 +92,6 @@ export const riskEngineConfigurationType: SavedObjectsType = { mappings: riskEngineConfigurationTypeMappings, modelVersions: { 1: version1, + 2: version2, }, }; diff --git a/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_score/calculate_risk_scores.test.ts b/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_score/calculate_risk_scores.test.ts index 47443510df6db..77d2bacfd5a58 100644 --- a/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_score/calculate_risk_scores.test.ts +++ b/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_score/calculate_risk_scores.test.ts @@ -12,6 +12,8 @@ import { assetCriticalityServiceMock } from '../asset_criticality/asset_critical import { calculateRiskScores } from './calculate_risk_scores'; import { calculateRiskScoresMock } from './calculate_risk_scores.mock'; +import { ALERT_WORKFLOW_STATUS } from '@kbn/rule-registry-plugin/common/technical_rule_data_field_names'; + describe('calculateRiskScores()', () => { let params: Parameters[0]; let esClient: ElasticsearchClient; @@ -146,6 +148,41 @@ describe('calculateRiskScores()', () => { ); }); }); + + describe('excludeAlertStatuses', () => { + it('should not add the filter when excludeAlertStatuses is empty', async () => { + params = { ...params, excludeAlertStatuses: [] }; + await calculateRiskScores(params); + expect( + (esClient.search as jest.Mock).mock.calls[0][0].query.function_score.query.bool.filter + ).toEqual( + expect.not.arrayContaining([ + { + bool: { + must_not: { terms: { [ALERT_WORKFLOW_STATUS]: params.excludeAlertStatuses } }, + }, + }, + ]) + ); + }); + + it('should add the filter when excludeAlertStatuses is not empty', async () => { + esClient.search as jest.Mock; + params = { ...params, excludeAlertStatuses: ['closed'] }; + await calculateRiskScores(params); + expect( + (esClient.search as jest.Mock).mock.calls[0][0].query.function_score.query.bool.filter + ).toEqual( + expect.arrayContaining([ + { + bool: { + must_not: { terms: { [ALERT_WORKFLOW_STATUS]: params.excludeAlertStatuses } }, + }, + }, + ]) + ); + }); + }); }); describe('outputs', () => { diff --git a/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_score/calculate_risk_scores.ts b/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_score/calculate_risk_scores.ts index 1dd03f553fede..45ad1241fda33 100644 --- a/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_score/calculate_risk_scores.ts +++ b/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_score/calculate_risk_scores.ts @@ -96,7 +96,7 @@ const formatForResponse = ({ }; }; -const filterFromRange = (range: CalculateScoresParams['range']): QueryDslQueryContainer => ({ +export const filterFromRange = (range: CalculateScoresParams['range']): QueryDslQueryContainer => ({ range: { '@timestamp': { lt: range.end, gte: range.start } }, }); @@ -225,6 +225,7 @@ export const calculateRiskScores = async ({ runtimeMappings, weights, alertSampleSizePerShard = 10_000, + excludeAlertStatuses = [], }: { assetCriticalityService: AssetCriticalityService; esClient: ElasticsearchClient; @@ -233,11 +234,12 @@ export const calculateRiskScores = async ({ withSecuritySpan('calculateRiskScores', async () => { const now = new Date().toISOString(); const scriptedMetricPainless = await getPainlessScripts(); - const filter = [ - filterFromRange(range), - { bool: { must_not: { term: { [ALERT_WORKFLOW_STATUS]: 'closed' } } } }, - { exists: { field: ALERT_RISK_SCORE } }, - ]; + const filter = [filterFromRange(range), { exists: { field: ALERT_RISK_SCORE } }]; + if (excludeAlertStatuses.length > 0) { + filter.push({ + bool: { must_not: { terms: { [ALERT_WORKFLOW_STATUS]: excludeAlertStatuses } } }, + }); + } if (!isEmpty(userFilter)) { filter.push(userFilter as QueryDslQueryContainer); } diff --git a/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_score/routes/preview.ts b/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_score/routes/preview.ts index ae265d415288a..e2a1e664c5e76 100644 --- a/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_score/routes/preview.ts +++ b/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_score/routes/preview.ts @@ -63,6 +63,7 @@ export const riskScorePreviewRoute = ( filter, range: userRange, weights, + excludeAlertStatuses, } = request.body; const entityAnalyticsConfig = await riskScoreService.getConfigurationWithDefaults( @@ -93,6 +94,7 @@ export const riskScorePreviewRoute = ( runtimeMappings, weights, alertSampleSizePerShard, + excludeAlertStatuses, }); securityContext.getAuditLogger()?.log({ diff --git a/x-pack/plugins/security_solution/server/lib/entity_analytics/types.ts b/x-pack/plugins/security_solution/server/lib/entity_analytics/types.ts index dfc4e45187e05..af683db517716 100644 --- a/x-pack/plugins/security_solution/server/lib/entity_analytics/types.ts +++ b/x-pack/plugins/security_solution/server/lib/entity_analytics/types.ts @@ -85,6 +85,7 @@ export interface CalculateScoresParams { runtimeMappings: MappingRuntimeFields; weights?: RiskScoreWeights; alertSampleSizePerShard?: number; + excludeAlertStatuses?: string[]; } export interface CalculateAndPersistScoresParams { diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/advanced_pivot_editor/advanced_pivot_editor.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/advanced_pivot_editor/advanced_pivot_editor.tsx index e0379ea26366a..f07f12be724f6 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/advanced_pivot_editor/advanced_pivot_editor.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/advanced_pivot_editor/advanced_pivot_editor.tsx @@ -18,7 +18,7 @@ import type { StepDefineFormHook } from '../step_define'; export const AdvancedPivotEditor: FC = memo( ({ actions: { convertToJson, setAdvancedEditorConfig, setAdvancedPivotEditorApplyButtonEnabled }, - state: { advancedEditorConfigLastApplied, advancedEditorConfig, xJsonMode }, + state: { advancedEditorConfigLastApplied, advancedEditorConfig }, }) => { return ( { return (
diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/hooks/use_advanced_pivot_editor.ts b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/hooks/use_advanced_pivot_editor.ts index 7e7c9470704c2..ecdac54c2c790 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/hooks/use_advanced_pivot_editor.ts +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/hooks/use_advanced_pivot_editor.ts @@ -6,7 +6,6 @@ */ import { useEffect, useState } from 'react'; -import { XJsonMode } from '@kbn/ace'; import { XJson } from '@kbn/es-ui-shared-plugin/public'; @@ -15,7 +14,6 @@ import type { PostTransformsPreviewRequestSchema } from '../../../../../../../se import type { StepDefineExposedState } from '../common'; const { useXJsonMode } = XJson; -const xJsonMode = new XJsonMode(); export const useAdvancedPivotEditor = ( defaults: StepDefineExposedState, @@ -71,7 +69,6 @@ export const useAdvancedPivotEditor = ( isAdvancedEditorSwitchModalVisible, isAdvancedPivotEditorApplyButtonEnabled, isAdvancedPivotEditorEnabled, - xJsonMode, }, }; }; diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/hooks/use_advanced_runtime_mappings_editor.ts b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/hooks/use_advanced_runtime_mappings_editor.ts index a4043fa4d81fb..62f19f720859e 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/hooks/use_advanced_runtime_mappings_editor.ts +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/hooks/use_advanced_runtime_mappings_editor.ts @@ -6,12 +6,10 @@ */ import { useState } from 'react'; -import { XJsonMode } from '@kbn/ace'; import { XJson } from '@kbn/es-ui-shared-plugin/public'; import type { StepDefineExposedState } from '../common'; const { useXJsonMode } = XJson; -const xJsonMode = new XJsonMode(); export const useAdvancedRuntimeMappingsEditor = (defaults: StepDefineExposedState) => { const stringifiedRuntimeMappings = JSON.stringify(defaults.runtimeMappings, null, 2); @@ -81,7 +79,6 @@ export const useAdvancedRuntimeMappingsEditor = (defaults: StepDefineExposedStat isRuntimeMappingsEditorSwitchModalVisible, runtimeMappingsUpdated, advancedRuntimeMappingsConfig, - xJsonMode, runtimeMappings, }, }; diff --git a/x-pack/plugins/transform/tsconfig.json b/x-pack/plugins/transform/tsconfig.json index c04d45922aa40..b1f43e3f096a0 100644 --- a/x-pack/plugins/transform/tsconfig.json +++ b/x-pack/plugins/transform/tsconfig.json @@ -29,7 +29,6 @@ "@kbn/triggers-actions-ui-plugin", "@kbn/i18n-react", "@kbn/kibana-react-plugin", - "@kbn/ace", "@kbn/es-ui-shared-plugin", "@kbn/discover-plugin", "@kbn/kibana-utils-plugin", diff --git a/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/risk_engine/trial_license_complete_tier/risk_score_preview.ts b/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/risk_engine/trial_license_complete_tier/risk_score_preview.ts index e1fcf5cc69ead..fbe2ca5cfe210 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/risk_engine/trial_license_complete_tier/risk_score_preview.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/risk_engine/trial_license_complete_tier/risk_score_preview.ts @@ -263,6 +263,60 @@ export default ({ getService }: FtrProviderContext): void => { }, ]); }); + + it('calculates risk from 5 alerts, all in closed state, all for the same host', async () => { + const documentId = uuidv4(); + const doc = buildDocument( + { host: { name: 'host-1' }, kibana: { alert: { workflow_status: 'closed' } } }, + documentId + ); + await indexListOfDocuments(Array(10).fill(doc)); + + const body = await getRiskScoreAfterRuleCreationAndExecution(documentId, { + alerts: 5, + }); + + expect(sanitizeScores(body.scores.host!)).to.eql([ + { + calculated_level: 'Unknown', + calculated_score: 41.90206636025764, + calculated_score_norm: 16.163426307767953, + category_1_count: 10, + category_1_score: 16.163426307767953, + id_field: 'host.name', + id_value: 'host-1', + }, + ]); + }); + it('calculates risk from 10 alerts, some in closed state, some in open state, all for the same host', async () => { + const documentId = uuidv4(); + const docStatusClosed = buildDocument( + { host: { name: 'host-1' }, kibana: { alert: { workflow_status: 'closed' } } }, + documentId + ); + const docStatusOpen = buildDocument( + { host: { name: 'host-1' }, kibana: { alert: { workflow_status: 'open' } } }, + documentId + ); + await indexListOfDocuments(Array(5).fill(docStatusClosed)); + await indexListOfDocuments(Array(5).fill(docStatusOpen)); + + const body = await getRiskScoreAfterRuleCreationAndExecution(documentId, { + alerts: 10, + }); + + expect(sanitizeScores(body.scores.host!)).to.eql([ + { + calculated_level: 'Unknown', + calculated_score: 41.90206636025764, + calculated_score_norm: 16.163426307767953, + category_1_count: 10, + category_1_score: 16.163426307767953, + id_field: 'host.name', + id_value: 'host-1', + }, + ]); + }); }); context('with a rule generating alerts with risk_score of 100', () => { diff --git a/x-pack/test_serverless/functional/page_objects/svl_search_index_detail_page.ts b/x-pack/test_serverless/functional/page_objects/svl_search_index_detail_page.ts index d3d459f6225dd..d69d9b8bfcfca 100644 --- a/x-pack/test_serverless/functional/page_objects/svl_search_index_detail_page.ts +++ b/x-pack/test_serverless/functional/page_objects/svl_search_index_detail_page.ts @@ -180,5 +180,18 @@ export function SvlSearchIndexDetailPageProvider({ getService }: FtrProviderCont apiKey ); }, + + async clickFirstDocumentDeleteAction() { + await testSubjects.existOrFail('documentMetadataButton'); + await testSubjects.click('documentMetadataButton'); + await testSubjects.existOrFail('deleteDocumentButton'); + await testSubjects.click('deleteDocumentButton'); + }, + + async expectDeleteDocumentActionNotVisible() { + await testSubjects.existOrFail('documentMetadataButton'); + await testSubjects.click('documentMetadataButton'); + await testSubjects.missingOrFail('deleteDocumentButton'); + }, }; } diff --git a/x-pack/test_serverless/functional/test_suites/search/search_index_detail.ts b/x-pack/test_serverless/functional/test_suites/search/search_index_detail.ts index 2f190cfdf7eef..db855e9522f9a 100644 --- a/x-pack/test_serverless/functional/test_suites/search/search_index_detail.ts +++ b/x-pack/test_serverless/functional/test_suites/search/search_index_detail.ts @@ -97,7 +97,6 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { }); describe('With data', () => { before(async () => { - await svlSearchNavigation.navigateToIndexDetailPage(indexName); await es.index({ index: indexName, body: { @@ -126,6 +125,11 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await pageObjects.svlSearchIndexDetailPage.expectUrlShouldChangeTo('settings'); await pageObjects.svlSearchIndexDetailPage.expectSettingsComponentIsVisible(); }); + it('should be able to delete document', async () => { + await pageObjects.svlSearchIndexDetailPage.withDataChangeTabs('dataTab'); + await pageObjects.svlSearchIndexDetailPage.clickFirstDocumentDeleteAction(); + await pageObjects.svlSearchIndexDetailPage.expectAddDocumentCodeExamples(); + }); }); describe('page loading error', () => {