diff --git a/.ci/pipeline-library/src/test/prChanges.groovy b/.ci/pipeline-library/src/test/prChanges.groovy index 0f354e768724..e3f82e6102ac 100644 --- a/.ci/pipeline-library/src/test/prChanges.groovy +++ b/.ci/pipeline-library/src/test/prChanges.groovy @@ -90,7 +90,7 @@ class PrChangesTest extends KibanaBasePipelineTest { props([ githubPrs: [ getChanges: { [ - [filename: 'docs/developer/architecture/code-exploration.asciidoc'], + [filename: 'docs/developer/plugin-list.asciidoc'], ] }, ], ]) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 52df586b8bda..66fb31cc91d5 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -8,7 +8,6 @@ /x-pack/plugins/lens/ @elastic/kibana-app /x-pack/plugins/graph/ @elastic/kibana-app /src/plugins/dashboard/ @elastic/kibana-app -/src/plugins/dashboard/**/*.scss @elastic/kibana-core-ui-designers /src/plugins/discover/ @elastic/kibana-app /src/plugins/input_control_vis/ @elastic/kibana-app /src/plugins/kibana_legacy/ @elastic/kibana-app @@ -59,7 +58,6 @@ # APM /x-pack/plugins/apm/ @elastic/apm-ui -/x-pack/plugins/apm/**/*.scss @elastic/observability-design /x-pack/test/functional/apps/apm/ @elastic/apm-ui /src/legacy/core_plugins/apm_oss/ @elastic/apm-ui /src/plugins/apm_oss/ @elastic/apm-ui @@ -70,7 +68,6 @@ # Canvas /x-pack/plugins/canvas/ @elastic/kibana-canvas -/x-pack/plugins/canvas/**/*.scss @elastic/kibana-core-ui-designers /x-pack/test/functional/apps/canvas/ @elastic/kibana-canvas # Core UI @@ -80,18 +77,14 @@ /src/plugins/home/server/services/ @elastic/kibana-core-ui # Exclude tutorial resources folder for now because they are not owned by Kibana app and most will move out soon /src/legacy/core_plugins/kibana/public/home/*.ts @elastic/kibana-core-ui -/src/legacy/core_plugins/kibana/public/home/**/*.scss @elastic/kibana-core-ui-designers /src/legacy/core_plugins/kibana/public/home/np_ready/ @elastic/kibana-core-ui # Observability UIs /x-pack/legacy/plugins/infra/ @elastic/logs-metrics-ui /x-pack/plugins/infra/ @elastic/logs-metrics-ui -/x-pack/plugins/infra/**/*.scss @elastic/observability-design /x-pack/plugins/ingest_manager/ @elastic/ingest-management -/x-pack/plugins/ingest_manager/**/*.scss @elastic/observability-design /x-pack/legacy/plugins/ingest_manager/ @elastic/ingest-management /x-pack/plugins/observability/ @elastic/observability-ui -/x-pack/plugins/observability/**/*.scss @elastic/observability-design /x-pack/legacy/plugins/monitoring/ @elastic/stack-monitoring-ui /x-pack/plugins/monitoring/ @elastic/stack-monitoring-ui /x-pack/plugins/uptime @elastic/uptime @@ -165,14 +158,10 @@ # Security /src/core/server/csp/ @elastic/kibana-security @elastic/kibana-platform /x-pack/legacy/plugins/security/ @elastic/kibana-security -/x-pack/legacy/plugins/security/**/*.scss @elastic/kibana-core-ui-designers /x-pack/legacy/plugins/spaces/ @elastic/kibana-security -/x-pack/legacy/plugins/spaces/**/*.scss @elastic/kibana-core-ui-designers /x-pack/plugins/spaces/ @elastic/kibana-security -/x-pack/plugins/spaces/**/*.scss @elastic/kibana-core-ui-designers /x-pack/plugins/encrypted_saved_objects/ @elastic/kibana-security /x-pack/plugins/security/ @elastic/kibana-security -/x-pack/plugins/security/**/*.scss @elastic/kibana-core-ui-designers /x-pack/test/api_integration/apis/security/ @elastic/kibana-security /x-pack/test/encrypted_saved_objects_api_integration/ @elastic/kibana-security /x-pack/test/functional/apps/security/ @elastic/kibana-security @@ -220,13 +209,9 @@ x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json @elastic/kib /x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/ @elastic/kibana-alerting-services /x-pack/test/functional_with_es_ssl/fixtures/plugins/alerts/ @elastic/kibana-alerting-services -# Design -**/*.scss @elastic/kibana-design - # Enterprise Search /x-pack/plugins/enterprise_search/ @elastic/app-search-frontend @elastic/workplace-search-frontend /x-pack/test/functional_enterprise_search/ @elastic/app-search-frontend @elastic/workplace-search-frontend -/x-pack/plugins/enterprise_search/**/*.scss @elastic/ent-search-design # Elasticsearch UI /src/plugins/dev_tools/ @elastic/es-ui @@ -255,7 +240,6 @@ x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json @elastic/kib # Endpoint /x-pack/plugins/endpoint/ @elastic/endpoint-app-team @elastic/siem -/x-pack/plugins/endpoint/**/*.scss @elastic/security-design /x-pack/test/api_integration/apis/endpoint/ @elastic/endpoint-app-team @elastic/siem /x-pack/test/endpoint_api_integration_no_ingest/ @elastic/endpoint-app-team @elastic/siem /x-pack/test/security_solution_endpoint/ @elastic/endpoint-app-team @elastic/siem @@ -265,7 +249,6 @@ x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json @elastic/kib # Security Solution /x-pack/plugins/security_solution/ @elastic/siem @elastic/endpoint-app-team -/x-pack/plugins/security_solution/**/*.scss @elastic/security-design /x-pack/test/detection_engine_api_integration @elastic/siem @elastic/endpoint-app-team /x-pack/test/lists_api_integration @elastic/siem @elastic/endpoint-app-team /x-pack/test/api_integration/apis/security_solution @elastic/siem @elastic/endpoint-app-team @@ -274,3 +257,29 @@ x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json @elastic/kib # Security Intelligence And Analytics /x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules @elastic/security-intelligence-analytics + +# Design (at the bottom for specificity of SASS files) +**/*.scss @elastic/kibana-design + +# Core design +/src/plugins/dashboard/**/*.scss @elastic/kibana-core-ui-designers +/x-pack/plugins/canvas/**/*.scss @elastic/kibana-core-ui-designers +/src/legacy/core_plugins/kibana/public/home/**/*.scss @elastic/kibana-core-ui-designers +/x-pack/legacy/plugins/security/**/*.scss @elastic/kibana-core-ui-designers +/x-pack/legacy/plugins/spaces/**/*.scss @elastic/kibana-core-ui-designers +/x-pack/plugins/spaces/**/*.scss @elastic/kibana-core-ui-designers +/x-pack/plugins/security/**/*.scss @elastic/kibana-core-ui-designers + +# Observability design +/x-pack/plugins/apm/**/*.scss @elastic/observability-design +/x-pack/plugins/infra/**/*.scss @elastic/observability-design +/x-pack/plugins/ingest_manager/**/*.scss @elastic/observability-design +/x-pack/plugins/observability/**/*.scss @elastic/observability-design + +# Ent. Search design +/x-pack/plugins/enterprise_search/**/*.scss @elastic/ent-search-design + +# Security design +/x-pack/plugins/endpoint/**/*.scss @elastic/security-design +/x-pack/plugins/security_solution/**/*.scss @elastic/security-design + diff --git a/docs/developer/architecture/code-exploration.asciidoc b/docs/developer/architecture/code-exploration.asciidoc deleted file mode 100644 index 6e814921d3f3..000000000000 --- a/docs/developer/architecture/code-exploration.asciidoc +++ /dev/null @@ -1,593 +0,0 @@ -//// - -NOTE: - This is an automatically generated file. Please do not edit directly. Instead, run the - following from within the kibana repository: - - node scripts/build_plugin_list_docs - - You can update the template within packages/kbn-dev-utils/target/plugin_list/generate_plugin_list.js - -//// - -[[code-exploration]] -== Exploring Kibana code - -The goals of our folder heirarchy are: - -- Easy for developers to know where to add new services, plugins and applications. -- Easy for developers to know where to find the code from services, plugins and applications. -- Easy to browse and understand our folder structure. - -To that aim, we strive to: - -- Avoid too many files in any given folder. -- Choose clear, unambigious folder names. -- Organize by domain. -- Every folder should contain a README that describes the contents of that folder. - -[discrete] -[[kibana-services-applications]] -=== Services and Applications - -[discrete] -==== src/plugins - -- {kib-repo}blob/{branch}/src/plugins/advanced_settings[advancedSettings] - -WARNING: Missing README. - - -- {kib-repo}blob/{branch}/src/plugins/apm_oss[apmOss] - -WARNING: Missing README. - - -- {kib-repo}blob/{branch}/src/plugins/bfetch/README.md[bfetch] - -bfetch allows to batch HTTP requests and streams responses back. - - -- {kib-repo}blob/{branch}/src/plugins/charts/README.md[charts] - -The Charts plugin is a way to create easier integration of shared colors, themes, types and other utilities across all Kibana charts and visualizations. - - -- {kib-repo}blob/{branch}/src/plugins/console[console] - -WARNING: Missing README. - - -- {kib-repo}blob/{branch}/src/plugins/dashboard/README.md[dashboard] - -Contains the dashboard application. - - -- {kib-repo}blob/{branch}/src/plugins/data/README.md[data] - -data plugin provides common data access services. - - -- {kib-repo}blob/{branch}/src/plugins/dev_tools/README.md[devTools] - -The ui/registry/dev_tools is removed in favor of the devTools plugin which exposes a register method in the setup contract. -Registering app works mostly the same as registering apps in core.application.register. -Routing will be handled by the id of the dev tool - your dev tool will be mounted when the URL matches /app/dev_tools#/. -This API doesn't support angular, for registering angular dev tools, bootstrap a local module on mount into the given HTML element. - - -- {kib-repo}blob/{branch}/src/plugins/discover/README.md[discover] - -Contains the Discover application and the saved search embeddable. - - -- {kib-repo}blob/{branch}/src/plugins/embeddable/README.md[embeddable] - -Embeddables are re-usable widgets that can be rendered in any environment or plugin. Developers can embed them directly in their plugin. End users can dynamically add them to any embeddable containers. - - -- {kib-repo}blob/{branch}/src/plugins/es_ui_shared/README.md[esUiShared] - -This plugin contains reusable code in the form of self-contained modules (or libraries). Each of these modules exports a set of functionality relevant to the domain of the module. - - -- {kib-repo}blob/{branch}/src/plugins/expressions/README.md[expressions] - -This plugin provides methods which will parse & execute an expression pipeline -string for you, as well as a series of registries for advanced users who might -want to incorporate their own functions, types, and renderers into the service -for use in their own application. - - -- {kib-repo}blob/{branch}/src/plugins/home/README.md[home] - -Moves the legacy ui/registry/feature_catalogue module for registering "features" that should be shown in the home page's feature catalogue to a service within a "home" plugin. The feature catalogue refered to here should not be confused with the "feature" plugin for registering features used to derive UI capabilities for feature controls. - - -- {kib-repo}blob/{branch}/src/plugins/index_pattern_management[indexPatternManagement] - -WARNING: Missing README. - - -- {kib-repo}blob/{branch}/src/plugins/input_control_vis/README.md[inputControlVis] - -Contains the input control visualization allowing to place custom filter controls on a dashboard. - - -- {kib-repo}blob/{branch}/src/plugins/inspector/README.md[inspector] - -The inspector is a contextual tool to gain insights into different elements -in Kibana, e.g. visualizations. It has the form of a flyout panel. - - -- {kib-repo}blob/{branch}/src/plugins/kibana_legacy/README.md[kibanaLegacy] - -This plugin will contain several helpers and services to integrate pieces of the legacy Kibana app with the new Kibana platform. - - -- {kib-repo}blob/{branch}/src/plugins/kibana_react/README.md[kibanaReact] - -Tools for building React applications in Kibana. - - -- {kib-repo}blob/{branch}/src/plugins/kibana_usage_collection/README.md[kibanaUsageCollection] - -This plugin registers the basic usage collectors from Kibana: - - -- {kib-repo}blob/{branch}/src/plugins/kibana_utils/README.md[kibanaUtils] - -Utilities for building Kibana plugins. - - -- {kib-repo}blob/{branch}/src/plugins/legacy_export[legacyExport] - -WARNING: Missing README. - - -- {kib-repo}blob/{branch}/src/plugins/management[management] - -WARNING: Missing README. - - -- {kib-repo}blob/{branch}/src/plugins/maps_legacy[mapsLegacy] - -WARNING: Missing README. - - -- {kib-repo}blob/{branch}/src/plugins/navigation/README.md[navigation] - -The navigation plugins exports the TopNavMenu component. -It also provides a stateful version of it on the start contract. - - -- {kib-repo}blob/{branch}/src/plugins/newsfeed[newsfeed] - -WARNING: Missing README. - - -- {kib-repo}blob/{branch}/src/plugins/region_map[regionMap] - -WARNING: Missing README. - - -- {kib-repo}blob/{branch}/src/plugins/saved_objects[savedObjects] - -WARNING: Missing README. - - -- {kib-repo}blob/{branch}/src/plugins/saved_objects_management[savedObjectsManagement] - -WARNING: Missing README. - - -- {kib-repo}blob/{branch}/src/plugins/share/README.md[share] - -Replaces the legacy ui/share module for registering share context menus. - - -- {kib-repo}blob/{branch}/src/plugins/telemetry/README.md[telemetry] - -Telemetry allows Kibana features to have usage tracked in the wild. The general term "telemetry" refers to multiple things: - - -- {kib-repo}blob/{branch}/src/plugins/telemetry_collection_manager/README.md[telemetryCollectionManager] - -Telemetry's collection manager to go through all the telemetry sources when fetching it before reporting. - - -- {kib-repo}blob/{branch}/src/plugins/telemetry_management_section/README.md[telemetryManagementSection] - -This plugin adds the Advanced Settings section for the Usage Data collection (aka Telemetry). - - -- {kib-repo}blob/{branch}/src/plugins/tile_map[tileMap] - -WARNING: Missing README. - - -- {kib-repo}blob/{branch}/src/plugins/timelion/README.md[timelion] - -Contains the deprecated timelion application. For the timelion visualization, -which also contains the timelion APIs and backend, look at the vis_type_timelion plugin. - - -- {kib-repo}blob/{branch}/src/plugins/ui_actions/README.md[uiActions] - -An API for: - - -- {kib-repo}blob/{branch}/src/plugins/usage_collection/README.md[usageCollection] - -Usage Collection allows collecting usage data for other services to consume (telemetry and monitoring). -To integrate with the telemetry services for usage collection of your feature, there are 2 steps: - - -- {kib-repo}blob/{branch}/src/plugins/vis_type_markdown/README.md[visTypeMarkdown] - -The markdown visualization that can be used to place text panels on dashboards. - - -- {kib-repo}blob/{branch}/src/plugins/vis_type_metric/README.md[visTypeMetric] - -Contains the metric visualization. - - -- {kib-repo}blob/{branch}/src/plugins/vis_type_table/README.md[visTypeTable] - -Contains the data table visualization, that allows presenting data in a simple table format. - - -- {kib-repo}blob/{branch}/src/plugins/vis_type_tagcloud/README.md[visTypeTagcloud] - -Contains the tagcloud visualization. - - -- {kib-repo}blob/{branch}/src/plugins/vis_type_timelion/README.md[visTypeTimelion] - -Contains the timelion visualization and the timelion backend. - - -- {kib-repo}blob/{branch}/src/plugins/vis_type_timeseries/README.md[visTypeTimeseries] - -Contains everything around TSVB (the editor, visualizatin implementations and backends). - - -- {kib-repo}blob/{branch}/src/plugins/vis_type_vega/README.md[visTypeVega] - -Contains the Vega visualization. - - -- {kib-repo}blob/{branch}/src/plugins/vis_type_vislib/README.md[visTypeVislib] - -Contains the vislib visualizations. These are the classical area/line/bar, pie, gauge/goal and -heatmap charts. - - -- {kib-repo}blob/{branch}/src/plugins/vis_type_xy/README.md[visTypeXy] - -Contains the new xy-axis chart using the elastic-charts library, which will eventually -replace the vislib xy-axis (bar, area, line) charts. - - -- {kib-repo}blob/{branch}/src/plugins/visualizations/README.md[visualizations] - -Contains most of the visualization infrastructure, e.g. the visualization type registry or the -visualization embeddable. - - -- {kib-repo}blob/{branch}/src/plugins/visualize/README.md[visualize] - -Contains the visualize application which includes the listing page and the app frame, -which will load the visualization's editor. - - -[discrete] -==== x-pack/plugins - -- {kib-repo}blob/{branch}/x-pack/plugins/actions/README.md[actions] - -The Kibana actions plugin provides a framework to create executable actions. You can: - - -- {kib-repo}blob/{branch}/x-pack/plugins/alerting_builtins/README.md[alertingBuiltins] - -This plugin provides alertTypes shipped with Kibana for use with the -the alerts plugin. When enabled, it will register -the built-in alertTypes with the alerting plugin, register associated HTTP -routes, etc. - - -- {kib-repo}blob/{branch}/x-pack/plugins/alerts/README.md[alerts] - -The Kibana alerting plugin provides a common place to set up alerts. You can: - - -- {kib-repo}blob/{branch}/x-pack/plugins/apm/readme.md[apm] - -To access an elasticsearch instance that has live data you have two options: - - -- {kib-repo}blob/{branch}/x-pack/plugins/audit_trail[auditTrail] - -WARNING: Missing README. - - -- {kib-repo}blob/{branch}/x-pack/plugins/beats_management/readme.md[beatsManagement] - -Notes: -Failure to have auth enabled in Kibana will make for a broken UI. UI-based errors not yet in place - - -- {kib-repo}blob/{branch}/x-pack/plugins/canvas/README.md[canvas] - -"Never look back. The past is done. The future is a blank canvas." ― Suzy Kassem, Rise Up and Salute the Sun - - -- {kib-repo}blob/{branch}/x-pack/plugins/case/README.md[case] - -Experimental Feature - - -- {kib-repo}blob/{branch}/x-pack/plugins/cloud[cloud] - -WARNING: Missing README. - - -- {kib-repo}blob/{branch}/x-pack/plugins/code[code] - -WARNING: Missing README. - - -- {kib-repo}blob/{branch}/x-pack/plugins/console_extensions[consoleExtensions] - -WARNING: Missing README. - - -- {kib-repo}blob/{branch}/x-pack/plugins/cross_cluster_replication/README.md[crossClusterReplication] - -You can run a local cluster and simulate a remote cluster within a single Kibana directory. - - -- {kib-repo}blob/{branch}/x-pack/plugins/dashboard_enhanced/README.md[dashboardEnhanced] - -Contains the enhancements to the OSS dashboard app. - - -- {kib-repo}blob/{branch}/x-pack/plugins/dashboard_mode/README.md[dashboardMode] - -The deprecated dashboard only mode. - - -- {kib-repo}blob/{branch}/x-pack/plugins/data_enhanced[dataEnhanced] - -WARNING: Missing README. - - -- {kib-repo}blob/{branch}/x-pack/plugins/discover_enhanced/README.md[discoverEnhanced] - -Contains the enhancements to the OSS discover app. - - -- {kib-repo}blob/{branch}/x-pack/plugins/embeddable_enhanced[embeddableEnhanced] - -WARNING: Missing README. - - -- {kib-repo}blob/{branch}/x-pack/plugins/encrypted_saved_objects/README.md[encryptedSavedObjects] - -The purpose of this plugin is to provide a way to encrypt/decrypt attributes on the custom Saved Objects that works with -security and spaces filtering as well as performing audit logging. - - -- {kib-repo}blob/{branch}/x-pack/plugins/enterprise_search/README.md[enterpriseSearch] - -This plugin's goal is to provide a Kibana user interface to the Enterprise Search solution's products (App Search and Workplace Search). In it's current MVP state, the plugin provides the following with the goal of gathering user feedback and raising product awareness: - - -- {kib-repo}blob/{branch}/x-pack/plugins/event_log/README.md[eventLog] - -The purpose of this plugin is to provide a way to persist a history of events -occuring in Kibana, initially just for the Make It Action project - alerts -and actions. - - -- {kib-repo}blob/{branch}/x-pack/plugins/features[features] - -WARNING: Missing README. - - -- {kib-repo}blob/{branch}/x-pack/plugins/file_upload[fileUpload] - -WARNING: Missing README. - - -- {kib-repo}blob/{branch}/x-pack/plugins/global_search/README.md[globalSearch] - -The GlobalSearch plugin provides an easy way to search for various objects, such as applications -or dashboards from the Kibana instance, from both server and client-side plugins - - -- {kib-repo}blob/{branch}/x-pack/plugins/global_search_providers[globalSearchProviders] - -WARNING: Missing README. - - -- {kib-repo}blob/{branch}/x-pack/plugins/graph/README.md[graph] - -This is the main source folder of the Graph plugin. It contains all of the Kibana server and client source code. x-pack/test/functional/apps/graph contains additional functional tests. - - -- {kib-repo}blob/{branch}/x-pack/plugins/grokdebugger/README.md[grokdebugger] - -- {kib-repo}blob/{branch}/x-pack/plugins/index_lifecycle_management/README.md[indexLifecycleManagement] - -You can test that the Frozen badge, phase filtering, and lifecycle information is surfaced in -Index Management by running this series of requests in Console: - - -- {kib-repo}blob/{branch}/x-pack/plugins/index_management[indexManagement] - -WARNING: Missing README. - - -- {kib-repo}blob/{branch}/x-pack/plugins/infra/README.md[infra] - -This is the home of the infra plugin, which aims to provide a solution for -the infrastructure monitoring use-case within Kibana. - - -- {kib-repo}blob/{branch}/x-pack/plugins/ingest_manager/README.md[ingestManager] - -Fleet needs to have Elasticsearch API keys enabled, and also to have TLS enabled on kibana, (if you want to run Kibana without TLS you can provide the following config flag --xpack.ingestManager.fleet.tlsCheckDisabled=false) - - -- {kib-repo}blob/{branch}/x-pack/plugins/ingest_pipelines/README.md[ingestPipelines] - -The ingest_pipelines plugin provides Kibana support for Elasticsearch's ingest nodes. Please refer to the Elasticsearch documentation for more details. - - -- {kib-repo}blob/{branch}/x-pack/plugins/lens/readme.md[lens] - -Run all tests from the x-pack root directory - - -- {kib-repo}blob/{branch}/x-pack/plugins/license_management[licenseManagement] - -WARNING: Missing README. - - -- {kib-repo}blob/{branch}/x-pack/plugins/licensing/README.md[licensing] - -The licensing plugin retrieves license data from Elasticsearch at regular configurable intervals. - - -- {kib-repo}blob/{branch}/x-pack/plugins/lists/README.md[lists] - -README.md for developers working on the backend lists on how to get started -using the CURL scripts in the scripts folder. - - -- {kib-repo}blob/{branch}/x-pack/plugins/logstash[logstash] - -WARNING: Missing README. - - -- {kib-repo}blob/{branch}/x-pack/plugins/maps/README.md[maps] - -Visualize geo data from Elasticsearch or 3rd party geo-services. - - -- {kib-repo}blob/{branch}/x-pack/plugins/maps_legacy_licensing/README.md[mapsLegacyLicensing] - -This plugin provides access to the detailed tile map services from Elastic. - - -- {kib-repo}blob/{branch}/x-pack/plugins/ml[ml] - -WARNING: Missing README. - - -- {kib-repo}blob/{branch}/x-pack/plugins/monitoring[monitoring] - -WARNING: Missing README. - - -- {kib-repo}blob/{branch}/x-pack/plugins/observability/README.md[observability] - -This plugin provides shared components and services for use across observability solutions, as well as the observability landing page UI. - - -- {kib-repo}blob/{branch}/x-pack/plugins/oss_telemetry[ossTelemetry] - -WARNING: Missing README. - - -- {kib-repo}blob/{branch}/x-pack/plugins/painless_lab[painlessLab] - -WARNING: Missing README. - - -- {kib-repo}blob/{branch}/x-pack/plugins/remote_clusters[remoteClusters] - -WARNING: Missing README. - - -- {kib-repo}blob/{branch}/x-pack/plugins/reporting/README.md[reporting] - -An awesome Kibana reporting plugin - - -- {kib-repo}blob/{branch}/x-pack/plugins/rollup/README.md[rollup] - -Welcome to the Kibana rollup plugin! This plugin provides Kibana support for Elasticsearch's rollup feature. Please refer to the Elasticsearch documentation to understand rollup indices and how to create rollup jobs. - - -- {kib-repo}blob/{branch}/x-pack/plugins/searchprofiler[searchprofiler] - -WARNING: Missing README. - - -- {kib-repo}blob/{branch}/x-pack/plugins/security/README.md[security] - -See Configuring security in Kibana. - - -- {kib-repo}blob/{branch}/x-pack/plugins/security_solution/README.md[securitySolution] - -Welcome to the Kibana Security Solution plugin! This README will go over getting started with development and testing. - - -- {kib-repo}blob/{branch}/x-pack/plugins/snapshot_restore/README.md[snapshotRestore] - -or - - -- {kib-repo}blob/{branch}/x-pack/plugins/spaces[spaces] - -WARNING: Missing README. - - -- {kib-repo}blob/{branch}/x-pack/plugins/task_manager[taskManager] - -WARNING: Missing README. - - -- {kib-repo}blob/{branch}/x-pack/plugins/telemetry_collection_xpack/README.md[telemetryCollectionXpack] - -Gathers all usage collection, retrieving them from both: OSS and X-Pack plugins. - - -- {kib-repo}blob/{branch}/x-pack/plugins/transform[transform] - -WARNING: Missing README. - - -- {kib-repo}blob/{branch}/x-pack/plugins/translations[translations] - -WARNING: Missing README. - - -- {kib-repo}blob/{branch}/x-pack/plugins/triggers_actions_ui/README.md[triggers_actions_ui] - -The Kibana alerts and actions UI plugin provides a user interface for managing alerts and actions. -As a developer you can reuse and extend built-in alerts and actions UI functionality: - - -- {kib-repo}blob/{branch}/x-pack/plugins/ui_actions_enhanced/README.md[uiActionsEnhanced] - -- {kib-repo}blob/{branch}/x-pack/plugins/upgrade_assistant[upgradeAssistant] - -WARNING: Missing README. - - -- {kib-repo}blob/{branch}/x-pack/plugins/uptime/README.md[uptime] - -The purpose of this plugin is to provide users of Heartbeat more visibility of what's happening -in their infrastructure. - - -- {kib-repo}blob/{branch}/x-pack/plugins/watcher/README.md[watcher] - -This plugins adopts some conventions in addition to or in place of conventions in Kibana (at the time of the plugin's creation): - diff --git a/docs/developer/architecture/index.asciidoc b/docs/developer/architecture/index.asciidoc index 2e6ab1a4ad6a..ac25fe003df0 100644 --- a/docs/developer/architecture/index.asciidoc +++ b/docs/developer/architecture/index.asciidoc @@ -17,7 +17,6 @@ A few notable services are called out below. * <> * <> * <> -* <> include::add-data-tutorials.asciidoc[leveloffset=+1] @@ -25,4 +24,3 @@ include::development-visualize-index.asciidoc[leveloffset=+1] include::security/index.asciidoc[leveloffset=+1] -include::code-exploration.asciidoc[leveloffset=+1] diff --git a/docs/developer/architecture/security/rbac.asciidoc b/docs/developer/architecture/security/rbac.asciidoc index 7b35a91ca73d..451e833651a7 100644 --- a/docs/developer/architecture/security/rbac.asciidoc +++ b/docs/developer/architecture/security/rbac.asciidoc @@ -1,4 +1,4 @@ -[[development-security-rbac]] +[[development-rbac]] == Role-based access control Role-based access control (RBAC) in {kib} relies upon the @@ -7,7 +7,7 @@ that {es} exposes. This allows {kib} to define the privileges that {kib} wishes to grant to users, assign them to the relevant users using roles, and then authorize the user to perform a specific action. This is handled within a secured instance of the `SavedObjectsClient` and available transparently to -consumers when using `request.getSavedObjectsClient()` or +consumers when using `request.getSavedObjectsClient()` or `savedObjects.getScopedSavedObjectsClient()`. [[development-rbac-privileges]] @@ -77,7 +77,7 @@ The application is created by concatenating the prefix of `kibana-` with the val } ---------------------------------- -Roles that grant <> should be managed using the <> or the *Management -> Security -> Roles* page, not directly using the {es} {ref}/security-api.html#security-role-apis[role management API]. This role can then be assigned to users using the {es} +Roles that grant <> should be managed using the <> or the *Management -> Security -> Roles* page, not directly using the {es} {ref}/security-api.html#security-role-apis[role management API]. This role can then be assigned to users using the {es} {ref}/security-api.html#security-user-apis[user management APIs]. [[development-rbac-authorization]] diff --git a/docs/developer/best-practices/index.asciidoc b/docs/developer/best-practices/index.asciidoc index 90b0092d835a..63a44b54d454 100644 --- a/docs/developer/best-practices/index.asciidoc +++ b/docs/developer/best-practices/index.asciidoc @@ -99,7 +99,7 @@ Re-using these services will help create a consistent experience across [discrete] === Backward compatibility -Eventually we want to garauntee to our plugin developers that their plugins will not break from minor to minor. +Eventually we want to guarantee to our plugin developers that their plugins will not break from minor to minor. Any time you create or change a public API, keep this in mind, and consider potential backward compatibility issues. While we have a formal diff --git a/docs/developer/contributing/development-tests.asciidoc b/docs/developer/contributing/development-tests.asciidoc index 2e40f664faba..e4bd49e12101 100644 --- a/docs/developer/contributing/development-tests.asciidoc +++ b/docs/developer/contributing/development-tests.asciidoc @@ -20,7 +20,7 @@ root) |`yarn test:jest_integration -t regexp [test path]` |Mocha -|`src/**/__tests__/**/*.js` `!src/**/public/__tests__/*.js``packages/kbn-datemath/test/**/*.js` `packages/kbn-dev-utils/src/**/__tests__/**/*.js` `tasks/**/__tests__/**/*.js` +|`src/**/__tests__/**/*.js` `!src/**/public/__tests__/*.js` `packages/kbn-dev-utils/src/**/__tests__/**/*.js` `tasks/**/__tests__/**/*.js` |`node scripts/mocha --grep=regexp [test path]` |Functional diff --git a/docs/developer/getting-started/index.asciidoc b/docs/developer/getting-started/index.asciidoc index 2ac51b6cf86f..eaa35eece5a2 100644 --- a/docs/developer/getting-started/index.asciidoc +++ b/docs/developer/getting-started/index.asciidoc @@ -99,7 +99,7 @@ preserving data inbetween runs, running remote cluster, etc. [discrete] === Run {kib} -In another terminal window, start up {kib}. Include developer examples by adding an optional `--run-examples` flag. +In another terminal window, start up {kib}. Include {kib-repo}tree/{branch}/examples[developer examples] by adding an optional `--run-examples` flag. [source,bash] ---- diff --git a/docs/developer/index.asciidoc b/docs/developer/index.asciidoc index db57815a1285..5f032a395217 100644 --- a/docs/developer/index.asciidoc +++ b/docs/developer/index.asciidoc @@ -12,6 +12,7 @@ running in no time. If you have any problems, file an issue in the https://githu * <> * <> * <> +* <> -- @@ -27,3 +28,5 @@ include::plugin/index.asciidoc[] include::advanced/index.asciidoc[] +include::plugin-list.asciidoc[] + diff --git a/docs/developer/plugin-list.asciidoc b/docs/developer/plugin-list.asciidoc new file mode 100644 index 000000000000..b3180a7a0387 --- /dev/null +++ b/docs/developer/plugin-list.asciidoc @@ -0,0 +1,497 @@ +//// + +NOTE: + This is an automatically generated file. Please do not edit directly. Instead, run the + following from within the kibana repository: + + node scripts/build_plugin_list_docs + + You can update the template within packages/kbn-dev-utils/target/plugin_list/generate_plugin_list.js + +//// + +[[plugin-list]] +== List of {kib} plugins + +[discrete] +=== src/plugins + +[%header,cols=2*] +|=== +|Name +|Description + + +|{kib-repo}blob/{branch}/src/plugins/advanced_settings[advancedSettings] +|WARNING: Missing README. + + +|{kib-repo}blob/{branch}/src/plugins/apm_oss[apmOss] +|WARNING: Missing README. + + +|{kib-repo}blob/{branch}/src/plugins/bfetch/README.md[bfetch] +|bfetch allows to batch HTTP requests and streams responses back. + + +|{kib-repo}blob/{branch}/src/plugins/charts/README.md[charts] +|The Charts plugin is a way to create easier integration of shared colors, themes, types and other utilities across all Kibana charts and visualizations. + + +|{kib-repo}blob/{branch}/src/plugins/console[console] +|WARNING: Missing README. + + +|<> +|- Registers the dashboard application. +- Adds a dashboard embeddable that can be used in other applications. + + +|{kib-repo}blob/{branch}/src/plugins/data/README.md[data] +|data plugin provides common data access services. + + +|{kib-repo}blob/{branch}/src/plugins/dev_tools/README.md[devTools] +|The ui/registry/dev_tools is removed in favor of the devTools plugin which exposes a register method in the setup contract. +Registering app works mostly the same as registering apps in core.application.register. +Routing will be handled by the id of the dev tool - your dev tool will be mounted when the URL matches /app/dev_tools#/. +This API doesn't support angular, for registering angular dev tools, bootstrap a local module on mount into the given HTML element. + + +|{kib-repo}blob/{branch}/src/plugins/discover/README.md[discover] +|Contains the Discover application and the saved search embeddable. + + +|{kib-repo}blob/{branch}/src/plugins/embeddable/README.md[embeddable] +|Embeddables are re-usable widgets that can be rendered in any environment or plugin. Developers can embed them directly in their plugin. End users can dynamically add them to any embeddable containers. + + +|{kib-repo}blob/{branch}/src/plugins/es_ui_shared/README.md[esUiShared] +|This plugin contains reusable code in the form of self-contained modules (or libraries). Each of these modules exports a set of functionality relevant to the domain of the module. + + +|{kib-repo}blob/{branch}/src/plugins/expressions/README.md[expressions] +|This plugin provides methods which will parse & execute an expression pipeline +string for you, as well as a series of registries for advanced users who might +want to incorporate their own functions, types, and renderers into the service +for use in their own application. + + +|{kib-repo}blob/{branch}/src/plugins/home/README.md[home] +|Moves the legacy ui/registry/feature_catalogue module for registering "features" that should be shown in the home page's feature catalogue to a service within a "home" plugin. The feature catalogue refered to here should not be confused with the "feature" plugin for registering features used to derive UI capabilities for feature controls. + + +|{kib-repo}blob/{branch}/src/plugins/index_pattern_management[indexPatternManagement] +|WARNING: Missing README. + + +|{kib-repo}blob/{branch}/src/plugins/input_control_vis/README.md[inputControlVis] +|Contains the input control visualization allowing to place custom filter controls on a dashboard. + + +|{kib-repo}blob/{branch}/src/plugins/inspector/README.md[inspector] +|The inspector is a contextual tool to gain insights into different elements +in Kibana, e.g. visualizations. It has the form of a flyout panel. + + +|{kib-repo}blob/{branch}/src/plugins/kibana_legacy/README.md[kibanaLegacy] +|This plugin will contain several helpers and services to integrate pieces of the legacy Kibana app with the new Kibana platform. + + +|{kib-repo}blob/{branch}/src/plugins/kibana_react/README.md[kibanaReact] +|Tools for building React applications in Kibana. + + +|{kib-repo}blob/{branch}/src/plugins/kibana_usage_collection/README.md[kibanaUsageCollection] +|This plugin registers the basic usage collectors from Kibana: + + +|{kib-repo}blob/{branch}/src/plugins/kibana_utils/README.md[kibanaUtils] +|Utilities for building Kibana plugins. + + +|{kib-repo}blob/{branch}/src/plugins/legacy_export[legacyExport] +|WARNING: Missing README. + + +|{kib-repo}blob/{branch}/src/plugins/management[management] +|WARNING: Missing README. + + +|{kib-repo}blob/{branch}/src/plugins/maps_legacy[mapsLegacy] +|WARNING: Missing README. + + +|{kib-repo}blob/{branch}/src/plugins/navigation/README.md[navigation] +|The navigation plugins exports the TopNavMenu component. +It also provides a stateful version of it on the start contract. + + +|{kib-repo}blob/{branch}/src/plugins/newsfeed[newsfeed] +|WARNING: Missing README. + + +|{kib-repo}blob/{branch}/src/plugins/region_map[regionMap] +|WARNING: Missing README. + + +|{kib-repo}blob/{branch}/src/plugins/saved_objects[savedObjects] +|WARNING: Missing README. + + +|{kib-repo}blob/{branch}/src/plugins/saved_objects_management[savedObjectsManagement] +|WARNING: Missing README. + + +|{kib-repo}blob/{branch}/src/plugins/share/README.md[share] +|Replaces the legacy ui/share module for registering share context menus. + + +|{kib-repo}blob/{branch}/src/plugins/telemetry/README.md[telemetry] +|Telemetry allows Kibana features to have usage tracked in the wild. The general term "telemetry" refers to multiple things: + + +|{kib-repo}blob/{branch}/src/plugins/telemetry_collection_manager/README.md[telemetryCollectionManager] +|Telemetry's collection manager to go through all the telemetry sources when fetching it before reporting. + + +|{kib-repo}blob/{branch}/src/plugins/telemetry_management_section/README.md[telemetryManagementSection] +|This plugin adds the Advanced Settings section for the Usage Data collection (aka Telemetry). + + +|{kib-repo}blob/{branch}/src/plugins/tile_map[tileMap] +|WARNING: Missing README. + + +|{kib-repo}blob/{branch}/src/plugins/timelion/README.md[timelion] +|Contains the deprecated timelion application. For the timelion visualization, +which also contains the timelion APIs and backend, look at the vis_type_timelion plugin. + + +|{kib-repo}blob/{branch}/src/plugins/ui_actions/README.md[uiActions] +|An API for: + + +|{kib-repo}blob/{branch}/src/plugins/usage_collection/README.md[usageCollection] +|Usage Collection allows collecting usage data for other services to consume (telemetry and monitoring). +To integrate with the telemetry services for usage collection of your feature, there are 2 steps: + + +|{kib-repo}blob/{branch}/src/plugins/vis_type_markdown/README.md[visTypeMarkdown] +|The markdown visualization that can be used to place text panels on dashboards. + + +|{kib-repo}blob/{branch}/src/plugins/vis_type_metric/README.md[visTypeMetric] +|Contains the metric visualization. + + +|{kib-repo}blob/{branch}/src/plugins/vis_type_table/README.md[visTypeTable] +|Contains the data table visualization, that allows presenting data in a simple table format. + + +|{kib-repo}blob/{branch}/src/plugins/vis_type_tagcloud/README.md[visTypeTagcloud] +|Contains the tagcloud visualization. + + +|{kib-repo}blob/{branch}/src/plugins/vis_type_timelion/README.md[visTypeTimelion] +|Contains the timelion visualization and the timelion backend. + + +|{kib-repo}blob/{branch}/src/plugins/vis_type_timeseries/README.md[visTypeTimeseries] +|Contains everything around TSVB (the editor, visualizatin implementations and backends). + + +|{kib-repo}blob/{branch}/src/plugins/vis_type_vega/README.md[visTypeVega] +|Contains the Vega visualization. + + +|{kib-repo}blob/{branch}/src/plugins/vis_type_vislib/README.md[visTypeVislib] +|Contains the vislib visualizations. These are the classical area/line/bar, pie, gauge/goal and +heatmap charts. + + +|{kib-repo}blob/{branch}/src/plugins/vis_type_xy/README.md[visTypeXy] +|Contains the new xy-axis chart using the elastic-charts library, which will eventually +replace the vislib xy-axis (bar, area, line) charts. + + +|{kib-repo}blob/{branch}/src/plugins/visualizations/README.md[visualizations] +|Contains most of the visualization infrastructure, e.g. the visualization type registry or the +visualization embeddable. + + +|{kib-repo}blob/{branch}/src/plugins/visualize/README.md[visualize] +|Contains the visualize application which includes the listing page and the app frame, +which will load the visualization's editor. + + +|=== + +[discrete] +=== x-pack/plugins + +[%header,cols=2*] +|=== +|Name +|Description + + +|{kib-repo}blob/{branch}/x-pack/plugins/actions/README.md[actions] +|The Kibana actions plugin provides a framework to create executable actions. You can: + + +|{kib-repo}blob/{branch}/x-pack/plugins/alerting_builtins/README.md[alertingBuiltins] +|This plugin provides alertTypes shipped with Kibana for use with the +the alerts plugin. When enabled, it will register +the built-in alertTypes with the alerting plugin, register associated HTTP +routes, etc. + + +|{kib-repo}blob/{branch}/x-pack/plugins/alerts/README.md[alerts] +|The Kibana alerting plugin provides a common place to set up alerts. You can: + + +|{kib-repo}blob/{branch}/x-pack/plugins/apm/readme.md[apm] +|To access an elasticsearch instance that has live data you have two options: + + +|{kib-repo}blob/{branch}/x-pack/plugins/audit_trail[auditTrail] +|WARNING: Missing README. + + +|{kib-repo}blob/{branch}/x-pack/plugins/beats_management/readme.md[beatsManagement] +|Notes: +Failure to have auth enabled in Kibana will make for a broken UI. UI-based errors not yet in place + + +|{kib-repo}blob/{branch}/x-pack/plugins/canvas/README.md[canvas] +|"Never look back. The past is done. The future is a blank canvas." ― Suzy Kassem, Rise Up and Salute the Sun + + +|{kib-repo}blob/{branch}/x-pack/plugins/case/README.md[case] +|Experimental Feature + + +|{kib-repo}blob/{branch}/x-pack/plugins/cloud[cloud] +|WARNING: Missing README. + + +|{kib-repo}blob/{branch}/x-pack/plugins/code[code] +|WARNING: Missing README. + + +|{kib-repo}blob/{branch}/x-pack/plugins/console_extensions[consoleExtensions] +|WARNING: Missing README. + + +|{kib-repo}blob/{branch}/x-pack/plugins/cross_cluster_replication/README.md[crossClusterReplication] +|You can run a local cluster and simulate a remote cluster within a single Kibana directory. + + +|<> +|Adds drilldown capabilities to dashboard. Owned by the Kibana App team. + + +|{kib-repo}blob/{branch}/x-pack/plugins/dashboard_mode/README.md[dashboardMode] +|The deprecated dashboard only mode. + + +|{kib-repo}blob/{branch}/x-pack/plugins/data_enhanced[dataEnhanced] +|WARNING: Missing README. + + +|{kib-repo}blob/{branch}/x-pack/plugins/discover_enhanced/README.md[discoverEnhanced] +|Contains the enhancements to the OSS discover app. + + +|<> +|Enhances Embeddables by registering a custom factory provider. The enhanced factory provider +adds dynamic actions to every embeddables state, in order to support drilldowns. + + +|{kib-repo}blob/{branch}/x-pack/plugins/encrypted_saved_objects/README.md[encryptedSavedObjects] +|The purpose of this plugin is to provide a way to encrypt/decrypt attributes on the custom Saved Objects that works with +security and spaces filtering as well as performing audit logging. + + +|{kib-repo}blob/{branch}/x-pack/plugins/enterprise_search/README.md[enterpriseSearch] +|This plugin's goal is to provide a Kibana user interface to the Enterprise Search solution's products (App Search and Workplace Search). In it's current MVP state, the plugin provides the following with the goal of gathering user feedback and raising product awareness: + + +|{kib-repo}blob/{branch}/x-pack/plugins/event_log/README.md[eventLog] +|The purpose of this plugin is to provide a way to persist a history of events +occuring in Kibana, initially just for the Make It Action project - alerts +and actions. + + +|{kib-repo}blob/{branch}/x-pack/plugins/features[features] +|WARNING: Missing README. + + +|{kib-repo}blob/{branch}/x-pack/plugins/file_upload[fileUpload] +|WARNING: Missing README. + + +|{kib-repo}blob/{branch}/x-pack/plugins/global_search/README.md[globalSearch] +|The GlobalSearch plugin provides an easy way to search for various objects, such as applications +or dashboards from the Kibana instance, from both server and client-side plugins + + +|{kib-repo}blob/{branch}/x-pack/plugins/global_search_providers[globalSearchProviders] +|WARNING: Missing README. + + +|{kib-repo}blob/{branch}/x-pack/plugins/graph/README.md[graph] +|This is the main source folder of the Graph plugin. It contains all of the Kibana server and client source code. x-pack/test/functional/apps/graph contains additional functional tests. + + +|{kib-repo}blob/{branch}/x-pack/plugins/grokdebugger[grokdebugger] +|WARNING: Missing README. + + +|{kib-repo}blob/{branch}/x-pack/plugins/index_lifecycle_management/README.md[indexLifecycleManagement] +|You can test that the Frozen badge, phase filtering, and lifecycle information is surfaced in +Index Management by running this series of requests in Console: + + +|{kib-repo}blob/{branch}/x-pack/plugins/index_management/README.md[indexManagement] +|Create a data stream using Console and you'll be able to view it in the UI: + + +|{kib-repo}blob/{branch}/x-pack/plugins/infra/README.md[infra] +|This is the home of the infra plugin, which aims to provide a solution for +the infrastructure monitoring use-case within Kibana. + + +|{kib-repo}blob/{branch}/x-pack/plugins/ingest_manager/README.md[ingestManager] +|Fleet needs to have Elasticsearch API keys enabled, and also to have TLS enabled on kibana, (if you want to run Kibana without TLS you can provide the following config flag --xpack.ingestManager.fleet.tlsCheckDisabled=false) + + +|{kib-repo}blob/{branch}/x-pack/plugins/ingest_pipelines/README.md[ingestPipelines] +|The ingest_pipelines plugin provides Kibana support for Elasticsearch's ingest nodes. Please refer to the Elasticsearch documentation for more details. + + +|{kib-repo}blob/{branch}/x-pack/plugins/lens/readme.md[lens] +|Run all tests from the x-pack root directory + + +|{kib-repo}blob/{branch}/x-pack/plugins/license_management[licenseManagement] +|WARNING: Missing README. + + +|{kib-repo}blob/{branch}/x-pack/plugins/licensing/README.md[licensing] +|The licensing plugin retrieves license data from Elasticsearch at regular configurable intervals. + + +|{kib-repo}blob/{branch}/x-pack/plugins/lists/README.md[lists] +|README.md for developers working on the backend lists on how to get started +using the CURL scripts in the scripts folder. + + +|{kib-repo}blob/{branch}/x-pack/plugins/logstash[logstash] +|WARNING: Missing README. + + +|{kib-repo}blob/{branch}/x-pack/plugins/maps/README.md[maps] +|Visualize geo data from Elasticsearch or 3rd party geo-services. + + +|{kib-repo}blob/{branch}/x-pack/plugins/maps_legacy_licensing/README.md[mapsLegacyLicensing] +|This plugin provides access to the detailed tile map services from Elastic. + + +|{kib-repo}blob/{branch}/x-pack/plugins/ml[ml] +|WARNING: Missing README. + + +|{kib-repo}blob/{branch}/x-pack/plugins/monitoring[monitoring] +|WARNING: Missing README. + + +|{kib-repo}blob/{branch}/x-pack/plugins/observability/README.md[observability] +|This plugin provides shared components and services for use across observability solutions, as well as the observability landing page UI. + + +|{kib-repo}blob/{branch}/x-pack/plugins/oss_telemetry[ossTelemetry] +|WARNING: Missing README. + + +|{kib-repo}blob/{branch}/x-pack/plugins/painless_lab[painlessLab] +|WARNING: Missing README. + + +|{kib-repo}blob/{branch}/x-pack/plugins/remote_clusters[remoteClusters] +|WARNING: Missing README. + + +|{kib-repo}blob/{branch}/x-pack/plugins/reporting/README.md[reporting] +|An awesome Kibana reporting plugin + + +|{kib-repo}blob/{branch}/x-pack/plugins/rollup/README.md[rollup] +|Welcome to the Kibana rollup plugin! This plugin provides Kibana support for Elasticsearch's rollup feature. Please refer to the Elasticsearch documentation to understand rollup indices and how to create rollup jobs. + + +|{kib-repo}blob/{branch}/x-pack/plugins/searchprofiler[searchprofiler] +|WARNING: Missing README. + + +|{kib-repo}blob/{branch}/x-pack/plugins/security/README.md[security] +|See Configuring security in Kibana. + + +|{kib-repo}blob/{branch}/x-pack/plugins/security_solution/README.md[securitySolution] +|Welcome to the Kibana Security Solution plugin! This README will go over getting started with development and testing. + + +|{kib-repo}blob/{branch}/x-pack/plugins/snapshot_restore/README.md[snapshotRestore] +|or + + +|{kib-repo}blob/{branch}/x-pack/plugins/spaces[spaces] +|WARNING: Missing README. + + +|{kib-repo}blob/{branch}/x-pack/plugins/task_manager[taskManager] +|WARNING: Missing README. + + +|{kib-repo}blob/{branch}/x-pack/plugins/telemetry_collection_xpack/README.md[telemetryCollectionXpack] +|Gathers all usage collection, retrieving them from both: OSS and X-Pack plugins. + + +|{kib-repo}blob/{branch}/x-pack/plugins/transform[transform] +|WARNING: Missing README. + + +|{kib-repo}blob/{branch}/x-pack/plugins/translations[translations] +|WARNING: Missing README. + + +|{kib-repo}blob/{branch}/x-pack/plugins/triggers_actions_ui/README.md[triggers_actions_ui] +|The Kibana alerts and actions UI plugin provides a user interface for managing alerts and actions. +As a developer you can reuse and extend built-in alerts and actions UI functionality: + + +|{kib-repo}blob/{branch}/x-pack/plugins/ui_actions_enhanced/README.md[uiActionsEnhanced] +|Registers commercially licensed generic actions like per panel time range and contains some code that supports drilldown work. + + +|{kib-repo}blob/{branch}/x-pack/plugins/upgrade_assistant[upgradeAssistant] +|WARNING: Missing README. + + +|{kib-repo}blob/{branch}/x-pack/plugins/uptime/README.md[uptime] +|The purpose of this plugin is to provide users of Heartbeat more visibility of what's happening +in their infrastructure. + + +|{kib-repo}blob/{branch}/x-pack/plugins/watcher/README.md[watcher] +|This plugins adopts some conventions in addition to or in place of conventions in Kibana (at the time of the plugin's creation): + + +|=== + +include::{kibana-root}/src/plugins/dashboard/README.asciidoc[leveloffset=+1] +include::{kibana-root}/x-pack/plugins/dashboard_enhanced/README.asciidoc[leveloffset=+1] +include::{kibana-root}/x-pack/plugins/embeddable_enhanced/README.asciidoc[leveloffset=+1] diff --git a/docs/development/core/public/kibana-plugin-core-public.doclinksstart.md b/docs/development/core/public/kibana-plugin-core-public.doclinksstart.md index fa2d9090e315..4644dc432bc9 100644 --- a/docs/development/core/public/kibana-plugin-core-public.doclinksstart.md +++ b/docs/development/core/public/kibana-plugin-core-public.doclinksstart.md @@ -17,5 +17,5 @@ export interface DocLinksStart | --- | --- | --- | | [DOC\_LINK\_VERSION](./kibana-plugin-core-public.doclinksstart.doc_link_version.md) | string | | | [ELASTIC\_WEBSITE\_URL](./kibana-plugin-core-public.doclinksstart.elastic_website_url.md) | string | | -| [links](./kibana-plugin-core-public.doclinksstart.links.md) | {
readonly dashboard: {
readonly drilldowns: string;
};
readonly filebeat: {
readonly base: string;
readonly installation: string;
readonly configuration: string;
readonly elasticsearchOutput: string;
readonly startup: string;
readonly exportedFields: string;
};
readonly auditbeat: {
readonly base: string;
};
readonly metricbeat: {
readonly base: string;
};
readonly heartbeat: {
readonly base: string;
};
readonly logstash: {
readonly base: string;
};
readonly functionbeat: {
readonly base: string;
};
readonly winlogbeat: {
readonly base: string;
};
readonly aggs: {
readonly date_histogram: string;
readonly date_range: string;
readonly filter: string;
readonly filters: string;
readonly geohash_grid: string;
readonly histogram: string;
readonly ip_range: string;
readonly range: string;
readonly significant_terms: string;
readonly terms: string;
readonly avg: string;
readonly avg_bucket: string;
readonly max_bucket: string;
readonly min_bucket: string;
readonly sum_bucket: string;
readonly cardinality: string;
readonly count: string;
readonly cumulative_sum: string;
readonly derivative: string;
readonly geo_bounds: string;
readonly geo_centroid: string;
readonly max: string;
readonly median: string;
readonly min: string;
readonly moving_avg: string;
readonly percentile_ranks: string;
readonly serial_diff: string;
readonly std_dev: string;
readonly sum: string;
readonly top_hits: string;
};
readonly scriptedFields: {
readonly scriptFields: string;
readonly scriptAggs: string;
readonly painless: string;
readonly painlessApi: string;
readonly painlessSyntax: string;
readonly luceneExpressions: string;
};
readonly indexPatterns: {
readonly loadingData: string;
readonly introduction: string;
};
readonly addData: string;
readonly kibana: string;
readonly siem: {
readonly guide: string;
readonly gettingStarted: string;
};
readonly query: {
readonly luceneQuerySyntax: string;
readonly queryDsl: string;
readonly kueryQuerySyntax: string;
};
readonly date: {
readonly dateMath: string;
};
readonly management: Record<string, string>;
} | | +| [links](./kibana-plugin-core-public.doclinksstart.links.md) | {
readonly dashboard: {
readonly drilldowns: string;
};
readonly filebeat: {
readonly base: string;
readonly installation: string;
readonly configuration: string;
readonly elasticsearchOutput: string;
readonly startup: string;
readonly exportedFields: string;
};
readonly auditbeat: {
readonly base: string;
};
readonly metricbeat: {
readonly base: string;
};
readonly heartbeat: {
readonly base: string;
};
readonly logstash: {
readonly base: string;
};
readonly functionbeat: {
readonly base: string;
};
readonly winlogbeat: {
readonly base: string;
};
readonly aggs: {
readonly date_histogram: string;
readonly date_range: string;
readonly filter: string;
readonly filters: string;
readonly geohash_grid: string;
readonly histogram: string;
readonly ip_range: string;
readonly range: string;
readonly significant_terms: string;
readonly terms: string;
readonly avg: string;
readonly avg_bucket: string;
readonly max_bucket: string;
readonly min_bucket: string;
readonly sum_bucket: string;
readonly cardinality: string;
readonly count: string;
readonly cumulative_sum: string;
readonly derivative: string;
readonly geo_bounds: string;
readonly geo_centroid: string;
readonly max: string;
readonly median: string;
readonly min: string;
readonly moving_avg: string;
readonly percentile_ranks: string;
readonly serial_diff: string;
readonly std_dev: string;
readonly sum: string;
readonly top_hits: string;
};
readonly scriptedFields: {
readonly scriptFields: string;
readonly scriptAggs: string;
readonly painless: string;
readonly painlessApi: string;
readonly painlessSyntax: string;
readonly luceneExpressions: string;
};
readonly indexPatterns: {
readonly loadingData: string;
readonly introduction: string;
};
readonly addData: string;
readonly kibana: string;
readonly siem: {
readonly guide: string;
readonly gettingStarted: string;
};
readonly query: {
readonly luceneQuerySyntax: string;
readonly queryDsl: string;
readonly kueryQuerySyntax: string;
};
readonly date: {
readonly dateMath: string;
};
readonly management: Record<string, string>;
readonly visualize: Record<string, string>;
} | | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.createtoomanyrequestserror.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.createtoomanyrequestserror.md new file mode 100644 index 000000000000..6d93dc97a107 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.createtoomanyrequestserror.md @@ -0,0 +1,23 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsErrorHelpers](./kibana-plugin-core-server.savedobjectserrorhelpers.md) > [createTooManyRequestsError](./kibana-plugin-core-server.savedobjectserrorhelpers.createtoomanyrequestserror.md) + +## SavedObjectsErrorHelpers.createTooManyRequestsError() method + +Signature: + +```typescript +static createTooManyRequestsError(type: string, id: string): DecoratedError; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| type | string | | +| id | string | | + +Returns: + +`DecoratedError` + diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.decoratetoomanyrequestserror.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.decoratetoomanyrequestserror.md new file mode 100644 index 000000000000..46c94e1756ed --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.decoratetoomanyrequestserror.md @@ -0,0 +1,23 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsErrorHelpers](./kibana-plugin-core-server.savedobjectserrorhelpers.md) > [decorateTooManyRequestsError](./kibana-plugin-core-server.savedobjectserrorhelpers.decoratetoomanyrequestserror.md) + +## SavedObjectsErrorHelpers.decorateTooManyRequestsError() method + +Signature: + +```typescript +static decorateTooManyRequestsError(error: Error, reason?: string): DecoratedError; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| error | Error | | +| reason | string | | + +Returns: + +`DecoratedError` + diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.istoomanyrequestserror.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.istoomanyrequestserror.md new file mode 100644 index 000000000000..4422966ee3e5 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.istoomanyrequestserror.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsErrorHelpers](./kibana-plugin-core-server.savedobjectserrorhelpers.md) > [isTooManyRequestsError](./kibana-plugin-core-server.savedobjectserrorhelpers.istoomanyrequestserror.md) + +## SavedObjectsErrorHelpers.isTooManyRequestsError() method + +Signature: + +```typescript +static isTooManyRequestsError(error: Error | DecoratedError): boolean; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| error | Error | DecoratedError | | + +Returns: + +`boolean` + diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.md index 7874be311d52..a2eff4dd99ea 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.md @@ -19,6 +19,7 @@ export declare class SavedObjectsErrorHelpers | [createConflictError(type, id)](./kibana-plugin-core-server.savedobjectserrorhelpers.createconflicterror.md) | static | | | [createGenericNotFoundError(type, id)](./kibana-plugin-core-server.savedobjectserrorhelpers.creategenericnotfounderror.md) | static | | | [createInvalidVersionError(versionInput)](./kibana-plugin-core-server.savedobjectserrorhelpers.createinvalidversionerror.md) | static | | +| [createTooManyRequestsError(type, id)](./kibana-plugin-core-server.savedobjectserrorhelpers.createtoomanyrequestserror.md) | static | | | [createUnsupportedTypeError(type)](./kibana-plugin-core-server.savedobjectserrorhelpers.createunsupportedtypeerror.md) | static | | | [decorateBadRequestError(error, reason)](./kibana-plugin-core-server.savedobjectserrorhelpers.decoratebadrequesterror.md) | static | | | [decorateConflictError(error, reason)](./kibana-plugin-core-server.savedobjectserrorhelpers.decorateconflicterror.md) | static | | @@ -28,6 +29,7 @@ export declare class SavedObjectsErrorHelpers | [decorateGeneralError(error, reason)](./kibana-plugin-core-server.savedobjectserrorhelpers.decorategeneralerror.md) | static | | | [decorateNotAuthorizedError(error, reason)](./kibana-plugin-core-server.savedobjectserrorhelpers.decoratenotauthorizederror.md) | static | | | [decorateRequestEntityTooLargeError(error, reason)](./kibana-plugin-core-server.savedobjectserrorhelpers.decoraterequestentitytoolargeerror.md) | static | | +| [decorateTooManyRequestsError(error, reason)](./kibana-plugin-core-server.savedobjectserrorhelpers.decoratetoomanyrequestserror.md) | static | | | [isBadRequestError(error)](./kibana-plugin-core-server.savedobjectserrorhelpers.isbadrequesterror.md) | static | | | [isConflictError(error)](./kibana-plugin-core-server.savedobjectserrorhelpers.isconflicterror.md) | static | | | [isEsCannotExecuteScriptError(error)](./kibana-plugin-core-server.savedobjectserrorhelpers.isescannotexecutescripterror.md) | static | | @@ -38,4 +40,5 @@ export declare class SavedObjectsErrorHelpers | [isNotFoundError(error)](./kibana-plugin-core-server.savedobjectserrorhelpers.isnotfounderror.md) | static | | | [isRequestEntityTooLargeError(error)](./kibana-plugin-core-server.savedobjectserrorhelpers.isrequestentitytoolargeerror.md) | static | | | [isSavedObjectsClientError(error)](./kibana-plugin-core-server.savedobjectserrorhelpers.issavedobjectsclienterror.md) | static | | +| [isTooManyRequestsError(error)](./kibana-plugin-core-server.savedobjectserrorhelpers.istoomanyrequestserror.md) | static | | diff --git a/docs/development/core/server/kibana-plugin-core-server.statusservicesetup.md b/docs/development/core/server/kibana-plugin-core-server.statusservicesetup.md index 0551a217520a..3d3b73ccda25 100644 --- a/docs/development/core/server/kibana-plugin-core-server.statusservicesetup.md +++ b/docs/development/core/server/kibana-plugin-core-server.statusservicesetup.md @@ -17,4 +17,5 @@ export interface StatusServiceSetup | Property | Type | Description | | --- | --- | --- | | [core$](./kibana-plugin-core-server.statusservicesetup.core_.md) | Observable<CoreStatus> | Current status for all Core services. | +| [overall$](./kibana-plugin-core-server.statusservicesetup.overall_.md) | Observable<ServiceStatus> | Overall system status for all of Kibana. | diff --git a/docs/development/core/server/kibana-plugin-core-server.statusservicesetup.overall_.md b/docs/development/core/server/kibana-plugin-core-server.statusservicesetup.overall_.md new file mode 100644 index 000000000000..bb7c31311d52 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.statusservicesetup.overall_.md @@ -0,0 +1,20 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [StatusServiceSetup](./kibana-plugin-core-server.statusservicesetup.md) > [overall$](./kibana-plugin-core-server.statusservicesetup.overall_.md) + +## StatusServiceSetup.overall$ property + +Overall system status for all of Kibana. + +Signature: + +```typescript +overall$: Observable; +``` + +## Remarks + +The level of the overall status will reflect the most severe status of any core service or plugin. + +Exposed only for reporting purposes to outside systems and should not be used by plugins. Instead, plugins should only depend on the statuses of [Core](./kibana-plugin-core-server.statusservicesetup.core_.md) or their dependencies. + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iessearchresponse.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iessearchresponse.md index 041d79de3282..7c9a6aa70246 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iessearchresponse.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iessearchresponse.md @@ -7,7 +7,7 @@ Signature: ```typescript -export interface IEsSearchResponse extends IKibanaSearchResponse +export interface IEsSearchResponse extends IKibanaSearchResponse ``` ## Properties @@ -16,5 +16,5 @@ export interface IEsSearchResponse extends IKibanaSearchResponse | --- | --- | --- | | [isPartial](./kibana-plugin-plugins-data-public.iessearchresponse.ispartial.md) | boolean | Indicates whether the results returned are complete or partial | | [isRunning](./kibana-plugin-plugins-data-public.iessearchresponse.isrunning.md) | boolean | Indicates whether async search is still in flight | -| [rawResponse](./kibana-plugin-plugins-data-public.iessearchresponse.rawresponse.md) | SearchResponse<any> | | +| [rawResponse](./kibana-plugin-plugins-data-public.iessearchresponse.rawresponse.md) | SearchResponse<Source> | | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iessearchresponse.rawresponse.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iessearchresponse.rawresponse.md index d7912f377ca9..f4648143ebc2 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iessearchresponse.rawresponse.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iessearchresponse.rawresponse.md @@ -7,5 +7,5 @@ Signature: ```typescript -rawResponse: SearchResponse; +rawResponse: SearchResponse; ``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchgeneric.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchgeneric.md index 3bd6a398c8df..861b59e73ef0 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchgeneric.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchgeneric.md @@ -7,5 +7,5 @@ Signature: ```typescript -export declare type ISearchGeneric = (request: IEsSearchRequest, options?: ISearchOptions) => Observable; +export declare type ISearchGeneric = (request: SearchStrategyRequest, options?: ISearchOptions) => Observable; ``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md index dc83cfb930d7..a5453c7c51d5 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md @@ -156,8 +156,6 @@ | [RangeFilterMeta](./kibana-plugin-plugins-data-public.rangefiltermeta.md) | | | [SavedQueryTimeFilter](./kibana-plugin-plugins-data-public.savedquerytimefilter.md) | | | [SearchBarProps](./kibana-plugin-plugins-data-public.searchbarprops.md) | | -| [SearchRequest](./kibana-plugin-plugins-data-public.searchrequest.md) | | -| [SearchResponse](./kibana-plugin-plugins-data-public.searchresponse.md) | | | [StatefulSearchBarProps](./kibana-plugin-plugins-data-public.statefulsearchbarprops.md) | | | [TabbedAggRow](./kibana-plugin-plugins-data-public.tabbedaggrow.md) | \* | | [TimefilterContract](./kibana-plugin-plugins-data-public.timefiltercontract.md) | | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchrequest.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchrequest.md deleted file mode 100644 index dbb465839e52..000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchrequest.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [SearchRequest](./kibana-plugin-plugins-data-public.searchrequest.md) - -## SearchRequest type - -Signature: - -```typescript -export declare type SearchRequest = any; -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchresponse.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchresponse.md deleted file mode 100644 index 6da31c8bced7..000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchresponse.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [SearchResponse](./kibana-plugin-plugins-data-public.searchresponse.md) - -## SearchResponse type - -Signature: - -```typescript -export declare type SearchResponse = any; -``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.iessearchresponse.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.iessearchresponse.md index 0407dce5fe41..55c0399e90e2 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.iessearchresponse.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.iessearchresponse.md @@ -7,7 +7,7 @@ Signature: ```typescript -export interface IEsSearchResponse extends IKibanaSearchResponse +export interface IEsSearchResponse extends IKibanaSearchResponse ``` ## Properties @@ -16,5 +16,5 @@ export interface IEsSearchResponse extends IKibanaSearchResponse | --- | --- | --- | | [isPartial](./kibana-plugin-plugins-data-server.iessearchresponse.ispartial.md) | boolean | Indicates whether the results returned are complete or partial | | [isRunning](./kibana-plugin-plugins-data-server.iessearchresponse.isrunning.md) | boolean | Indicates whether async search is still in flight | -| [rawResponse](./kibana-plugin-plugins-data-server.iessearchresponse.rawresponse.md) | SearchResponse<any> | | +| [rawResponse](./kibana-plugin-plugins-data-server.iessearchresponse.rawresponse.md) | SearchResponse<Source> | | diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.iessearchresponse.rawresponse.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.iessearchresponse.rawresponse.md index 0ee1691d0f69..9987debfa551 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.iessearchresponse.rawresponse.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.iessearchresponse.rawresponse.md @@ -7,5 +7,5 @@ Signature: ```typescript -rawResponse: SearchResponse; +rawResponse: SearchResponse; ``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchsetup.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchsetup.md index e5b11a0b997e..ac2ae13372f7 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchsetup.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchsetup.md @@ -15,6 +15,6 @@ export interface ISearchSetup | Property | Type | Description | | --- | --- | --- | | [aggs](./kibana-plugin-plugins-data-server.isearchsetup.aggs.md) | AggsSetup | | -| [registerSearchStrategy](./kibana-plugin-plugins-data-server.isearchsetup.registersearchstrategy.md) | (name: string, strategy: ISearchStrategy) => void | Extension point exposed for other plugins to register their own search strategies. | +| [registerSearchStrategy](./kibana-plugin-plugins-data-server.isearchsetup.registersearchstrategy.md) | <SearchStrategyRequest extends IEsSearchRequest = IEsSearchRequest, SearchStrategyResponse extends IEsSearchResponse = IEsSearchResponse>(name: string, strategy: ISearchStrategy<SearchStrategyRequest, SearchStrategyResponse>) => void | Extension point exposed for other plugins to register their own search strategies. | | [usage](./kibana-plugin-plugins-data-server.isearchsetup.usage.md) | SearchUsage | Used internally for telemetry | diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchsetup.registersearchstrategy.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchsetup.registersearchstrategy.md index 73c575e7095e..f20c6f491106 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchsetup.registersearchstrategy.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchsetup.registersearchstrategy.md @@ -9,5 +9,5 @@ Extension point exposed for other plugins to register their own search strategie Signature: ```typescript -registerSearchStrategy: (name: string, strategy: ISearchStrategy) => void; +registerSearchStrategy: (name: string, strategy: ISearchStrategy) => void; ``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchstart.getsearchstrategy.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchstart.getsearchstrategy.md index 970b2811a574..398ea2164194 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchstart.getsearchstrategy.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchstart.getsearchstrategy.md @@ -9,5 +9,5 @@ Get other registered search strategies. For example, if a new strategy needs to Signature: ```typescript -getSearchStrategy: (name: string) => ISearchStrategy; +getSearchStrategy: (name: string) => ISearchStrategy; ``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchstart.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchstart.md index 3762da963d4d..62d954cb80eb 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchstart.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchstart.md @@ -7,7 +7,7 @@ Signature: ```typescript -export interface ISearchStart +export interface ISearchStart ``` ## Properties @@ -15,6 +15,6 @@ export interface ISearchStart | Property | Type | Description | | --- | --- | --- | | [aggs](./kibana-plugin-plugins-data-server.isearchstart.aggs.md) | AggsStart | | -| [getSearchStrategy](./kibana-plugin-plugins-data-server.isearchstart.getsearchstrategy.md) | (name: string) => ISearchStrategy | Get other registered search strategies. For example, if a new strategy needs to use the already-registered ES search strategy, it can use this function to accomplish that. | +| [getSearchStrategy](./kibana-plugin-plugins-data-server.isearchstart.getsearchstrategy.md) | (name: string) => ISearchStrategy<SearchStrategyRequest, SearchStrategyResponse> | Get other registered search strategies. For example, if a new strategy needs to use the already-registered ES search strategy, it can use this function to accomplish that. | | [search](./kibana-plugin-plugins-data-server.isearchstart.search.md) | (context: RequestHandlerContext, request: IKibanaSearchRequest, options: ISearchOptions) => Promise<IKibanaSearchResponse> | | diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchstrategy.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchstrategy.md index d54e027c4b84..dc076455ab27 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchstrategy.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchstrategy.md @@ -9,7 +9,7 @@ Search strategy interface contains a search method that takes in a request and r Signature: ```typescript -export interface ISearchStrategy +export interface ISearchStrategy ``` ## Properties @@ -17,5 +17,5 @@ export interface ISearchStrategy | Property | Type | Description | | --- | --- | --- | | [cancel](./kibana-plugin-plugins-data-server.isearchstrategy.cancel.md) | (context: RequestHandlerContext, id: string) => Promise<void> | | -| [search](./kibana-plugin-plugins-data-server.isearchstrategy.search.md) | (context: RequestHandlerContext, request: IEsSearchRequest, options?: ISearchOptions) => Promise<IEsSearchResponse> | | +| [search](./kibana-plugin-plugins-data-server.isearchstrategy.search.md) | (context: RequestHandlerContext, request: SearchStrategyRequest, options?: ISearchOptions) => Promise<SearchStrategyResponse> | | diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchstrategy.search.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchstrategy.search.md index 1a225d0c9aea..45f43648ab60 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchstrategy.search.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchstrategy.search.md @@ -7,5 +7,5 @@ Signature: ```typescript -search: (context: RequestHandlerContext, request: IEsSearchRequest, options?: ISearchOptions) => Promise; +search: (context: RequestHandlerContext, request: SearchStrategyRequest, options?: ISearchOptions) => Promise; ``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.plugin.start.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.plugin.start.md index 74bffc516725..2d9104ef894b 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.plugin.start.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.plugin.start.md @@ -8,7 +8,7 @@ ```typescript start(core: CoreStart): { - search: ISearchStart; + search: ISearchStart>; fieldFormats: { fieldFormatServiceFactory: (uiSettings: import("../../../core/server").IUiSettingsClient) => Promise; }; @@ -27,7 +27,7 @@ start(core: CoreStart): { Returns: `{ - search: ISearchStart; + search: ISearchStart>; fieldFormats: { fieldFormatServiceFactory: (uiSettings: import("../../../core/server").IUiSettingsClient) => Promise; }; diff --git a/docs/redirects.asciidoc b/docs/redirects.asciidoc index 58687d99627b..1a20c1df582e 100644 --- a/docs/redirects.asciidoc +++ b/docs/redirects.asciidoc @@ -90,3 +90,8 @@ Watcher error reports have been removed and replaced with Kibana's <>. + +[role="exclude",id="development-security-rbac"] +== Role-based access control + +This content has moved to the <> page. diff --git a/packages/elastic-datemath/.babelrc b/packages/elastic-datemath/.babelrc deleted file mode 100644 index 64daee413aeb..000000000000 --- a/packages/elastic-datemath/.babelrc +++ /dev/null @@ -1,4 +0,0 @@ -{ - "presets": ["@babel/preset-env"], - "plugins": ["add-module-exports"] -} diff --git a/packages/elastic-datemath/.npmignore b/packages/elastic-datemath/.npmignore index a56a2f3ff793..591be7afd166 100644 --- a/packages/elastic-datemath/.npmignore +++ b/packages/elastic-datemath/.npmignore @@ -1,6 +1,2 @@ -/src -/test /tsconfig.json -/.babelrc -/yarn.lock /__tests__ diff --git a/packages/elastic-datemath/__tests__/index.js b/packages/elastic-datemath/__tests__/index.js index 8f06ff0ab4aa..1a61021b48a6 100644 --- a/packages/elastic-datemath/__tests__/index.js +++ b/packages/elastic-datemath/__tests__/index.js @@ -17,10 +17,10 @@ * under the License. */ -import dateMath from '../src'; -import moment from 'moment'; -import sinon from 'sinon'; -import expect from '@kbn/expect'; +const dateMath = require('../index'); +const moment = require('moment'); +const sinon = require('sinon'); +const expect = require('@kbn/expect'); /** * Require a new instance of the moment library, bypassing the require cache. diff --git a/packages/elastic-datemath/src/index.d.ts b/packages/elastic-datemath/index.d.ts similarity index 100% rename from packages/elastic-datemath/src/index.d.ts rename to packages/elastic-datemath/index.d.ts diff --git a/packages/elastic-datemath/src/index.js b/packages/elastic-datemath/index.js similarity index 98% rename from packages/elastic-datemath/src/index.js rename to packages/elastic-datemath/index.js index 52ce12ddf702..8a69d251d057 100644 --- a/packages/elastic-datemath/src/index.js +++ b/packages/elastic-datemath/index.js @@ -17,7 +17,7 @@ * under the License. */ -import moment from 'moment'; +const moment = require('moment'); const unitsMap = { ms: { weight: 1, type: 'fixed', base: 1 }, @@ -151,7 +151,7 @@ function parseDateMath(mathString, time, roundUp) { return dateTime; } -export default { +module.exports = { parse: parse, unitsMap: Object.freeze(unitsMap), units: Object.freeze(units), diff --git a/packages/elastic-datemath/package.json b/packages/elastic-datemath/package.json index ad4190f98143..04bb96206e47 100644 --- a/packages/elastic-datemath/package.json +++ b/packages/elastic-datemath/package.json @@ -3,22 +3,8 @@ "version": "5.0.3", "description": "elasticsearch datemath parser, used in kibana", "license": "Apache-2.0", - "main": "target/index.js", - "typings": "target/index.d.ts", - "scripts": { - "build": "babel src --out-dir target --copy-files", - "kbn:bootstrap": "yarn build --quiet", - "kbn:watch": "yarn build --watch" - }, - "devDependencies": { - "@babel/cli": "^7.10.5", - "@babel/preset-env": "^7.11.0", - "babel-plugin-add-module-exports": "^1.0.2", - "moment": "^2.24.0" - }, - "dependencies": { - "tslib": "^2.0.0" - }, + "main": "index.js", + "typings": "index.d.ts", "peerDependencies": { "moment": "^2.24.0" } diff --git a/packages/elastic-datemath/tsconfig.json b/packages/elastic-datemath/tsconfig.json index c23b6635a5c1..3604f1004cf6 100644 --- a/packages/elastic-datemath/tsconfig.json +++ b/packages/elastic-datemath/tsconfig.json @@ -1,10 +1,6 @@ { "extends": "../../tsconfig.json", - "compilerOptions": { - "declaration": true, - "outDir": "./target" - }, "include": [ - "./src/**/*.ts" - ] + "index.d.ts" + ], } diff --git a/packages/elastic-datemath/yarn.lock b/packages/elastic-datemath/yarn.lock deleted file mode 120000 index 3f82ebc9cdba..000000000000 --- a/packages/elastic-datemath/yarn.lock +++ /dev/null @@ -1 +0,0 @@ -../../yarn.lock \ No newline at end of file diff --git a/packages/kbn-dev-utils/src/plugin_list/discover_plugins.ts b/packages/kbn-dev-utils/src/plugin_list/discover_plugins.ts index 733b9f23a539..783d584656b1 100644 --- a/packages/kbn-dev-utils/src/plugin_list/discover_plugins.ts +++ b/packages/kbn-dev-utils/src/plugin_list/discover_plugins.ts @@ -25,12 +25,14 @@ import cheerio from 'cheerio'; import { REPO_ROOT } from '../repo_root'; import { simpleKibanaPlatformPluginDiscovery } from '../simple_kibana_platform_plugin_discovery'; +import { extractAsciidocInfo } from './extract_asciidoc_info'; export interface Plugin { id: string; relativeDir?: string; relativeReadmePath?: string; readmeSnippet?: string; + readmeAsciidocAnchor?: string; } export type Plugins = Plugin[]; @@ -38,14 +40,29 @@ export type Plugins = Plugin[]; const getReadmeName = (directory: string) => Fs.readdirSync(directory).find((name) => name.toLowerCase() === 'readme.md'); +const getReadmeAsciidocName = (directory: string) => + Fs.readdirSync(directory).find((name) => name.toLowerCase() === 'readme.asciidoc'); + export const discoverPlugins = (pluginsRootDir: string): Plugins => simpleKibanaPlatformPluginDiscovery([pluginsRootDir], []).map( ({ directory, manifest: { id } }): Plugin => { const readmeName = getReadmeName(directory); + const readmeAsciidocName = getReadmeAsciidocName(directory); let relativeReadmePath: string | undefined; let readmeSnippet: string | undefined; - if (readmeName) { + let readmeAsciidocAnchor: string | undefined; + + if (readmeAsciidocName) { + const readmePath = Path.resolve(directory, readmeAsciidocName); + relativeReadmePath = Path.relative(REPO_ROOT, readmePath); + + const readmeText = Fs.readFileSync(relativeReadmePath).toString(); + + const { firstParagraph, anchor } = extractAsciidocInfo(readmeText); + readmeSnippet = firstParagraph; + readmeAsciidocAnchor = anchor; + } else if (readmeName) { const readmePath = Path.resolve(directory, readmeName); relativeReadmePath = Path.relative(REPO_ROOT, readmePath); @@ -64,6 +81,7 @@ export const discoverPlugins = (pluginsRootDir: string): Plugins => relativeReadmePath, relativeDir: relativeReadmePath || Path.relative(REPO_ROOT, directory), readmeSnippet, + readmeAsciidocAnchor, }; } ); diff --git a/packages/kbn-dev-utils/src/plugin_list/extract_asciidoc_info.test.ts b/packages/kbn-dev-utils/src/plugin_list/extract_asciidoc_info.test.ts new file mode 100644 index 000000000000..baa88bbe1d2f --- /dev/null +++ b/packages/kbn-dev-utils/src/plugin_list/extract_asciidoc_info.test.ts @@ -0,0 +1,86 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { extractAsciidocInfo } from './extract_asciidoc_info'; + +it('Returns the info and anchor when there is only one paragraph', () => { + const { firstParagraph, anchor } = extractAsciidocInfo( + `[[this-is-the-anchor]] +== I'm the heading! + +Hello + +I'm an intro paragraph!` + ); + + expect(firstParagraph).toEqual(`Hello\n\nI'm an intro paragraph!`); + expect(anchor).toEqual('this-is-the-anchor'); +}); + +it('Returns the info and anchor when there are multiple paragraphs without an anchor', () => { + const { firstParagraph, anchor } = extractAsciidocInfo( + `[[this-is-the-anchor]] +== Heading here + +Intro. + +=== Another heading + +More details` + ); + + expect(firstParagraph).toEqual(`Intro.`); + expect(anchor).toEqual('this-is-the-anchor'); +}); + +it('Returns the info and anchor when there are multiple paragraphs with anchors', () => { + const { firstParagraph, anchor } = extractAsciidocInfo( + `[[this-is-the-anchor]] +== Heading here + +Intro. + +[[an-anchor]] +=== Another heading + +More details + ` + ); + + expect(firstParagraph).toEqual(`Intro.`); + expect(anchor).toEqual('this-is-the-anchor'); +}); + +it('Returns the info and anchor when there are multiple paragraphs with discrete prefixes', () => { + const { firstParagraph, anchor } = extractAsciidocInfo( + `[[this-is-the-anchor]] +== Heading here + +Intro. + +[discrete] +=== Another heading + +More details + ` + ); + + expect(firstParagraph).toEqual(`Intro.`); + expect(anchor).toEqual('this-is-the-anchor'); +}); diff --git a/packages/kbn-dev-utils/src/plugin_list/extract_asciidoc_info.ts b/packages/kbn-dev-utils/src/plugin_list/extract_asciidoc_info.ts new file mode 100644 index 000000000000..85b63141a217 --- /dev/null +++ b/packages/kbn-dev-utils/src/plugin_list/extract_asciidoc_info.ts @@ -0,0 +1,32 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export function extractAsciidocInfo(text: string): { firstParagraph?: string; anchor?: string } { + // First group is to grab the anchor - \[\[(.*)\]\] + // Tecond group, (== ), removes the equals from the header + // Third group could perhaps be done better, but is essentially: + // If there is a sub heading after the intro, match the intro and stop - (([\s\S]*?)(?=\=\=\=|\[\[))) + // If there is not a sub heading after the intro, match the intro - ([\s\S]*) + const matchAnchorAndIntro = /\[\[(.*)\]\]\n(== .*)\n(((([\s\S]*?)(?=\=\=\=|\[)))|([\s\S]*))/gm; + + const matches = matchAnchorAndIntro.exec(text); + const firstParagraph = matches && matches.length >= 4 ? matches[3].toString().trim() : undefined; + const anchor = matches && matches.length >= 2 ? matches[1].toString().trim() : undefined; + return { firstParagraph, anchor }; +} diff --git a/packages/kbn-dev-utils/src/plugin_list/generate_plugin_list.ts b/packages/kbn-dev-utils/src/plugin_list/generate_plugin_list.ts index f0f799862e24..43dac1cb7d41 100644 --- a/packages/kbn-dev-utils/src/plugin_list/generate_plugin_list.ts +++ b/packages/kbn-dev-utils/src/plugin_list/generate_plugin_list.ts @@ -24,21 +24,29 @@ import normalizePath from 'normalize-path'; import { REPO_ROOT } from '../repo_root'; import { Plugins } from './discover_plugins'; -function* printPlugins(plugins: Plugins) { +function* printPlugins(plugins: Plugins, includes: string[]) { for (const plugin of plugins) { const path = plugin.relativeReadmePath || plugin.relativeDir; yield ''; - yield `- {kib-repo}blob/{branch}/${path}[${plugin.id}]`; + + if (plugin.readmeAsciidocAnchor) { + yield `|<<${plugin.readmeAsciidocAnchor}>>`; + + includes.push(`include::{kibana-root}/${path}[leveloffset=+1]`); + } else { + yield `|{kib-repo}blob/{branch}/${path}[${plugin.id}]`; + } if (!plugin.relativeReadmePath || plugin.readmeSnippet) { - yield ''; - yield plugin.readmeSnippet || 'WARNING: Missing README.'; + yield plugin.readmeSnippet ? `|${plugin.readmeSnippet}` : '|WARNING: Missing README.'; yield ''; } } } export function generatePluginList(ossPlugins: Plugins, xpackPlugins: Plugins) { + const includes: string[] = []; + return `//// NOTE: @@ -53,32 +61,33 @@ NOTE: //// -[[code-exploration]] -== Exploring Kibana code +[[plugin-list]] +== List of {kib} plugins -The goals of our folder heirarchy are: +[discrete] +=== src/plugins -- Easy for developers to know where to add new services, plugins and applications. -- Easy for developers to know where to find the code from services, plugins and applications. -- Easy to browse and understand our folder structure. +[%header,cols=2*] +|=== +|Name +|Description -To that aim, we strive to: +${Array.from(printPlugins(ossPlugins, includes)).join('\n')} -- Avoid too many files in any given folder. -- Choose clear, unambigious folder names. -- Organize by domain. -- Every folder should contain a README that describes the contents of that folder. +|=== [discrete] -[[kibana-services-applications]] -=== Services and Applications +=== x-pack/plugins -[discrete] -==== src/plugins -${Array.from(printPlugins(ossPlugins)).join('\n')} +[%header,cols=2*] +|=== +|Name +|Description -[discrete] -==== x-pack/plugins -${Array.from(printPlugins(xpackPlugins)).join('\n')} +${Array.from(printPlugins(xpackPlugins, includes)).join('\n')} + +|=== + +${Array.from(includes).join('\n')} `; } diff --git a/packages/kbn-dev-utils/src/plugin_list/run_plugin_list_cli.ts b/packages/kbn-dev-utils/src/plugin_list/run_plugin_list_cli.ts index 817534ba5b15..553eb1dd8afa 100644 --- a/packages/kbn-dev-utils/src/plugin_list/run_plugin_list_cli.ts +++ b/packages/kbn-dev-utils/src/plugin_list/run_plugin_list_cli.ts @@ -28,10 +28,7 @@ import { generatePluginList } from './generate_plugin_list'; const OSS_PLUGIN_DIR = Path.resolve(REPO_ROOT, 'src/plugins'); const XPACK_PLUGIN_DIR = Path.resolve(REPO_ROOT, 'x-pack/plugins'); -const OUTPUT_PATH = Path.resolve( - REPO_ROOT, - 'docs/developer/architecture/code-exploration.asciidoc' -); +const OUTPUT_PATH = Path.resolve(REPO_ROOT, 'docs/developer/plugin-list.asciidoc'); export function runPluginListCli() { run(async ({ log }) => { diff --git a/packages/kbn-telemetry-tools/src/cli/run_telemetry_check.ts b/packages/kbn-telemetry-tools/src/cli/run_telemetry_check.ts index 116c484a5c36..2f85fd2cdd2a 100644 --- a/packages/kbn-telemetry-tools/src/cli/run_telemetry_check.ts +++ b/packages/kbn-telemetry-tools/src/cli/run_telemetry_check.ts @@ -65,7 +65,8 @@ export function runTelemetryCheck() { }, { title: 'Checking Matching collector.schema against stored json files', - task: (context) => new Listr(checkMatchingSchemasTask(context), { exitOnError: true }), + task: (context) => + new Listr(checkMatchingSchemasTask(context, !fix), { exitOnError: true }), }, { enabled: (_) => fix, diff --git a/packages/kbn-telemetry-tools/src/tools/tasks/check_matching_schemas_task.ts b/packages/kbn-telemetry-tools/src/tools/tasks/check_matching_schemas_task.ts index a1f23bcd44c7..2f73a0ee6ad4 100644 --- a/packages/kbn-telemetry-tools/src/tools/tasks/check_matching_schemas_task.ts +++ b/packages/kbn-telemetry-tools/src/tools/tasks/check_matching_schemas_task.ts @@ -22,7 +22,7 @@ import { TaskContext } from './task_context'; import { checkMatchingMapping } from '../check_collector_integrity'; import { readFileAsync } from '../utils'; -export function checkMatchingSchemasTask({ roots }: TaskContext) { +export function checkMatchingSchemasTask({ roots }: TaskContext, throwOnDiff: boolean) { return roots.map((root) => ({ task: async () => { const fullPath = path.resolve(process.cwd(), root.config.output); @@ -31,8 +31,16 @@ export function checkMatchingSchemasTask({ roots }: TaskContext) { if (root.parsedCollections) { const differences = checkMatchingMapping(root.parsedCollections, esMapping); - root.esMappingDiffs = Object.keys(differences); + if (root.esMappingDiffs.length && throwOnDiff) { + throw Error( + `The following changes must be persisted in ${fullPath} file. Use '--fix' to update.\n${JSON.stringify( + differences, + null, + 2 + )}` + ); + } } }, title: `Checking in ${root.config.root}`, diff --git a/src/core/server/core_app/integration_tests/static_assets.test.ts b/src/core/server/core_app/integration_tests/static_assets.test.ts index 23125cb3a670..160ef064a14d 100644 --- a/src/core/server/core_app/integration_tests/static_assets.test.ts +++ b/src/core/server/core_app/integration_tests/static_assets.test.ts @@ -23,7 +23,7 @@ describe('Platform assets', function () { let root: Root; beforeAll(async function () { - root = kbnTestServer.createRoot(); + root = kbnTestServer.createRoot({ plugins: { initialize: false } }); await root.setup(); await root.start(); diff --git a/src/core/server/elasticsearch/client/configure_client.test.ts b/src/core/server/elasticsearch/client/configure_client.test.ts index 11e3199a79fd..716e2fd98a5e 100644 --- a/src/core/server/elasticsearch/client/configure_client.test.ts +++ b/src/core/server/elasticsearch/client/configure_client.test.ts @@ -157,6 +157,44 @@ describe('configureClient', () => { `); }); + it('logs default error info when the error response body is empty', () => { + const client = configureClient(config, { logger, scoped: false }); + + let response = createApiResponse({ + statusCode: 400, + headers: {}, + body: { + error: {}, + }, + }); + client.emit('response', new errors.ResponseError(response), response); + + expect(loggingSystemMock.collect(logger).error).toMatchInlineSnapshot(` + Array [ + Array [ + "[ResponseError]: Response Error", + ], + ] + `); + + logger.error.mockClear(); + + response = createApiResponse({ + statusCode: 400, + headers: {}, + body: {} as any, + }); + client.emit('response', new errors.ResponseError(response), response); + + expect(loggingSystemMock.collect(logger).error).toMatchInlineSnapshot(` + Array [ + Array [ + "[ResponseError]: Response Error", + ], + ] + `); + }); + it('logs each queries if `logQueries` is true', () => { const client = configureClient( createFakeConfig({ diff --git a/src/core/server/elasticsearch/client/configure_client.ts b/src/core/server/elasticsearch/client/configure_client.ts index 9746ecb538b7..a77734481306 100644 --- a/src/core/server/elasticsearch/client/configure_client.ts +++ b/src/core/server/elasticsearch/client/configure_client.ts @@ -21,7 +21,6 @@ import { stringify } from 'querystring'; import { Client } from '@elastic/elasticsearch'; import { Logger } from '../../logging'; import { parseClientOptions, ElasticsearchClientConfig } from './client_config'; -import { isResponseError } from './errors'; export const configureClient = ( config: ElasticsearchClientConfig, @@ -39,10 +38,8 @@ const addLogging = (client: Client, logger: Logger, logQueries: boolean) => { client.on('response', (error, event) => { if (error) { const errorMessage = - // error details for response errors provided by elasticsearch - isResponseError(error) - ? `[${event.body.error.type}]: ${event.body.error.reason}` - : `[${error.name}]: ${error.message}`; + // error details for response errors provided by elasticsearch, defaults to error name/message + `[${event.body?.error?.type ?? error.name}]: ${event.body?.error?.reason ?? error.message}`; logger.error(errorMessage); } diff --git a/src/core/server/http_resources/integration_tests/http_resources_service.test.ts b/src/core/server/http_resources/integration_tests/http_resources_service.test.ts index 0a5daa02e17e..eee7dc278607 100644 --- a/src/core/server/http_resources/integration_tests/http_resources_service.test.ts +++ b/src/core/server/http_resources/integration_tests/http_resources_service.test.ts @@ -28,6 +28,7 @@ describe('http resources service', () => { csp: { rules: [defaultCspRules], }, + plugins: { initialize: false }, }); }, 30000); diff --git a/src/core/server/legacy/integration_tests/legacy_service.test.ts b/src/core/server/legacy/integration_tests/legacy_service.test.ts index e8bcf7a42d19..1dc8d53e7c3d 100644 --- a/src/core/server/legacy/integration_tests/legacy_service.test.ts +++ b/src/core/server/legacy/integration_tests/legacy_service.test.ts @@ -22,7 +22,10 @@ describe('legacy service', () => { describe('http server', () => { let root: ReturnType; beforeEach(() => { - root = kbnTestServer.createRoot({ migrations: { skip: true } }); + root = kbnTestServer.createRoot({ + migrations: { skip: true }, + plugins: { initialize: false }, + }); }, 30000); afterEach(async () => await root.shutdown()); diff --git a/src/core/server/legacy/integration_tests/logging.test.ts b/src/core/server/legacy/integration_tests/logging.test.ts index 66234f677903..2581c85debf2 100644 --- a/src/core/server/legacy/integration_tests/logging.test.ts +++ b/src/core/server/legacy/integration_tests/logging.test.ts @@ -28,6 +28,7 @@ import { LegacyLoggingConfig } from '../config/legacy_object_to_config_adapter'; function createRoot(legacyLoggingConfig: LegacyLoggingConfig = {}) { return kbnTestServer.createRoot({ migrations: { skip: true }, // otherwise stuck in polling ES + plugins: { initialize: false }, logging: { // legacy platform config silent: false, diff --git a/src/core/server/legacy/legacy_service.ts b/src/core/server/legacy/legacy_service.ts index 0c1e8562a1de..f39282a6f9cb 100644 --- a/src/core/server/legacy/legacy_service.ts +++ b/src/core/server/legacy/legacy_service.ts @@ -322,6 +322,7 @@ export class LegacyService implements CoreService { }, status: { core$: setupDeps.core.status.core$, + overall$: setupDeps.core.status.overall$, }, uiSettings: { register: setupDeps.core.uiSettings.register, diff --git a/src/core/server/plugins/plugin_context.ts b/src/core/server/plugins/plugin_context.ts index 5235f3ee6d58..62058f6d478e 100644 --- a/src/core/server/plugins/plugin_context.ts +++ b/src/core/server/plugins/plugin_context.ts @@ -178,6 +178,7 @@ export function createPluginSetupContext( }, status: { core$: deps.status.core$, + overall$: deps.status.overall$, }, uiSettings: { register: deps.uiSettings.register, diff --git a/src/core/server/saved_objects/routes/integration_tests/migrate.test.ts b/src/core/server/saved_objects/routes/integration_tests/migrate.test.ts index 5bc7d126ace0..7a0e39b71afb 100644 --- a/src/core/server/saved_objects/routes/integration_tests/migrate.test.ts +++ b/src/core/server/saved_objects/routes/integration_tests/migrate.test.ts @@ -24,7 +24,7 @@ describe('SavedObjects /_migrate endpoint', () => { let root: ReturnType; beforeEach(async () => { - root = kbnTestServer.createRoot({ migrations: { skip: true } }); + root = kbnTestServer.createRoot({ migrations: { skip: true }, plugins: { initialize: false } }); await root.setup(); await root.start(); migratorInstanceMock.runMigrations.mockClear(); diff --git a/src/core/server/saved_objects/service/lib/decorate_es_error.test.ts b/src/core/server/saved_objects/service/lib/decorate_es_error.test.ts index 623610eebd8d..3358de1c1031 100644 --- a/src/core/server/saved_objects/service/lib/decorate_es_error.test.ts +++ b/src/core/server/saved_objects/service/lib/decorate_es_error.test.ts @@ -73,6 +73,15 @@ describe('savedObjectsClient/decorateEsError', () => { expect(SavedObjectsErrorHelpers.isConflictError(error)).toBe(true); }); + it('makes TooManyRequests a SavedObjectsClient/tooManyRequests error', () => { + const error = new esErrors.ResponseError( + elasticsearchClientMock.createApiResponse({ statusCode: 429 }) + ); + expect(SavedObjectsErrorHelpers.isTooManyRequestsError(error)).toBe(false); + expect(decorateEsError(error)).toBe(error); + expect(SavedObjectsErrorHelpers.isTooManyRequestsError(error)).toBe(true); + }); + it('makes NotAuthorized a SavedObjectsClient/NotAuthorized error', () => { const error = new esErrors.ResponseError( elasticsearchClientMock.createApiResponse({ statusCode: 401 }) diff --git a/src/core/server/saved_objects/service/lib/decorate_es_error.ts b/src/core/server/saved_objects/service/lib/decorate_es_error.ts index cf8a16cdaae6..592b268d8219 100644 --- a/src/core/server/saved_objects/service/lib/decorate_es_error.ts +++ b/src/core/server/saved_objects/service/lib/decorate_es_error.ts @@ -28,6 +28,7 @@ const responseErrors = { isRequestEntityTooLarge: (statusCode: number) => statusCode === 413, isNotFound: (statusCode: number) => statusCode === 404, isBadRequest: (statusCode: number) => statusCode === 400, + isTooManyRequests: (statusCode: number) => statusCode === 429, }; const { ConnectionError, NoLivingConnectionsError, TimeoutError } = esErrors; const SCRIPT_CONTEXT_DISABLED_REGEX = /(?:cannot execute scripts using \[)([a-z]*)(?:\] context)/; @@ -76,6 +77,10 @@ export function decorateEsError(error: EsErrors) { return SavedObjectsErrorHelpers.createGenericNotFoundError(); } + if (responseErrors.isTooManyRequests(error.statusCode)) { + return SavedObjectsErrorHelpers.decorateTooManyRequestsError(error, reason); + } + if (responseErrors.isBadRequest(error.statusCode)) { if ( SCRIPT_CONTEXT_DISABLED_REGEX.test(reason || '') || diff --git a/src/core/server/saved_objects/service/lib/errors.test.ts b/src/core/server/saved_objects/service/lib/errors.test.ts index 324d19e27921..931d9f725e41 100644 --- a/src/core/server/saved_objects/service/lib/errors.test.ts +++ b/src/core/server/saved_objects/service/lib/errors.test.ts @@ -274,6 +274,53 @@ describe('savedObjectsClient/errorTypes', () => { }); }); + describe('TooManyRequests error', () => { + describe('decorateTooManyRequestsError', () => { + it('returns original object', () => { + const error = new Error(); + expect(SavedObjectsErrorHelpers.decorateTooManyRequestsError(error)).toBe(error); + }); + + it('makes the error identifiable as a TooManyRequests error', () => { + const error = new Error(); + expect(SavedObjectsErrorHelpers.isTooManyRequestsError(error)).toBe(false); + SavedObjectsErrorHelpers.decorateTooManyRequestsError(error); + expect(SavedObjectsErrorHelpers.isTooManyRequestsError(error)).toBe(true); + }); + + it('adds boom properties', () => { + const error = SavedObjectsErrorHelpers.decorateTooManyRequestsError(new Error()); + expect(error).toHaveProperty('isBoom', true); + }); + + describe('error.output', () => { + it('defaults to message of error', () => { + const error = SavedObjectsErrorHelpers.decorateTooManyRequestsError(new Error('foobar')); + expect(error.output.payload).toHaveProperty('message', 'foobar'); + }); + + it('prefixes message with passed reason', () => { + const error = SavedObjectsErrorHelpers.decorateTooManyRequestsError( + new Error('foobar'), + 'biz' + ); + expect(error.output.payload).toHaveProperty('message', 'biz: foobar'); + }); + + it('sets statusCode to 429', () => { + const error = SavedObjectsErrorHelpers.decorateTooManyRequestsError(new Error('foo')); + expect(error.output).toHaveProperty('statusCode', 429); + }); + + it('preserves boom properties of input', () => { + const error = Boom.tooManyRequests(); + SavedObjectsErrorHelpers.decorateTooManyRequestsError(error); + expect(error.output).toHaveProperty('statusCode', 429); + }); + }); + }); + }); + describe('EsCannotExecuteScript error', () => { describe('decorateEsCannotExecuteScriptError', () => { it('returns original object', () => { diff --git a/src/core/server/saved_objects/service/lib/errors.ts b/src/core/server/saved_objects/service/lib/errors.ts index 9614d692741e..6fd5bc9de0ec 100644 --- a/src/core/server/saved_objects/service/lib/errors.ts +++ b/src/core/server/saved_objects/service/lib/errors.ts @@ -33,6 +33,8 @@ const CODE_REQUEST_ENTITY_TOO_LARGE = 'SavedObjectsClient/requestEntityTooLarge' const CODE_NOT_FOUND = 'SavedObjectsClient/notFound'; // 409 - Conflict const CODE_CONFLICT = 'SavedObjectsClient/conflict'; +// 429 - Too Many Requests +const CODE_TOO_MANY_REQUESTS = 'SavedObjectsClient/tooManyRequests'; // 400 - Es Cannot Execute Script const CODE_ES_CANNOT_EXECUTE_SCRIPT = 'SavedObjectsClient/esCannotExecuteScript'; // 503 - Es Unavailable @@ -162,6 +164,18 @@ export class SavedObjectsErrorHelpers { return isSavedObjectsClientError(error) && error[code] === CODE_CONFLICT; } + public static decorateTooManyRequestsError(error: Error, reason?: string) { + return decorate(error, CODE_TOO_MANY_REQUESTS, 429, reason); + } + + public static createTooManyRequestsError(type: string, id: string) { + return SavedObjectsErrorHelpers.decorateTooManyRequestsError(Boom.tooManyRequests()); + } + + public static isTooManyRequestsError(error: Error | DecoratedError) { + return isSavedObjectsClientError(error) && error[code] === CODE_TOO_MANY_REQUESTS; + } + public static decorateEsCannotExecuteScriptError(error: Error, reason?: string) { return decorate(error, CODE_ES_CANNOT_EXECUTE_SCRIPT, 400, reason); } diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index 772be68f507d..cd7f4973f886 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -2220,6 +2220,8 @@ export class SavedObjectsErrorHelpers { // (undocumented) static createInvalidVersionError(versionInput?: string): DecoratedError; // (undocumented) + static createTooManyRequestsError(type: string, id: string): DecoratedError; + // (undocumented) static createUnsupportedTypeError(type: string): DecoratedError; // (undocumented) static decorateBadRequestError(error: Error, reason?: string): DecoratedError; @@ -2238,6 +2240,8 @@ export class SavedObjectsErrorHelpers { // (undocumented) static decorateRequestEntityTooLargeError(error: Error, reason?: string): DecoratedError; // (undocumented) + static decorateTooManyRequestsError(error: Error, reason?: string): DecoratedError; + // (undocumented) static isBadRequestError(error: Error | DecoratedError): boolean; // (undocumented) static isConflictError(error: Error | DecoratedError): boolean; @@ -2259,6 +2263,8 @@ export class SavedObjectsErrorHelpers { // // (undocumented) static isSavedObjectsClientError(error: any): error is DecoratedError; + // (undocumented) + static isTooManyRequestsError(error: Error | DecoratedError): boolean; } // @public @@ -2796,6 +2802,7 @@ export type StartServicesAccessor; + overall$: Observable; } // @public diff --git a/src/core/server/status/status_service.mock.ts b/src/core/server/status/status_service.mock.ts index c6eb11be6967..47ef8659b407 100644 --- a/src/core/server/status/status_service.mock.ts +++ b/src/core/server/status/status_service.mock.ts @@ -39,6 +39,7 @@ const availableCoreStatus: CoreStatus = { const createSetupContractMock = () => { const setupContract: jest.Mocked = { core$: new BehaviorSubject(availableCoreStatus), + overall$: new BehaviorSubject(available), }; return setupContract; diff --git a/src/core/server/status/types.ts b/src/core/server/status/types.ts index b04c25a1eee9..2ecf11deb296 100644 --- a/src/core/server/status/types.ts +++ b/src/core/server/status/types.ts @@ -123,13 +123,20 @@ export interface StatusServiceSetup { * Current status for all Core services. */ core$: Observable; -} -/** @internal */ -export interface InternalStatusServiceSetup extends StatusServiceSetup { /** - * Overall system status used for HTTP API + * Overall system status for all of Kibana. + * + * @remarks + * The level of the overall status will reflect the most severe status of any core service or plugin. + * + * Exposed only for reporting purposes to outside systems and should not be used by plugins. Instead, plugins should + * only depend on the statuses of {@link StatusServiceSetup.core$ | Core} or their dependencies. */ overall$: Observable; +} + +/** @internal */ +export interface InternalStatusServiceSetup extends StatusServiceSetup { isStatusPageAnonymous: () => boolean; } diff --git a/src/core/server/ui_settings/integration_tests/routes.test.ts b/src/core/server/ui_settings/integration_tests/routes.test.ts index c1261bc7c135..b18cc370fac3 100644 --- a/src/core/server/ui_settings/integration_tests/routes.test.ts +++ b/src/core/server/ui_settings/integration_tests/routes.test.ts @@ -24,7 +24,7 @@ describe('ui settings service', () => { describe('routes', () => { let root: ReturnType; beforeAll(async () => { - root = kbnTestServer.createRoot(); + root = kbnTestServer.createRoot({ plugins: { initialize: false } }); const { uiSettings } = await root.setup(); uiSettings.register({ diff --git a/src/core/server/ui_settings/settings/accessibility.test.ts b/src/core/server/ui_settings/settings/accessibility.test.ts new file mode 100644 index 000000000000..8d8f9d00fada --- /dev/null +++ b/src/core/server/ui_settings/settings/accessibility.test.ts @@ -0,0 +1,44 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { UiSettingsParams } from '../../../types'; +import { getAccessibilitySettings } from './accessibility'; + +describe('accessibility settings', () => { + const accessibilitySettings = getAccessibilitySettings(); + + const getValidationFn = (setting: UiSettingsParams) => (value: any) => + setting.schema.validate(value); + + describe('accessibility:disableAnimations', () => { + const validate = getValidationFn(accessibilitySettings['accessibility:disableAnimations']); + + it('should only accept boolean', () => { + expect(() => validate(true)).not.toThrow(); + expect(() => validate(false)).not.toThrow(); + + expect(() => validate(42)).toThrowErrorMatchingInlineSnapshot( + `"expected value of type [boolean] but got [number]"` + ); + expect(() => validate('foo')).toThrowErrorMatchingInlineSnapshot( + `"expected value of type [boolean] but got [string]"` + ); + }); + }); +}); diff --git a/src/core/server/ui_settings/settings/accessibility.ts b/src/core/server/ui_settings/settings/accessibility.ts new file mode 100644 index 000000000000..ddf3e53d9118 --- /dev/null +++ b/src/core/server/ui_settings/settings/accessibility.ts @@ -0,0 +1,40 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { schema } from '@kbn/config-schema'; +import { i18n } from '@kbn/i18n'; +import { UiSettingsParams } from '../../../types'; + +export const getAccessibilitySettings = (): Record => { + return { + 'accessibility:disableAnimations': { + name: i18n.translate('core.ui_settings.params.disableAnimationsTitle', { + defaultMessage: 'Disable Animations', + }), + value: false, + description: i18n.translate('core.ui_settings.params.disableAnimationsText', { + defaultMessage: + 'Turn off all unnecessary animations in the Kibana UI. Refresh the page to apply the changes.', + }), + category: ['accessibility'], + requiresPageReload: true, + schema: schema.boolean(), + }, + }; +}; diff --git a/src/core/server/ui_settings/settings/date_formats.test.ts b/src/core/server/ui_settings/settings/date_formats.test.ts new file mode 100644 index 000000000000..3c179af0b1d0 --- /dev/null +++ b/src/core/server/ui_settings/settings/date_formats.test.ts @@ -0,0 +1,104 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import moment from 'moment-timezone'; +import { UiSettingsParams } from '../../../types'; +import { getDateFormatSettings } from './date_formats'; + +describe('accessibility settings', () => { + const dateFormatSettings = getDateFormatSettings(); + + const getValidationFn = (setting: UiSettingsParams) => (value: any) => + setting.schema.validate(value); + + describe('dateFormat', () => { + const validate = getValidationFn(dateFormatSettings.dateFormat); + + it('should only accept string values', () => { + expect(() => validate('some format')).not.toThrow(); + + expect(() => validate(42)).toThrowErrorMatchingInlineSnapshot( + `"expected value of type [string] but got [number]"` + ); + expect(() => validate(true)).toThrowErrorMatchingInlineSnapshot( + `"expected value of type [string] but got [boolean]"` + ); + }); + }); + + describe('dateFormat:tz', () => { + const validate = getValidationFn(dateFormatSettings['dateFormat:tz']); + + it('should only accept valid timezones or `Browser`', () => { + expect(() => validate('Browser')).not.toThrow(); + expect(() => validate('UTC')).not.toThrow(); + + expect(() => validate('EST')).toThrowErrorMatchingInlineSnapshot(`"Invalid timezone: EST"`); + expect(() => validate('random string')).toThrowErrorMatchingInlineSnapshot( + `"Invalid timezone: random string"` + ); + }); + }); + + describe('dateFormat:scaled', () => { + const validate = getValidationFn(dateFormatSettings['dateFormat:scaled']); + + it('should only accept string values', () => { + expect(() => validate('some format')).not.toThrow(); + + expect(() => validate(42)).toThrowErrorMatchingInlineSnapshot( + `"expected value of type [string] but got [number]"` + ); + expect(() => validate(true)).toThrowErrorMatchingInlineSnapshot( + `"expected value of type [string] but got [boolean]"` + ); + }); + }); + + describe('dateFormat:dow', () => { + const [validDay] = moment.weekdays(); + const validate = getValidationFn(dateFormatSettings['dateFormat:dow']); + + it('should only accept DOW values', () => { + expect(() => validate(validDay)).not.toThrow(); + + expect(() => validate('invalid value')).toThrowErrorMatchingInlineSnapshot( + `"Invalid day of week: invalid value"` + ); + expect(() => validate(true)).toThrowErrorMatchingInlineSnapshot( + `"expected value of type [string] but got [boolean]"` + ); + }); + }); + + describe('dateNanosFormat', () => { + const validate = getValidationFn(dateFormatSettings.dateNanosFormat); + + it('should only accept string values', () => { + expect(() => validate('some format')).not.toThrow(); + + expect(() => validate(42)).toThrowErrorMatchingInlineSnapshot( + `"expected value of type [string] but got [number]"` + ); + expect(() => validate(true)).toThrowErrorMatchingInlineSnapshot( + `"expected value of type [string] but got [boolean]"` + ); + }); + }); +}); diff --git a/src/core/server/ui_settings/settings/date_formats.ts b/src/core/server/ui_settings/settings/date_formats.ts new file mode 100644 index 000000000000..22351d36ac4b --- /dev/null +++ b/src/core/server/ui_settings/settings/date_formats.ts @@ -0,0 +1,168 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import moment from 'moment-timezone'; +import { schema } from '@kbn/config-schema'; +import { i18n } from '@kbn/i18n'; +import { UiSettingsParams } from '../../../types'; + +export const getDateFormatSettings = (): Record => { + const weekdays = moment.weekdays().slice(); + const [defaultWeekday] = weekdays; + + const timezones = [ + 'Browser', + ...moment.tz + .names() + // We need to filter out some time zones, that moment.js knows about, but Elasticsearch + // does not understand and would fail thus with a 400 bad request when using them. + .filter((tz) => !['America/Nuuk', 'EST', 'HST', 'ROC', 'MST'].includes(tz)), + ]; + + return { + dateFormat: { + name: i18n.translate('core.ui_settings.params.dateFormatTitle', { + defaultMessage: 'Date format', + }), + value: 'MMM D, YYYY @ HH:mm:ss.SSS', + description: i18n.translate('core.ui_settings.params.dateFormatText', { + defaultMessage: 'When displaying a pretty formatted date, use this {formatLink}', + description: + 'Part of composite text: core.ui_settings.params.dateFormatText + ' + + 'core.ui_settings.params.dateFormat.optionsLinkText', + values: { + formatLink: + '' + + i18n.translate('core.ui_settings.params.dateFormat.optionsLinkText', { + defaultMessage: 'format', + }) + + '', + }, + }), + schema: schema.string(), + }, + 'dateFormat:tz': { + name: i18n.translate('core.ui_settings.params.dateFormat.timezoneTitle', { + defaultMessage: 'Timezone for date formatting', + }), + value: 'Browser', + description: i18n.translate('core.ui_settings.params.dateFormat.timezoneText', { + defaultMessage: + 'Which timezone should be used. {defaultOption} will use the timezone detected by your browser.', + values: { + defaultOption: '"Browser"', + }, + }), + type: 'select', + options: timezones, + requiresPageReload: true, + schema: schema.string({ + validate: (value) => { + if (!timezones.includes(value)) { + return i18n.translate( + 'core.ui_settings.params.dateFormat.timezone.invalidValidationMessage', + { + defaultMessage: 'Invalid timezone: {timezone}', + values: { + timezone: value, + }, + } + ); + } + }, + }), + }, + 'dateFormat:scaled': { + name: i18n.translate('core.ui_settings.params.dateFormat.scaledTitle', { + defaultMessage: 'Scaled date format', + }), + type: 'json', + value: `[ + ["", "HH:mm:ss.SSS"], + ["PT1S", "HH:mm:ss"], + ["PT1M", "HH:mm"], + ["PT1H", "YYYY-MM-DD HH:mm"], + ["P1DT", "YYYY-MM-DD"], + ["P1YT", "YYYY"] +]`, + description: i18n.translate('core.ui_settings.params.dateFormat.scaledText', { + defaultMessage: + 'Values that define the format used in situations where time-based ' + + 'data is rendered in order, and formatted timestamps should adapt to the ' + + 'interval between measurements. Keys are {intervalsLink}.', + description: + 'Part of composite text: core.ui_settings.params.dateFormat.scaledText + ' + + 'core.ui_settings.params.dateFormat.scaled.intervalsLinkText', + values: { + intervalsLink: + '' + + i18n.translate('core.ui_settings.params.dateFormat.scaled.intervalsLinkText', { + defaultMessage: 'ISO8601 intervals', + }) + + '', + }, + }), + schema: schema.string(), + }, + 'dateFormat:dow': { + name: i18n.translate('core.ui_settings.params.dateFormat.dayOfWeekTitle', { + defaultMessage: 'Day of week', + }), + value: defaultWeekday, + description: i18n.translate('core.ui_settings.params.dateFormat.dayOfWeekText', { + defaultMessage: 'What day should weeks start on?', + }), + type: 'select', + options: weekdays, + schema: schema.string({ + validate: (value) => { + if (!weekdays.includes(value)) { + return i18n.translate( + 'core.ui_settings.params.dayOfWeekText.invalidValidationMessage', + { + defaultMessage: 'Invalid day of week: {dayOfWeek}', + values: { + dayOfWeek: value, + }, + } + ); + } + }, + }), + }, + dateNanosFormat: { + name: i18n.translate('core.ui_settings.params.dateNanosFormatTitle', { + defaultMessage: 'Date with nanoseconds format', + }), + value: 'MMM D, YYYY @ HH:mm:ss.SSSSSSSSS', + description: i18n.translate('core.ui_settings.params.dateNanosFormatText', { + defaultMessage: 'Used for the {dateNanosLink} datatype of Elasticsearch', + values: { + dateNanosLink: + '' + + i18n.translate('core.ui_settings.params.dateNanosLinkTitle', { + defaultMessage: 'date_nanos', + }) + + '', + }, + }), + schema: schema.string(), + }, + }; +}; diff --git a/src/core/server/ui_settings/settings/index.test.ts b/src/core/server/ui_settings/settings/index.test.ts new file mode 100644 index 000000000000..e234160fbb4a --- /dev/null +++ b/src/core/server/ui_settings/settings/index.test.ts @@ -0,0 +1,44 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { getAccessibilitySettings } from './accessibility'; +import { getDateFormatSettings } from './date_formats'; +import { getMiscUiSettings } from './misc'; +import { getNavigationSettings } from './navigation'; +import { getNotificationsSettings } from './notifications'; +import { getThemeSettings } from './theme'; +import { getCoreSettings } from './index'; +import { getStateSettings } from './state'; + +describe('getCoreSettings', () => { + it('should not have setting overlaps', () => { + const coreSettingsLength = Object.keys(getCoreSettings()).length; + const summedLength = [ + getAccessibilitySettings(), + getDateFormatSettings(), + getMiscUiSettings(), + getNavigationSettings(), + getNotificationsSettings(), + getThemeSettings(), + getStateSettings(), + ].reduce((sum, settings) => sum + Object.keys(settings).length, 0); + + expect(coreSettingsLength).toBe(summedLength); + }); +}); diff --git a/src/core/server/ui_settings/settings/index.ts b/src/core/server/ui_settings/settings/index.ts new file mode 100644 index 000000000000..88baf7cd22ee --- /dev/null +++ b/src/core/server/ui_settings/settings/index.ts @@ -0,0 +1,39 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { UiSettingsParams } from '../../../types'; +import { getAccessibilitySettings } from './accessibility'; +import { getDateFormatSettings } from './date_formats'; +import { getMiscUiSettings } from './misc'; +import { getNavigationSettings } from './navigation'; +import { getNotificationsSettings } from './notifications'; +import { getThemeSettings } from './theme'; +import { getStateSettings } from './state'; + +export const getCoreSettings = (): Record => { + return { + ...getAccessibilitySettings(), + ...getDateFormatSettings(), + ...getMiscUiSettings(), + ...getNavigationSettings(), + ...getNotificationsSettings(), + ...getThemeSettings(), + ...getStateSettings(), + }; +}; diff --git a/src/core/server/ui_settings/settings/misc.test.ts b/src/core/server/ui_settings/settings/misc.test.ts new file mode 100644 index 000000000000..db2c039d9b42 --- /dev/null +++ b/src/core/server/ui_settings/settings/misc.test.ts @@ -0,0 +1,42 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { UiSettingsParams } from '../../../types'; +import { getMiscUiSettings } from './misc'; + +describe('misc settings', () => { + const miscSettings = getMiscUiSettings(); + + const getValidationFn = (setting: UiSettingsParams) => (value: any) => + setting.schema.validate(value); + + describe('truncate:maxHeight', () => { + const validate = getValidationFn(miscSettings['truncate:maxHeight']); + + it('should only accept positive numeric values', () => { + expect(() => validate(127)).not.toThrow(); + expect(() => validate(-12)).toThrowErrorMatchingInlineSnapshot( + `"Value must be equal to or greater than [0]."` + ); + expect(() => validate('foo')).toThrowErrorMatchingInlineSnapshot( + `"expected value of type [number] but got [string]"` + ); + }); + }); +}); diff --git a/src/core/server/ui_settings/settings/misc.ts b/src/core/server/ui_settings/settings/misc.ts new file mode 100644 index 000000000000..d158b07839c6 --- /dev/null +++ b/src/core/server/ui_settings/settings/misc.ts @@ -0,0 +1,42 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { i18n } from '@kbn/i18n'; +import { schema } from '@kbn/config-schema'; +import { UiSettingsParams } from '../types'; + +export const getMiscUiSettings = (): Record => { + return { + 'truncate:maxHeight': { + name: i18n.translate('core.ui_settings.params.maxCellHeightTitle', { + defaultMessage: 'Maximum table cell height', + }), + value: 115, + description: i18n.translate('core.ui_settings.params.maxCellHeightText', { + defaultMessage: + 'The maximum height that a cell in a table should occupy. Set to 0 to disable truncation', + }), + schema: schema.number({ min: 0 }), + }, + buildNum: { + readonly: true, + schema: schema.maybe(schema.number()), + }, + }; +}; diff --git a/src/core/server/ui_settings/settings/navigation.test.ts b/src/core/server/ui_settings/settings/navigation.test.ts new file mode 100644 index 000000000000..40cd0e172468 --- /dev/null +++ b/src/core/server/ui_settings/settings/navigation.test.ts @@ -0,0 +1,56 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { UiSettingsParams } from '../../../types'; +import { getNavigationSettings } from './navigation'; + +describe('navigation settings', () => { + const navigationSettings = getNavigationSettings(); + + const getValidationFn = (setting: UiSettingsParams) => (value: any) => + setting.schema.validate(value); + + describe('defaultRoute', () => { + const validate = getValidationFn(navigationSettings.defaultRoute); + + it('should only accept relative urls', () => { + expect(() => validate('/some-url')).not.toThrow(); + expect(() => validate('http://some-url')).toThrowErrorMatchingInlineSnapshot( + `"Must be a relative URL."` + ); + expect(() => validate(125)).toThrowErrorMatchingInlineSnapshot( + `"expected value of type [string] but got [number]"` + ); + }); + }); + + describe('pageNavigation', () => { + const validate = getValidationFn(navigationSettings.pageNavigation); + + it('should only accept valid values', () => { + expect(() => validate('modern')).not.toThrow(); + expect(() => validate('legacy')).not.toThrow(); + expect(() => validate('invalid')).toThrowErrorMatchingInlineSnapshot(` +"types that failed validation: +- [0]: expected value to equal [modern] +- [1]: expected value to equal [legacy]" +`); + }); + }); +}); diff --git a/src/core/server/ui_settings/settings/navigation.ts b/src/core/server/ui_settings/settings/navigation.ts new file mode 100644 index 000000000000..6483e86a1395 --- /dev/null +++ b/src/core/server/ui_settings/settings/navigation.ts @@ -0,0 +1,72 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { schema } from '@kbn/config-schema'; +import { i18n } from '@kbn/i18n'; +import { UiSettingsParams } from '../../../types'; +import { isRelativeUrl } from '../../../utils'; + +export const getNavigationSettings = (): Record => { + return { + defaultRoute: { + name: i18n.translate('core.ui_settings.params.defaultRoute.defaultRouteTitle', { + defaultMessage: 'Default route', + }), + value: '/app/home', + schema: schema.string({ + validate(value) { + if (!value.startsWith('/') || !isRelativeUrl(value)) { + return i18n.translate( + 'core.ui_settings.params.defaultRoute.defaultRouteIsRelativeValidationMessage', + { + defaultMessage: 'Must be a relative URL.', + } + ); + } + }, + }), + description: i18n.translate('core.ui_settings.params.defaultRoute.defaultRouteText', { + defaultMessage: + 'This setting specifies the default route when opening Kibana. ' + + 'You can use this setting to modify the landing page when opening Kibana. ' + + 'The route must be a relative URL.', + }), + }, + pageNavigation: { + name: i18n.translate('core.ui_settings.params.pageNavigationName', { + defaultMessage: 'Side nav style', + }), + value: 'modern', + description: i18n.translate('core.ui_settings.params.pageNavigationDesc', { + defaultMessage: 'Change the style of navigation', + }), + type: 'select', + options: ['modern', 'legacy'], + optionLabels: { + modern: i18n.translate('core.ui_settings.params.pageNavigationModern', { + defaultMessage: 'Modern', + }), + legacy: i18n.translate('core.ui_settings.params.pageNavigationLegacy', { + defaultMessage: 'Legacy', + }), + }, + schema: schema.oneOf([schema.literal('modern'), schema.literal('legacy')]), + }, + }; +}; diff --git a/src/core/server/ui_settings/settings/notifications.test.ts b/src/core/server/ui_settings/settings/notifications.test.ts new file mode 100644 index 000000000000..e1bdf63c7e0d --- /dev/null +++ b/src/core/server/ui_settings/settings/notifications.test.ts @@ -0,0 +1,118 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { UiSettingsParams } from '../../../types'; +import { getNotificationsSettings } from './notifications'; + +describe('notifications settings', () => { + const notificationsSettings = getNotificationsSettings(); + + const getValidationFn = (setting: UiSettingsParams) => (value: any) => + setting.schema.validate(value); + + describe('notifications:banner', () => { + const validate = getValidationFn(notificationsSettings['notifications:banner']); + + it('should only accept string values', () => { + expect(() => validate('some text')).not.toThrow(); + expect(() => validate(true)).toThrowErrorMatchingInlineSnapshot( + `"expected value of type [string] but got [boolean]"` + ); + expect(() => validate(12)).toThrowErrorMatchingInlineSnapshot( + `"expected value of type [string] but got [number]"` + ); + }); + }); + + describe('notifications:lifetime:banner', () => { + const validate = getValidationFn(notificationsSettings['notifications:lifetime:banner']); + + it('should only accept positive numeric values or `Infinity`', () => { + expect(() => validate(42)).not.toThrow(); + expect(() => validate('Infinity')).not.toThrow(); + expect(() => validate(-12)).toThrowErrorMatchingInlineSnapshot(` +"types that failed validation: +- [0]: Value must be equal to or greater than [0]. +- [1]: expected value to equal [Infinity]" +`); + expect(() => validate('foo')).toThrowErrorMatchingInlineSnapshot(` +"types that failed validation: +- [0]: expected value of type [number] but got [string] +- [1]: expected value to equal [Infinity]" +`); + }); + }); + + describe('notifications:lifetime:error', () => { + const validate = getValidationFn(notificationsSettings['notifications:lifetime:error']); + + it('should only accept positive numeric values or `Infinity`', () => { + expect(() => validate(42)).not.toThrow(); + expect(() => validate('Infinity')).not.toThrow(); + expect(() => validate(-12)).toThrowErrorMatchingInlineSnapshot(` +"types that failed validation: +- [0]: Value must be equal to or greater than [0]. +- [1]: expected value to equal [Infinity]" +`); + expect(() => validate('foo')).toThrowErrorMatchingInlineSnapshot(` +"types that failed validation: +- [0]: expected value of type [number] but got [string] +- [1]: expected value to equal [Infinity]" +`); + }); + }); + + describe('notifications:lifetime:warning', () => { + const validate = getValidationFn(notificationsSettings['notifications:lifetime:warning']); + + it('should only accept positive numeric values or `Infinity`', () => { + expect(() => validate(42)).not.toThrow(); + expect(() => validate('Infinity')).not.toThrow(); + expect(() => validate(-12)).toThrowErrorMatchingInlineSnapshot(` +"types that failed validation: +- [0]: Value must be equal to or greater than [0]. +- [1]: expected value to equal [Infinity]" +`); + expect(() => validate('foo')).toThrowErrorMatchingInlineSnapshot(` +"types that failed validation: +- [0]: expected value of type [number] but got [string] +- [1]: expected value to equal [Infinity]" +`); + }); + }); + + describe('notifications:lifetime:info', () => { + const validate = getValidationFn(notificationsSettings['notifications:lifetime:info']); + + it('should only accept positive numeric values or `Infinity`', () => { + expect(() => validate(42)).not.toThrow(); + expect(() => validate('Infinity')).not.toThrow(); + expect(() => validate(-12)).toThrowErrorMatchingInlineSnapshot(` +"types that failed validation: +- [0]: Value must be equal to or greater than [0]. +- [1]: expected value to equal [Infinity]" +`); + expect(() => validate('foo')).toThrowErrorMatchingInlineSnapshot(` +"types that failed validation: +- [0]: expected value of type [number] but got [string] +- [1]: expected value to equal [Infinity]" +`); + }); + }); +}); diff --git a/src/core/server/ui_settings/settings/notifications.ts b/src/core/server/ui_settings/settings/notifications.ts new file mode 100644 index 000000000000..7d9e70dc9036 --- /dev/null +++ b/src/core/server/ui_settings/settings/notifications.ts @@ -0,0 +1,120 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { schema } from '@kbn/config-schema'; +import { i18n } from '@kbn/i18n'; +import { UiSettingsParams } from '../../../types'; + +export const getNotificationsSettings = (): Record => { + return { + 'notifications:banner': { + name: i18n.translate('core.ui_settings.params.notifications.bannerTitle', { + defaultMessage: 'Custom banner notification', + }), + value: '', + type: 'markdown', + description: i18n.translate('core.ui_settings.params.notifications.bannerText', { + defaultMessage: + 'A custom banner intended for temporary notices to all users. {markdownLink}.', + description: + 'Part of composite text: core.ui_settings.params.notifications.bannerText + ' + + 'core.ui_settings.params.notifications.banner.markdownLinkText', + values: { + markdownLink: + `` + + i18n.translate('core.ui_settings.params.notifications.banner.markdownLinkText', { + defaultMessage: 'Markdown supported', + }) + + '', + }, + }), + category: ['notifications'], + schema: schema.string(), + }, + 'notifications:lifetime:banner': { + name: i18n.translate('core.ui_settings.params.notifications.bannerLifetimeTitle', { + defaultMessage: 'Banner notification lifetime', + }), + value: 3000000, + description: i18n.translate('core.ui_settings.params.notifications.bannerLifetimeText', { + defaultMessage: + 'The time in milliseconds which a banner notification will be displayed on-screen for. ' + + 'Setting to {infinityValue} will disable the countdown.', + values: { + infinityValue: 'Infinity', + }, + }), + type: 'number', + category: ['notifications'], + schema: schema.oneOf([schema.number({ min: 0 }), schema.literal('Infinity')]), + }, + 'notifications:lifetime:error': { + name: i18n.translate('core.ui_settings.params.notifications.errorLifetimeTitle', { + defaultMessage: 'Error notification lifetime', + }), + value: 300000, + description: i18n.translate('core.ui_settings.params.notifications.errorLifetimeText', { + defaultMessage: + 'The time in milliseconds which an error notification will be displayed on-screen for. ' + + 'Setting to {infinityValue} will disable.', + values: { + infinityValue: 'Infinity', + }, + }), + type: 'number', + category: ['notifications'], + schema: schema.oneOf([schema.number({ min: 0 }), schema.literal('Infinity')]), + }, + 'notifications:lifetime:warning': { + name: i18n.translate('core.ui_settings.params.notifications.warningLifetimeTitle', { + defaultMessage: 'Warning notification lifetime', + }), + value: 10000, + description: i18n.translate('core.ui_settings.params.notifications.warningLifetimeText', { + defaultMessage: + 'The time in milliseconds which a warning notification will be displayed on-screen for. ' + + 'Setting to {infinityValue} will disable.', + values: { + infinityValue: 'Infinity', + }, + }), + type: 'number', + category: ['notifications'], + schema: schema.oneOf([schema.number({ min: 0 }), schema.literal('Infinity')]), + }, + 'notifications:lifetime:info': { + name: i18n.translate('core.ui_settings.params.notifications.infoLifetimeTitle', { + defaultMessage: 'Info notification lifetime', + }), + value: 5000, + description: i18n.translate('core.ui_settings.params.notifications.infoLifetimeText', { + defaultMessage: + 'The time in milliseconds which an information notification will be displayed on-screen for. ' + + 'Setting to {infinityValue} will disable.', + values: { + infinityValue: 'Infinity', + }, + }), + type: 'number', + category: ['notifications'], + schema: schema.oneOf([schema.number({ min: 0 }), schema.literal('Infinity')]), + }, + }; +}; diff --git a/src/core/server/ui_settings/settings/state.test.ts b/src/core/server/ui_settings/settings/state.test.ts new file mode 100644 index 000000000000..7be30abe71bb --- /dev/null +++ b/src/core/server/ui_settings/settings/state.test.ts @@ -0,0 +1,43 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { UiSettingsParams } from '../../../types'; +import { getStateSettings } from './state'; + +describe('state settings', () => { + const state = getStateSettings(); + + const getValidationFn = (setting: UiSettingsParams) => (value: any) => + setting.schema.validate(value); + + describe('state:storeInSessionStorage', () => { + const validate = getValidationFn(state['state:storeInSessionStorage']); + + it('should only accept boolean values', () => { + expect(() => validate(true)).not.toThrow(); + expect(() => validate(false)).not.toThrow(); + expect(() => validate('foo')).toThrowErrorMatchingInlineSnapshot( + `"expected value of type [boolean] but got [string]"` + ); + expect(() => validate(12)).toThrowErrorMatchingInlineSnapshot( + `"expected value of type [boolean] but got [number]"` + ); + }); + }); +}); diff --git a/src/core/server/ui_settings/settings/state.ts b/src/core/server/ui_settings/settings/state.ts new file mode 100644 index 000000000000..ee85cc844259 --- /dev/null +++ b/src/core/server/ui_settings/settings/state.ts @@ -0,0 +1,40 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { schema } from '@kbn/config-schema'; +import { i18n } from '@kbn/i18n'; +import { UiSettingsParams } from '../../../types'; + +export const getStateSettings = (): Record => { + return { + 'state:storeInSessionStorage': { + name: i18n.translate('core.ui_settings.params.storeUrlTitle', { + defaultMessage: 'Store URLs in session storage', + }), + value: false, + description: i18n.translate('core.ui_settings.params.storeUrlText', { + defaultMessage: + 'The URL can sometimes grow to be too large for some browsers to handle. ' + + 'To counter-act this we are testing if storing parts of the URL in session storage could help. ' + + 'Please let us know how it goes!', + }), + schema: schema.boolean(), + }, + }; +}; diff --git a/src/core/server/ui_settings/settings/theme.test.ts b/src/core/server/ui_settings/settings/theme.test.ts new file mode 100644 index 000000000000..eb18bcc2dd0c --- /dev/null +++ b/src/core/server/ui_settings/settings/theme.test.ts @@ -0,0 +1,57 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { UiSettingsParams } from '../../../types'; +import { getThemeSettings } from './theme'; + +describe('theme settings', () => { + const themeSettings = getThemeSettings(); + + const getValidationFn = (setting: UiSettingsParams) => (value: any) => + setting.schema.validate(value); + + describe('theme:darkMode', () => { + const validate = getValidationFn(themeSettings['theme:darkMode']); + + it('should only accept boolean values', () => { + expect(() => validate(true)).not.toThrow(); + expect(() => validate(false)).not.toThrow(); + expect(() => validate('foo')).toThrowErrorMatchingInlineSnapshot( + `"expected value of type [boolean] but got [string]"` + ); + expect(() => validate(12)).toThrowErrorMatchingInlineSnapshot( + `"expected value of type [boolean] but got [number]"` + ); + }); + }); + + describe('theme:version', () => { + const validate = getValidationFn(themeSettings['theme:version']); + + it('should only accept valid values', () => { + expect(() => validate('v7')).not.toThrow(); + expect(() => validate('v8 (beta)')).not.toThrow(); + expect(() => validate('v12')).toThrowErrorMatchingInlineSnapshot(` +"types that failed validation: +- [0]: expected value to equal [v7] +- [1]: expected value to equal [v8 (beta)]" +`); + }); + }); +}); diff --git a/src/core/server/ui_settings/settings/theme.ts b/src/core/server/ui_settings/settings/theme.ts new file mode 100644 index 000000000000..9f1857932f01 --- /dev/null +++ b/src/core/server/ui_settings/settings/theme.ts @@ -0,0 +1,51 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { schema } from '@kbn/config-schema'; +import { i18n } from '@kbn/i18n'; +import { UiSettingsParams } from '../../../types'; + +export const getThemeSettings = (): Record => { + return { + 'theme:darkMode': { + name: i18n.translate('core.ui_settings.params.darkModeTitle', { + defaultMessage: 'Dark mode', + }), + value: false, + description: i18n.translate('core.ui_settings.params.darkModeText', { + defaultMessage: `Enable a dark mode for the Kibana UI. A page refresh is required for the setting to be applied.`, + }), + requiresPageReload: true, + schema: schema.boolean(), + }, + 'theme:version': { + name: i18n.translate('core.ui_settings.params.themeVersionTitle', { + defaultMessage: 'Theme version', + }), + value: 'v7', + type: 'select', + options: ['v7', 'v8 (beta)'], + description: i18n.translate('core.ui_settings.params.themeVersionText', { + defaultMessage: `Switch between the theme used for the current and next version of Kibana. A page refresh is required for the setting to be applied.`, + }), + requiresPageReload: true, + schema: schema.oneOf([schema.literal('v7'), schema.literal('v8 (beta)')]), + }, + }; +}; diff --git a/src/core/server/ui_settings/ui_settings_config.ts b/src/core/server/ui_settings/ui_settings_config.ts index a0ac48e2dd08..3a3573a06d49 100644 --- a/src/core/server/ui_settings/ui_settings_config.ts +++ b/src/core/server/ui_settings/ui_settings_config.ts @@ -27,20 +27,7 @@ const deprecations: ConfigDeprecationProvider = ({ unused, renameFromRoot }) => ]; const configSchema = schema.object({ - overrides: schema.object( - { - defaultRoute: schema.maybe( - schema.string({ - validate(value) { - if (!value.startsWith('/')) { - return 'must start with a slash'; - } - }, - }) - ), - }, - { unknowns: 'allow' } - ), + overrides: schema.object({}, { unknowns: 'allow' }), }); export type UiSettingsConfigType = TypeOf; diff --git a/src/core/server/ui_settings/ui_settings_service.test.mock.ts b/src/core/server/ui_settings/ui_settings_service.test.mock.ts index 586ad3049ed6..b4e98f55e159 100644 --- a/src/core/server/ui_settings/ui_settings_service.test.mock.ts +++ b/src/core/server/ui_settings/ui_settings_service.test.mock.ts @@ -18,7 +18,11 @@ */ export const MockUiSettingsClientConstructor = jest.fn(); - jest.doMock('./ui_settings_client', () => ({ UiSettingsClient: MockUiSettingsClientConstructor, })); + +export const getCoreSettingsMock = jest.fn(); +jest.doMock('./settings', () => ({ + getCoreSettings: getCoreSettingsMock, +})); diff --git a/src/core/server/ui_settings/ui_settings_service.test.ts b/src/core/server/ui_settings/ui_settings_service.test.ts index 096ca347e6f4..0c17a3a614d6 100644 --- a/src/core/server/ui_settings/ui_settings_service.test.ts +++ b/src/core/server/ui_settings/ui_settings_service.test.ts @@ -19,7 +19,10 @@ import { BehaviorSubject } from 'rxjs'; import { schema } from '@kbn/config-schema'; -import { MockUiSettingsClientConstructor } from './ui_settings_service.test.mock'; +import { + MockUiSettingsClientConstructor, + getCoreSettingsMock, +} from './ui_settings_service.test.mock'; import { UiSettingsService, SetupDeps } from './ui_settings_service'; import { httpServiceMock } from '../http/http_service.mock'; import { savedObjectsClientMock } from '../mocks'; @@ -58,6 +61,7 @@ describe('uiSettings', () => { afterEach(() => { MockUiSettingsClientConstructor.mockClear(); + getCoreSettingsMock.mockClear(); }); describe('#setup', () => { @@ -67,6 +71,11 @@ describe('uiSettings', () => { expect(setupDeps.savedObjects.registerType).toHaveBeenCalledWith(uiSettingsType); }); + it('calls `getCoreSettings`', async () => { + await service.setup(setupDeps); + expect(getCoreSettingsMock).toHaveBeenCalledTimes(1); + }); + describe('#register', () => { it('throws if registers the same key twice', async () => { const setup = await service.setup(setupDeps); diff --git a/src/core/server/ui_settings/ui_settings_service.ts b/src/core/server/ui_settings/ui_settings_service.ts index 93593b29221d..8598cf7a6228 100644 --- a/src/core/server/ui_settings/ui_settings_service.ts +++ b/src/core/server/ui_settings/ui_settings_service.ts @@ -36,6 +36,7 @@ import { import { mapToObject } from '../../utils/'; import { uiSettingsType } from './saved_objects'; import { registerRoutes } from './routes'; +import { getCoreSettings } from './settings'; export interface SetupDeps { http: InternalHttpServiceSetup; @@ -60,6 +61,8 @@ export class UiSettingsService savedObjects.registerType(uiSettingsType); registerRoutes(http.createRouter('')); + this.register(getCoreSettings()); + const config = await this.config$.pipe(first()).toPromise(); this.overrides = config.overrides; diff --git a/src/legacy/core_plugins/kibana/server/ui_setting_defaults.js b/src/legacy/core_plugins/kibana/server/ui_setting_defaults.js index 625c2c02510d..2562657a7162 100644 --- a/src/legacy/core_plugins/kibana/server/ui_setting_defaults.js +++ b/src/legacy/core_plugins/kibana/server/ui_setting_defaults.js @@ -17,159 +17,11 @@ * under the License. */ -import moment from 'moment-timezone'; import { i18n } from '@kbn/i18n'; -import { schema } from '@kbn/config-schema'; - -import { isRelativeUrl } from '../../../../core/server'; export function getUiSettingDefaults() { - const weekdays = moment.weekdays().slice(); - const [defaultWeekday] = weekdays; - // wrapped in provider so that a new instance is given to each app/test return { - buildNum: { - readonly: true, - }, - 'state:storeInSessionStorage': { - name: i18n.translate('kbn.advancedSettings.storeUrlTitle', { - defaultMessage: 'Store URLs in session storage', - }), - value: false, - description: i18n.translate('kbn.advancedSettings.storeUrlText', { - defaultMessage: - 'The URL can sometimes grow to be too large for some browsers to handle. ' + - 'To counter-act this we are testing if storing parts of the URL in session storage could help. ' + - 'Please let us know how it goes!', - }), - }, - defaultRoute: { - name: i18n.translate('kbn.advancedSettings.defaultRoute.defaultRouteTitle', { - defaultMessage: 'Default route', - }), - value: '/app/home', - schema: schema.string({ - validate(value) { - if (!value.startsWith('/') || !isRelativeUrl(value)) { - return i18n.translate( - 'kbn.advancedSettings.defaultRoute.defaultRouteIsRelativeValidationMessage', - { - defaultMessage: 'Must be a relative URL.', - } - ); - } - }, - }), - description: i18n.translate('kbn.advancedSettings.defaultRoute.defaultRouteText', { - defaultMessage: - 'This setting specifies the default route when opening Kibana. ' + - 'You can use this setting to modify the landing page when opening Kibana. ' + - 'The route must be a relative URL.', - }), - }, - dateFormat: { - name: i18n.translate('kbn.advancedSettings.dateFormatTitle', { - defaultMessage: 'Date format', - }), - value: 'MMM D, YYYY @ HH:mm:ss.SSS', - description: i18n.translate('kbn.advancedSettings.dateFormatText', { - defaultMessage: 'When displaying a pretty formatted date, use this {formatLink}', - description: - 'Part of composite text: kbn.advancedSettings.dateFormatText + ' + - 'kbn.advancedSettings.dateFormat.optionsLinkText', - values: { - formatLink: - '' + - i18n.translate('kbn.advancedSettings.dateFormat.optionsLinkText', { - defaultMessage: 'format', - }) + - '', - }, - }), - }, - 'dateFormat:tz': { - name: i18n.translate('kbn.advancedSettings.dateFormat.timezoneTitle', { - defaultMessage: 'Timezone for date formatting', - }), - value: 'Browser', - description: i18n.translate('kbn.advancedSettings.dateFormat.timezoneText', { - defaultMessage: - 'Which timezone should be used. {defaultOption} will use the timezone detected by your browser.', - values: { - defaultOption: '"Browser"', - }, - }), - type: 'select', - options: [ - 'Browser', - ...moment.tz - .names() - // We need to filter out some time zones, that moment.js knows about, but Elasticsearch - // does not understand and would fail thus with a 400 bad request when using them. - .filter((tz) => !['America/Nuuk', 'EST', 'HST', 'ROC', 'MST'].includes(tz)), - ], - requiresPageReload: true, - }, - 'dateFormat:scaled': { - name: i18n.translate('kbn.advancedSettings.dateFormat.scaledTitle', { - defaultMessage: 'Scaled date format', - }), - type: 'json', - value: `[ - ["", "HH:mm:ss.SSS"], - ["PT1S", "HH:mm:ss"], - ["PT1M", "HH:mm"], - ["PT1H", "YYYY-MM-DD HH:mm"], - ["P1DT", "YYYY-MM-DD"], - ["P1YT", "YYYY"] -]`, - description: i18n.translate('kbn.advancedSettings.dateFormat.scaledText', { - defaultMessage: - 'Values that define the format used in situations where time-based ' + - 'data is rendered in order, and formatted timestamps should adapt to the ' + - 'interval between measurements. Keys are {intervalsLink}.', - description: - 'Part of composite text: kbn.advancedSettings.dateFormat.scaledText + ' + - 'kbn.advancedSettings.dateFormat.scaled.intervalsLinkText', - values: { - intervalsLink: - '' + - i18n.translate('kbn.advancedSettings.dateFormat.scaled.intervalsLinkText', { - defaultMessage: 'ISO8601 intervals', - }) + - '', - }, - }), - }, - 'dateFormat:dow': { - name: i18n.translate('kbn.advancedSettings.dateFormat.dayOfWeekTitle', { - defaultMessage: 'Day of week', - }), - value: defaultWeekday, - description: i18n.translate('kbn.advancedSettings.dateFormat.dayOfWeekText', { - defaultMessage: 'What day should weeks start on?', - }), - type: 'select', - options: weekdays, - }, - dateNanosFormat: { - name: i18n.translate('kbn.advancedSettings.dateNanosFormatTitle', { - defaultMessage: 'Date with nanoseconds format', - }), - value: 'MMM D, YYYY @ HH:mm:ss.SSSSSSSSS', - description: i18n.translate('kbn.advancedSettings.dateNanosFormatText', { - defaultMessage: 'Used for the {dateNanosLink} datatype of Elasticsearch', - values: { - dateNanosLink: - '' + - i18n.translate('kbn.advancedSettings.dateNanosLinkTitle', { - defaultMessage: 'date_nanos', - }) + - '', - }, - }), - }, 'visualization:tileMap:maxPrecision': { name: i18n.translate('kbn.advancedSettings.visualization.tileMap.maxPrecisionTitle', { defaultMessage: 'Maximum tile map precision', @@ -248,157 +100,5 @@ export function getUiSettingDefaults() { }), category: ['visualization'], }, - 'truncate:maxHeight': { - name: i18n.translate('kbn.advancedSettings.maxCellHeightTitle', { - defaultMessage: 'Maximum table cell height', - }), - value: 115, - description: i18n.translate('kbn.advancedSettings.maxCellHeightText', { - defaultMessage: - 'The maximum height that a cell in a table should occupy. Set to 0 to disable truncation', - }), - }, - 'theme:darkMode': { - name: i18n.translate('kbn.advancedSettings.darkModeTitle', { - defaultMessage: 'Dark mode', - }), - value: false, - description: i18n.translate('kbn.advancedSettings.darkModeText', { - defaultMessage: `Enable a dark mode for the Kibana UI. A page refresh is required for the setting to be applied.`, - }), - requiresPageReload: true, - }, - 'theme:version': { - name: i18n.translate('kbn.advancedSettings.themeVersionTitle', { - defaultMessage: 'Theme version', - }), - value: 'v7', - type: 'select', - options: ['v7', 'v8 (beta)'], - description: i18n.translate('kbn.advancedSettings.themeVersionText', { - defaultMessage: `Switch between the theme used for the current and next version of Kibana. A page refresh is required for the setting to be applied.`, - }), - requiresPageReload: true, - }, - 'notifications:banner': { - name: i18n.translate('kbn.advancedSettings.notifications.bannerTitle', { - defaultMessage: 'Custom banner notification', - }), - value: '', - type: 'markdown', - description: i18n.translate('kbn.advancedSettings.notifications.bannerText', { - defaultMessage: - 'A custom banner intended for temporary notices to all users. {markdownLink}.', - description: - 'Part of composite text: kbn.advancedSettings.notifications.bannerText + ' + - 'kbn.advancedSettings.notifications.banner.markdownLinkText', - values: { - markdownLink: - `` + - i18n.translate('kbn.advancedSettings.notifications.banner.markdownLinkText', { - defaultMessage: 'Markdown supported', - }) + - '', - }, - }), - category: ['notifications'], - }, - 'notifications:lifetime:banner': { - name: i18n.translate('kbn.advancedSettings.notifications.bannerLifetimeTitle', { - defaultMessage: 'Banner notification lifetime', - }), - value: 3000000, - description: i18n.translate('kbn.advancedSettings.notifications.bannerLifetimeText', { - defaultMessage: - 'The time in milliseconds which a banner notification will be displayed on-screen for. ' + - 'Setting to {infinityValue} will disable the countdown.', - values: { - infinityValue: 'Infinity', - }, - }), - type: 'number', - category: ['notifications'], - }, - 'notifications:lifetime:error': { - name: i18n.translate('kbn.advancedSettings.notifications.errorLifetimeTitle', { - defaultMessage: 'Error notification lifetime', - }), - value: 300000, - description: i18n.translate('kbn.advancedSettings.notifications.errorLifetimeText', { - defaultMessage: - 'The time in milliseconds which an error notification will be displayed on-screen for. ' + - 'Setting to {infinityValue} will disable.', - values: { - infinityValue: 'Infinity', - }, - }), - type: 'number', - category: ['notifications'], - }, - 'notifications:lifetime:warning': { - name: i18n.translate('kbn.advancedSettings.notifications.warningLifetimeTitle', { - defaultMessage: 'Warning notification lifetime', - }), - value: 10000, - description: i18n.translate('kbn.advancedSettings.notifications.warningLifetimeText', { - defaultMessage: - 'The time in milliseconds which a warning notification will be displayed on-screen for. ' + - 'Setting to {infinityValue} will disable.', - values: { - infinityValue: 'Infinity', - }, - }), - type: 'number', - category: ['notifications'], - }, - 'notifications:lifetime:info': { - name: i18n.translate('kbn.advancedSettings.notifications.infoLifetimeTitle', { - defaultMessage: 'Info notification lifetime', - }), - value: 5000, - description: i18n.translate('kbn.advancedSettings.notifications.infoLifetimeText', { - defaultMessage: - 'The time in milliseconds which an information notification will be displayed on-screen for. ' + - 'Setting to {infinityValue} will disable.', - values: { - infinityValue: 'Infinity', - }, - }), - type: 'number', - category: ['notifications'], - }, - 'accessibility:disableAnimations': { - name: i18n.translate('kbn.advancedSettings.disableAnimationsTitle', { - defaultMessage: 'Disable Animations', - }), - value: false, - description: i18n.translate('kbn.advancedSettings.disableAnimationsText', { - defaultMessage: - 'Turn off all unnecessary animations in the Kibana UI. Refresh the page to apply the changes.', - }), - category: ['accessibility'], - requiresPageReload: true, - }, - pageNavigation: { - name: i18n.translate('kbn.advancedSettings.pageNavigationName', { - defaultMessage: 'Side nav style', - }), - value: 'modern', - description: i18n.translate('kbn.advancedSettings.pageNavigationDesc', { - defaultMessage: 'Change the style of navigation', - }), - type: 'select', - options: ['modern', 'legacy'], - optionLabels: { - modern: i18n.translate('kbn.advancedSettings.pageNavigationModern', { - defaultMessage: 'Modern', - }), - legacy: i18n.translate('kbn.advancedSettings.pageNavigationLegacy', { - defaultMessage: 'Legacy', - }), - }, - schema: schema.oneOf([schema.literal('modern'), schema.literal('legacy')]), - }, }; } diff --git a/src/legacy/server/http/integration_tests/max_payload_size.test.js b/src/legacy/server/http/integration_tests/max_payload_size.test.js index a019220ca7a2..789a54f681ba 100644 --- a/src/legacy/server/http/integration_tests/max_payload_size.test.js +++ b/src/legacy/server/http/integration_tests/max_payload_size.test.js @@ -21,7 +21,11 @@ import * as kbnTestServer from '../../../../test_utils/kbn_server'; let root; beforeAll(async () => { - root = kbnTestServer.createRoot({ server: { maxPayloadBytes: 100 }, migrations: { skip: true } }); + root = kbnTestServer.createRoot({ + server: { maxPayloadBytes: 100 }, + migrations: { skip: true }, + plugins: { initialize: false }, + }); await root.setup(); await root.start(); diff --git a/src/plugins/data/common/search/es_search/types.ts b/src/plugins/data/common/search/es_search/types.ts index 8853e40dd0ad..3184fbe34170 100644 --- a/src/plugins/data/common/search/es_search/types.ts +++ b/src/plugins/data/common/search/es_search/types.ts @@ -22,16 +22,16 @@ import { IKibanaSearchRequest, IKibanaSearchResponse } from '../types'; export const ES_SEARCH_STRATEGY = 'es'; -export type ISearchRequestParams = { +export type ISearchRequestParams> = { trackTotalHits?: boolean; -} & Search; +} & Search; export interface IEsSearchRequest extends IKibanaSearchRequest { params?: ISearchRequestParams; indexType?: string; } -export interface IEsSearchResponse extends IKibanaSearchResponse { +export interface IEsSearchResponse extends IKibanaSearchResponse { /** * Indicates whether async search is still in flight */ @@ -40,5 +40,5 @@ export interface IEsSearchResponse extends IKibanaSearchResponse { * Indicates whether the results returned are complete or partial */ isPartial?: boolean; - rawResponse: SearchResponse; + rawResponse: SearchResponse; } diff --git a/src/plugins/data/public/index.ts b/src/plugins/data/public/index.ts index ecf076aa517f..eb5703f1c63c 100644 --- a/src/plugins/data/public/index.ts +++ b/src/plugins/data/public/index.ts @@ -360,7 +360,6 @@ export { SearchInterceptor, SearchInterceptorDeps, SearchRequest, - SearchResponse, SearchSourceFields, SortDirection, // expression functions and types diff --git a/src/plugins/data/public/mocks.ts b/src/plugins/data/public/mocks.ts index 78e40cfedd90..0eb0e3b65804 100644 --- a/src/plugins/data/public/mocks.ts +++ b/src/plugins/data/public/mocks.ts @@ -61,7 +61,7 @@ const createStartContract = (): Start => { query: queryStartMock, ui: { IndexPatternSelect: jest.fn(), - SearchBar: jest.fn(), + SearchBar: jest.fn().mockReturnValue(null), }, indexPatterns: ({ createField: jest.fn(() => {}), diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md index 7defddb8f570..f8a108a5a4c5 100644 --- a/src/plugins/data/public/public.api.md +++ b/src/plugins/data/public/public.api.md @@ -61,7 +61,7 @@ import { SavedObject } from 'src/core/server'; import { SavedObject as SavedObject_3 } from 'src/core/public'; import { SavedObjectsClientContract } from 'src/core/public'; import { Search } from '@elastic/elasticsearch/api/requestParams'; -import { SearchResponse as SearchResponse_2 } from 'elasticsearch'; +import { SearchResponse } from 'elasticsearch'; import { SerializedFieldFormat as SerializedFieldFormat_2 } from 'src/plugins/expressions/common'; import { Subscription } from 'rxjs'; import { Toast } from 'kibana/public'; @@ -727,6 +727,7 @@ export function getEsPreference(uiSettings: IUiSettingsClient_2, sessionId?: str export const getKbnTypeNames: () => string[]; // Warning: (ae-forgotten-export) The symbol "ISearchRequestParams" needs to be exported by the entry point index.d.ts +// Warning: (ae-incompatible-release-tags) The symbol "getSearchParamsFromRequest" is marked as @public, but its signature references "SearchRequest" which is marked as @internal // // @public (undocumented) export function getSearchParamsFromRequest(searchRequest: SearchRequest, dependencies: { @@ -791,11 +792,11 @@ export interface IEsSearchRequest extends IKibanaSearchRequest { // Warning: (ae-missing-release-tag) "IEsSearchResponse" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export interface IEsSearchResponse extends IKibanaSearchResponse { +export interface IEsSearchResponse extends IKibanaSearchResponse { isPartial?: boolean; isRunning?: boolean; // (undocumented) - rawResponse: SearchResponse_2; + rawResponse: SearchResponse; } // Warning: (ae-missing-release-tag) "IFieldFormat" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -1240,7 +1241,7 @@ export type ISearch = (request: IKibanaSearchRequest, options?: ISearchOptions) // Warning: (ae-missing-release-tag) "ISearchGeneric" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export type ISearchGeneric = (request: IEsSearchRequest, options?: ISearchOptions) => Observable; +export type ISearchGeneric = (request: SearchStrategyRequest, options?: ISearchOptions) => Observable; // Warning: (ae-missing-release-tag) "ISearchOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -1783,15 +1784,8 @@ export interface SearchInterceptorDeps { usageCollector?: SearchUsageCollector; } -// Warning: (ae-missing-release-tag) "SearchRequest" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export type SearchRequest = any; - -// Warning: (ae-missing-release-tag) "SearchResponse" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export type SearchResponse = any; +// @internal +export type SearchRequest = Record; // Warning: (ae-missing-release-tag) "SearchSourceFields" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -1991,21 +1985,21 @@ export const UI_SETTINGS: { // src/plugins/data/public/index.ts:234:27 - (ae-forgotten-export) The symbol "getFromSavedObject" needs to be exported by the entry point index.d.ts // src/plugins/data/public/index.ts:234:27 - (ae-forgotten-export) The symbol "flattenHitWrapper" needs to be exported by the entry point index.d.ts // src/plugins/data/public/index.ts:234:27 - (ae-forgotten-export) The symbol "formatHitProvider" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:372:20 - (ae-forgotten-export) The symbol "getRequestInspectorStats" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:372:20 - (ae-forgotten-export) The symbol "getResponseInspectorStats" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:372:20 - (ae-forgotten-export) The symbol "tabifyAggResponse" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:372:20 - (ae-forgotten-export) The symbol "tabifyGetColumns" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:374:1 - (ae-forgotten-export) The symbol "CidrMask" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:375:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:384:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:385:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:386:1 - (ae-forgotten-export) The symbol "Ipv4Address" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:387:1 - (ae-forgotten-export) The symbol "isDateHistogramBucketAggConfig" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:391:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:392:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:395:1 - (ae-forgotten-export) The symbol "parseInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:396:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:399:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:371:20 - (ae-forgotten-export) The symbol "getRequestInspectorStats" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:371:20 - (ae-forgotten-export) The symbol "getResponseInspectorStats" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:371:20 - (ae-forgotten-export) The symbol "tabifyAggResponse" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:371:20 - (ae-forgotten-export) The symbol "tabifyGetColumns" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:373:1 - (ae-forgotten-export) The symbol "CidrMask" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:374:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:383:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:384:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:385:1 - (ae-forgotten-export) The symbol "Ipv4Address" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:386:1 - (ae-forgotten-export) The symbol "isDateHistogramBucketAggConfig" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:390:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:391:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:394:1 - (ae-forgotten-export) The symbol "parseInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:395:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:398:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts // src/plugins/data/public/query/state_sync/connect_to_query_state.ts:45:5 - (ae-forgotten-export) The symbol "FilterStateStore" needs to be exported by the entry point index.d.ts // src/plugins/data/public/types.ts:62:5 - (ae-forgotten-export) The symbol "createFiltersFromValueClickAction" needs to be exported by the entry point index.d.ts // src/plugins/data/public/types.ts:63:5 - (ae-forgotten-export) The symbol "createFiltersFromRangeSelectAction" needs to be exported by the entry point index.d.ts diff --git a/src/plugins/data/public/search/fetch/handle_response.test.ts b/src/plugins/data/public/search/fetch/handle_response.test.ts index 10e6eda3de3d..9a9d806bc9cf 100644 --- a/src/plugins/data/public/search/fetch/handle_response.test.ts +++ b/src/plugins/data/public/search/fetch/handle_response.test.ts @@ -23,6 +23,7 @@ import { handleResponse } from './handle_response'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { notificationServiceMock } from '../../../../../core/public/notifications/notifications_service.mock'; import { setNotifications } from '../../services'; +import { SearchResponse } from 'elasticsearch'; jest.mock('@kbn/i18n', () => { return { @@ -44,7 +45,7 @@ describe('handleResponse', () => { const request = { body: {} }; const response = { timed_out: true, - }; + } as SearchResponse; const result = handleResponse(request, response); expect(result).toBe(response); expect(notifications.toasts.addWarning).toBeCalled(); @@ -57,9 +58,12 @@ describe('handleResponse', () => { const request = { body: {} }; const response = { _shards: { - failed: true, + failed: 1, + total: 2, + successful: 1, + skipped: 1, }, - }; + } as SearchResponse; const result = handleResponse(request, response); expect(result).toBe(response); expect(notifications.toasts.addWarning).toBeCalled(); @@ -70,7 +74,7 @@ describe('handleResponse', () => { test('returns the response', () => { const request = {}; - const response = {}; + const response = {} as SearchResponse; const result = handleResponse(request, response); expect(result).toBe(response); }); diff --git a/src/plugins/data/public/search/fetch/handle_response.tsx b/src/plugins/data/public/search/fetch/handle_response.tsx index 7905468f91c5..14e9b59f49bf 100644 --- a/src/plugins/data/public/search/fetch/handle_response.tsx +++ b/src/plugins/data/public/search/fetch/handle_response.tsx @@ -20,12 +20,13 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; import { EuiSpacer } from '@elastic/eui'; -import { ShardFailureOpenModalButton, ShardFailureRequest, ShardFailureResponse } from '../../ui'; +import { SearchResponse } from 'elasticsearch'; +import { ShardFailureOpenModalButton } from '../../ui'; import { toMountPoint } from '../../../../kibana_react/public'; import { getNotifications } from '../../services'; -import { SearchRequest, SearchResponse } from '..'; +import { SearchRequest } from '..'; -export function handleResponse(request: SearchRequest, response: SearchResponse) { +export function handleResponse(request: SearchRequest, response: SearchResponse) { if (response.timed_out) { getNotifications().toasts.addWarning({ title: i18n.translate('data.search.searchSource.fetch.requestTimedOutNotificationMessage', { @@ -53,11 +54,7 @@ export function handleResponse(request: SearchRequest, response: SearchResponse) <> {description} - + ); diff --git a/src/plugins/data/public/search/fetch/request_error.ts b/src/plugins/data/public/search/fetch/request_error.ts index 5e42a6fcf5b6..efaaafadf404 100644 --- a/src/plugins/data/public/search/fetch/request_error.ts +++ b/src/plugins/data/public/search/fetch/request_error.ts @@ -17,8 +17,9 @@ * under the License. */ +import { SearchResponse } from 'elasticsearch'; import { KbnError } from '../../../../kibana_utils/common'; -import { SearchError, SearchResponse } from './types'; +import { SearchError } from './types'; /** * Request Failure - When an entire multi request fails @@ -26,8 +27,8 @@ import { SearchError, SearchResponse } from './types'; * @param {Object} resp - optional HTTP response */ export class RequestFailure extends KbnError { - public resp: SearchResponse; - constructor(err: SearchError | null = null, resp?: SearchResponse) { + public resp?: SearchResponse; + constructor(err: SearchError | null = null, resp?: SearchResponse) { super(`Request to Elasticsearch failed: ${JSON.stringify(resp || err?.message)}`); this.resp = resp; diff --git a/src/plugins/data/public/search/fetch/types.ts b/src/plugins/data/public/search/fetch/types.ts index 18d277204815..670c4f731971 100644 --- a/src/plugins/data/public/search/fetch/types.ts +++ b/src/plugins/data/public/search/fetch/types.ts @@ -20,8 +20,14 @@ import { GetConfigFn } from '../../../common'; import { ISearchStartLegacy } from '../types'; -export type SearchRequest = any; -export type SearchResponse = any; +/** + * @internal + * + * This type is used when flattenning a SearchSource and passing it down to legacy search. + * Once legacy search is removed, this type should become internal to `SearchSource`, + * where `ISearchRequestParams` is used externally instead. + */ +export type SearchRequest = Record; export interface FetchOptions { abortSignal?: AbortSignal; diff --git a/src/plugins/data/public/search/index.ts b/src/plugins/data/public/search/index.ts index 32bcd8a27903..14eff13b378e 100644 --- a/src/plugins/data/public/search/index.ts +++ b/src/plugins/data/public/search/index.ts @@ -34,13 +34,7 @@ export { getEsPreference } from './es_search'; export { IKibanaSearchResponse, IKibanaSearchRequest } from '../../common/search'; -export { - SearchError, - FetchOptions, - SearchRequest, - SearchResponse, - getSearchParamsFromRequest, -} from './fetch'; +export { SearchError, FetchOptions, getSearchParamsFromRequest, SearchRequest } from './fetch'; export { ISearchSource, diff --git a/src/plugins/data/public/search/legacy/call_client.ts b/src/plugins/data/public/search/legacy/call_client.ts index 4b12f517daf7..3dcf11f72a74 100644 --- a/src/plugins/data/public/search/legacy/call_client.ts +++ b/src/plugins/data/public/search/legacy/call_client.ts @@ -17,6 +17,7 @@ * under the License. */ +import { SearchResponse } from 'elasticsearch'; import { FetchOptions, FetchHandlers, handleResponse } from '../fetch'; import { defaultSearchStrategy } from './default_search_strategy'; import { SearchRequest } from '../index'; @@ -32,7 +33,7 @@ export function callClient( FetchOptions ]> = searchRequests.map((request, i) => [request, requestsOptions[i]]); const requestOptionsMap = new Map(requestOptionEntries); - const requestResponseMap = new Map(); + const requestResponseMap = new Map>>(); const { searching, abort } = defaultSearchStrategy.search({ searchRequests, @@ -45,5 +46,5 @@ export function callClient( if (abortSignal) abortSignal.addEventListener('abort', abort); requestResponseMap.set(request, response); }); - return searchRequests.map((request) => requestResponseMap.get(request)); + return searchRequests.map((request) => requestResponseMap.get(request)!); } diff --git a/src/plugins/data/public/search/legacy/es_client/types.ts b/src/plugins/data/public/search/legacy/es_client/types.ts index 7a56b9b0cb00..2d35188322a4 100644 --- a/src/plugins/data/public/search/legacy/es_client/types.ts +++ b/src/plugins/data/public/search/legacy/es_client/types.ts @@ -17,13 +17,14 @@ * under the License. */ -import { SearchRequest, SearchResponse } from '../../fetch'; +import { SearchResponse } from 'elasticsearch'; +import { SearchRequest } from '../../fetch'; export interface LegacyApiCaller { search: (searchRequest: SearchRequest) => LegacyApiCallerResponse; msearch: (searchRequest: SearchRequest) => LegacyApiCallerResponse; } -interface LegacyApiCallerResponse extends Promise { +interface LegacyApiCallerResponse extends Promise> { abort: () => void; } diff --git a/src/plugins/data/public/search/legacy/fetch_soon.test.ts b/src/plugins/data/public/search/legacy/fetch_soon.test.ts index d375398af1ad..d7a85e65b475 100644 --- a/src/plugins/data/public/search/legacy/fetch_soon.test.ts +++ b/src/plugins/data/public/search/legacy/fetch_soon.test.ts @@ -20,17 +20,27 @@ import { fetchSoon } from './fetch_soon'; import { callClient } from './call_client'; import { FetchHandlers, FetchOptions } from '../fetch/types'; -import { SearchRequest, SearchResponse } from '../index'; +import { SearchRequest } from '../index'; +import { SearchResponse } from 'elasticsearch'; import { GetConfigFn, UI_SETTINGS } from '../../../common'; function getConfigStub(config: any = {}): GetConfigFn { return (key) => config[key]; } -const mockResponses: Record = { - foo: {}, - bar: {}, - baz: {}, +const mockResponses: Record> = { + foo: { + took: 1, + timed_out: false, + } as SearchResponse, + bar: { + took: 2, + timed_out: false, + } as SearchResponse, + baz: { + took: 3, + timed_out: false, + } as SearchResponse, }; jest.useFakeTimers(); diff --git a/src/plugins/data/public/search/legacy/fetch_soon.ts b/src/plugins/data/public/search/legacy/fetch_soon.ts index 252682c7c8e5..16920a8a4dd9 100644 --- a/src/plugins/data/public/search/legacy/fetch_soon.ts +++ b/src/plugins/data/public/search/legacy/fetch_soon.ts @@ -17,9 +17,10 @@ * under the License. */ +import { SearchResponse } from 'elasticsearch'; import { callClient } from './call_client'; import { FetchHandlers, FetchOptions } from '../fetch/types'; -import { SearchRequest, SearchResponse } from '../index'; +import { SearchRequest } from '../index'; import { UI_SETTINGS } from '../../../common'; /** @@ -53,7 +54,7 @@ let requestsToFetch: SearchRequest[] = []; let requestOptions: FetchOptions[] = []; // The in-progress fetch (if there is one) -let fetchInProgress: Promise | null = null; +let fetchInProgress: any = null; /** * Delay fetching for a given amount of time, while batching up the requests to be fetched. @@ -67,7 +68,7 @@ async function delayedFetch( options: FetchOptions, fetchHandlers: FetchHandlers, ms: number -) { +): Promise> { if (ms === 0) { return callClient([request], [options], fetchHandlers)[0]; } @@ -75,7 +76,10 @@ async function delayedFetch( const i = requestsToFetch.length; requestsToFetch = [...requestsToFetch, request]; requestOptions = [...requestOptions, options]; - const responses: SearchResponse[] = await (fetchInProgress = + + // Note: the typescript here only worked because `SearchResponse` was `any` + // Since this code is legacy, I'm leaving the any here. + const responses: any[] = await (fetchInProgress = fetchInProgress || delay(() => { const response = callClient(requestsToFetch, requestOptions, fetchHandlers); diff --git a/src/plugins/data/public/search/legacy/types.ts b/src/plugins/data/public/search/legacy/types.ts index 3812cec7a2aa..ed17db464fef 100644 --- a/src/plugins/data/public/search/legacy/types.ts +++ b/src/plugins/data/public/search/legacy/types.ts @@ -17,8 +17,9 @@ * under the License. */ +import { SearchResponse } from 'elasticsearch'; import { FetchHandlers } from '../fetch'; -import { SearchRequest, SearchResponse } from '..'; +import { SearchRequest } from '..'; export interface SearchStrategySearchParams extends FetchHandlers { searchRequests: SearchRequest[]; @@ -30,7 +31,7 @@ export interface SearchStrategyProvider { search: (params: SearchStrategySearchParams) => SearchStrategyResponse; } -export interface SearchStrategyResponse { - searching: Promise; +export interface SearchStrategyResponse { + searching: Promise>>; abort: () => void; } diff --git a/src/plugins/data/public/search/search_interceptor.ts b/src/plugins/data/public/search/search_interceptor.ts index 99fccda7fddf..30e509edd498 100644 --- a/src/plugins/data/public/search/search_interceptor.ts +++ b/src/plugins/data/public/search/search_interceptor.ts @@ -106,7 +106,7 @@ export class SearchInterceptor { ): Observable { const { id, ...searchRequest } = request; const path = trimEnd(`/internal/search/${strategy || ES_SEARCH_STRATEGY}/${id || ''}`, '/'); - const body = JSON.stringify(id != null ? {} : searchRequest); + const body = JSON.stringify(searchRequest); return from( this.deps.http.fetch({ method: 'POST', diff --git a/src/plugins/data/public/search/search_service.ts b/src/plugins/data/public/search/search_service.ts index a65b2b4b71f2..9a30a15936fe 100644 --- a/src/plugins/data/public/search/search_service.ts +++ b/src/plugins/data/public/search/search_service.ts @@ -103,9 +103,9 @@ export class SearchService implements Plugin { { application, http, injectedMetadata, notifications, uiSettings }: CoreStart, { fieldFormats, indexPatterns }: SearchServiceStartDependencies ): ISearchStart { - const search: ISearchGeneric = (request, options) => { + const search = ((request, options) => { return this.searchInterceptor.search(request, options); - }; + }) as ISearchGeneric; const legacySearch = { esClient: this.esClient!, diff --git a/src/plugins/data/public/search/search_source/search_source.ts b/src/plugins/data/public/search/search_source/search_source.ts index 06ad13bfcfdf..d2e337076205 100644 --- a/src/plugins/data/public/search/search_source/search_source.ts +++ b/src/plugins/data/public/search/search_source/search_source.ts @@ -75,9 +75,15 @@ import { map } from 'rxjs/operators'; import { normalizeSortRequest } from './normalize_sort_request'; import { filterDocvalueFields } from './filter_docvalue_fields'; import { fieldWildcardFilter } from '../../../../kibana_utils/common'; -import { IIndexPattern, ISearchGeneric, SearchRequest } from '../..'; +import { IIndexPattern, ISearchGeneric } from '../..'; import { SearchSourceOptions, SearchSourceFields } from './types'; -import { FetchOptions, RequestFailure, handleResponse, getSearchParamsFromRequest } from '../fetch'; +import { + FetchOptions, + RequestFailure, + handleResponse, + getSearchParamsFromRequest, + SearchRequest, +} from '../fetch'; import { getEsQueryConfig, buildEsQuery, Filter, UI_SETTINGS } from '../../../common'; import { getHighlightRequest } from '../../../common/field_formats'; @@ -268,10 +274,11 @@ export class SearchSource { if (getConfig(UI_SETTINGS.COURIER_BATCH_SEARCHES)) { response = await this.legacyFetch(searchRequest, options); } else { - response = this.fetch$(searchRequest, options.abortSignal).toPromise(); + response = await this.fetch$(searchRequest, options.abortSignal).toPromise(); } - if (response.error) { + // TODO: Remove casting when https://github.com/elastic/elasticsearch-js/issues/1287 is resolved + if ((response as any).error) { throw new RequestFailure(null, response); } @@ -403,7 +410,7 @@ export class SearchSource { return searchRequest; } - private getIndexType(index: IIndexPattern) { + private getIndexType(index?: IIndexPattern) { if (this.searchStrategyId) { return this.searchStrategyId === 'default' ? undefined : this.searchStrategyId; } else { diff --git a/src/plugins/data/public/search/types.ts b/src/plugins/data/public/search/types.ts index d1a443794340..55726e40f5a7 100644 --- a/src/plugins/data/public/search/types.ts +++ b/src/plugins/data/public/search/types.ts @@ -43,10 +43,13 @@ export type ISearch = ( options?: ISearchOptions ) => Observable; -export type ISearchGeneric = ( - request: IEsSearchRequest, +export type ISearchGeneric = < + SearchStrategyRequest extends IEsSearchRequest = IEsSearchRequest, + SearchStrategyResponse extends IEsSearchResponse = IEsSearchResponse +>( + request: SearchStrategyRequest, options?: ISearchOptions -) => Observable; +) => Observable; export interface ISearchStartLegacy { esClient: LegacyApiCaller; diff --git a/src/plugins/data/public/ui/index.ts b/src/plugins/data/public/ui/index.ts index cb46a838a8c3..35b1bc50ddb1 100644 --- a/src/plugins/data/public/ui/index.ts +++ b/src/plugins/data/public/ui/index.ts @@ -24,11 +24,7 @@ export { QueryStringInput } from './query_string_input/query_string_input'; export { SearchBar, SearchBarProps, StatefulSearchBarProps } from './search_bar'; // @internal -export { - ShardFailureOpenModalButton, - ShardFailureRequest, - ShardFailureResponse, -} from './shard_failure_modal'; +export { ShardFailureOpenModalButton, ShardFailureRequest } from './shard_failure_modal'; // @internal export { SavedQueryManagementComponent } from './saved_query_management'; diff --git a/src/plugins/data/public/ui/shard_failure_modal/__mocks__/shard_failure_response.ts b/src/plugins/data/public/ui/shard_failure_modal/__mocks__/shard_failure_response.ts index 573aeefcdf46..6178fcf92a79 100644 --- a/src/plugins/data/public/ui/shard_failure_modal/__mocks__/shard_failure_response.ts +++ b/src/plugins/data/public/ui/shard_failure_modal/__mocks__/shard_failure_response.ts @@ -16,9 +16,10 @@ * specific language governing permissions and limitations * under the License. */ -import { ShardFailureResponse } from '../shard_failure_types'; -export const shardFailureResponse = { +import { SearchResponse } from 'elasticsearch'; + +export const shardFailureResponse: SearchResponse = { _shards: { total: 2, successful: 1, @@ -43,4 +44,4 @@ export const shardFailureResponse = { }, ], }, -} as ShardFailureResponse; +} as any; diff --git a/src/plugins/data/public/ui/shard_failure_modal/index.ts b/src/plugins/data/public/ui/shard_failure_modal/index.ts index f4c2e26a756e..e5af9633e73b 100644 --- a/src/plugins/data/public/ui/shard_failure_modal/index.ts +++ b/src/plugins/data/public/ui/shard_failure_modal/index.ts @@ -17,5 +17,5 @@ * under the License. */ -export { ShardFailureRequest, ShardFailureResponse } from './shard_failure_types'; +export { ShardFailureRequest } from './shard_failure_types'; export { ShardFailureOpenModalButton } from './shard_failure_open_modal_button'; diff --git a/src/plugins/data/public/ui/shard_failure_modal/shard_failure_description.test.tsx b/src/plugins/data/public/ui/shard_failure_modal/shard_failure_description.test.tsx index 49983c992638..d40770fb74ef 100644 --- a/src/plugins/data/public/ui/shard_failure_modal/shard_failure_description.test.tsx +++ b/src/plugins/data/public/ui/shard_failure_modal/shard_failure_description.test.tsx @@ -24,7 +24,8 @@ import { ShardFailure } from './shard_failure_types'; describe('ShardFailureDescription', () => { it('renders matching snapshot given valid properties', () => { - const failure = shardFailureResponse._shards.failures[0] as ShardFailure; + // TODO: remove cast once https://github.com/elastic/elasticsearch-js/issues/1286 is resolved + const failure = (shardFailureResponse._shards as any).failures[0] as ShardFailure; const component = shallowWithIntl(); expect(component).toMatchSnapshot(); }); diff --git a/src/plugins/data/public/ui/shard_failure_modal/shard_failure_modal.tsx b/src/plugins/data/public/ui/shard_failure_modal/shard_failure_modal.tsx index 535f63161966..52f3cf702c2f 100644 --- a/src/plugins/data/public/ui/shard_failure_modal/shard_failure_modal.tsx +++ b/src/plugins/data/public/ui/shard_failure_modal/shard_failure_modal.tsx @@ -32,18 +32,24 @@ import { EuiButtonEmpty, EuiCallOut, } from '@elastic/eui'; +import { SearchResponse } from 'elasticsearch'; import { ShardFailureTable } from './shard_failure_table'; -import { ShardFailureResponse, ShardFailureRequest } from './shard_failure_types'; +import { ShardFailureRequest } from './shard_failure_types'; export interface Props { onClose: () => void; request: ShardFailureRequest; - response: ShardFailureResponse; + response: SearchResponse; title: string; } export function ShardFailureModal({ request, response, title, onClose }: Props) { - if (!response || !response._shards || !Array.isArray(response._shards.failures) || !request) { + if ( + !response || + !response._shards || + !Array.isArray((response._shards as any).failures) || + !request + ) { // this should never ever happen, but just in case return ( @@ -51,10 +57,9 @@ export function ShardFailureModal({ request, response, title, onClose }: Props) ); } - + const failures = (response._shards as any).failures; const requestJSON = JSON.stringify(request, null, 2); const responseJSON = JSON.stringify(response, null, 2); - const failures = response._shards.failures; const tabs = [ { diff --git a/src/plugins/data/public/ui/shard_failure_modal/shard_failure_open_modal_button.tsx b/src/plugins/data/public/ui/shard_failure_modal/shard_failure_open_modal_button.tsx index fa42745da2e4..9d89dc4cb1a2 100644 --- a/src/plugins/data/public/ui/shard_failure_modal/shard_failure_open_modal_button.tsx +++ b/src/plugins/data/public/ui/shard_failure_modal/shard_failure_open_modal_button.tsx @@ -20,14 +20,15 @@ import React from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiButton, EuiTextAlign } from '@elastic/eui'; +import { SearchResponse } from 'elasticsearch'; import { getOverlays } from '../../services'; import { toMountPoint } from '../../../../kibana_react/public'; import { ShardFailureModal } from './shard_failure_modal'; -import { ShardFailureResponse, ShardFailureRequest } from './shard_failure_types'; +import { ShardFailureRequest } from './shard_failure_types'; interface Props { request: ShardFailureRequest; - response: ShardFailureResponse; + response: SearchResponse; title: string; } diff --git a/src/plugins/data/public/ui/shard_failure_modal/shard_failure_table.test.tsx b/src/plugins/data/public/ui/shard_failure_modal/shard_failure_table.test.tsx index 9d00233d37f8..22478ebd5739 100644 --- a/src/plugins/data/public/ui/shard_failure_modal/shard_failure_table.test.tsx +++ b/src/plugins/data/public/ui/shard_failure_modal/shard_failure_table.test.tsx @@ -24,7 +24,7 @@ import { ShardFailure } from './shard_failure_types'; describe('ShardFailureTable', () => { it('renders matching snapshot given valid properties', () => { - const failures = shardFailureResponse._shards.failures as ShardFailure[]; + const failures = (shardFailureResponse._shards as any).failures as ShardFailure[]; const component = shallowWithIntl(); expect(component).toMatchSnapshot(); }); diff --git a/src/plugins/data/public/ui/shard_failure_modal/shard_failure_types.ts b/src/plugins/data/public/ui/shard_failure_modal/shard_failure_types.ts index b1ce3f30c427..a7a56d2de962 100644 --- a/src/plugins/data/public/ui/shard_failure_modal/shard_failure_types.ts +++ b/src/plugins/data/public/ui/shard_failure_modal/shard_failure_types.ts @@ -25,16 +25,6 @@ export interface ShardFailureRequest { stored_fields: string[]; } -export interface ShardFailureResponse { - _shards: { - failed: number; - failures: ShardFailure[]; - skipped: number; - successful: number; - total: number; - }; -} - export interface ShardFailure { index: string; node: string; diff --git a/src/plugins/data/server/index_patterns/fetcher/lib/field_capabilities/field_capabilities.ts b/src/plugins/data/server/index_patterns/fetcher/lib/field_capabilities/field_capabilities.ts index b4b86b73a5f4..6b26c82dc95e 100644 --- a/src/plugins/data/server/index_patterns/fetcher/lib/field_capabilities/field_capabilities.ts +++ b/src/plugins/data/server/index_patterns/fetcher/lib/field_capabilities/field_capabilities.ts @@ -25,10 +25,6 @@ import { FieldCapsResponse, readFieldCapsResponse } from './field_caps_response' import { mergeOverrides } from './overrides'; import { FieldDescriptor } from '../../index_patterns_fetcher'; -export function concatIfUniq(arr: T[], value: T) { - return arr.includes(value) ? arr : arr.concat(value); -} - /** * Get the field capabilities for field in `indices`, excluding * all internal/underscore-prefixed fields that are not in `metaFields` @@ -49,8 +45,20 @@ export async function getFieldCapabilities( const allFieldsUnsorted = Object.keys(fieldsFromFieldCapsByName) .filter((name) => !name.startsWith('_')) .concat(metaFields) - .reduce(concatIfUniq, [] as string[]) - .map((name) => + .reduce<{ names: string[]; hash: Record }>( + (agg, value) => { + // This is intentionally using a "hash" and a "push" to be highly optimized with very large indexes + if (agg.hash[value] != null) { + return agg; + } else { + agg.hash[value] = value; + agg.names.push(value); + return agg; + } + }, + { names: [], hash: {} } + ) + .names.map((name) => defaults({}, fieldsFromFieldCapsByName[name], { name, type: 'string', diff --git a/src/plugins/data/server/index_patterns/fetcher/lib/field_capabilities/field_caps_response.ts b/src/plugins/data/server/index_patterns/fetcher/lib/field_capabilities/field_caps_response.ts index cb1ec6a2ebcf..861b92569faf 100644 --- a/src/plugins/data/server/index_patterns/fetcher/lib/field_capabilities/field_caps_response.ts +++ b/src/plugins/data/server/index_patterns/fetcher/lib/field_capabilities/field_caps_response.ts @@ -93,8 +93,12 @@ export interface FieldCapsResponse { */ export function readFieldCapsResponse(fieldCapsResponse: FieldCapsResponse): FieldDescriptor[] { const capsByNameThenType = fieldCapsResponse.fields; - const kibanaFormattedCaps: FieldDescriptor[] = Object.keys(capsByNameThenType).map( - (fieldName) => { + + const kibanaFormattedCaps = Object.keys(capsByNameThenType).reduce<{ + array: FieldDescriptor[]; + hash: Record; + }>( + (agg, fieldName) => { const capsByType = capsByNameThenType[fieldName]; const types = Object.keys(capsByType); @@ -119,7 +123,7 @@ export function readFieldCapsResponse(fieldCapsResponse: FieldCapsResponse): Fie // ignore the conflict and carry on (my wayward son) const uniqueKibanaTypes = uniq(types.map(castEsToKbnFieldTypeName)); if (uniqueKibanaTypes.length > 1) { - return { + const field = { name: fieldName, type: 'conflict', esTypes: types, @@ -134,10 +138,14 @@ export function readFieldCapsResponse(fieldCapsResponse: FieldCapsResponse): Fie {} ), }; + // This is intentionally using a "hash" and a "push" to be highly optimized with very large indexes + agg.array.push(field); + agg.hash[fieldName] = field; + return agg; } const esType = types[0]; - return { + const field = { name: fieldName, type: castEsToKbnFieldTypeName(esType), esTypes: types, @@ -145,11 +153,19 @@ export function readFieldCapsResponse(fieldCapsResponse: FieldCapsResponse): Fie aggregatable: isAggregatable, readFromDocValues: shouldReadFieldFromDocValues(isAggregatable, esType), }; + // This is intentionally using a "hash" and a "push" to be highly optimized with very large indexes + agg.array.push(field); + agg.hash[fieldName] = field; + return agg; + }, + { + array: [], + hash: {}, } ); // Get all types of sub fields. These could be multi fields or children of nested/object types - const subFields = kibanaFormattedCaps.filter((field) => { + const subFields = kibanaFormattedCaps.array.filter((field) => { return field.name.includes('.'); }); @@ -161,9 +177,9 @@ export function readFieldCapsResponse(fieldCapsResponse: FieldCapsResponse): Fie .map((_, index, parentFieldNameParts) => { return parentFieldNameParts.slice(0, index + 1).join('.'); }); - const parentFieldCaps = parentFieldNames.map((parentFieldName) => { - return kibanaFormattedCaps.find((caps) => caps.name === parentFieldName); - }); + const parentFieldCaps = parentFieldNames.map( + (parentFieldName) => kibanaFormattedCaps.hash[parentFieldName] + ); const parentFieldCapsAscending = parentFieldCaps.reverse(); if (parentFieldCaps && parentFieldCaps.length > 0) { @@ -188,7 +204,7 @@ export function readFieldCapsResponse(fieldCapsResponse: FieldCapsResponse): Fie } }); - return kibanaFormattedCaps.filter((field) => { + return kibanaFormattedCaps.array.filter((field) => { return !['object', 'nested'].includes(field.type); }); } diff --git a/src/plugins/data/server/search/es_search/es_search_strategy.test.ts b/src/plugins/data/server/search/es_search/es_search_strategy.test.ts index 11564bb336b0..c34c3a310814 100644 --- a/src/plugins/data/server/search/es_search/es_search_strategy.test.ts +++ b/src/plugins/data/server/search/es_search/es_search_strategy.test.ts @@ -23,7 +23,7 @@ import { esSearchStrategyProvider } from './es_search_strategy'; describe('ES search strategy', () => { const mockLogger: any = { - info: () => {}, + debug: () => {}, }; const mockApiCaller = jest.fn().mockResolvedValue({ body: { diff --git a/src/plugins/data/server/search/es_search/es_search_strategy.ts b/src/plugins/data/server/search/es_search/es_search_strategy.ts index 7f9cb665b96b..eabbf3e3e260 100644 --- a/src/plugins/data/server/search/es_search/es_search_strategy.ts +++ b/src/plugins/data/server/search/es_search/es_search_strategy.ts @@ -31,7 +31,7 @@ export const esSearchStrategyProvider = ( ): ISearchStrategy => { return { search: async (context, request, options) => { - logger.info(`search ${request.params?.index}`); + logger.debug(`search ${request.params?.index}`); const config = await config$.pipe(first()).toPromise(); const defaultParams = getDefaultSearchParams(config); diff --git a/src/plugins/data/server/search/routes.test.ts b/src/plugins/data/server/search/routes.test.ts index 167bd5af5d51..d91aeee1fe81 100644 --- a/src/plugins/data/server/search/routes.test.ts +++ b/src/plugins/data/server/search/routes.test.ts @@ -36,7 +36,7 @@ describe('Search service', () => { const response = { id: 'yay' }; mockDataStart.search.search.mockResolvedValue(response); const mockContext = {}; - const mockBody = { params: {} }; + const mockBody = { id: undefined, params: {} }; const mockParams = { strategy: 'foo' }; const mockRequest = httpServerMock.createKibanaRequest({ body: mockBody, @@ -67,7 +67,7 @@ describe('Search service', () => { }); const mockContext = {}; - const mockBody = { params: {} }; + const mockBody = { id: undefined, params: {} }; const mockParams = { strategy: 'foo' }; const mockRequest = httpServerMock.createKibanaRequest({ body: mockBody, diff --git a/src/plugins/data/server/search/routes.ts b/src/plugins/data/server/search/routes.ts index 32d8f8c1b09e..3d813f745305 100644 --- a/src/plugins/data/server/search/routes.ts +++ b/src/plugins/data/server/search/routes.ts @@ -47,10 +47,14 @@ export function registerSearchRoute(core: CoreSetup): v const [, , selfStart] = await core.getStartServices(); try { - const response = await selfStart.search.search(context, id ? { id } : searchRequest, { - signal, - strategy, - }); + const response = await selfStart.search.search( + context, + { ...searchRequest, id }, + { + signal, + strategy, + } + ); return res.ok({ body: response }); } catch (err) { return res.customError({ diff --git a/src/plugins/data/server/search/search_service.test.ts b/src/plugins/data/server/search/search_service.test.ts index 030f37d0f7c4..7057c9c7ca15 100644 --- a/src/plugins/data/server/search/search_service.test.ts +++ b/src/plugins/data/server/search/search_service.test.ts @@ -32,7 +32,7 @@ describe('Search service', () => { beforeEach(() => { const mockLogger: any = { - info: () => {}, + debug: () => {}, }; plugin = new SearchService(coreMock.createPluginInitializerContext({}), mockLogger); mockCoreSetup = coreMock.createSetup(); diff --git a/src/plugins/data/server/search/search_service.ts b/src/plugins/data/server/search/search_service.ts index a8b1cdd608a8..cc23c455bed2 100644 --- a/src/plugins/data/server/search/search_service.ts +++ b/src/plugins/data/server/search/search_service.ts @@ -37,11 +37,12 @@ import { UsageCollectionSetup } from '../../../usage_collection/server'; import { registerUsageCollector } from './collectors/register'; import { usageProvider } from './collectors/usage'; import { searchTelemetry } from '../saved_objects'; -import { IEsSearchRequest } from '../../common'; +import { IEsSearchRequest, IEsSearchResponse } from '../../common'; -interface StrategyMap { - [name: string]: ISearchStrategy; -} +type StrategyMap< + SearchStrategyRequest extends IEsSearchRequest = IEsSearchRequest, + SearchStrategyResponse extends IEsSearchResponse = IEsSearchResponse +> = Record>; /** @internal */ export interface SearchServiceSetupDependencies { @@ -56,7 +57,7 @@ export interface SearchServiceStartDependencies { export class SearchService implements Plugin { private readonly aggsService = new AggsService(); - private searchStrategies: StrategyMap = {}; + private searchStrategies: StrategyMap = {}; constructor( private initializerContext: PluginInitializerContext, @@ -125,13 +126,19 @@ export class SearchService implements Plugin { this.aggsService.stop(); } - private registerSearchStrategy = (name: string, strategy: ISearchStrategy) => { - this.logger.info(`Register strategy ${name}`); + private registerSearchStrategy = < + SearchStrategyRequest extends IEsSearchRequest = IEsSearchRequest, + SearchStrategyResponse extends IEsSearchResponse = IEsSearchResponse + >( + name: string, + strategy: ISearchStrategy + ) => { + this.logger.debug(`Register strategy ${name}`); this.searchStrategies[name] = strategy; }; private getSearchStrategy = (name: string): ISearchStrategy => { - this.logger.info(`Get strategy ${name}`); + this.logger.debug(`Get strategy ${name}`); const strategy = this.searchStrategies[name]; if (!strategy) { throw new Error(`Search strategy ${name} not found`); diff --git a/src/plugins/data/server/search/types.ts b/src/plugins/data/server/search/types.ts index fe54975d7662..56f803512aa1 100644 --- a/src/plugins/data/server/search/types.ts +++ b/src/plugins/data/server/search/types.ts @@ -37,7 +37,13 @@ export interface ISearchSetup { * Extension point exposed for other plugins to register their own search * strategies. */ - registerSearchStrategy: (name: string, strategy: ISearchStrategy) => void; + registerSearchStrategy: < + SearchStrategyRequest extends IEsSearchRequest = IEsSearchRequest, + SearchStrategyResponse extends IEsSearchResponse = IEsSearchResponse + >( + name: string, + strategy: ISearchStrategy + ) => void; /** * Used internally for telemetry @@ -45,13 +51,18 @@ export interface ISearchSetup { usage?: SearchUsage; } -export interface ISearchStart { +export interface ISearchStart< + SearchStrategyRequest extends IEsSearchRequest = IEsSearchRequest, + SearchStrategyResponse extends IEsSearchResponse = IEsSearchResponse +> { aggs: AggsStart; /** * Get other registered search strategies. For example, if a new strategy needs to use the * already-registered ES search strategy, it can use this function to accomplish that. */ - getSearchStrategy: (name: string) => ISearchStrategy; + getSearchStrategy: ( + name: string + ) => ISearchStrategy; search: ( context: RequestHandlerContext, request: IKibanaSearchRequest, @@ -63,11 +74,14 @@ export interface ISearchStart { * Search strategy interface contains a search method that takes in a request and returns a promise * that resolves to a response. */ -export interface ISearchStrategy { +export interface ISearchStrategy< + SearchStrategyRequest extends IEsSearchRequest = IEsSearchRequest, + SearchStrategyResponse extends IEsSearchResponse = IEsSearchResponse +> { search: ( context: RequestHandlerContext, - request: IEsSearchRequest, + request: SearchStrategyRequest, options?: ISearchOptions - ) => Promise; + ) => Promise; cancel?: (context: RequestHandlerContext, id: string) => Promise; } diff --git a/src/plugins/data/server/server.api.md b/src/plugins/data/server/server.api.md index f0a0d2763ff2..f870030ae956 100644 --- a/src/plugins/data/server/server.api.md +++ b/src/plugins/data/server/server.api.md @@ -498,11 +498,11 @@ export interface IEsSearchRequest extends IKibanaSearchRequest { // Warning: (ae-missing-release-tag) "IEsSearchResponse" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export interface IEsSearchResponse extends IKibanaSearchResponse { +export interface IEsSearchResponse extends IKibanaSearchResponse { isPartial?: boolean; isRunning?: boolean; // (undocumented) - rawResponse: SearchResponse; + rawResponse: SearchResponse; } // Warning: (ae-missing-release-tag) "IFieldFormatsRegistry" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -689,19 +689,19 @@ export interface ISearchSetup { // // (undocumented) aggs: AggsSetup; - registerSearchStrategy: (name: string, strategy: ISearchStrategy) => void; + registerSearchStrategy: (name: string, strategy: ISearchStrategy) => void; usage?: SearchUsage; } // Warning: (ae-missing-release-tag) "ISearchStart" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export interface ISearchStart { +export interface ISearchStart { // Warning: (ae-forgotten-export) The symbol "AggsStart" needs to be exported by the entry point index.d.ts // // (undocumented) aggs: AggsStart; - getSearchStrategy: (name: string) => ISearchStrategy; + getSearchStrategy: (name: string) => ISearchStrategy; // Warning: (ae-forgotten-export) The symbol "RequestHandlerContext" needs to be exported by the entry point index.d.ts // // (undocumented) @@ -711,11 +711,11 @@ export interface ISearchStart { // Warning: (ae-missing-release-tag) "ISearchStrategy" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public -export interface ISearchStrategy { +export interface ISearchStrategy { // (undocumented) cancel?: (context: RequestHandlerContext, id: string) => Promise; // (undocumented) - search: (context: RequestHandlerContext, request: IEsSearchRequest, options?: ISearchOptions) => Promise; + search: (context: RequestHandlerContext, request: SearchStrategyRequest, options?: ISearchOptions) => Promise; } // @public (undocumented) @@ -862,7 +862,7 @@ export class Plugin implements Plugin_2>; fieldFormats: { fieldFormatServiceFactory: (uiSettings: import("../../../core/server").IUiSettingsClient) => Promise; }; diff --git a/src/plugins/discover/public/application/angular/context/api/utils/fetch_hits_in_interval.ts b/src/plugins/discover/public/application/angular/context/api/utils/fetch_hits_in_interval.ts index 437898201863..9a199ea4a62f 100644 --- a/src/plugins/discover/public/application/angular/context/api/utils/fetch_hits_in_interval.ts +++ b/src/plugins/discover/public/application/angular/context/api/utils/fetch_hits_in_interval.ts @@ -18,7 +18,7 @@ */ import { ISearchSource, EsQuerySortValue, SortDirection } from '../../../../../../../data/public'; import { convertTimeValueToIso } from './date_conversion'; -import { EsHitRecordList } from '../context'; +import { EsHitRecordList, EsHitRecord } from '../context'; import { IntervalValue } from './generate_intervals'; import { EsQuerySearchAfter } from './get_es_query_search_after'; @@ -76,5 +76,6 @@ export async function fetchHitsInInterval( .setField('version', true) .fetch(); - return response.hits ? response.hits.hits : []; + // TODO: There's a difference in the definition of SearchResponse and EsHitRecord + return ((response.hits?.hits as unknown) as EsHitRecord[]) || []; } diff --git a/src/plugins/input_control_vis/public/control/list_control_factory.ts b/src/plugins/input_control_vis/public/control/list_control_factory.ts index acbbf08c7d00..325eb471d510 100644 --- a/src/plugins/input_control_vis/public/control/list_control_factory.ts +++ b/src/plugins/input_control_vis/public/control/list_control_factory.ts @@ -190,7 +190,9 @@ export class ListControl extends Control { return; } - this.partialResults = resp.terminated_early || resp.timed_out; + // TODO: terminated_early is missing from response definition. + // https://github.com/elastic/elasticsearch-js/issues/1289 + this.partialResults = (resp as any).terminated_early || resp.timed_out; this.selectOptions = selectOptions; this.enable = true; this.disabledReason = ''; diff --git a/src/plugins/legacy_export/server/lib/import/import_dashboards.test.ts b/src/plugins/legacy_export/server/lib/import/import_dashboards.test.ts index 9d4dbb606794..37e00a8c67fe 100644 --- a/src/plugins/legacy_export/server/lib/import/import_dashboards.test.ts +++ b/src/plugins/legacy_export/server/lib/import/import_dashboards.test.ts @@ -30,12 +30,18 @@ describe('importDashboards(req)', () => { savedObjectClient.bulkCreate.mockResolvedValue({ saved_objects: [] }); importedObjects = [ - { id: 'dashboard-01', type: 'dashboard', attributes: { panelJSON: '{}' }, references: [] }, + { + id: 'dashboard-01', + type: 'dashboard', + attributes: { panelJSON: '{}' }, + references: [], + version: 'foo', + }, { id: 'panel-01', type: 'visualization', attributes: { visState: '{}' }, references: [] }, ]; }); - test('should call bulkCreate with each asset', async () => { + test('should call bulkCreate with each asset, filtering out any version if present', async () => { await importDashboards(savedObjectClient, importedObjects, { overwrite: false, exclude: [] }); expect(savedObjectClient.bulkCreate).toHaveBeenCalledTimes(1); diff --git a/src/plugins/legacy_export/server/lib/import/import_dashboards.ts b/src/plugins/legacy_export/server/lib/import/import_dashboards.ts index 7b7562aecd7b..8c9eb2fac61a 100644 --- a/src/plugins/legacy_export/server/lib/import/import_dashboards.ts +++ b/src/plugins/legacy_export/server/lib/import/import_dashboards.ts @@ -31,7 +31,8 @@ export async function importDashboards( // docs are not seen as automatically up-to-date. const docs = objects .filter((item) => !exclude.includes(item.type)) - .map((doc) => ({ ...doc, migrationVersion: doc.migrationVersion || {} })); + // filter out any document version, if present + .map(({ version, ...doc }) => ({ ...doc, migrationVersion: doc.migrationVersion || {} })); const results = await savedObjectsClient.bulkCreate(docs, { overwrite }); return { objects: results.saved_objects }; diff --git a/src/plugins/telemetry/schema/oss_plugins.json b/src/plugins/telemetry/schema/oss_plugins.json index a5172c01b1da..c306446b9780 100644 --- a/src/plugins/telemetry/schema/oss_plugins.json +++ b/src/plugins/telemetry/schema/oss_plugins.json @@ -13,6 +13,19 @@ } } }, + "search": { + "properties": { + "successCount": { + "type": "number" + }, + "errorCount": { + "type": "number" + }, + "averageDuration": { + "type": "long" + } + } + }, "sample-data": { "properties": { "installed": { @@ -35,6 +48,19 @@ } } }, + "csp": { + "properties": { + "strict": { + "type": "boolean" + }, + "warnLegacyBrowsers": { + "type": "boolean" + }, + "rulesChangedFromDefault": { + "type": "boolean" + } + } + }, "telemetry": { "properties": { "opt_in_status": { diff --git a/src/plugins/telemetry/server/plugin.ts b/src/plugins/telemetry/server/plugin.ts index 6c8888feafc1..bd7a2a8c1a8c 100644 --- a/src/plugins/telemetry/server/plugin.ts +++ b/src/plugins/telemetry/server/plugin.ts @@ -89,6 +89,7 @@ export class TelemetryPlugin implements Plugin { config$, currentKibanaVersion, isDev, + logger: this.logger, router, telemetryCollectionManager, }); diff --git a/src/plugins/telemetry/server/routes/index.ts b/src/plugins/telemetry/server/routes/index.ts index ad84cb9d2665..f46c616a734e 100644 --- a/src/plugins/telemetry/server/routes/index.ts +++ b/src/plugins/telemetry/server/routes/index.ts @@ -18,7 +18,7 @@ */ import { Observable } from 'rxjs'; -import { IRouter } from 'kibana/server'; +import { IRouter, Logger } from 'kibana/server'; import { TelemetryCollectionManagerPluginSetup } from 'src/plugins/telemetry_collection_manager/server'; import { registerTelemetryOptInRoutes } from './telemetry_opt_in'; import { registerTelemetryUsageStatsRoutes } from './telemetry_usage_stats'; @@ -28,6 +28,7 @@ import { TelemetryConfigType } from '../config'; interface RegisterRoutesParams { isDev: boolean; + logger: Logger; config$: Observable; currentKibanaVersion: string; router: IRouter; diff --git a/src/plugins/telemetry/server/routes/telemetry_opt_in.ts b/src/plugins/telemetry/server/routes/telemetry_opt_in.ts index 7dd15f73029e..aa1de4b2443a 100644 --- a/src/plugins/telemetry/server/routes/telemetry_opt_in.ts +++ b/src/plugins/telemetry/server/routes/telemetry_opt_in.ts @@ -21,7 +21,7 @@ import moment from 'moment'; import { Observable } from 'rxjs'; import { take } from 'rxjs/operators'; import { schema } from '@kbn/config-schema'; -import { IRouter } from 'kibana/server'; +import { IRouter, Logger } from 'kibana/server'; import { StatsGetterConfig, TelemetryCollectionManagerPluginSetup, @@ -39,12 +39,14 @@ import { TelemetryConfigType } from '../config'; interface RegisterOptInRoutesParams { currentKibanaVersion: string; router: IRouter; + logger: Logger; config$: Observable; telemetryCollectionManager: TelemetryCollectionManagerPluginSetup; } export function registerTelemetryOptInRoutes({ config$, + logger, router, currentKibanaVersion, telemetryCollectionManager, @@ -95,11 +97,16 @@ export function registerTelemetryOptInRoutes({ if (config.sendUsageFrom === 'server') { const optInStatusUrl = config.optInStatusUrl; - await sendTelemetryOptInStatus( + sendTelemetryOptInStatus( telemetryCollectionManager, { optInStatusUrl, newOptInStatus }, statsGetterConfig - ); + ).catch((err) => { + // The server is likely behind a firewall and can't reach the remote service + logger.warn( + `Failed to notify "${optInStatusUrl}" from the server about the opt-in selection. Possibly blocked by a firewall? - Error: ${err.message}` + ); + }); } await updateTelemetrySavedObject(context.core.savedObjects.client, attributes); diff --git a/src/plugins/telemetry/server/telemetry_collection/get_data_telemetry/get_data_telemetry.test.ts b/src/plugins/telemetry/server/telemetry_collection/get_data_telemetry/get_data_telemetry.test.ts index ad19def16020..dee718decdc1 100644 --- a/src/plugins/telemetry/server/telemetry_collection/get_data_telemetry/get_data_telemetry.test.ts +++ b/src/plugins/telemetry/server/telemetry_collection/get_data_telemetry/get_data_telemetry.test.ts @@ -59,16 +59,16 @@ describe('get_data_telemetry', () => { test('matches some indices and puts them in their own category', () => { expect( buildDataTelemetryPayload([ - // APM Indices have known shipper (so we can infer the datasetType from mapping constant) + // APM Indices have known shipper (so we can infer the dataStreamType from mapping constant) { name: 'apm-7.7.0-error-000001', shipper: 'apm', isECS: true }, { name: 'apm-7.7.0-metric-000001', shipper: 'apm', isECS: true }, { name: 'apm-7.7.0-onboarding-2020.05.17', shipper: 'apm', isECS: true }, { name: 'apm-7.7.0-profile-000001', shipper: 'apm', isECS: true }, { name: 'apm-7.7.0-span-000001', shipper: 'apm', isECS: true }, { name: 'apm-7.7.0-transaction-000001', shipper: 'apm', isECS: true }, - // Packetbeat indices with known shipper (we can infer datasetType from mapping constant) + // Packetbeat indices with known shipper (we can infer dataStreamType from mapping constant) { name: 'packetbeat-7.7.0-2020.06.11-000001', shipper: 'packetbeat', isECS: true }, - // Matching patterns from the list => known datasetName but the rest is unknown + // Matching patterns from the list => known dataStreamDataset but the rest is unknown { name: 'filebeat-12314', docCount: 100, sizeInBytes: 10 }, { name: 'metricbeat-1234', docCount: 100, sizeInBytes: 10, isECS: false }, { name: '.app-search-1234', docCount: 0 }, @@ -76,8 +76,8 @@ describe('get_data_telemetry', () => { // New Indexing strategy: everything can be inferred from the constant_keyword values { name: '.ds-logs-nginx.access-default-000001', - datasetName: 'nginx.access', - datasetType: 'logs', + dataStreamDataset: 'nginx.access', + dataStreamType: 'logs', shipper: 'filebeat', isECS: true, docCount: 1000, @@ -85,8 +85,8 @@ describe('get_data_telemetry', () => { }, { name: '.ds-logs-nginx.access-default-000002', - datasetName: 'nginx.access', - datasetType: 'logs', + dataStreamDataset: 'nginx.access', + dataStreamType: 'logs', shipper: 'filebeat', isECS: true, docCount: 1000, @@ -94,8 +94,8 @@ describe('get_data_telemetry', () => { }, { name: '.ds-traces-something-default-000002', - datasetName: 'something', - datasetType: 'traces', + dataStreamDataset: 'something', + dataStreamType: 'traces', packageName: 'some-package', isECS: true, docCount: 1000, @@ -103,26 +103,26 @@ describe('get_data_telemetry', () => { }, { name: '.ds-metrics-something.else-default-000002', - datasetName: 'something.else', - datasetType: 'metrics', + dataStreamDataset: 'something.else', + dataStreamType: 'metrics', managedBy: 'ingest-manager', isECS: true, docCount: 1000, sizeInBytes: 60, }, - // Filter out if it has datasetName and datasetType but none of the shipper, packageName or managedBy === 'ingest-manager' + // Filter out if it has dataStreamDataset and dataStreamType but none of the shipper, packageName or managedBy === 'ingest-manager' { name: 'some-index-that-should-not-show', - datasetName: 'should-not-show', - datasetType: 'logs', + dataStreamDataset: 'should-not-show', + dataStreamType: 'logs', isECS: true, docCount: 1000, sizeInBytes: 60, }, { name: 'other-index-that-should-not-show', - datasetName: 'should-not-show-either', - datasetType: 'metrics', + dataStreamDataset: 'should-not-show-either', + dataStreamType: 'metrics', managedBy: 'me', isECS: true, docCount: 1000, @@ -167,7 +167,7 @@ describe('get_data_telemetry', () => { doc_count: 0, }, { - dataset: { name: 'nginx.access', type: 'logs' }, + data_stream: { dataset: 'nginx.access', type: 'logs' }, shipper: 'filebeat', index_count: 2, ecs_index_count: 2, @@ -175,7 +175,7 @@ describe('get_data_telemetry', () => { size_in_bytes: 1060, }, { - dataset: { name: 'something', type: 'traces' }, + data_stream: { dataset: 'something', type: 'traces' }, package: { name: 'some-package' }, index_count: 1, ecs_index_count: 1, @@ -183,7 +183,7 @@ describe('get_data_telemetry', () => { size_in_bytes: 60, }, { - dataset: { name: 'something.else', type: 'metrics' }, + data_stream: { dataset: 'something.else', type: 'metrics' }, index_count: 1, ecs_index_count: 1, doc_count: 1000, @@ -236,7 +236,7 @@ describe('get_data_telemetry', () => { test('find an index that does not match any index pattern but has mappings metadata', async () => { const callCluster = mockCallCluster( ['cannot_match_anything'], - { isECS: true, datasetType: 'traces', shipper: 'my-beat' }, + { isECS: true, dataStreamType: 'traces', shipper: 'my-beat' }, { indices: { cannot_match_anything: { @@ -247,7 +247,7 @@ describe('get_data_telemetry', () => { ); await expect(getDataTelemetry(callCluster)).resolves.toStrictEqual([ { - dataset: { name: undefined, type: 'traces' }, + data_stream: { dataset: undefined, type: 'traces' }, shipper: 'my-beat', index_count: 1, ecs_index_count: 1, @@ -266,7 +266,7 @@ describe('get_data_telemetry', () => { function mockCallCluster( indicesMappings: string[] = [], - { isECS = false, datasetName = '', datasetType = '', shipper = '' } = {}, + { isECS = false, dataStreamDataset = '', dataStreamType = '', shipper = '' } = {}, indexStats: any = {} ) { return jest.fn().mockImplementation(async (method: string, opts: any) => { @@ -279,14 +279,14 @@ function mockCallCluster( ...(shipper && { _meta: { beat: shipper } }), properties: { ...(isECS && { ecs: { properties: { version: { type: 'keyword' } } } }), - ...((datasetType || datasetName) && { - dataset: { + ...((dataStreamType || dataStreamDataset) && { + data_stream: { properties: { - ...(datasetName && { - name: { type: 'constant_keyword', value: datasetName }, + ...(dataStreamDataset && { + dataset: { type: 'constant_keyword', value: dataStreamDataset }, }), - ...(datasetType && { - type: { type: 'constant_keyword', value: datasetType }, + ...(dataStreamType && { + type: { type: 'constant_keyword', value: dataStreamType }, }), }, }, diff --git a/src/plugins/telemetry/server/telemetry_collection/get_data_telemetry/get_data_telemetry.ts b/src/plugins/telemetry/server/telemetry_collection/get_data_telemetry/get_data_telemetry.ts index 079f510bb256..f4734dde251c 100644 --- a/src/plugins/telemetry/server/telemetry_collection/get_data_telemetry/get_data_telemetry.ts +++ b/src/plugins/telemetry/server/telemetry_collection/get_data_telemetry/get_data_telemetry.ts @@ -32,9 +32,9 @@ export interface DataTelemetryBasePayload { } export interface DataTelemetryDocument extends DataTelemetryBasePayload { - dataset?: { - name?: string; - type?: DataTelemetryType | 'unknown' | string; // The union of types is to help autocompletion with some known `dataset.type`s + data_stream?: { + dataset?: string; + type?: DataTelemetryType | string; // The union of types is to help autocompletion with some known `data_stream.type`s }; package?: { name: string; @@ -49,8 +49,8 @@ export interface DataTelemetryIndex { name: string; packageName?: string; // Populated by Ingest Manager at `_meta.package.name` managedBy?: string; // Populated by Ingest Manager at `_meta.managed_by` - datasetName?: string; // To be obtained from `mappings.dataset.name` if it's a constant keyword - datasetType?: string; // To be obtained from `mappings.dataset.type` if it's a constant keyword + dataStreamDataset?: string; // To be obtained from `mappings.data_stream.dataset` if it's a constant keyword + dataStreamType?: string; // To be obtained from `mappings.data_stream.type` if it's a constant keyword shipper?: string; // To be obtained from `_meta.beat` if it's set isECS?: boolean; // Optional because it can't be obtained via Monitoring. @@ -64,8 +64,8 @@ type AtLeastOne }> = Partial & U[keyof U] type DataDescriptor = AtLeastOne<{ packageName: string; - datasetName: string; - datasetType: string; + dataStreamDataset: string; + dataStreamType: string; shipper: string; patternName: DataPatternName; // When found from the list of the index patterns }>; @@ -75,24 +75,24 @@ function findMatchingDescriptors({ shipper, packageName, managedBy, - datasetName, - datasetType, + dataStreamDataset, + dataStreamType, }: DataTelemetryIndex): DataDescriptor[] { // If we already have the data from the indices' mappings... if ( [shipper, packageName].some(Boolean) || - (managedBy === 'ingest-manager' && [datasetType, datasetName].some(Boolean)) + (managedBy === 'ingest-manager' && [dataStreamType, dataStreamDataset].some(Boolean)) ) { return [ { ...(shipper && { shipper }), ...(packageName && { packageName }), - ...(datasetName && { datasetName }), - ...(datasetType && { datasetType }), + ...(dataStreamDataset && { dataStreamDataset }), + ...(dataStreamType && { dataStreamType }), } as AtLeastOne<{ packageName: string; - datasetName: string; - datasetType: string; + dataStreamDataset: string; + dataStreamType: string; shipper: string; }>, // Using casting here because TS doesn't infer at least one exists from the if clause ]; @@ -149,15 +149,17 @@ export function buildDataTelemetryPayload(indices: DataTelemetryIndex[]): DataTe for (const indexCandidate of indexCandidates) { const matchingDescriptors = findMatchingDescriptors(indexCandidate); for (const { - datasetName, - datasetType, + dataStreamDataset, + dataStreamType, packageName, shipper, patternName, } of matchingDescriptors) { - const key = `${datasetName}-${datasetType}-${packageName}-${shipper}-${patternName}`; + const key = `${dataStreamDataset}-${dataStreamType}-${packageName}-${shipper}-${patternName}`; acc.set(key, { - ...((datasetName || datasetType) && { dataset: { name: datasetName, type: datasetType } }), + ...((dataStreamDataset || dataStreamType) && { + data_stream: { dataset: dataStreamDataset, type: dataStreamType }, + }), ...(packageName && { package: { name: packageName } }), ...(shipper && { shipper }), ...(patternName && { pattern_name: patternName }), @@ -198,9 +200,9 @@ interface IndexMappings { managed_by?: string; // Typically "ingest-manager" }; properties: { - dataset?: { + data_stream?: { properties: { - name?: { + dataset?: { type: string; value?: string; }; @@ -242,10 +244,10 @@ export async function getDataTelemetry(callCluster: LegacyAPICaller) { // Does it have `ecs.version` in the mappings? => It follows the ECS conventions '*.mappings.properties.ecs.properties.version.type', - // If `dataset.type` is a `constant_keyword`, it can be reported as a type - '*.mappings.properties.dataset.properties.type.value', - // If `dataset.name` is a `constant_keyword`, it can be reported as the dataset - '*.mappings.properties.dataset.properties.name.value', + // If `data_stream.type` is a `constant_keyword`, it can be reported as a type + '*.mappings.properties.data_stream.properties.type.value', + // If `data_stream.dataset` is a `constant_keyword`, it can be reported as the dataset + '*.mappings.properties.data_stream.properties.dataset.value', ], }), // GET /_stats/docs,store?level=indices&filter_path=indices.*.total @@ -265,8 +267,10 @@ export async function getDataTelemetry(callCluster: LegacyAPICaller) { shipper: indexMappings[name]?.mappings?._meta?.beat, packageName: indexMappings[name]?.mappings?._meta?.package?.name, managedBy: indexMappings[name]?.mappings?._meta?.managed_by, - datasetName: indexMappings[name]?.mappings?.properties.dataset?.properties.name?.value, - datasetType: indexMappings[name]?.mappings?.properties.dataset?.properties.type?.value, + dataStreamDataset: + indexMappings[name]?.mappings?.properties.data_stream?.properties.dataset?.value, + dataStreamType: + indexMappings[name]?.mappings?.properties.data_stream?.properties.type?.value, }; const stats = (indexStats?.indices || {})[name]; diff --git a/src/plugins/timelion/server/plugin.ts b/src/plugins/timelion/server/plugin.ts index 3e4cd5467dd4..fe77ebeb0866 100644 --- a/src/plugins/timelion/server/plugin.ts +++ b/src/plugins/timelion/server/plugin.ts @@ -17,13 +17,42 @@ * under the License. */ -import { CoreSetup, Plugin, PluginInitializerContext } from 'src/core/server'; +import { CoreSetup, CoreStart, Plugin, PluginInitializerContext, Logger } from 'src/core/server'; import { i18n } from '@kbn/i18n'; import { schema } from '@kbn/config-schema'; import { TimelionConfigType } from './config'; +import { timelionSheetSavedObjectType } from './saved_objects'; + +/** + * Deprecated since 7.0, the Timelion app will be removed in 8.0. + * To continue using your Timelion worksheets, migrate them to a dashboard. + * + * @link https://www.elastic.co/guide/en/kibana/master/timelion.html#timelion-deprecation + **/ +const showWarningMessageIfTimelionSheetWasFound = (core: CoreStart, logger: Logger) => { + const { savedObjects } = core; + const savedObjectsClient = savedObjects.createInternalRepository(); + + savedObjectsClient + .find({ + type: 'timelion-sheet', + perPage: 1, + }) + .then( + ({ total }) => + total && + logger.warn( + 'Deprecated since 7.0, the Timelion app will be removed in 8.0. To continue using your Timelion worksheets, migrate them to a dashboard. See https://www.elastic.co/guide/en/kibana/master/timelion.html#timelion-deprecation.' + ) + ); +}; export class TimelionPlugin implements Plugin { - constructor(context: PluginInitializerContext) {} + private logger: Logger; + + constructor(context: PluginInitializerContext) { + this.logger = context.logger.get(); + } public setup(core: CoreSetup) { core.capabilities.registerProvider(() => ({ @@ -31,30 +60,7 @@ export class TimelionPlugin implements Plugin { save: true, }, })); - core.savedObjects.registerType({ - name: 'timelion-sheet', - hidden: false, - namespaceType: 'single', - mappings: { - properties: { - description: { type: 'text' }, - hits: { type: 'integer' }, - kibanaSavedObjectMeta: { - properties: { - searchSourceJSON: { type: 'text' }, - }, - }, - timelion_chart_height: { type: 'integer' }, - timelion_columns: { type: 'integer' }, - timelion_interval: { type: 'keyword' }, - timelion_other_interval: { type: 'keyword' }, - timelion_rows: { type: 'integer' }, - timelion_sheet: { type: 'text' }, - title: { type: 'text' }, - version: { type: 'integer' }, - }, - }, - }); + core.savedObjects.registerType(timelionSheetSavedObjectType); core.uiSettings.register({ 'timelion:showTutorial': { @@ -92,6 +98,8 @@ export class TimelionPlugin implements Plugin { }, }); } - start() {} + start(core: CoreStart) { + showWarningMessageIfTimelionSheetWasFound(core, this.logger); + } stop() {} } diff --git a/src/plugins/timelion/server/saved_objects/index.ts b/src/plugins/timelion/server/saved_objects/index.ts new file mode 100644 index 000000000000..102dc2581101 --- /dev/null +++ b/src/plugins/timelion/server/saved_objects/index.ts @@ -0,0 +1,20 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export { timelionSheetSavedObjectType } from './timelion_sheet'; diff --git a/src/plugins/timelion/server/saved_objects/timelion_sheet.ts b/src/plugins/timelion/server/saved_objects/timelion_sheet.ts new file mode 100644 index 000000000000..6a46217c3e61 --- /dev/null +++ b/src/plugins/timelion/server/saved_objects/timelion_sheet.ts @@ -0,0 +1,45 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { SavedObjectsType } from 'kibana/server'; + +export const timelionSheetSavedObjectType: SavedObjectsType = { + name: 'timelion-sheet', + hidden: false, + namespaceType: 'single', + mappings: { + properties: { + description: { type: 'text' }, + hits: { type: 'integer' }, + kibanaSavedObjectMeta: { + properties: { + searchSourceJSON: { type: 'text' }, + }, + }, + timelion_chart_height: { type: 'integer' }, + timelion_columns: { type: 'integer' }, + timelion_interval: { type: 'keyword' }, + timelion_other_interval: { type: 'keyword' }, + timelion_rows: { type: 'integer' }, + timelion_sheet: { type: 'text' }, + title: { type: 'text' }, + version: { type: 'integer' }, + }, + }, +}; diff --git a/test/functional/apps/visualize/_vega_chart.ts b/test/functional/apps/visualize/_vega_chart.ts index b59d9590bb62..f599afa3afc3 100644 --- a/test/functional/apps/visualize/_vega_chart.ts +++ b/test/functional/apps/visualize/_vega_chart.ts @@ -50,7 +50,8 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { const retry = getService('retry'); const browser = getService('browser'); - describe('vega chart in visualize app', () => { + // FLAKY: https://github.com/elastic/kibana/issues/75699 + describe.skip('vega chart in visualize app', () => { before(async () => { log.debug('navigateToApp visualize'); await PageObjects.visualize.navigateToNewVisualization(); diff --git a/test/functional/page_objects/login_page.ts b/test/functional/page_objects/login_page.ts index 350ab8be1a27..6cab2d39f3a9 100644 --- a/test/functional/page_objects/login_page.ts +++ b/test/functional/page_objects/login_page.ts @@ -48,10 +48,8 @@ export function LoginPageProvider({ getService }: FtrProviderContext) { class LoginPage { async login(user: string, pwd: string) { - if ( - process.env.VM === 'ubuntu18_deb_oidc' || - process.env.VM === 'ubuntu16_deb_desktop_saml' - ) { + const loginType = process.env.VM || ''; + if (loginType.includes('oidc') || loginType.includes('saml')) { await samlLogin(user, pwd); return; } diff --git a/vars/prChanges.groovy b/vars/prChanges.groovy index 5bdd62946caf..d082672c065a 100644 --- a/vars/prChanges.groovy +++ b/vars/prChanges.groovy @@ -22,7 +22,7 @@ def getSkippablePaths() { def getNotSkippablePaths() { return [ // this file is auto-generated and changes to it need to be validated with CI - /^docs\/developer\/architecture\/code-exploration.asciidoc$/, + /^docs\/developer\/plugin-list.asciidoc$/, // don't skip CI on prs with changes to plugin readme files (?i) is for case-insensitive matching /(?i)\/plugins\/[^\/]+\/readme\.(md|asciidoc)$/, ] diff --git a/x-pack/plugins/dashboard_enhanced/README.asciidoc b/x-pack/plugins/dashboard_enhanced/README.asciidoc new file mode 100644 index 000000000000..2abeeb6a74e0 --- /dev/null +++ b/x-pack/plugins/dashboard_enhanced/README.asciidoc @@ -0,0 +1,5 @@ + +[[dashboard-enhanced-plugin]] +== Dashboard app enhancements plugin + +Adds drilldown capabilities to dashboard. Owned by the Kibana App team. diff --git a/x-pack/plugins/dashboard_enhanced/README.md b/x-pack/plugins/dashboard_enhanced/README.md deleted file mode 100644 index 0aeb156a99f1..000000000000 --- a/x-pack/plugins/dashboard_enhanced/README.md +++ /dev/null @@ -1 +0,0 @@ -Contains the enhancements to the OSS dashboard app. \ No newline at end of file diff --git a/x-pack/plugins/data_enhanced/public/search/search_interceptor.ts b/x-pack/plugins/data_enhanced/public/search/search_interceptor.ts index ae6dddf33536..47099e32fcc7 100644 --- a/x-pack/plugins/data_enhanced/public/search/search_interceptor.ts +++ b/x-pack/plugins/data_enhanced/public/search/search_interceptor.ts @@ -96,7 +96,7 @@ export class EnhancedSearchInterceptor extends SearchInterceptor { return timer(pollInterval).pipe( // Send future requests using just the ID from the response mergeMap(() => { - return this.runSearch({ id }, combinedSignal, options?.strategy); + return this.runSearch({ ...request, id }, combinedSignal, options?.strategy); }) ); }), diff --git a/x-pack/plugins/data_enhanced/server/search/es_search_strategy.test.ts b/x-pack/plugins/data_enhanced/server/search/es_search_strategy.test.ts index 5cff7fea601d..054baa6ac81d 100644 --- a/x-pack/plugins/data_enhanced/server/search/es_search_strategy.test.ts +++ b/x-pack/plugins/data_enhanced/server/search/es_search_strategy.test.ts @@ -36,7 +36,7 @@ const mockRollupResponse = { describe('ES search strategy', () => { const mockApiCaller = jest.fn(); const mockLogger: any = { - info: () => {}, + debug: () => {}, }; const mockContext = { core: { diff --git a/x-pack/plugins/data_enhanced/server/search/es_search_strategy.ts b/x-pack/plugins/data_enhanced/server/search/es_search_strategy.ts index f6425a63cef6..46609af52d07 100644 --- a/x-pack/plugins/data_enhanced/server/search/es_search_strategy.ts +++ b/x-pack/plugins/data_enhanced/server/search/es_search_strategy.ts @@ -39,7 +39,7 @@ export const enhancedEsSearchStrategyProvider = ( request: IEnhancedEsSearchRequest, options?: ISearchOptions ) => { - logger.info(`search ${JSON.stringify(request.params) || request.id}`); + logger.debug(`search ${JSON.stringify(request.params) || request.id}`); const config = await config$.pipe(first()).toPromise(); const client = context.core.elasticsearch.client.asCurrentUser; const defaultParams = getDefaultSearchParams(config); @@ -70,7 +70,7 @@ export const enhancedEsSearchStrategyProvider = ( }; const cancel = async (context: RequestHandlerContext, id: string) => { - logger.info(`cancel ${id}`); + logger.debug(`cancel ${id}`); await context.core.elasticsearch.client.asCurrentUser.transport.request({ method: 'DELETE', path: encodeURI(`/_async_search/${id}`), diff --git a/x-pack/plugins/discover_enhanced/common/config.ts b/x-pack/plugins/discover_enhanced/common/config.ts new file mode 100644 index 000000000000..1ee329ea8d11 --- /dev/null +++ b/x-pack/plugins/discover_enhanced/common/config.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export interface Config { + actions: { exploreDataInChart: { enabled: boolean } }; +} diff --git a/x-pack/plugins/discover_enhanced/common/index.ts b/x-pack/plugins/discover_enhanced/common/index.ts new file mode 100644 index 000000000000..876fca26dc6f --- /dev/null +++ b/x-pack/plugins/discover_enhanced/common/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export * from './config'; diff --git a/x-pack/plugins/discover_enhanced/kibana.json b/x-pack/plugins/discover_enhanced/kibana.json index 531a84cd4c0e..da95a0f21a02 100644 --- a/x-pack/plugins/discover_enhanced/kibana.json +++ b/x-pack/plugins/discover_enhanced/kibana.json @@ -5,7 +5,7 @@ "server": true, "ui": true, "requiredPlugins": ["uiActions", "embeddable", "discover"], - "optionalPlugins": ["share", "kibanaLegacy"], + "optionalPlugins": ["share", "kibanaLegacy", "usageCollection"], "configPath": ["xpack", "discoverEnhanced"], "requiredBundles": ["kibanaUtils", "data"] } diff --git a/x-pack/plugins/discover_enhanced/public/plugin.ts b/x-pack/plugins/discover_enhanced/public/plugin.ts index 9e66925132a7..f1273ab00bdd 100644 --- a/x-pack/plugins/discover_enhanced/public/plugin.ts +++ b/x-pack/plugins/discover_enhanced/public/plugin.ts @@ -28,6 +28,7 @@ import { ACTION_EXPLORE_DATA_CHART, ExploreDataChartActionContext, } from './actions'; +import { Config } from '../common'; declare module '../../../../src/plugins/ui_actions/public' { export interface ActionContextMapping { @@ -55,10 +56,10 @@ export interface DiscoverEnhancedStartDependencies { export class DiscoverEnhancedPlugin implements Plugin { - public readonly config: { actions: { exploreDataInChart: { enabled: boolean } } }; + public readonly config: Config; constructor(protected readonly context: PluginInitializerContext) { - this.config = context.config.get(); + this.config = context.config.get(); } setup( diff --git a/x-pack/plugins/discover_enhanced/server/index.ts b/x-pack/plugins/discover_enhanced/server/index.ts index e361b9fb075e..461a2616efdb 100644 --- a/x-pack/plugins/discover_enhanced/server/index.ts +++ b/x-pack/plugins/discover_enhanced/server/index.ts @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ +import { PluginInitializerContext } from '../../../../src/core/server'; +import { DiscoverEnhancedPlugin } from './plugin'; + export { config } from './config'; -export const plugin = () => ({ - setup() {}, - start() {}, -}); +export const plugin = (context: PluginInitializerContext) => new DiscoverEnhancedPlugin(context); diff --git a/x-pack/plugins/discover_enhanced/server/plugin.ts b/x-pack/plugins/discover_enhanced/server/plugin.ts new file mode 100644 index 000000000000..9d80a6dc7dcd --- /dev/null +++ b/x-pack/plugins/discover_enhanced/server/plugin.ts @@ -0,0 +1,58 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Observable } from 'rxjs'; +import { take } from 'rxjs/operators'; +import { + CoreSetup, + CoreStart, + Plugin, + PluginInitializerContext, +} from '../../../../src/core/server'; +import { UsageCollectionSetup } from '../../../../src/plugins/usage_collection/server'; +import { Config } from '../common'; + +interface SetupDependencies { + usageCollection?: UsageCollectionSetup; +} + +interface StartDependencies { + usageCollection?: unknown; +} + +export class DiscoverEnhancedPlugin + implements Plugin { + private config$: Observable; + + constructor(protected readonly context: PluginInitializerContext) { + this.config$ = context.config.create(); + } + + public setup(core: CoreSetup, { usageCollection }: SetupDependencies) { + if (!!usageCollection) { + const collector = usageCollection.makeUsageCollector<{ + exploreDataInChartActionEnabled: boolean; + }>({ + type: 'discoverEnhanced', + schema: { + exploreDataInChartActionEnabled: { + type: 'boolean', + }, + }, + isReady: () => true, + fetch: async () => { + const config = await this.config$.pipe(take(1)).toPromise(); + return { + exploreDataInChartActionEnabled: config.actions.exploreDataInChart.enabled, + }; + }, + }); + usageCollection.registerCollector(collector); + } + } + + public start(core: CoreStart) {} +} diff --git a/x-pack/plugins/grokdebugger/README.md b/x-pack/plugins/grokdebugger/README.md deleted file mode 100644 index e69de29bb2d1..000000000000 diff --git a/x-pack/plugins/index_management/README.md b/x-pack/plugins/index_management/README.md new file mode 100644 index 000000000000..07c5b9317b5c --- /dev/null +++ b/x-pack/plugins/index_management/README.md @@ -0,0 +1,22 @@ +# Index Management UI + +## Data streams tab + +### Quick steps for testing + +Create a data stream using Console and you'll be able to view it in the UI: + +``` +# Configure template for creating a data stream +PUT _index_template/ds +{ + "index_patterns": ["ds"], + "data_stream": {} +} + +# Add a document to the data stream +POST ds/_doc +{ + "@timestamp": "2020-01-27" +} +``` \ No newline at end of file diff --git a/x-pack/plugins/index_management/__jest__/client_integration/helpers/test_subjects.ts b/x-pack/plugins/index_management/__jest__/client_integration/helpers/test_subjects.ts index ecedf819e618..8a610a04f8bb 100644 --- a/x-pack/plugins/index_management/__jest__/client_integration/helpers/test_subjects.ts +++ b/x-pack/plugins/index_management/__jest__/client_integration/helpers/test_subjects.ts @@ -20,6 +20,7 @@ export type TestSubjects = | 'documentationLink' | 'emptyPrompt' | 'filterList.filterItem' + | 'includeStatsSwitch' | 'indexTable' | 'indexTableIncludeHiddenIndicesToggle' | 'indexTableIndexNameLink' @@ -28,16 +29,17 @@ export type TestSubjects = | 'legacyTemplateTable' | 'manageTemplateButton' | 'mappingsTabContent' - | 'previewTabContent' | 'noAliasesCallout' | 'noMappingsCallout' | 'noSettingsCallout' + | 'previewTabContent' | 'reloadButton' | 'reloadIndicesButton' | 'row' | 'sectionError' | 'sectionLoading' | 'settingsTabContent' + | 'simulateTemplatePreview' | 'summaryTab' | 'summaryTitle' | 'systemTemplatesSwitch' @@ -49,5 +51,4 @@ export type TestSubjects = | 'templateList' | 'templatesTab' | 'templateTable' - | 'viewButton' - | 'simulateTemplatePreview'; + | 'viewButton'; diff --git a/x-pack/plugins/index_management/__jest__/client_integration/home/data_streams_tab.helpers.ts b/x-pack/plugins/index_management/__jest__/client_integration/home/data_streams_tab.helpers.ts index db7541c93f9a..2fcf2a822cb2 100644 --- a/x-pack/plugins/index_management/__jest__/client_integration/home/data_streams_tab.helpers.ts +++ b/x-pack/plugins/index_management/__jest__/client_integration/home/data_streams_tab.helpers.ts @@ -22,6 +22,7 @@ export interface DataStreamsTabTestBed extends TestBed { actions: { goToDataStreamsList: () => void; clickEmptyPromptIndexTemplateLink: () => void; + clickIncludeStatsSwitch: () => void; clickReloadButton: () => void; clickNameAt: (index: number) => void; clickIndicesAt: (index: number) => void; @@ -74,6 +75,11 @@ export const setup = async (overridingDependencies: any = {}): Promise { + const { find } = testBed; + find('includeStatsSwitch').simulate('click'); + }; + const clickReloadButton = () => { const { find } = testBed; find('reloadButton').simulate('click'); @@ -149,6 +155,7 @@ export const setup = async (overridingDependencies: any = {}): Promise ({ }, ], generation: 1, + health: 'green', + indexTemplateName: 'indexTemplate', + storageSize: '1b', + maxTimeStamp: 420, }); diff --git a/x-pack/plugins/index_management/__jest__/client_integration/home/data_streams_tab.test.ts b/x-pack/plugins/index_management/__jest__/client_integration/home/data_streams_tab.test.ts index 89a95135bb07..ade4a62ceb19 100644 --- a/x-pack/plugins/index_management/__jest__/client_integration/home/data_streams_tab.test.ts +++ b/x-pack/plugins/index_management/__jest__/client_integration/home/data_streams_tab.test.ts @@ -78,7 +78,13 @@ describe('Data Streams tab', () => { describe('when there are data streams', () => { beforeEach(async () => { - httpRequestsMockHelpers.setLoadIndicesResponse([ + const { + setLoadIndicesResponse, + setLoadDataStreamsResponse, + setLoadDataStreamResponse, + } = httpRequestsMockHelpers; + + setLoadIndicesResponse([ { health: '', status: '', @@ -105,20 +111,16 @@ describe('Data Streams tab', () => { ]); const dataStreamForDetailPanel = createDataStreamPayload('dataStream1'); - - httpRequestsMockHelpers.setLoadDataStreamsResponse([ + setLoadDataStreamsResponse([ dataStreamForDetailPanel, createDataStreamPayload('dataStream2'), ]); - - httpRequestsMockHelpers.setLoadDataStreamResponse(dataStreamForDetailPanel); + setLoadDataStreamResponse(dataStreamForDetailPanel); testBed = await setup(); - await act(async () => { testBed.actions.goToDataStreamsList(); }); - testBed.component.update(); }); @@ -127,8 +129,8 @@ describe('Data Streams tab', () => { const { tableCellsValues } = table.getMetaData('dataStreamTable'); expect(tableCellsValues).toEqual([ - ['', 'dataStream1', '1', 'Delete'], - ['', 'dataStream2', '1', 'Delete'], + ['', 'dataStream1', 'green', '1', 'Delete'], + ['', 'dataStream2', 'green', '1', 'Delete'], ]); }); @@ -146,6 +148,30 @@ describe('Data Streams tab', () => { expect(server.requests[server.requests.length - 1].url).toBe(`${API_BASE_PATH}/data_streams`); }); + test('has a switch that will reload the data streams with additional stats when clicked', async () => { + const { exists, actions, table, component } = testBed; + const totalRequests = server.requests.length; + + expect(exists('includeStatsSwitch')).toBe(true); + + // Changing the switch will automatically reload the data streams. + await act(async () => { + actions.clickIncludeStatsSwitch(); + }); + component.update(); + + // A request is sent, but sinon isn't capturing the query parameters for some reason. + expect(server.requests.length).toBe(totalRequests + 1); + expect(server.requests[server.requests.length - 1].url).toBe(`${API_BASE_PATH}/data_streams`); + + // The table renders with the stats columns though. + const { tableCellsValues } = table.getMetaData('dataStreamTable'); + expect(tableCellsValues).toEqual([ + ['', 'dataStream1', 'green', 'December 31st, 1969 7:00:00 PM', '1b', '1', 'Delete'], + ['', 'dataStream2', 'green', 'December 31st, 1969 7:00:00 PM', '1b', '1', 'Delete'], + ]); + }); + test('clicking the indices count navigates to the backing indices', async () => { const { table, actions } = testBed; await actions.clickIndicesAt(0); diff --git a/x-pack/plugins/index_management/common/lib/data_stream_serialization.ts b/x-pack/plugins/index_management/common/lib/data_stream_serialization.ts index 7832662aea49..69004eaa020e 100644 --- a/x-pack/plugins/index_management/common/lib/data_stream_serialization.ts +++ b/x-pack/plugins/index_management/common/lib/data_stream_serialization.ts @@ -4,10 +4,20 @@ * you may not use this file except in compliance with the Elastic License. */ -import { DataStream, DataStreamFromEs } from '../types'; +import { DataStream, DataStreamFromEs, Health } from '../types'; export function deserializeDataStream(dataStreamFromEs: DataStreamFromEs): DataStream { - const { name, timestamp_field: timeStampField, indices, generation } = dataStreamFromEs; + const { + name, + timestamp_field: timeStampField, + indices, + generation, + status, + template, + ilm_policy: ilmPolicyName, + store_size: storageSize, + maximum_timestamp: maxTimeStamp, + } = dataStreamFromEs; return { name, @@ -20,6 +30,11 @@ export function deserializeDataStream(dataStreamFromEs: DataStreamFromEs): DataS }) ), generation, + health: status.toLowerCase() as Health, // ES typically returns status in all-caps + indexTemplateName: template, + ilmPolicyName, + storageSize, + maxTimeStamp, }; } diff --git a/x-pack/plugins/index_management/common/types/data_streams.ts b/x-pack/plugins/index_management/common/types/data_streams.ts index d1936c4426b4..7c348f9a8085 100644 --- a/x-pack/plugins/index_management/common/types/data_streams.ts +++ b/x-pack/plugins/index_management/common/types/data_streams.ts @@ -10,11 +10,18 @@ interface TimestampFieldFromEs { type TimestampField = TimestampFieldFromEs; +export type HealthFromEs = 'GREEN' | 'YELLOW' | 'RED'; + export interface DataStreamFromEs { name: string; timestamp_field: TimestampFieldFromEs; indices: DataStreamIndexFromEs[]; generation: number; + status: HealthFromEs; + template: string; + ilm_policy?: string; + store_size?: string; + maximum_timestamp?: number; } export interface DataStreamIndexFromEs { @@ -22,11 +29,18 @@ export interface DataStreamIndexFromEs { index_uuid: string; } +export type Health = 'green' | 'yellow' | 'red'; + export interface DataStream { name: string; timeStampField: TimestampField; indices: DataStreamIndex[]; generation: number; + health: Health; + indexTemplateName: string; + ilmPolicyName?: string; + storageSize?: string; + maxTimeStamp?: number; } export interface DataStreamIndex { diff --git a/x-pack/plugins/index_management/common/types/index.ts b/x-pack/plugins/index_management/common/types/index.ts index c4ba60573d43..0a6f18be2e03 100644 --- a/x-pack/plugins/index_management/common/types/index.ts +++ b/x-pack/plugins/index_management/common/types/index.ts @@ -12,6 +12,6 @@ export * from './mappings'; export * from './templates'; -export { DataStreamFromEs, DataStream, DataStreamIndex } from './data_streams'; +export { DataStreamFromEs, Health, DataStream, DataStreamIndex } from './data_streams'; export * from './component_templates'; diff --git a/x-pack/plugins/index_management/public/application/services/health_to_color.ts b/x-pack/plugins/index_management/public/application/components/data_health.tsx similarity index 53% rename from x-pack/plugins/index_management/public/application/services/health_to_color.ts rename to x-pack/plugins/index_management/public/application/components/data_health.tsx index ab3d042529aa..21df8a16947d 100644 --- a/x-pack/plugins/index_management/public/application/services/health_to_color.ts +++ b/x-pack/plugins/index_management/public/application/components/data_health.tsx @@ -4,7 +4,15 @@ * you may not use this file except in compliance with the Elastic License. */ -export const healthToColor = (health: 'green' | 'yellow' | 'red') => { +import React from 'react'; +import { EuiHealth } from '@elastic/eui'; +import { Health } from '../../../common/types'; + +interface Props { + health: Health; +} + +const healthToColor = (health: Health) => { switch (health) { case 'green': return 'success'; @@ -14,3 +22,7 @@ export const healthToColor = (health: 'green' | 'yellow' | 'red') => { return 'danger'; } }; + +export const DataHealth: React.FunctionComponent = ({ health }) => ( + {health} +); diff --git a/x-pack/plugins/index_management/public/application/components/index.ts b/x-pack/plugins/index_management/public/application/components/index.ts index b6a325c0d56a..4b0bf4680c49 100644 --- a/x-pack/plugins/index_management/public/application/components/index.ts +++ b/x-pack/plugins/index_management/public/application/components/index.ts @@ -10,6 +10,7 @@ export { NoMatch } from './no_match'; export { PageErrorForbidden } from './page_error'; export { TemplateDeleteModal } from './template_delete_modal'; export { TemplateForm } from './template_form'; +export { DataHealth } from './data_health'; export * from './mappings_editor'; export * from './component_templates'; export * from './index_templates'; diff --git a/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_detail_panel/data_stream_detail_panel.tsx b/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_detail_panel/data_stream_detail_panel.tsx index a0381557db21..0af22b497a4c 100644 --- a/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_detail_panel/data_stream_detail_panel.tsx +++ b/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_detail_panel/data_stream_detail_panel.tsx @@ -5,7 +5,7 @@ */ import React, { useState } from 'react'; -import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; import { EuiButton, EuiButtonEmpty, @@ -24,9 +24,51 @@ import { } from '@elastic/eui'; import { reactRouterNavigate } from '../../../../../shared_imports'; -import { SectionLoading, SectionError, Error } from '../../../../components'; +import { SectionLoading, SectionError, Error, DataHealth } from '../../../../components'; import { useLoadDataStream } from '../../../../services/api'; import { DeleteDataStreamConfirmationModal } from '../delete_data_stream_confirmation_modal'; +import { humanizeTimeStamp } from '../humanize_time_stamp'; + +interface DetailsListProps { + details: Array<{ + name: string; + toolTip: string; + content: any; + }>; +} + +const DetailsList: React.FunctionComponent = ({ details }) => { + const groups: any[] = []; + let items: any[]; + + details.forEach((detail, index) => { + const { name, toolTip, content } = detail; + + if (index % 2 === 0) { + items = []; + + groups.push({items}); + } + + items.push( + + + + {name} + + + {toolTip && } + + + + + {content} + + ); + }); + + return {groups}; +}; interface Props { dataStreamName: string; @@ -48,116 +90,123 @@ export const DataStreamDetailPanel: React.FunctionComponent = ({ if (isLoading) { content = ( - + {i18n.translate('xpack.idxMgmt.dataStreamDetailPanel.loadingDataStreamDescription', { + defaultMessage: 'Loading data stream', + })} ); } else if (error) { content = ( - } + title={i18n.translate('xpack.idxMgmt.dataStreamDetailPanel.loadingDataStreamErrorMessage', { + defaultMessage: 'Error loading data stream', + })} error={error as Error} data-test-subj="sectionError" /> ); } else if (dataStream) { - const { indices, timeStampField, generation } = dataStream; + const { + health, + indices, + timeStampField, + generation, + indexTemplateName, + ilmPolicyName, + storageSize, + maxTimeStamp, + } = dataStream; + const details = [ + { + name: i18n.translate('xpack.idxMgmt.dataStreamDetailPanel.healthTitle', { + defaultMessage: 'Health', + }), + toolTip: i18n.translate('xpack.idxMgmt.dataStreamDetailPanel.healthToolTip', { + defaultMessage: `The health of the data stream's current backing indices`, + }), + content: , + }, + { + name: i18n.translate('xpack.idxMgmt.dataStreamDetailPanel.maxTimeStampTitle', { + defaultMessage: 'Last updated', + }), + toolTip: i18n.translate('xpack.idxMgmt.dataStreamDetailPanel.maxTimeStampToolTip', { + defaultMessage: 'The most recent document to be added to the data stream', + }), + content: maxTimeStamp ? ( + humanizeTimeStamp(maxTimeStamp) + ) : ( + + {i18n.translate('xpack.idxMgmt.dataStreamDetailPanel.maxTimeStampNoneMessage', { + defaultMessage: `Never`, + })} + + ), + }, + { + name: i18n.translate('xpack.idxMgmt.dataStreamDetailPanel.storageSizeTitle', { + defaultMessage: 'Storage size', + }), + toolTip: i18n.translate('xpack.idxMgmt.dataStreamDetailPanel.storageSizeToolTip', { + defaultMessage: `Total size of all shards in the data stream’s backing indices`, + }), + content: storageSize, + }, + { + name: i18n.translate('xpack.idxMgmt.dataStreamDetailPanel.indicesTitle', { + defaultMessage: 'Indices', + }), + toolTip: i18n.translate('xpack.idxMgmt.dataStreamDetailPanel.indicesToolTip', { + defaultMessage: `The data stream's current backing indices`, + }), + content: {indices.length}, + }, + { + name: i18n.translate('xpack.idxMgmt.dataStreamDetailPanel.timestampFieldTitle', { + defaultMessage: 'Timestamp field', + }), + toolTip: i18n.translate('xpack.idxMgmt.dataStreamDetailPanel.timestampFieldToolTip', { + defaultMessage: 'Timestamp field shared by all documents in the data stream', + }), + content: timeStampField.name, + }, + { + name: i18n.translate('xpack.idxMgmt.dataStreamDetailPanel.generationTitle', { + defaultMessage: 'Generation', + }), + toolTip: i18n.translate('xpack.idxMgmt.dataStreamDetailPanel.generationToolTip', { + defaultMessage: 'Cumulative count of backing indices created for the data stream', + }), + content: generation, + }, + { + name: i18n.translate('xpack.idxMgmt.dataStreamDetailPanel.indexTemplateTitle', { + defaultMessage: 'Index template', + }), + toolTip: i18n.translate('xpack.idxMgmt.dataStreamDetailPanel.indexTemplateToolTip', { + defaultMessage: + 'The index template that configured the data stream and configures its backing indices', + }), + content: indexTemplateName, + }, + { + name: i18n.translate('xpack.idxMgmt.dataStreamDetailPanel.ilmPolicyTitle', { + defaultMessage: 'Index lifecycle policy', + }), + toolTip: i18n.translate('xpack.idxMgmt.dataStreamDetailPanel.ilmPolicyToolTip', { + defaultMessage: `The index lifecycle policy that manages the data stream's data`, + }), + content: ilmPolicyName ?? ( + + {i18n.translate('xpack.idxMgmt.dataStreamDetailPanel.ilmPolicyContentNoneMessage', { + defaultMessage: `None`, + })} + + ), + }, + ]; - content = ( - - - - - - - - - - - - } - position="top" - /> - - - - - - {indices.length} - - - - - - - - - - - } - position="top" - /> - - - - - {timeStampField.name} - - - - - - - - - - - - - - } - position="top" - /> - - - - - {generation} - - - - ); + content = ; } return ( @@ -201,10 +250,9 @@ export const DataStreamDetailPanel: React.FunctionComponent = ({ onClick={() => onClose()} data-test-subj="closeDetailsButton" > - + {i18n.translate('xpack.idxMgmt.dataStreamDetailPanel.closeButtonLabel', { + defaultMessage: 'Close', + })} @@ -216,10 +264,9 @@ export const DataStreamDetailPanel: React.FunctionComponent = ({ onClick={() => setIsDeleting(true)} data-test-subj="deleteDataStreamButton" > - + {i18n.translate('xpack.idxMgmt.dataStreamDetailPanel.deleteButtonLabel', { + defaultMessage: 'Delete data stream', + })} ) : null} diff --git a/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_list.tsx b/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_list.tsx index 239b119051c0..d37576f18e84 100644 --- a/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_list.tsx +++ b/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_list.tsx @@ -4,11 +4,20 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; +import React, { useState } from 'react'; import { RouteComponentProps } from 'react-router-dom'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; -import { EuiText, EuiSpacer, EuiEmptyPrompt, EuiLink } from '@elastic/eui'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiSwitch, + EuiText, + EuiIconTip, + EuiSpacer, + EuiEmptyPrompt, + EuiLink, +} from '@elastic/eui'; import { ScopedHistory } from 'kibana/public'; import { reactRouterNavigate, extractQueryParams } from '../../../../shared_imports'; @@ -39,7 +48,10 @@ export const DataStreamList: React.FunctionComponent 0) { content = ( <> - {/* TODO: Add a switch for toggling on data streams created by Ingest Manager */} - - - {i18n.translate('xpack.idxMgmt.dataStreamListDescription.learnMoreLinkText', { - defaultMessage: 'Learn more.', - })} - - ), - }} - /> - + + + {/* TODO: Add a switch for toggling on data streams created by Ingest Manager */} + + + {i18n.translate('xpack.idxMgmt.dataStreamListDescription.learnMoreLinkText', { + defaultMessage: 'Learn more.', + })} + + ), + }} + /> + + + + + + + setIsIncludeStatsChecked(e.target.checked)} + data-test-subj="includeStatsSwitch" + /> + + + + + + + + @@ -166,6 +212,7 @@ export const DataStreamList: React.FunctionComponent ); diff --git a/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_table/data_stream_table.tsx b/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_table/data_stream_table.tsx index d1e093f1ffc8..a586e9495cf7 100644 --- a/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_table/data_stream_table.tsx +++ b/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_table/data_stream_table.tsx @@ -13,13 +13,16 @@ import { ScopedHistory } from 'kibana/public'; import { DataStream } from '../../../../../../common/types'; import { reactRouterNavigate } from '../../../../../shared_imports'; import { encodePathForReactRouter } from '../../../../services/routing'; +import { DataHealth } from '../../../../components'; import { Section } from '../../../home'; import { DeleteDataStreamConfirmationModal } from '../delete_data_stream_confirmation_modal'; +import { humanizeTimeStamp } from '../humanize_time_stamp'; interface Props { dataStreams?: DataStream[]; reload: () => {}; history: ScopedHistory; + includeStats: boolean; filters?: string; } @@ -28,76 +31,118 @@ export const DataStreamTable: React.FunctionComponent = ({ reload, history, filters, + includeStats, }) => { const [selection, setSelection] = useState([]); const [dataStreamsToDelete, setDataStreamsToDelete] = useState([]); - const columns: Array> = [ - { - field: 'name', - name: i18n.translate('xpack.idxMgmt.dataStreamList.table.nameColumnTitle', { - defaultMessage: 'Name', - }), - truncateText: true, - sortable: true, - render: (name: DataStream['name'], item: DataStream) => { - return ( - - {name} - - ); - }, - }, - { - field: 'indices', - name: i18n.translate('xpack.idxMgmt.dataStreamList.table.indicesColumnTitle', { - defaultMessage: 'Indices', - }), - truncateText: true, - sortable: true, - render: (indices: DataStream['indices'], dataStream) => ( + const columns: Array> = []; + + columns.push({ + field: 'name', + name: i18n.translate('xpack.idxMgmt.dataStreamList.table.nameColumnTitle', { + defaultMessage: 'Name', + }), + truncateText: true, + sortable: true, + render: (name: DataStream['name'], item: DataStream) => { + return ( - {indices.length} + {name} - ), + ); }, - { - name: i18n.translate('xpack.idxMgmt.dataStreamList.table.actionColumnTitle', { - defaultMessage: 'Actions', + }); + + columns.push({ + field: 'health', + name: i18n.translate('xpack.idxMgmt.dataStreamList.table.healthColumnTitle', { + defaultMessage: 'Health', + }), + truncateText: true, + sortable: true, + render: (health: DataStream['health']) => { + return ; + }, + }); + + if (includeStats) { + columns.push({ + field: 'maxTimeStamp', + name: i18n.translate('xpack.idxMgmt.dataStreamList.table.maxTimeStampColumnTitle', { + defaultMessage: 'Last updated', }), - actions: [ - { - name: i18n.translate('xpack.idxMgmt.dataStreamList.table.actionDeleteText', { - defaultMessage: 'Delete', - }), - description: i18n.translate('xpack.idxMgmt.dataStreamList.table.actionDeleteDecription', { - defaultMessage: 'Delete this data stream', - }), - icon: 'trash', - color: 'danger', - type: 'icon', - onClick: ({ name }: DataStream) => { - setDataStreamsToDelete([name]); - }, - isPrimary: true, - 'data-test-subj': 'deleteDataStream', + width: '300px', + truncateText: true, + sortable: true, + render: (maxTimeStamp: DataStream['maxTimeStamp']) => + maxTimeStamp + ? humanizeTimeStamp(maxTimeStamp) + : i18n.translate('xpack.idxMgmt.dataStreamList.table.maxTimeStampColumnNoneMessage', { + defaultMessage: 'Never', + }), + }); + + columns.push({ + field: 'storageSize', + name: i18n.translate('xpack.idxMgmt.dataStreamList.table.storageSizeColumnTitle', { + defaultMessage: 'Storage size', + }), + truncateText: true, + sortable: true, + }); + } + + columns.push({ + field: 'indices', + name: i18n.translate('xpack.idxMgmt.dataStreamList.table.indicesColumnTitle', { + defaultMessage: 'Indices', + }), + truncateText: true, + sortable: true, + render: (indices: DataStream['indices'], dataStream) => ( + + {indices.length} + + ), + }); + + columns.push({ + name: i18n.translate('xpack.idxMgmt.dataStreamList.table.actionColumnTitle', { + defaultMessage: 'Actions', + }), + actions: [ + { + name: i18n.translate('xpack.idxMgmt.dataStreamList.table.actionDeleteText', { + defaultMessage: 'Delete', + }), + description: i18n.translate('xpack.idxMgmt.dataStreamList.table.actionDeleteDecription', { + defaultMessage: 'Delete this data stream', + }), + icon: 'trash', + color: 'danger', + type: 'icon', + onClick: ({ name }: DataStream) => { + setDataStreamsToDelete([name]); }, - ], - }, - ]; + isPrimary: true, + 'data-test-subj': 'deleteDataStream', + }, + ], + }); const pagination = { initialPageSize: 20, diff --git a/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/humanize_time_stamp.ts b/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/humanize_time_stamp.ts new file mode 100644 index 000000000000..b346f8289017 --- /dev/null +++ b/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/humanize_time_stamp.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import moment from 'moment'; + +export const humanizeTimeStamp = (timeStamp: number): string => + moment(timeStamp).format('MMMM Do, YYYY h:mm:ss A'); diff --git a/x-pack/plugins/index_management/public/application/sections/home/index_list/detail_panel/summary/summary.js b/x-pack/plugins/index_management/public/application/sections/home/index_list/detail_panel/summary/summary.js index 2fda71035fb5..6b3c83daa28a 100644 --- a/x-pack/plugins/index_management/public/application/sections/home/index_list/detail_panel/summary/summary.js +++ b/x-pack/plugins/index_management/public/application/sections/home/index_list/detail_panel/summary/summary.js @@ -10,7 +10,6 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { EuiFlexGroup, EuiFlexItem, - EuiHealth, EuiDescriptionList, EuiHorizontalRule, EuiDescriptionListTitle, @@ -18,7 +17,7 @@ import { EuiSpacer, EuiTitle, } from '@elastic/eui'; -import { healthToColor } from '../../../../../services'; +import { DataHealth } from '../../../../../components'; import { AppContextConsumer } from '../../../../../app_context'; const getHeaders = () => { @@ -78,7 +77,7 @@ export class Summary extends React.PureComponent { const value = index[fieldName]; let content = value; if (fieldName === 'health') { - content = {value}; + content = ; } if (Array.isArray(content)) { content = content.join(', '); diff --git a/x-pack/plugins/index_management/public/application/sections/home/index_list/index_table/index_table.js b/x-pack/plugins/index_management/public/application/sections/home/index_list/index_table/index_table.js index 85a8571c1e72..b4e003b66707 100644 --- a/x-pack/plugins/index_management/public/application/sections/home/index_list/index_table/index_table.js +++ b/x-pack/plugins/index_management/public/application/sections/home/index_list/index_table/index_table.js @@ -13,7 +13,6 @@ import qs from 'query-string'; import { EuiButton, EuiCallOut, - EuiHealth, EuiLink, EuiCheckbox, EuiFlexGroup, @@ -38,12 +37,11 @@ import { import { UIM_SHOW_DETAILS_CLICK } from '../../../../../../common/constants'; import { reactRouterNavigate } from '../../../../../shared_imports'; import { REFRESH_RATE_INDEX_LIST } from '../../../../constants'; -import { healthToColor } from '../../../../services'; import { encodePathForReactRouter } from '../../../../services/routing'; import { documentationService } from '../../../../services/documentation'; import { AppContextConsumer } from '../../../../app_context'; import { renderBadges } from '../../../../lib/render_badges'; -import { NoMatch, PageErrorForbidden } from '../../../../components'; +import { NoMatch, PageErrorForbidden, DataHealth } from '../../../../components'; import { IndexActionsContextMenu } from '../index_actions_context_menu'; const HEADERS = { @@ -260,7 +258,7 @@ export class IndexTable extends Component { const { openDetailPanel, filterChanged, history } = this.props; if (fieldName === 'health') { - return {value}; + return ; } else if (fieldName === 'name') { return ( diff --git a/x-pack/plugins/index_management/public/application/services/api.ts b/x-pack/plugins/index_management/public/application/services/api.ts index 546a0115ee4a..35ded3ea73d9 100644 --- a/x-pack/plugins/index_management/public/application/services/api.ts +++ b/x-pack/plugins/index_management/public/application/services/api.ts @@ -47,10 +47,13 @@ export const setUiMetricService = (_uiMetricService: UiMetricService({ path: `${API_BASE_PATH}/data_streams`, method: 'get', + query: { + includeStats, + }, }); } diff --git a/x-pack/plugins/index_management/public/application/services/index.ts b/x-pack/plugins/index_management/public/application/services/index.ts index a78e0bac14ae..86c11b4a9c1e 100644 --- a/x-pack/plugins/index_management/public/application/services/index.ts +++ b/x-pack/plugins/index_management/public/application/services/index.ts @@ -24,7 +24,6 @@ export { useLoadIndexTemplates, simulateIndexTemplate, } from './api'; -export { healthToColor } from './health_to_color'; export { sortTable } from './sort_table'; export { UiMetricService } from './ui_metric'; diff --git a/x-pack/plugins/index_management/server/routes/api/data_streams/register_get_route.ts b/x-pack/plugins/index_management/server/routes/api/data_streams/register_get_route.ts index b91c7b465018..34edcb6fb747 100644 --- a/x-pack/plugins/index_management/server/routes/api/data_streams/register_get_route.ts +++ b/x-pack/plugins/index_management/server/routes/api/data_streams/register_get_route.ts @@ -10,19 +10,52 @@ import { deserializeDataStream, deserializeDataStreamList } from '../../../../co import { RouteDependencies } from '../../../types'; import { addBasePath } from '../index'; +const querySchema = schema.object({ + includeStats: schema.maybe(schema.oneOf([schema.literal('true'), schema.literal('false')])), +}); + export function registerGetAllRoute({ router, license, lib: { isEsError } }: RouteDependencies) { router.get( - { path: addBasePath('/data_streams'), validate: false }, + { path: addBasePath('/data_streams'), validate: { query: querySchema } }, license.guardApiRoute(async (ctx, req, res) => { const { callAsCurrentUser } = ctx.dataManagement!.client; + const includeStats = (req.query as TypeOf).includeStats === 'true'; + try { const { data_streams: dataStreams } = await callAsCurrentUser( 'dataManagement.getDataStreams' ); - const body = deserializeDataStreamList(dataStreams); - return res.ok({ body }); + if (includeStats) { + const { + data_streams: dataStreamsStats, + } = await ctx.core.elasticsearch.legacy.client.callAsCurrentUser('transport.request', { + path: '/_data_stream/*/_stats', + method: 'GET', + query: { + human: true, + }, + }); + + // Merge stats into data streams. + for (let i = 0; i < dataStreams.length; i++) { + const dataStream = dataStreams[i]; + + // eslint-disable-next-line @typescript-eslint/naming-convention + const { store_size, maximum_timestamp } = dataStreamsStats.find( + ({ data_stream: statsName }: { data_stream: string }) => statsName === dataStream.name + ); + + dataStreams[i] = { + ...dataStream, + store_size, + maximum_timestamp, + }; + } + } + + return res.ok({ body: deserializeDataStreamList(dataStreams) }); } catch (error) { if (isEsError(error)) { return res.customError({ @@ -52,12 +85,28 @@ export function registerGetOneRoute({ router, license, lib: { isEsError } }: Rou const { callAsCurrentUser } = ctx.dataManagement!.client; try { - const { data_streams: dataStream } = await callAsCurrentUser( - 'dataManagement.getDataStream', - { name } - ); + const [ + { data_streams: dataStream }, + { data_streams: dataStreamsStats }, + ] = await Promise.all([ + callAsCurrentUser('dataManagement.getDataStream', { name }), + ctx.core.elasticsearch.legacy.client.callAsCurrentUser('transport.request', { + path: `/_data_stream/${name}/_stats`, + method: 'GET', + query: { + human: true, + }, + }), + ]); if (dataStream[0]) { + // eslint-disable-next-line @typescript-eslint/naming-convention + const { store_size, maximum_timestamp } = dataStreamsStats[0]; + dataStream[0] = { + ...dataStream[0], + store_size, + maximum_timestamp, + }; const body = deserializeDataStream(dataStream[0]); return res.ok({ body }); } diff --git a/x-pack/plugins/infra/public/utils/logs_overview_fetchers.ts b/x-pack/plugins/infra/public/utils/logs_overview_fetchers.ts index 02d5a415eec2..9ca6db40a305 100644 --- a/x-pack/plugins/infra/public/utils/logs_overview_fetchers.ts +++ b/x-pack/plugins/infra/public/utils/logs_overview_fetchers.ts @@ -5,7 +5,7 @@ */ import { encode } from 'rison-node'; -import { SearchResponse } from 'src/plugins/data/public'; +import { SearchResponse } from 'elasticsearch'; import { FetchData, FetchDataParams, @@ -87,7 +87,7 @@ async function fetchLogsOverview( dataPlugin: InfraClientStartDeps['data'] ): Promise { return new Promise((resolve, reject) => { - let esResponse: SearchResponse = {}; + let esResponse: SearchResponse | undefined; dataPlugin.search .search({ @@ -104,8 +104,8 @@ async function fetchLogsOverview( (response) => (esResponse = response.rawResponse), (error) => reject(error), () => { - if (esResponse.aggregations) { - resolve(processLogsOverviewAggregations(esResponse.aggregations)); + if (esResponse?.aggregations) { + resolve(processLogsOverviewAggregations(esResponse!.aggregations)); } else { resolve({ stats: {}, series: {} }); } diff --git a/x-pack/plugins/ingest_manager/common/constants/epm.ts b/x-pack/plugins/ingest_manager/common/constants/epm.ts index 73cd8463bb6a..571580e81258 100644 --- a/x-pack/plugins/ingest_manager/common/constants/epm.ts +++ b/x-pack/plugins/ingest_manager/common/constants/epm.ts @@ -7,3 +7,4 @@ export const PACKAGES_SAVED_OBJECT_TYPE = 'epm-packages'; export const INDEX_PATTERN_SAVED_OBJECT_TYPE = 'index-pattern'; export const INDEX_PATTERN_PLACEHOLDER_SUFFIX = '-index_pattern_placeholder'; +export const MAX_TIME_COMPLETE_INSTALL = 60000; diff --git a/x-pack/plugins/ingest_manager/common/types/models/epm.ts b/x-pack/plugins/ingest_manager/common/types/models/epm.ts index 6ec5b73eaa43..140a76ac85e6 100644 --- a/x-pack/plugins/ingest_manager/common/types/models/epm.ts +++ b/x-pack/plugins/ingest_manager/common/types/models/epm.ts @@ -19,6 +19,8 @@ export enum InstallStatus { uninstalling = 'uninstalling', } +export type EpmPackageInstallStatus = 'installed' | 'installing'; + export type DetailViewPanelName = 'overview' | 'usages' | 'settings'; export type ServiceName = 'kibana' | 'elasticsearch'; export type AssetType = KibanaAssetType | ElasticsearchAssetType | AgentAssetType; @@ -234,6 +236,9 @@ export interface Installation extends SavedObjectAttributes { es_index_patterns: Record; name: string; version: string; + install_status: EpmPackageInstallStatus; + install_version: string; + install_started_at: string; } export type Installable = Installed | NotInstalled; diff --git a/x-pack/plugins/ingest_manager/server/constants/index.ts b/x-pack/plugins/ingest_manager/server/constants/index.ts index 54d2d876c75b..d677b79bb46f 100644 --- a/x-pack/plugins/ingest_manager/server/constants/index.ts +++ b/x-pack/plugins/ingest_manager/server/constants/index.ts @@ -14,6 +14,7 @@ export { AGENT_POLICY_ROLLOUT_RATE_LIMIT_INTERVAL_MS, AGENT_UPDATE_ACTIONS_INTERVAL_MS, INDEX_PATTERN_PLACEHOLDER_SUFFIX, + MAX_TIME_COMPLETE_INSTALL, // Routes LIMITED_CONCURRENCY_ROUTE_TAG, PLUGIN_ID, diff --git a/x-pack/plugins/ingest_manager/server/saved_objects/index.ts b/x-pack/plugins/ingest_manager/server/saved_objects/index.ts index 50957c48f70e..1bbe3b71bf91 100644 --- a/x-pack/plugins/ingest_manager/server/saved_objects/index.ts +++ b/x-pack/plugins/ingest_manager/server/saved_objects/index.ts @@ -285,6 +285,9 @@ const savedObjectTypes: { [key: string]: SavedObjectsType } = { type: { type: 'keyword' }, }, }, + install_started_at: { type: 'date' }, + install_version: { type: 'keyword' }, + install_status: { type: 'keyword' }, }, }, }, diff --git a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/install.ts b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/install.ts index 2a3120f06490..f4e8c3bfd99d 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/install.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/install.ts @@ -22,7 +22,6 @@ import { removeAssetsFromInstalledEsByType, saveInstalledEsRefs } from '../../pa export const installTemplates = async ( registryPackage: RegistryPackage, - isUpdate: boolean, callCluster: CallESAsCurrentUser, paths: string[], savedObjectsClient: SavedObjectsClientContract diff --git a/x-pack/plugins/ingest_manager/server/services/epm/kibana/assets/install.ts b/x-pack/plugins/ingest_manager/server/services/epm/kibana/assets/install.ts index 84892d202784..ff1a91b00d84 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/kibana/assets/install.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/kibana/assets/install.ts @@ -48,7 +48,6 @@ export async function installKibanaAssets(options: { savedObjectsClient: SavedObjectsClientContract; pkgName: string; kibanaAssets: ArchiveAsset[]; - isUpdate: boolean; }): Promise { const { savedObjectsClient, kibanaAssets } = options; diff --git a/x-pack/plugins/ingest_manager/server/services/epm/packages/get.ts b/x-pack/plugins/ingest_manager/server/services/epm/packages/get.ts index 1688900fc175..c4232247cc4b 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/packages/get.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/packages/get.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SavedObjectsClientContract } from 'src/core/server'; +import { SavedObjectsClientContract, SavedObjectsFindOptions } from 'src/core/server'; import { isPackageLimited } from '../../../../common'; import { PACKAGES_SAVED_OBJECT_TYPE } from '../../../constants'; import { Installation, InstallationStatus, PackageInfo, KibanaAssetType } from '../../../types'; @@ -72,8 +72,12 @@ export async function getLimitedPackages(options: { return installedPackagesInfo.filter(isPackageLimited).map((pkgInfo) => pkgInfo.name); } -export async function getPackageSavedObjects(savedObjectsClient: SavedObjectsClientContract) { +export async function getPackageSavedObjects( + savedObjectsClient: SavedObjectsClientContract, + options?: Omit +) { return savedObjectsClient.find({ + ...(options || {}), type: PACKAGES_SAVED_OBJECT_TYPE, }); } diff --git a/x-pack/plugins/ingest_manager/server/services/epm/packages/install.ts b/x-pack/plugins/ingest_manager/server/services/epm/packages/install.ts index 6bc461845f12..e49dbe8f0b5d 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/packages/install.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/packages/install.ts @@ -6,7 +6,7 @@ import { SavedObjectsClientContract } from 'src/core/server'; import semver from 'semver'; -import { PACKAGES_SAVED_OBJECT_TYPE } from '../../../constants'; +import { PACKAGES_SAVED_OBJECT_TYPE, MAX_TIME_COMPLETE_INSTALL } from '../../../constants'; import { AssetReference, Installation, @@ -33,6 +33,7 @@ import { import { updateCurrentWriteIndices } from '../elasticsearch/template/template'; import { deleteKibanaSavedObjectsAssets } from './remove'; import { PackageOutdatedError } from '../../../errors'; +import { getPackageSavedObjects } from './get'; export async function installLatestPackage(options: { savedObjectsClient: SavedObjectsClientContract; @@ -107,22 +108,24 @@ export async function installPackage({ // TODO: calls to getInstallationObject, Registry.fetchInfo, and Registry.fetchFindLatestPackge // and be replaced by getPackageInfo after adjusting for it to not group/use archive assets const latestPackage = await Registry.fetchFindLatestPackage(pkgName); - if (semver.lt(pkgVersion, latestPackage.version) && !force) - throw new PackageOutdatedError(`${pkgkey} is out-of-date and cannot be installed or updated`); + // get the currently installed package + const installedPkg = await getInstallationObject({ savedObjectsClient, pkgName }); + const reinstall = pkgVersion === installedPkg?.attributes.version; + const reupdate = pkgVersion === installedPkg?.attributes.install_version; + // let the user install if using the force flag or this is a reinstall or reupdate due to intallation interruption + if (semver.lt(pkgVersion, latestPackage.version) && !force && !reinstall && !reupdate) { + throw new PackageOutdatedError(`${pkgkey} is out-of-date and cannot be installed or updated`); + } const paths = await Registry.getArchiveInfo(pkgName, pkgVersion); const registryPackageInfo = await Registry.fetchInfo(pkgName, pkgVersion); - // get the currently installed package - const installedPkg = await getInstallationObject({ savedObjectsClient, pkgName }); - const isUpdate = installedPkg && installedPkg.attributes.version < pkgVersion ? true : false; - - const reinstall = pkgVersion === installedPkg?.attributes.version; const removable = !isRequiredPackage(pkgName); const { internal = false } = registryPackageInfo; const toSaveESIndexPatterns = generateESIndexPatterns(registryPackageInfo.datasets); - // add the package installation to the saved object + // add the package installation to the saved object. + // if some installation already exists, just update install info if (!installedPkg) { await createInstallation({ savedObjectsClient, @@ -134,6 +137,12 @@ export async function installPackage({ installed_es: [], toSaveESIndexPatterns, }); + } else { + await savedObjectsClient.update(PACKAGES_SAVED_OBJECT_TYPE, pkgName, { + install_version: pkgVersion, + install_status: 'installing', + install_started_at: new Date().toISOString(), + }); } const installIndexPatternPromise = installIndexPatterns(savedObjectsClient, pkgName, pkgVersion); const kibanaAssets = await getKibanaAssets(paths); @@ -152,7 +161,6 @@ export async function installPackage({ savedObjectsClient, pkgName, kibanaAssets, - isUpdate, }); // the rest of the installation must happen in sequential order @@ -172,7 +180,6 @@ export async function installPackage({ // install or update the templates referencing the newly installed pipelines const installedTemplates = await installTemplates( registryPackageInfo, - isUpdate, callCluster, paths, savedObjectsClient @@ -197,9 +204,14 @@ export async function installPackage({ })); await Promise.all([installKibanaAssetsPromise, installIndexPatternPromise]); // update to newly installed version when all assets are successfully installed - if (isUpdate) await updateVersion(savedObjectsClient, pkgName, pkgVersion); + if (installedPkg) await updateVersion(savedObjectsClient, pkgName, pkgVersion); + await savedObjectsClient.update(PACKAGES_SAVED_OBJECT_TYPE, pkgName, { + install_version: pkgVersion, + install_status: 'installed', + }); return [...installedKibanaAssetsRefs, ...installedPipelines, ...installedTemplateRefs]; } + const updateVersion = async ( savedObjectsClient: SavedObjectsClientContract, pkgName: string, @@ -239,6 +251,9 @@ export async function createInstallation(options: { version: pkgVersion, internal, removable, + install_version: pkgVersion, + install_status: 'installing', + install_started_at: new Date().toISOString(), }, { id: pkgName, overwrite: true } ); @@ -286,3 +301,28 @@ export const removeAssetsFromInstalledEsByType = async ( installed_es: installedAssetsToSave, }); }; + +export async function ensurePackagesCompletedInstall( + savedObjectsClient: SavedObjectsClientContract, + callCluster: CallESAsCurrentUser +) { + const installingPackages = await getPackageSavedObjects(savedObjectsClient, { + searchFields: ['install_status'], + search: 'installing', + }); + const installingPromises = installingPackages.saved_objects.reduce< + Array> + >((acc, pkg) => { + const startDate = pkg.attributes.install_started_at; + const nowDate = new Date().toISOString(); + const elapsedTime = Date.parse(nowDate) - Date.parse(startDate); + const pkgkey = `${pkg.attributes.name}-${pkg.attributes.install_version}`; + // reinstall package + if (elapsedTime > MAX_TIME_COMPLETE_INSTALL) { + acc.push(installPackage({ savedObjectsClient, pkgkey, callCluster })); + } + return acc; + }, []); + await Promise.all(installingPromises); + return installingPackages; +} diff --git a/x-pack/plugins/ingest_manager/server/services/setup.ts b/x-pack/plugins/ingest_manager/server/services/setup.ts index fb4430f8cf72..fd5d94a71d67 100644 --- a/x-pack/plugins/ingest_manager/server/services/setup.ts +++ b/x-pack/plugins/ingest_manager/server/services/setup.ts @@ -10,7 +10,10 @@ import { SavedObjectsClientContract } from 'src/core/server'; import { CallESAsCurrentUser } from '../types'; import { agentPolicyService } from './agent_policy'; import { outputService } from './output'; -import { ensureInstalledDefaultPackages } from './epm/packages/install'; +import { + ensureInstalledDefaultPackages, + ensurePackagesCompletedInstall, +} from './epm/packages/install'; import { ensureDefaultIndices } from './epm/kibana/index_pattern/install'; import { packageToPackagePolicy, @@ -51,6 +54,7 @@ async function createSetupSideEffects( ensureInstalledDefaultPackages(soClient, callCluster), outputService.ensureDefaultOutput(soClient), agentPolicyService.ensureDefaultAgentPolicy(soClient), + ensurePackagesCompletedInstall(soClient, callCluster), ensureDefaultIndices(callCluster), settingsService.getSettings(soClient).catch((e: any) => { if (e.isBoom && e.output.statusCode === 404) { diff --git a/x-pack/plugins/ingest_manager/server/types/index.tsx b/x-pack/plugins/ingest_manager/server/types/index.tsx index 8e3219a8c08e..aabe4bd3e359 100644 --- a/x-pack/plugins/ingest_manager/server/types/index.tsx +++ b/x-pack/plugins/ingest_manager/server/types/index.tsx @@ -37,6 +37,7 @@ export { EnrollmentAPIKey, EnrollmentAPIKeySOAttributes, Installation, + EpmPackageInstallStatus, InstallationStatus, PackageInfo, RegistryVarsEntry, diff --git a/x-pack/plugins/lens/public/app_plugin/app.test.tsx b/x-pack/plugins/lens/public/app_plugin/app.test.tsx index 892058d82a80..2b979f064b8e 100644 --- a/x-pack/plugins/lens/public/app_plugin/app.test.tsx +++ b/x-pack/plugins/lens/public/app_plugin/app.test.tsx @@ -300,7 +300,7 @@ describe('Lens App', () => { ]); }); - it.skip('sets originatingApp breadcrumb when the document title changes', async () => { + it('sets originatingApp breadcrumb when the document title changes', async () => { const defaultArgs = makeDefaultArgs(); defaultArgs.originatingApp = 'ultraCoolDashboard'; defaultArgs.getAppNameFromId = () => 'The Coolest Container Ever Made'; @@ -315,11 +315,11 @@ describe('Lens App', () => { (defaultArgs.docStorage.load as jest.Mock).mockResolvedValue({ id: '1234', title: 'Daaaaaaadaumching!', - expression: 'valid expression', state: { query: 'fake query', - datasourceMetaData: { filterableIndexPatterns: [{ id: '1', title: 'saved' }] }, + filters: [], }, + references: [], }); await act(async () => { instance.setProps({ docId: '1234' }); diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.tsx index 4f914bc65dc7..06cd858eda21 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.tsx @@ -13,7 +13,6 @@ import { EuiIcon, EuiImage, EuiText, - EuiBetaBadge, EuiButtonEmpty, EuiLink, } from '@elastic/eui'; @@ -210,10 +209,6 @@ export function InnerWorkspacePanel({ } function renderEmptyWorkspace() { - const tooltipContent = i18n.translate('xpack.lens.editorFrame.tooltipContent', { - defaultMessage: - 'Lens is in beta and is subject to change. The design and code is less mature than official GA features and is being provided as-is with no warranties. Beta features are not subject to the support SLA of official GA features', - }); return (
@@ -232,8 +227,7 @@ export function InnerWorkspacePanel({ {' '} - + />

diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx index 5bcfbc64ec70..a0cc5ec35213 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx @@ -186,9 +186,13 @@ export const InnerFieldItem = function InnerFieldItem(props: FieldItemProps) { ? i18n.translate('xpack.lens.indexPattern.fieldItemTooltip', { defaultMessage: 'Drag and drop to visualize.', }) - : i18n.translate('xpack.lens.indexPattern.fieldStatsButtonLabel', { + : exists + ? i18n.translate('xpack.lens.indexPattern.fieldStatsButtonLabel', { defaultMessage: 'Click for a field preview, or drag and drop to visualize.', }) + : i18n.translate('xpack.lens.indexPattern.fieldStatsButtonEmptyLabel', { + defaultMessage: "This field doesn't have data. Drag and drop to visualize.", + }) } type="iInCircle" color="subdued" @@ -217,11 +221,7 @@ export const InnerFieldItem = function InnerFieldItem(props: FieldItemProps) { isDraggable isActive={infoIsOpen} data-test-subj={`lnsFieldListPanelField-${field.name}`} - onClick={() => { - if (exists) { - togglePopover(); - } - }} + onClick={togglePopover} aria-label={i18n.translate('xpack.lens.indexPattern.fieldStatsButtonAriaLabel', { defaultMessage: '{fieldName}: {fieldType}. Hit enter for a field preview.', values: { @@ -313,7 +313,8 @@ function FieldItemPopoverContents(props: State & FieldItemProps) { return ( {i18n.translate('xpack.lens.indexPattern.fieldStatsNoData', { - defaultMessage: 'No data to display.', + defaultMessage: + 'This field is empty because it doesn’t exist in the 500 sampled documents.', })} ); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/fields_accordion.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/fields_accordion.tsx index 1d60b0b5cbbe..af2ed97ad812 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/fields_accordion.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/fields_accordion.tsx @@ -64,9 +64,9 @@ export const InnerFieldsAccordion = function InnerFieldsAccordion({ showExistenceFetchError, }: FieldsAccordionProps) { const renderField = useCallback( - (field: IndexPatternField) => { - return ; - }, + (field: IndexPatternField) => ( + + ), [fieldProps, exists] ); diff --git a/x-pack/plugins/lens/public/vis_type_alias.ts b/x-pack/plugins/lens/public/vis_type_alias.ts index 3bb2dbbae1f9..d0dceed03db2 100644 --- a/x-pack/plugins/lens/public/vis_type_alias.ts +++ b/x-pack/plugins/lens/public/vis_type_alias.ts @@ -27,7 +27,7 @@ export const getLensAliasConfig = (): VisTypeAlias => ({ defaultMessage: `Lens is a simpler way to create basic visualizations`, }), icon: 'lensApp', - stage: 'beta', + stage: 'production', appExtensions: { visualizations: { docTypes: ['lens'], @@ -42,7 +42,7 @@ export const getLensAliasConfig = (): VisTypeAlias => ({ editUrl: getEditPath(id), editApp: 'lens', icon: 'lensApp', - stage: 'beta', + stage: 'production', savedObjectType: type, typeTitle: i18n.translate('xpack.lens.visTypeAlias.type', { defaultMessage: 'Lens' }), }; diff --git a/x-pack/plugins/lists/public/common/mocks/kibana_core.ts b/x-pack/plugins/lists/public/common/mocks/kibana_core.ts deleted file mode 100644 index c078e8ccd5ea..000000000000 --- a/x-pack/plugins/lists/public/common/mocks/kibana_core.ts +++ /dev/null @@ -1,12 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { coreMock } from '../../../../../../src/core/public/mocks'; -import { CoreStart } from '../../../../../../src/core/public'; - -export type GlobalServices = Pick; - -export const createKibanaCoreStartMock = (): GlobalServices => coreMock.createStart(); diff --git a/x-pack/plugins/lists/public/exceptions/api.test.ts b/x-pack/plugins/lists/public/exceptions/api.test.ts index 9add15c533d1..457a8708ec34 100644 --- a/x-pack/plugins/lists/public/exceptions/api.test.ts +++ b/x-pack/plugins/lists/public/exceptions/api.test.ts @@ -3,7 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { createKibanaCoreStartMock } from '../common/mocks/kibana_core'; +import { coreMock } from '../../../../../src/core/public/mocks'; import { getExceptionListSchemaMock } from '../../common/schemas/response/exception_list_schema.mock'; import { getExceptionListItemSchemaMock } from '../../common/schemas/response/exception_list_item_schema.mock'; import { getCreateExceptionListSchemaMock } from '../../common/schemas/request/create_exception_list_schema.mock'; @@ -34,39 +34,28 @@ import { ApiCallByIdProps, ApiCallByListIdProps } from './types'; const abortCtrl = new AbortController(); -jest.mock('../common/mocks/kibana_core', () => ({ - createKibanaCoreStartMock: (): jest.Mock => jest.fn(), -})); -const fetchMock = jest.fn(); +describe('Exceptions Lists API', () => { + let httpMock: ReturnType['http']; -/* - This is a little funky, in order for typescript to not - yell at us for converting 'Pick' to type 'Mock' - have to first convert to type 'unknown' - */ -const mockKibanaHttpService = ((createKibanaCoreStartMock() as unknown) as jest.Mock).mockReturnValue( - { - fetch: fetchMock, - } -); + beforeEach(() => { + httpMock = coreMock.createStart().http; + }); -describe('Exceptions Lists API', () => { describe('#addExceptionList', () => { beforeEach(() => { - fetchMock.mockClear(); - fetchMock.mockResolvedValue(getExceptionListSchemaMock()); + httpMock.fetch.mockResolvedValue(getExceptionListSchemaMock()); }); test('it invokes "addExceptionList" with expected url and body values', async () => { const payload = getCreateExceptionListSchemaMock(); await addExceptionList({ - http: mockKibanaHttpService(), + http: httpMock, list: payload, signal: abortCtrl.signal, }); // TODO Would like to just use getExceptionListSchemaMock() here, but // validation returns object in different order, making the strings not match - expect(fetchMock).toHaveBeenCalledWith('/api/exception_lists', { + expect(httpMock.fetch).toHaveBeenCalledWith('/api/exception_lists', { body: JSON.stringify(payload), method: 'POST', signal: abortCtrl.signal, @@ -76,7 +65,7 @@ describe('Exceptions Lists API', () => { test('it returns expected exception list on success', async () => { const payload = getCreateExceptionListSchemaMock(); const exceptionResponse = await addExceptionList({ - http: mockKibanaHttpService(), + http: httpMock, list: payload, signal: abortCtrl.signal, }); @@ -90,7 +79,7 @@ describe('Exceptions Lists API', () => { await expect( addExceptionList({ - http: mockKibanaHttpService(), + http: httpMock, list: (payload as unknown) as ExceptionListSchema, signal: abortCtrl.signal, }) @@ -101,11 +90,11 @@ describe('Exceptions Lists API', () => { const payload = getCreateExceptionListSchemaMock(); const badPayload = getExceptionListSchemaMock(); delete badPayload.id; - fetchMock.mockResolvedValue(badPayload); + httpMock.fetch.mockResolvedValue(badPayload); await expect( addExceptionList({ - http: mockKibanaHttpService(), + http: httpMock, list: payload, signal: abortCtrl.signal, }) @@ -115,20 +104,19 @@ describe('Exceptions Lists API', () => { describe('#addExceptionListItem', () => { beforeEach(() => { - fetchMock.mockClear(); - fetchMock.mockResolvedValue(getExceptionListItemSchemaMock()); + httpMock.fetch.mockResolvedValue(getExceptionListItemSchemaMock()); }); test('it invokes "addExceptionListItem" with expected url and body values', async () => { const payload = getCreateExceptionListItemSchemaMock(); await addExceptionListItem({ - http: mockKibanaHttpService(), + http: httpMock, listItem: payload, signal: abortCtrl.signal, }); // TODO Would like to just use getExceptionListSchemaMock() here, but // validation returns object in different order, making the strings not match - expect(fetchMock).toHaveBeenCalledWith('/api/exception_lists/items', { + expect(httpMock.fetch).toHaveBeenCalledWith('/api/exception_lists/items', { body: JSON.stringify(payload), method: 'POST', signal: abortCtrl.signal, @@ -138,7 +126,7 @@ describe('Exceptions Lists API', () => { test('it returns expected exception list on success', async () => { const payload = getCreateExceptionListItemSchemaMock(); const exceptionResponse = await addExceptionListItem({ - http: mockKibanaHttpService(), + http: httpMock, listItem: payload, signal: abortCtrl.signal, }); @@ -152,7 +140,7 @@ describe('Exceptions Lists API', () => { await expect( addExceptionListItem({ - http: mockKibanaHttpService(), + http: httpMock, listItem: (payload as unknown) as ExceptionListItemSchema, signal: abortCtrl.signal, }) @@ -163,11 +151,11 @@ describe('Exceptions Lists API', () => { const payload = getCreateExceptionListItemSchemaMock(); const badPayload = getExceptionListItemSchemaMock(); delete badPayload.id; - fetchMock.mockResolvedValue(badPayload); + httpMock.fetch.mockResolvedValue(badPayload); await expect( addExceptionListItem({ - http: mockKibanaHttpService(), + http: httpMock, listItem: payload, signal: abortCtrl.signal, }) @@ -177,20 +165,19 @@ describe('Exceptions Lists API', () => { describe('#updateExceptionList', () => { beforeEach(() => { - fetchMock.mockClear(); - fetchMock.mockResolvedValue(getExceptionListSchemaMock()); + httpMock.fetch.mockResolvedValue(getExceptionListSchemaMock()); }); test('it invokes "updateExceptionList" with expected url and body values', async () => { const payload = getUpdateExceptionListSchemaMock(); await updateExceptionList({ - http: mockKibanaHttpService(), + http: httpMock, list: payload, signal: abortCtrl.signal, }); // TODO Would like to just use getExceptionListSchemaMock() here, but // validation returns object in different order, making the strings not match - expect(fetchMock).toHaveBeenCalledWith('/api/exception_lists', { + expect(httpMock.fetch).toHaveBeenCalledWith('/api/exception_lists', { body: JSON.stringify(payload), method: 'PUT', signal: abortCtrl.signal, @@ -200,7 +187,7 @@ describe('Exceptions Lists API', () => { test('it returns expected exception list on success', async () => { const payload = getUpdateExceptionListSchemaMock(); const exceptionResponse = await updateExceptionList({ - http: mockKibanaHttpService(), + http: httpMock, list: payload, signal: abortCtrl.signal, }); @@ -213,7 +200,7 @@ describe('Exceptions Lists API', () => { await expect( updateExceptionList({ - http: mockKibanaHttpService(), + http: httpMock, list: payload, signal: abortCtrl.signal, }) @@ -224,11 +211,11 @@ describe('Exceptions Lists API', () => { const payload = getUpdateExceptionListSchemaMock(); const badPayload = getExceptionListSchemaMock(); delete badPayload.id; - fetchMock.mockResolvedValue(badPayload); + httpMock.fetch.mockResolvedValue(badPayload); await expect( updateExceptionList({ - http: mockKibanaHttpService(), + http: httpMock, list: payload, signal: abortCtrl.signal, }) @@ -238,20 +225,19 @@ describe('Exceptions Lists API', () => { describe('#updateExceptionListItem', () => { beforeEach(() => { - fetchMock.mockClear(); - fetchMock.mockResolvedValue(getExceptionListItemSchemaMock()); + httpMock.fetch.mockResolvedValue(getExceptionListItemSchemaMock()); }); test('it invokes "updateExceptionListItem" with expected url and body values', async () => { const payload = getUpdateExceptionListItemSchemaMock(); await updateExceptionListItem({ - http: mockKibanaHttpService(), + http: httpMock, listItem: payload, signal: abortCtrl.signal, }); // TODO Would like to just use getExceptionListSchemaMock() here, but // validation returns object in different order, making the strings not match - expect(fetchMock).toHaveBeenCalledWith('/api/exception_lists/items', { + expect(httpMock.fetch).toHaveBeenCalledWith('/api/exception_lists/items', { body: JSON.stringify(payload), method: 'PUT', signal: abortCtrl.signal, @@ -261,7 +247,7 @@ describe('Exceptions Lists API', () => { test('it returns expected exception list on success', async () => { const payload = getUpdateExceptionListItemSchemaMock(); const exceptionResponse = await updateExceptionListItem({ - http: mockKibanaHttpService(), + http: httpMock, listItem: payload, signal: abortCtrl.signal, }); @@ -274,7 +260,7 @@ describe('Exceptions Lists API', () => { await expect( updateExceptionListItem({ - http: mockKibanaHttpService(), + http: httpMock, listItem: payload, signal: abortCtrl.signal, }) @@ -285,11 +271,11 @@ describe('Exceptions Lists API', () => { const payload = getUpdateExceptionListItemSchemaMock(); const badPayload = getExceptionListItemSchemaMock(); delete badPayload.id; - fetchMock.mockResolvedValue(badPayload); + httpMock.fetch.mockResolvedValue(badPayload); await expect( updateExceptionListItem({ - http: mockKibanaHttpService(), + http: httpMock, listItem: payload, signal: abortCtrl.signal, }) @@ -299,18 +285,17 @@ describe('Exceptions Lists API', () => { describe('#fetchExceptionListById', () => { beforeEach(() => { - fetchMock.mockClear(); - fetchMock.mockResolvedValue(getExceptionListSchemaMock()); + httpMock.fetch.mockResolvedValue(getExceptionListSchemaMock()); }); test('it invokes "fetchExceptionListById" with expected url and body values', async () => { await fetchExceptionListById({ - http: mockKibanaHttpService(), + http: httpMock, id: '1', namespaceType: 'single', signal: abortCtrl.signal, }); - expect(fetchMock).toHaveBeenCalledWith('/api/exception_lists', { + expect(httpMock.fetch).toHaveBeenCalledWith('/api/exception_lists', { method: 'GET', query: { id: '1', @@ -322,7 +307,7 @@ describe('Exceptions Lists API', () => { test('it returns expected exception list on success', async () => { const exceptionResponse = await fetchExceptionListById({ - http: mockKibanaHttpService(), + http: httpMock, id: '1', namespaceType: 'single', signal: abortCtrl.signal, @@ -332,7 +317,7 @@ describe('Exceptions Lists API', () => { test('it returns error and does not make request if request payload fails decode', async () => { const payload = ({ - http: mockKibanaHttpService(), + http: httpMock, id: 1, namespaceType: 'single', signal: abortCtrl.signal, @@ -345,11 +330,11 @@ describe('Exceptions Lists API', () => { test('it returns error if response payload fails decode', async () => { const badPayload = getExceptionListSchemaMock(); delete badPayload.id; - fetchMock.mockResolvedValue(badPayload); + httpMock.fetch.mockResolvedValue(badPayload); await expect( fetchExceptionListById({ - http: mockKibanaHttpService(), + http: httpMock, id: '1', namespaceType: 'single', signal: abortCtrl.signal, @@ -360,14 +345,13 @@ describe('Exceptions Lists API', () => { describe('#fetchExceptionListsItemsByListIds', () => { beforeEach(() => { - fetchMock.mockClear(); - fetchMock.mockResolvedValue(getFoundExceptionListItemSchemaMock()); + httpMock.fetch.mockResolvedValue(getFoundExceptionListItemSchemaMock()); }); test('it invokes "fetchExceptionListsItemsByListIds" with expected url and body values', async () => { await fetchExceptionListsItemsByListIds({ filterOptions: [], - http: mockKibanaHttpService(), + http: httpMock, listIds: ['myList', 'myOtherListId'], namespaceTypes: ['single', 'single'], pagination: { @@ -377,7 +361,7 @@ describe('Exceptions Lists API', () => { signal: abortCtrl.signal, }); - expect(fetchMock).toHaveBeenCalledWith('/api/exception_lists/items/_find', { + expect(httpMock.fetch).toHaveBeenCalledWith('/api/exception_lists/items/_find', { method: 'GET', query: { list_id: 'myList,myOtherListId', @@ -397,7 +381,7 @@ describe('Exceptions Lists API', () => { tags: [], }, ], - http: mockKibanaHttpService(), + http: httpMock, listIds: ['myList'], namespaceTypes: ['single'], pagination: { @@ -407,7 +391,7 @@ describe('Exceptions Lists API', () => { signal: abortCtrl.signal, }); - expect(fetchMock).toHaveBeenCalledWith('/api/exception_lists/items/_find', { + expect(httpMock.fetch).toHaveBeenCalledWith('/api/exception_lists/items/_find', { method: 'GET', query: { filter: 'exception-list.attributes.entries.field:hello world*', @@ -428,7 +412,7 @@ describe('Exceptions Lists API', () => { tags: [], }, ], - http: mockKibanaHttpService(), + http: httpMock, listIds: ['myList'], namespaceTypes: ['agnostic'], pagination: { @@ -438,7 +422,7 @@ describe('Exceptions Lists API', () => { signal: abortCtrl.signal, }); - expect(fetchMock).toHaveBeenCalledWith('/api/exception_lists/items/_find', { + expect(httpMock.fetch).toHaveBeenCalledWith('/api/exception_lists/items/_find', { method: 'GET', query: { filter: 'exception-list-agnostic.attributes.entries.field:hello world*', @@ -459,7 +443,7 @@ describe('Exceptions Lists API', () => { tags: ['malware'], }, ], - http: mockKibanaHttpService(), + http: httpMock, listIds: ['myList'], namespaceTypes: ['agnostic'], pagination: { @@ -469,7 +453,7 @@ describe('Exceptions Lists API', () => { signal: abortCtrl.signal, }); - expect(fetchMock).toHaveBeenCalledWith('/api/exception_lists/items/_find', { + expect(httpMock.fetch).toHaveBeenCalledWith('/api/exception_lists/items/_find', { method: 'GET', query: { filter: 'exception-list-agnostic.attributes.tags:malware', @@ -490,7 +474,7 @@ describe('Exceptions Lists API', () => { tags: ['malware'], }, ], - http: mockKibanaHttpService(), + http: httpMock, listIds: ['myList'], namespaceTypes: ['agnostic'], pagination: { @@ -500,7 +484,7 @@ describe('Exceptions Lists API', () => { signal: abortCtrl.signal, }); - expect(fetchMock).toHaveBeenCalledWith('/api/exception_lists/items/_find', { + expect(httpMock.fetch).toHaveBeenCalledWith('/api/exception_lists/items/_find', { method: 'GET', query: { filter: @@ -517,7 +501,7 @@ describe('Exceptions Lists API', () => { test('it returns expected format when call succeeds', async () => { const exceptionResponse = await fetchExceptionListsItemsByListIds({ filterOptions: [], - http: mockKibanaHttpService(), + http: httpMock, listIds: ['endpoint_list_id'], namespaceTypes: ['single'], pagination: { @@ -532,7 +516,7 @@ describe('Exceptions Lists API', () => { test('it returns error and does not make request if request payload fails decode', async () => { const payload = ({ filterOptions: [], - http: mockKibanaHttpService(), + http: httpMock, listIds: ['myList'], namespaceTypes: ['not a namespace type'], pagination: { @@ -549,12 +533,12 @@ describe('Exceptions Lists API', () => { test('it returns error if response payload fails decode', async () => { const badPayload = getExceptionListItemSchemaMock(); delete badPayload.id; - fetchMock.mockResolvedValue(badPayload); + httpMock.fetch.mockResolvedValue(badPayload); await expect( fetchExceptionListsItemsByListIds({ filterOptions: [], - http: mockKibanaHttpService(), + http: httpMock, listIds: ['myList'], namespaceTypes: ['single'], pagination: { @@ -571,18 +555,17 @@ describe('Exceptions Lists API', () => { describe('#fetchExceptionListItemById', () => { beforeEach(() => { - fetchMock.mockClear(); - fetchMock.mockResolvedValue(getExceptionListItemSchemaMock()); + httpMock.fetch.mockResolvedValue(getExceptionListItemSchemaMock()); }); test('it invokes "fetchExceptionListItemById" with expected url and body values', async () => { await fetchExceptionListItemById({ - http: mockKibanaHttpService(), + http: httpMock, id: '1', namespaceType: 'single', signal: abortCtrl.signal, }); - expect(fetchMock).toHaveBeenCalledWith('/api/exception_lists/items', { + expect(httpMock.fetch).toHaveBeenCalledWith('/api/exception_lists/items', { method: 'GET', query: { id: '1', @@ -594,7 +577,7 @@ describe('Exceptions Lists API', () => { test('it returns expected format when call succeeds', async () => { const exceptionResponse = await fetchExceptionListItemById({ - http: mockKibanaHttpService(), + http: httpMock, id: '1', namespaceType: 'single', signal: abortCtrl.signal, @@ -604,7 +587,7 @@ describe('Exceptions Lists API', () => { test('it returns error and does not make request if request payload fails decode', async () => { const payload = ({ - http: mockKibanaHttpService(), + http: httpMock, id: '1', namespaceType: 'not a namespace type', signal: abortCtrl.signal, @@ -617,11 +600,11 @@ describe('Exceptions Lists API', () => { test('it returns error if response payload fails decode', async () => { const badPayload = getExceptionListItemSchemaMock(); delete badPayload.id; - fetchMock.mockResolvedValue(badPayload); + httpMock.fetch.mockResolvedValue(badPayload); await expect( fetchExceptionListItemById({ - http: mockKibanaHttpService(), + http: httpMock, id: '1', namespaceType: 'single', signal: abortCtrl.signal, @@ -632,18 +615,17 @@ describe('Exceptions Lists API', () => { describe('#deleteExceptionListById', () => { beforeEach(() => { - fetchMock.mockClear(); - fetchMock.mockResolvedValue(getExceptionListSchemaMock()); + httpMock.fetch.mockResolvedValue(getExceptionListSchemaMock()); }); test('check parameter url, body when deleting exception item', async () => { await deleteExceptionListById({ - http: mockKibanaHttpService(), + http: httpMock, id: '1', namespaceType: 'single', signal: abortCtrl.signal, }); - expect(fetchMock).toHaveBeenCalledWith('/api/exception_lists', { + expect(httpMock.fetch).toHaveBeenCalledWith('/api/exception_lists', { method: 'DELETE', query: { id: '1', @@ -655,7 +637,7 @@ describe('Exceptions Lists API', () => { test('it returns expected format when call succeeds', async () => { const exceptionResponse = await deleteExceptionListById({ - http: mockKibanaHttpService(), + http: httpMock, id: '1', namespaceType: 'single', signal: abortCtrl.signal, @@ -665,7 +647,7 @@ describe('Exceptions Lists API', () => { test('it returns error and does not make request if request payload fails decode', async () => { const payload = ({ - http: mockKibanaHttpService(), + http: httpMock, id: 1, namespaceType: 'single', signal: abortCtrl.signal, @@ -678,11 +660,11 @@ describe('Exceptions Lists API', () => { test('it returns error if response payload fails decode', async () => { const badPayload = getExceptionListSchemaMock(); delete badPayload.id; - fetchMock.mockResolvedValue(badPayload); + httpMock.fetch.mockResolvedValue(badPayload); await expect( deleteExceptionListById({ - http: mockKibanaHttpService(), + http: httpMock, id: '1', namespaceType: 'single', signal: abortCtrl.signal, @@ -693,18 +675,17 @@ describe('Exceptions Lists API', () => { describe('#deleteExceptionListItemById', () => { beforeEach(() => { - fetchMock.mockClear(); - fetchMock.mockResolvedValue(getExceptionListItemSchemaMock()); + httpMock.fetch.mockResolvedValue(getExceptionListItemSchemaMock()); }); test('check parameter url, body when deleting exception item', async () => { await deleteExceptionListItemById({ - http: mockKibanaHttpService(), + http: httpMock, id: '1', namespaceType: 'single', signal: abortCtrl.signal, }); - expect(fetchMock).toHaveBeenCalledWith('/api/exception_lists/items', { + expect(httpMock.fetch).toHaveBeenCalledWith('/api/exception_lists/items', { method: 'DELETE', query: { id: '1', @@ -716,7 +697,7 @@ describe('Exceptions Lists API', () => { test('it returns expected format when call succeeds', async () => { const exceptionResponse = await deleteExceptionListItemById({ - http: mockKibanaHttpService(), + http: httpMock, id: '1', namespaceType: 'single', signal: abortCtrl.signal, @@ -726,7 +707,7 @@ describe('Exceptions Lists API', () => { test('it returns error and does not make request if request payload fails decode', async () => { const payload = ({ - http: mockKibanaHttpService(), + http: httpMock, id: 1, namespaceType: 'single', signal: abortCtrl.signal, @@ -739,11 +720,11 @@ describe('Exceptions Lists API', () => { test('it returns error if response payload fails decode', async () => { const badPayload = getExceptionListItemSchemaMock(); delete badPayload.id; - fetchMock.mockResolvedValue(badPayload); + httpMock.fetch.mockResolvedValue(badPayload); await expect( deleteExceptionListItemById({ - http: mockKibanaHttpService(), + http: httpMock, id: '1', namespaceType: 'single', signal: abortCtrl.signal, @@ -754,16 +735,15 @@ describe('Exceptions Lists API', () => { describe('#addEndpointExceptionList', () => { beforeEach(() => { - fetchMock.mockClear(); - fetchMock.mockResolvedValue(getExceptionListSchemaMock()); + httpMock.fetch.mockResolvedValue(getExceptionListSchemaMock()); }); test('it invokes "addEndpointExceptionList" with expected url and body values', async () => { await addEndpointExceptionList({ - http: mockKibanaHttpService(), + http: httpMock, signal: abortCtrl.signal, }); - expect(fetchMock).toHaveBeenCalledWith('/api/endpoint_list', { + expect(httpMock.fetch).toHaveBeenCalledWith('/api/endpoint_list', { method: 'POST', signal: abortCtrl.signal, }); @@ -771,16 +751,16 @@ describe('Exceptions Lists API', () => { test('it returns expected exception list on success', async () => { const exceptionResponse = await addEndpointExceptionList({ - http: mockKibanaHttpService(), + http: httpMock, signal: abortCtrl.signal, }); expect(exceptionResponse).toEqual(getExceptionListSchemaMock()); }); test('it returns an empty object when list already exists', async () => { - fetchMock.mockResolvedValue({}); + httpMock.fetch.mockResolvedValue({}); const exceptionResponse = await addEndpointExceptionList({ - http: mockKibanaHttpService(), + http: httpMock, signal: abortCtrl.signal, }); expect(exceptionResponse).toEqual({}); diff --git a/x-pack/plugins/lists/public/exceptions/hooks/persist_exception_item.test.ts b/x-pack/plugins/lists/public/exceptions/hooks/persist_exception_item.test.ts index ebee2cbace9c..9460432cbc9c 100644 --- a/x-pack/plugins/lists/public/exceptions/hooks/persist_exception_item.test.ts +++ b/x-pack/plugins/lists/public/exceptions/hooks/persist_exception_item.test.ts @@ -6,16 +6,16 @@ import { act, renderHook } from '@testing-library/react-hooks'; +import { coreMock } from '../../../../../../src/core/public/mocks'; import * as api from '../api'; import { getCreateExceptionListItemSchemaMock } from '../../../common/schemas/request/create_exception_list_item_schema.mock'; import { getUpdateExceptionListItemSchemaMock } from '../../../common/schemas/request/update_exception_list_item_schema.mock'; import { getExceptionListItemSchemaMock } from '../../../common/schemas/response/exception_list_item_schema.mock'; -import { createKibanaCoreStartMock } from '../../common/mocks/kibana_core'; import { PersistHookProps } from '../types'; import { ReturnPersistExceptionItem, usePersistExceptionItem } from './persist_exception_item'; -const mockKibanaHttpService = createKibanaCoreStartMock().http; +const mockKibanaHttpService = coreMock.createStart().http; describe('usePersistExceptionItem', () => { const onError = jest.fn(); diff --git a/x-pack/plugins/lists/public/exceptions/hooks/persist_exception_list.test.ts b/x-pack/plugins/lists/public/exceptions/hooks/persist_exception_list.test.ts index 0541f893e279..d5dfe1174d00 100644 --- a/x-pack/plugins/lists/public/exceptions/hooks/persist_exception_list.test.ts +++ b/x-pack/plugins/lists/public/exceptions/hooks/persist_exception_list.test.ts @@ -6,16 +6,16 @@ import { act, renderHook } from '@testing-library/react-hooks'; +import { coreMock } from '../../../../../../src/core/public/mocks'; import * as api from '../api'; import { getCreateExceptionListSchemaMock } from '../../../common/schemas/request/create_exception_list_schema.mock'; import { getUpdateExceptionListSchemaMock } from '../../../common/schemas/request/update_exception_list_schema.mock'; import { getExceptionListSchemaMock } from '../../../common/schemas/response/exception_list_schema.mock'; -import { createKibanaCoreStartMock } from '../../common/mocks/kibana_core'; import { PersistHookProps } from '../types'; import { ReturnPersistExceptionList, usePersistExceptionList } from './persist_exception_list'; -const mockKibanaHttpService = createKibanaCoreStartMock().http; +const mockKibanaHttpService = coreMock.createStart().http; describe('usePersistExceptionList', () => { const onError = jest.fn(); diff --git a/x-pack/plugins/lists/public/exceptions/hooks/use_api.test.ts b/x-pack/plugins/lists/public/exceptions/hooks/use_api.test.ts index c93155274937..6469dc49c460 100644 --- a/x-pack/plugins/lists/public/exceptions/hooks/use_api.test.ts +++ b/x-pack/plugins/lists/public/exceptions/hooks/use_api.test.ts @@ -6,8 +6,8 @@ import { act, renderHook } from '@testing-library/react-hooks'; +import { coreMock } from '../../../../../../src/core/public/mocks'; import * as api from '../api'; -import { createKibanaCoreStartMock } from '../../common/mocks/kibana_core'; import { getExceptionListSchemaMock } from '../../../common/schemas/response/exception_list_schema.mock'; import { getFoundExceptionListItemSchemaMock } from '../../../common/schemas/response/found_exception_list_item_schema.mock'; import { getExceptionListItemSchemaMock } from '../../../common/schemas/response/exception_list_item_schema.mock'; @@ -16,7 +16,7 @@ import { ApiCallByIdProps, ApiCallByListIdProps } from '../types'; import { ExceptionsApi, useApi } from './use_api'; -const mockKibanaHttpService = createKibanaCoreStartMock().http; +const mockKibanaHttpService = coreMock.createStart().http; describe('useApi', () => { const onErrorMock = jest.fn(); diff --git a/x-pack/plugins/lists/public/exceptions/hooks/use_exception_list.test.ts b/x-pack/plugins/lists/public/exceptions/hooks/use_exception_list.test.ts index 3a8b1713b901..5c544c7e96e3 100644 --- a/x-pack/plugins/lists/public/exceptions/hooks/use_exception_list.test.ts +++ b/x-pack/plugins/lists/public/exceptions/hooks/use_exception_list.test.ts @@ -6,15 +6,15 @@ import { act, renderHook } from '@testing-library/react-hooks'; +import { coreMock } from '../../../../../../src/core/public/mocks'; import * as api from '../api'; -import { createKibanaCoreStartMock } from '../../common/mocks/kibana_core'; import { getFoundExceptionListItemSchemaMock } from '../../../common/schemas/response/found_exception_list_item_schema.mock'; import { ExceptionListItemSchema } from '../../../common/schemas'; import { UseExceptionListProps, UseExceptionListSuccess } from '../types'; import { ReturnExceptionListAndItems, useExceptionList } from './use_exception_list'; -const mockKibanaHttpService = createKibanaCoreStartMock().http; +const mockKibanaHttpService = coreMock.createStart().http; describe('useExceptionList', () => { const onErrorMock = jest.fn(); diff --git a/x-pack/plugins/maps/common/descriptor_types/layer_descriptor_types.ts b/x-pack/plugins/maps/common/descriptor_types/layer_descriptor_types.ts index a04d0e1a978f..b67f05cb169f 100644 --- a/x-pack/plugins/maps/common/descriptor_types/layer_descriptor_types.ts +++ b/x-pack/plugins/maps/common/descriptor_types/layer_descriptor_types.ts @@ -38,36 +38,3 @@ export type LayerDescriptor = { export type VectorLayerDescriptor = LayerDescriptor & { style: VectorStyleDescriptor; }; - -export type RangeFieldMeta = { - min: number; - max: number; - delta: number; - isMinOutsideStdRange?: boolean; - isMaxOutsideStdRange?: boolean; -}; - -export type Category = { - key: string; - count: number; -}; - -export type CategoryFieldMeta = { - categories: Category[]; -}; - -export type GeometryTypes = { - isPointsOnly: boolean; - isLinesOnly: boolean; - isPolygonsOnly: boolean; -}; - -export type StyleMetaDescriptor = { - geometryTypes?: GeometryTypes; - fieldMeta: { - [key: string]: { - range: RangeFieldMeta; - categories: CategoryFieldMeta; - }; - }; -}; diff --git a/x-pack/plugins/maps/common/descriptor_types/style_property_descriptor_types.ts b/x-pack/plugins/maps/common/descriptor_types/style_property_descriptor_types.ts index ce6539c9c452..5aba9b06a6cc 100644 --- a/x-pack/plugins/maps/common/descriptor_types/style_property_descriptor_types.ts +++ b/x-pack/plugins/maps/common/descriptor_types/style_property_descriptor_types.ts @@ -12,7 +12,6 @@ import { SYMBOLIZE_AS_TYPES, VECTOR_STYLES, STYLE_TYPE, - LAYER_STYLE_TYPE, } from '../constants'; // Non-static/dynamic options @@ -193,9 +192,47 @@ export type StyleDescriptor = { type: string; }; +export type RangeFieldMeta = { + min: number; + max: number; + delta: number; + isMinOutsideStdRange?: boolean; + isMaxOutsideStdRange?: boolean; +}; + +export type Category = { + key: string; + count: number; +}; + +export type CategoryFieldMeta = { + categories: Category[]; +}; + +export type GeometryTypes = { + isPointsOnly: boolean; + isLinesOnly: boolean; + isPolygonsOnly: boolean; +}; + +export type StyleMetaDescriptor = { + geometryTypes?: GeometryTypes; + fieldMeta: { + [key: string]: { + range?: RangeFieldMeta; + categories?: CategoryFieldMeta; + }; + }; +}; + export type VectorStyleDescriptor = StyleDescriptor & { - type: LAYER_STYLE_TYPE.VECTOR; properties: VectorStylePropertiesDescriptor; + isTimeAware: boolean; + __styleMeta?: StyleMetaDescriptor; +}; + +export type HeatmapStyleDescriptor = StyleDescriptor & { + colorRampName: string; }; export type StylePropertyOptions = diff --git a/x-pack/plugins/maps/public/actions/data_request_actions.ts b/x-pack/plugins/maps/public/actions/data_request_actions.ts index 6f5ed680ac64..2876f3d668a6 100644 --- a/x-pack/plugins/maps/public/actions/data_request_actions.ts +++ b/x-pack/plugins/maps/public/actions/data_request_actions.ts @@ -11,11 +11,10 @@ import uuid from 'uuid/v4'; import { multiPoint } from '@turf/helpers'; import { FeatureCollection } from 'geojson'; import { MapStoreState } from '../reducers/store'; -import { LAYER_TYPE, SOURCE_DATA_REQUEST_ID } from '../../common/constants'; +import { LAYER_STYLE_TYPE, LAYER_TYPE, SOURCE_DATA_REQUEST_ID } from '../../common/constants'; import { getDataFilters, getDataRequestDescriptor, - getFittableLayers, getLayerById, getLayerList, } from '../selectors/map_selectors'; @@ -42,6 +41,7 @@ import { IVectorLayer } from '../classes/layers/vector_layer/vector_layer'; import { DataMeta, MapExtent, MapFilters } from '../../common/descriptor_types'; import { DataRequestAbortError } from '../classes/util/data_request'; import { scaleBounds, turfBboxToBounds } from '../../common/elasticsearch_geo_utils'; +import { IVectorStyle } from '../classes/styles/vector/vector_style'; const FIT_TO_BOUNDS_SCALE_FACTOR = 0.1; @@ -85,10 +85,12 @@ export function updateStyleMeta(layerId: string | null) { } const sourceDataRequest = layer.getSourceDataRequest(); const style = layer.getCurrentStyle(); - if (!style || !sourceDataRequest) { + if (!style || !sourceDataRequest || style.getType() !== LAYER_STYLE_TYPE.VECTOR) { return; } - const styleMeta = await style.pluckStyleMetaFromSourceDataRequest(sourceDataRequest); + const styleMeta = await (style as IVectorStyle).pluckStyleMetaFromSourceDataRequest( + sourceDataRequest + ); dispatch({ type: SET_LAYER_STYLE_META, layerId, @@ -321,13 +323,16 @@ export function fitToLayerExtent(layerId: string) { export function fitToDataBounds(onNoBounds?: () => void) { return async (dispatch: Dispatch, getState: () => MapStoreState) => { - const layerList = getFittableLayers(getState()); + const layerList = getLayerList(getState()); if (!layerList.length) { return; } const boundsPromises = layerList.map(async (layer: ILayer) => { + if (!(await layer.isFittable())) { + return null; + } return layer.getBounds(getDataRequestContext(dispatch, getState, layer.getId())); }); diff --git a/x-pack/plugins/maps/public/actions/layer_actions.ts b/x-pack/plugins/maps/public/actions/layer_actions.ts index 472e42129816..675bb1472288 100644 --- a/x-pack/plugins/maps/public/actions/layer_actions.ts +++ b/x-pack/plugins/maps/public/actions/layer_actions.ts @@ -40,7 +40,8 @@ import { cleanTooltipStateForLayer } from './tooltip_actions'; import { JoinDescriptor, LayerDescriptor, StyleDescriptor } from '../../common/descriptor_types'; import { ILayer } from '../classes/layers/layer'; import { IVectorLayer } from '../classes/layers/vector_layer/vector_layer'; -import { LAYER_TYPE } from '../../common/constants'; +import { LAYER_STYLE_TYPE, LAYER_TYPE } from '../../common/constants'; +import { IVectorStyle } from '../classes/styles/vector/vector_style'; export function trackCurrentLayerState(layerId: string) { return { @@ -381,12 +382,15 @@ export function clearMissingStyleProperties(layerId: string) { } const style = targetLayer!.getCurrentStyle(); - if (!style) { + if (!style || style.getType() !== LAYER_STYLE_TYPE.VECTOR) { return; } const nextFields = await (targetLayer as IVectorLayer).getFields(); // take into account all fields, since labels can be driven by any field (source or join) - const { hasChanges, nextStyleDescriptor } = style.getDescriptorWithMissingStylePropsRemoved( + const { + hasChanges, + nextStyleDescriptor, + } = (style as IVectorStyle).getDescriptorWithMissingStylePropsRemoved( nextFields, getMapColors(getState()) ); diff --git a/x-pack/plugins/maps/public/classes/joins/inner_join.d.ts b/x-pack/plugins/maps/public/classes/joins/inner_join.d.ts index 452a3fdc3a19..befff0965fb7 100644 --- a/x-pack/plugins/maps/public/classes/joins/inner_join.d.ts +++ b/x-pack/plugins/maps/public/classes/joins/inner_join.d.ts @@ -17,4 +17,6 @@ export class InnerJoin implements IJoin { toDescriptor(): JoinDescriptor; getSourceMetaDataRequestId(): string; + + getSourceFormattersDataRequestId(): string; } diff --git a/x-pack/plugins/maps/public/classes/joins/join.ts b/x-pack/plugins/maps/public/classes/joins/join.ts index 4a551c5bcc48..5bcc4bfdec87 100644 --- a/x-pack/plugins/maps/public/classes/joins/join.ts +++ b/x-pack/plugins/maps/public/classes/joins/join.ts @@ -13,4 +13,6 @@ export interface IJoin { toDescriptor(): JoinDescriptor; getSourceMetaDataRequestId(): string; + + getSourceFormattersDataRequestId(): string; } diff --git a/x-pack/plugins/maps/public/classes/layers/blended_vector_layer/blended_vector_layer.ts b/x-pack/plugins/maps/public/classes/layers/blended_vector_layer/blended_vector_layer.ts index 950d9890a3c6..90e8d25a7795 100644 --- a/x-pack/plugins/maps/public/classes/layers/blended_vector_layer/blended_vector_layer.ts +++ b/x-pack/plugins/maps/public/classes/layers/blended_vector_layer/blended_vector_layer.ts @@ -102,6 +102,7 @@ function getClusterStyleDescriptor( }, }, }, + isTimeAware: true, }; documentStyle .getAllStyleProperties() diff --git a/x-pack/plugins/maps/public/classes/layers/layer.test.ts b/x-pack/plugins/maps/public/classes/layers/layer.test.ts index f25ecd710645..7bc91d71f83e 100644 --- a/x-pack/plugins/maps/public/classes/layers/layer.test.ts +++ b/x-pack/plugins/maps/public/classes/layers/layer.test.ts @@ -21,6 +21,10 @@ jest.mock('uuid/v4', () => { class MockLayer extends AbstractLayer {} class MockSource { + private readonly _fitToBounds: boolean; + constructor({ fitToBounds = true } = {}) { + this._fitToBounds = fitToBounds; + } cloneDescriptor() { return {}; } @@ -28,6 +32,10 @@ class MockSource { getDisplayName() { return 'mySource'; } + + async supportsFitToBounds() { + return this._fitToBounds; + } } class MockStyle {} @@ -126,3 +134,40 @@ describe('cloneDescriptor', () => { }); }); }); + +describe('isFittable', () => { + [ + { + isVisible: true, + fitToBounds: true, + canFit: true, + }, + { + isVisible: false, + fitToBounds: true, + canFit: false, + }, + { + isVisible: true, + fitToBounds: false, + canFit: false, + }, + { + isVisible: false, + fitToBounds: false, + canFit: false, + }, + ].forEach((test) => { + it(`Should take into account layer visibility and bounds-retrieval: ${JSON.stringify( + test + )}`, async () => { + const layerDescriptor = AbstractLayer.createDescriptor({ visible: test.isVisible }); + const layer = new MockLayer({ + layerDescriptor, + source: (new MockSource({ fitToBounds: test.fitToBounds }) as unknown) as ISource, + style: (new MockStyle() as unknown) as IStyle, + }); + expect(await layer.isFittable()).toBe(test.canFit); + }); + }); +}); diff --git a/x-pack/plugins/maps/public/classes/layers/layer.tsx b/x-pack/plugins/maps/public/classes/layers/layer.tsx index 424100c5a7e3..8026f48fe609 100644 --- a/x-pack/plugins/maps/public/classes/layers/layer.tsx +++ b/x-pack/plugins/maps/public/classes/layers/layer.tsx @@ -90,6 +90,7 @@ export interface ILayer { supportsLabelsOnTop: () => boolean; showJoinEditor(): boolean; getJoinsDisabledReason(): string | null; + isFittable(): Promise; } export type Footnote = { icon: ReactElement; @@ -233,6 +234,10 @@ export class AbstractLayer implements ILayer { return await this.getSource().supportsFitToBounds(); } + async isFittable(): Promise { + return (await this.supportsFitToBounds()) && this.isVisible(); + } + async getDisplayName(source?: ISource): Promise { if (this._descriptor.label) { return this._descriptor.label; diff --git a/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.d.ts b/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.d.ts index e6cb212dadda..ad4479d3a324 100644 --- a/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.d.ts +++ b/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.d.ts @@ -83,4 +83,5 @@ export class VectorLayer extends AbstractLayer implements IVectorLayer { getFeatureById(id: string | number): Feature | null; getPropertiesForTooltip(properties: GeoJsonProperties): Promise; hasJoins(): boolean; + isFittable(): Promise; } diff --git a/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.js b/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.js index f5f5071bab15..2ba7f750e9b4 100644 --- a/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.js +++ b/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.js @@ -309,7 +309,7 @@ export class VectorLayer extends AbstractLayer { _getSearchFilters(dataFilters, source, style) { const fieldNames = [ ...source.getFieldNames(), - ...style.getSourceFieldNames(), + ...(style.getType() === LAYER_STYLE_TYPE.VECTOR ? style.getSourceFieldNames() : []), ...this.getValidJoins().map((join) => join.getLeftField().getName()), ]; @@ -415,7 +415,7 @@ export class VectorLayer extends AbstractLayer { } async _syncSourceStyleMeta(syncContext, source, style) { - if (this.getCurrentStyle().constructor.type !== LAYER_STYLE_TYPE.VECTOR) { + if (this.getCurrentStyle().getType() !== LAYER_STYLE_TYPE.VECTOR) { return; } @@ -511,7 +511,7 @@ export class VectorLayer extends AbstractLayer { } async _syncSourceFormatters(syncContext, source, style) { - if (style.constructor.type !== LAYER_STYLE_TYPE.VECTOR) { + if (style.getType() !== LAYER_STYLE_TYPE.VECTOR) { return; } diff --git a/x-pack/plugins/maps/public/classes/sources/es_agg_source/es_agg_source.d.ts b/x-pack/plugins/maps/public/classes/sources/es_agg_source/es_agg_source.d.ts index 91ed0ab52b08..eb50cd7528c8 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_agg_source/es_agg_source.d.ts +++ b/x-pack/plugins/maps/public/classes/sources/es_agg_source/es_agg_source.d.ts @@ -15,6 +15,7 @@ export interface IESAggSource extends IESSource { getAggLabel(aggType: AGG_TYPE, fieldName: string): string; getMetricFields(): IESAggField[]; hasMatchingMetricField(fieldName: string): boolean; + getMetricFieldForName(fieldName: string): IESAggField | null; } export class AbstractESAggSource extends AbstractESSource implements IESAggSource { @@ -24,4 +25,5 @@ export class AbstractESAggSource extends AbstractESSource implements IESAggSourc getAggLabel(aggType: AGG_TYPE, fieldName: string): string; getMetricFields(): IESAggField[]; hasMatchingMetricField(fieldName: string): boolean; + getMetricFieldForName(fieldName: string): IESAggField | null; } diff --git a/x-pack/plugins/maps/public/classes/sources/vector_source/vector_source.d.ts b/x-pack/plugins/maps/public/classes/sources/vector_source/vector_source.d.ts index 62fc5a283e5f..271505010f36 100644 --- a/x-pack/plugins/maps/public/classes/sources/vector_source/vector_source.d.ts +++ b/x-pack/plugins/maps/public/classes/sources/vector_source/vector_source.d.ts @@ -53,6 +53,7 @@ export interface IVectorSource extends ISource { getApplyGlobalQuery(): boolean; createField({ fieldName }: { fieldName: string }): IField; canFormatFeatureProperties(): boolean; + getSupportedShapeTypes(): Promise; } export class AbstractVectorSource extends AbstractSource implements IVectorSource { diff --git a/x-pack/plugins/maps/public/classes/styles/components/ranged_style_legend_row.js b/x-pack/plugins/maps/public/classes/styles/components/ranged_style_legend_row.tsx similarity index 71% rename from x-pack/plugins/maps/public/classes/styles/components/ranged_style_legend_row.js rename to x-pack/plugins/maps/public/classes/styles/components/ranged_style_legend_row.tsx index 4a43cc24e2c0..24812cb35b07 100644 --- a/x-pack/plugins/maps/public/classes/styles/components/ranged_style_legend_row.js +++ b/x-pack/plugins/maps/public/classes/styles/components/ranged_style_legend_row.tsx @@ -4,12 +4,25 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; -import PropTypes from 'prop-types'; +import React, { ReactElement } from 'react'; import { EuiFlexGroup, EuiFlexItem, EuiText, EuiToolTip } from '@elastic/eui'; -export function RangedStyleLegendRow({ header, minLabel, maxLabel, propertyLabel, fieldLabel }) { +interface Props { + header: ReactElement; + minLabel: string | number; + maxLabel: string | number; + propertyLabel: string; + fieldLabel: string; +} + +export function RangedStyleLegendRow({ + header, + minLabel, + maxLabel, + propertyLabel, + fieldLabel, +}: Props) { return (

@@ -39,11 +52,3 @@ export function RangedStyleLegendRow({ header, minLabel, maxLabel, propertyLabel
); } - -RangedStyleLegendRow.propTypes = { - header: PropTypes.node.isRequired, - minLabel: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired, - maxLabel: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired, - propertyLabel: PropTypes.string.isRequired, - fieldLabel: PropTypes.string.isRequired, -}; diff --git a/x-pack/plugins/maps/public/classes/styles/heatmap/components/legend/heatmap_legend.js b/x-pack/plugins/maps/public/classes/styles/heatmap/components/legend/heatmap_legend.tsx similarity index 80% rename from x-pack/plugins/maps/public/classes/styles/heatmap/components/legend/heatmap_legend.js rename to x-pack/plugins/maps/public/classes/styles/heatmap/components/legend/heatmap_legend.tsx index 5c3600a149af..7024adc891b5 100644 --- a/x-pack/plugins/maps/public/classes/styles/heatmap/components/legend/heatmap_legend.js +++ b/x-pack/plugins/maps/public/classes/styles/heatmap/components/legend/heatmap_legend.tsx @@ -4,18 +4,27 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; +import React, { Component } from 'react'; import { i18n } from '@kbn/i18n'; import { ColorGradient } from './color_gradient'; import { RangedStyleLegendRow } from '../../../components/ranged_style_legend_row'; import { HEATMAP_COLOR_RAMP_LABEL } from '../heatmap_constants'; +import { IField } from '../../../../fields/field'; -export class HeatmapLegend extends React.Component { - constructor() { - super(); - this.state = { label: '' }; - } +interface Props { + colorRampName: string; + field: IField; +} + +interface State { + label: string; +} + +export class HeatmapLegend extends Component { + private _isMounted: boolean = false; + + state: State = { label: '' }; componentDidUpdate() { this._loadLabel(); diff --git a/x-pack/plugins/maps/public/classes/styles/heatmap/heatmap_style.js b/x-pack/plugins/maps/public/classes/styles/heatmap/heatmap_style.tsx similarity index 68% rename from x-pack/plugins/maps/public/classes/styles/heatmap/heatmap_style.js rename to x-pack/plugins/maps/public/classes/styles/heatmap/heatmap_style.tsx index 55bbbc9319df..a30022517852 100644 --- a/x-pack/plugins/maps/public/classes/styles/heatmap/heatmap_style.js +++ b/x-pack/plugins/maps/public/classes/styles/heatmap/heatmap_style.tsx @@ -5,43 +5,48 @@ */ import React from 'react'; -import { AbstractStyle } from '../style'; +import { Map as MbMap } from 'mapbox-gl'; +import { i18n } from '@kbn/i18n'; +import { EuiIcon } from '@elastic/eui'; +import { IStyle } from '../style'; import { HeatmapStyleEditor } from './components/heatmap_style_editor'; import { HeatmapLegend } from './components/legend/heatmap_legend'; import { DEFAULT_HEATMAP_COLOR_RAMP_NAME, getOrdinalMbColorRampStops } from '../color_palettes'; import { LAYER_STYLE_TYPE, GRID_RESOLUTION } from '../../../../common/constants'; +import { HeatmapStyleDescriptor, StyleDescriptor } from '../../../../common/descriptor_types'; +import { IField } from '../../fields/field'; -import { i18n } from '@kbn/i18n'; -import { EuiIcon } from '@elastic/eui'; - -//The heatmap range chosen hear runs from 0 to 1. It is arbitrary. -//Weighting is on the raw count/sum values. +// The heatmap range chosen hear runs from 0 to 1. It is arbitrary. +// Weighting is on the raw count/sum values. const MIN_RANGE = 0.1; // 0 to 0.1 is displayed as transparent color stop const MAX_RANGE = 1; -export class HeatmapStyle extends AbstractStyle { - static type = LAYER_STYLE_TYPE.HEATMAP; +export class HeatmapStyle implements IStyle { + readonly _descriptor: HeatmapStyleDescriptor; - constructor(descriptor = {}) { - super(); + constructor( + descriptor: { colorRampName: string } = { colorRampName: DEFAULT_HEATMAP_COLOR_RAMP_NAME } + ) { this._descriptor = HeatmapStyle.createDescriptor(descriptor.colorRampName); } - static createDescriptor(colorRampName) { + static createDescriptor(colorRampName: string) { return { - type: HeatmapStyle.type, + type: LAYER_STYLE_TYPE.HEATMAP, colorRampName: colorRampName ? colorRampName : DEFAULT_HEATMAP_COLOR_RAMP_NAME, }; } - static getDisplayName() { - return i18n.translate('xpack.maps.style.heatmap.displayNameLabel', { - defaultMessage: 'Heatmap style', - }); + getType() { + return LAYER_STYLE_TYPE.HEATMAP; } - renderEditor({ onStyleDescriptorChange }) { - const onHeatmapColorChange = ({ colorRampName }) => { + renderEditor({ + onStyleDescriptorChange, + }: { + onStyleDescriptorChange: (styleDescriptor: StyleDescriptor) => void; + }) { + const onHeatmapColorChange = ({ colorRampName }: { colorRampName: string }) => { const styleDescriptor = HeatmapStyle.createDescriptor(colorRampName); onStyleDescriptorChange(styleDescriptor); }; @@ -54,7 +59,7 @@ export class HeatmapStyle extends AbstractStyle { ); } - renderLegendDetails(field) { + renderLegendDetails(field: IField) { return ; } @@ -62,7 +67,17 @@ export class HeatmapStyle extends AbstractStyle { return ; } - setMBPaintProperties({ mbMap, layerId, propertyName, resolution }) { + setMBPaintProperties({ + mbMap, + layerId, + propertyName, + resolution, + }: { + mbMap: MbMap; + layerId: string; + propertyName: string; + resolution: GRID_RESOLUTION; + }) { let radius; if (resolution === GRID_RESOLUTION.COARSE) { radius = 128; diff --git a/x-pack/plugins/maps/public/classes/styles/style.ts b/x-pack/plugins/maps/public/classes/styles/style.ts index 1859c7875ad1..abaa6184b0ca 100644 --- a/x-pack/plugins/maps/public/classes/styles/style.ts +++ b/x-pack/plugins/maps/public/classes/styles/style.ts @@ -5,18 +5,11 @@ */ import { ReactElement } from 'react'; -import { StyleDescriptor, StyleMetaDescriptor } from '../../../common/descriptor_types'; +import { StyleDescriptor } from '../../../common/descriptor_types'; import { ILayer } from '../layers/layer'; -import { IField } from '../fields/field'; -import { DataRequest } from '../util/data_request'; export interface IStyle { - getDescriptor(): StyleDescriptor | null; - getDescriptorWithMissingStylePropsRemoved( - nextFields: IField[], - mapColors: string[] - ): { hasChanges: boolean; nextStyleDescriptor?: StyleDescriptor }; - pluckStyleMetaFromSourceDataRequest(sourceDataRequest: DataRequest): StyleMetaDescriptor; + getType(): string; renderEditor({ layer, onStyleDescriptorChange, @@ -24,38 +17,4 @@ export interface IStyle { layer: ILayer; onStyleDescriptorChange: (styleDescriptor: StyleDescriptor) => void; }): ReactElement | null; - getSourceFieldNames(): string[]; -} - -export class AbstractStyle implements IStyle { - readonly _descriptor: StyleDescriptor | null; - - constructor(descriptor: StyleDescriptor | null) { - this._descriptor = descriptor; - } - - getDescriptorWithMissingStylePropsRemoved( - nextFields: IField[], - mapColors: string[] - ): { hasChanges: boolean; nextStyleDescriptor?: StyleDescriptor } { - return { - hasChanges: false, - }; - } - - pluckStyleMetaFromSourceDataRequest(sourceDataRequest: DataRequest): StyleMetaDescriptor { - return { fieldMeta: {} }; - } - - getDescriptor(): StyleDescriptor | null { - return this._descriptor; - } - - renderEditor(/* { layer, onStyleDescriptorChange } */) { - return null; - } - - getSourceFieldNames(): string[] { - return []; - } } diff --git a/x-pack/plugins/maps/public/classes/styles/tile/tile_style.ts b/x-pack/plugins/maps/public/classes/styles/tile/tile_style.ts index f658d0821edf..cac3913d3149 100644 --- a/x-pack/plugins/maps/public/classes/styles/tile/tile_style.ts +++ b/x-pack/plugins/maps/public/classes/styles/tile/tile_style.ts @@ -4,13 +4,24 @@ * you may not use this file except in compliance with the Elastic License. */ -import { AbstractStyle } from '../style'; +import { IStyle } from '../style'; +import { StyleDescriptor } from '../../../../common/descriptor_types'; import { LAYER_STYLE_TYPE } from '../../../../common/constants'; -export class TileStyle extends AbstractStyle { +export class TileStyle implements IStyle { + readonly _descriptor: StyleDescriptor; + constructor() { - super({ + this._descriptor = { type: LAYER_STYLE_TYPE.TILE, - }); + }; + } + + getType() { + return LAYER_STYLE_TYPE.TILE; + } + + renderEditor(/* { layer, onStyleDescriptorChange } */) { + return null; } } diff --git a/x-pack/plugins/maps/public/classes/styles/vector/components/legend/ordinal_legend.tsx b/x-pack/plugins/maps/public/classes/styles/vector/components/legend/ordinal_legend.tsx index a99548b6af7b..1851c14f7b95 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/components/legend/ordinal_legend.tsx +++ b/x-pack/plugins/maps/public/classes/styles/vector/components/legend/ordinal_legend.tsx @@ -7,7 +7,6 @@ import React, { Component, Fragment } from 'react'; import _ from 'lodash'; import { EuiFlexGroup, EuiFlexItem, EuiHorizontalRule } from '@elastic/eui'; -// @ts-expect-error import { RangedStyleLegendRow } from '../../../components/ranged_style_legend_row'; import { VECTOR_STYLES } from '../../../../../../common/constants'; import { CircleIcon } from './circle_icon'; diff --git a/x-pack/plugins/maps/public/classes/styles/vector/components/legend/vector_style_legend.tsx b/x-pack/plugins/maps/public/classes/styles/vector/components/legend/vector_style_legend.tsx index 4d50c632bfd6..fcf7a735d743 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/components/legend/vector_style_legend.tsx +++ b/x-pack/plugins/maps/public/classes/styles/vector/components/legend/vector_style_legend.tsx @@ -11,7 +11,7 @@ interface Props { isLinesOnly: boolean; isPointsOnly: boolean; styles: Array>; - symbolId: string; + symbolId?: string; } export function VectorStyleLegend({ isLinesOnly, isPointsOnly, styles, symbolId }: Props) { @@ -31,5 +31,5 @@ export function VectorStyleLegend({ isLinesOnly, isPointsOnly, styles, symbolId ); } - return legendRows; + return <>{legendRows}; } diff --git a/x-pack/plugins/maps/public/classes/styles/vector/properties/__tests__/test_util.ts b/x-pack/plugins/maps/public/classes/styles/vector/properties/__tests__/test_util.ts index a8fba834d65a..3f6edc81e30e 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/properties/__tests__/test_util.ts +++ b/x-pack/plugins/maps/public/classes/styles/vector/properties/__tests__/test_util.ts @@ -14,7 +14,7 @@ import { StyleMetaDescriptor, } from '../../../../../../common/descriptor_types'; import { AbstractField, IField } from '../../../../fields/field'; -import { IStyle, AbstractStyle } from '../../../style'; +import { IStyle } from '../../../style'; class MockField extends AbstractField { async getLabel(): Promise { @@ -30,16 +30,23 @@ export const mockField: IField = new MockField({ origin: FIELD_ORIGIN.SOURCE, }); -export class MockStyle extends AbstractStyle implements IStyle { +export class MockStyle implements IStyle { private readonly _min: number; private readonly _max: number; constructor({ min = 0, max = 100 } = {}) { - super(null); this._min = min; this._max = max; } + renderEditor() { + return null; + } + + getType() { + return 'mockStyle'; + } + getStyleMeta(): StyleMeta { const geomTypes: GeometryTypes = { isPointsOnly: false, diff --git a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_orientation_property.js b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_orientation_property.ts similarity index 82% rename from x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_orientation_property.js rename to x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_orientation_property.ts index 763eb81ad0f9..dd976027a50f 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_orientation_property.js +++ b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_orientation_property.ts @@ -4,12 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ +import { Map as MbMap } from 'mapbox-gl'; import { DynamicStyleProperty } from './dynamic_style_property'; import { getComputedFieldName } from '../style_util'; import { VECTOR_STYLES } from '../../../../../common/constants'; +import { OrientationDynamicOptions } from '../../../../../common/descriptor_types'; -export class DynamicOrientationProperty extends DynamicStyleProperty { - syncIconRotationWithMb(symbolLayerId, mbMap) { +export class DynamicOrientationProperty extends DynamicStyleProperty { + syncIconRotationWithMb(symbolLayerId: string, mbMap: MbMap) { if (this._field && this._field.isValid()) { const targetName = this._field.supportsAutoDomain() ? getComputedFieldName(VECTOR_STYLES.ICON_ORIENTATION, this.getFieldName()) diff --git a/x-pack/plugins/maps/public/classes/styles/vector/style_meta.ts b/x-pack/plugins/maps/public/classes/styles/vector/style_meta.ts index 646b88d005af..590a859c6755 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/style_meta.ts +++ b/x-pack/plugins/maps/public/classes/styles/vector/style_meta.ts @@ -17,14 +17,14 @@ export class StyleMeta { } getRangeFieldMetaDescriptor(fieldName: string): RangeFieldMeta | null { - return this._descriptor && this._descriptor.fieldMeta[fieldName] - ? this._descriptor.fieldMeta[fieldName].range + return this._descriptor.fieldMeta[fieldName] && this._descriptor.fieldMeta[fieldName].range + ? this._descriptor.fieldMeta[fieldName].range! : null; } getCategoryFieldMetaDescriptor(fieldName: string): CategoryFieldMeta | null { - return this._descriptor && this._descriptor.fieldMeta[fieldName] - ? this._descriptor.fieldMeta[fieldName].categories + return this._descriptor.fieldMeta[fieldName] && this._descriptor.fieldMeta[fieldName].categories + ? this._descriptor.fieldMeta[fieldName].categories! : null; } diff --git a/x-pack/plugins/maps/public/classes/styles/vector/vector_style.d.ts b/x-pack/plugins/maps/public/classes/styles/vector/vector_style.d.ts deleted file mode 100644 index d48d075288a2..000000000000 --- a/x-pack/plugins/maps/public/classes/styles/vector/vector_style.d.ts +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ -import { IStyleProperty } from './properties/style_property'; -import { IDynamicStyleProperty } from './properties/dynamic_style_property'; -import { IVectorLayer } from '../../layers/vector_layer/vector_layer'; -import { IVectorSource } from '../../sources/vector_source'; -import { AbstractStyle, IStyle } from '../style'; -import { - DynamicStylePropertyOptions, - StylePropertyOptions, - VectorStyleDescriptor, - VectorStylePropertiesDescriptor, -} from '../../../../common/descriptor_types'; -import { StyleMeta } from './style_meta'; - -export interface IVectorStyle extends IStyle { - getAllStyleProperties(): Array>; - getDynamicPropertiesArray(): Array>; - getSourceFieldNames(): string[]; - getStyleMeta(): StyleMeta; -} - -export class VectorStyle extends AbstractStyle implements IVectorStyle { - static createDescriptor(properties: VectorStylePropertiesDescriptor): VectorStyleDescriptor; - static createDefaultStyleProperties(mapColors: string[]): VectorStylePropertiesDescriptor; - constructor(descriptor: VectorStyleDescriptor, source: IVectorSource, layer: IVectorLayer); - getSourceFieldNames(): string[]; - getAllStyleProperties(): Array>; - getDynamicPropertiesArray(): Array>; - getStyleMeta(): StyleMeta; -} diff --git a/x-pack/plugins/maps/public/classes/styles/vector/vector_style.js b/x-pack/plugins/maps/public/classes/styles/vector/vector_style.tsx similarity index 65% rename from x-pack/plugins/maps/public/classes/styles/vector/vector_style.js rename to x-pack/plugins/maps/public/classes/styles/vector/vector_style.tsx index 907e16a6a842..956522524a2e 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/vector_style.js +++ b/x-pack/plugins/maps/public/classes/styles/vector/vector_style.tsx @@ -6,6 +6,9 @@ import _ from 'lodash'; import React from 'react'; +import { Map as MbMap, FeatureIdentifier } from 'mapbox-gl'; +import { FeatureCollection } from 'geojson'; +// @ts-expect-error import { VectorStyleEditor } from './components/vector_style_editor'; import { getDefaultProperties, @@ -13,7 +16,6 @@ import { LINE_STYLES, POLYGON_STYLES, } from './vector_style_defaults'; -import { AbstractStyle } from '../style'; import { GEO_JSON_TYPE, FIELD_ORIGIN, @@ -43,45 +45,109 @@ import { extractColorFromStyleProperty } from './components/legend/extract_color import { SymbolizeAsProperty } from './properties/symbolize_as_property'; import { StaticIconProperty } from './properties/static_icon_property'; import { DynamicIconProperty } from './properties/dynamic_icon_property'; +import { + ColorDynamicOptions, + ColorStaticOptions, + ColorStylePropertyDescriptor, + DynamicStylePropertyOptions, + IconDynamicOptions, + IconStaticOptions, + IconStylePropertyDescriptor, + LabelDynamicOptions, + LabelStaticOptions, + LabelStylePropertyDescriptor, + OrientationDynamicOptions, + OrientationStaticOptions, + OrientationStylePropertyDescriptor, + SizeDynamicOptions, + SizeStaticOptions, + SizeStylePropertyDescriptor, + StyleDescriptor, + StyleMetaDescriptor, + StylePropertyField, + StylePropertyOptions, + VectorStyleDescriptor, + VectorStylePropertiesDescriptor, +} from '../../../../common/descriptor_types'; +import { DataRequest } from '../../util/data_request'; +import { IStyle } from '../style'; +import { IStyleProperty } from './properties/style_property'; +import { IDynamicStyleProperty } from './properties/dynamic_style_property'; +import { IField } from '../../fields/field'; +import { IVectorLayer } from '../../layers/vector_layer/vector_layer'; +import { IVectorSource } from '../../sources/vector_source'; +import { ILayer } from '../../layers/layer'; const POINTS = [GEO_JSON_TYPE.POINT, GEO_JSON_TYPE.MULTI_POINT]; const LINES = [GEO_JSON_TYPE.LINE_STRING, GEO_JSON_TYPE.MULTI_LINE_STRING]; const POLYGONS = [GEO_JSON_TYPE.POLYGON, GEO_JSON_TYPE.MULTI_POLYGON]; -function getNumericalMbFeatureStateValue(value) { +function getNumericalMbFeatureStateValue(value: string) { const valueAsFloat = parseFloat(value); return isNaN(valueAsFloat) ? null : valueAsFloat; } -export class VectorStyle extends AbstractStyle { - static type = LAYER_STYLE_TYPE.VECTOR; +export interface IVectorStyle extends IStyle { + getAllStyleProperties(): Array>; + getDynamicPropertiesArray(): Array>; + getSourceFieldNames(): string[]; + getStyleMeta(): StyleMeta; + getDescriptorWithMissingStylePropsRemoved( + nextFields: IField[], + mapColors: string[] + ): { hasChanges: boolean; nextStyleDescriptor?: VectorStyleDescriptor }; + pluckStyleMetaFromSourceDataRequest(sourceDataRequest: DataRequest): Promise; +} + +export class VectorStyle implements IVectorStyle { + private readonly _descriptor: VectorStyleDescriptor; + private readonly _layer: IVectorLayer; + private readonly _source: IVectorSource; + private readonly _styleMeta: StyleMeta; + + private readonly _symbolizeAsStyleProperty: SymbolizeAsProperty; + private readonly _lineColorStyleProperty: StaticColorProperty | DynamicColorProperty; + private readonly _fillColorStyleProperty: StaticColorProperty | DynamicColorProperty; + private readonly _lineWidthStyleProperty: StaticSizeProperty | DynamicSizeProperty; + private readonly _iconStyleProperty: StaticIconProperty | DynamicIconProperty; + private readonly _iconSizeStyleProperty: StaticSizeProperty | DynamicSizeProperty; + private readonly _iconOrientationProperty: StaticOrientationProperty | DynamicOrientationProperty; + private readonly _labelStyleProperty: StaticTextProperty | DynamicTextProperty; + private readonly _labelSizeStyleProperty: StaticSizeProperty | DynamicSizeProperty; + private readonly _labelColorStyleProperty: StaticColorProperty | DynamicColorProperty; + private readonly _labelBorderColorStyleProperty: StaticColorProperty | DynamicColorProperty; + private readonly _labelBorderSizeStyleProperty: LabelBorderSizeProperty; static createDescriptor(properties = {}, isTimeAware = true) { return { - type: VectorStyle.type, + type: LAYER_STYLE_TYPE.VECTOR, properties: { ...getDefaultProperties(), ...properties }, isTimeAware, }; } - static createDefaultStyleProperties(mapColors) { + static createDefaultStyleProperties(mapColors: string[]) { return getDefaultProperties(mapColors); } - constructor(descriptor = {}, source, layer) { - super(); - descriptor = descriptor === null ? {} : descriptor; + constructor( + descriptor: VectorStyleDescriptor | null, + source: IVectorSource, + layer: IVectorLayer + ) { this._source = source; this._layer = layer; - this._descriptor = { - ...descriptor, - ...VectorStyle.createDescriptor(descriptor.properties, descriptor.isTimeAware), - }; + this._descriptor = descriptor + ? { + ...descriptor, + ...VectorStyle.createDescriptor(descriptor.properties, descriptor.isTimeAware), + } + : VectorStyle.createDescriptor(); this._styleMeta = new StyleMeta(this._descriptor.__styleMeta); this._symbolizeAsStyleProperty = new SymbolizeAsProperty( - this._descriptor.properties[VECTOR_STYLES.SYMBOLIZE_AS].options, + this._descriptor.properties[VECTOR_STYLES.SYMBOLIZE_AS]!.options, VECTOR_STYLES.SYMBOLIZE_AS ); this._lineColorStyleProperty = this._makeColorProperty( @@ -94,7 +160,8 @@ export class VectorStyle extends AbstractStyle { ); this._lineWidthStyleProperty = this._makeSizeProperty( this._descriptor.properties[VECTOR_STYLES.LINE_WIDTH], - VECTOR_STYLES.LINE_WIDTH + VECTOR_STYLES.LINE_WIDTH, + this._symbolizeAsStyleProperty.isSymbolizedAsIcon() ); this._iconStyleProperty = this._makeIconProperty( this._descriptor.properties[VECTOR_STYLES.ICON] @@ -113,7 +180,8 @@ export class VectorStyle extends AbstractStyle { ); this._labelSizeStyleProperty = this._makeSizeProperty( this._descriptor.properties[VECTOR_STYLES.LABEL_SIZE], - VECTOR_STYLES.LABEL_SIZE + VECTOR_STYLES.LABEL_SIZE, + this._symbolizeAsStyleProperty.isSymbolizedAsIcon() ); this._labelColorStyleProperty = this._makeColorProperty( this._descriptor.properties[VECTOR_STYLES.LABEL_COLOR], @@ -124,12 +192,16 @@ export class VectorStyle extends AbstractStyle { VECTOR_STYLES.LABEL_BORDER_COLOR ); this._labelBorderSizeStyleProperty = new LabelBorderSizeProperty( - this._descriptor.properties[VECTOR_STYLES.LABEL_BORDER_SIZE].options, + this._descriptor.properties[VECTOR_STYLES.LABEL_BORDER_SIZE]!.options, VECTOR_STYLES.LABEL_BORDER_SIZE, this._labelSizeStyleProperty ); } + getType() { + return LAYER_STYLE_TYPE.VECTOR; + } + getAllStyleProperties() { return [ this._symbolizeAsStyleProperty, @@ -150,18 +222,24 @@ export class VectorStyle extends AbstractStyle { _hasBorder() { return this._lineWidthStyleProperty.isDynamic() ? this._lineWidthStyleProperty.isComplete() - : this._lineWidthStyleProperty.getOptions().size !== 0; + : (this._lineWidthStyleProperty as StaticSizeProperty).getOptions().size !== 0; } - renderEditor({ layer, onStyleDescriptorChange }) { + renderEditor({ + layer, + onStyleDescriptorChange, + }: { + layer: ILayer; + onStyleDescriptorChange: (styleDescriptor: StyleDescriptor) => void; + }) { const rawProperties = this.getRawProperties(); - const handlePropertyChange = (propertyName, settings) => { - rawProperties[propertyName] = settings; //override single property, but preserve the rest + const handlePropertyChange = (propertyName: VECTOR_STYLES, settings: any) => { + rawProperties[propertyName] = settings; // override single property, but preserve the rest const vectorStyleDescriptor = VectorStyle.createDescriptor(rawProperties, this.isTimeAware()); onStyleDescriptorChange(vectorStyleDescriptor); }; - const onIsTimeAwareChange = (isTimeAware) => { + const onIsTimeAwareChange = (isTimeAware: boolean) => { const vectorStyleDescriptor = VectorStyle.createDescriptor(rawProperties, isTimeAware); onStyleDescriptorChange(vectorStyleDescriptor); }; @@ -170,8 +248,9 @@ export class VectorStyle extends AbstractStyle { return dynamicStyleProp.isFieldMetaEnabled(); }); - const styleProperties = {}; + const styleProperties: VectorStylePropertiesDescriptor = {}; this.getAllStyleProperties().forEach((styleProperty) => { + // @ts-expect-error styleProperties[styleProperty.getStyleName()] = styleProperty; }); @@ -201,26 +280,40 @@ export class VectorStyle extends AbstractStyle { * This method does not update its descriptor. It just returns a new descriptor that the caller * can then use to update store state via dispatch. */ - getDescriptorWithMissingStylePropsRemoved(nextFields, mapColors) { + getDescriptorWithMissingStylePropsRemoved(nextFields: IField[], mapColors: string[]) { const originalProperties = this.getRawProperties(); - const updatedProperties = {}; + const updatedProperties = {} as VectorStylePropertiesDescriptor; - const dynamicProperties = Object.keys(originalProperties).filter((key) => { - const { type, options } = originalProperties[key] || {}; - return type === STYLE_TYPE.DYNAMIC && options.field && options.field.name; + const dynamicProperties = (Object.keys(originalProperties) as VECTOR_STYLES[]).filter((key) => { + if (!originalProperties[key]) { + return false; + } + const propertyDescriptor = originalProperties[key]; + if ( + !propertyDescriptor || + !('type' in propertyDescriptor) || + propertyDescriptor.type !== STYLE_TYPE.DYNAMIC || + !propertyDescriptor.options + ) { + return false; + } + const dynamicOptions = propertyDescriptor.options as DynamicStylePropertyOptions; + return dynamicOptions.field && dynamicOptions.field.name; }); - dynamicProperties.forEach((key) => { + dynamicProperties.forEach((key: VECTOR_STYLES) => { // Convert dynamic styling to static stying when there are no nextFields if (nextFields.length === 0) { const staticProperties = getDefaultStaticProperties(mapColors); - updatedProperties[key] = staticProperties[key]; + updatedProperties[key] = staticProperties[key] as any; return; } const dynamicProperty = originalProperties[key]; - const fieldName = - dynamicProperty && dynamicProperty.options.field && dynamicProperty.options.field.name; + if (!dynamicProperty || !dynamicProperty.options) { + return; + } + const fieldName = (dynamicProperty.options as DynamicStylePropertyOptions).field!.name; if (!fieldName) { return; } @@ -236,9 +329,10 @@ export class VectorStyle extends AbstractStyle { updatedProperties[key] = { type: DynamicStyleProperty.type, options: { - ...originalProperties[key].options, + ...originalProperties[key]!.options, }, - }; + } as any; + // @ts-expect-error delete updatedProperties[key].options.field; }); @@ -261,7 +355,7 @@ export class VectorStyle extends AbstractStyle { }; } - async pluckStyleMetaFromSourceDataRequest(sourceDataRequest) { + async pluckStyleMetaFromSourceDataRequest(sourceDataRequest: DataRequest) { const features = _.get(sourceDataRequest.getData(), 'features', []); const supportedFeatures = await this._source.getSupportedShapeTypes(); @@ -307,7 +401,7 @@ export class VectorStyle extends AbstractStyle { ), }, fieldMeta: {}, - }; + } as StyleMetaDescriptor; const dynamicProperties = this.getDynamicPropertiesArray(); if (dynamicProperties.length === 0 || features.length === 0) { @@ -318,7 +412,7 @@ export class VectorStyle extends AbstractStyle { dynamicProperties.forEach((dynamicProperty) => { const categoricalStyleMeta = dynamicProperty.pluckCategoricalStyleMetaFromFeatures(features); const ordinalStyleMeta = dynamicProperty.pluckOrdinalStyleMetaFromFeatures(features); - const name = dynamicProperty.getField().getName(); + const name = dynamicProperty.getFieldName(); if (!styleMeta.fieldMeta[name]) { styleMeta.fieldMeta[name] = {}; } @@ -335,10 +429,10 @@ export class VectorStyle extends AbstractStyle { } getSourceFieldNames() { - const fieldNames = []; + const fieldNames: string[] = []; this.getDynamicPropertiesArray().forEach((styleProperty) => { if (styleProperty.getFieldOrigin() === FIELD_ORIGIN.SOURCE) { - fieldNames.push(styleProperty.getField().getName()); + fieldNames.push(styleProperty.getFieldName()); } }); return fieldNames; @@ -356,7 +450,7 @@ export class VectorStyle extends AbstractStyle { const styleProperties = this.getAllStyleProperties(); return styleProperties.filter( (styleProperty) => styleProperty.isDynamic() && styleProperty.isComplete() - ); + ) as Array>; } _getIsPointsOnly = () => { @@ -371,10 +465,10 @@ export class VectorStyle extends AbstractStyle { return this._styleMeta.isPolygonsOnly(); }; - _getDynamicPropertyByFieldName(fieldName) { + _getDynamicPropertyByFieldName(fieldName: string) { const dynamicProps = this.getDynamicPropertiesArray(); return dynamicProps.find((dynamicProp) => { - return fieldName === dynamicProp.getField().getName(); + return fieldName === dynamicProp.getFieldName(); }); } @@ -382,7 +476,7 @@ export class VectorStyle extends AbstractStyle { return this._styleMeta; } - _getFieldFormatter = (fieldName) => { + _getFieldFormatter = (fieldName: string) => { const dynamicProp = this._getDynamicPropertyByFieldName(fieldName); if (!dynamicProp) { return null; @@ -392,11 +486,11 @@ export class VectorStyle extends AbstractStyle { if (dynamicProp.getFieldOrigin() === FIELD_ORIGIN.SOURCE) { dataRequestId = SOURCE_FORMATTERS_DATA_REQUEST_ID; } else { - const join = this._layer.getValidJoins().find((join) => { + const targetJoin = this._layer.getValidJoins().find((join) => { return join.getRightJoinSource().hasMatchingMetricField(fieldName); }); - if (join) { - dataRequestId = join.getSourceFormattersDataRequestId(); + if (targetJoin) { + dataRequestId = targetJoin.getSourceFormattersDataRequestId(); } } @@ -410,13 +504,14 @@ export class VectorStyle extends AbstractStyle { } const formatters = formattersDataRequest.getData(); - return formatters[fieldName]; + // @ts-expect-error + return formatters ? formatters[fieldName] : null; }; _getSymbolId() { - return this.arePointsSymbolizedAsCircles() + return this.arePointsSymbolizedAsCircles() || this._iconStyleProperty.isDynamic() ? undefined - : this._iconStyleProperty.getOptions().value; + : (this._iconStyleProperty as StaticIconProperty).getOptions().value; } getIcon = () => { @@ -434,7 +529,7 @@ export class VectorStyle extends AbstractStyle { ); } const fillColor = isLinesOnly - ? null + ? undefined : extractColorFromStyleProperty( this._descriptor.properties[VECTOR_STYLES.FILL_COLOR], 'grey' @@ -485,10 +580,10 @@ export class VectorStyle extends AbstractStyle { ); } - clearFeatureState(featureCollection, mbMap, sourceId) { - const tmpFeatureIdentifier = { - source: null, - id: null, + clearFeatureState(featureCollection: FeatureCollection, mbMap: MbMap, sourceId: string) { + const tmpFeatureIdentifier: FeatureIdentifier = { + source: '', + id: undefined, }; for (let i = 0; i < featureCollection.features.length; i++) { const feature = featureCollection.features[i]; @@ -498,7 +593,11 @@ export class VectorStyle extends AbstractStyle { } } - setFeatureStateAndStyleProps(featureCollection, mbMap, mbSourceId) { + setFeatureStateAndStyleProps( + featureCollection: FeatureCollection, + mbMap: MbMap, + mbSourceId: string + ) { if (!featureCollection) { return; } @@ -508,24 +607,24 @@ export class VectorStyle extends AbstractStyle { return; } - const tmpFeatureIdentifier = { - source: null, - id: null, + const tmpFeatureIdentifier: FeatureIdentifier = { + source: '', + id: undefined, }; - const tmpFeatureState = {}; + const tmpFeatureState: any = {}; for (let i = 0; i < featureCollection.features.length; i++) { const feature = featureCollection.features[i]; for (let j = 0; j < dynamicStyleProps.length; j++) { const dynamicStyleProp = dynamicStyleProps[j]; - const name = dynamicStyleProp.getField().getName(); + const name = dynamicStyleProp.getFieldName(); const computedName = getComputedFieldName(dynamicStyleProp.getStyleName(), name); - const rawValue = feature.properties[name]; + const rawValue = feature.properties ? feature.properties[name] : undefined; if (dynamicStyleProp.supportsMbFeatureState()) { - tmpFeatureState[name] = getNumericalMbFeatureStateValue(rawValue); //the same value will be potentially overridden multiple times, if the name remains identical + tmpFeatureState[name] = getNumericalMbFeatureStateValue(rawValue); // the same value will be potentially overridden multiple times, if the name remains identical } else { - //in practice, a new system property will only be created for: + // in practice, a new system property will only be created for: // - label text: this requires the value to be formatted first. // - icon orientation: this is a lay-out property which do not support feature-state (but we're still coercing to a number) @@ -533,7 +632,7 @@ export class VectorStyle extends AbstractStyle { ? getNumericalMbFeatureStateValue(rawValue) : dynamicStyleProp.formatField(rawValue); - feature.properties[computedName] = formattedValue; + if (feature.properties) feature.properties[computedName] = formattedValue; } } tmpFeatureIdentifier.source = mbSourceId; @@ -541,10 +640,10 @@ export class VectorStyle extends AbstractStyle { mbMap.setFeatureState(tmpFeatureIdentifier, tmpFeatureState); } - //returns boolean indicating if styles do not support feature-state and some values are stored in geojson properties - //this return-value is used in an optimization for style-updates with mapbox-gl. - //`true` indicates the entire data needs to reset on the source (otherwise the style-rules will not be reapplied) - //`false` indicates the data does not need to be reset on the store, because styles are re-evaluated if they use featureState + // returns boolean indicating if styles do not support feature-state and some values are stored in geojson properties + // this return-value is used in an optimization for style-updates with mapbox-gl. + // `true` indicates the entire data needs to reset on the source (otherwise the style-rules will not be reapplied) + // `false` indicates the data does not need to be reset on the store, because styles are re-evaluated if they use featureState return dynamicStyleProps.some((dynamicStyleProp) => !dynamicStyleProp.supportsMbFeatureState()); } @@ -552,23 +651,49 @@ export class VectorStyle extends AbstractStyle { return !this._symbolizeAsStyleProperty.isSymbolizedAsIcon(); } - setMBPaintProperties({ alpha, mbMap, fillLayerId, lineLayerId }) { + setMBPaintProperties({ + alpha, + mbMap, + fillLayerId, + lineLayerId, + }: { + alpha: number; + mbMap: MbMap; + fillLayerId: string; + lineLayerId: string; + }) { this._fillColorStyleProperty.syncFillColorWithMb(fillLayerId, mbMap, alpha); this._lineColorStyleProperty.syncLineColorWithMb(lineLayerId, mbMap, alpha); this._lineWidthStyleProperty.syncLineWidthWithMb(lineLayerId, mbMap); } - setMBPaintPropertiesForPoints({ alpha, mbMap, pointLayerId }) { + setMBPaintPropertiesForPoints({ + alpha, + mbMap, + pointLayerId, + }: { + alpha: number; + mbMap: MbMap; + pointLayerId: string; + }) { this._fillColorStyleProperty.syncCircleColorWithMb(pointLayerId, mbMap, alpha); this._lineColorStyleProperty.syncCircleStrokeWithMb(pointLayerId, mbMap, alpha); const hasNoRadius = !this._iconSizeStyleProperty.isDynamic() && - this._iconSizeStyleProperty.getOptions().size === 0; + (this._iconSizeStyleProperty as StaticSizeProperty).getOptions().size === 0; this._lineWidthStyleProperty.syncCircleStrokeWidthWithMb(pointLayerId, mbMap, hasNoRadius); - this._iconSizeStyleProperty.syncCircleRadiusWithMb(pointLayerId, mbMap, hasNoRadius); + this._iconSizeStyleProperty.syncCircleRadiusWithMb(pointLayerId, mbMap); } - setMBPropertiesForLabelText({ alpha, mbMap, textLayerId }) { + setMBPropertiesForLabelText({ + alpha, + mbMap, + textLayerId, + }: { + alpha: number; + mbMap: MbMap; + textLayerId: string; + }) { mbMap.setLayoutProperty(textLayerId, 'icon-allow-overlap', true); mbMap.setLayoutProperty(textLayerId, 'text-allow-overlap', true); this._labelStyleProperty.syncTextFieldWithMb(textLayerId, mbMap); @@ -578,7 +703,15 @@ export class VectorStyle extends AbstractStyle { this._labelBorderColorStyleProperty.syncLabelBorderColorWithMb(textLayerId, mbMap); } - setMBSymbolPropertiesForPoints({ mbMap, symbolLayerId, alpha }) { + setMBSymbolPropertiesForPoints({ + mbMap, + symbolLayerId, + alpha, + }: { + alpha: number; + mbMap: MbMap; + symbolLayerId: string; + }) { mbMap.setLayoutProperty(symbolLayerId, 'icon-ignore-placement', true); mbMap.setPaintProperty(symbolLayerId, 'icon-opacity', alpha); @@ -595,34 +728,41 @@ export class VectorStyle extends AbstractStyle { this._iconOrientationProperty.syncIconRotationWithMb(symbolLayerId, mbMap); } - _makeField(fieldDescriptor) { + _makeField(fieldDescriptor?: StylePropertyField) { if (!fieldDescriptor || !fieldDescriptor.name) { return null; } - //fieldDescriptor.label is ignored. This is essentially cruft duplicating label-info from the metric-selection - //Ignore this custom label + // fieldDescriptor.label is ignored. This is essentially cruft duplicating label-info from the metric-selection + // Ignore this custom label if (fieldDescriptor.origin === FIELD_ORIGIN.SOURCE) { return this._source.getFieldByName(fieldDescriptor.name); } else if (fieldDescriptor.origin === FIELD_ORIGIN.JOIN) { - const join = this._layer.getValidJoins().find((join) => { + const targetJoin = this._layer.getValidJoins().find((join) => { return join.getRightJoinSource().hasMatchingMetricField(fieldDescriptor.name); }); - return join ? join.getRightJoinSource().getMetricFieldForName(fieldDescriptor.name) : null; + return targetJoin + ? targetJoin.getRightJoinSource().getMetricFieldForName(fieldDescriptor.name) + : null; } else { throw new Error(`Unknown origin-type ${fieldDescriptor.origin}`); } } - _makeSizeProperty(descriptor, styleName, isSymbolizedAsIcon) { + _makeSizeProperty( + descriptor: SizeStylePropertyDescriptor | undefined, + styleName: VECTOR_STYLES, + isSymbolizedAsIcon: boolean + ) { if (!descriptor || !descriptor.options) { return new StaticSizeProperty({ size: 0 }, styleName); } else if (descriptor.type === StaticStyleProperty.type) { - return new StaticSizeProperty(descriptor.options, styleName); + return new StaticSizeProperty(descriptor.options as SizeStaticOptions, styleName); } else if (descriptor.type === DynamicStyleProperty.type) { - const field = this._makeField(descriptor.options.field); + const options = descriptor.options as SizeDynamicOptions; + const field = this._makeField(options.field); return new DynamicSizeProperty( - descriptor.options, + options, styleName, field, this._layer, @@ -634,15 +774,19 @@ export class VectorStyle extends AbstractStyle { } } - _makeColorProperty(descriptor, styleName) { + _makeColorProperty( + descriptor: ColorStylePropertyDescriptor | undefined, + styleName: VECTOR_STYLES + ) { if (!descriptor || !descriptor.options) { - return new StaticColorProperty({ color: null }, styleName); + return new StaticColorProperty({ color: '' }, styleName); } else if (descriptor.type === StaticStyleProperty.type) { - return new StaticColorProperty(descriptor.options, styleName); + return new StaticColorProperty(descriptor.options as ColorStaticOptions, styleName); } else if (descriptor.type === DynamicStyleProperty.type) { - const field = this._makeField(descriptor.options.field); + const options = descriptor.options as ColorDynamicOptions; + const field = this._makeField(options.field); return new DynamicColorProperty( - descriptor.options, + options, styleName, field, this._layer, @@ -653,15 +797,22 @@ export class VectorStyle extends AbstractStyle { } } - _makeOrientationProperty(descriptor, styleName) { + _makeOrientationProperty( + descriptor: OrientationStylePropertyDescriptor | undefined, + styleName: VECTOR_STYLES + ) { if (!descriptor || !descriptor.options) { return new StaticOrientationProperty({ orientation: 0 }, styleName); } else if (descriptor.type === StaticStyleProperty.type) { - return new StaticOrientationProperty(descriptor.options, styleName); + return new StaticOrientationProperty( + descriptor.options as OrientationStaticOptions, + styleName + ); } else if (descriptor.type === DynamicStyleProperty.type) { - const field = this._makeField(descriptor.options.field); + const options = descriptor.options as OrientationDynamicOptions; + const field = this._makeField(options.field); return new DynamicOrientationProperty( - descriptor.options, + options, styleName, field, this._layer, @@ -672,15 +823,19 @@ export class VectorStyle extends AbstractStyle { } } - _makeLabelProperty(descriptor) { + _makeLabelProperty(descriptor?: LabelStylePropertyDescriptor) { if (!descriptor || !descriptor.options) { return new StaticTextProperty({ value: '' }, VECTOR_STYLES.LABEL_TEXT); } else if (descriptor.type === StaticStyleProperty.type) { - return new StaticTextProperty(descriptor.options, VECTOR_STYLES.LABEL_TEXT); + return new StaticTextProperty( + descriptor.options as LabelStaticOptions, + VECTOR_STYLES.LABEL_TEXT + ); } else if (descriptor.type === DynamicStyleProperty.type) { - const field = this._makeField(descriptor.options.field); + const options = descriptor.options as LabelDynamicOptions; + const field = this._makeField(options.field); return new DynamicTextProperty( - descriptor.options, + options, VECTOR_STYLES.LABEL_TEXT, field, this._layer, @@ -691,15 +846,16 @@ export class VectorStyle extends AbstractStyle { } } - _makeIconProperty(descriptor) { + _makeIconProperty(descriptor?: IconStylePropertyDescriptor) { if (!descriptor || !descriptor.options) { return new StaticIconProperty({ value: DEFAULT_ICON }, VECTOR_STYLES.ICON); } else if (descriptor.type === StaticStyleProperty.type) { - return new StaticIconProperty(descriptor.options, VECTOR_STYLES.ICON); + return new StaticIconProperty(descriptor.options as IconStaticOptions, VECTOR_STYLES.ICON); } else if (descriptor.type === DynamicStyleProperty.type) { - const field = this._makeField(descriptor.options.field); + const options = descriptor.options as IconDynamicOptions; + const field = this._makeField(options.field); return new DynamicIconProperty( - descriptor.options, + options, VECTOR_STYLES.ICON, field, this._layer, diff --git a/x-pack/plugins/maps/public/connected_components/toolbar_overlay/fit_to_data/fit_to_data.tsx b/x-pack/plugins/maps/public/connected_components/toolbar_overlay/fit_to_data/fit_to_data.tsx index ca75060c4f8d..3f56d8d50b0f 100644 --- a/x-pack/plugins/maps/public/connected_components/toolbar_overlay/fit_to_data/fit_to_data.tsx +++ b/x-pack/plugins/maps/public/connected_components/toolbar_overlay/fit_to_data/fit_to_data.tsx @@ -15,24 +15,59 @@ interface Props { fitToBounds: () => void; } -export const FitToData: React.FunctionComponent = ({ layerList, fitToBounds }: Props) => { - if (layerList.length === 0) { - return null; +interface State { + canFit: boolean; +} + +export class FitToData extends React.Component { + _isMounted: boolean = false; + + state = { canFit: false }; + + componentDidMount(): void { + this._isMounted = true; + this._loadCanFit(); } - return ( - - ); -}; + componentWillUnmount(): void { + this._isMounted = false; + } + + componentDidUpdate(): void { + this._loadCanFit(); + } + + async _loadCanFit() { + const promises = this.props.layerList.map(async (layer) => { + return await layer.isFittable(); + }); + const canFit = (await Promise.all(promises)).some((isFittable) => isFittable); + if (this._isMounted && this.state.canFit !== canFit) { + this.setState({ + canFit, + }); + } + } + + render() { + if (!this.state.canFit) { + return null; + } + + return ( + + ); + } +} diff --git a/x-pack/plugins/maps/public/connected_components/toolbar_overlay/fit_to_data/index.ts b/x-pack/plugins/maps/public/connected_components/toolbar_overlay/fit_to_data/index.ts index 51bf0a519e38..8790f6f35c57 100644 --- a/x-pack/plugins/maps/public/connected_components/toolbar_overlay/fit_to_data/index.ts +++ b/x-pack/plugins/maps/public/connected_components/toolbar_overlay/fit_to_data/index.ts @@ -8,12 +8,12 @@ import { AnyAction, Dispatch } from 'redux'; import { connect } from 'react-redux'; import { MapStoreState } from '../../../reducers/store'; import { fitToDataBounds } from '../../../actions'; -import { getFittableLayers } from '../../../selectors/map_selectors'; +import { getLayerList } from '../../../selectors/map_selectors'; import { FitToData } from './fit_to_data'; function mapStateToProps(state: MapStoreState) { return { - layerList: getFittableLayers(state), + layerList: getLayerList(state), }; } diff --git a/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/toc_entry_actions_popover/toc_entry_actions_popover.test.tsx b/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/toc_entry_actions_popover/toc_entry_actions_popover.test.tsx index 95f13574105b..6c6cb6ba143c 100644 --- a/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/toc_entry_actions_popover/toc_entry_actions_popover.test.tsx +++ b/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/toc_entry_actions_popover/toc_entry_actions_popover.test.tsx @@ -9,7 +9,7 @@ import React from 'react'; import { shallow } from 'enzyme'; import { AbstractLayer, ILayer } from '../../../../../../classes/layers/layer'; import { AbstractSource, ISource } from '../../../../../../classes/sources/source'; -import { AbstractStyle, IStyle } from '../../../../../../classes/styles/style'; +import { IStyle } from '../../../../../../classes/styles/style'; import { TOCEntryActionsPopover } from './toc_entry_actions_popover'; @@ -17,7 +17,15 @@ let supportsFitToBounds: boolean; class MockSource extends AbstractSource implements ISource {} -class MockStyle extends AbstractStyle implements IStyle {} +class MockStyle implements IStyle { + renderEditor() { + return null; + } + + getType() { + return 'mockStyle'; + } +} class LayerMock extends AbstractLayer implements ILayer { constructor() { @@ -25,7 +33,7 @@ class LayerMock extends AbstractLayer implements ILayer { type: 'mySourceType', }; const source = new MockSource(sourceDescriptor); - const style = new MockStyle({ type: 'myStyleType' }); + const style = new MockStyle(); const layerDescriptor = { id: 'testLayer', sourceDescriptor, diff --git a/x-pack/plugins/maps/public/selectors/map_selectors.ts b/x-pack/plugins/maps/public/selectors/map_selectors.ts index d48ee2402756..03e0f753812c 100644 --- a/x-pack/plugins/maps/public/selectors/map_selectors.ts +++ b/x-pack/plugins/maps/public/selectors/map_selectors.ts @@ -25,7 +25,6 @@ import { InnerJoin } from '../classes/joins/inner_join'; import { getSourceByType } from '../classes/sources/source_registry'; import { GeojsonFileSource } from '../classes/sources/geojson_file_source'; import { - LAYER_TYPE, SOURCE_DATA_REQUEST_ID, STYLE_TYPE, VECTOR_STYLES, @@ -307,19 +306,6 @@ export function getLayerById(layerId: string | null, state: MapStoreState): ILay }); } -export const getFittableLayers = createSelector(getLayerList, (layerList) => { - return layerList.filter((layer) => { - // These are the only layer-types that implement bounding-box retrieval reliably - // This will _not_ work if Maps will allow register custom layer types - const isFittable = - layer.getType() === LAYER_TYPE.VECTOR || - layer.getType() === LAYER_TYPE.BLENDED_VECTOR || - layer.getType() === LAYER_TYPE.HEATMAP; - - return isFittable && layer.isVisible(); - }); -}); - export const getHiddenLayerIds = createSelector(getLayerListRaw, (layers) => layers.filter((layer) => !layer.visible).map((layer) => layer.id) ); diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/analytics_list.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/analytics_list.tsx index 81494a43193d..c4c7a8a4ca11 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/analytics_list.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/analytics_list.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { FC, useState, useEffect } from 'react'; +import React, { FC, useCallback, useState, useEffect } from 'react'; import { i18n } from '@kbn/i18n'; @@ -119,11 +119,13 @@ export const DataFrameAnalyticsList: FC = ({ } }, [selectedIdFromUrlInitialized, analytics]); + const getAnalyticsCallback = useCallback(() => getAnalytics(true), []); + // Subscribe to the refresh observable to trigger reloading the analytics list. useRefreshAnalyticsList( { isLoading: setIsLoading, - onRefresh: () => getAnalytics(true), + onRefresh: getAnalyticsCallback, }, isManagementTable ); diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/expanded_row_messages_pane.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/expanded_row_messages_pane.tsx index 0dd9eba172e1..942e335526d6 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/expanded_row_messages_pane.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/expanded_row_messages_pane.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { FC, useState } from 'react'; +import React, { FC, useState, useEffect, useCallback } from 'react'; import { i18n } from '@kbn/i18n'; import { ml } from '../../../../../services/ml_api_service'; import { useRefreshAnalyticsList } from '../../../../common'; @@ -20,45 +20,34 @@ export const ExpandedRowMessagesPane: FC = ({ analyticsId }) => { const [isLoading, setIsLoading] = useState(false); const [errorMessage, setErrorMessage] = useState(''); - const getMessagesFactory = () => { - let concurrentLoads = 0; - return async function getMessages() { - try { - concurrentLoads++; - - if (concurrentLoads > 1) { - return; - } - - setIsLoading(true); - const messagesResp = await ml.dataFrameAnalytics.getAnalyticsAuditMessages(analyticsId); - setIsLoading(false); - setMessages(messagesResp); - - concurrentLoads--; - - if (concurrentLoads > 0) { - concurrentLoads = 0; - getMessages(); - } - } catch (error) { - setIsLoading(false); - setErrorMessage( - i18n.translate('xpack.ml.dfAnalyticsList.analyticsDetails.messagesPane.errorMessage', { - defaultMessage: 'Messages could not be loaded', - }) - ); - } - }; - }; - useRefreshAnalyticsList({ onRefresh: getMessagesFactory() }); + const getMessages = useCallback(async () => { + try { + setIsLoading(true); + const messagesResp = await ml.dataFrameAnalytics.getAnalyticsAuditMessages(analyticsId); + setIsLoading(false); + setMessages(messagesResp); + } catch (error) { + setIsLoading(false); + setErrorMessage( + i18n.translate('xpack.ml.dfAnalyticsList.analyticsDetails.messagesPane.errorMessage', { + defaultMessage: 'Messages could not be loaded', + }) + ); + } + }, []); + + useEffect(() => { + getMessages(); + }, []); + + useRefreshAnalyticsList({ onRefresh: getMessages }); return ( ); }; diff --git a/x-pack/plugins/ml/public/application/datavisualizer/index_based/page.tsx b/x-pack/plugins/ml/public/application/datavisualizer/index_based/page.tsx index 3c332d305d7e..26ed3152058d 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/index_based/page.tsx +++ b/x-pack/plugins/ml/public/application/datavisualizer/index_based/page.tsx @@ -31,6 +31,7 @@ import { } from '../../../../../../../src/plugins/data/public'; import { SavedSearchSavedObject } from '../../../../common/types/kibana'; import { NavigationMenu } from '../../components/navigation_menu'; +import { DatePickerWrapper } from '../../components/navigation_menu/date_picker_wrapper'; import { ML_JOB_FIELD_TYPES } from '../../../../common/constants/field_types'; import { SEARCH_QUERY_LANGUAGE } from '../../../../common/constants/search'; import { isFullLicense } from '../../license'; @@ -655,20 +656,24 @@ export const Page: FC = () => {

{currentIndexPattern.title}

- {currentIndexPattern.timeFieldName !== undefined && ( - - - - )} + + + {currentIndexPattern.timeFieldName !== undefined && ( + + + + )} + + + + + - {showActionsPanel === true && ( - - )} diff --git a/x-pack/plugins/ml/public/application/overview/components/analytics_panel/actions.tsx b/x-pack/plugins/ml/public/application/overview/components/analytics_panel/actions.tsx index 081101e24199..c4508a8c19c5 100644 --- a/x-pack/plugins/ml/public/application/overview/components/analytics_panel/actions.tsx +++ b/x-pack/plugins/ml/public/application/overview/components/analytics_panel/actions.tsx @@ -14,6 +14,7 @@ import { getResultsUrl, DataFrameAnalyticsListRow, } from '../../../data_frame_analytics/pages/analytics_management/components/analytics_list/common'; +import { getViewLinkStatus } from '../../../data_frame_analytics/pages/analytics_management/components/action_view/get_view_link_status'; interface Props { item: DataFrameAnalyticsListRow; @@ -27,23 +28,28 @@ export const ViewLink: FC = ({ item }) => { navigateToPath(getResultsUrl(item.id, analysisType)); }, []); - const openJobsInAnomalyExplorerText = i18n.translate( + const { disabled, tooltipContent } = getViewLinkStatus(item); + + const viewJobResultsButtonText = i18n.translate( 'xpack.ml.overview.analytics.resultActions.openJobText', { defaultMessage: 'View job results', } ); + const tooltipText = disabled === false ? viewJobResultsButtonText : tooltipContent; + return ( - + {i18n.translate('xpack.ml.overview.analytics.viewActionName', { defaultMessage: 'View', diff --git a/x-pack/plugins/ml/server/models/job_service/new_job/population_chart.ts b/x-pack/plugins/ml/server/models/job_service/new_job/population_chart.ts index a9a2ce57f966..ab75787a0069 100644 --- a/x-pack/plugins/ml/server/models/job_service/new_job/population_chart.ts +++ b/x-pack/plugins/ml/server/models/job_service/new_job/population_chart.ts @@ -159,6 +159,14 @@ function getPopulationSearchJsonFromConfig( }, }; + if (query.bool === undefined) { + query.bool = { + must: [], + }; + } else if (query.bool.must === undefined) { + query.bool.must = []; + } + query.bool.must.push({ range: { [timeField]: { diff --git a/x-pack/plugins/security_solution/common/ecs/auditd/index.ts b/x-pack/plugins/security_solution/common/ecs/auditd/index.ts new file mode 100644 index 000000000000..4b170eec98c0 --- /dev/null +++ b/x-pack/plugins/security_solution/common/ecs/auditd/index.ts @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export interface AuditdEcs { + result?: string[]; + + session?: string[]; + + data?: AuditdDataEcs; + + summary?: SummaryEcs; + + sequence?: string[]; +} + +export interface AuditdDataEcs { + acct?: string[]; + + terminal?: string[]; + + op?: string[]; +} + +export interface SummaryEcs { + actor?: PrimarySecondaryEcs; + + object?: PrimarySecondaryEcs; + + how?: string[]; + + message_type?: string[]; + + sequence?: string[]; +} + +export interface PrimarySecondaryEcs { + primary?: string[]; + + secondary?: string[]; + + type?: string[]; +} diff --git a/x-pack/plugins/security_solution/common/ecs/cloud/index.ts b/x-pack/plugins/security_solution/common/ecs/cloud/index.ts new file mode 100644 index 000000000000..812b30bcc13f --- /dev/null +++ b/x-pack/plugins/security_solution/common/ecs/cloud/index.ts @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export interface CloudEcs { + instance?: CloudInstanceEcs; + machine?: CloudMachineEcs; + provider?: string[]; + region?: string[]; +} + +export interface CloudMachineEcs { + type?: string[]; +} + +export interface CloudInstanceEcs { + id?: string[]; +} diff --git a/x-pack/plugins/security_solution/common/ecs/destination/index.ts b/x-pack/plugins/security_solution/common/ecs/destination/index.ts new file mode 100644 index 000000000000..9b4038205350 --- /dev/null +++ b/x-pack/plugins/security_solution/common/ecs/destination/index.ts @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { GeoEcs } from '../geo'; + +export interface DestinationEcs { + bytes?: number[]; + + ip?: string[]; + + port?: number[]; + + domain?: string[]; + + geo?: GeoEcs; + + packets?: number[]; +} diff --git a/x-pack/plugins/security_solution/common/ecs/dns/index.ts b/x-pack/plugins/security_solution/common/ecs/dns/index.ts new file mode 100644 index 000000000000..6844cd517ace --- /dev/null +++ b/x-pack/plugins/security_solution/common/ecs/dns/index.ts @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export interface DnsEcs { + question?: DnsQuestionEcs; + + resolved_ip?: string[]; + + response_code?: string[]; +} + +export interface DnsQuestionEcs { + name?: string[]; + + type?: string[]; +} diff --git a/x-pack/plugins/security_solution/common/ecs/endgame/index.ts b/x-pack/plugins/security_solution/common/ecs/endgame/index.ts new file mode 100644 index 000000000000..f435db4f4781 --- /dev/null +++ b/x-pack/plugins/security_solution/common/ecs/endgame/index.ts @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export interface EndgameEcs { + exit_code?: number; + + file_name?: string; + + file_path?: string; + + logon_type?: number; + + parent_process_name?: string; + + pid?: number; + + process_name?: string; + + subject_domain_name?: string; + + subject_logon_id?: string; + + subject_user_name?: string; + + target_domain_name?: string; + + target_logon_id?: string; + + target_user_name?: string; +} diff --git a/x-pack/plugins/security_solution/common/ecs/event/index.ts b/x-pack/plugins/security_solution/common/ecs/event/index.ts new file mode 100644 index 000000000000..cb18a8c5881e --- /dev/null +++ b/x-pack/plugins/security_solution/common/ecs/event/index.ts @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export interface EventEcs { + action?: string[]; + + category?: string[]; + + code?: string[]; + + created?: string[]; + + dataset?: string[]; + + duration?: number[]; + + end?: string[]; + + hash?: string[]; + + id?: string[]; + + kind?: string[]; + + module?: string[]; + + original?: string[]; + + outcome?: string[]; + + risk_score?: number[]; + + risk_score_norm?: number[]; + + severity?: number[]; + + start?: string[]; + + timezone?: string[]; + + type?: string[]; +} diff --git a/x-pack/plugins/security_solution/common/ecs/file/index.ts b/x-pack/plugins/security_solution/common/ecs/file/index.ts new file mode 100644 index 000000000000..808e9eaa3c85 --- /dev/null +++ b/x-pack/plugins/security_solution/common/ecs/file/index.ts @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export interface FileEcs { + name?: string[]; + + path?: string[]; + + target_path?: string[]; + + extension?: string[]; + + type?: string[]; + + device?: string[]; + + inode?: string[]; + + uid?: string[]; + + owner?: string[]; + + gid?: string[]; + + group?: string[]; + + mode?: string[]; + + size?: number[]; + + mtime?: string[]; + + ctime?: string[]; +} diff --git a/x-pack/plugins/security_solution/common/ecs/geo/index.ts b/x-pack/plugins/security_solution/common/ecs/geo/index.ts new file mode 100644 index 000000000000..409b5bbdc17a --- /dev/null +++ b/x-pack/plugins/security_solution/common/ecs/geo/index.ts @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export interface GeoEcs { + city_name?: string[]; + + continent_name?: string[]; + + country_iso_code?: string[]; + + country_name?: string[]; + + location?: Location; + + region_iso_code?: string[]; + + region_name?: string[]; +} + +export interface Location { + lon?: number[]; + + lat?: number[]; +} diff --git a/x-pack/plugins/security_solution/common/ecs/host/index.ts b/x-pack/plugins/security_solution/common/ecs/host/index.ts new file mode 100644 index 000000000000..056291a70b62 --- /dev/null +++ b/x-pack/plugins/security_solution/common/ecs/host/index.ts @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export interface HostEcs { + architecture?: string[]; + + id?: string[]; + + ip?: string[]; + + mac?: string[]; + + name?: string[]; + + os?: OsEcs; + + type?: string[]; +} + +export interface OsEcs { + platform?: string[]; + + name?: string[]; + + full?: string[]; + + family?: string[]; + + version?: string[]; + + kernel?: string[]; +} diff --git a/x-pack/plugins/security_solution/common/ecs/http/index.ts b/x-pack/plugins/security_solution/common/ecs/http/index.ts new file mode 100644 index 000000000000..ff56d15e70bb --- /dev/null +++ b/x-pack/plugins/security_solution/common/ecs/http/index.ts @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export interface HttpEcs { + version?: string[]; + + request?: HttpRequestData; + + response?: HttpResponseData; +} + +export interface HttpRequestData { + method?: string[]; + + body?: HttpBodyData; + + referrer?: string[]; + + bytes?: number[]; +} + +export interface HttpBodyData { + content?: string[]; + + bytes?: number[]; +} + +export interface HttpResponseData { + status_code?: number[]; + + body?: HttpBodyData; + + bytes?: number[]; +} diff --git a/x-pack/plugins/security_solution/common/ecs/index.ts b/x-pack/plugins/security_solution/common/ecs/index.ts new file mode 100644 index 000000000000..ff21ebc5ef97 --- /dev/null +++ b/x-pack/plugins/security_solution/common/ecs/index.ts @@ -0,0 +1,78 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { AuditdEcs } from './auditd'; +import { DestinationEcs } from './destination'; +import { DnsEcs } from './dns'; +import { EndgameEcs } from './endgame'; +import { EventEcs } from './event'; +import { GeoEcs } from './geo'; +import { HostEcs } from './host'; +import { NetworkEcs } from './network'; +import { RuleEcs } from './rule'; +import { SignalEcs } from './signal'; +import { SourceEcs } from './source'; +import { SuricataEcs } from './suricata'; +import { TlsEcs } from './tls'; +import { ZeekEcs } from './zeek'; +import { HttpEcs } from './http'; +import { UrlEcs } from './url'; +import { UserEcs } from './user'; +import { WinlogEcs } from './winlog'; +import { ProcessEcs } from './process'; +import { SystemEcs } from './system'; + +export interface Ecs { + _id: string; + + _index?: string; + + auditd?: AuditdEcs; + + destination?: DestinationEcs; + + dns?: DnsEcs; + + endgame?: EndgameEcs; + + event?: EventEcs; + + geo?: GeoEcs; + + host?: HostEcs; + + network?: NetworkEcs; + + rule?: RuleEcs; + + signal?: SignalEcs; + + source?: SourceEcs; + + suricata?: SuricataEcs; + + tls?: TlsEcs; + + zeek?: ZeekEcs; + + http?: HttpEcs; + + url?: UrlEcs; + + timestamp?: string; + + message?: string[]; + + user?: UserEcs; + + winlog?: WinlogEcs; + + process?: ProcessEcs; + + file?: File; + + system?: SystemEcs; +} diff --git a/x-pack/plugins/security_solution/common/ecs/network/index.ts b/x-pack/plugins/security_solution/common/ecs/network/index.ts new file mode 100644 index 000000000000..c2fc3cb4b9f4 --- /dev/null +++ b/x-pack/plugins/security_solution/common/ecs/network/index.ts @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export interface NetworkEcs { + bytes?: number[]; + + community_id?: string[]; + + direction?: string[]; + + packets?: number[]; + + protocol?: string[]; + + transport?: string[]; +} diff --git a/x-pack/plugins/security_solution/common/ecs/process/index.ts b/x-pack/plugins/security_solution/common/ecs/process/index.ts new file mode 100644 index 000000000000..0584d95c8059 --- /dev/null +++ b/x-pack/plugins/security_solution/common/ecs/process/index.ts @@ -0,0 +1,39 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export interface ProcessEcs { + hash?: ProcessHashData; + + pid?: number[]; + + name?: string[]; + + ppid?: number[]; + + args?: string[]; + + executable?: string[]; + + title?: string[]; + + thread?: Thread; + + working_directory?: string[]; +} + +export interface ProcessHashData { + md5?: string[]; + + sha1?: string[]; + + sha256?: string[]; +} + +export interface Thread { + id?: number[]; + + start?: string[]; +} diff --git a/x-pack/plugins/security_solution/common/ecs/rule/index.ts b/x-pack/plugins/security_solution/common/ecs/rule/index.ts new file mode 100644 index 000000000000..c1ef1ee17ca0 --- /dev/null +++ b/x-pack/plugins/security_solution/common/ecs/rule/index.ts @@ -0,0 +1,69 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export interface RuleEcs { + id?: string[]; + + rule_id?: string[]; + + false_positives: string[]; + + saved_id?: string[]; + + timeline_id?: string[]; + + timeline_title?: string[]; + + max_signals?: number[]; + + risk_score?: string[]; + + output_index?: string[]; + + description?: string[]; + + from?: string[]; + + immutable?: boolean[]; + + index?: string[]; + + interval?: string[]; + + language?: string[]; + + query?: string[]; + + references?: string[]; + + severity?: string[]; + + tags?: string[]; + + threat?: unknown; + + type?: string[]; + + size?: string[]; + + to?: string[]; + + enabled?: boolean[]; + + filters?: unknown; + + created_at?: string[]; + + updated_at?: string[]; + + created_by?: string[]; + + updated_by?: string[]; + + version?: string[]; + + note?: string[]; +} diff --git a/x-pack/plugins/security_solution/common/ecs/signal/index.ts b/x-pack/plugins/security_solution/common/ecs/signal/index.ts new file mode 100644 index 000000000000..66e35e26af34 --- /dev/null +++ b/x-pack/plugins/security_solution/common/ecs/signal/index.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { RuleEcs } from '../rule'; + +export interface SignalEcs { + rule?: RuleEcs; + + original_time?: string[]; +} diff --git a/x-pack/plugins/security_solution/common/ecs/source/index.ts b/x-pack/plugins/security_solution/common/ecs/source/index.ts new file mode 100644 index 000000000000..9e6b6563cec6 --- /dev/null +++ b/x-pack/plugins/security_solution/common/ecs/source/index.ts @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { GeoEcs } from '../geo'; + +export interface SourceEcs { + bytes?: number[]; + + ip?: string[]; + + port?: number[]; + + domain?: string[]; + + geo?: GeoEcs; + + packets?: number[]; +} diff --git a/x-pack/plugins/security_solution/common/ecs/suricata/index.ts b/x-pack/plugins/security_solution/common/ecs/suricata/index.ts new file mode 100644 index 000000000000..53c193edddaf --- /dev/null +++ b/x-pack/plugins/security_solution/common/ecs/suricata/index.ts @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export interface SuricataEcs { + eve?: SuricataEveData; +} + +export interface SuricataEveData { + alert?: SuricataAlertData; + + flow_id?: number[]; + + proto?: string[]; +} + +export interface SuricataAlertData { + signature?: string[]; + + signature_id?: number[]; +} diff --git a/x-pack/plugins/security_solution/common/ecs/system/index.ts b/x-pack/plugins/security_solution/common/ecs/system/index.ts new file mode 100644 index 000000000000..803d8197080f --- /dev/null +++ b/x-pack/plugins/security_solution/common/ecs/system/index.ts @@ -0,0 +1,39 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export interface SystemEcs { + audit?: AuditEcs; + + auth?: AuthEcs; +} + +export interface AuditEcs { + package?: PackageEcs; +} + +export interface PackageEcs { + arch?: string[]; + + entity_id?: string[]; + + name?: string[]; + + size?: number[]; + + summary?: string[]; + + version?: string[]; +} + +export interface AuthEcs { + ssh?: SshEcs; +} + +export interface SshEcs { + method?: string[]; + + signature?: string[]; +} diff --git a/x-pack/plugins/security_solution/common/ecs/tls/index.ts b/x-pack/plugins/security_solution/common/ecs/tls/index.ts new file mode 100644 index 000000000000..86a2a1a9459a --- /dev/null +++ b/x-pack/plugins/security_solution/common/ecs/tls/index.ts @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export interface TlsEcs { + client_certificate?: TlsClientCertificateData; + + fingerprints?: TlsFingerprintsData; + + server_certificate?: TlsServerCertificateData; +} + +export interface TlsClientCertificateData { + fingerprint?: FingerprintData; +} + +export interface FingerprintData { + sha1?: string[]; +} + +export interface TlsFingerprintsData { + ja3?: TlsJa3Data; +} + +export interface TlsJa3Data { + hash?: string[]; +} + +export interface TlsServerCertificateData { + fingerprint?: FingerprintData; +} diff --git a/x-pack/plugins/security_solution/common/ecs/url/index.ts b/x-pack/plugins/security_solution/common/ecs/url/index.ts new file mode 100644 index 000000000000..66033ea9f072 --- /dev/null +++ b/x-pack/plugins/security_solution/common/ecs/url/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; + * you may not use this file except in compliance with the Elastic License. + */ + +export interface UrlEcs { + domain?: string[]; + + original?: string[]; + + username?: string[]; + + password?: string[]; +} diff --git a/x-pack/plugins/security_solution/common/ecs/user/index.ts b/x-pack/plugins/security_solution/common/ecs/user/index.ts new file mode 100644 index 000000000000..d72362d5f5cf --- /dev/null +++ b/x-pack/plugins/security_solution/common/ecs/user/index.ts @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export interface UserEcs { + domain?: string[]; + + id?: string[]; + + name?: string[]; + + full_name?: string[]; + + email?: string[]; + + hash?: string[]; + + group?: string[]; +} diff --git a/x-pack/plugins/security_solution/common/ecs/winlog/index.ts b/x-pack/plugins/security_solution/common/ecs/winlog/index.ts new file mode 100644 index 000000000000..a449fb9130e6 --- /dev/null +++ b/x-pack/plugins/security_solution/common/ecs/winlog/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export interface WinlogEcs { + event_id?: number[]; +} diff --git a/x-pack/plugins/security_solution/common/ecs/zeek/index.ts b/x-pack/plugins/security_solution/common/ecs/zeek/index.ts new file mode 100644 index 000000000000..289390a87db1 --- /dev/null +++ b/x-pack/plugins/security_solution/common/ecs/zeek/index.ts @@ -0,0 +1,133 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export interface ZeekEcs { + session_id?: string[]; + + connection?: ZeekConnectionData; + + notice?: ZeekNoticeData; + + dns?: ZeekDnsData; + + http?: ZeekHttpData; + + files?: ZeekFileData; + + ssl?: ZeekSslData; +} + +export interface ZeekConnectionData { + local_resp?: boolean[]; + + local_orig?: boolean[]; + + missed_bytes?: number[]; + + state?: string[]; + + history?: string[]; +} + +export interface ZeekNoticeData { + suppress_for?: number[]; + + msg?: string[]; + + note?: string[]; + + sub?: string[]; + + dst?: string[]; + + dropped?: boolean[]; + + peer_descr?: string[]; +} + +export interface ZeekDnsData { + AA?: boolean[]; + + qclass_name?: string[]; + + RD?: boolean[]; + + qtype_name?: string[]; + + rejected?: boolean[]; + + qtype?: string[]; + + query?: string[]; + + trans_id?: number[]; + + qclass?: string[]; + + RA?: boolean[]; + + TC?: boolean[]; +} + +export interface ZeekHttpData { + resp_mime_types?: string[]; + + trans_depth?: string[]; + + status_msg?: string[]; + + resp_fuids?: string[]; + + tags?: string[]; +} + +export interface ZeekFileData { + session_ids?: string[]; + + timedout?: boolean[]; + + local_orig?: boolean[]; + + tx_host?: string[]; + + source?: string[]; + + is_orig?: boolean[]; + + overflow_bytes?: number[]; + + sha1?: string[]; + + duration?: number[]; + + depth?: number[]; + + analyzers?: string[]; + + mime_type?: string[]; + + rx_host?: string[]; + + total_bytes?: number[]; + + fuid?: string[]; + + seen_bytes?: number[]; + + missing_bytes?: number[]; + + md5?: string[]; +} + +export interface ZeekSslData { + cipher?: string[]; + + established?: boolean[]; + + resumed?: boolean[]; + + version?: string[]; +} diff --git a/x-pack/plugins/security_solution/common/endpoint/generate_data.ts b/x-pack/plugins/security_solution/common/endpoint/generate_data.ts index 0a6473b83386..7340b1c021eb 100644 --- a/x-pack/plugins/security_solution/common/endpoint/generate_data.ts +++ b/x-pack/plugins/security_solution/common/endpoint/generate_data.ts @@ -1158,6 +1158,9 @@ export class EndpointDocGenerator { version: '0.5.0', internal: false, removable: false, + install_version: '0.5.0', + install_status: 'installed', + install_started_at: '2020-06-24T14:41:23.098Z', }, references: [], updated_at: '2020-06-24T14:41:23.098Z', diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/index.ts new file mode 100644 index 000000000000..3a0942d2decb --- /dev/null +++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/index.ts @@ -0,0 +1,84 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { IEsSearchResponse } from '../../../../../../../src/plugins/data/common'; +import { CloudEcs } from '../../../ecs/cloud'; +import { HostEcs } from '../../../ecs/host'; + +import { + CursorType, + Inspect, + Maybe, + PageInfoPaginated, + RequestOptionsPaginated, + SortField, + TimerangeInput, +} from '..'; + +export enum HostsQueries { + hosts = 'hosts', + hostOverview = 'hostOverview', +} + +export enum HostPolicyResponseActionStatus { + success = 'success', + failure = 'failure', + warning = 'warning', +} + +export interface EndpointFields { + endpointPolicy?: Maybe; + + sensorVersion?: Maybe; + + policyStatus?: Maybe; +} + +export interface HostItem { + _id?: Maybe; + + cloud?: Maybe; + + endpoint?: Maybe; + + host?: Maybe; + + lastSeen?: Maybe; +} + +export interface HostsEdges { + node: HostItem; + + cursor: CursorType; +} + +export interface HostsStrategyResponse extends IEsSearchResponse { + edges: HostsEdges[]; + + totalCount: number; + + pageInfo: PageInfoPaginated; + + inspect?: Maybe; +} + +export interface HostOverviewStrategyResponse extends IEsSearchResponse, HostItem { + inspect?: Maybe; +} + +export interface HostsRequestOptions extends RequestOptionsPaginated { + sort: SortField; + defaultIndex: string[]; +} + +export interface HostLastFirstSeenRequestOptions extends Partial { + hostName: string; +} + +export interface HostOverviewRequestOptions extends HostLastFirstSeenRequestOptions { + fields: string[]; + timerange: TimerangeInput; +} diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/index.ts new file mode 100644 index 000000000000..edb5dda2ca6d --- /dev/null +++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/index.ts @@ -0,0 +1,109 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { IEsSearchRequest } from '../../../../../../src/plugins/data/common'; +import { ESQuery } from '../../typed_json'; +import { + HostOverviewStrategyResponse, + HostOverviewRequestOptions, + HostsQueries, + HostsRequestOptions, + HostsStrategyResponse, +} from './hosts'; +export * from './hosts'; +export type Maybe = T | null; + +export type FactoryQueryTypes = HostsQueries; + +export interface Inspect { + dsl: string[]; + response: string[]; +} + +export interface PageInfoPaginated { + activePage: number; + fakeTotalCount: number; + showMorePagesIndicator: boolean; +} + +export interface CursorType { + value?: Maybe; + tiebreaker?: Maybe; +} + +export enum Direction { + asc = 'asc', + desc = 'desc', +} + +export interface SortField { + field: string; + direction: Direction; +} + +export interface TimerangeInput { + /** The interval string to use for last bucket. The format is '{value}{unit}'. For example '5m' would return the metrics for the last 5 minutes of the timespan. */ + interval: string; + /** The end of the timerange */ + to: string; + /** The beginning of the timerange */ + from: string; +} + +export interface PaginationInput { + /** The limit parameter allows you to configure the maximum amount of items to be returned */ + limit: number; + /** The cursor parameter defines the next result you want to fetch */ + cursor?: Maybe; + /** The tiebreaker parameter allow to be more precise to fetch the next item */ + tiebreaker?: Maybe; +} + +export interface PaginationInputPaginated { + /** The activePage parameter defines the page of results you want to fetch */ + activePage: number; + /** The cursorStart parameter defines the start of the results to be displayed */ + cursorStart: number; + /** The fakePossibleCount parameter determines the total count in order to show 5 additional pages */ + fakePossibleCount: number; + /** The querySize parameter is the number of items to be returned */ + querySize: number; +} + +export interface DocValueFields { + field: string; + format: string; +} + +export interface RequestBasicOptions extends IEsSearchRequest { + timerange: TimerangeInput; + filterQuery: ESQuery | string | undefined; + defaultIndex: string[]; + docValueFields?: DocValueFields[]; + factoryQueryType?: FactoryQueryTypes; +} + +export interface RequestOptions extends RequestBasicOptions { + pagination: PaginationInput; + sortField?: SortField; +} + +export interface RequestOptionsPaginated extends RequestBasicOptions { + pagination: PaginationInputPaginated; + sortField?: SortField; +} + +export type StrategyResponseType = T extends HostsQueries.hosts + ? HostsStrategyResponse + : T extends HostsQueries.hostOverview + ? HostOverviewStrategyResponse + : never; + +export type StrategyRequestType = T extends HostsQueries.hosts + ? HostsRequestOptions + : T extends HostsQueries.hostOverview + ? HostOverviewRequestOptions + : never; diff --git a/x-pack/plugins/security_solution/cypress/integration/timeline_local_storage.spec.ts b/x-pack/plugins/security_solution/cypress/integration/timeline_local_storage.spec.ts index 383ebe222058..c2ff2c58687f 100644 --- a/x-pack/plugins/security_solution/cypress/integration/timeline_local_storage.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/timeline_local_storage.spec.ts @@ -13,7 +13,8 @@ import { TABLE_COLUMN_EVENTS_MESSAGE } from '../screens/hosts/external_events'; import { waitsForEventsToBeLoaded, openEventsViewerFieldsBrowser } from '../tasks/hosts/events'; import { removeColumn, resetFields } from '../tasks/timeline'; -describe('persistent timeline', () => { +// FLAKY: https://github.com/elastic/kibana/issues/75794 +describe.skip('persistent timeline', () => { before(() => { loginAndWaitForPage(HOSTS_URL); openEvents(); diff --git a/x-pack/plugins/security_solution/cypress/integration/url_compatibility.spec.ts b/x-pack/plugins/security_solution/cypress/integration/url_compatibility.spec.ts index 5b42897b065e..d55a8faae021 100644 --- a/x-pack/plugins/security_solution/cypress/integration/url_compatibility.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/url_compatibility.spec.ts @@ -18,7 +18,8 @@ const ABSOLUTE_DATE = { startTime: '2019-08-01T20:03:29.186Z', }; -describe('URL compatibility', () => { +// FLAKY: https://github.com/elastic/kibana/issues/75697 +describe.skip('URL compatibility', () => { it('Redirects to Detection alerts from old Detections URL', () => { loginAndWaitForPage(DETECTIONS); diff --git a/x-pack/plugins/security_solution/cypress/integration/url_state.spec.ts b/x-pack/plugins/security_solution/cypress/integration/url_state.spec.ts index cdcdde252d6d..6d605e1d577a 100644 --- a/x-pack/plugins/security_solution/cypress/integration/url_state.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/url_state.spec.ts @@ -36,6 +36,7 @@ import { addNameToTimeline, closeTimeline, executeTimelineKQL, + waitForTimelineChanges, } from '../tasks/timeline'; import { HOSTS_URL } from '../urls/navigation'; @@ -217,7 +218,7 @@ describe('url state', () => { cy.get(KQL_INPUT).invoke('text').should('eq', 'source.ip: "10.142.0.9"'); }); - it.skip('sets and reads the url state for timeline by id', () => { + it('sets and reads the url state for timeline by id', () => { loginAndWaitForPage(HOSTS_URL); openTimeline(); executeTimelineKQL('host.name: *'); @@ -229,20 +230,24 @@ describe('url state', () => { cy.wrap(intCount).should('be.above', 0); }); + cy.server(); + cy.route('PATCH', '**/api/timeline').as('timeline'); + const timelineName = 'Security'; + const timelineDescription = 'This is the best timeline of the world'; addNameToTimeline(timelineName); - addDescriptionToTimeline('This is the best timeline of the world'); - cy.wait(5000); - - cy.url({ timeout: 30000 }).should('match', /\w*-\w*-\w*-\w*-\w*/); - cy.url().then((url) => { - const matched = url.match(/\w*-\w*-\w*-\w*-\w*/); - const newTimelineId = matched && matched.length > 0 ? matched[0] : 'null'; - expect(matched).to.have.lengthOf(1); + waitForTimelineChanges(); + addDescriptionToTimeline(timelineDescription); + waitForTimelineChanges(); + + cy.wait('@timeline').then((response) => { closeTimeline(); + cy.wrap(response.status).should('eql', 200); + const JsonResponse = JSON.parse(response.xhr.responseText); + const timelineId = JsonResponse.data.persistTimeline.timeline.savedObjectId; cy.visit('/app/home'); - cy.visit(`/app/security/timelines?timeline=(id:%27${newTimelineId}%27,isOpen:!t)`); - cy.contains('a', 'Security'); + cy.visit(`/app/security/timelines?timeline=(id:'${timelineId}',isOpen:!t)`); + cy.get(DATE_PICKER_APPLY_BUTTON_TIMELINE).should('exist'); cy.get(DATE_PICKER_APPLY_BUTTON_TIMELINE).invoke('text').should('not.equal', 'Updating'); cy.get(TIMELINE_TITLE).should('be.visible'); cy.get(TIMELINE_TITLE).should('have.attr', 'value', timelineName); diff --git a/x-pack/plugins/security_solution/cypress/screens/timeline.ts b/x-pack/plugins/security_solution/cypress/screens/timeline.ts index 135dea35ca0d..26203a8ca3b8 100644 --- a/x-pack/plugins/security_solution/cypress/screens/timeline.ts +++ b/x-pack/plugins/security_solution/cypress/screens/timeline.ts @@ -39,6 +39,8 @@ export const TIMELINE = (id: string) => { return `[data-test-subj="title-${id}"]`; }; +export const TIMELINE_CHANGES_IN_PROGRESS = '[data-test-subj="timeline"] .euiProgress'; + export const TIMELINE_COLUMN_SPINNER = '[data-test-subj="timeline-loading-spinner"]'; export const TIMELINE_DATA_PROVIDERS = '[data-test-subj="dataProviders"]'; diff --git a/x-pack/plugins/security_solution/cypress/tasks/timeline.ts b/x-pack/plugins/security_solution/cypress/tasks/timeline.ts index 9eeb9fc8bdf8..08624df06e09 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/timeline.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/timeline.ts @@ -4,8 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { DATE_PICKER_APPLY_BUTTON_TIMELINE } from '../screens/date_picker'; - import { CLOSE_TIMELINE_BTN, CREATE_NEW_TIMELINE, @@ -16,6 +14,7 @@ import { PIN_EVENT, SEARCH_OR_FILTER_CONTAINER, SERVER_SIDE_EVENT_COUNT, + TIMELINE_CHANGES_IN_PROGRESS, TIMELINE_DESCRIPTION, TIMELINE_FIELDS_BUTTON, TIMELINE_INSPECT_BUTTON, @@ -33,7 +32,7 @@ export const hostExistsQuery = 'host.name: *'; export const addDescriptionToTimeline = (description: string) => { cy.get(TIMELINE_DESCRIPTION).type(`${description}{enter}`); - cy.get(DATE_PICKER_APPLY_BUTTON_TIMELINE).click().invoke('text').should('not.equal', 'Updating'); + cy.get(TIMELINE_DESCRIPTION).should('have.attr', 'value', description); }; export const addNameToTimeline = (name: string) => { @@ -122,3 +121,8 @@ export const removeColumn = (column: number) => { export const resetFields = () => { cy.get(RESET_FIELDS).click({ force: true }); }; + +export const waitForTimelineChanges = () => { + cy.get(TIMELINE_CHANGES_IN_PROGRESS).should('exist'); + cy.get(TIMELINE_CHANGES_IN_PROGRESS).should('not.exist'); +}; diff --git a/x-pack/plugins/security_solution/public/cases/components/all_cases/index.test.tsx b/x-pack/plugins/security_solution/public/cases/components/all_cases/index.test.tsx index f5ed151ebac3..e6e082321419 100644 --- a/x-pack/plugins/security_solution/public/cases/components/all_cases/index.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/all_cases/index.test.tsx @@ -15,7 +15,6 @@ import { useGetCasesMockState } from '../../containers/mock'; import * as i18n from './translations'; import { useKibana } from '../../../common/lib/kibana'; -import { createUseKibanaMock } from '../../../common/mock/kibana_react'; import { getEmptyTagValue } from '../../../common/components/empty_value'; import { useDeleteCases } from '../../containers/use_delete_cases'; import { useGetCases } from '../../containers/use_get_cases'; @@ -28,7 +27,7 @@ jest.mock('../../containers/use_delete_cases'); jest.mock('../../containers/use_get_cases'); jest.mock('../../containers/use_get_cases_status'); -const useKibanaMock = useKibana as jest.Mock; +const useKibanaMock = useKibana as jest.Mocked; const useDeleteCasesMock = useDeleteCases as jest.Mock; const useGetCasesMock = useGetCases as jest.Mock; const useGetCasesStatusMock = useGetCasesStatus as jest.Mock; @@ -97,23 +96,16 @@ describe('AllCases', () => { }); /* eslint-enable no-console */ beforeEach(() => { - jest.resetAllMocks(); + jest.clearAllMocks(); navigateToApp = jest.fn(); - const kibanaMock = createUseKibanaMock()(); - useKibanaMock.mockReturnValue({ - ...kibanaMock, - services: { - application: { - navigateToApp, - }, - }, - }); + useKibanaMock().services.application.navigateToApp = navigateToApp; useUpdateCasesMock.mockReturnValue(defaultUpdateCases); useGetCasesMock.mockReturnValue(defaultGetCases); useDeleteCasesMock.mockReturnValue(defaultDeleteCases); useGetCasesStatusMock.mockReturnValue(defaultCasesStatus); moment.tz.setDefault('UTC'); }); + it('should render AllCases', () => { const wrapper = mount( diff --git a/x-pack/plugins/security_solution/public/cases/components/configure_cases/__mock__/index.tsx b/x-pack/plugins/security_solution/public/cases/components/configure_cases/__mock__/index.tsx index 23c76953a6a0..08303ddc9397 100644 --- a/x-pack/plugins/security_solution/public/cases/components/configure_cases/__mock__/index.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/configure_cases/__mock__/index.tsx @@ -8,10 +8,7 @@ import { Connector } from '../../../containers/configure/types'; import { ReturnConnectors } from '../../../containers/configure/use_connectors'; import { connectorsMock } from '../../../containers/configure/mock'; import { ReturnUseCaseConfigure } from '../../../containers/configure/use_configure'; -import { createUseKibanaMock } from '../../../../common/mock/kibana_react'; export { mapping } from '../../../containers/configure/mock'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { actionTypeRegistryMock } from '../../../../../../triggers_actions_ui/public/application/action_type_registry.mock'; export const connectors: Connector[] = connectorsMock; @@ -46,10 +43,3 @@ export const useConnectorsResponse: ReturnConnectors = { connectors, refetchConnectors: jest.fn(), }; - -export const kibanaMockImplementationArgs = { - services: { - ...createUseKibanaMock()().services, - triggers_actions_ui: { actionTypeRegistry: actionTypeRegistryMock.create() }, - }, -}; diff --git a/x-pack/plugins/security_solution/public/cases/components/configure_cases/index.test.tsx b/x-pack/plugins/security_solution/public/cases/components/configure_cases/index.test.tsx index 7974116f4dc4..3c17a9191d20 100644 --- a/x-pack/plugins/security_solution/public/cases/components/configure_cases/index.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/configure_cases/index.test.tsx @@ -15,38 +15,39 @@ import { ActionsConnectorsContextProvider, ConnectorAddFlyout, ConnectorEditFlyout, + TriggersAndActionsUIPublicPluginStart, } from '../../../../../triggers_actions_ui/public'; +import { actionTypeRegistryMock } from '../../../../../triggers_actions_ui/public/application/action_type_registry.mock'; import { useKibana } from '../../../common/lib/kibana'; import { useConnectors } from '../../containers/configure/use_connectors'; import { useCaseConfigure } from '../../containers/configure/use_configure'; import { useGetUrlSearch } from '../../../common/components/navigation/use_get_url_search'; -import { - connectors, - searchURL, - useCaseConfigureResponse, - useConnectorsResponse, - kibanaMockImplementationArgs, -} from './__mock__'; +import { connectors, searchURL, useCaseConfigureResponse, useConnectorsResponse } from './__mock__'; jest.mock('../../../common/lib/kibana'); jest.mock('../../containers/configure/use_connectors'); jest.mock('../../containers/configure/use_configure'); jest.mock('../../../common/components/navigation/use_get_url_search'); -const useKibanaMock = useKibana as jest.Mock; +const useKibanaMock = useKibana as jest.Mocked; const useConnectorsMock = useConnectors as jest.Mock; const useCaseConfigureMock = useCaseConfigure as jest.Mock; const useGetUrlSearchMock = useGetUrlSearch as jest.Mock; + describe('ConfigureCases', () => { + beforeEach(() => { + useKibanaMock().services.triggers_actions_ui = ({ + actionTypeRegistry: actionTypeRegistryMock.create(), + } as unknown) as TriggersAndActionsUIPublicPluginStart; + }); + describe('rendering', () => { let wrapper: ReactWrapper; beforeEach(() => { - jest.resetAllMocks(); useCaseConfigureMock.mockImplementation(() => useCaseConfigureResponse); useConnectorsMock.mockImplementation(() => ({ ...useConnectorsResponse, connectors: [] })); - useKibanaMock.mockImplementation(() => kibanaMockImplementationArgs); useGetUrlSearchMock.mockImplementation(() => searchURL); wrapper = mount(, { wrappingComponent: TestProviders }); @@ -84,8 +85,8 @@ describe('ConfigureCases', () => { describe('Unhappy path', () => { let wrapper: ReactWrapper; + beforeEach(() => { - jest.resetAllMocks(); useCaseConfigureMock.mockImplementation(() => ({ ...useCaseConfigureResponse, closureType: 'close-by-user', @@ -98,7 +99,6 @@ describe('ConfigureCases', () => { }, })); useConnectorsMock.mockImplementation(() => ({ ...useConnectorsResponse, connectors: [] })); - useKibanaMock.mockImplementation(() => kibanaMockImplementationArgs); useGetUrlSearchMock.mockImplementation(() => searchURL); wrapper = mount(, { wrappingComponent: TestProviders }); }); @@ -122,7 +122,6 @@ describe('ConfigureCases', () => { let wrapper: ReactWrapper; beforeEach(() => { - jest.resetAllMocks(); useCaseConfigureMock.mockImplementation(() => ({ ...useCaseConfigureResponse, mapping: connectors[0].config.incidentConfiguration.mapping, @@ -136,7 +135,6 @@ describe('ConfigureCases', () => { }, })); useConnectorsMock.mockImplementation(() => useConnectorsResponse); - useKibanaMock.mockImplementation(() => kibanaMockImplementationArgs); useGetUrlSearchMock.mockImplementation(() => searchURL); wrapper = mount(, { wrappingComponent: TestProviders }); @@ -211,9 +209,6 @@ describe('ConfigureCases', () => { let wrapper: ReactWrapper; beforeEach(() => { - jest.resetAllMocks(); - jest.restoreAllMocks(); - jest.clearAllMocks(); useCaseConfigureMock.mockImplementation(() => ({ ...useCaseConfigureResponse, mapping: connectors[1].config.incidentConfiguration.mapping, @@ -230,7 +225,6 @@ describe('ConfigureCases', () => { ...useConnectorsResponse, loading: true, })); - useKibanaMock.mockImplementation(() => kibanaMockImplementationArgs); useGetUrlSearchMock.mockImplementation(() => searchURL); wrapper = mount(, { wrappingComponent: TestProviders }); }); @@ -262,7 +256,6 @@ describe('ConfigureCases', () => { let wrapper: ReactWrapper; beforeEach(() => { - jest.resetAllMocks(); useCaseConfigureMock.mockImplementation(() => ({ ...useCaseConfigureResponse, connectorId: 'servicenow-1', @@ -270,7 +263,6 @@ describe('ConfigureCases', () => { })); useConnectorsMock.mockImplementation(() => useConnectorsResponse); - useKibanaMock.mockImplementation(() => kibanaMockImplementationArgs); useGetUrlSearchMock.mockImplementation(() => searchURL); wrapper = mount(, { wrappingComponent: TestProviders }); }); @@ -305,7 +297,6 @@ describe('ConfigureCases', () => { let wrapper: ReactWrapper; beforeEach(() => { - jest.resetAllMocks(); useCaseConfigureMock.mockImplementation(() => ({ ...useCaseConfigureResponse, loading: true, @@ -313,7 +304,6 @@ describe('ConfigureCases', () => { useConnectorsMock.mockImplementation(() => ({ ...useConnectorsResponse, })); - useKibanaMock.mockImplementation(() => kibanaMockImplementationArgs); useGetUrlSearchMock.mockImplementation(() => searchURL); wrapper = mount(, { wrappingComponent: TestProviders }); }); @@ -329,10 +319,10 @@ describe('ConfigureCases', () => { describe('connectors', () => { let wrapper: ReactWrapper; - const persistCaseConfigure = jest.fn(); + let persistCaseConfigure: jest.Mock; beforeEach(() => { - jest.resetAllMocks(); + persistCaseConfigure = jest.fn(); useCaseConfigureMock.mockImplementation(() => ({ ...useCaseConfigureResponse, mapping: connectors[0].config.incidentConfiguration.mapping, @@ -347,7 +337,6 @@ describe('ConfigureCases', () => { persistCaseConfigure, })); useConnectorsMock.mockImplementation(() => useConnectorsResponse); - useKibanaMock.mockImplementation(() => kibanaMockImplementationArgs); useGetUrlSearchMock.mockImplementation(() => searchURL); wrapper = mount(, { wrappingComponent: TestProviders }); @@ -396,10 +385,10 @@ describe('ConfigureCases', () => { describe('closure options', () => { let wrapper: ReactWrapper; - const persistCaseConfigure = jest.fn(); + let persistCaseConfigure: jest.Mock; beforeEach(() => { - jest.resetAllMocks(); + persistCaseConfigure = jest.fn(); useCaseConfigureMock.mockImplementation(() => ({ ...useCaseConfigureResponse, mapping: connectors[0].config.incidentConfiguration.mapping, @@ -414,7 +403,6 @@ describe('closure options', () => { persistCaseConfigure, })); useConnectorsMock.mockImplementation(() => useConnectorsResponse); - useKibanaMock.mockImplementation(() => kibanaMockImplementationArgs); useGetUrlSearchMock.mockImplementation(() => searchURL); wrapper = mount(, { wrappingComponent: TestProviders }); @@ -435,7 +423,6 @@ describe('closure options', () => { describe('user interactions', () => { beforeEach(() => { - jest.resetAllMocks(); useCaseConfigureMock.mockImplementation(() => ({ ...useCaseConfigureResponse, mapping: connectors[1].config.incidentConfiguration.mapping, @@ -449,7 +436,6 @@ describe('user interactions', () => { }, })); useConnectorsMock.mockImplementation(() => useConnectorsResponse); - useKibanaMock.mockImplementation(() => kibanaMockImplementationArgs); useGetUrlSearchMock.mockImplementation(() => searchURL); }); diff --git a/x-pack/plugins/security_solution/public/cases/components/use_all_cases_modal/index.test.tsx b/x-pack/plugins/security_solution/public/cases/components/use_all_cases_modal/index.test.tsx index b5bf68cbf6dc..3b203e81cd07 100644 --- a/x-pack/plugins/security_solution/public/cases/components/use_all_cases_modal/index.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/use_all_cases_modal/index.test.tsx @@ -14,26 +14,17 @@ import '../../../common/mock/match_media'; import { TimelineId } from '../../../../common/types/timeline'; import { useAllCasesModal, UseAllCasesModalProps, UseAllCasesModalReturnedValues } from '.'; import { TestProviders } from '../../../common/mock'; -import { createUseKibanaMock } from '../../../common/mock/kibana_react'; jest.mock('../../../common/lib/kibana'); -const useKibanaMock = useKibana as jest.Mock; +const useKibanaMock = useKibana as jest.Mocked; describe('useAllCasesModal', () => { - const navigateToApp = jest.fn(() => Promise.resolve()); + let navigateToApp: jest.Mock; beforeEach(() => { - jest.clearAllMocks(); - const kibanaMock = createUseKibanaMock()(); - useKibanaMock.mockImplementation(() => ({ - ...kibanaMock, - services: { - application: { - navigateToApp, - }, - }, - })); + navigateToApp = jest.fn(); + useKibanaMock().services.application.navigateToApp = navigateToApp; }); it('init', async () => { diff --git a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper_hover_content.test.tsx b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper_hover_content.test.tsx index 8e76a88572e4..b53da42da55f 100644 --- a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper_hover_content.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper_hover_content.test.tsx @@ -7,12 +7,12 @@ import { mount, ReactWrapper } from 'enzyme'; import React from 'react'; +import { coreMock } from '../../../../../../../src/core/public/mocks'; import { useWithSource } from '../../containers/source'; import { mockBrowserFields } from '../../containers/source/mock'; import '../../mock/match_media'; import { useKibana } from '../../lib/kibana'; import { TestProviders } from '../../mock'; -import { createKibanaCoreStartMock } from '../../mock/kibana_core'; import { FilterManager } from '../../../../../../../src/plugins/data/public'; import { useAddToTimeline } from '../../hooks/use_add_to_timeline'; @@ -60,7 +60,7 @@ jest.mock('../../../timelines/components/manage_timeline', () => { }; }); -const mockUiSettingsForFilterManager = createKibanaCoreStartMock().uiSettings; +const mockUiSettingsForFilterManager = coreMock.createStart().uiSettings; const timelineId = TimelineId.active; const field = 'process.name'; const value = 'nice'; diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.test.tsx index 2b713636862b..cef92ce2a781 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.test.tsx @@ -11,14 +11,13 @@ import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; import { act } from 'react-dom/test-utils'; import { AddExceptionModal } from './'; -import { useKibana, useCurrentUser } from '../../../../common/lib/kibana'; +import { useCurrentUser } from '../../../../common/lib/kibana'; import { getExceptionListSchemaMock } from '../../../../../../lists/common/schemas/response/exception_list_schema.mock'; import { useFetchIndexPatterns } from '../../../../detections/containers/detection_engine/rules'; import { stubIndexPattern } from 'src/plugins/data/common/index_patterns/index_pattern.stub'; import { useAddOrUpdateException } from '../use_add_exception'; import { useFetchOrCreateRuleExceptionList } from '../use_fetch_or_create_rule_exception_list'; import { useSignalIndex } from '../../../../detections/containers/detection_engine/alerts/use_signal_index'; -import { createUseKibanaMock } from '../../../mock/kibana_react'; import { TimelineNonEcsData, Ecs } from '../../../../graphql/types'; import * as builder from '../builder'; import * as helpers from '../helpers'; @@ -33,8 +32,6 @@ jest.mock('../use_add_exception'); jest.mock('../use_fetch_or_create_rule_exception_list'); jest.mock('../builder'); -const useKibanaMock = useKibana as jest.Mock; - describe('When the add exception modal is opened', () => { const ruleName = 'test rule'; let defaultEndpointItems: jest.SpyInstance { .spyOn(builder, 'ExceptionBuilderComponent') .mockReturnValue(<>); - const kibanaMock = createUseKibanaMock()(); - useKibanaMock.mockImplementation(() => ({ - ...kibanaMock, - })); (useAddOrUpdateException as jest.Mock).mockImplementation(() => [ { isLoading: false }, jest.fn(), diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.test.tsx index 8ad80eba569c..6ff218ca0605 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.test.tsx @@ -11,7 +11,7 @@ import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; import { act } from 'react-dom/test-utils'; import { EditExceptionModal } from './'; -import { useKibana, useCurrentUser } from '../../../../common/lib/kibana'; +import { useCurrentUser } from '../../../../common/lib/kibana'; import { useFetchIndexPatterns } from '../../../../detections/containers/detection_engine/rules'; import { stubIndexPattern, @@ -19,7 +19,6 @@ import { } from 'src/plugins/data/common/index_patterns/index_pattern.stub'; import { useAddOrUpdateException } from '../use_add_exception'; import { useSignalIndex } from '../../../../detections/containers/detection_engine/alerts/use_signal_index'; -import { createUseKibanaMock } from '../../../mock/kibana_react'; import { getExceptionListItemSchemaMock } from '../../../../../../lists/common/schemas/response/exception_list_item_schema.mock'; import { EntriesArray } from '../../../../../../lists/common/schemas/types'; import * as builder from '../builder'; @@ -31,8 +30,6 @@ jest.mock('../use_fetch_or_create_rule_exception_list'); jest.mock('../../../../detections/containers/detection_engine/alerts/use_signal_index'); jest.mock('../builder'); -const useKibanaMock = useKibana as jest.Mock; - describe('When the edit exception modal is opened', () => { const ruleName = 'test rule'; @@ -45,10 +42,6 @@ describe('When the edit exception modal is opened', () => { .spyOn(builder, 'ExceptionBuilderComponent') .mockReturnValue(<>); - const kibanaMock = createUseKibanaMock()(); - useKibanaMock.mockImplementation(() => ({ - ...kibanaMock, - })); (useSignalIndex as jest.Mock).mockReturnValue({ loading: false, signalIndexName: 'test-signal', diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/use_add_exception.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/use_add_exception.test.tsx index cb1a80abedb2..6611ee2385d1 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/use_add_exception.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/use_add_exception.test.tsx @@ -5,6 +5,7 @@ */ import { act, renderHook, RenderHookResult } from '@testing-library/react-hooks'; +import { coreMock } from '../../../../../../../src/core/public/mocks'; import { KibanaServices } from '../../../common/lib/kibana'; import * as alertsApi from '../../../detections/containers/detection_engine/alerts/api'; @@ -14,7 +15,6 @@ import * as buildAlertStatusFilterHelper from '../../../detections/components/al import { getExceptionListItemSchemaMock } from '../../../../../lists/common/schemas/response/exception_list_item_schema.mock'; import { getCreateExceptionListItemSchemaMock } from '../../../../../lists/common/schemas/request/create_exception_list_item_schema.mock'; import { getUpdateExceptionListItemSchemaMock } from '../../../../../lists/common/schemas/request/update_exception_list_item_schema.mock'; -import { createKibanaCoreStartMock } from '../../../common/mock/kibana_core'; import { ExceptionListItemSchema, CreateExceptionListItemSchema, @@ -27,7 +27,7 @@ import { AddOrUpdateExceptionItemsFunc, } from './use_add_exception'; -const mockKibanaHttpService = createKibanaCoreStartMock().http; +const mockKibanaHttpService = coreMock.createStart().http; const mockKibanaServices = KibanaServices.get as jest.Mock; jest.mock('../../../common/lib/kibana'); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/use_fetch_or_create_rule_exception_list.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/use_fetch_or_create_rule_exception_list.test.tsx index 6dbf5922e0a9..39d88bd8e472 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/use_fetch_or_create_rule_exception_list.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/use_fetch_or_create_rule_exception_list.test.tsx @@ -6,11 +6,11 @@ import { act, renderHook, RenderHookResult } from '@testing-library/react-hooks'; +import { coreMock } from '../../../../../../../src/core/public/mocks'; import * as rulesApi from '../../../detections/containers/detection_engine/rules/api'; import * as listsApi from '../../../../../lists/public/exceptions/api'; import { getExceptionListSchemaMock } from '../../../../../lists/common/schemas/response/exception_list_schema.mock'; import { savedRuleMock } from '../../../detections/containers/detection_engine/rules/mock'; -import { createKibanaCoreStartMock } from '../../mock/kibana_core'; import { ExceptionListType } from '../../../lists_plugin_deps'; import { ListArray } from '../../../../common/detection_engine/schemas/types'; import { getListArrayMock } from '../../../../common/detection_engine/schemas/types/lists.mock'; @@ -20,7 +20,7 @@ import { ReturnUseFetchOrCreateRuleExceptionList, } from './use_fetch_or_create_rule_exception_list'; -const mockKibanaHttpService = createKibanaCoreStartMock().http; +const mockKibanaHttpService = coreMock.createStart().http; jest.mock('../../../detections/containers/detection_engine/rules/api'); describe('useFetchOrCreateRuleExceptionList', () => { diff --git a/x-pack/plugins/security_solution/public/common/components/ml/hooks/use_installed_security_jobs.ts b/x-pack/plugins/security_solution/public/common/components/ml/hooks/use_installed_security_jobs.ts index a9a728f81cc6..dde5eebe624b 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml/hooks/use_installed_security_jobs.ts +++ b/x-pack/plugins/security_solution/public/common/components/ml/hooks/use_installed_security_jobs.ts @@ -29,6 +29,9 @@ export interface UseInstalledSecurityJobsReturn { * Use the corresponding helper functions to filter the job list as * necessary (running jobs, etc). * + * NOTE: If you need to include jobs that are not currently installed, try the + * {@link useInstalledSecurityJobs} hook. + * */ export const useInstalledSecurityJobs = (): UseInstalledSecurityJobsReturn => { const [jobs, setJobs] = useState([]); diff --git a/x-pack/plugins/security_solution/public/common/components/ml_popover/hooks/use_security_jobs.ts b/x-pack/plugins/security_solution/public/common/components/ml_popover/hooks/use_security_jobs.ts index e8809e8366ee..2ba5cb84d272 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml_popover/hooks/use_security_jobs.ts +++ b/x-pack/plugins/security_solution/public/common/components/ml_popover/hooks/use_security_jobs.ts @@ -32,6 +32,7 @@ export interface UseSecurityJobsReturn { * list as necessary. E.g. installed jobs, running jobs, etc. * * NOTE: If the user is not an ml admin, jobs will be empty and isMlAdmin will be false. + * If you only need installed jobs, try the {@link useInstalledSecurityJobs} hook. * * @param refetchData */ @@ -39,7 +40,7 @@ export const useSecurityJobs = (refetchData: boolean): UseSecurityJobsReturn => const [jobs, setJobs] = useState([]); const [loading, setLoading] = useState(true); const mlCapabilities = useMlCapabilities(); - const [siemDefaultIndex] = useUiSetting$(DEFAULT_INDEX_KEY); + const [securitySolutionDefaultIndex] = useUiSetting$(DEFAULT_INDEX_KEY); const http = useHttp(); const { addError } = useAppToasts(); @@ -54,12 +55,12 @@ export const useSecurityJobs = (refetchData: boolean): UseSecurityJobsReturn => async function fetchSecurityJobIdsFromGroupsData() { if (isMlAdmin && isLicensed) { try { - // Batch fetch all installed jobs, ML modules, and check which modules are compatible with siemDefaultIndex + // Batch fetch all installed jobs, ML modules, and check which modules are compatible with securitySolutionDefaultIndex const [jobSummaryData, modulesData, compatibleModules] = await Promise.all([ getJobsSummary({ http, signal: abortCtrl.signal }), getModules({ signal: abortCtrl.signal }), checkRecognizer({ - indexPatternName: siemDefaultIndex, + indexPatternName: securitySolutionDefaultIndex, signal: abortCtrl.signal, }), ]); @@ -89,7 +90,7 @@ export const useSecurityJobs = (refetchData: boolean): UseSecurityJobsReturn => isSubscribed = false; abortCtrl.abort(); }; - }, [refetchData, isMlAdmin, isLicensed, siemDefaultIndex, addError, http]); + }, [refetchData, isMlAdmin, isLicensed, securitySolutionDefaultIndex, addError, http]); return { isLicensed, isMlAdmin, jobs, loading }; }; diff --git a/x-pack/plugins/security_solution/public/common/components/ml_popover/types.ts b/x-pack/plugins/security_solution/public/common/components/ml_popover/types.ts index c839f5110fe7..7120fcf4a9e5 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml_popover/types.ts +++ b/x-pack/plugins/security_solution/public/common/components/ml_popover/types.ts @@ -111,7 +111,7 @@ export interface CustomURL { } /** - * Representation of an ML Job as used by the SIEM App -- a composition of ModuleJob and MlSummaryJob + * Representation of an ML Job as used by the Security Solution App -- a composition of ModuleJob and MlSummaryJob * that includes necessary metadata like moduleName, defaultIndexPattern, etc. */ export interface SecurityJob extends MlSummaryJob { diff --git a/x-pack/plugins/security_solution/public/common/components/query_bar/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/query_bar/index.test.tsx index aac83ce650d8..aa61688f1f98 100644 --- a/x-pack/plugins/security_solution/public/common/components/query_bar/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/query_bar/index.test.tsx @@ -7,14 +7,13 @@ import { mount } from 'enzyme'; import React from 'react'; +import { coreMock } from '../../../../../../../src/core/public/mocks'; import { DEFAULT_FROM, DEFAULT_TO } from '../../../../common/constants'; import { TestProviders, mockIndexPattern } from '../../mock'; -import { createKibanaCoreStartMock } from '../../mock/kibana_core'; import { FilterManager, SearchBar } from '../../../../../../../src/plugins/data/public'; import { QueryBar, QueryBarComponentProps } from '.'; -import { createKibanaContextProviderMock } from '../../mock/kibana_react'; -const mockUiSettingsForFilterManager = createKibanaCoreStartMock().uiSettings; +const mockUiSettingsForFilterManager = coreMock.createStart().uiSettings; describe('QueryBar ', () => { // We are doing that because we need to wrapped this component with redux @@ -187,13 +186,9 @@ describe('QueryBar ', () => { describe('state', () => { test('clears draftQuery when filterQueryDraft has been cleared', () => { - const KibanaWithStorageProvider = createKibanaContextProviderMock(); - const Proxy = (props: QueryBarComponentProps) => ( - - - + ); @@ -231,13 +226,9 @@ describe('QueryBar ', () => { describe('#onQueryChange', () => { test(' is the only reference that changed when filterQueryDraft props get updated', () => { - const KibanaWithStorageProvider = createKibanaContextProviderMock(); - const Proxy = (props: QueryBarComponentProps) => ( - - - + ); @@ -382,24 +373,9 @@ describe('QueryBar ', () => { describe('SavedQueryManagementComponent state', () => { test('popover should hidden when "Save current query" button was clicked', () => { - const KibanaWithStorageProvider = createKibanaContextProviderMock(); - const Proxy = (props: QueryBarComponentProps) => ( - - - + ); diff --git a/x-pack/plugins/security_solution/public/common/components/search_bar/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/search_bar/index.test.tsx new file mode 100644 index 000000000000..356456c77779 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/search_bar/index.test.tsx @@ -0,0 +1,44 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { mount } from 'enzyme'; + +import { InputsModelId } from '../../store/inputs/constants'; +import { SearchBarComponent } from '.'; +import { TestProviders } from '../../mock'; + +jest.mock('../../lib/kibana'); + +describe('SearchBarComponent', () => { + const props = { + id: 'global' as InputsModelId, + indexPattern: { + fields: [], + title: '', + }, + updateSearch: jest.fn(), + setSavedQuery: jest.fn(), + setSearchBarFilter: jest.fn(), + end: '', + start: '', + toStr: '', + fromStr: '', + isLoading: false, + filterQuery: { + query: '', + language: '', + }, + queries: [], + savedQuery: undefined, + }; + + it('calls setSearchBarFilter on mount', () => { + mount(, { wrappingComponent: TestProviders }); + + expect(props.setSearchBarFilter).toHaveBeenCalled(); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/components/search_bar/index.tsx b/x-pack/plugins/security_solution/public/common/components/search_bar/index.tsx index de60bca73ced..2dc44fd48e66 100644 --- a/x-pack/plugins/security_solution/public/common/components/search_bar/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/search_bar/index.tsx @@ -33,7 +33,6 @@ import { filterQuerySelector, fromStrSelector, isLoadingSelector, - kindSelector, queriesSelector, savedQuerySelector, startSelector, @@ -44,6 +43,8 @@ import { networkActions } from '../../../network/store'; import { timelineActions } from '../../../timelines/store/timeline'; import { useKibana } from '../../lib/kibana'; +const APP_STATE_STORAGE_KEY = 'securitySolution.searchBar.appState'; + interface SiemSearchBarProps { id: InputsModelId; indexPattern: IIndexPattern; @@ -57,7 +58,7 @@ const SearchBarContainer = styled.div` } `; -const SearchBarComponent = memo( +export const SearchBarComponent = memo( ({ end, filterQuery, @@ -74,20 +75,27 @@ const SearchBarComponent = memo( updateSearch, dataTestSubj, }) => { - const { data } = useKibana().services; const { - timefilter: { timefilter }, - filterManager, - } = data.query; - - if (fromStr != null && toStr != null) { - timefilter.setTime({ from: fromStr, to: toStr }); - } else if (start != null && end != null) { - timefilter.setTime({ - from: new Date(start).toISOString(), - to: new Date(end).toISOString(), - }); - } + data: { + query: { + timefilter: { timefilter }, + filterManager, + }, + ui: { SearchBar }, + }, + storage, + } = useKibana().services; + + useEffect(() => { + if (fromStr != null && toStr != null) { + timefilter.setTime({ from: fromStr, to: toStr }); + } else if (start != null && end != null) { + timefilter.setTime({ + from: new Date(start).toISOString(), + to: new Date(end).toISOString(), + }); + } + }, [end, fromStr, start, timefilter, toStr]); const onQuerySubmit = useCallback( (payload: { dateRange: TimeRange; query?: Query }) => { @@ -135,8 +143,7 @@ const SearchBarComponent = memo( window.setTimeout(() => updateSearch(updateSearchBar), 0); }, - // eslint-disable-next-line react-hooks/exhaustive-deps - [id, end, filterQuery, fromStr, queries, start, toStr] + [id, toStr, end, fromStr, start, filterManager, filterQuery, queries, updateSearch] ); const onRefresh = useCallback( @@ -155,16 +162,14 @@ const SearchBarComponent = memo( queries.forEach((q) => q.refetch && (q.refetch as inputsModel.Refetch)()); } }, - // eslint-disable-next-line react-hooks/exhaustive-deps - [id, queries, filterManager] + [updateSearch, id, filterManager, queries] ); const onSaved = useCallback( (newSavedQuery: SavedQuery) => { setSavedQuery({ id, savedQuery: newSavedQuery }); }, - // eslint-disable-next-line react-hooks/exhaustive-deps - [id] + [id, setSavedQuery] ); const onSavedQueryUpdated = useCallback( @@ -200,8 +205,7 @@ const SearchBarComponent = memo( updateSearch(updateSearchBar); }, - // eslint-disable-next-line react-hooks/exhaustive-deps - [id, end, fromStr, start, toStr] + [id, toStr, end, fromStr, start, filterManager, updateSearch] ); const onClearSavedQuery = useCallback(() => { @@ -223,8 +227,16 @@ const SearchBarComponent = memo( filterManager, }); } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [id, end, filterManager, fromStr, start, toStr, savedQuery]); + }, [savedQuery, updateSearch, id, toStr, end, fromStr, start, filterManager]); + + const saveAppStateToStorage = useCallback( + (filters: Filter[]) => storage.set(APP_STATE_STORAGE_KEY, filters), + [storage] + ); + + const getAppStateFromStorage = useCallback(() => storage.get(APP_STATE_STORAGE_KEY) ?? [], [ + storage, + ]); useEffect(() => { let isSubscribed = true; @@ -234,6 +246,7 @@ const SearchBarComponent = memo( filterManager.getUpdates$().subscribe({ next: () => { if (isSubscribed) { + saveAppStateToStorage(filterManager.getAppFilters()); setSearchBarFilter({ id, filters: filterManager.getFilters(), @@ -243,16 +256,25 @@ const SearchBarComponent = memo( }) ); + // for the initial state + filterManager.setAppFilters(getAppStateFromStorage()); + setSearchBarFilter({ + id, + filters: filterManager.getFilters(), + }); + return () => { isSubscribed = false; subscriptions.unsubscribe(); }; // eslint-disable-next-line react-hooks/exhaustive-deps }, []); + const indexPatterns = useMemo(() => [indexPattern], [indexPattern]); + return ( - { const getEndSelector = endSelector(); const getFromStrSelector = fromStrSelector(); const getIsLoadingSelector = isLoadingSelector(); - const getKindSelector = kindSelector(); const getQueriesSelector = queriesSelector(); const getStartSelector = startSelector(); const getToStrSelector = toStrSelector(); @@ -292,7 +313,6 @@ const makeMapStateToProps = () => { fromStr: getFromStrSelector(inputsRange), filterQuery: getFilterQuerySelector(inputsRange), isLoading: getIsLoadingSelector(inputsRange), - kind: getKindSelector(inputsRange), queries: getQueriesSelector(inputsRange), savedQuery: getSavedQuerySelector(inputsRange), start: getStartSelector(inputsRange), diff --git a/x-pack/plugins/security_solution/public/common/components/super_date_picker/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/super_date_picker/index.test.tsx index 0795e46c9e45..956ee4b05f9d 100644 --- a/x-pack/plugins/security_solution/public/common/components/super_date_picker/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/super_date_picker/index.test.tsx @@ -17,7 +17,7 @@ import { kibanaObservable, createSecuritySolutionStorageMock, } from '../../mock'; -import { createUseUiSetting$Mock } from '../../mock/kibana_react'; +import { createUseUiSetting$Mock } from '../../lib/kibana/kibana_react.mock'; import { createStore, State } from '../../store'; import { SuperDatePicker, makeMapStateToProps } from '.'; diff --git a/x-pack/plugins/security_solution/public/common/components/top_n/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/top_n/index.test.tsx index 1e93fdb93672..31318122eb56 100644 --- a/x-pack/plugins/security_solution/public/common/components/top_n/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/top_n/index.test.tsx @@ -18,7 +18,6 @@ import { createSecuritySolutionStorageMock, mockIndexPattern, } from '../../mock'; -import { createKibanaCoreStartMock } from '../../mock/kibana_core'; import { FilterManager } from '../../../../../../../src/plugins/data/public'; import { createStore, State } from '../../store'; @@ -29,6 +28,7 @@ import { getTimelineDefaults, } from '../../../timelines/components/manage_timeline'; import { TimelineId } from '../../../../common/types/timeline'; +import { coreMock } from '../../../../../../../src/core/public/mocks'; jest.mock('react-router-dom', () => { const original = jest.requireActual('react-router-dom'); @@ -45,7 +45,7 @@ jest.mock('../link_to'); jest.mock('../../lib/kibana'); jest.mock('../../../timelines/store/timeline/actions'); -const mockUiSettingsForFilterManager = createKibanaCoreStartMock().uiSettings; +const mockUiSettingsForFilterManager = coreMock.createStart().uiSettings; const field = 'process.name'; const value = 'nice'; diff --git a/x-pack/plugins/security_solution/public/common/containers/local_storage/use_messages_storage.test.tsx b/x-pack/plugins/security_solution/public/common/containers/local_storage/use_messages_storage.test.tsx index 7085894e4a51..58f5c1a9beb2 100644 --- a/x-pack/plugins/security_solution/public/common/containers/local_storage/use_messages_storage.test.tsx +++ b/x-pack/plugins/security_solution/public/common/containers/local_storage/use_messages_storage.test.tsx @@ -6,17 +6,13 @@ import { renderHook, act } from '@testing-library/react-hooks'; import { useKibana } from '../../lib/kibana'; -import { createUseKibanaMock } from '../../mock/kibana_react'; import { useMessagesStorage, UseMessagesStorage } from './use_messages_storage'; jest.mock('../../lib/kibana'); -const useKibanaMock = useKibana as jest.Mock; describe('useLocalStorage', () => { beforeEach(() => { - const services = { ...createUseKibanaMock()().services }; - useKibanaMock.mockImplementation(() => ({ services })); - services.storage.store.clear(); + useKibana().services.storage.clear(); }); it('should return an empty array when there is no messages', async () => { diff --git a/x-pack/plugins/security_solution/public/common/lib/kibana/__mocks__/index.ts b/x-pack/plugins/security_solution/public/common/lib/kibana/__mocks__/index.ts index 5f4285f2747a..573ef92f7e06 100644 --- a/x-pack/plugins/security_solution/public/common/lib/kibana/__mocks__/index.ts +++ b/x-pack/plugins/security_solution/public/common/lib/kibana/__mocks__/index.ts @@ -9,19 +9,21 @@ import { createKibanaContextProviderMock, createUseUiSettingMock, createUseUiSetting$Mock, - createUseKibanaMock, + createStartServicesMock, createWithKibanaMock, -} from '../../../mock/kibana_react'; +} from '../kibana_react.mock'; export const KibanaServices = { get: jest.fn(), getKibanaVersion: jest.fn(() => '8.0.0') }; -export const useKibana = jest.fn(createUseKibanaMock()); +export const useKibana = jest.fn().mockReturnValue({ services: createStartServicesMock() }); export const useUiSetting = jest.fn(createUseUiSettingMock()); export const useUiSetting$ = jest.fn(createUseUiSetting$Mock()); -export const useHttp = jest.fn(() => useKibana().services.http); +export const useHttp = jest.fn().mockReturnValue(createStartServicesMock().http); export const useTimeZone = jest.fn(); export const useDateFormat = jest.fn(); export const useBasePath = jest.fn(() => '/test/base/path'); -export const useToasts = jest.fn(() => notificationServiceMock.createStartContract().toasts); +export const useToasts = jest + .fn() + .mockReturnValue(notificationServiceMock.createStartContract().toasts); export const useCurrentUser = jest.fn(); export const withKibana = jest.fn(createWithKibanaMock()); export const KibanaContextProvider = jest.fn(createKibanaContextProviderMock()); diff --git a/x-pack/plugins/security_solution/public/common/lib/kibana/kibana_react.mock.ts b/x-pack/plugins/security_solution/public/common/lib/kibana/kibana_react.mock.ts new file mode 100644 index 000000000000..c026b65853a4 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/lib/kibana/kibana_react.mock.ts @@ -0,0 +1,103 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +/* eslint-disable react/display-name */ + +import React from 'react'; + +import { coreMock } from '../../../../../../../src/core/public/mocks'; +import { KibanaContextProvider } from '../../../../../../../src/plugins/kibana_react/public'; +import { dataPluginMock } from '../../../../../../../src/plugins/data/public/mocks'; +import { securityMock } from '../../../../../../plugins/security/public/mocks'; +import { + DEFAULT_APP_TIME_RANGE, + DEFAULT_APP_REFRESH_INTERVAL, + DEFAULT_INDEX_KEY, + DEFAULT_DATE_FORMAT, + DEFAULT_DATE_FORMAT_TZ, + DEFAULT_DARK_MODE, + DEFAULT_TIME_RANGE, + DEFAULT_REFRESH_RATE_INTERVAL, + DEFAULT_FROM, + DEFAULT_TO, + DEFAULT_INTERVAL_PAUSE, + DEFAULT_INTERVAL_VALUE, + DEFAULT_BYTES_FORMAT, + DEFAULT_INDEX_PATTERN, +} from '../../../../common/constants'; +import { StartServices } from '../../../types'; +import { createSecuritySolutionStorageMock } from '../../mock/mock_local_storage'; + +const mockUiSettings: Record = { + [DEFAULT_TIME_RANGE]: { from: 'now-15m', to: 'now', mode: 'quick' }, + [DEFAULT_REFRESH_RATE_INTERVAL]: { pause: false, value: 0 }, + [DEFAULT_APP_TIME_RANGE]: { + from: DEFAULT_FROM, + to: DEFAULT_TO, + }, + [DEFAULT_APP_REFRESH_INTERVAL]: { + pause: DEFAULT_INTERVAL_PAUSE, + value: DEFAULT_INTERVAL_VALUE, + }, + [DEFAULT_INDEX_KEY]: DEFAULT_INDEX_PATTERN, + [DEFAULT_BYTES_FORMAT]: '0,0.[0]b', + [DEFAULT_DATE_FORMAT_TZ]: 'UTC', + [DEFAULT_DATE_FORMAT]: 'MMM D, YYYY @ HH:mm:ss.SSS', + [DEFAULT_DARK_MODE]: false, +}; + +export const createUseUiSettingMock = () => (key: string, defaultValue?: unknown): unknown => { + const result = mockUiSettings[key]; + + if (typeof result != null) return result; + + if (defaultValue != null) { + return defaultValue; + } + + throw new TypeError(`Unexpected config key: ${key}`); +}; + +export const createUseUiSetting$Mock = () => { + const useUiSettingMock = createUseUiSettingMock(); + + return (key: string, defaultValue?: unknown): [unknown, () => void] | undefined => [ + useUiSettingMock(key, defaultValue), + jest.fn(), + ]; +}; + +export const createStartServicesMock = (): StartServices => { + const core = coreMock.createStart(); + core.uiSettings.get.mockImplementation(createUseUiSettingMock()); + const { storage } = createSecuritySolutionStorageMock(); + const data = dataPluginMock.createStartContract(); + const security = securityMock.createSetup(); + + const services = ({ + ...core, + data, + security, + storage, + } as unknown) as StartServices; + + return services; +}; + +export const createWithKibanaMock = () => { + const services = createStartServicesMock(); + + return (Component: unknown) => (props: unknown) => { + return React.createElement(Component as string, { ...(props as object), kibana: { services } }); + }; +}; + +export const createKibanaContextProviderMock = () => { + const services = createStartServicesMock(); + + return ({ children }: { children: React.ReactNode }) => + React.createElement(KibanaContextProvider, { services }, children); +}; diff --git a/x-pack/plugins/security_solution/public/common/mock/endpoint/app_context_render.tsx b/x-pack/plugins/security_solution/public/common/mock/endpoint/app_context_render.tsx index 1ed459521cc7..1b9e95f7d073 100644 --- a/x-pack/plugins/security_solution/public/common/mock/endpoint/app_context_render.tsx +++ b/x-pack/plugins/security_solution/public/common/mock/endpoint/app_context_render.tsx @@ -17,7 +17,7 @@ import { apolloClientObservable, kibanaObservable } from '../test_providers'; import { createStore, State } from '../../store'; import { AppRootProvider } from './app_root_provider'; import { managementMiddlewareFactory } from '../../../management/store/middleware'; -import { createKibanaContextProviderMock } from '../kibana_react'; +import { createKibanaContextProviderMock } from '../../lib/kibana/kibana_react.mock'; import { SUB_PLUGINS_REDUCER, mockGlobalState, createSecuritySolutionStorageMock } from '..'; type UiRender = (ui: React.ReactElement, options?: RenderOptions) => RenderResult; diff --git a/x-pack/plugins/security_solution/public/common/mock/index.ts b/x-pack/plugins/security_solution/public/common/mock/index.ts index 678ad4d84b58..7e076772c42f 100644 --- a/x-pack/plugins/security_solution/public/common/mock/index.ts +++ b/x-pack/plugins/security_solution/public/common/mock/index.ts @@ -16,4 +16,3 @@ export * from './test_providers'; export * from './utils'; export * from './mock_ecs'; export * from './timeline_results'; -export * from './kibana_react'; diff --git a/x-pack/plugins/security_solution/public/common/mock/kibana_core.ts b/x-pack/plugins/security_solution/public/common/mock/kibana_core.ts deleted file mode 100644 index f8eed75cf9bf..000000000000 --- a/x-pack/plugins/security_solution/public/common/mock/kibana_core.ts +++ /dev/null @@ -1,15 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { coreMock } from '../../../../../../src/core/public/mocks'; -import { dataPluginMock } from '../../../../../../src/plugins/data/public/mocks'; -import { securityMock } from '../../../../../plugins/security/public/mocks'; - -export const createKibanaCoreStartMock = () => coreMock.createStart(); -export const createKibanaPluginsStartMock = () => ({ - data: dataPluginMock.createStartContract(), - security: securityMock.createSetup(), -}); diff --git a/x-pack/plugins/security_solution/public/common/mock/kibana_react.ts b/x-pack/plugins/security_solution/public/common/mock/kibana_react.ts deleted file mode 100644 index bdb8ca85b0d7..000000000000 --- a/x-pack/plugins/security_solution/public/common/mock/kibana_react.ts +++ /dev/null @@ -1,126 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -/* eslint-disable react/display-name */ - -import React from 'react'; -import { KibanaContextProvider } from '../../../../../../src/plugins/kibana_react/public'; - -import { - DEFAULT_APP_TIME_RANGE, - DEFAULT_APP_REFRESH_INTERVAL, - DEFAULT_INDEX_KEY, - DEFAULT_DATE_FORMAT, - DEFAULT_DATE_FORMAT_TZ, - DEFAULT_DARK_MODE, - DEFAULT_TIME_RANGE, - DEFAULT_REFRESH_RATE_INTERVAL, - DEFAULT_FROM, - DEFAULT_TO, - DEFAULT_INTERVAL_PAUSE, - DEFAULT_INTERVAL_VALUE, - DEFAULT_BYTES_FORMAT, - DEFAULT_INDEX_PATTERN, -} from '../../../common/constants'; -import { createKibanaCoreStartMock, createKibanaPluginsStartMock } from './kibana_core'; -import { StartServices } from '../../types'; -import { createSecuritySolutionStorageMock } from './mock_local_storage'; - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export const mockUiSettings: Record = { - [DEFAULT_TIME_RANGE]: { from: 'now-15m', to: 'now', mode: 'quick' }, - [DEFAULT_REFRESH_RATE_INTERVAL]: { pause: false, value: 0 }, - [DEFAULT_APP_TIME_RANGE]: { - from: DEFAULT_FROM, - to: DEFAULT_TO, - }, - [DEFAULT_APP_REFRESH_INTERVAL]: { - pause: DEFAULT_INTERVAL_PAUSE, - value: DEFAULT_INTERVAL_VALUE, - }, - [DEFAULT_INDEX_KEY]: DEFAULT_INDEX_PATTERN, - [DEFAULT_BYTES_FORMAT]: '0,0.[0]b', - [DEFAULT_DATE_FORMAT_TZ]: 'UTC', - [DEFAULT_DATE_FORMAT]: 'MMM D, YYYY @ HH:mm:ss.SSS', - [DEFAULT_DARK_MODE]: false, -}; - -export const createUseUiSettingMock = () => ( - key: string, - defaultValue?: T -): T => { - const result = mockUiSettings[key]; - - if (typeof result != null) return result; - - if (defaultValue != null) { - return defaultValue; - } - - throw new Error(`Unexpected config key: ${key}`); -}; - -export const createUseUiSetting$Mock = () => { - const useUiSettingMock = createUseUiSettingMock(); - - return ( - key: string, - defaultValue?: T - ): [T, () => void] | undefined => [useUiSettingMock(key, defaultValue), jest.fn()]; -}; - -export const createKibanaObservable$Mock = createKibanaCoreStartMock; - -export const createUseKibanaMock = () => { - const core = createKibanaCoreStartMock(); - const plugins = createKibanaPluginsStartMock(); - const useUiSetting = createUseUiSettingMock(); - const { storage } = createSecuritySolutionStorageMock(); - - const services = { - ...core, - ...plugins, - uiSettings: { - ...core.uiSettings, - get: useUiSetting, - }, - storage, - }; - - return () => ({ services }); -}; - -export const createStartServices = () => { - const core = createKibanaCoreStartMock(); - const plugins = createKibanaPluginsStartMock(); - - const services = ({ - ...core, - ...plugins, - } as unknown) as StartServices; - - return services; -}; - -export const createWithKibanaMock = () => { - const kibana = createUseKibanaMock()(); - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return (Component: any) => (props: any) => { - return React.createElement(Component, { ...props, kibana }); - }; -}; - -export const createKibanaContextProviderMock = () => { - const kibana = createUseKibanaMock()(); - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return ({ services, ...rest }: any) => - React.createElement(KibanaContextProvider, { - ...rest, - services: { ...kibana.services, ...services }, - }); -}; diff --git a/x-pack/plugins/security_solution/public/common/mock/test_providers.tsx b/x-pack/plugins/security_solution/public/common/mock/test_providers.tsx index 010d2fac18af..9ead8171bfef 100644 --- a/x-pack/plugins/security_solution/public/common/mock/test_providers.tsx +++ b/x-pack/plugins/security_solution/public/common/mock/test_providers.tsx @@ -19,7 +19,10 @@ import { ThemeProvider } from 'styled-components'; import { createStore, State } from '../store'; import { mockGlobalState } from './global_state'; -import { createKibanaContextProviderMock, createStartServices } from './kibana_react'; +import { + createKibanaContextProviderMock, + createStartServicesMock, +} from '../lib/kibana/kibana_react.mock'; import { FieldHook, useForm } from '../../shared_imports'; import { SUB_PLUGINS_REDUCER } from './utils'; import { createSecuritySolutionStorageMock, localStorageMock } from './mock_local_storage'; @@ -38,7 +41,7 @@ export const apolloClient = new ApolloClient({ }); export const apolloClientObservable = new BehaviorSubject(apolloClient); -export const kibanaObservable = new BehaviorSubject(createStartServices()); +export const kibanaObservable = new BehaviorSubject(createStartServicesMock()); Object.defineProperty(window, 'localStorage', { value: localStorageMock(), diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/lists/__mocks__/use_lists_config.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/lists/__mocks__/use_lists_config.tsx index 0f8e0fba1e3a..291587e9f69c 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/lists/__mocks__/use_lists_config.tsx +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/lists/__mocks__/use_lists_config.tsx @@ -4,4 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -export const useListsConfig = jest.fn().mockReturnValue({}); +import { getUseListsConfigMock } from '../use_lists_config.mock'; + +export const useListsConfig = jest.fn(getUseListsConfigMock); diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/lists/use_lists_config.mock.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/lists/use_lists_config.mock.ts new file mode 100644 index 000000000000..90f47972a3a2 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/lists/use_lists_config.mock.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { UseListsConfigReturn } from './use_lists_config'; + +export const getUseListsConfigMock: () => jest.Mocked = () => ({ + canManageIndex: null, + canWriteIndex: null, + enabled: true, + loading: false, + needsConfiguration: false, +}); diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/lists/use_lists_config.test.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/lists/use_lists_config.test.tsx new file mode 100644 index 000000000000..a5ff29e2091b --- /dev/null +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/lists/use_lists_config.test.tsx @@ -0,0 +1,86 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { renderHook } from '@testing-library/react-hooks'; + +import { useKibana } from '../../../../common/lib/kibana'; +import { useListsIndex } from './use_lists_index'; +import { useListsPrivileges } from './use_lists_privileges'; +import { getUseListsIndexMock } from './use_lists_index.mock'; +import { getUseListsPrivilegesMock } from './use_lists_privileges.mock'; +import { useListsConfig } from './use_lists_config'; + +jest.mock('../../../../common/lib/kibana'); +jest.mock('./use_lists_index'); +jest.mock('./use_lists_privileges'); + +describe('useListsConfig', () => { + let listsIndexMock: ReturnType; + let listsPrivilegesMock: ReturnType; + + beforeEach(() => { + listsIndexMock = getUseListsIndexMock(); + listsPrivilegesMock = getUseListsPrivilegesMock(); + (useListsIndex as jest.Mock).mockReturnValue(listsIndexMock); + (useListsPrivileges as jest.Mock).mockReturnValue(listsPrivilegesMock); + }); + + it("returns the user's write permissions", () => { + listsPrivilegesMock.canWriteIndex = false; + const { result } = renderHook(() => useListsConfig()); + expect(result.current.canWriteIndex).toEqual(false); + + listsPrivilegesMock.canWriteIndex = true; + const { result: result2 } = renderHook(() => useListsConfig()); + expect(result2.current.canWriteIndex).toEqual(true); + }); + + describe('when lists are disabled', () => { + beforeEach(() => { + useKibana().services.lists = undefined; + }); + + it('indicates that lists are not enabled, and need configuration', () => { + const { result } = renderHook(() => useListsConfig()); + expect(result.current.enabled).toEqual(false); + expect(result.current.needsConfiguration).toEqual(true); + }); + }); + + describe('when lists are enabled but indexes do not exist', () => { + beforeEach(() => { + useKibana().services.lists = {}; + listsIndexMock.indexExists = false; + }); + + it('needs configuration if the user cannot manage indexes', () => { + listsPrivilegesMock.canManageIndex = false; + + const { result } = renderHook(() => useListsConfig()); + expect(result.current.needsConfiguration).toEqual(true); + expect(listsIndexMock.createIndex).not.toHaveBeenCalled(); + }); + + it('attempts to create the indexes if the user can manage indexes', () => { + listsPrivilegesMock.canManageIndex = true; + + renderHook(() => useListsConfig()); + expect(listsIndexMock.createIndex).toHaveBeenCalled(); + }); + }); + + describe('when lists are enabled and indexes exist', () => { + beforeEach(() => { + useKibana().services.lists = {}; + listsIndexMock.indexExists = true; + }); + + it('does not need configuration', () => { + const { result } = renderHook(() => useListsConfig()); + expect(result.current.needsConfiguration).toEqual(false); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/lists/use_lists_index.mock.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/lists/use_lists_index.mock.ts new file mode 100644 index 000000000000..e2169442d80e --- /dev/null +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/lists/use_lists_index.mock.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { UseListsIndexReturn } from './use_lists_index'; + +export const getUseListsIndexMock: () => jest.Mocked = () => ({ + createIndex: jest.fn(), + indexExists: null, + error: null, + loading: false, +}); diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/lists/use_lists_privileges.mock.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/lists/use_lists_privileges.mock.ts new file mode 100644 index 000000000000..4f583a72460e --- /dev/null +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/lists/use_lists_privileges.mock.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { UseListsPrivilegesReturn } from './use_lists_privileges'; + +export const getUseListsPrivilegesMock: () => jest.Mocked = () => ({ + isAuthenticated: null, + canManageIndex: null, + canWriteIndex: null, + loading: false, +}); diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/index.test.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/index.test.tsx index b07caa754aec..9f486dc11e99 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/index.test.tsx @@ -9,7 +9,6 @@ import { shallow, mount } from 'enzyme'; import { act } from 'react-dom/test-utils'; import '../../../../../common/mock/match_media'; -import { createKibanaContextProviderMock } from '../../../../../common/mock/kibana_react'; import { TestProviders } from '../../../../../common/mock'; // we don't have the types for waitFor just yet, so using "as waitFor" until when we do import { wait as waitFor } from '@testing-library/react'; @@ -182,23 +181,20 @@ describe('AllRules', () => { }); it('renders rules tab', async () => { - const KibanaContext = createKibanaContextProviderMock(); const wrapper = mount( - - - + ); @@ -211,24 +207,20 @@ describe('AllRules', () => { }); it('renders monitoring tab when monitoring tab clicked', async () => { - const KibanaContext = createKibanaContextProviderMock(); - const wrapper = mount( - - - + ); const monitoringTab = wrapper.find('[data-test-subj="allRulesTableTab-monitoring"] button'); diff --git a/x-pack/plugins/security_solution/public/hosts/containers/hosts/index.tsx b/x-pack/plugins/security_solution/public/hosts/containers/hosts/index.tsx index 8af24e6e6abc..346de9f87313 100644 --- a/x-pack/plugins/security_solution/public/hosts/containers/hosts/index.tsx +++ b/x-pack/plugins/security_solution/public/hosts/containers/hosts/index.tsx @@ -4,185 +4,211 @@ * you may not use this file except in compliance with the Elastic License. */ -import { get, getOr } from 'lodash/fp'; -import memoizeOne from 'memoize-one'; -import React from 'react'; -import { Query } from 'react-apollo'; -import { connect } from 'react-redux'; -import { compose } from 'redux'; +import deepEqual from 'fast-deep-equal'; +import { noop } from 'lodash/fp'; +import { useCallback, useEffect, useRef, useState } from 'react'; +import { useSelector } from 'react-redux'; import { DEFAULT_INDEX_KEY } from '../../../../common/constants'; -import { - Direction, - GetHostsTableQuery, - HostsEdges, - HostsFields, - PageInfoPaginated, -} from '../../../graphql/types'; -import { inputsModel, State, inputsSelectors } from '../../../common/store'; -import { createFilter, getDefaultFetchPolicy } from '../../../common/containers/helpers'; -import { - QueryTemplatePaginated, - QueryTemplatePaginatedProps, -} from '../../../common/containers/query_template_paginated'; -import { withKibana, WithKibanaProps } from '../../../common/lib/kibana'; +import { HostsEdges, PageInfoPaginated } from '../../../graphql/types'; +import { inputsModel, State } from '../../../common/store'; +import { createFilter } from '../../../common/containers/helpers'; +import { useKibana } from '../../../common/lib/kibana'; import { hostsModel, hostsSelectors } from '../../store'; -import { HostsTableQuery } from './hosts_table.gql_query'; import { generateTablePaginationOptions } from '../../../common/components/paginated_table/helpers'; +import { + DocValueFields, + HostsQueries, + HostsRequestOptions, + HostsStrategyResponse, +} from '../../../../common/search_strategy/security_solution'; +import { ESTermQuery } from '../../../../common/typed_json'; + +import * as i18n from './translations'; +import { AbortError } from '../../../../../../../src/plugins/data/common'; const ID = 'hostsQuery'; +type LoadPage = (newActivePage: number) => void; export interface HostsArgs { endDate: string; hosts: HostsEdges[]; id: string; inspect: inputsModel.InspectQuery; isInspected: boolean; - loading: boolean; - loadPage: (newActivePage: number) => void; + loadPage: LoadPage; pageInfo: PageInfoPaginated; refetch: inputsModel.Refetch; startDate: string; totalCount: number; } -export interface OwnProps extends QueryTemplatePaginatedProps { - children: (args: HostsArgs) => React.ReactNode; - type: hostsModel.HostsType; - startDate: string; +interface UseAllHost { + docValueFields?: DocValueFields[]; + filterQuery?: ESTermQuery | string; endDate: string; + startDate: string; + type: hostsModel.HostsType; } -export interface HostsComponentReduxProps { - activePage: number; - isInspected: boolean; - limit: number; - sortField: HostsFields; - direction: Direction; -} - -type HostsProps = OwnProps & HostsComponentReduxProps & WithKibanaProps; +export const useAllHost = ({ + docValueFields, + filterQuery, + endDate, + startDate, + type, +}: UseAllHost): [boolean, HostsArgs] => { + const getHostsSelector = hostsSelectors.hostsSelector(); + const { activePage, direction, limit, sortField } = useSelector((state: State) => + getHostsSelector(state, type) + ); + const { data, notifications, uiSettings } = useKibana().services; + const refetch = useRef(noop); + const abortCtrl = useRef(new AbortController()); + const defaultIndex = uiSettings.get(DEFAULT_INDEX_KEY); + const [loading, setLoading] = useState(false); + const [hostsRequest, setHostRequest] = useState({ + defaultIndex, + docValueFields: docValueFields ?? [], + factoryQueryType: HostsQueries.hosts, + filterQuery: createFilter(filterQuery), + pagination: generateTablePaginationOptions(activePage, limit), + timerange: { + interval: '12h', + from: startDate, + to: endDate, + }, + sort: { + direction, + field: sortField, + }, + // inspect: isInspected, + }); -class HostsComponentQuery extends QueryTemplatePaginated< - HostsProps, - GetHostsTableQuery.Query, - GetHostsTableQuery.Variables -> { - private memoizedHosts: ( - variables: string, - data: GetHostsTableQuery.Source | undefined - ) => HostsEdges[]; + const wrappedLoadMore = useCallback( + (newActivePage: number) => { + setHostRequest((prevRequest) => { + return { + ...prevRequest, + pagination: generateTablePaginationOptions(newActivePage, limit), + }; + }); + }, + [limit] + ); - constructor(props: HostsProps) { - super(props); - this.memoizedHosts = memoizeOne(this.getHosts); - } + const [hostsResponse, setHostsResponse] = useState({ + endDate, + hosts: [], + id: ID, + inspect: { + dsl: [], + response: [], + }, + isInspected: false, + loadPage: wrappedLoadMore, + pageInfo: { + activePage: 0, + fakeTotalCount: 0, + showMorePagesIndicator: false, + }, + refetch: refetch.current, + startDate, + totalCount: -1, + }); - public render() { - const { - activePage, - docValueFields, - id = ID, - isInspected, - children, - direction, - filterQuery, - endDate, - kibana, - limit, - startDate, - skip, - sourceId, - sortField, - } = this.props; - const defaultIndex = kibana.services.uiSettings.get(DEFAULT_INDEX_KEY); + const hostsSearch = useCallback( + (request: HostsRequestOptions) => { + let didCancel = false; + const asyncSearch = async () => { + abortCtrl.current = new AbortController(); + setLoading(true); - const variables: GetHostsTableQuery.Variables = { - sourceId, - timerange: { - interval: '12h', - from: startDate, - to: endDate, - }, - sort: { - direction, - field: sortField, - }, - pagination: generateTablePaginationOptions(activePage, limit), - filterQuery: createFilter(filterQuery), - defaultIndex, - docValueFields: docValueFields ?? [], - inspect: isInspected, - }; - return ( - - query={HostsTableQuery} - fetchPolicy={getDefaultFetchPolicy()} - notifyOnNetworkStatusChange - variables={variables} - skip={skip} - > - {({ data, loading, fetchMore, networkStatus, refetch }) => { - this.setFetchMore(fetchMore); - this.setFetchMoreOptions((newActivePage: number) => ({ - variables: { - pagination: generateTablePaginationOptions(newActivePage, limit), + const searchSubscription$ = data.search + .search(request, { + strategy: 'securitySolutionSearchStrategy', + signal: abortCtrl.current.signal, + }) + .subscribe({ + next: (response) => { + if (!response.isPartial && !response.isRunning) { + if (!didCancel) { + setLoading(false); + setHostsResponse((prevResponse) => ({ + ...prevResponse, + hosts: response.edges, + inspect: response.inspect ?? prevResponse.inspect, + pageInfo: response.pageInfo, + refetch: refetch.current, + totalCount: response.totalCount, + })); + } + searchSubscription$.unsubscribe(); + } else if (response.isPartial && !response.isRunning) { + if (!didCancel) { + setLoading(false); + } + // TODO: Make response error status clearer + notifications.toasts.addWarning(i18n.ERROR_ALL_HOST); + searchSubscription$.unsubscribe(); + } }, - updateQuery: (prev, { fetchMoreResult }) => { - if (!fetchMoreResult) { - return prev; + error: (msg) => { + if (!(msg instanceof AbortError)) { + notifications.toasts.addDanger({ title: i18n.FAIL_ALL_HOST, text: msg.message }); } - return { - ...fetchMoreResult, - source: { - ...fetchMoreResult.source, - Hosts: { - ...fetchMoreResult.source.Hosts, - edges: [...fetchMoreResult.source.Hosts.edges], - }, - }, - }; }, - })); - const isLoading = this.isItAValidLoading(loading, variables, networkStatus); - return children({ - endDate, - hosts: this.memoizedHosts(JSON.stringify(variables), get('source', data)), - id, - inspect: getOr(null, 'source.Hosts.inspect', data), - isInspected, - loading: isLoading, - loadPage: this.wrappedLoadMore, - pageInfo: getOr({}, 'source.Hosts.pageInfo', data), - refetch: this.memoizedRefetchQuery(variables, limit, refetch), - startDate, - totalCount: getOr(-1, 'source.Hosts.totalCount', data), }); - }} - - ); - } + }; + abortCtrl.current.abort(); + asyncSearch(); + refetch.current = asyncSearch; + return () => { + didCancel = true; + abortCtrl.current.abort(); + }; + }, + [data.search, notifications.toasts] + ); - private getHosts = ( - variables: string, - source: GetHostsTableQuery.Source | undefined - ): HostsEdges[] => getOr([], 'Hosts.edges', source); -} + useEffect(() => { + setHostRequest((prevRequest) => { + const myRequest = { + ...prevRequest, + defaultIndex, + docValueFields: docValueFields ?? [], + filterQuery: createFilter(filterQuery), + pagination: generateTablePaginationOptions(activePage, limit), + timerange: { + interval: '12h', + from: startDate, + to: endDate, + }, + sort: { + direction, + field: sortField, + }, + }; + if (!deepEqual(prevRequest, myRequest)) { + return myRequest; + } + return prevRequest; + }); + }, [ + activePage, + defaultIndex, + direction, + docValueFields, + endDate, + filterQuery, + limit, + startDate, + sortField, + ]); -const makeMapStateToProps = () => { - const getHostsSelector = hostsSelectors.hostsSelector(); - const getQuery = inputsSelectors.globalQueryByIdSelector(); - const mapStateToProps = (state: State, { type, id = ID }: OwnProps) => { - const { isInspected } = getQuery(state, id); - return { - ...getHostsSelector(state, type), - isInspected, - }; - }; - return mapStateToProps; -}; + useEffect(() => { + hostsSearch(hostsRequest); + }, [hostsRequest, hostsSearch]); -export const HostsQuery = compose>( - connect(makeMapStateToProps), - withKibana -)(HostsComponentQuery); + return [loading, hostsResponse]; +}; diff --git a/x-pack/plugins/security_solution/public/hosts/containers/hosts/translations.ts b/x-pack/plugins/security_solution/public/hosts/containers/hosts/translations.ts new file mode 100644 index 000000000000..ada713d135c2 --- /dev/null +++ b/x-pack/plugins/security_solution/public/hosts/containers/hosts/translations.ts @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export const ERROR_ALL_HOST = i18n.translate( + 'xpack.securitySolution.allHost.errorSearchDescription', + { + defaultMessage: `An error has occurred on all hosts search`, + } +); + +export const FAIL_ALL_HOST = i18n.translate( + 'xpack.securitySolution.allHost.failSearchDescription', + { + defaultMessage: `Failed to run search on all hosts`, + } +); diff --git a/x-pack/plugins/security_solution/public/hosts/pages/navigation/hosts_query_tab_body.tsx b/x-pack/plugins/security_solution/public/hosts/pages/navigation/hosts_query_tab_body.tsx index 80cf62bc49f7..5232dcfd8818 100644 --- a/x-pack/plugins/security_solution/public/hosts/pages/navigation/hosts_query_tab_body.tsx +++ b/x-pack/plugins/security_solution/public/hosts/pages/navigation/hosts_query_tab_body.tsx @@ -6,7 +6,7 @@ import { getOr } from 'lodash/fp'; import React from 'react'; -import { HostsQuery } from '../../containers/hosts'; +import { useAllHost } from '../../containers/hosts'; import { HostsComponentsQueryProps } from './types'; import { HostsTable } from '../../components/hosts_table'; import { manageQuery } from '../../../common/components/page/manage_query'; @@ -23,35 +23,29 @@ export const HostsQueryTabBody = ({ setQuery, startDate, type, -}: HostsComponentsQueryProps) => ( - - {({ hosts, totalCount, loading, pageInfo, loadPage, id, inspect, isInspected, refetch }) => ( - - )} - -); +}: HostsComponentsQueryProps) => { + const [ + loading, + { hosts, totalCount, pageInfo, loadPage, id, inspect, isInspected, refetch }, + ] = useAllHost({ docValueFields, endDate, filterQuery, startDate, type }); + return ( + + ); +}; HostsQueryTabBody.displayName = 'HostsQueryTabBody'; diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/index.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/index.tsx index 40227ec24a9e..a37ddd0bd61d 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/index.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/index.tsx @@ -81,9 +81,11 @@ export const EndpointDetailsFlyout = memo(() => { > -

- {loading ? : details?.host?.hostname} -

+ {loading ? ( + + ) : ( +

{details?.host?.hostname}

+ )}
{details === undefined ? ( diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx index 5ab9df79ee14..6e3736793046 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx @@ -246,7 +246,8 @@ describe('when on the list page', () => { }); }); - describe('when polling on Endpoint List', () => { + // FLAKY: https://github.com/elastic/kibana/issues/75721 + describe.skip('when polling on Endpoint List', () => { beforeEach(async () => { await reactTestingLibrary.act(() => { const hostListData = mockEndpointResultList({ total: 4 }).hosts; diff --git a/x-pack/plugins/security_solution/public/overview/components/recent_cases/no_cases/index.test.tsx b/x-pack/plugins/security_solution/public/overview/components/recent_cases/no_cases/index.test.tsx index 99902a31975d..446679ae26d9 100644 --- a/x-pack/plugins/security_solution/public/overview/components/recent_cases/no_cases/index.test.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/recent_cases/no_cases/index.test.tsx @@ -9,29 +9,19 @@ import { mount } from 'enzyme'; import { useKibana } from '../../../../common/lib/kibana'; import '../../../../common/mock/match_media'; -import { createUseKibanaMock, TestProviders } from '../../../../common/mock'; +import { TestProviders } from '../../../../common/mock'; import { NoCases } from '.'; jest.mock('../../../../common/lib/kibana'); -const useKibanaMock = useKibana as jest.Mock; - -let navigateToApp: jest.Mock; +const useKibanaMock = useKibana as jest.Mocked; describe('RecentCases', () => { + let navigateToApp: jest.Mock; + beforeEach(() => { - jest.resetAllMocks(); navigateToApp = jest.fn(); - const kibanaMock = createUseKibanaMock()(); - useKibanaMock.mockReturnValue({ - ...kibanaMock, - services: { - application: { - navigateToApp, - getUrlForApp: jest.fn(), - }, - }, - }); + useKibanaMock().services.application.navigateToApp = navigateToApp; }); it('if no cases, you should be able to create a case by clicking on the link "start a new case"', () => { diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/data_providers.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/data_providers.test.tsx index 754d7f9c47ed..d48be25b0889 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/data_providers.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/data_providers.test.tsx @@ -15,8 +15,9 @@ import { DataProvider } from './data_provider'; import { mockDataProviders } from './mock/mock_data_providers'; import { ManageGlobalTimeline, getTimelineDefaults } from '../../manage_timeline'; import { FilterManager } from '../../../../../../../../src/plugins/data/public/query/filter_manager'; -import { createKibanaCoreStartMock } from '../../../../common/mock/kibana_core'; -const mockUiSettingsForFilterManager = createKibanaCoreStartMock().uiSettings; +import { coreMock } from '../../../../../../../../src/core/public/mocks'; + +const mockUiSettingsForFilterManager = coreMock.createStart().uiSettings; const filterManager = new FilterManager(mockUiSettingsForFilterManager); describe('DataProviders', () => { diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/providers.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/providers.test.tsx index b788f70cb2e4..3f371349aa75 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/providers.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/providers.test.tsx @@ -7,7 +7,7 @@ import { shallow } from 'enzyme'; import React from 'react'; -import { createKibanaCoreStartMock } from '../../../../common/mock/kibana_core'; +import { coreMock } from '../../../../../../../../src/core/public/mocks'; import { TestProviders } from '../../../../common/mock/test_providers'; import { DroppableWrapper } from '../../../../common/components/drag_and_drop/droppable_wrapper'; import { FilterManager } from '../../../../../../../../src/plugins/data/public'; @@ -18,7 +18,7 @@ import { DELETE_CLASS_NAME, ENABLE_CLASS_NAME, EXCLUDE_CLASS_NAME } from './prov import { useMountAppended } from '../../../../common/utils/use_mount_appended'; import { ManageGlobalTimeline, getTimelineDefaults } from '../../manage_timeline'; -const mockUiSettingsForFilterManager = createKibanaCoreStartMock().uiSettings; +const mockUiSettingsForFilterManager = coreMock.createStart().uiSettings; describe('Providers', () => { const isLoading: boolean = true; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/header/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/header/index.test.tsx index e7b0ce7b7428..329bcf24ba7e 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/header/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/header/index.test.tsx @@ -7,8 +7,8 @@ import { shallow } from 'enzyme'; import React from 'react'; +import { coreMock } from '../../../../../../../../src/core/public/mocks'; import { mockIndexPattern } from '../../../../common/mock'; -import { createKibanaCoreStartMock } from '../../../../common/mock/kibana_core'; import { TestProviders } from '../../../../common/mock/test_providers'; import { FilterManager } from '../../../../../../../../src/plugins/data/public'; import { mockDataProviders } from '../data_providers/mock/mock_data_providers'; @@ -17,7 +17,7 @@ import { useMountAppended } from '../../../../common/utils/use_mount_appended'; import { TimelineHeader } from '.'; import { TimelineStatus, TimelineType } from '../../../../../common/types/timeline'; -const mockUiSettingsForFilterManager = createKibanaCoreStartMock().uiSettings; +const mockUiSettingsForFilterManager = coreMock.createStart().uiSettings; jest.mock('../../../../common/lib/kibana'); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/query_bar/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/query_bar/index.test.tsx index 75f684c629c7..6c8fd4975c65 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/query_bar/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/query_bar/index.test.tsx @@ -7,11 +7,11 @@ import { mount } from 'enzyme'; import React from 'react'; +import { coreMock } from '../../../../../../../../src/core/public/mocks'; import { DEFAULT_FROM, DEFAULT_TO } from '../../../../../common/constants'; import { mockBrowserFields } from '../../../../common/containers/source/mock'; import { convertKueryToElasticSearchQuery } from '../../../../common/lib/keury'; import { mockIndexPattern, TestProviders } from '../../../../common/mock'; -import { createKibanaCoreStartMock } from '../../../../common/mock/kibana_core'; import { QueryBar } from '../../../../common/components/query_bar'; import { FilterManager } from '../../../../../../../../src/plugins/data/public'; import { mockDataProviders } from '../data_providers/mock/mock_data_providers'; @@ -19,7 +19,7 @@ import { buildGlobalQuery } from '../helpers'; import { QueryBarTimeline, QueryBarTimelineComponentProps, getDataProviderFilter } from './index'; -const mockUiSettingsForFilterManager = createKibanaCoreStartMock().uiSettings; +const mockUiSettingsForFilterManager = coreMock.createStart().uiSettings; jest.mock('../../../../common/lib/kibana'); diff --git a/x-pack/plugins/security_solution/public/timelines/containers/local_storage/index.test.ts b/x-pack/plugins/security_solution/public/timelines/containers/local_storage/index.test.ts index e1bccbdff488..7a8750b279b8 100644 --- a/x-pack/plugins/security_solution/public/timelines/containers/local_storage/index.test.ts +++ b/x-pack/plugins/security_solution/public/timelines/containers/local_storage/index.test.ts @@ -15,23 +15,16 @@ import { import { TimelineId } from '../../../../common/types/timeline'; import { mockTimelineModel, createSecuritySolutionStorageMock } from '../../../common/mock'; import { useKibana } from '../../../common/lib/kibana'; -import { createUseKibanaMock } from '../../../common/mock/kibana_react'; jest.mock('../../../common/lib/kibana'); -const useKibanaMock = useKibana as jest.Mock; +const useKibanaMock = useKibana as jest.Mocked; describe('SiemLocalStorage', () => { const { localStorage, storage } = createSecuritySolutionStorageMock(); beforeEach(() => { - jest.resetAllMocks(); - useKibanaMock.mockImplementation(() => ({ - services: { - ...createUseKibanaMock()().services, - storage, - }, - })); + useKibanaMock().services.storage = storage; localStorage.clear(); }); diff --git a/x-pack/plugins/security_solution/server/plugin.ts b/x-pack/plugins/security_solution/server/plugin.ts index 85764eaaee32..25ca89ce9186 100644 --- a/x-pack/plugins/security_solution/server/plugin.ts +++ b/x-pack/plugins/security_solution/server/plugin.ts @@ -17,6 +17,8 @@ import { PluginInitializerContext, SavedObjectsClient, } from '../../../../src/core/server'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { DataPluginSetup, DataPluginStart } from '../../../../src/plugins/data/server/plugin'; import { UsageCollectionSetup } from '../../../../src/plugins/usage_collection/server'; import { PluginSetupContract as AlertingSetup } from '../../alerts/server'; import { SecurityPluginSetup as SecuritySetup } from '../../security/server'; @@ -58,9 +60,11 @@ import { EndpointAppContext } from './endpoint/types'; import { registerDownloadExceptionListRoute } from './endpoint/routes/artifacts'; import { initUsageCollectors } from './usage'; import { AppRequestContext } from './types'; +import { securitySolutionSearchStrategyProvider } from './search_strategy/security_solution'; export interface SetupPlugins { alerts: AlertingSetup; + data: DataPluginSetup; encryptedSavedObjects?: EncryptedSavedObjectsSetup; features: FeaturesSetup; licensing: LicensingPluginSetup; @@ -73,6 +77,7 @@ export interface SetupPlugins { } export interface StartPlugins { + data: DataPluginStart; ingestManager?: IngestManagerStartContract; taskManager?: TaskManagerStartContract; } @@ -263,6 +268,14 @@ export class Plugin implements IPlugin { + const securitySolutionSearchStrategy = securitySolutionSearchStrategyProvider(depsStart.data); + plugins.data.search.registerSearchStrategy( + 'securitySolutionSearchStrategy', + securitySolutionSearchStrategy + ); + }); + return {}; } diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/dsl/query.detail_host.dsl.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/dsl/query.detail_host.dsl.ts new file mode 100644 index 000000000000..5c5dec92a510 --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/dsl/query.detail_host.dsl.ts @@ -0,0 +1,49 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ISearchRequestParams } from '../../../../../../../../../src/plugins/data/common'; +import { HostOverviewRequestOptions } from '../../../../../../common/search_strategy/security_solution'; +import { cloudFieldsMap, hostFieldsMap } from '../../../../../lib/ecs_fields'; +import { buildFieldsTermAggregation } from '../../../../../lib/hosts/helpers'; +import { reduceFields } from '../../../../../utils/build_query/reduce_fields'; + +export const buildHostOverviewQuery = ({ + fields, + hostName, + defaultIndex, + timerange: { from, to }, +}: HostOverviewRequestOptions): ISearchRequestParams => { + const esFields = reduceFields(fields, { ...hostFieldsMap, ...cloudFieldsMap }); + + const filter = [ + { term: { 'host.name': hostName } }, + { + range: { + '@timestamp': { + format: 'strict_date_optional_time', + gte: from, + lte: to, + }, + }, + }, + ]; + + const dslQuery = { + allowNoIndices: true, + index: defaultIndex, + ignoreUnavailable: true, + body: { + aggregations: { + ...buildFieldsTermAggregation(esFields.filter((field) => !['@timestamp'].includes(field))), + }, + query: { bool: { filter } }, + size: 0, + track_total_hits: false, + }, + }; + + return dslQuery; +}; diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/dsl/query.hosts.dsl.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/dsl/query.hosts.dsl.ts new file mode 100644 index 000000000000..3d72f98f3535 --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/dsl/query.hosts.dsl.ts @@ -0,0 +1,89 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { isEmpty, isString } from 'lodash/fp'; +import { ISearchRequestParams } from '../../../../../../../../../src/plugins/data/common'; +import { + Direction, + HostsRequestOptions, + SortField, +} from '../../../../../../common/search_strategy/security_solution'; +import { assertUnreachable, createQueryFilterClauses } from '../../../../../utils/build_query'; + +export const buildHostsQuery = ({ + defaultIndex, + docValueFields, + filterQuery, + pagination: { querySize }, + sort, + timerange: { from, to }, +}: HostsRequestOptions): ISearchRequestParams => { + const filter = [ + ...createQueryFilterClauses(isString(filterQuery) ? JSON.parse(filterQuery) : filterQuery), + { + range: { + '@timestamp': { + gte: from, + lte: to, + format: 'strict_date_optional_time', + }, + }, + }, + ]; + + const agg = { host_count: { cardinality: { field: 'host.name' } } }; + + const dslQuery = { + allowNoIndices: true, + index: defaultIndex, + ignoreUnavailable: true, + body: { + ...(isEmpty(docValueFields) ? { docvalue_fields: docValueFields } : {}), + aggregations: { + ...agg, + host_data: { + terms: { size: querySize, field: 'host.name', order: getQueryOrder(sort) }, + aggs: { + lastSeen: { max: { field: '@timestamp' } }, + os: { + top_hits: { + size: 1, + sort: [ + { + '@timestamp': { + order: 'desc', + }, + }, + ], + _source: { + includes: ['host.os.*'], + }, + }, + }, + }, + }, + }, + query: { bool: { filter } }, + size: 0, + track_total_hits: false, + }, + }; + + return dslQuery; +}; + +type QueryOrder = { lastSeen: Direction } | { _key: Direction }; + +const getQueryOrder = (sort: SortField): QueryOrder => { + switch (sort.field) { + case 'lastSeen': + return { lastSeen: sort.direction }; + case 'hostName': + return { _key: sort.direction }; + default: + return assertUnreachable(sort.field); + } +}; diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/dsl/query.last_first_seen_host.dsl.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/dsl/query.last_first_seen_host.dsl.ts new file mode 100644 index 000000000000..b57bbd2960e4 --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/dsl/query.last_first_seen_host.dsl.ts @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { isEmpty } from 'lodash/fp'; +import { ISearchRequestParams } from '../../../../../../../../../src/plugins/data/common'; +import { HostLastFirstSeenRequestOptions } from '../../../../../../common/search_strategy/security_solution'; + +export const buildLastFirstSeenHostQuery = ({ + hostName, + defaultIndex, + docValueFields, +}: HostLastFirstSeenRequestOptions): ISearchRequestParams => { + const filter = [{ term: { 'host.name': hostName } }]; + + const dslQuery = { + allowNoIndices: true, + index: defaultIndex, + ignoreUnavailable: true, + body: { + ...(isEmpty(docValueFields) ? { docvalue_fields: docValueFields } : {}), + aggregations: { + firstSeen: { min: { field: '@timestamp' } }, + lastSeen: { max: { field: '@timestamp' } }, + }, + query: { bool: { filter } }, + size: 0, + track_total_hits: false, + }, + }; + + return dslQuery; +}; diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/helpers.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/helpers.ts new file mode 100644 index 000000000000..a7ec822839d2 --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/helpers.ts @@ -0,0 +1,115 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { set } from '@elastic/safer-lodash-set/fp'; +import { get, has, head } from 'lodash/fp'; +import { + HostsEdges, + HostItem, +} from '../../../../../common/search_strategy/security_solution/hosts'; +import { hostFieldsMap } from '../../../../lib/ecs_fields'; + +import { HostAggEsItem, HostBuckets, HostValue } from '../../../../lib/hosts/types'; + +const hostsFields = ['_id', 'lastSeen', 'host.id', 'host.name', 'host.os.name', 'host.os.version']; + +export const formatHostEdgesData = (bucket: HostAggEsItem): HostsEdges => + hostsFields.reduce( + (flattenedFields, fieldName) => { + const hostId = get('key', bucket); + flattenedFields.node._id = hostId || null; + flattenedFields.cursor.value = hostId || ''; + const fieldValue = getHostFieldValue(fieldName, bucket); + if (fieldValue != null) { + return set( + `node.${fieldName}`, + Array.isArray(fieldValue) ? fieldValue : [fieldValue], + flattenedFields + ); + } + return flattenedFields; + }, + { + node: {}, + cursor: { + value: '', + tiebreaker: null, + }, + } as HostsEdges + ); + +const hostFields = [ + '_id', + 'host.architecture', + 'host.id', + 'host.ip', + 'host.id', + 'host.mac', + 'host.name', + 'host.os.family', + 'host.os.name', + 'host.os.platform', + 'host.os.version', + 'host.type', + 'cloud.instance.id', + 'cloud.machine.type', + 'cloud.provider', + 'cloud.region', + 'endpoint.endpointPolicy', + 'endpoint.policyStatus', + 'endpoint.sensorVersion', +]; + +export const formatHostItem = (bucket: HostAggEsItem): HostItem => + hostFields.reduce((flattenedFields, fieldName) => { + const fieldValue = getHostFieldValue(fieldName, bucket); + if (fieldValue != null) { + return set(fieldName, fieldValue, flattenedFields); + } + return flattenedFields; + }, {}); + +const getHostFieldValue = (fieldName: string, bucket: HostAggEsItem): string | string[] | null => { + const aggField = hostFieldsMap[fieldName] + ? hostFieldsMap[fieldName].replace(/\./g, '_') + : fieldName.replace(/\./g, '_'); + if ( + [ + 'host.ip', + 'host.mac', + 'cloud.instance.id', + 'cloud.machine.type', + 'cloud.provider', + 'cloud.region', + ].includes(fieldName) && + has(aggField, bucket) + ) { + const data: HostBuckets = get(aggField, bucket); + return data.buckets.map((obj) => obj.key); + } else if (has(`${aggField}.buckets`, bucket)) { + return getFirstItem(get(`${aggField}`, bucket)); + } else if (has(aggField, bucket)) { + const valueObj: HostValue = get(aggField, bucket); + return valueObj.value_as_string; + } else if (['host.name', 'host.os.name', 'host.os.version'].includes(fieldName)) { + switch (fieldName) { + case 'host.name': + return get('key', bucket) || null; + case 'host.os.name': + return get('os.hits.hits[0]._source.host.os.name', bucket) || null; + case 'host.os.version': + return get('os.hits.hits[0]._source.host.os.version', bucket) || null; + } + } + return null; +}; + +const getFirstItem = (data: HostBuckets): string | null => { + const firstItem = head(data.buckets); + if (firstItem == null) { + return null; + } + return firstItem.key; +}; diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/index.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/index.ts new file mode 100644 index 000000000000..443e524d71ca --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/index.ts @@ -0,0 +1,92 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { get, getOr } from 'lodash/fp'; + +import { IEsSearchResponse } from '../../../../../../../../src/plugins/data/common'; + +import { DEFAULT_MAX_TABLE_QUERY_SIZE } from '../../../../../common/constants'; +import { FactoryQueryTypes } from '../../../../../common/search_strategy/security_solution'; +import { + HostsStrategyResponse, + HostOverviewStrategyResponse, + HostsQueries, + HostsRequestOptions, + HostOverviewRequestOptions, +} from '../../../../../common/search_strategy/security_solution/hosts'; + +// TO DO need to move all this types in common +import { HostAggEsData, HostAggEsItem } from '../../../../lib/hosts/types'; + +import { inspectStringifyObject } from '../../../../utils/build_query'; +import { SecuritySolutionFactory } from '../types'; +import { buildHostOverviewQuery } from './dsl/query.detail_host.dsl'; +import { buildHostsQuery } from './dsl/query.hosts.dsl'; +import { formatHostEdgesData, formatHostItem } from './helpers'; + +export const allHosts: SecuritySolutionFactory = { + buildDsl: (options: HostsRequestOptions) => { + if (options.pagination && options.pagination.querySize >= DEFAULT_MAX_TABLE_QUERY_SIZE) { + throw new Error(`No query size above ${DEFAULT_MAX_TABLE_QUERY_SIZE}`); + } + return buildHostsQuery(options); + }, + parse: async ( + options: HostsRequestOptions, + response: IEsSearchResponse + ): Promise => { + const { activePage, cursorStart, fakePossibleCount, querySize } = options.pagination; + const totalCount = getOr(0, 'aggregations.host_count.value', response.rawResponse); + const buckets: HostAggEsItem[] = getOr( + [], + 'aggregations.host_data.buckets', + response.rawResponse + ); + const hostsEdges = buckets.map((bucket) => formatHostEdgesData(bucket)); + const fakeTotalCount = fakePossibleCount <= totalCount ? fakePossibleCount : totalCount; + const edges = hostsEdges.splice(cursorStart, querySize - cursorStart); + const inspect = { + dsl: [inspectStringifyObject(buildHostsQuery(options))], + response: [inspectStringifyObject(response)], + }; + const showMorePagesIndicator = totalCount > fakeTotalCount; + + return { + ...response, + inspect, + edges, + totalCount, + pageInfo: { + activePage: activePage ? activePage : 0, + fakeTotalCount, + showMorePagesIndicator, + }, + }; + }, +}; + +export const overviewHost: SecuritySolutionFactory = { + buildDsl: (options: HostOverviewRequestOptions) => { + return buildHostOverviewQuery(options); + }, + parse: async ( + options: HostOverviewRequestOptions, + response: IEsSearchResponse + ): Promise => { + const aggregations: HostAggEsItem = get('aggregations', response.rawResponse) || {}; + const inspect = { + dsl: [inspectStringifyObject(buildHostOverviewQuery(options))], + response: [inspectStringifyObject(response)], + }; + const formattedHostItem = formatHostItem(aggregations); + return { ...response, inspect, _id: options.hostName, ...formattedHostItem }; + }, +}; + +export const hostsFactory: Record> = { + [HostsQueries.hosts]: allHosts, + [HostsQueries.hostOverview]: overviewHost, +}; diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/index.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/index.ts new file mode 100644 index 000000000000..53433dfc208c --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { FactoryQueryTypes } from '../../../../common/search_strategy/security_solution'; + +import { hostsFactory } from './hosts'; +import { SecuritySolutionFactory } from './types'; + +export const securitySolutionFactory: Record< + FactoryQueryTypes, + SecuritySolutionFactory +> = { + ...hostsFactory, +}; diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/types.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/types.ts new file mode 100644 index 000000000000..cb9e3a3d7628 --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/types.ts @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + IEsSearchResponse, + ISearchRequestParams, +} from '../../../../../../../src/plugins/data/common'; +import { + FactoryQueryTypes, + StrategyRequestType, + StrategyResponseType, +} from '../../../../common/search_strategy/security_solution'; + +export interface SecuritySolutionFactory { + buildDsl: (options: StrategyRequestType) => ISearchRequestParams; + parse: ( + options: StrategyRequestType, + response: IEsSearchResponse + ) => Promise>; +} diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/index.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/index.ts new file mode 100644 index 000000000000..d94a32174cd7 --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/index.ts @@ -0,0 +1,38 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ISearchStrategy, PluginStart } from '../../../../../../src/plugins/data/server'; +import { + FactoryQueryTypes, + StrategyResponseType, + StrategyRequestType, +} from '../../../common/search_strategy/security_solution'; +import { securitySolutionFactory } from './factory'; +import { SecuritySolutionFactory } from './factory/types'; + +export const securitySolutionSearchStrategyProvider = ( + data: PluginStart +): ISearchStrategy, StrategyResponseType> => { + const es = data.search.getSearchStrategy('es'); + + return { + search: async (context, request, options) => { + if (request.factoryQueryType == null) { + throw new Error('factoryQueryType is required'); + } + const queryFactory: SecuritySolutionFactory = + securitySolutionFactory[request.factoryQueryType]; + const dsl = queryFactory.buildDsl(request); + const esSearchRes = await es.search(context, { ...request, params: dsl }, options); + return queryFactory.parse(request, esSearchRes); + }, + cancel: async (context, id) => { + if (es.cancel) { + es.cancel(context, id); + } + }, + }; +}; diff --git a/x-pack/plugins/security_solution/server/utils/build_query/filters.ts b/x-pack/plugins/security_solution/server/utils/build_query/filters.ts index 95c9a975454f..ac736e8cb51e 100644 --- a/x-pack/plugins/security_solution/server/utils/build_query/filters.ts +++ b/x-pack/plugins/security_solution/server/utils/build_query/filters.ts @@ -8,5 +8,5 @@ import { isEmpty } from 'lodash/fp'; import { ESQuery } from '../../../common/typed_json'; -export const createQueryFilterClauses = (filterQuery: ESQuery | undefined) => +export const createQueryFilterClauses = (filterQuery: ESQuery | string | undefined) => !isEmpty(filterQuery) ? [filterQuery] : []; diff --git a/x-pack/plugins/security_solution/server/utils/build_query/index.ts b/x-pack/plugins/security_solution/server/utils/build_query/index.ts index c97e78aad2b6..233ba70968fa 100644 --- a/x-pack/plugins/security_solution/server/utils/build_query/index.ts +++ b/x-pack/plugins/security_solution/server/utils/build_query/index.ts @@ -10,7 +10,7 @@ export * from './merge_fields_with_hits'; export * from './calculate_timeseries_interval'; export const assertUnreachable = ( - x: never, + x: unknown, message: string = 'Unknown Field in switch statement' ): never => { throw new Error(`${message} ${x}`); diff --git a/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json b/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json index fd21b70660bb..5280f094e3a5 100644 --- a/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json +++ b/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json @@ -7,6 +7,13 @@ } } }, + "discoverEnhanced": { + "properties": { + "exploreDataInChartActionEnabled": { + "type": "boolean" + } + } + }, "app_search": { "properties": { "ui_viewed": { diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 6a7737d36700..da5392848475 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -526,6 +526,47 @@ "core.ui.securityNavList.label": "セキュリティ", "core.ui.welcomeErrorMessage": "Elasticが正常に読み込まれませんでした。詳細はサーバーアウトプットを確認してください。", "core.ui.welcomeMessage": "Elasticの読み込み中", + "core.ui_settings.params.darkModeText": "Kibana UI のダークモードを有効にします。この設定を適用するにはページの更新が必要です。", + "core.ui_settings.params.darkModeTitle": "ダークモード", + "core.ui_settings.params.dateFormat.dayOfWeekText": "週の初めの曜日を設定します", + "core.ui_settings.params.dateFormat.dayOfWeekTitle": "曜日", + "core.ui_settings.params.dateFormat.optionsLinkText": "フォーマット", + "core.ui_settings.params.dateFormat.scaled.intervalsLinkText": "ISO8601 間隔", + "core.ui_settings.params.dateFormat.scaledText": "時間ベースのデータが順番にレンダリングされ、フォーマットされたタイムスタンプが測定値の間隔に適応すべき状況で使用されるフォーマットを定義する値です。キーは {intervalsLink}。", + "core.ui_settings.params.dateFormat.scaledTitle": "スケーリングされたデータフォーマットです", + "core.ui_settings.params.dateFormat.timezoneText": "使用されるタイムゾーンです。{defaultOption} ではご使用のブラウザにより検知されたタイムゾーンが使用されます。", + "core.ui_settings.params.dateFormat.timezoneTitle": "データフォーマットのタイムゾーン", + "core.ui_settings.params.dateFormatText": "きちんとフォーマットされたデータを表示する際、この {formatLink} を使用します", + "core.ui_settings.params.dateFormatTitle": "データフォーマット", + "core.ui_settings.params.dateNanosFormatText": "Elasticsearch の {dateNanosLink} データタイプに使用されます", + "core.ui_settings.params.dateNanosFormatTitle": "ナノ秒フォーマットでの日付", + "core.ui_settings.params.dateNanosLinkTitle": "date_nanos", + "core.ui_settings.params.defaultRoute.defaultRouteIsRelativeValidationMessage": "相対 URL でなければなりません。", + "core.ui_settings.params.defaultRoute.defaultRouteText": "この設定は、Kibana 起動時のデフォルトのルートを設定します。この設定で、Kibana 起動時のランディングページを変更できます。経路は相対 URL でなければなりません。", + "core.ui_settings.params.defaultRoute.defaultRouteTitle": "デフォルトのルート", + "core.ui_settings.params.disableAnimationsText": "Kibana UI の不要なアニメーションをオフにします。変更を適用するにはページを更新してください。", + "core.ui_settings.params.disableAnimationsTitle": "アニメーションを無効にする", + "core.ui_settings.params.maxCellHeightText": "表のセルが使用する高さの上限です。この切り捨てを無効にするには 0 に設定します", + "core.ui_settings.params.maxCellHeightTitle": "表のセルの高さの上限", + "core.ui_settings.params.notifications.banner.markdownLinkText": "マークダウン対応", + "core.ui_settings.params.notifications.bannerLifetimeText": "バナー通知が画面に表示されるミリ秒単位での時間です。{infinityValue} に設定するとカウントダウンが無効になります。", + "core.ui_settings.params.notifications.bannerLifetimeTitle": "バナー通知時間", + "core.ui_settings.params.notifications.bannerText": "すべてのユーザーへの一時的な通知を目的としたカスタムバナーです。{markdownLink}", + "core.ui_settings.params.notifications.bannerTitle": "カスタムバナー通知", + "core.ui_settings.params.notifications.errorLifetimeText": "エラー通知が画面に表示されるミリ秒単位での時間です。{infinityValue} に設定すると無効になります。", + "core.ui_settings.params.notifications.errorLifetimeTitle": "エラー通知時間", + "core.ui_settings.params.notifications.infoLifetimeText": "情報通知が画面に表示されるミリ秒単位での時間です。{infinityValue} に設定すると無効になります。", + "core.ui_settings.params.notifications.infoLifetimeTitle": "情報通知時間", + "core.ui_settings.params.notifications.warningLifetimeText": "警告通知が画面に表示されるミリ秒単位での時間です。{infinityValue} に設定すると無効になります。", + "core.ui_settings.params.notifications.warningLifetimeTitle": "警告通知時間", + "core.ui_settings.params.pageNavigationDesc": "ナビゲーションのスタイルを変更", + "core.ui_settings.params.pageNavigationLegacy": "レガシー", + "core.ui_settings.params.pageNavigationModern": "モダン", + "core.ui_settings.params.pageNavigationName": "サイドナビゲーションスタイル", + "core.ui_settings.params.themeVersionText": "現在のバージョンと次のバージョンのKibanaで使用されるテーマを切り替えます。この設定を適用するにはページの更新が必要です。", + "core.ui_settings.params.themeVersionTitle": "テーマバージョン", + "core.ui_settings.params.storeUrlText": "URL は長くなりすぎてブラウザが対応できない場合があります。セッションストレージに URL の一部を保存することがで この問題に対処できるかテストしています。結果を教えてください!", + "core.ui_settings.params.storeUrlTitle": "セッションストレージに URL を格納", "dashboard.actions.toggleExpandPanelMenuItem.expandedDisplayName": "最小化", "dashboard.actions.toggleExpandPanelMenuItem.notExpandedDisplayName": "全画面", "dashboard.addExistingVisualizationLinkText": "既存のユーザーを追加", @@ -2734,47 +2775,6 @@ "inspector.requests.statisticsTabLabel": "統計", "inspector.title": "インスペクター", "inspector.view": "{viewName} を表示", - "kbn.advancedSettings.darkModeText": "Kibana UI のダークモードを有効にします。この設定を適用するにはページの更新が必要です。", - "kbn.advancedSettings.darkModeTitle": "ダークモード", - "kbn.advancedSettings.dateFormat.dayOfWeekText": "週の初めの曜日を設定します", - "kbn.advancedSettings.dateFormat.dayOfWeekTitle": "曜日", - "kbn.advancedSettings.dateFormat.optionsLinkText": "フォーマット", - "kbn.advancedSettings.dateFormat.scaled.intervalsLinkText": "ISO8601 間隔", - "kbn.advancedSettings.dateFormat.scaledText": "時間ベースのデータが順番にレンダリングされ、フォーマットされたタイムスタンプが測定値の間隔に適応すべき状況で使用されるフォーマットを定義する値です。キーは {intervalsLink}。", - "kbn.advancedSettings.dateFormat.scaledTitle": "スケーリングされたデータフォーマットです", - "kbn.advancedSettings.dateFormat.timezoneText": "使用されるタイムゾーンです。{defaultOption} ではご使用のブラウザにより検知されたタイムゾーンが使用されます。", - "kbn.advancedSettings.dateFormat.timezoneTitle": "データフォーマットのタイムゾーン", - "kbn.advancedSettings.dateFormatText": "きちんとフォーマットされたデータを表示する際、この {formatLink} を使用します", - "kbn.advancedSettings.dateFormatTitle": "データフォーマット", - "kbn.advancedSettings.dateNanosFormatText": "Elasticsearch の {dateNanosLink} データタイプに使用されます", - "kbn.advancedSettings.dateNanosFormatTitle": "ナノ秒フォーマットでの日付", - "kbn.advancedSettings.dateNanosLinkTitle": "date_nanos", - "kbn.advancedSettings.defaultRoute.defaultRouteIsRelativeValidationMessage": "相対 URL でなければなりません。", - "kbn.advancedSettings.defaultRoute.defaultRouteText": "この設定は、Kibana 起動時のデフォルトのルートを設定します。この設定で、Kibana 起動時のランディングページを変更できます。経路は相対 URL でなければなりません。", - "kbn.advancedSettings.defaultRoute.defaultRouteTitle": "デフォルトのルート", - "kbn.advancedSettings.disableAnimationsText": "Kibana UI の不要なアニメーションをオフにします。変更を適用するにはページを更新してください。", - "kbn.advancedSettings.disableAnimationsTitle": "アニメーションを無効にする", - "kbn.advancedSettings.maxCellHeightText": "表のセルが使用する高さの上限です。この切り捨てを無効にするには 0 に設定します", - "kbn.advancedSettings.maxCellHeightTitle": "表のセルの高さの上限", - "kbn.advancedSettings.notifications.banner.markdownLinkText": "マークダウン対応", - "kbn.advancedSettings.notifications.bannerLifetimeText": "バナー通知が画面に表示されるミリ秒単位での時間です。{infinityValue} に設定するとカウントダウンが無効になります。", - "kbn.advancedSettings.notifications.bannerLifetimeTitle": "バナー通知時間", - "kbn.advancedSettings.notifications.bannerText": "すべてのユーザーへの一時的な通知を目的としたカスタムバナーです。{markdownLink}", - "kbn.advancedSettings.notifications.bannerTitle": "カスタムバナー通知", - "kbn.advancedSettings.notifications.errorLifetimeText": "エラー通知が画面に表示されるミリ秒単位での時間です。{infinityValue} に設定すると無効になります。", - "kbn.advancedSettings.notifications.errorLifetimeTitle": "エラー通知時間", - "kbn.advancedSettings.notifications.infoLifetimeText": "情報通知が画面に表示されるミリ秒単位での時間です。{infinityValue} に設定すると無効になります。", - "kbn.advancedSettings.notifications.infoLifetimeTitle": "情報通知時間", - "kbn.advancedSettings.notifications.warningLifetimeText": "警告通知が画面に表示されるミリ秒単位での時間です。{infinityValue} に設定すると無効になります。", - "kbn.advancedSettings.notifications.warningLifetimeTitle": "警告通知時間", - "kbn.advancedSettings.pageNavigationDesc": "ナビゲーションのスタイルを変更", - "kbn.advancedSettings.pageNavigationLegacy": "レガシー", - "kbn.advancedSettings.pageNavigationModern": "モダン", - "kbn.advancedSettings.pageNavigationName": "サイドナビゲーションスタイル", - "kbn.advancedSettings.storeUrlText": "URL は長くなりすぎてブラウザが対応できない場合があります。セッションストレージに URL の一部を保存することがで この問題に対処できるかテストしています。結果を教えてください!", - "kbn.advancedSettings.storeUrlTitle": "セッションストレージに URL を格納", - "kbn.advancedSettings.themeVersionText": "現在のバージョンと次のバージョンのKibanaで使用されるテーマを切り替えます。この設定を適用するにはページの更新が必要です。", - "kbn.advancedSettings.themeVersionTitle": "テーマバージョン", "kbn.advancedSettings.visualization.showRegionMapWarningsText": "用語がマップの形に合わない場合に地域マップに警告を表示するかどうかです。", "kbn.advancedSettings.visualization.showRegionMapWarningsTitle": "地域マップに警告を表示", "kbn.advancedSettings.visualization.tileMap.maxPrecision.cellDimensionsLinkText": "ディメンションの説明", @@ -9879,7 +9879,6 @@ "xpack.lens.editorFrame.quickFunctionsLabel": "クイック機能", "xpack.lens.editorFrame.requiredDimensionWarningLabel": "必要な次元", "xpack.lens.editorFrame.suggestionPanelTitle": "提案", - "xpack.lens.editorFrame.tooltipContent": "レンズはベータ段階で、変更される可能性があります。 デザインとコードはオフィシャル GA 機能よりも完成度が低く、現状のまま保証なしで提供されています。ベータ機能にはオフィシャル GA 機能の SLA が適用されません", "xpack.lens.embeddable.failure": "ビジュアライゼーションを表示できませんでした", "xpack.lens.embeddableDisplayName": "レンズ", "xpack.lens.excludeValueButtonAriaLabel": "{value}を除外", @@ -9934,7 +9933,6 @@ "xpack.lens.indexPattern.fieldStatsButtonLabel": "フィールドプレビューを表示するには、クリックします。可視化するには、ドラッグアンドドロップします。", "xpack.lens.indexPattern.fieldStatsCountLabel": "カウント", "xpack.lens.indexPattern.fieldStatsDisplayToggle": "次のどちらかを切り替えます:", - "xpack.lens.indexPattern.fieldStatsNoData": "表示するデータがありません", "xpack.lens.indexPattern.fieldTimeDistributionLabel": "時間分布", "xpack.lens.indexPattern.fieldTopValuesLabel": "トップの値", "xpack.lens.indexPattern.groupByDropdown": "グループ分けの条件", @@ -10610,7 +10608,6 @@ "xpack.maps.style.customColorPaletteLabel": "カスタムカラーパレット", "xpack.maps.style.customColorRampLabel": "カスタマカラーランプ", "xpack.maps.style.fieldSelect.OriginLabel": "{fieldOrigin} からのフィールド", - "xpack.maps.style.heatmap.displayNameLabel": "ヒートマップスタイル", "xpack.maps.style.heatmap.resolutionStyleErrorMessage": "解像度パラメーターが認識されません: {resolution}", "xpack.maps.styles.categorical.otherCategoryLabel": "その他", "xpack.maps.styles.color.staticDynamicSelect.staticLabel": "塗りつぶし", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 4d0186141f38..e892ff228cd4 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -526,6 +526,47 @@ "core.ui.securityNavList.label": "安全", "core.ui.welcomeErrorMessage": "Elastic 未正确加载。检查服务器输出以了解详情。", "core.ui.welcomeMessage": "正在加载 Elastic", + "core.ui_settings.params.darkModeText": "为 Kibana UI 启用深色模式需要刷新页面,才能应用设置。", + "core.ui_settings.params.darkModeTitle": "深色模式", + "core.ui_settings.params.dateFormat.dayOfWeekText": "一周从哪一日开始?", + "core.ui_settings.params.dateFormat.dayOfWeekTitle": "周内日", + "core.ui_settings.params.dateFormat.optionsLinkText": "格式", + "core.ui_settings.params.dateFormat.scaled.intervalsLinkText": "ISO8601 时间间隔", + "core.ui_settings.params.dateFormat.scaledText": "定义在基于时间的数据按顺序呈现且格式化时间戳应适应度量时间间隔时所用格式的值。键是 {intervalsLink}。", + "core.ui_settings.params.dateFormat.scaledTitle": "缩放的日期格式", + "core.ui_settings.params.dateFormat.timezoneText": "应使用哪个时区。{defaultOption} 将使用您的浏览器检测到的时区。", + "core.ui_settings.params.dateFormat.timezoneTitle": "用于设置日期格式的时区", + "core.ui_settings.params.dateFormatText": "显示格式正确的日期时,请使用此{formatLink}", + "core.ui_settings.params.dateFormatTitle": "日期格式", + "core.ui_settings.params.dateNanosFormatText": "用于 Elasticsearch 的 {dateNanosLink} 数据类型", + "core.ui_settings.params.dateNanosFormatTitle": "纳秒格式的日期", + "core.ui_settings.params.dateNanosLinkTitle": "date_nanos", + "core.ui_settings.params.defaultRoute.defaultRouteIsRelativeValidationMessage": "必须是相对 URL。", + "core.ui_settings.params.defaultRoute.defaultRouteText": "此设置指定打开 Kibana 时的默认路由。您可以使用此设置修改打开 Kibana 时的登陆页面。路由必须是相对 URL。", + "core.ui_settings.params.defaultRoute.defaultRouteTitle": "默认路由", + "core.ui_settings.params.disableAnimationsText": "在 Kibana UI 中关闭所有没有必要的动画。刷新页面以应用更改。", + "core.ui_settings.params.disableAnimationsTitle": "禁用动画", + "core.ui_settings.params.maxCellHeightText": "表中单元格应占用的最大高度。设置为 0 可禁用截短", + "core.ui_settings.params.maxCellHeightTitle": "最大表单元格高度", + "core.ui_settings.params.notifications.banner.markdownLinkText": "Markdown 受支持", + "core.ui_settings.params.notifications.bannerLifetimeText": "在屏幕上显示横幅通知的时间(毫秒)。设置为 {infinityValue} 将禁用倒计时。", + "core.ui_settings.params.notifications.bannerLifetimeTitle": "横幅通知生存时间", + "core.ui_settings.params.notifications.bannerText": "用于向所有用户发送临时通知的定制横幅。{markdownLink}", + "core.ui_settings.params.notifications.bannerTitle": "定制横幅通知", + "core.ui_settings.params.notifications.errorLifetimeText": "在屏幕上显示错误通知的时间(毫秒)。设置为 {infinityValue} 将禁用。", + "core.ui_settings.params.notifications.errorLifetimeTitle": "错误通知生存时间", + "core.ui_settings.params.notifications.infoLifetimeText": "在屏幕上显示信息通知的时间(毫秒)。设置为 {infinityValue} 将禁用。", + "core.ui_settings.params.notifications.infoLifetimeTitle": "信息通知生存时间", + "core.ui_settings.params.notifications.warningLifetimeText": "在屏幕上显示警告通知的时间(毫秒)。设置为 {infinityValue} 将禁用。", + "core.ui_settings.params.notifications.warningLifetimeTitle": "警告通知生存时间", + "core.ui_settings.params.pageNavigationDesc": "更改导航样式", + "core.ui_settings.params.pageNavigationLegacy": "旧版", + "core.ui_settings.params.pageNavigationModern": "现代", + "core.ui_settings.params.pageNavigationName": "侧边导航样式", + "core.ui_settings.params.themeVersionText": "在用于 Kibana 当前和下一版本的主题间切换。需要刷新页面,才能应用设置。", + "core.ui_settings.params.themeVersionTitle": "主题版本", + "core.ui_settings.params.storeUrlText": "URL 有时会变得过长,以使得某些浏览器无法处理。为此,我们正在测试将 URL 的各个组成部分存储在会话存储中是否会有帮助。请告知我们这样做的效果!", + "core.ui_settings.params.storeUrlTitle": "将 URL 存储在会话存储中", "dashboard.actions.toggleExpandPanelMenuItem.expandedDisplayName": "最小化", "dashboard.actions.toggleExpandPanelMenuItem.notExpandedDisplayName": "全屏", "dashboard.addExistingVisualizationLinkText": "将现有", @@ -2735,47 +2776,6 @@ "inspector.requests.statisticsTabLabel": "统计信息", "inspector.title": "检查器", "inspector.view": "视图:{viewName}", - "kbn.advancedSettings.darkModeText": "为 Kibana UI 启用深色模式需要刷新页面,才能应用设置。", - "kbn.advancedSettings.darkModeTitle": "深色模式", - "kbn.advancedSettings.dateFormat.dayOfWeekText": "一周从哪一日开始?", - "kbn.advancedSettings.dateFormat.dayOfWeekTitle": "周内日", - "kbn.advancedSettings.dateFormat.optionsLinkText": "格式", - "kbn.advancedSettings.dateFormat.scaled.intervalsLinkText": "ISO8601 时间间隔", - "kbn.advancedSettings.dateFormat.scaledText": "定义在基于时间的数据按顺序呈现且格式化时间戳应适应度量时间间隔时所用格式的值。键是 {intervalsLink}。", - "kbn.advancedSettings.dateFormat.scaledTitle": "缩放的日期格式", - "kbn.advancedSettings.dateFormat.timezoneText": "应使用哪个时区。{defaultOption} 将使用您的浏览器检测到的时区。", - "kbn.advancedSettings.dateFormat.timezoneTitle": "用于设置日期格式的时区", - "kbn.advancedSettings.dateFormatText": "显示格式正确的日期时,请使用此{formatLink}", - "kbn.advancedSettings.dateFormatTitle": "日期格式", - "kbn.advancedSettings.dateNanosFormatText": "用于 Elasticsearch 的 {dateNanosLink} 数据类型", - "kbn.advancedSettings.dateNanosFormatTitle": "纳秒格式的日期", - "kbn.advancedSettings.dateNanosLinkTitle": "date_nanos", - "kbn.advancedSettings.defaultRoute.defaultRouteIsRelativeValidationMessage": "必须是相对 URL。", - "kbn.advancedSettings.defaultRoute.defaultRouteText": "此设置指定打开 Kibana 时的默认路由。您可以使用此设置修改打开 Kibana 时的登陆页面。路由必须是相对 URL。", - "kbn.advancedSettings.defaultRoute.defaultRouteTitle": "默认路由", - "kbn.advancedSettings.disableAnimationsText": "在 Kibana UI 中关闭所有没有必要的动画。刷新页面以应用更改。", - "kbn.advancedSettings.disableAnimationsTitle": "禁用动画", - "kbn.advancedSettings.maxCellHeightText": "表中单元格应占用的最大高度。设置为 0 可禁用截短", - "kbn.advancedSettings.maxCellHeightTitle": "最大表单元格高度", - "kbn.advancedSettings.notifications.banner.markdownLinkText": "Markdown 受支持", - "kbn.advancedSettings.notifications.bannerLifetimeText": "在屏幕上显示横幅通知的时间(毫秒)。设置为 {infinityValue} 将禁用倒计时。", - "kbn.advancedSettings.notifications.bannerLifetimeTitle": "横幅通知生存时间", - "kbn.advancedSettings.notifications.bannerText": "用于向所有用户发送临时通知的定制横幅。{markdownLink}", - "kbn.advancedSettings.notifications.bannerTitle": "定制横幅通知", - "kbn.advancedSettings.notifications.errorLifetimeText": "在屏幕上显示错误通知的时间(毫秒)。设置为 {infinityValue} 将禁用。", - "kbn.advancedSettings.notifications.errorLifetimeTitle": "错误通知生存时间", - "kbn.advancedSettings.notifications.infoLifetimeText": "在屏幕上显示信息通知的时间(毫秒)。设置为 {infinityValue} 将禁用。", - "kbn.advancedSettings.notifications.infoLifetimeTitle": "信息通知生存时间", - "kbn.advancedSettings.notifications.warningLifetimeText": "在屏幕上显示警告通知的时间(毫秒)。设置为 {infinityValue} 将禁用。", - "kbn.advancedSettings.notifications.warningLifetimeTitle": "警告通知生存时间", - "kbn.advancedSettings.pageNavigationDesc": "更改导航样式", - "kbn.advancedSettings.pageNavigationLegacy": "旧版", - "kbn.advancedSettings.pageNavigationModern": "现代", - "kbn.advancedSettings.pageNavigationName": "侧边导航样式", - "kbn.advancedSettings.storeUrlText": "URL 有时会变得过长,以使得某些浏览器无法处理。为此,我们正在测试将 URL 的各个组成部分存储在会话存储中是否会有帮助。请告知我们这样做的效果!", - "kbn.advancedSettings.storeUrlTitle": "将 URL 存储在会话存储中", - "kbn.advancedSettings.themeVersionText": "在用于 Kibana 当前和下一版本的主题间切换。需要刷新页面,才能应用设置。", - "kbn.advancedSettings.themeVersionTitle": "主题版本", "kbn.advancedSettings.visualization.showRegionMapWarningsText": "词无法联接到地图上的形状时,区域地图是否显示警告。", "kbn.advancedSettings.visualization.showRegionMapWarningsTitle": "显示区域地图警告", "kbn.advancedSettings.visualization.tileMap.maxPrecision.cellDimensionsLinkText": "单元格维度的解释", @@ -9882,7 +9882,6 @@ "xpack.lens.editorFrame.quickFunctionsLabel": "快选函数", "xpack.lens.editorFrame.requiredDimensionWarningLabel": "所需尺寸", "xpack.lens.editorFrame.suggestionPanelTitle": "建议", - "xpack.lens.editorFrame.tooltipContent": "Lens 为公测版,可能会进行更改。 设计和代码相对于正式发行版功能还不够成熟,将按原样提供,且不提供任何保证。公测版功能不受正式发行版功能支持 SLA 的约束", "xpack.lens.embeddable.failure": "无法显示可视化", "xpack.lens.embeddableDisplayName": "lens", "xpack.lens.excludeValueButtonAriaLabel": "排除 {value}", @@ -9937,7 +9936,6 @@ "xpack.lens.indexPattern.fieldStatsButtonLabel": "单击以进行字段预览,或拖放以进行可视化。", "xpack.lens.indexPattern.fieldStatsCountLabel": "计数", "xpack.lens.indexPattern.fieldStatsDisplayToggle": "切换", - "xpack.lens.indexPattern.fieldStatsNoData": "没有可显示的数据", "xpack.lens.indexPattern.fieldTimeDistributionLabel": "时间分布", "xpack.lens.indexPattern.fieldTopValuesLabel": "排名最前值", "xpack.lens.indexPattern.groupByDropdown": "分组依据", @@ -10613,7 +10611,6 @@ "xpack.maps.style.customColorPaletteLabel": "定制调色板", "xpack.maps.style.customColorRampLabel": "定制颜色渐变", "xpack.maps.style.fieldSelect.OriginLabel": "来自 {fieldOrigin} 的字段", - "xpack.maps.style.heatmap.displayNameLabel": "热图样式", "xpack.maps.style.heatmap.resolutionStyleErrorMessage": "无法识别分辨率参数:{resolution}", "xpack.maps.styles.categorical.otherCategoryLabel": "其他", "xpack.maps.styles.color.staticDynamicSelect.staticLabel": "纯色", diff --git a/x-pack/plugins/ui_actions_enhanced/README.md b/x-pack/plugins/ui_actions_enhanced/README.md index 1a72a431e397..a4a37b559ff8 100644 --- a/x-pack/plugins/ui_actions_enhanced/README.md +++ b/x-pack/plugins/ui_actions_enhanced/README.md @@ -1,3 +1,5 @@ # `ui_actions_enhanced` +Registers commercially licensed generic actions like per panel time range and contains some code that supports drilldown work. + - [__Dashboard drilldown user docs__](https://www.elastic.co/guide/en/kibana/master/drilldowns.html) diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/jira.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/jira.ts index a0ba5331105b..78831fe8ff06 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/jira.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/jira.ts @@ -78,7 +78,8 @@ export default function jiraTest({ getService }: FtrProviderContext) { let proxyServer: any; let proxyHaveBeenCalled = false; - describe('Jira', () => { + // FLAKY: https://github.com/elastic/kibana/issues/75722 + describe.skip('Jira', () => { before(() => { jiraSimulatorURL = kibanaServer.resolveUrl( getExternalServiceSimulatorPath(ExternalServiceSimulator.JIRA) diff --git a/x-pack/test/api_integration/apis/management/index_management/data_streams.ts b/x-pack/test/api_integration/apis/management/index_management/data_streams.ts index f8ddca374209..f4b947336e04 100644 --- a/x-pack/test/api_integration/apis/management/index_management/data_streams.ts +++ b/x-pack/test/api_integration/apis/management/index_management/data_streams.ts @@ -51,6 +51,14 @@ export default function ({ getService }: FtrProviderContext) { await deleteComposableIndexTemplate(name); }; + const assertDataStreamStorageSizeExists = (storageSize: string) => { + // Storage size of a document doesn't like it would be deterministic (could vary depending + // on how ES, Lucene, and the file system interact), so we'll just assert its presence and + // type. + expect(storageSize).to.be.ok(); + expect(typeof storageSize).to.be('string'); + }; + describe('Data streams', function () { describe('Get', () => { const testDataStreamName = 'test-data-stream'; @@ -77,10 +85,40 @@ export default function ({ getService }: FtrProviderContext) { }, ], generation: 1, + health: 'yellow', + indexTemplateName: testDataStreamName, }, ]); }); + it('includes stats when provided the includeStats query parameter', async () => { + const { body: dataStreams } = await supertest + .get(`${API_BASE_PATH}/data_streams?includeStats=true`) + .set('kbn-xsrf', 'xxx') + .expect(200); + + // ES determines these values so we'll just echo them back. + const { name: indexName, uuid } = dataStreams[0].indices[0]; + const { storageSize, ...dataStreamWithoutStorageSize } = dataStreams[0]; + assertDataStreamStorageSizeExists(storageSize); + + expect(dataStreams.length).to.be(1); + expect(dataStreamWithoutStorageSize).to.eql({ + name: testDataStreamName, + timeStampField: { name: '@timestamp' }, + indices: [ + { + name: indexName, + uuid, + }, + ], + generation: 1, + health: 'yellow', + indexTemplateName: testDataStreamName, + maxTimeStamp: 0, + }); + }); + it('returns a single data stream by ID', async () => { const { body: dataStream } = await supertest .get(`${API_BASE_PATH}/data_streams/${testDataStreamName}`) @@ -89,7 +127,10 @@ export default function ({ getService }: FtrProviderContext) { // ES determines these values so we'll just echo them back. const { name: indexName, uuid } = dataStream.indices[0]; - expect(dataStream).to.eql({ + const { storageSize, ...dataStreamWithoutStorageSize } = dataStream; + assertDataStreamStorageSizeExists(storageSize); + + expect(dataStreamWithoutStorageSize).to.eql({ name: testDataStreamName, timeStampField: { name: '@timestamp' }, indices: [ @@ -99,6 +140,9 @@ export default function ({ getService }: FtrProviderContext) { }, ], generation: 1, + health: 'yellow', + indexTemplateName: testDataStreamName, + maxTimeStamp: 0, }); }); }); diff --git a/x-pack/test/api_integration/apis/security/basic_login.js b/x-pack/test/api_integration/apis/security/basic_login.js index 70eddc9aee4d..db58f17582c6 100644 --- a/x-pack/test/api_integration/apis/security/basic_login.js +++ b/x-pack/test/api_integration/apis/security/basic_login.js @@ -15,7 +15,8 @@ export default function ({ getService }) { const validUsername = kibanaServerConfig.username; const validPassword = kibanaServerConfig.password; - describe('Basic authentication', () => { + // Failing: See https://github.com/elastic/kibana/issues/75707 + describe.skip('Basic authentication', () => { it('should redirect non-AJAX requests to the login page if not authenticated', async () => { const response = await supertest.get('/abc/xyz').expect(302); diff --git a/x-pack/test/functional/apps/reporting_management/report_listing.ts b/x-pack/test/functional/apps/reporting_management/report_listing.ts index 6662bfb17949..e58b12fafb11 100644 --- a/x-pack/test/functional/apps/reporting_management/report_listing.ts +++ b/x-pack/test/functional/apps/reporting_management/report_listing.ts @@ -5,20 +5,8 @@ */ import expect from '@kbn/expect'; -import { WebElementWrapper } from 'test/functional/services/lib/web_element_wrapper'; import { FtrProviderContext } from '../../ftr_provider_context'; -const getTableTextFromElement = async (tableEl: WebElementWrapper) => { - const rows = await tableEl.findAllByCssSelector('tbody tr'); - return ( - await Promise.all( - rows.map(async (row) => { - return await row.getVisibleText(); - }) - ) - ).join('\n'); -}; - export default ({ getPageObjects, getService }: FtrProviderContext) => { const pageObjects = getPageObjects(['common', 'reporting']); const log = getService('log'); @@ -28,8 +16,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { const testSubjects = getService('testSubjects'); const esArchiver = getService('esArchiver'); - // FLAKY: https://github.com/elastic/kibana/issues/75044 - describe.skip('Listing of Reports', function () { + describe('Listing of Reports', function () { before(async () => { await security.testUser.setRoles(['kibana_admin', 'reporting_user']); await esArchiver.load('empty_kibana'); @@ -69,60 +56,20 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { }); it('Paginates historical reports', async () => { - // wait for first row of page 1 - await testSubjects.find('checkboxSelectRow-k9a9xlwl0gpe1457b10rraq3'); - - const previousButton = await testSubjects.find('pagination-button-previous'); - // previous CAN NOT be clicked + const previousButton = await testSubjects.find('pagination-button-previous'); expect(await previousButton.getAttribute('disabled')).to.be('true'); - // scan page 1 - let tableText = await getTableTextFromElement(await testSubjects.find('reportJobListing')); - const PAGE_CONTENT_1 = `[Logs] File Type Scatter Plot\nvisualization\n2020-04-21 @ 07:01 PM\ntest_user\nCompleted at 2020-04-21 @ 07:02 PM -[Logs] File Type Scatter Plot\nvisualization\n2020-04-21 @ 07:01 PM\ntest_user\nCompleted at 2020-04-21 @ 07:02 PM -[Logs] Heatmap\nvisualization\n2020-04-21 @ 07:00 PM\ntest_user\nCompleted at 2020-04-21 @ 07:01 PM -[Logs] Heatmap\nvisualization\n2020-04-21 @ 07:00 PM\ntest_user\nCompleted at 2020-04-21 @ 07:01 PM -[Flights] Flight Delays\nvisualization\n2020-04-21 @ 07:00 PM\ntest_user\nCompleted at 2020-04-21 @ 07:01 PM -[Flights] Flight Delays\nvisualization\n2020-04-21 @ 07:00 PM\ntest_user\nCompleted at 2020-04-21 @ 07:01 PM -pdf\ndashboard\n2020-04-21 @ 07:00 PM\ntest_user\nCompleted at 2020-04-21 @ 07:00 PM -pdf\ndashboard\n2020-04-21 @ 07:00 PM\ntest_user\nCompleted at 2020-04-21 @ 07:00 PM -[Flights] Flight Cancellations\nvisualization\n2020-04-21 @ 06:59 PM\ntest_user\nCompleted at 2020-04-21 @ 07:00 PM -[Flights] Markdown Instructions\nvisualization\n2020-04-21 @ 06:59 PM\ntest_user\nCompleted at 2020-04-21 @ 07:00 PM`; - expect(tableText).to.be(PAGE_CONTENT_1); + await testSubjects.find('checkboxSelectRow-k9a9xlwl0gpe1457b10rraq3'); // find first row of page 1 - // click page 2 - await testSubjects.click('pagination-button-1'); + await testSubjects.click('pagination-button-1'); // click page 2 + await testSubjects.find('checkboxSelectRow-k9a9uc4x0gpe1457b16wthc8'); // wait for first row of page 2 - // wait for first row of page 2 - await testSubjects.find('checkboxSelectRow-k9a9uc4x0gpe1457b16wthc8'); + await testSubjects.click('pagination-button-2'); // click page 3 + await testSubjects.find('checkboxSelectRow-k9a9p1840gpe1457b1ghfxw5'); // wait for first row of page 3 // previous CAN be clicked expect(await previousButton.getAttribute('disabled')).to.be(null); - - // scan page 2 - tableText = await getTableTextFromElement(await testSubjects.find('reportJobListing')); - const PAGE_CONTENT_2 = `[eCommerce] Revenue Tracking\ncanvas workpad\n2020-04-21 @ 06:58 PM\ntest_user\nCompleted at 2020-04-21 @ 06:59 PM -[Logs] Web Traffic\ncanvas workpad\n2020-04-21 @ 06:58 PM\ntest_user\nCompleted at 2020-04-21 @ 06:59 PM -[Flights] Overview\ncanvas workpad\n2020-04-21 @ 06:58 PM\ntest_user\nCompleted at 2020-04-21 @ 06:59 PM -[eCommerce] Revenue Dashboard\ndashboard\n2020-04-21 @ 06:57 PM\ntest_user\nCompleted at 2020-04-21 @ 06:58 PM -[Logs] Web Traffic\ndashboard\n2020-04-21 @ 06:57 PM\ntest_user\nCompleted at 2020-04-21 @ 06:58 PM -[Flights] Global Flight Dashboard\ndashboard\n2020-04-21 @ 06:56 PM\ntest_user\nCompleted at 2020-04-21 @ 06:57 PM -[Flights] Global Flight Dashboard\ndashboard\n2020-04-21 @ 06:56 PM\ntest_user\nCompleted at 2020-04-21 @ 06:57 PM -report4csv\n2020-04-21 @ 06:55 PM\ntest_user\nCompleted at 2020-04-21 @ 06:56 PM - Max size reached\nreport3csv\n2020-04-21 @ 06:55 PM -test_user\nCompleted at 2020-04-21 @ 06:55 PM - Max size reached\nreport2csv\n2020-04-21 @ 06:54 PM\ntest_user\nCompleted at 2020-04-21 @ 06:55 PM - Max size reached`; - expect(tableText).to.be(PAGE_CONTENT_2); - - // click page 3 - await testSubjects.click('pagination-button-2'); - - // wait for first row of page 3 - await testSubjects.find('checkboxSelectRow-k9a9p1840gpe1457b1ghfxw5'); - - // scan page 3 - tableText = await getTableTextFromElement(await testSubjects.find('reportJobListing')); - const PAGE_CONTENT_3 = `report1csv\n2020-04-21 @ 06:54 PM\ntest_user\nCompleted at 2020-04-21 @ 06:54 PM - Max size reached`; - expect(tableText).to.be(PAGE_CONTENT_3); }); }); }; diff --git a/x-pack/test/ingest_manager_api_integration/apis/epm/index.js b/x-pack/test/ingest_manager_api_integration/apis/epm/index.js index 0a259cb96bf5..855581424590 100644 --- a/x-pack/test/ingest_manager_api_integration/apis/epm/index.js +++ b/x-pack/test/ingest_manager_api_integration/apis/epm/index.js @@ -17,5 +17,6 @@ export default function loadTests({ loadTestFile }) { loadTestFile(require.resolve('./install_update')); loadTestFile(require.resolve('./update_assets')); loadTestFile(require.resolve('./data_stream')); + loadTestFile(require.resolve('./package_install_complete')); }); } diff --git a/x-pack/test/ingest_manager_api_integration/apis/epm/install_remove_assets.ts b/x-pack/test/ingest_manager_api_integration/apis/epm/install_remove_assets.ts index e575d7b68030..c7cfee565b2e 100644 --- a/x-pack/test/ingest_manager_api_integration/apis/epm/install_remove_assets.ts +++ b/x-pack/test/ingest_manager_api_integration/apis/epm/install_remove_assets.ts @@ -170,6 +170,9 @@ export default function (providerContext: FtrProviderContext) { version: '0.1.0', internal: false, removable: true, + install_version: '0.1.0', + install_status: 'installed', + install_started_at: res.attributes.install_started_at, }); }); }); diff --git a/x-pack/test/ingest_manager_api_integration/apis/epm/install_update.ts b/x-pack/test/ingest_manager_api_integration/apis/epm/install_update.ts index 9de6cd9118fe..bdcd202d8c46 100644 --- a/x-pack/test/ingest_manager_api_integration/apis/epm/install_update.ts +++ b/x-pack/test/ingest_manager_api_integration/apis/epm/install_update.ts @@ -7,6 +7,10 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../../api_integration/ftr_provider_context'; import { skipIfNoDockerRegistry } from '../../helpers'; +import { + PACKAGES_SAVED_OBJECT_TYPE, + MAX_TIME_COMPLETE_INSTALL, +} from '../../../../plugins/ingest_manager/common'; export default function (providerContext: FtrProviderContext) { const { getService } = providerContext; @@ -62,6 +66,12 @@ export default function (providerContext: FtrProviderContext) { .send({ force: true }) .expect(200); }); + it('should return 200 if trying to reinstall an out-of-date package', async function () { + await supertest + .post(`/api/ingest_manager/epm/packages/multiple_versions-0.1.0`) + .set('kbn-xsrf', 'xxxx') + .expect(200); + }); it('should return 400 if trying to update to an out-of-date package', async function () { await supertest .post(`/api/ingest_manager/epm/packages/multiple_versions-0.2.0`) @@ -75,6 +85,24 @@ export default function (providerContext: FtrProviderContext) { .send({ force: true }) .expect(200); }); + it('should return 200 if trying to reupdate an out-of-date package', async function () { + const previousInstallDate = new Date(Date.now() - MAX_TIME_COMPLETE_INSTALL).toISOString(); + // mock package to be stuck installing an update + await kibanaServer.savedObjects.update({ + id: 'multiple_versions', + type: PACKAGES_SAVED_OBJECT_TYPE, + attributes: { + install_status: 'installing', + install_started_at: previousInstallDate, + install_version: '0.2.0', + version: '0.1.0', + }, + }); + await supertest + .post(`/api/ingest_manager/epm/packages/multiple_versions-0.2.0`) + .set('kbn-xsrf', 'xxxx') + .expect(200); + }); it('should return 200 if trying to update to the latest package', async function () { await supertest .post(`/api/ingest_manager/epm/packages/multiple_versions-0.3.0`) diff --git a/x-pack/test/ingest_manager_api_integration/apis/epm/package_install_complete.ts b/x-pack/test/ingest_manager_api_integration/apis/epm/package_install_complete.ts new file mode 100644 index 000000000000..dc311c9db191 --- /dev/null +++ b/x-pack/test/ingest_manager_api_integration/apis/epm/package_install_complete.ts @@ -0,0 +1,167 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; +import { + PACKAGES_SAVED_OBJECT_TYPE, + MAX_TIME_COMPLETE_INSTALL, +} from '../../../../plugins/ingest_manager/common'; +import { FtrProviderContext } from '../../../api_integration/ftr_provider_context'; + +export default function (providerContext: FtrProviderContext) { + const { getService } = providerContext; + const supertest = getService('supertest'); + const kibanaServer = getService('kibanaServer'); + const pkgName = 'multiple_versions'; + const pkgVersion = '0.1.0'; + const pkgUpdateVersion = '0.2.0'; + describe('setup checks packages completed install', async () => { + describe('package install', async () => { + before(async () => { + await supertest + .post(`/api/ingest_manager/epm/packages/${pkgName}-0.1.0`) + .set('kbn-xsrf', 'xxxx') + .send({ force: true }); + }); + it('should have not reinstalled if package install completed', async function () { + const packageBeforeSetup = await kibanaServer.savedObjects.get({ + type: 'epm-packages', + id: pkgName, + }); + const installStartedAtBeforeSetup = packageBeforeSetup.attributes.install_started_at; + await supertest.post(`/api/ingest_manager/setup`).set('kbn-xsrf', 'xxx').send(); + const packageAfterSetup = await kibanaServer.savedObjects.get({ + type: PACKAGES_SAVED_OBJECT_TYPE, + id: pkgName, + }); + const installStartedAfterSetup = packageAfterSetup.attributes.install_started_at; + expect(installStartedAtBeforeSetup).equal(installStartedAfterSetup); + }); + it('should have reinstalled if package installing did not complete in elapsed time', async function () { + // change the saved object to installing to mock kibana crashing and not finishing the install + const previousInstallDate = new Date(Date.now() - MAX_TIME_COMPLETE_INSTALL).toISOString(); + await kibanaServer.savedObjects.update({ + id: pkgName, + type: PACKAGES_SAVED_OBJECT_TYPE, + attributes: { + install_status: 'installing', + install_started_at: previousInstallDate, + }, + }); + await supertest.post(`/api/ingest_manager/setup`).set('kbn-xsrf', 'xxx').send(); + const packageAfterSetup = await kibanaServer.savedObjects.get({ + type: PACKAGES_SAVED_OBJECT_TYPE, + id: pkgName, + }); + const installStartedAfterSetup = packageAfterSetup.attributes.install_started_at; + expect(Date.parse(installStartedAfterSetup)).greaterThan(Date.parse(previousInstallDate)); + expect(packageAfterSetup.attributes.install_status).equal('installed'); + }); + it('should have not reinstalled if package installing did not surpass elapsed time', async function () { + // change the saved object to installing to mock package still installing, but a time less than the max time allowable + const previousInstallDate = new Date(Date.now()).toISOString(); + await kibanaServer.savedObjects.update({ + id: pkgName, + type: PACKAGES_SAVED_OBJECT_TYPE, + attributes: { + install_status: 'installing', + install_started_at: previousInstallDate, + }, + }); + await supertest.post(`/api/ingest_manager/setup`).set('kbn-xsrf', 'xxx').send(); + const packageAfterSetup = await kibanaServer.savedObjects.get({ + type: PACKAGES_SAVED_OBJECT_TYPE, + id: pkgName, + }); + const installStartedAfterSetup = packageAfterSetup.attributes.install_started_at; + expect(Date.parse(installStartedAfterSetup)).equal(Date.parse(previousInstallDate)); + expect(packageAfterSetup.attributes.install_status).equal('installing'); + }); + after(async () => { + await supertest + .delete(`/api/ingest_manager/epm/packages/multiple_versions-0.1.0`) + .set('kbn-xsrf', 'xxxx'); + }); + }); + describe('package update', async () => { + before(async () => { + await supertest + .post(`/api/ingest_manager/epm/packages/${pkgName}-0.1.0`) + .set('kbn-xsrf', 'xxxx') + .send({ force: true }); + await supertest + .post(`/api/ingest_manager/epm/packages/${pkgName}-0.2.0`) + .set('kbn-xsrf', 'xxxx') + .send({ force: true }); + }); + it('should have not reinstalled if package update completed', async function () { + const packageBeforeSetup = await kibanaServer.savedObjects.get({ + type: 'epm-packages', + id: pkgName, + }); + const installStartedAtBeforeSetup = packageBeforeSetup.attributes.install_started_at; + await supertest.post(`/api/ingest_manager/setup`).set('kbn-xsrf', 'xxx').send(); + const packageAfterSetup = await kibanaServer.savedObjects.get({ + type: PACKAGES_SAVED_OBJECT_TYPE, + id: pkgName, + }); + const installStartedAfterSetup = packageAfterSetup.attributes.install_started_at; + expect(installStartedAtBeforeSetup).equal(installStartedAfterSetup); + }); + it('should have reinstalled if package updating did not complete in elapsed time', async function () { + // change the saved object to installing to mock kibana crashing and not finishing the update + const previousInstallDate = new Date(Date.now() - MAX_TIME_COMPLETE_INSTALL).toISOString(); + await kibanaServer.savedObjects.update({ + id: pkgName, + type: PACKAGES_SAVED_OBJECT_TYPE, + attributes: { + version: pkgVersion, + install_status: 'installing', + install_started_at: previousInstallDate, + install_version: pkgUpdateVersion, // set version back + }, + }); + await supertest.post(`/api/ingest_manager/setup`).set('kbn-xsrf', 'xxx').send(); + const packageAfterSetup = await kibanaServer.savedObjects.get({ + type: PACKAGES_SAVED_OBJECT_TYPE, + id: pkgName, + }); + const installStartedAfterSetup = packageAfterSetup.attributes.install_started_at; + expect(Date.parse(installStartedAfterSetup)).greaterThan(Date.parse(previousInstallDate)); + expect(packageAfterSetup.attributes.install_status).equal('installed'); + expect(packageAfterSetup.attributes.version).equal(pkgUpdateVersion); + expect(packageAfterSetup.attributes.install_version).equal(pkgUpdateVersion); + }); + it('should have not reinstalled if package updating did not surpass elapsed time', async function () { + // change the saved object to installing to mock package still installing, but a time less than the max time allowable + const previousInstallDate = new Date(Date.now()).toISOString(); + await kibanaServer.savedObjects.update({ + id: pkgName, + type: PACKAGES_SAVED_OBJECT_TYPE, + attributes: { + install_status: 'installing', + install_started_at: previousInstallDate, + version: pkgVersion, // set version back + }, + }); + await supertest.post(`/api/ingest_manager/setup`).set('kbn-xsrf', 'xxx').send(); + const packageAfterSetup = await kibanaServer.savedObjects.get({ + type: PACKAGES_SAVED_OBJECT_TYPE, + id: pkgName, + }); + const installStartedAfterSetup = packageAfterSetup.attributes.install_started_at; + expect(Date.parse(installStartedAfterSetup)).equal(Date.parse(previousInstallDate)); + expect(packageAfterSetup.attributes.install_status).equal('installing'); + expect(packageAfterSetup.attributes.version).equal(pkgVersion); + }); + after(async () => { + await supertest + .delete(`/api/ingest_manager/epm/packages/multiple_versions-0.1.0`) + .set('kbn-xsrf', 'xxxx'); + }); + }); + }); +} diff --git a/x-pack/test/ingest_manager_api_integration/apis/epm/update_assets.ts b/x-pack/test/ingest_manager_api_integration/apis/epm/update_assets.ts index 8ad6fe12dcd4..9af27f5f9855 100644 --- a/x-pack/test/ingest_manager_api_integration/apis/epm/update_assets.ts +++ b/x-pack/test/ingest_manager_api_integration/apis/epm/update_assets.ts @@ -322,6 +322,9 @@ export default function (providerContext: FtrProviderContext) { version: '0.2.0', internal: false, removable: true, + install_version: '0.2.0', + install_status: 'installed', + install_started_at: res.attributes.install_started_at, }); }); }); diff --git a/x-pack/test/kerberos_api_integration/apis/security/kerberos_login.ts b/x-pack/test/kerberos_api_integration/apis/security/kerberos_login.ts index 38a8697e0525..3c211dca2a78 100644 --- a/x-pack/test/kerberos_api_integration/apis/security/kerberos_login.ts +++ b/x-pack/test/kerberos_api_integration/apis/security/kerberos_login.ts @@ -37,7 +37,8 @@ export default function ({ getService }: FtrProviderContext) { expect(cookie.maxAge).to.be(0); } - describe('Kerberos authentication', () => { + // FAILING: https://github.com/elastic/kibana/issues/75707 + describe.skip('Kerberos authentication', () => { before(async () => { await getService('esSupertest') .post('/_security/role_mapping/krb5')