diff --git a/.eslintrc.js b/.eslintrc.js index 5c2a2817eae53..561e9bc55bf9d 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -640,6 +640,10 @@ module.exports = { name: 'lodash/fp/assocPath', message: 'Please use @elastic/safer-lodash-set instead', }, + { + name: 'react-use', + message: 'Please use react-use/lib/{method} instead.', + }, ], }, ], diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 96670b5d5107b..0e0099ff672fc 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -26,30 +26,17 @@ /src/plugins/vis_type_vislib/ @elastic/kibana-app /src/plugins/vis_type_xy/ @elastic/kibana-app /src/plugins/visualize/ @elastic/kibana-app -/src/plugins/visualizations/ @elastic/kibana-app -#CC# /src/legacy/core_plugins/kibana/public/local_application_service/ @elastic/kibana-app -#CC# /src/legacy/core_plugins/kibana/ @elastic/kibana-app -#CC# /src/legacy/core_plugins/kibana/common/utils @elastic/kibana-app -#CC# /src/legacy/core_plugins/kibana/migrations @elastic/kibana-app -#CC# /src/legacy/core_plugins/kibana/public @elastic/kibana-app -#CC# /src/legacy/core_plugins/kibana/public/discover/ @elastic/kibana-app -#CC# /src/legacy/core_plugins/kibana/public/local_application_service/ @elastic/kibana-app -#CC# /src/legacy/core_plugins/timelion @elastic/kibana-app -#CC# /src/legacy/core_plugins/vis_type_tagcloud @elastic/kibana-app -#CC# /src/legacy/core_plugins/vis_type_vega @elastic/kibana-app -#CC# /src/legacy/core_plugins/vis_type_vislib/ @elastic/kibana-app -#CC# /src/legacy/server/url_shortening/ @elastic/kibana-app -#CC# /src/legacy/ui/public/state_management @elastic/kibana-app +/src/plugins/visualizations/ @elastic/kibana-application -# App Architecture +# Application Services /examples/bfetch_explorer/ @elastic/kibana-app-arch /examples/dashboard_embeddable_examples/ @elastic/kibana-app-arch /examples/demo_search/ @elastic/kibana-app-arch /examples/developer_examples/ @elastic/kibana-app-arch /examples/embeddable_examples/ @elastic/kibana-app-arch /examples/embeddable_explorer/ @elastic/kibana-app-arch -/examples/state_container_examples/ @elastic/kibana-app-arch -/examples/ui_actions_examples/ @elastic/kibana-app-arch +/examples/state_containers_examples/ @elastic/kibana-app-arch +/examples/ui_action_examples/ @elastic/kibana-app-arch /examples/ui_actions_explorer/ @elastic/kibana-app-arch /examples/url_generators_examples/ @elastic/kibana-app-arch /examples/url_generators_explorer/ @elastic/kibana-app-arch @@ -74,7 +61,6 @@ #CC# /src/plugins/index_pattern_management/ @elastic/kibana-app-arch #CC# /src/plugins/inspector/ @elastic/kibana-app-arch #CC# /src/plugins/share/ @elastic/kibana-app-arch -#CC# /x-pack/plugins/advanced_ui_actions/ @elastic/kibana-app-arch #CC# /x-pack/plugins/drilldowns/ @elastic/kibana-app-arch #CC# /packages/kbn-interpreter/ @elastic/kibana-app-arch @@ -84,9 +70,6 @@ /src/plugins/apm_oss/ @elastic/apm-ui /src/apm.js @watson @vigneshshanmugam #CC# /src/plugins/apm_oss/ @elastic/apm-ui -#CC# /src/legacy/core_plugins/apm_oss/ @elastic/apm-ui -#CC# /src/legacy/ui/public/apm @elastic/apm-ui -#CC# /x-pack/legacy/plugins/apm/ @elastic/apm-ui #CC# /x-pack/plugins/observability/ @elastic/apm-ui # Client Side Monitoring (lives in APM directories but owned by Uptime) @@ -97,12 +80,9 @@ /x-pack/plugins/apm/server/lib/rum_client @elastic/uptime /x-pack/plugins/apm/server/routes/rum_client.ts @elastic/uptime /x-pack/plugins/apm/server/projections/rum_page_load_transactions.ts @elastic/uptime -/x-pack/plugins/apm/server/projections/rum_overview.ts @elastic/uptime -#CC# /x-pack/legacy/plugins/uptime @elastic/uptime # Beats /x-pack/plugins/beats_management/ @elastic/beats -/x-pack/legacy/plugins/beats_management/ @elastic/beats #CC# /x-pack/plugins/beats_management/ @elastic/beats # Presentation @@ -112,12 +92,9 @@ /x-pack/plugins/canvas/ @elastic/kibana-presentation /x-pack/plugins/dashboard_enhanced/ @elastic/kibana-presentation /x-pack/test/functional/apps/canvas/ @elastic/kibana-presentation -#CC# /src/legacy/core_plugins/kibana/public/dashboard/ @elastic/kibana-presentation -#CC# /src/legacy/core_plugins/input_control_vis @elastic/kibana-presentation #CC# /src/plugins/kibana_react/public/code_editor/ @elastic/kibana-presentation #CC# /x-pack/legacy/plugins/canvas/ @elastic/kibana-presentation #CC# /x-pack/plugins/dashboard_mode @elastic/kibana-presentation -#CC# /x-pack/legacy/plugins/dashboard_mode/ @elastic/kibana-presentation # Core UI # Exclude tutorials folder for now because they are not owned by Kibana app and most will move out soon @@ -126,8 +103,6 @@ /src/plugins/home/server/services/ @elastic/kibana-core-ui /src/plugins/kibana_overview/ @elastic/kibana-core-ui /x-pack/plugins/global_search_bar/ @elastic/kibana-core-ui -#CC# /src/legacy/core_plugins/newsfeed @elastic/kibana-core-ui -#CC# /src/legacy/server/sample_data/ @elastic/kibana-core-ui #CC# /src/plugins/newsfeed @elastic/kibana-core-ui #CC# /src/plugins/home/public @elastic/kibana-core-ui #CC# /src/plugins/home/server/services/ @elastic/kibana-core-ui @@ -136,21 +111,24 @@ # Observability UIs /x-pack/plugins/infra/ @elastic/logs-metrics-ui -/x-pack/plugins/ingest_manager/ @elastic/ingest-management +/x-pack/plugins/fleet/ @elastic/ingest-management /x-pack/plugins/observability/ @elastic/observability-ui /x-pack/plugins/monitoring/ @elastic/stack-monitoring-ui /x-pack/plugins/uptime @elastic/uptime # Machine Learning /x-pack/plugins/ml/ @elastic/ml-ui -/x-pack/test/functional/apps/machine_learning/ @elastic/ml-ui -/x-pack/test/functional/services/machine_learning/ @elastic/ml-ui -/x-pack/test/functional/services/ml.ts @elastic/ml-ui +/x-pack/test/functional/apps/ml/ @elastic/ml-ui +/x-pack/test/functional/services/ml/ @elastic/ml-ui # ML team owns and maintains the transform plugin despite it living in the Elasticsearch management section. /x-pack/plugins/transform/ @elastic/ml-ui /x-pack/test/functional/apps/transform/ @elastic/ml-ui -/x-pack/test/functional/services/transform_ui/ @elastic/ml-ui -/x-pack/test/functional/services/transform.ts @elastic/ml-ui +/x-pack/test/functional/services/transform/ @elastic/ml-ui/ +x-pack/test/api_integration_basic/apis/ml/ @elastic/ml-ui +/x-pack/test/functional_basic/apps/ml/ @elastic/ml-ui + +/x-pack/test/api_integration_basic/apis/transform/ @elastic/ml-ui +/x-pack/test/functional_basic/apps/transform/ @elastic/ml-ui # Maps /x-pack/plugins/maps/ @elastic/kibana-gis @@ -180,9 +158,6 @@ /packages/kbn-es-archiver/ @elastic/kibana-operations /packages/kbn-utils/ @elastic/kibana-operations /src/legacy/server/keystore/ @elastic/kibana-operations -/src/legacy/server/pid/ @elastic/kibana-operations -/src/legacy/server/sass/ @elastic/kibana-operations -/src/legacy/server/utils/ @elastic/kibana-operations /src/legacy/server/warnings/ @elastic/kibana-operations /.ci/es-snapshots/ @elastic/kibana-operations /vars/ @elastic/kibana-operations @@ -211,37 +186,19 @@ /src/legacy/server/config/ @elastic/kibana-platform /src/legacy/server/http/ @elastic/kibana-platform /src/legacy/server/logging/ @elastic/kibana-platform -/src/legacy/server/saved_objects/ @elastic/kibana-platform -/src/legacy/server/status/ @elastic/kibana-platform /src/plugins/status_page/ @elastic/kibana-platform /src/plugins/saved_objects_management/ @elastic/kibana-platform /src/dev/run_check_published_api_changes.ts @elastic/kibana-platform #CC# /src/core/server/csp/ @elastic/kibana-platform -#CC# /src/legacy/core_plugins/kibana/server/lib @elastic/kibana-platform -#CC# /src/legacy/core_plugins/kibana/server/lib/management/saved_objects @elastic/kibana-platform -#CC# /src/legacy/core_plugins/kibana/server/routes/api/import/ @elastic/kibana-platform -#CC# /src/legacy/core_plugins/kibana/server/routes/api/export/ @elastic/kibana-platform -#CC# /src/legacy/core_plugins/elasticsearch @elastic/kibana-platform -#CC# /src/legacy/core_plugins/testbed @elastic/kibana-platform #CC# /src/legacy/server/config/ @elastic/kibana-platform #CC# /src/legacy/server/http/ @elastic/kibana-platform -#CC# /src/legacy/server/status/ @elastic/kibana-platform -#CC# /src/legacy/ui/public/new_platform @elastic/kibana-platform -#CC# /src/legacy/ui/public/plugin_discovery @elastic/kibana-platform -#CC# /src/legacy/ui/public/chrome @elastic/kibana-platform -#CC# /src/legacy/ui/public/notify @elastic/kibana-platform #CC# /src/legacy/ui/public/documentation_links @elastic/kibana-platform -#CC# /src/legacy/ui/public/autoload @elastic/kibana-platform #CC# /src/plugins/legacy_export/ @elastic/kibana-platform #CC# /src/plugins/saved_objects/ @elastic/kibana-platform #CC# /src/plugins/status_page/ @elastic/kibana-platform -#CC# /src/plugins/testbed/server/ @elastic/kibana-platform -#CC# /x-pack/legacy/plugins/xpack_main/server/ @elastic/kibana-platform -#CC# /x-pack/legacy/server/lib/ @elastic/kibana-platform #CC# /x-pack/plugins/cloud/ @elastic/kibana-platform #CC# /x-pack/plugins/features/ @elastic/kibana-platform #CC# /x-pack/plugins/global_search/ @elastic/kibana-platform -#CC# /src/legacy/plugin_discovery/ @elastic/kibana-platform # Security /src/core/server/csp/ @elastic/kibana-security @elastic/kibana-platform @@ -254,25 +211,16 @@ /x-pack/test/ui_capabilities/ @elastic/kibana-security /x-pack/test/encrypted_saved_objects_api_integration/ @elastic/kibana-security /x-pack/test/functional/apps/security/ @elastic/kibana-security -/x-pack/test/kerberos_api_integration/ @elastic/kibana-security -/x-pack/test/oidc_api_integration/ @elastic/kibana-security -/x-pack/test/pki_api_integration/ @elastic/kibana-security /x-pack/test/security_api_integration/ @elastic/kibana-security /x-pack/test/security_functional/ @elastic/kibana-security /x-pack/test/spaces_api_integration/ @elastic/kibana-security -/x-pack/test/token_api_integration/ @elastic/kibana-security -#CC# /src/legacy/ui/public/capabilities @elastic/kibana-security -#CC# /x-pack/legacy/plugins/encrypted_saved_objects/ @elastic/kibana-security #CC# /x-pack/plugins/security_solution/ @elastic/kibana-security #CC# /x-pack/plugins/security/ @elastic/kibana-security -#CC# /x-pack/plugins/audit_trail/ @elastic/kibana-security # Kibana Localization /src/dev/i18n/ @elastic/kibana-localization -/src/legacy/server/i18n/ @elastic/kibana-localization /src/core/public/i18n/ @elastic/kibana-localization /packages/kbn-i18n/ @elastic/kibana-localization -#CC# /src/legacy/server/i18n/ @elastic/kibana-localization #CC# /x-pack/plugins/translations/ @elastic/kibana-localization # Kibana Telemetry @@ -297,15 +245,12 @@ x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json @elastic/kib /x-pack/plugins/event_log/ @elastic/kibana-alerting-services /x-pack/plugins/task_manager/ @elastic/kibana-alerting-services /x-pack/test/alerting_api_integration/ @elastic/kibana-alerting-services -/x-pack/test/plugin_api_integration/plugins/task_manager/ @elastic/kibana-alerting-services /x-pack/test/plugin_api_integration/test_suites/task_manager/ @elastic/kibana-alerting-services /x-pack/plugins/triggers_actions_ui/ @elastic/kibana-alerting-services /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 -#CC# /x-pack/legacy/plugins/actions/ @elastic/kibana-alerting-services -#CC# /x-pack/legacy/plugins/alerting/ @elastic/kibana-alerting-services -#CC# /x-pack/legacy/plugins/task_manager @elastic/kibana-alerting-services -#CC# /x-pack/legacy/plugins/triggers_actions_ui/ @elastic/kibana-alerting-services +/docs/user/alerting/ @elastic/kibana-alerting-services +/docs/management/alerting/ @elastic/kibana-alerting-services #CC# /x-pack/plugins/stack_alerts @elastic/kibana-alerting-services # Enterprise Search @@ -316,11 +261,9 @@ x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json @elastic/kib # Elasticsearch UI /src/plugins/dev_tools/ @elastic/es-ui /src/plugins/console/ @elastic/es-ui -/src/plugins/es_ui_shared/ @elastic/es-ui /x-pack/plugins/cross_cluster_replication/ @elastic/es-ui /x-pack/plugins/index_lifecycle_management/ @elastic/es-ui /x-pack/plugins/console_extensions/ @elastic/es-ui -/x-pack/plugins/es_ui_shared/ @elastic/es-ui /x-pack/plugins/grokdebugger/ @elastic/es-ui /x-pack/plugins/index_management/ @elastic/es-ui /x-pack/plugins/license_management/ @elastic/es-ui @@ -334,18 +277,11 @@ x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json @elastic/kib /x-pack/plugins/ingest_pipelines/ @elastic/es-ui /packages/kbn-ace/ @elastic/es-ui /packages/kbn-monaco/ @elastic/es-ui -#CC# /src/legacy/core_plugins/kibana/public/dev_tools/ @elastic/es-ui -#CC# /src/legacy/core_plugins/console_legacy @elastic/es-ui -#CC# /x-pack/legacy/plugins/rollup/ @elastic/es-ui -#CC# /x-pack/legacy/server/lib/create_router/ @elastic/es-ui -#CC# /x-pack/legacy/server/lib/check_license/ @elastic/es-ui #CC# /x-pack/plugins/console_extensions/ @elastic/es-ui #CC# /x-pack/plugins/cross_cluster_replication/ @elastic/es-ui -#CC# /x-pack/plugins/es_ui_shared/ @elastic/es-u # Endpoint /x-pack/plugins/endpoint/ @elastic/endpoint-app-team @elastic/siem -/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 /x-pack/test/functional/es_archives/endpoint/ @elastic/endpoint-app-team @elastic/siem @@ -362,6 +298,7 @@ x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json @elastic/kib /x-pack/test/api_integration/apis/security_solution @elastic/siem @elastic/endpoint-app-team /x-pack/plugins/case @elastic/siem @elastic/endpoint-app-team /x-pack/plugins/lists @elastic/siem @elastic/endpoint-app-team +#CC# /x-pack/plugins/security_solution/ @elastic/siem # Security Intelligence And Analytics /x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules @elastic/security-intelligence-analytics @@ -370,17 +307,10 @@ x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json @elastic/kib **/*.scss @elastic/kibana-design #CC# /packages/kbn-ui-framework/ @elastic/kibana-design -# Core UI design -/src/plugins/dashboard/**/*.scss @elastic/kibana-core-ui-designers -/src/plugins/embeddable/**/*.scss @elastic/kibana-core-ui-designers -/x-pack/plugins/canvas/**/*.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/fleet/**/*.scss @elastic/observability-design /x-pack/plugins/observability/**/*.scss @elastic/observability-design /x-pack/plugins/monitoring/**/*.scss @elastic/observability-design diff --git a/.github/ISSUE_TEMPLATE/v8_breaking_change.md b/.github/ISSUE_TEMPLATE/v8_breaking_change.md index a64ce33b8f976..86e321990d05f 100644 --- a/.github/ISSUE_TEMPLATE/v8_breaking_change.md +++ b/.github/ISSUE_TEMPLATE/v8_breaking_change.md @@ -30,6 +30,8 @@ requesting the breaking change to be surfaced in the Upgrade Assistant. +**How can we programmatically determine whether the cluster is affected by this breaking change?** + **What can users do to address the change manually?** diff --git a/.github/paths-labeller.yml b/.github/paths-labeller.yml index 2e8529b4a7704..bd8427ea18d69 100644 --- a/.github/paths-labeller.yml +++ b/.github/paths-labeller.yml @@ -11,7 +11,7 @@ - "Team:apm": - "x-pack/plugins/apm/**/*.*" - "Team:Ingest Management": - - "x-pack/plugins/ingest_manager/**/*.*" + - "x-pack/plugins/fleet/**/*.*" - "x-pack/test/api_integration/apis/fleet/**/*.*" - "x-pack/test/epm_api_integration/**/*.*" - "Team:uptime": diff --git a/config/kibana.yml b/config/kibana.yml index 58ae8b9346f51..ce9fe28dae916 100644 --- a/config/kibana.yml +++ b/config/kibana.yml @@ -80,7 +80,7 @@ #elasticsearch.logQueries: false # Specifies the path where Kibana creates the process ID file. -#pid.file: /var/run/kibana.pid +#pid.file: /run/kibana/kibana.pid # Enables you to specify a file where Kibana stores log output. #logging.dest: stdout diff --git a/docs/developer/best-practices/stability.asciidoc b/docs/developer/best-practices/stability.asciidoc index 348412e593d9e..29be86f58317f 100644 --- a/docs/developer/best-practices/stability.asciidoc +++ b/docs/developer/best-practices/stability.asciidoc @@ -43,11 +43,26 @@ dependency list! [discrete] === Test coverage -* Does the feature have sufficient unit test coverage? (does it handle -storeinSessions?) -* Does the feature have sufficient Functional UI test coverage? -* Does the feature have sufficient Rest API coverage test coverage? -* Does the feature have sufficient Integration test coverage? +Testing UI code is hard. We strive for https://github.com/elastic/engineering/blob/master/kibana_dev_principles.md#automate-tests-through-ci[total automated test coverage] of our code and UX, +but this is difficult to measure and we're constrained by time. During development, test coverage +measurement is subjective and manual, based on our understanding of the feature. Code coverage +reports indicate possible gaps, but it ultimately comes down to a judgment call. Here are some +guidelines to help you ensure sufficient automated test coverage. + +* Every PR should be accompanied by tests. +* Check the before and after automated test coverage metrics. If coverage has gone down you might +have missed some tests. +* Cover failure cases, edge cases, and happy paths with your tests. +* Pay special attention to code that could contain bugs that harm to the user. "Harm" includes +direct problems like data loss and data entering a bad state, as well as indirect problems like +making a poor business decision based on misinformation presented by the UI. For example, state +migrations and security permissions are important areas to cover. +* Pay special attention to public APIs, which may be used in unexpected ways. Any code you release +for consumption by other plugins should be rigorously tested with many permutations. +* Include end-to-end tests for areas where the logic spans global state, URLs, and multiple plugin APIs. +* Every time a bug is reported, add a test to cover it. +* Retrospectively gauge the quality of the code you ship by tracking how many bugs are reported for +features that are released. How can you reduce this number by improving your testing approach? [discrete] === Browser coverage @@ -63,4 +78,4 @@ Does the feature work efficiently on the list of supported browsers? * Does the feature affect old indices or saved objects? * Has the feature been tested with {kib} aliases? * Read/Write privileges of the indices before and after the -upgrade? +upgrade? \ No newline at end of file diff --git a/docs/developer/contributing/development-tests.asciidoc b/docs/developer/contributing/development-tests.asciidoc index e4bd49e12101b..4cf667195153d 100644 --- a/docs/developer/contributing/development-tests.asciidoc +++ b/docs/developer/contributing/development-tests.asciidoc @@ -50,6 +50,8 @@ yarn test:ftr:runner –config test/api_integration/config **Testing IE on OS X** +**Note:** IE11 is not supported from 7.9 onwards. + * http://www.vmware.com/products/fusion/fusion-evaluation.html[Download VMWare Fusion]. * https://developer.microsoft.com/en-us/microsoft-edge/tools/vms/#downloads[Download diff --git a/docs/developer/plugin-list.asciidoc b/docs/developer/plugin-list.asciidoc index 3c62c1fbca982..e89b6d86361c7 100644 --- a/docs/developer/plugin-list.asciidoc +++ b/docs/developer/plugin-list.asciidoc @@ -413,7 +413,7 @@ Index Management by running this series of requests in Console: the infrastructure monitoring use-case within Kibana. -|{kib-repo}blob/{branch}/x-pack/plugins/ingest_manager/README.md[ingestManager] +|{kib-repo}blob/{branch}/x-pack/plugins/fleet/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.fleet.agents.tlsCheckDisabled=false) diff --git a/docs/development/core/server/kibana-plugin-core-server.coresetup.i18n.md b/docs/development/core/server/kibana-plugin-core-server.coresetup.i18n.md new file mode 100644 index 0000000000000..cac878c1e4449 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.coresetup.i18n.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [CoreSetup](./kibana-plugin-core-server.coresetup.md) > [i18n](./kibana-plugin-core-server.coresetup.i18n.md) + +## CoreSetup.i18n property + +[I18nServiceSetup](./kibana-plugin-core-server.i18nservicesetup.md) + +Signature: + +```typescript +i18n: I18nServiceSetup; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.coresetup.md b/docs/development/core/server/kibana-plugin-core-server.coresetup.md index 7a733cc34dace..1171dbad570ce 100644 --- a/docs/development/core/server/kibana-plugin-core-server.coresetup.md +++ b/docs/development/core/server/kibana-plugin-core-server.coresetup.md @@ -21,6 +21,7 @@ export interface CoreSetupElasticsearchServiceSetup | [ElasticsearchServiceSetup](./kibana-plugin-core-server.elasticsearchservicesetup.md) | | [getStartServices](./kibana-plugin-core-server.coresetup.getstartservices.md) | StartServicesAccessor<TPluginsStart, TStart> | [StartServicesAccessor](./kibana-plugin-core-server.startservicesaccessor.md) | | [http](./kibana-plugin-core-server.coresetup.http.md) | HttpServiceSetup & {
resources: HttpResources;
} | [HttpServiceSetup](./kibana-plugin-core-server.httpservicesetup.md) | +| [i18n](./kibana-plugin-core-server.coresetup.i18n.md) | I18nServiceSetup | [I18nServiceSetup](./kibana-plugin-core-server.i18nservicesetup.md) | | [logging](./kibana-plugin-core-server.coresetup.logging.md) | LoggingServiceSetup | [LoggingServiceSetup](./kibana-plugin-core-server.loggingservicesetup.md) | | [metrics](./kibana-plugin-core-server.coresetup.metrics.md) | MetricsServiceSetup | [MetricsServiceSetup](./kibana-plugin-core-server.metricsservicesetup.md) | | [savedObjects](./kibana-plugin-core-server.coresetup.savedobjects.md) | SavedObjectsServiceSetup | [SavedObjectsServiceSetup](./kibana-plugin-core-server.savedobjectsservicesetup.md) | diff --git a/docs/development/core/server/kibana-plugin-core-server.i18nservicesetup.getlocale.md b/docs/development/core/server/kibana-plugin-core-server.i18nservicesetup.getlocale.md new file mode 100644 index 0000000000000..2fe8e564e7ce5 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.i18nservicesetup.getlocale.md @@ -0,0 +1,17 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [I18nServiceSetup](./kibana-plugin-core-server.i18nservicesetup.md) > [getLocale](./kibana-plugin-core-server.i18nservicesetup.getlocale.md) + +## I18nServiceSetup.getLocale() method + +Return the locale currently in use. + +Signature: + +```typescript +getLocale(): string; +``` +Returns: + +`string` + diff --git a/docs/development/core/server/kibana-plugin-core-server.i18nservicesetup.gettranslationfiles.md b/docs/development/core/server/kibana-plugin-core-server.i18nservicesetup.gettranslationfiles.md new file mode 100644 index 0000000000000..81caed287454e --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.i18nservicesetup.gettranslationfiles.md @@ -0,0 +1,17 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [I18nServiceSetup](./kibana-plugin-core-server.i18nservicesetup.md) > [getTranslationFiles](./kibana-plugin-core-server.i18nservicesetup.gettranslationfiles.md) + +## I18nServiceSetup.getTranslationFiles() method + +Return the absolute paths to translation files currently in use. + +Signature: + +```typescript +getTranslationFiles(): string[]; +``` +Returns: + +`string[]` + diff --git a/docs/development/core/server/kibana-plugin-core-server.i18nservicesetup.md b/docs/development/core/server/kibana-plugin-core-server.i18nservicesetup.md new file mode 100644 index 0000000000000..f68b7877953e7 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.i18nservicesetup.md @@ -0,0 +1,20 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [I18nServiceSetup](./kibana-plugin-core-server.i18nservicesetup.md) + +## I18nServiceSetup interface + + +Signature: + +```typescript +export interface I18nServiceSetup +``` + +## Methods + +| Method | Description | +| --- | --- | +| [getLocale()](./kibana-plugin-core-server.i18nservicesetup.getlocale.md) | Return the locale currently in use. | +| [getTranslationFiles()](./kibana-plugin-core-server.i18nservicesetup.gettranslationfiles.md) | Return the absolute paths to translation files currently in use. | + diff --git a/docs/development/core/server/kibana-plugin-core-server.md b/docs/development/core/server/kibana-plugin-core-server.md index 68f5e72915556..adbb2460dc80a 100644 --- a/docs/development/core/server/kibana-plugin-core-server.md +++ b/docs/development/core/server/kibana-plugin-core-server.md @@ -89,6 +89,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [HttpServerInfo](./kibana-plugin-core-server.httpserverinfo.md) | | | [HttpServiceSetup](./kibana-plugin-core-server.httpservicesetup.md) | Kibana HTTP Service provides own abstraction for work with HTTP stack. Plugins don't have direct access to hapi server and its primitives anymore. Moreover, plugins shouldn't rely on the fact that HTTP Service uses one or another library under the hood. This gives the platform flexibility to upgrade or changing our internal HTTP stack without breaking plugins. If the HTTP Service lacks functionality you need, we are happy to discuss and support your needs. | | [HttpServiceStart](./kibana-plugin-core-server.httpservicestart.md) | | +| [I18nServiceSetup](./kibana-plugin-core-server.i18nservicesetup.md) | | | [IClusterClient](./kibana-plugin-core-server.iclusterclient.md) | Represents an Elasticsearch cluster API client created by the platform. It allows to call API on behalf of the internal Kibana user and the actual user that is derived from the request headers (via asScoped(...)). | | [IContextContainer](./kibana-plugin-core-server.icontextcontainer.md) | An object that handles registration of context providers and configuring handlers with context. | | [ICspConfig](./kibana-plugin-core-server.icspconfig.md) | CSP configuration for use in Kibana. | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.executioncontextsearch.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.executioncontextsearch.md new file mode 100644 index 0000000000000..67dcb2fa44241 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.executioncontextsearch.md @@ -0,0 +1,15 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [ExecutionContextSearch](./kibana-plugin-plugins-data-public.executioncontextsearch.md) + +## ExecutionContextSearch type + +Signature: + +```typescript +export declare type ExecutionContextSearch = { + filters?: Filter[]; + query?: Query | Query[]; + timeRange?: TimeRange; +}; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.expressionfunctionkibana.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.expressionfunctionkibana.md new file mode 100644 index 0000000000000..c91f2e8144ead --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.expressionfunctionkibana.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [ExpressionFunctionKibana](./kibana-plugin-plugins-data-public.expressionfunctionkibana.md) + +## ExpressionFunctionKibana type + +Signature: + +```typescript +export declare type ExpressionFunctionKibana = ExpressionFunctionDefinition<'kibana', ExpressionValueSearchContext | null, object, ExpressionValueSearchContext, ExecutionContext>; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.expressionfunctionkibanacontext.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.expressionfunctionkibanacontext.md new file mode 100644 index 0000000000000..97d2e81d45554 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.expressionfunctionkibanacontext.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [ExpressionFunctionKibanaContext](./kibana-plugin-plugins-data-public.expressionfunctionkibanacontext.md) + +## ExpressionFunctionKibanaContext type + +Signature: + +```typescript +export declare type ExpressionFunctionKibanaContext = ExpressionFunctionDefinition<'kibana_context', KibanaContext | null, Arguments, Promise, ExecutionContext>; +``` diff --git a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionvaluesearchcontext.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.expressionvaluesearchcontext.md similarity index 54% rename from docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionvaluesearchcontext.md rename to docs/development/plugins/data/public/kibana-plugin-plugins-data-public.expressionvaluesearchcontext.md index 6e38adde3ba91..4849d82b94a62 100644 --- a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionvaluesearchcontext.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.expressionvaluesearchcontext.md @@ -1,6 +1,6 @@ -[Home](./index.md) > [kibana-plugin-plugins-expressions-server](./kibana-plugin-plugins-expressions-server.md) > [ExpressionValueSearchContext](./kibana-plugin-plugins-expressions-server.expressionvaluesearchcontext.md) +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [ExpressionValueSearchContext](./kibana-plugin-plugins-data-public.expressionvaluesearchcontext.md) ## ExpressionValueSearchContext type diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.getfieldsforindexpattern.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.getfieldsforindexpattern.md index c06c3c6f68492..f288573cd7abb 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.getfieldsforindexpattern.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.getfieldsforindexpattern.md @@ -9,5 +9,5 @@ Get field list by providing an index patttern (or spec) Signature: ```typescript -getFieldsForIndexPattern: (indexPattern: IndexPattern | IndexPatternSpec, options?: GetFieldsOptions) => Promise; +getFieldsForIndexPattern: (indexPattern: IndexPattern | IndexPatternSpec, options?: GetFieldsOptions | undefined) => Promise; ``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.getfieldsforwildcard.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.getfieldsforwildcard.md index aec84866b9e58..32bf6fc13b02c 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.getfieldsforwildcard.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.getfieldsforwildcard.md @@ -9,5 +9,5 @@ Get field list by providing { pattern } Signature: ```typescript -getFieldsForWildcard: (options?: GetFieldsOptions) => Promise; +getFieldsForWildcard: (options: GetFieldsOptions) => Promise; ``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.md index 34df8656e9175..57bb98de09ebd 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.md @@ -26,8 +26,8 @@ export declare class IndexPatternsService | [get](./kibana-plugin-plugins-data-public.indexpatternsservice.get.md) | | (id: string) => Promise<IndexPattern> | Get an index pattern by id. Cache optimized | | [getCache](./kibana-plugin-plugins-data-public.indexpatternsservice.getcache.md) | | () => Promise<SavedObject<IndexPatternSavedObjectAttrs>[] | null | undefined> | | | [getDefault](./kibana-plugin-plugins-data-public.indexpatternsservice.getdefault.md) | | () => Promise<IndexPattern | null> | Get default index pattern | -| [getFieldsForIndexPattern](./kibana-plugin-plugins-data-public.indexpatternsservice.getfieldsforindexpattern.md) | | (indexPattern: IndexPattern | IndexPatternSpec, options?: GetFieldsOptions) => Promise<any> | Get field list by providing an index patttern (or spec) | -| [getFieldsForWildcard](./kibana-plugin-plugins-data-public.indexpatternsservice.getfieldsforwildcard.md) | | (options?: GetFieldsOptions) => Promise<any> | Get field list by providing { pattern } | +| [getFieldsForIndexPattern](./kibana-plugin-plugins-data-public.indexpatternsservice.getfieldsforindexpattern.md) | | (indexPattern: IndexPattern | IndexPatternSpec, options?: GetFieldsOptions | undefined) => Promise<any> | Get field list by providing an index patttern (or spec) | +| [getFieldsForWildcard](./kibana-plugin-plugins-data-public.indexpatternsservice.getfieldsforwildcard.md) | | (options: GetFieldsOptions) => Promise<any> | Get field list by providing { pattern } | | [getIds](./kibana-plugin-plugins-data-public.indexpatternsservice.getids.md) | | (refresh?: boolean) => Promise<string[]> | Get list of index pattern ids | | [getIdsWithTitle](./kibana-plugin-plugins-data-public.indexpatternsservice.getidswithtitle.md) | | (refresh?: boolean) => Promise<Array<{
id: string;
title: string;
}>> | Get list of index pattern ids with titles | | [getTitles](./kibana-plugin-plugins-data-public.indexpatternsservice.gettitles.md) | | (refresh?: boolean) => Promise<string[]> | Get list of index pattern titles | diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.kibanacontext.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.kibanacontext.md similarity index 51% rename from docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.kibanacontext.md rename to docs/development/plugins/data/public/kibana-plugin-plugins-data-public.kibanacontext.md index 108533e8de357..cb8842c66761d 100644 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.kibanacontext.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.kibanacontext.md @@ -1,6 +1,6 @@ -[Home](./index.md) > [kibana-plugin-plugins-expressions-public](./kibana-plugin-plugins-expressions-public.md) > [KibanaContext](./kibana-plugin-plugins-expressions-public.kibanacontext.md) +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [KibanaContext](./kibana-plugin-plugins-data-public.kibanacontext.md) ## KibanaContext type 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 255a9947858f6..bafcd8bdffff9 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 @@ -142,7 +142,11 @@ | [EsdslExpressionFunctionDefinition](./kibana-plugin-plugins-data-public.esdslexpressionfunctiondefinition.md) | | | [EsQuerySortValue](./kibana-plugin-plugins-data-public.esquerysortvalue.md) | | | [EsRawResponseExpressionTypeDefinition](./kibana-plugin-plugins-data-public.esrawresponseexpressiontypedefinition.md) | | +| [ExecutionContextSearch](./kibana-plugin-plugins-data-public.executioncontextsearch.md) | | | [ExistsFilter](./kibana-plugin-plugins-data-public.existsfilter.md) | | +| [ExpressionFunctionKibana](./kibana-plugin-plugins-data-public.expressionfunctionkibana.md) | | +| [ExpressionFunctionKibanaContext](./kibana-plugin-plugins-data-public.expressionfunctionkibanacontext.md) | | +| [ExpressionValueSearchContext](./kibana-plugin-plugins-data-public.expressionvaluesearchcontext.md) | | | [FieldFormatId](./kibana-plugin-plugins-data-public.fieldformatid.md) | id type is needed for creating custom converters. | | [FieldFormatsContentType](./kibana-plugin-plugins-data-public.fieldformatscontenttype.md) | \* | | [FieldFormatsGetConfigFn](./kibana-plugin-plugins-data-public.fieldformatsgetconfigfn.md) | | @@ -161,6 +165,7 @@ | [InputTimeRange](./kibana-plugin-plugins-data-public.inputtimerange.md) | | | [ISearchGeneric](./kibana-plugin-plugins-data-public.isearchgeneric.md) | | | [ISearchSource](./kibana-plugin-plugins-data-public.isearchsource.md) | search source interface | +| [KibanaContext](./kibana-plugin-plugins-data-public.kibanacontext.md) | | | [MatchAllFilter](./kibana-plugin-plugins-data-public.matchallfilter.md) | | | [ParsedInterval](./kibana-plugin-plugins-data-public.parsedinterval.md) | | | [PhraseFilter](./kibana-plugin-plugins-data-public.phrasefilter.md) | | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.querysuggestiongetfnargs.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.querysuggestiongetfnargs.md index 96e43ca836891..de6f4563b678a 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.querysuggestiongetfnargs.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.querysuggestiongetfnargs.md @@ -23,4 +23,5 @@ export interface QuerySuggestionGetFnArgs | [selectionEnd](./kibana-plugin-plugins-data-public.querysuggestiongetfnargs.selectionend.md) | number | | | [selectionStart](./kibana-plugin-plugins-data-public.querysuggestiongetfnargs.selectionstart.md) | number | | | [signal](./kibana-plugin-plugins-data-public.querysuggestiongetfnargs.signal.md) | AbortSignal | | +| [useTimeRange](./kibana-plugin-plugins-data-public.querysuggestiongetfnargs.usetimerange.md) | boolean | | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.querysuggestiongetfnargs.usetimerange.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.querysuggestiongetfnargs.usetimerange.md new file mode 100644 index 0000000000000..a29cddd81d885 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.querysuggestiongetfnargs.usetimerange.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [QuerySuggestionGetFnArgs](./kibana-plugin-plugins-data-public.querysuggestiongetfnargs.md) > [useTimeRange](./kibana-plugin-plugins-data-public.querysuggestiongetfnargs.usetimerange.md) + +## QuerySuggestionGetFnArgs.useTimeRange property + +Signature: + +```typescript +useTimeRange?: boolean; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchbar.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchbar.md index d1d20291a6799..b886aafcfc00f 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchbar.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchbar.md @@ -7,7 +7,7 @@ Signature: ```typescript -SearchBar: React.ComponentClass, "query" | "isLoading" | "filters" | "onRefresh" | "onRefreshChange" | "refreshInterval" | "indexPatterns" | "dataTestSubj" | "timeHistory" | "customSubmitButton" | "screenTitle" | "showQueryBar" | "showQueryInput" | "showFilterBar" | "showDatePicker" | "showAutoRefreshOnly" | "isRefreshPaused" | "dateRangeFrom" | "dateRangeTo" | "showSaveQuery" | "savedQuery" | "onQueryChange" | "onQuerySubmit" | "onSaved" | "onSavedQueryUpdated" | "onClearSavedQuery" | "indicateNoData" | "onFiltersUpdated">, any> & { - WrappedComponent: React.ComponentType & ReactIntl.InjectedIntlProps>; +SearchBar: React.ComponentClass, "query" | "isLoading" | "filters" | "onRefresh" | "onRefreshChange" | "refreshInterval" | "indexPatterns" | "dataTestSubj" | "screenTitle" | "showQueryInput" | "showDatePicker" | "showAutoRefreshOnly" | "dateRangeFrom" | "dateRangeTo" | "isRefreshPaused" | "customSubmitButton" | "timeHistory" | "indicateNoData" | "onFiltersUpdated" | "trackUiMetric" | "savedQuery" | "showSaveQuery" | "onClearSavedQuery" | "showQueryBar" | "showFilterBar" | "onQueryChange" | "onQuerySubmit" | "onSaved" | "onSavedQueryUpdated">, any> & { + WrappedComponent: React.ComponentType & ReactIntl.InjectedIntlProps>; } ``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.ui_settings.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.ui_settings.md index 6ed20beb396f1..9a0c37c8edd38 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.ui_settings.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.ui_settings.md @@ -37,5 +37,6 @@ UI_SETTINGS: { readonly INDEXPATTERN_PLACEHOLDER: "indexPattern:placeholder"; readonly FILTERS_PINNED_BY_DEFAULT: "filters:pinnedByDefault"; readonly FILTERS_EDITOR_SUGGEST_VALUES: "filterEditor:suggestValues"; + readonly AUTOCOMPLETE_USE_TIMERANGE: "autocomplete:useTimeRange"; } ``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.executioncontextsearch.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.executioncontextsearch.md new file mode 100644 index 0000000000000..2f94dbe970d44 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.executioncontextsearch.md @@ -0,0 +1,15 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [ExecutionContextSearch](./kibana-plugin-plugins-data-server.executioncontextsearch.md) + +## ExecutionContextSearch type + +Signature: + +```typescript +export declare type ExecutionContextSearch = { + filters?: Filter[]; + query?: Query | Query[]; + timeRange?: TimeRange; +}; +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.expressionfunctionkibana.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.expressionfunctionkibana.md new file mode 100644 index 0000000000000..d80ff78dd803c --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.expressionfunctionkibana.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [ExpressionFunctionKibana](./kibana-plugin-plugins-data-server.expressionfunctionkibana.md) + +## ExpressionFunctionKibana type + +Signature: + +```typescript +export declare type ExpressionFunctionKibana = ExpressionFunctionDefinition<'kibana', ExpressionValueSearchContext | null, object, ExpressionValueSearchContext, ExecutionContext>; +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.expressionfunctionkibanacontext.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.expressionfunctionkibanacontext.md new file mode 100644 index 0000000000000..b67f7b8c4b60d --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.expressionfunctionkibanacontext.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [ExpressionFunctionKibanaContext](./kibana-plugin-plugins-data-server.expressionfunctionkibanacontext.md) + +## ExpressionFunctionKibanaContext type + +Signature: + +```typescript +export declare type ExpressionFunctionKibanaContext = ExpressionFunctionDefinition<'kibana_context', KibanaContext | null, Arguments, Promise, ExecutionContext>; +``` diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionvaluesearchcontext.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.expressionvaluesearchcontext.md similarity index 54% rename from docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionvaluesearchcontext.md rename to docs/development/plugins/data/server/kibana-plugin-plugins-data-server.expressionvaluesearchcontext.md index bf64dfe4c86f7..8015482ddec1e 100644 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionvaluesearchcontext.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.expressionvaluesearchcontext.md @@ -1,6 +1,6 @@ -[Home](./index.md) > [kibana-plugin-plugins-expressions-public](./kibana-plugin-plugins-expressions-public.md) > [ExpressionValueSearchContext](./kibana-plugin-plugins-expressions-public.expressionvaluesearchcontext.md) +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [ExpressionValueSearchContext](./kibana-plugin-plugins-data-server.expressionvaluesearchcontext.md) ## ExpressionValueSearchContext type diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.getcapabilitiesforrollupindices.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.getcapabilitiesforrollupindices.md new file mode 100644 index 0000000000000..ba2efcc9b75ca --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.getcapabilitiesforrollupindices.md @@ -0,0 +1,28 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [getCapabilitiesForRollupIndices](./kibana-plugin-plugins-data-server.getcapabilitiesforrollupindices.md) + +## getCapabilitiesForRollupIndices() function + +Signature: + +```typescript +export declare function getCapabilitiesForRollupIndices(indices: { + [key: string]: any; +}): { + [key: string]: any; +}; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| indices | {
[key: string]: any;
} | | + +Returns: + +`{ + [key: string]: any; +}` + diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsfetcher.getfieldsforwildcard.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsfetcher.getfieldsforwildcard.md index addd29916d81d..f0989097a727d 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsfetcher.getfieldsforwildcard.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsfetcher.getfieldsforwildcard.md @@ -15,6 +15,8 @@ getFieldsForWildcard(options: { fieldCapsOptions?: { allow_no_indices: boolean; }; + type?: string; + rollupIndex?: string; }): Promise; ``` @@ -22,7 +24,7 @@ getFieldsForWildcard(options: { | Parameter | Type | Description | | --- | --- | --- | -| options | {
pattern: string | string[];
metaFields?: string[];
fieldCapsOptions?: {
allow_no_indices: boolean;
};
} | | +| options | {
pattern: string | string[];
metaFields?: string[];
fieldCapsOptions?: {
allow_no_indices: boolean;
};
type?: string;
rollupIndex?: string;
} | | Returns: diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.start.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.start.md index e7c331bad64e8..6528b1c213cca 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.start.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.start.md @@ -8,7 +8,7 @@ ```typescript start(core: CoreStart, { fieldFormats, logger }: IndexPatternsServiceStartDeps): { - indexPatternsServiceFactory: (savedObjectsClient: SavedObjectsClientContract) => Promise; + indexPatternsServiceFactory: (savedObjectsClient: SavedObjectsClientContract, elasticsearchClient: ElasticsearchClient) => Promise; }; ``` @@ -22,6 +22,6 @@ start(core: CoreStart, { fieldFormats, logger }: IndexPatternsServiceStartDeps): Returns: `{ - indexPatternsServiceFactory: (savedObjectsClient: SavedObjectsClientContract) => Promise; + indexPatternsServiceFactory: (savedObjectsClient: SavedObjectsClientContract, elasticsearchClient: ElasticsearchClient) => Promise; }` diff --git a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.kibanacontext.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.kibanacontext.md similarity index 51% rename from docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.kibanacontext.md rename to docs/development/plugins/data/server/kibana-plugin-plugins-data-server.kibanacontext.md index 023748173e7dd..1ddc43c633b26 100644 --- a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.kibanacontext.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.kibanacontext.md @@ -1,6 +1,6 @@ -[Home](./index.md) > [kibana-plugin-plugins-expressions-server](./kibana-plugin-plugins-expressions-server.md) > [KibanaContext](./kibana-plugin-plugins-expressions-server.kibanacontext.md) +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [KibanaContext](./kibana-plugin-plugins-data-server.kibanacontext.md) ## KibanaContext type diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.md index 82d0a5a3182b9..8957f6d0f06b4 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.md @@ -28,6 +28,7 @@ | Function | Description | | --- | --- | +| [getCapabilitiesForRollupIndices(indices)](./kibana-plugin-plugins-data-server.getcapabilitiesforrollupindices.md) | | | [getDefaultSearchParams(uiSettingsClient)](./kibana-plugin-plugins-data-server.getdefaultsearchparams.md) | | | [getShardTimeout(config)](./kibana-plugin-plugins-data-server.getshardtimeout.md) | | | [getTime(indexPattern, timeRange, options)](./kibana-plugin-plugins-data-server.gettime.md) | | @@ -77,6 +78,7 @@ | [esQuery](./kibana-plugin-plugins-data-server.esquery.md) | | | [fieldFormats](./kibana-plugin-plugins-data-server.fieldformats.md) | | | [indexPatterns](./kibana-plugin-plugins-data-server.indexpatterns.md) | | +| [mergeCapabilitiesWithFields](./kibana-plugin-plugins-data-server.mergecapabilitieswithfields.md) | | | [search](./kibana-plugin-plugins-data-server.search.md) | | | [UI\_SETTINGS](./kibana-plugin-plugins-data-server.ui_settings.md) | | @@ -88,6 +90,10 @@ | [AggGroupName](./kibana-plugin-plugins-data-server.agggroupname.md) | | | [AggParam](./kibana-plugin-plugins-data-server.aggparam.md) | | | [EsaggsExpressionFunctionDefinition](./kibana-plugin-plugins-data-server.esaggsexpressionfunctiondefinition.md) | | +| [ExecutionContextSearch](./kibana-plugin-plugins-data-server.executioncontextsearch.md) | | +| [ExpressionFunctionKibana](./kibana-plugin-plugins-data-server.expressionfunctionkibana.md) | | +| [ExpressionFunctionKibanaContext](./kibana-plugin-plugins-data-server.expressionfunctionkibanacontext.md) | | +| [ExpressionValueSearchContext](./kibana-plugin-plugins-data-server.expressionvaluesearchcontext.md) | | | [FieldFormatsGetConfigFn](./kibana-plugin-plugins-data-server.fieldformatsgetconfigfn.md) | | | [Filter](./kibana-plugin-plugins-data-server.filter.md) | | | [IAggConfig](./kibana-plugin-plugins-data-server.iaggconfig.md) | AggConfig This class represents an aggregation, which is displayed in the left-hand nav of the Visualize app. | @@ -96,6 +102,7 @@ | [IFieldFormatsRegistry](./kibana-plugin-plugins-data-server.ifieldformatsregistry.md) | | | [IFieldParamType](./kibana-plugin-plugins-data-server.ifieldparamtype.md) | | | [IMetricAggType](./kibana-plugin-plugins-data-server.imetricaggtype.md) | | +| [KibanaContext](./kibana-plugin-plugins-data-server.kibanacontext.md) | | | [ParsedInterval](./kibana-plugin-plugins-data-server.parsedinterval.md) | | | [Query](./kibana-plugin-plugins-data-server.query.md) | | | [TabbedAggRow](./kibana-plugin-plugins-data-server.tabbedaggrow.md) | \* | diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.mergecapabilitieswithfields.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.mergecapabilitieswithfields.md new file mode 100644 index 0000000000000..2880e2d0d8f2c --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.mergecapabilitieswithfields.md @@ -0,0 +1,15 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [mergeCapabilitiesWithFields](./kibana-plugin-plugins-data-server.mergecapabilitieswithfields.md) + +## mergeCapabilitiesWithFields variable + +Signature: + +```typescript +mergeCapabilitiesWithFields: (rollupIndexCapabilities: { + [key: string]: any; +}, fieldsFromFieldCapsApi: { + [key: string]: any; +}, previousFields?: FieldDescriptor[]) => FieldDescriptor[] +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.plugin.setup.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.plugin.setup.md index 139c5794f0146..43129891c5412 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.plugin.setup.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.plugin.setup.md @@ -11,7 +11,7 @@ setup(core: CoreSetup, { expressio __enhance: (enhancements: DataEnhancements) => void; search: ISearchSetup; fieldFormats: { - register: (customFieldFormat: import("../public").FieldFormatInstanceType) => number; + register: (customFieldFormat: import("../common").FieldFormatInstanceType) => number; }; }; ``` @@ -29,7 +29,7 @@ setup(core: CoreSetup, { expressio __enhance: (enhancements: DataEnhancements) => void; search: ISearchSetup; fieldFormats: { - register: (customFieldFormat: import("../public").FieldFormatInstanceType) => number; + register: (customFieldFormat: import("../common").FieldFormatInstanceType) => number; }; }` 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 03d3485fce9ee..8a3dbe5a6350c 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 @@ -12,7 +12,7 @@ start(core: CoreStart): { fieldFormatServiceFactory: (uiSettings: import("src/core/server").IUiSettingsClient) => Promise; }; indexPatterns: { - indexPatternsServiceFactory: (savedObjectsClient: Pick) => Promise; + indexPatternsServiceFactory: (savedObjectsClient: Pick, elasticsearchClient: import("src/core/server").ElasticsearchClient) => Promise; }; search: ISearchStart>; }; @@ -31,7 +31,7 @@ start(core: CoreStart): { fieldFormatServiceFactory: (uiSettings: import("src/core/server").IUiSettingsClient) => Promise; }; indexPatterns: { - indexPatternsServiceFactory: (savedObjectsClient: Pick) => Promise; + indexPatternsServiceFactory: (savedObjectsClient: Pick, elasticsearchClient: import("src/core/server").ElasticsearchClient) => Promise; }; search: ISearchStart>; }` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.ui_settings.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.ui_settings.md index 2d4ce75b956df..c2edc64f292d2 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.ui_settings.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.ui_settings.md @@ -37,5 +37,6 @@ UI_SETTINGS: { readonly INDEXPATTERN_PLACEHOLDER: "indexPattern:placeholder"; readonly FILTERS_PINNED_BY_DEFAULT: "filters:pinnedByDefault"; readonly FILTERS_EDITOR_SUGGEST_VALUES: "filterEditor:suggestValues"; + readonly AUTOCOMPLETE_USE_TIMERANGE: "autocomplete:useTimeRange"; } ``` diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.executioncontext.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.executioncontext.md index 2a1a78b8fcb1a..86d24534f7a44 100644 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.executioncontext.md +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.executioncontext.md @@ -9,7 +9,7 @@ Signature: ```typescript -export interface ExecutionContext +export interface ExecutionContext ``` ## Properties diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionfunctiondefinitions.kibana.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionfunctiondefinitions.kibana.md deleted file mode 100644 index abe8e0ae161ad..0000000000000 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionfunctiondefinitions.kibana.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-expressions-public](./kibana-plugin-plugins-expressions-public.md) > [ExpressionFunctionDefinitions](./kibana-plugin-plugins-expressions-public.expressionfunctiondefinitions.md) > [kibana](./kibana-plugin-plugins-expressions-public.expressionfunctiondefinitions.kibana.md) - -## ExpressionFunctionDefinitions.kibana property - -Signature: - -```typescript -kibana: ExpressionFunctionKibana; -``` diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionfunctiondefinitions.kibana_context.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionfunctiondefinitions.kibana_context.md deleted file mode 100644 index 4b58fd84e160d..0000000000000 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionfunctiondefinitions.kibana_context.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-expressions-public](./kibana-plugin-plugins-expressions-public.md) > [ExpressionFunctionDefinitions](./kibana-plugin-plugins-expressions-public.expressionfunctiondefinitions.md) > [kibana\_context](./kibana-plugin-plugins-expressions-public.expressionfunctiondefinitions.kibana_context.md) - -## ExpressionFunctionDefinitions.kibana\_context property - -Signature: - -```typescript -kibana_context: ExpressionFunctionKibanaContext; -``` diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionfunctiondefinitions.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionfunctiondefinitions.md index 53f090ea30c3f..c6e00842a31e6 100644 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionfunctiondefinitions.md +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionfunctiondefinitions.md @@ -20,8 +20,6 @@ export interface ExpressionFunctionDefinitions | [cumulative\_sum](./kibana-plugin-plugins-expressions-public.expressionfunctiondefinitions.cumulative_sum.md) | ExpressionFunctionCumulativeSum | | | [derivative](./kibana-plugin-plugins-expressions-public.expressionfunctiondefinitions.derivative.md) | ExpressionFunctionDerivative | | | [font](./kibana-plugin-plugins-expressions-public.expressionfunctiondefinitions.font.md) | ExpressionFunctionFont | | -| [kibana\_context](./kibana-plugin-plugins-expressions-public.expressionfunctiondefinitions.kibana_context.md) | ExpressionFunctionKibanaContext | | -| [kibana](./kibana-plugin-plugins-expressions-public.expressionfunctiondefinitions.kibana.md) | ExpressionFunctionKibana | | | [moving\_average](./kibana-plugin-plugins-expressions-public.expressionfunctiondefinitions.moving_average.md) | ExpressionFunctionMovingAverage | | | [theme](./kibana-plugin-plugins-expressions-public.expressionfunctiondefinitions.theme.md) | ExpressionFunctionTheme | | | [var\_set](./kibana-plugin-plugins-expressions-public.expressionfunctiondefinitions.var_set.md) | ExpressionFunctionVarSet | | diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionfunctionkibana.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionfunctionkibana.md deleted file mode 100644 index 8ccf48ba28527..0000000000000 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionfunctionkibana.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-expressions-public](./kibana-plugin-plugins-expressions-public.md) > [ExpressionFunctionKibana](./kibana-plugin-plugins-expressions-public.expressionfunctionkibana.md) - -## ExpressionFunctionKibana type - -Signature: - -```typescript -export declare type ExpressionFunctionKibana = ExpressionFunctionDefinition<'kibana', ExpressionValueSearchContext | null, object, ExpressionValueSearchContext>; -``` diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.iexpressionloaderparams.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.iexpressionloaderparams.md index e2ad6215e25d0..2dfc67d2af5fa 100644 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.iexpressionloaderparams.md +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.iexpressionloaderparams.md @@ -21,7 +21,7 @@ export interface IExpressionLoaderParams | [disableCaching](./kibana-plugin-plugins-expressions-public.iexpressionloaderparams.disablecaching.md) | boolean | | | [inspectorAdapters](./kibana-plugin-plugins-expressions-public.iexpressionloaderparams.inspectoradapters.md) | Adapters | | | [onRenderError](./kibana-plugin-plugins-expressions-public.iexpressionloaderparams.onrendererror.md) | RenderErrorHandlerFnType | | -| [searchContext](./kibana-plugin-plugins-expressions-public.iexpressionloaderparams.searchcontext.md) | ExecutionContextSearch | | +| [searchContext](./kibana-plugin-plugins-expressions-public.iexpressionloaderparams.searchcontext.md) | SerializableState | | | [searchSessionId](./kibana-plugin-plugins-expressions-public.iexpressionloaderparams.searchsessionid.md) | string | | | [uiState](./kibana-plugin-plugins-expressions-public.iexpressionloaderparams.uistate.md) | unknown | | | [variables](./kibana-plugin-plugins-expressions-public.iexpressionloaderparams.variables.md) | Record<string, any> | | diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.iexpressionloaderparams.searchcontext.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.iexpressionloaderparams.searchcontext.md index 523d0c562f7ca..6b5fad950c4e9 100644 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.iexpressionloaderparams.searchcontext.md +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.iexpressionloaderparams.searchcontext.md @@ -7,5 +7,5 @@ Signature: ```typescript -searchContext?: ExecutionContextSearch; +searchContext?: SerializableState; ``` diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.kibana_context_name.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.kibana_context_name.md deleted file mode 100644 index e568db84f383d..0000000000000 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.kibana_context_name.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-expressions-public](./kibana-plugin-plugins-expressions-public.md) > [KIBANA\_CONTEXT\_NAME](./kibana-plugin-plugins-expressions-public.kibana_context_name.md) - -## KIBANA\_CONTEXT\_NAME type - -Signature: - -```typescript -export declare type KIBANA_CONTEXT_NAME = 'kibana_context'; -``` diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.md index db09f966e2fa5..a03ea32482011 100644 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.md +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.md @@ -99,7 +99,6 @@ | [ExpressionAstExpression](./kibana-plugin-plugins-expressions-public.expressionastexpression.md) | | | [ExpressionAstFunction](./kibana-plugin-plugins-expressions-public.expressionastfunction.md) | | | [ExpressionAstNode](./kibana-plugin-plugins-expressions-public.expressionastnode.md) | | -| [ExpressionFunctionKibana](./kibana-plugin-plugins-expressions-public.expressionfunctionkibana.md) | | | [ExpressionRendererComponent](./kibana-plugin-plugins-expressions-public.expressionrenderercomponent.md) | | | [ExpressionsServiceSetup](./kibana-plugin-plugins-expressions-public.expressionsservicesetup.md) | The public contract that ExpressionsService provides to other plugins in Kibana Platform in \*setup\* life-cycle. | | [ExpressionsSetup](./kibana-plugin-plugins-expressions-public.expressionssetup.md) | Expressions public setup contract, extends [ExpressionsServiceSetup](./kibana-plugin-plugins-expressions-public.expressionsservicesetup.md) | @@ -110,13 +109,10 @@ | [ExpressionValueFilter](./kibana-plugin-plugins-expressions-public.expressionvaluefilter.md) | Represents an object that is a Filter. | | [ExpressionValueNum](./kibana-plugin-plugins-expressions-public.expressionvaluenum.md) | | | [ExpressionValueRender](./kibana-plugin-plugins-expressions-public.expressionvaluerender.md) | Represents an object that is intended to be rendered. | -| [ExpressionValueSearchContext](./kibana-plugin-plugins-expressions-public.expressionvaluesearchcontext.md) | | | [ExpressionValueUnboxed](./kibana-plugin-plugins-expressions-public.expressionvalueunboxed.md) | | | [FontLabel](./kibana-plugin-plugins-expressions-public.fontlabel.md) | This type contains a unions of all supported font labels, or the the name of the font the user would see in a UI. | | [FontValue](./kibana-plugin-plugins-expressions-public.fontvalue.md) | This type contains a union of all supported font values, equivalent to the CSS font-value property. | | [InterpreterErrorType](./kibana-plugin-plugins-expressions-public.interpretererrortype.md) | | -| [KIBANA\_CONTEXT\_NAME](./kibana-plugin-plugins-expressions-public.kibana_context_name.md) | | -| [KibanaContext](./kibana-plugin-plugins-expressions-public.kibanacontext.md) | | | [KnownTypeToString](./kibana-plugin-plugins-expressions-public.knowntypetostring.md) | Map the type of the generic to a string-based representation of the type.If the provided generic is its own type interface, we use the value of the type key as a string literal type for it. | | [PointSeries](./kibana-plugin-plugins-expressions-public.pointseries.md) | A PointSeries is a unique structure that represents dots on a chart. | | [PointSeriesColumnName](./kibana-plugin-plugins-expressions-public.pointseriescolumnname.md) | Allowed column names in a PointSeries | diff --git a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.executioncontext.md b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.executioncontext.md index 047879fd99255..e2547cc9470d1 100644 --- a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.executioncontext.md +++ b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.executioncontext.md @@ -9,7 +9,7 @@ Signature: ```typescript -export interface ExecutionContext +export interface ExecutionContext ``` ## Properties diff --git a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionfunctiondefinitions.kibana.md b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionfunctiondefinitions.kibana.md deleted file mode 100644 index 8e6d189f8f450..0000000000000 --- a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionfunctiondefinitions.kibana.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-expressions-server](./kibana-plugin-plugins-expressions-server.md) > [ExpressionFunctionDefinitions](./kibana-plugin-plugins-expressions-server.expressionfunctiondefinitions.md) > [kibana](./kibana-plugin-plugins-expressions-server.expressionfunctiondefinitions.kibana.md) - -## ExpressionFunctionDefinitions.kibana property - -Signature: - -```typescript -kibana: ExpressionFunctionKibana; -``` diff --git a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionfunctiondefinitions.kibana_context.md b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionfunctiondefinitions.kibana_context.md deleted file mode 100644 index f9e248ad6d913..0000000000000 --- a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionfunctiondefinitions.kibana_context.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-expressions-server](./kibana-plugin-plugins-expressions-server.md) > [ExpressionFunctionDefinitions](./kibana-plugin-plugins-expressions-server.expressionfunctiondefinitions.md) > [kibana\_context](./kibana-plugin-plugins-expressions-server.expressionfunctiondefinitions.kibana_context.md) - -## ExpressionFunctionDefinitions.kibana\_context property - -Signature: - -```typescript -kibana_context: ExpressionFunctionKibanaContext; -``` diff --git a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionfunctiondefinitions.md b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionfunctiondefinitions.md index 6f152bb10b37e..219678244951b 100644 --- a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionfunctiondefinitions.md +++ b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionfunctiondefinitions.md @@ -20,8 +20,6 @@ export interface ExpressionFunctionDefinitions | [cumulative\_sum](./kibana-plugin-plugins-expressions-server.expressionfunctiondefinitions.cumulative_sum.md) | ExpressionFunctionCumulativeSum | | | [derivative](./kibana-plugin-plugins-expressions-server.expressionfunctiondefinitions.derivative.md) | ExpressionFunctionDerivative | | | [font](./kibana-plugin-plugins-expressions-server.expressionfunctiondefinitions.font.md) | ExpressionFunctionFont | | -| [kibana\_context](./kibana-plugin-plugins-expressions-server.expressionfunctiondefinitions.kibana_context.md) | ExpressionFunctionKibanaContext | | -| [kibana](./kibana-plugin-plugins-expressions-server.expressionfunctiondefinitions.kibana.md) | ExpressionFunctionKibana | | | [moving\_average](./kibana-plugin-plugins-expressions-server.expressionfunctiondefinitions.moving_average.md) | ExpressionFunctionMovingAverage | | | [theme](./kibana-plugin-plugins-expressions-server.expressionfunctiondefinitions.theme.md) | ExpressionFunctionTheme | | | [var\_set](./kibana-plugin-plugins-expressions-server.expressionfunctiondefinitions.var_set.md) | ExpressionFunctionVarSet | | diff --git a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionfunctionkibana.md b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionfunctionkibana.md deleted file mode 100644 index aac2ae1c3ca4e..0000000000000 --- a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionfunctionkibana.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-expressions-server](./kibana-plugin-plugins-expressions-server.md) > [ExpressionFunctionKibana](./kibana-plugin-plugins-expressions-server.expressionfunctionkibana.md) - -## ExpressionFunctionKibana type - -Signature: - -```typescript -export declare type ExpressionFunctionKibana = ExpressionFunctionDefinition<'kibana', ExpressionValueSearchContext | null, object, ExpressionValueSearchContext>; -``` diff --git a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.kibana_context_name.md b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.kibana_context_name.md deleted file mode 100644 index bd47c52e0d5ce..0000000000000 --- a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.kibana_context_name.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-expressions-server](./kibana-plugin-plugins-expressions-server.md) > [KIBANA\_CONTEXT\_NAME](./kibana-plugin-plugins-expressions-server.kibana_context_name.md) - -## KIBANA\_CONTEXT\_NAME type - -Signature: - -```typescript -export declare type KIBANA_CONTEXT_NAME = 'kibana_context'; -``` diff --git a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.md b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.md index 9e2189dad2732..5f7f373cd927f 100644 --- a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.md +++ b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.md @@ -83,7 +83,6 @@ | [ExpressionAstExpression](./kibana-plugin-plugins-expressions-server.expressionastexpression.md) | | | [ExpressionAstFunction](./kibana-plugin-plugins-expressions-server.expressionastfunction.md) | | | [ExpressionAstNode](./kibana-plugin-plugins-expressions-server.expressionastnode.md) | | -| [ExpressionFunctionKibana](./kibana-plugin-plugins-expressions-server.expressionfunctionkibana.md) | | | [ExpressionsServerSetup](./kibana-plugin-plugins-expressions-server.expressionsserversetup.md) | | | [ExpressionsServerStart](./kibana-plugin-plugins-expressions-server.expressionsserverstart.md) | | | [ExpressionValue](./kibana-plugin-plugins-expressions-server.expressionvalue.md) | | @@ -93,13 +92,10 @@ | [ExpressionValueFilter](./kibana-plugin-plugins-expressions-server.expressionvaluefilter.md) | Represents an object that is a Filter. | | [ExpressionValueNum](./kibana-plugin-plugins-expressions-server.expressionvaluenum.md) | | | [ExpressionValueRender](./kibana-plugin-plugins-expressions-server.expressionvaluerender.md) | Represents an object that is intended to be rendered. | -| [ExpressionValueSearchContext](./kibana-plugin-plugins-expressions-server.expressionvaluesearchcontext.md) | | | [ExpressionValueUnboxed](./kibana-plugin-plugins-expressions-server.expressionvalueunboxed.md) | | | [FontLabel](./kibana-plugin-plugins-expressions-server.fontlabel.md) | This type contains a unions of all supported font labels, or the the name of the font the user would see in a UI. | | [FontValue](./kibana-plugin-plugins-expressions-server.fontvalue.md) | This type contains a union of all supported font values, equivalent to the CSS font-value property. | | [InterpreterErrorType](./kibana-plugin-plugins-expressions-server.interpretererrortype.md) | | -| [KIBANA\_CONTEXT\_NAME](./kibana-plugin-plugins-expressions-server.kibana_context_name.md) | | -| [KibanaContext](./kibana-plugin-plugins-expressions-server.kibanacontext.md) | | | [KnownTypeToString](./kibana-plugin-plugins-expressions-server.knowntypetostring.md) | Map the type of the generic to a string-based representation of the type.If the provided generic is its own type interface, we use the value of the type key as a string literal type for it. | | [PointSeries](./kibana-plugin-plugins-expressions-server.pointseries.md) | A PointSeries is a unique structure that represents dots on a chart. | | [PointSeriesColumnName](./kibana-plugin-plugins-expressions-server.pointseriescolumnname.md) | Allowed column names in a PointSeries | diff --git a/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.addtriggeraction.md b/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.addtriggeraction.md index ba9060e01e57d..5a1ab83551d34 100644 --- a/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.addtriggeraction.md +++ b/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.addtriggeraction.md @@ -11,5 +11,5 @@ Signature: ```typescript -readonly addTriggerAction: (triggerId: T, action: ActionDefinition | Action) => void; +readonly addTriggerAction: (triggerId: T, action: ActionDefinition | Action) => void; ``` diff --git a/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.getaction.md b/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.getaction.md index 3e433809f9471..5b0b3eea01cb1 100644 --- a/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.getaction.md +++ b/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.getaction.md @@ -7,5 +7,5 @@ Signature: ```typescript -readonly getAction: >(id: string) => Action, "" | "ACTION_VISUALIZE_FIELD" | "ACTION_VISUALIZE_GEO_FIELD" | "ACTION_VISUALIZE_LENS_FIELD" | "ACTION_CUSTOMIZE_PANEL" | "ACTION_ADD_PANEL" | "openInspector" | "deletePanel" | "editPanel" | "togglePanel" | "replacePanel" | "clonePanel" | "addToFromLibrary" | "unlinkFromLibrary" | "ACTION_LIBRARY_NOTIFICATION" | "ACTION_GLOBAL_APPLY_FILTER" | "ACTION_SELECT_RANGE" | "ACTION_VALUE_CLICK">; +readonly getAction: >(id: string) => Action, "" | "ACTION_VISUALIZE_FIELD" | "ACTION_VISUALIZE_GEO_FIELD" | "ACTION_VISUALIZE_LENS_FIELD" | "ACTION_GLOBAL_APPLY_FILTER" | "ACTION_SELECT_RANGE" | "ACTION_VALUE_CLICK" | "ACTION_CUSTOMIZE_PANEL" | "ACTION_ADD_PANEL" | "openInspector" | "deletePanel" | "editPanel" | "togglePanel" | "replacePanel" | "clonePanel" | "addToFromLibrary" | "unlinkFromLibrary" | "ACTION_LIBRARY_NOTIFICATION">; ``` diff --git a/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.gettriggeractions.md b/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.gettriggeractions.md index 83afcab29689d..2dda422046318 100644 --- a/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.gettriggeractions.md +++ b/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.gettriggeractions.md @@ -7,5 +7,5 @@ Signature: ```typescript -readonly getTriggerActions: (triggerId: T) => Action[]; +readonly getTriggerActions: (triggerId: T) => Action[]; ``` diff --git a/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.gettriggercompatibleactions.md b/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.gettriggercompatibleactions.md index 879f5a3d8628a..e087753726a8a 100644 --- a/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.gettriggercompatibleactions.md +++ b/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.gettriggercompatibleactions.md @@ -7,5 +7,5 @@ Signature: ```typescript -readonly getTriggerCompatibleActions: (triggerId: T, context: TriggerContextMapping[T]) => Promise[]>; +readonly getTriggerCompatibleActions: (triggerId: T, context: TriggerContextMapping[T]) => Promise[]>; ``` diff --git a/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.md b/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.md index 7fade7c4c841b..f9eb693b492f7 100644 --- a/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.md +++ b/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.md @@ -21,19 +21,19 @@ export declare class UiActionsService | Property | Modifiers | Type | Description | | --- | --- | --- | --- | | [actions](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.actions.md) | | ActionRegistry | | -| [addTriggerAction](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.addtriggeraction.md) | | <T extends "" | "SELECT_RANGE_TRIGGER" | "VALUE_CLICK_TRIGGER" | "FILTER_TRIGGER" | "VISUALIZE_FIELD_TRIGGER" | "VISUALIZE_GEO_FIELD_TRIGGER" | "CONTEXT_MENU_TRIGGER" | "PANEL_BADGE_TRIGGER" | "PANEL_NOTIFICATION_TRIGGER">(triggerId: T, action: ActionDefinition<TriggerContextMapping[T]> | Action<TriggerContextMapping[T], "" | "ACTION_VISUALIZE_FIELD" | "ACTION_VISUALIZE_GEO_FIELD" | "ACTION_VISUALIZE_LENS_FIELD" | "ACTION_CUSTOMIZE_PANEL" | "ACTION_ADD_PANEL" | "openInspector" | "deletePanel" | "editPanel" | "togglePanel" | "replacePanel" | "clonePanel" | "addToFromLibrary" | "unlinkFromLibrary" | "ACTION_LIBRARY_NOTIFICATION" | "ACTION_GLOBAL_APPLY_FILTER" | "ACTION_SELECT_RANGE" | "ACTION_VALUE_CLICK">) => void | addTriggerAction is similar to attachAction as it attaches action to a trigger, but it also registers the action, if it has not been registered, yet.addTriggerAction also infers better typing of the action argument. | +| [addTriggerAction](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.addtriggeraction.md) | | <T extends "" | "SELECT_RANGE_TRIGGER" | "VALUE_CLICK_TRIGGER" | "FILTER_TRIGGER" | "VISUALIZE_FIELD_TRIGGER" | "VISUALIZE_GEO_FIELD_TRIGGER" | "CONTEXT_MENU_TRIGGER" | "PANEL_BADGE_TRIGGER" | "PANEL_NOTIFICATION_TRIGGER">(triggerId: T, action: ActionDefinition<TriggerContextMapping[T]> | Action<TriggerContextMapping[T], "" | "ACTION_VISUALIZE_FIELD" | "ACTION_VISUALIZE_GEO_FIELD" | "ACTION_VISUALIZE_LENS_FIELD" | "ACTION_GLOBAL_APPLY_FILTER" | "ACTION_SELECT_RANGE" | "ACTION_VALUE_CLICK" | "ACTION_CUSTOMIZE_PANEL" | "ACTION_ADD_PANEL" | "openInspector" | "deletePanel" | "editPanel" | "togglePanel" | "replacePanel" | "clonePanel" | "addToFromLibrary" | "unlinkFromLibrary" | "ACTION_LIBRARY_NOTIFICATION">) => void | addTriggerAction is similar to attachAction as it attaches action to a trigger, but it also registers the action, if it has not been registered, yet.addTriggerAction also infers better typing of the action argument. | | [attachAction](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.attachaction.md) | | <T extends "" | "SELECT_RANGE_TRIGGER" | "VALUE_CLICK_TRIGGER" | "FILTER_TRIGGER" | "VISUALIZE_FIELD_TRIGGER" | "VISUALIZE_GEO_FIELD_TRIGGER" | "CONTEXT_MENU_TRIGGER" | "PANEL_BADGE_TRIGGER" | "PANEL_NOTIFICATION_TRIGGER">(triggerId: T, actionId: string) => void | | | [clear](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.clear.md) | | () => void | Removes all registered triggers and actions. | | [detachAction](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.detachaction.md) | | (triggerId: TriggerId, actionId: string) => void | | | [executeTriggerActions](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.executetriggeractions.md) | | <T extends "" | "SELECT_RANGE_TRIGGER" | "VALUE_CLICK_TRIGGER" | "FILTER_TRIGGER" | "VISUALIZE_FIELD_TRIGGER" | "VISUALIZE_GEO_FIELD_TRIGGER" | "CONTEXT_MENU_TRIGGER" | "PANEL_BADGE_TRIGGER" | "PANEL_NOTIFICATION_TRIGGER">(triggerId: T, context: TriggerContext<T>) => Promise<void> | | | [executionService](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.executionservice.md) | | UiActionsExecutionService | | | [fork](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.fork.md) | | () => UiActionsService | "Fork" a separate instance of UiActionsService that inherits all existing triggers and actions, but going forward all new triggers and actions added to this instance of UiActionsService are only available within this instance. | -| [getAction](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.getaction.md) | | <T extends ActionDefinition<{}>>(id: string) => Action<ActionContext<T>, "" | "ACTION_VISUALIZE_FIELD" | "ACTION_VISUALIZE_GEO_FIELD" | "ACTION_VISUALIZE_LENS_FIELD" | "ACTION_CUSTOMIZE_PANEL" | "ACTION_ADD_PANEL" | "openInspector" | "deletePanel" | "editPanel" | "togglePanel" | "replacePanel" | "clonePanel" | "addToFromLibrary" | "unlinkFromLibrary" | "ACTION_LIBRARY_NOTIFICATION" | "ACTION_GLOBAL_APPLY_FILTER" | "ACTION_SELECT_RANGE" | "ACTION_VALUE_CLICK"> | | +| [getAction](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.getaction.md) | | <T extends ActionDefinition<{}>>(id: string) => Action<ActionContext<T>, "" | "ACTION_VISUALIZE_FIELD" | "ACTION_VISUALIZE_GEO_FIELD" | "ACTION_VISUALIZE_LENS_FIELD" | "ACTION_GLOBAL_APPLY_FILTER" | "ACTION_SELECT_RANGE" | "ACTION_VALUE_CLICK" | "ACTION_CUSTOMIZE_PANEL" | "ACTION_ADD_PANEL" | "openInspector" | "deletePanel" | "editPanel" | "togglePanel" | "replacePanel" | "clonePanel" | "addToFromLibrary" | "unlinkFromLibrary" | "ACTION_LIBRARY_NOTIFICATION"> | | | [getTrigger](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.gettrigger.md) | | <T extends "" | "SELECT_RANGE_TRIGGER" | "VALUE_CLICK_TRIGGER" | "FILTER_TRIGGER" | "VISUALIZE_FIELD_TRIGGER" | "VISUALIZE_GEO_FIELD_TRIGGER" | "CONTEXT_MENU_TRIGGER" | "PANEL_BADGE_TRIGGER" | "PANEL_NOTIFICATION_TRIGGER">(triggerId: T) => TriggerContract<T> | | -| [getTriggerActions](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.gettriggeractions.md) | | <T extends "" | "SELECT_RANGE_TRIGGER" | "VALUE_CLICK_TRIGGER" | "FILTER_TRIGGER" | "VISUALIZE_FIELD_TRIGGER" | "VISUALIZE_GEO_FIELD_TRIGGER" | "CONTEXT_MENU_TRIGGER" | "PANEL_BADGE_TRIGGER" | "PANEL_NOTIFICATION_TRIGGER">(triggerId: T) => Action<TriggerContextMapping[T], "" | "ACTION_VISUALIZE_FIELD" | "ACTION_VISUALIZE_GEO_FIELD" | "ACTION_VISUALIZE_LENS_FIELD" | "ACTION_CUSTOMIZE_PANEL" | "ACTION_ADD_PANEL" | "openInspector" | "deletePanel" | "editPanel" | "togglePanel" | "replacePanel" | "clonePanel" | "addToFromLibrary" | "unlinkFromLibrary" | "ACTION_LIBRARY_NOTIFICATION" | "ACTION_GLOBAL_APPLY_FILTER" | "ACTION_SELECT_RANGE" | "ACTION_VALUE_CLICK">[] | | -| [getTriggerCompatibleActions](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.gettriggercompatibleactions.md) | | <T extends "" | "SELECT_RANGE_TRIGGER" | "VALUE_CLICK_TRIGGER" | "FILTER_TRIGGER" | "VISUALIZE_FIELD_TRIGGER" | "VISUALIZE_GEO_FIELD_TRIGGER" | "CONTEXT_MENU_TRIGGER" | "PANEL_BADGE_TRIGGER" | "PANEL_NOTIFICATION_TRIGGER">(triggerId: T, context: TriggerContextMapping[T]) => Promise<Action<TriggerContextMapping[T], "" | "ACTION_VISUALIZE_FIELD" | "ACTION_VISUALIZE_GEO_FIELD" | "ACTION_VISUALIZE_LENS_FIELD" | "ACTION_CUSTOMIZE_PANEL" | "ACTION_ADD_PANEL" | "openInspector" | "deletePanel" | "editPanel" | "togglePanel" | "replacePanel" | "clonePanel" | "addToFromLibrary" | "unlinkFromLibrary" | "ACTION_LIBRARY_NOTIFICATION" | "ACTION_GLOBAL_APPLY_FILTER" | "ACTION_SELECT_RANGE" | "ACTION_VALUE_CLICK">[]> | | +| [getTriggerActions](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.gettriggeractions.md) | | <T extends "" | "SELECT_RANGE_TRIGGER" | "VALUE_CLICK_TRIGGER" | "FILTER_TRIGGER" | "VISUALIZE_FIELD_TRIGGER" | "VISUALIZE_GEO_FIELD_TRIGGER" | "CONTEXT_MENU_TRIGGER" | "PANEL_BADGE_TRIGGER" | "PANEL_NOTIFICATION_TRIGGER">(triggerId: T) => Action<TriggerContextMapping[T], "" | "ACTION_VISUALIZE_FIELD" | "ACTION_VISUALIZE_GEO_FIELD" | "ACTION_VISUALIZE_LENS_FIELD" | "ACTION_GLOBAL_APPLY_FILTER" | "ACTION_SELECT_RANGE" | "ACTION_VALUE_CLICK" | "ACTION_CUSTOMIZE_PANEL" | "ACTION_ADD_PANEL" | "openInspector" | "deletePanel" | "editPanel" | "togglePanel" | "replacePanel" | "clonePanel" | "addToFromLibrary" | "unlinkFromLibrary" | "ACTION_LIBRARY_NOTIFICATION">[] | | +| [getTriggerCompatibleActions](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.gettriggercompatibleactions.md) | | <T extends "" | "SELECT_RANGE_TRIGGER" | "VALUE_CLICK_TRIGGER" | "FILTER_TRIGGER" | "VISUALIZE_FIELD_TRIGGER" | "VISUALIZE_GEO_FIELD_TRIGGER" | "CONTEXT_MENU_TRIGGER" | "PANEL_BADGE_TRIGGER" | "PANEL_NOTIFICATION_TRIGGER">(triggerId: T, context: TriggerContextMapping[T]) => Promise<Action<TriggerContextMapping[T], "" | "ACTION_VISUALIZE_FIELD" | "ACTION_VISUALIZE_GEO_FIELD" | "ACTION_VISUALIZE_LENS_FIELD" | "ACTION_GLOBAL_APPLY_FILTER" | "ACTION_SELECT_RANGE" | "ACTION_VALUE_CLICK" | "ACTION_CUSTOMIZE_PANEL" | "ACTION_ADD_PANEL" | "openInspector" | "deletePanel" | "editPanel" | "togglePanel" | "replacePanel" | "clonePanel" | "addToFromLibrary" | "unlinkFromLibrary" | "ACTION_LIBRARY_NOTIFICATION">[]> | | | [hasAction](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.hasaction.md) | | (actionId: string) => boolean | | -| [registerAction](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.registeraction.md) | | <A extends ActionDefinition<{}>>(definition: A) => Action<ActionContext<A>, "" | "ACTION_VISUALIZE_FIELD" | "ACTION_VISUALIZE_GEO_FIELD" | "ACTION_VISUALIZE_LENS_FIELD" | "ACTION_CUSTOMIZE_PANEL" | "ACTION_ADD_PANEL" | "openInspector" | "deletePanel" | "editPanel" | "togglePanel" | "replacePanel" | "clonePanel" | "addToFromLibrary" | "unlinkFromLibrary" | "ACTION_LIBRARY_NOTIFICATION" | "ACTION_GLOBAL_APPLY_FILTER" | "ACTION_SELECT_RANGE" | "ACTION_VALUE_CLICK"> | | +| [registerAction](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.registeraction.md) | | <A extends ActionDefinition<{}>>(definition: A) => Action<ActionContext<A>, "" | "ACTION_VISUALIZE_FIELD" | "ACTION_VISUALIZE_GEO_FIELD" | "ACTION_VISUALIZE_LENS_FIELD" | "ACTION_GLOBAL_APPLY_FILTER" | "ACTION_SELECT_RANGE" | "ACTION_VALUE_CLICK" | "ACTION_CUSTOMIZE_PANEL" | "ACTION_ADD_PANEL" | "openInspector" | "deletePanel" | "editPanel" | "togglePanel" | "replacePanel" | "clonePanel" | "addToFromLibrary" | "unlinkFromLibrary" | "ACTION_LIBRARY_NOTIFICATION"> | | | [registerTrigger](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.registertrigger.md) | | (trigger: Trigger) => void | | | [triggers](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.triggers.md) | | TriggerRegistry | | | [triggerToActions](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.triggertoactions.md) | | TriggerToActionsRegistry | | diff --git a/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.registeraction.md b/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.registeraction.md index eeda7b503037d..bd340eb76fbac 100644 --- a/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.registeraction.md +++ b/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.registeraction.md @@ -7,5 +7,5 @@ Signature: ```typescript -readonly registerAction: >(definition: A) => Action, "" | "ACTION_VISUALIZE_FIELD" | "ACTION_VISUALIZE_GEO_FIELD" | "ACTION_VISUALIZE_LENS_FIELD" | "ACTION_CUSTOMIZE_PANEL" | "ACTION_ADD_PANEL" | "openInspector" | "deletePanel" | "editPanel" | "togglePanel" | "replacePanel" | "clonePanel" | "addToFromLibrary" | "unlinkFromLibrary" | "ACTION_LIBRARY_NOTIFICATION" | "ACTION_GLOBAL_APPLY_FILTER" | "ACTION_SELECT_RANGE" | "ACTION_VALUE_CLICK">; +readonly registerAction: >(definition: A) => Action, "" | "ACTION_VISUALIZE_FIELD" | "ACTION_VISUALIZE_GEO_FIELD" | "ACTION_VISUALIZE_LENS_FIELD" | "ACTION_GLOBAL_APPLY_FILTER" | "ACTION_SELECT_RANGE" | "ACTION_VALUE_CLICK" | "ACTION_CUSTOMIZE_PANEL" | "ACTION_ADD_PANEL" | "openInspector" | "deletePanel" | "editPanel" | "togglePanel" | "replacePanel" | "clonePanel" | "addToFromLibrary" | "unlinkFromLibrary" | "ACTION_LIBRARY_NOTIFICATION">; ``` diff --git a/docs/glossary.asciidoc b/docs/glossary.asciidoc index be24402170bbe..ff03a60173961 100644 --- a/docs/glossary.asciidoc +++ b/docs/glossary.asciidoc @@ -330,6 +330,11 @@ See {kibana-ref}/xpack-spaces.html[Spaces]. // end::space-def[] +[[glossary-stack-alerts]] stack alerts :: +// tag::stack-alert-def[] +The general purpose alert types {kib} provides out of the box. Index threshold and geo alerts are currently the two stack alert types. +// end::stack-alert-def[] + [float] [[t_glos]] diff --git a/docs/maps/images/create_phrase_filter.png b/docs/maps/images/create_phrase_filter.png deleted file mode 100644 index 720aecf44d9fa..0000000000000 Binary files a/docs/maps/images/create_phrase_filter.png and /dev/null differ diff --git a/docs/maps/images/create_spatial_filter.png b/docs/maps/images/create_spatial_filter.png index abb52bd0b5b0a..21614aa0f4e24 100644 Binary files a/docs/maps/images/create_spatial_filter.png and b/docs/maps/images/create_spatial_filter.png differ diff --git a/docs/maps/images/gs_add_cloropeth_layer.png b/docs/maps/images/gs_add_cloropeth_layer.png index 2800a5a2d2584..1528f404026f2 100644 Binary files a/docs/maps/images/gs_add_cloropeth_layer.png and b/docs/maps/images/gs_add_cloropeth_layer.png differ diff --git a/docs/maps/images/gs_create_new_map.png b/docs/maps/images/gs_create_new_map.png deleted file mode 100644 index bf5fd56ceba13..0000000000000 Binary files a/docs/maps/images/gs_create_new_map.png and /dev/null differ diff --git a/docs/maps/maps-getting-started.asciidoc b/docs/maps/maps-getting-started.asciidoc index 5c6cd87b235e1..32a81c8e65f56 100644 --- a/docs/maps/maps-getting-started.asciidoc +++ b/docs/maps/maps-getting-started.asciidoc @@ -1,231 +1,197 @@ [role="xpack"] [[maps-getting-started]] -== Get started with Maps +== Create a map with multiple layers and data sources ++++ -Get started +Create a multilayer map ++++ +If you are new to **Maps**, this tutorial is a good place to start. +It guides you through the common steps for working with your location data. +You'll learn to: -You work with Maps by adding layers. The data for a layer can come from -sources such as {es} documents, vector sources, tile map services, web map -services, and more. You can symbolize the data in different ways. -For example, you might show which airports have the longest flight -delays by using circles from small to big. Or, -you might show the amount of web log traffic by shading countries from -light to dark. +- Create a map with multiple layers and data sources +- Use symbols, colors, and labels to style data values +- Embed a map in a dashboard +- Search across panels in your dashboard -[role="screenshot"] -image::maps/images/sample_data_web_logs.png[] - -[[maps-read-only-access]] -NOTE: If you have insufficient privileges to create or save maps, a read-only icon -appears in the application header. The buttons to create new maps or edit -existing maps won't be visible. For more information on granting access to -Kibana see <>. +When you complete this tutorial, you’ll have a map that looks like this: [role="screenshot"] -image::maps/images/read-only-badge.png[Example of Maps' read only access indicator in Kibana's header] +image::maps/images/sample_data_web_logs.png[] [float] === Prerequisites -Before you start this tutorial, <>. Each -sample data set includes a map to go along with the data. Once you've added the data, open Maps and -explore the different layers of the *[Logs] Total Requests and Bytes* map. -You'll re-create this map in this tutorial. -[float] -=== Take-away skills -In this tutorial, you'll learn to: +- If you don’t already have {kib}, set it up with https://www.elastic.co/cloud/elasticsearch-service/signup?baymax=docs-body&elektra=docs[our free trial]. +- This tutorial requires the <>. The sample data includes a [Logs] Total Requests and Bytes map, which you’ll re-create in this tutorial. +- You must have the correct privileges for creating a map. +If you don't have sufficient privileges to create or save maps, +a read-only icon appears in the toolbar. For more information, +refer to <>. -* Create a multi-layer map -* Connect a layer to a data source -* Use symbols, colors, and labels to style a layer -* Create layers for {es} data - -[role="xpack"] +[float] [[maps-create]] -=== Create a map - -The first thing to do is to create a new map. +=== Step 1. Create a map -. If you haven't already, open the main menu, then click *Maps*. -. On the maps list page, click *Create map*. +. Open the main menu, and then click *Dashboard*. +. Click **Create dashboard**. . Set the time range to *Last 7 days*. -+ -A new map is created using a base tile layer. -+ -[role="screenshot"] -image::maps/images/gs_create_new_map.png[] +. Click **Create new**. +. Click **Maps**. -[role="xpack"] +[float] [[maps-add-choropleth-layer]] -=== Add a choropleth layer +=== Step 2. Add a choropleth layer -Now that you have a map, you'll want to add layers to it. The first layer you'll add is a choropleth layer to shade world countries -by web log traffic. Darker shades symbolize countries with more web log traffic, -and lighter shades symbolize countries with less traffic. - -. Click *Add layer*. -. Select *Choropleth*. -. From the *Layer* dropdown menu, select *World Countries*. -. Under *Statistics source*, set *Index pattern* to *kibana_sample_data_logs*. -. Set *Join field* to *geo.src*. -. Click the *Add layer* button. -. Set *Name* to `Total Requests by Country`. -. Set *Opacity* to 50%. -. Click *Add* under *Tooltip fields*. -. In the popover, select *ISO 3166-1 alpha-2 code* and *name* and click *Add*. -. Under *Fill color*, select the grey color ramp. -. Under *Border color*, change the selected color to *white*. -. Click *Save & close*. +by web log traffic. Darker shades will symbolize countries with more web log traffic, +and lighter shades will symbolize countries with less traffic. + +. Click **Add layer**, and then click **Choropleth**. + +. From the **Layer** dropdown menu, select **World Countries**. + +. In **Statistics source**, set: +** **Index pattern** to **kibana_sample_data_logs** +** **Join field** to **geo.src** + +. Click **Add layer**. + +. In **Layer settings**, set: + +** **Name** to `Total Requests by Country` +** **Opacity** to 50% + +. Add a Tooltip field: + +** Select **ISO 3166-1 alpha-2 code** and **name**. +** Click **Add**. + +. In **Layer style**, set: + +** **Fill color** to the grey color ramp +** **Border color** to white + +. Click **Save & close**. + Your map now looks like this: + [role="screenshot"] image::maps/images/gs_add_cloropeth_layer.png[] -[role="xpack"] +[float] [[maps-add-elasticsearch-layer]] -=== Add layers for the {es} data - -To avoid overwhelming the user with too much data at once, you'll add two layers for {es} data. +=== Step 3. Add layers for the Elasticsearch data -* The first layer will display individual documents. -The layer will appear when the user zooms in the map to show smaller regions. -* The second layer will display aggregated data that represents many documents. -The layer will appear when the user zooms out the map to show larger amounts of the globe. +To avoid overwhelming the user with too much data at once, you'll add two layers +for the Elasticsearch data. The first layer will display individual documents +when users zoom in on the map. The second layer will +display aggregated data when users zoom the map out. -==== Add a vector layer to display individual documents +[float] +==== Add a layer for individual documents This layer displays web log documents as points. -The layer is only visible when users zoom in the map past zoom level 9. - -. Click *Add layer*. -. Select *Documents*. -. Set *Index pattern* to *kibana_sample_data_logs*. -. Click the *Add layer* button. -. Set *Name* to `Actual Requests`. -. Set *Visibilty* to the range [9, 24]. -. Set *Opacity* to 100%. -. Click *Add* under *Tooltip fields*. -. In the popover, select *clientip*, *timestamp*, *host*, *request*, *response*, *machine.os*, *agent*, and *bytes* and click *Add*. -. Set *Fill color* to *#2200ff*. -. Click *Save & close*. +The layer is only visible when users zoom in. + +. Click **Add layer**, and then click **Documents**. + +. Set **Index pattern** to **kibana_sample_data_logs**. + +. Set **Scaling** to *Limits results to 10000.* + +. Click **Add layer**. + +. In **Layer settings**, set: +** **Name** to `Actual Requests` +** **Visibilty** to the range [9, 24] +** **Opacity** to 100% + +. Add a tooltip field and select **agent**, **bytes**, **clientip**, **host**, +**machine.os**, **request**, **response**, and **timestamp**. + +. In **Layer style**, set **Fill color** to **#2200FF**. + +. Click **Save & close**. + -Your map now looks like this between zoom levels 9 and 24: +Your map will look like this from zoom level 9 to 24: + [role="screenshot"] image::maps/images/gs_add_es_document_layer.png[] -==== Add a vector layer to display aggregated data - -Aggregations group {es} documents into grids. You can calculate metrics -for each gridded cell. +[float] +==== Add a layer for aggregated data -You'll create a layer for aggregated data and make it visible only when the map -is zoomed out past zoom level 9. Darker colors will symbolize grids +You'll create a layer for {ref}/search-aggregations.html[aggregated data] and make it visible only when the map +is zoomed out. Darker colors will symbolize grids with more web log traffic, and lighter colors will symbolize grids with less traffic. Larger circles will symbolize grids with more total bytes transferred, and smaller circles will symbolize grids with less bytes transferred. -[role="screenshot"] -image::maps/images/grid_metrics_both.png[] - -===== Add the layer - -. Click *Add layer*. -. Select *Clusters and grids*. -. Set *Index pattern* to *kibana_sample_data_logs*. -. Click the *Add layer* button. -. Set *Name* to `Total Requests and Bytes`. -. Set *Visibility* to the range [0, 9]. -. Set *Opacity* to 100%. - -===== Configure the aggregation metrics - -. Click *Add metric* under of *Metrics* label. -. Select *Sum* in the aggregation select. -. Select *bytes* in the field select. - -===== Set the layer style - -. In *Layer style*, change *Symbol size*: - .. Set *Min size* to 7. - .. Set *Max size* to 25. - .. Change the field select from *count* to *sum of bytes*. -. Click *Save & close* button. -+ -Your map now looks like this between zoom levels 0 and 9: +. Click **Add layer**, and select **Clusters and grids**. +. Set **Index pattern** to **kibana_sample_data_logs**. +. Click **Add layer**. +. In **Layer settings**, set: +** **Name** to `Total Requests and Bytes` +** **Visibility** to the range [0, 9] +** **Opacity** to 100% +. Add a metric with: +** **Aggregation** set to **Sum** +** **Field** set to **bytes** +. In **Layer style**, change **Symbol size**: +** Set the field select to *sum bytes*. +** Set the min size to 7 and the max size to 25 px. +. Click **Save & close** button. ++ +Your map will look like this between zoom levels 0 and 9: + [role="screenshot"] image::maps/images/sample_data_web_logs.png[] -[role="xpack"] +[float] [[maps-save]] -=== Save the map -Now that your map is complete, you'll want to save it so others can use it. +=== Step 4. Save the map +Now that your map is complete, save it and return to the dashboard. -. In the application toolbar, click *Save*. +. In the toolbar, click *Save*. . Enter `Tutorial web logs map` for the title. -. Click *Save*. -+ -You have completed the steps for re-creating the sample data map. - -*Next steps:* +. Ensure *Add to Dashboard after saving* is enabled. +. Click *Save and return*. -* Continue with this tutorial and <>. -* Create a map using your own data. You might find these resources helpful: -** <> -** <> -** <> - -[role="xpack"] +[float] [[maps-embedding]] -=== Add the map to a dashboard -You can add your saved map to a {kibana-ref}/dashboard.html[dashboard] and view your geospatial data alongside bar charts, pie charts, and other visualizations. +=== Step 5. Explore your data from the dashboard -. Open the main menu, then click *Dashboard*. -. Click *Create dashboard*. -. Set the time range to *Last 7 days*. -. Click *Add*. -+ -A panel opens with a list of objects that you can add to the dashboard. You'll add a map and two visualizations. -+ -. Set the *Types* select to *Map*. -. Click the name of your saved map or the *[Logs] Total Requests and Bytes* map included with the sample data set to add a map to the dashboard. -. Set the *Types* select to *Visualization*. -. Click *[Logs] Heatmap* to add a heatmap to the dashboard. -. Click *[Logs] Visitors by OS* to add a pie chart to the dashboard. -. Close the panel. -+ -Your dashboard should look like this: +View your geospatial data alongside a heat map and pie chart, and then filter the data. +When you apply a filter in one panel, it is applied to all panels on the dashboard. + +. In the toolbar, click **Add** to open a list of objects that you can add to the dashboard. +. Set the **Types** select to **Visualization**. +. Add **[Logs] Heatmap** and **[Logs] Visitors by OS** to the dashboard. + [role="screenshot"] image::maps/images/gs_dashboard_with_map.png[] -==== Explore your data using filters +. To filter for documents where **machine.os.keyword** is **osx**, click +the **osx** slice in the pie chart. -You can apply filters to your dashboard to hone in on the data that you are most interested in. -The dashboard is interactive--you can quickly create filters by clicking on the desired data in the map and visualizations. -The panels are linked, so that when you apply a filter in one panel, the filter is applied to all panels on the dashboard. +. Remove the filter by clicking **x** next to its name in the filter bar. -. In the *[Logs] Visitors by OS* visualization, click on the *osx* pie slice. -+ -Both the visualizations and map are filtered to only show documents where *machine.os.keyword* is *osx*. -The *machine.os.keyword: osx* filter appears in the dashboard query bar. -+ -. Click the *x* to remove the *machine.os.keyword: osx* filter. -. In the map, click in the United States vector. -. Click plus image:maps/images/gs_plus_icon.png[] next to the *iso2* row in the tooltip. -+ -Both the visualizations and the map are filtered to only show documents where *geo.src* is *US*. -The *geo.src: US* filter appears in the dashboard query bar. -+ -Your dashboard should look like this: +. Set a filter from the map: + +.. Open a tooltip by clicking anywhere in the United States vector. + +.. To show only documents where **geo.src** is **US**, click the filter icon in the row for **ISO 3066-1 alpha-2**. + [role="screenshot"] image::maps/images/gs_dashboard_with_terms_filter.png[] + +[float] +=== What's next? + +* Check out <> that you can add to your map. +* Learn more ways <>. +* Learn more about <>. diff --git a/docs/maps/search.asciidoc b/docs/maps/search.asciidoc index 09d9788cd37e0..764a84d88b1d2 100644 --- a/docs/maps/search.asciidoc +++ b/docs/maps/search.asciidoc @@ -30,6 +30,9 @@ You can create two types of filters by interacting with your map: * <> * <> +[role="screenshot"] +image::maps/images/create_spatial_filter.png[] + [float] [[maps-spatial-filters]] ==== Spatial filters @@ -40,9 +43,6 @@ You can create spatial filters in two ways: * Click the tool icon image:maps/images/tools_icon.png[], and then draw a polygon or bounding box on the map to define the spatial filter. * Click *Filter by geometry* in a <>, and then use the feature's geometry for the spatial filter. -+ -[role="screenshot"] -image::maps/images/create_spatial_filter.png[] Spatial filters have the following properties: @@ -59,9 +59,6 @@ A phrase filter narrows search results to documents that contain the specified t You can create a phrase filter by clicking the plus icon image:maps/images/gs_plus_icon.png[] in a <>. If the map is a dashboard panel with drilldowns, you can apply a phrase filter to a drilldown by selecting the drilldown action. -[role="screenshot"] -image::maps/images/create_phrase_filter.png[] - [role="xpack"] [[maps-layer-based-filtering]] === Filter a single layer diff --git a/docs/user/alerting/alert-types.asciidoc b/docs/user/alerting/alert-types.asciidoc index f71e43c5defc7..7de5ff56228cc 100644 --- a/docs/user/alerting/alert-types.asciidoc +++ b/docs/user/alerting/alert-types.asciidoc @@ -2,11 +2,13 @@ [[alert-types]] == Alert types -{kib} supplies alerts types in two ways: some are built into {kib}, while domain-specific alert types are registered by {kib} apps such as <>, <>, and <>. +{kib} supplies alert types in two ways: some are built into {kib} (these are known as stack alerts), while domain-specific alert types are registered by {kib} apps such as <>, <>, and <>. -This section covers built-in alert types. For domain-specific alert types, refer to the documentation for that app. +This section covers stack alerts. For domain-specific alert types, refer to the documentation for that app. +Users will need `all` access to the *Stack Alerts* feature to be able to create and edit any of the alerts listed below. +See <> for more information on configuring roles that provide access to this feature. -Currently {kib} provides one built-in alert type: the <> type. +Currently {kib} provides one stack alert: the <> type. [float] [[alert-type-index-threshold]] diff --git a/docs/user/alerting/alerting-getting-started.asciidoc b/docs/user/alerting/alerting-getting-started.asciidoc index 2b22b49375676..53aef4aaa062e 100644 --- a/docs/user/alerting/alerting-getting-started.asciidoc +++ b/docs/user/alerting/alerting-getting-started.asciidoc @@ -6,7 +6,7 @@ beta[] -- -Alerting allows you to detect complex conditions within different {kib} apps and trigger actions when those conditions are met. Alerting is integrated with <>, <>, <>, <>, can be centrally managed from the <> UI, and provides a set of built-in <> and <> for you to use. +Alerting allows you to detect complex conditions within different {kib} apps and trigger actions when those conditions are met. Alerting is integrated with <>, <>, <>, <>, can be centrally managed from the <> UI, and provides a set of built-in <> and <> (known as stack alerts) for you to use. image::images/alerting-overview.png[Alerts and actions UI] diff --git a/docs/user/dashboard/dashboard-drilldown.asciidoc b/docs/user/dashboard/dashboard-drilldown.asciidoc deleted file mode 100644 index bdff7355d7467..0000000000000 --- a/docs/user/dashboard/dashboard-drilldown.asciidoc +++ /dev/null @@ -1,96 +0,0 @@ -[[dashboard-drilldown]] -=== Dashboard drilldown - -The dashboard drilldown allows you to navigate from one dashboard to another dashboard. -For example, you might have a dashboard that shows the overall status of multiple data centers. -You can create a drilldown that navigates from this dashboard to a dashboard -that shows a single data center or server. - -This example shows a dashboard panel that contains a pie chart with a configured dashboard drilldown: - -[role="screenshot"] -image::images/drilldown_on_piechart.gif[Drilldown on pie chart that navigates to another dashboard] - -[float] -[[dashboard-drilldown-supported-panels]] -==== Supported panels - -The following panels support dashboard drilldowns: - -* Lens -* Area -* Data table -* Heat map -* Horizontal bar -* Line -* Maps -* Pie -* TSVB -* Tag cloud -* Timelion -* Vega -* Vertical bar - -[float] -[[drilldowns-example]] -==== Try it: Create a dashboard drilldown - -Create the *Host Overview* drilldown shown above. - -*Set up the dashboards* - -. Add the sample web logs data set. - -. Create a new dashboard, called `Host Overview`, and include these visualizations -from the sample data set: -+ -[%hardbreaks] -*[Logs] Heatmap* -*[Logs] Visitors by OS* -*[Logs] Host, Visits, and Bytes Table* -*[Logs] Total Requests and Bytes* -+ -TIP: If you don’t see data for a panel, try changing the time range. - -. Open the *[Logs] Web traffic* dashboard. - -. Set a search and filter. -+ -[%hardbreaks] -Search: `extension.keyword: ("gz" or "css" or "deb")` -Filter: `geo.src: CN` - - -*Create the drilldown* - - -. In the dashboard menu bar, click *Edit*. - -. In *[Logs] Visitors by OS*, open the panel menu, and then select *Create drilldown*. - -. Pick *Go to dashboard* action. - -. Give the drilldown a name. - -. Select *Host Overview* as the destination dashboard. - -. Keep both filters enabled so that the drilldown carries over the global filters and date range. -+ -Your input should look similar to this: -+ -[role="screenshot"] -image::images/drilldown_create.png[Create drilldown with entries for drilldown name and destination] - -. Click *Create drilldown.* - -. Save the dashboard. -+ -If you don’t save the drilldown, and then navigate away, the drilldown is lost. - -. In *[Logs] Visitors by OS*, click the `win 8` slice of the pie, and then select the name of your drilldown. -+ -[role="screenshot"] -image::images/drilldown_on_panel.png[Drilldown on pie chart that navigates to another dashboard] -+ -You are navigated to your destination dashboard. Verify that the search query, filters, -and time range are carried over. diff --git a/docs/user/dashboard/drilldowns.asciidoc b/docs/user/dashboard/drilldowns.asciidoc index e3d0e16630c5c..ca788020d9286 100644 --- a/docs/user/dashboard/drilldowns.asciidoc +++ b/docs/user/dashboard/drilldowns.asciidoc @@ -1,52 +1,259 @@ [role="xpack"] [[drilldowns]] -== Use drilldowns for dashboard actions +== Create custom dashboard actions -Drilldowns, also known as custom actions, allow you to configure a -workflow for analyzing and troubleshooting your data. -For example, using a drilldown, you can navigate from one dashboard to another, -taking the current time range, filters, and other parameters with you, -so the context remains the same. You can continue your analysis from a new perspective. +Custom dashboard actions, also known as drilldowns, allow you to create +workflows for analyzing and troubleshooting your data. Drilldowns apply only to the panel that you created the drilldown from, and are not shared across all of the panels. Each panel can have multiple drilldowns. -[role="screenshot"] -image::images/drilldown_on_piechart.gif[Drilldown on pie chart that navigates to another dashboard] +Third-party developers can create drilldowns. To learn how to code drilldowns, refer to {kib-repo}blob/{branch}/x-pack/examples/ui_actions_enhanced_examples[this example plugin]. + +[float] +[[supported-drilldowns]] +=== Supported drilldowns + +{kib} supports two types of drilldowns. + +[NOTE] +============================================== +Some drilldowns are paid subscription features, while others are free. +For a comparison of the Elastic subscription levels, +refer https://www.elastic.co/subscriptions[the subscription page]. +============================================== + +[float] +[[dashboard-drilldown]] +==== Dashboard drilldowns + +Dashboard drilldowns enable you to open a dashboard from another dashboard, +taking the time range, filters, and other parameters with you, +so the context remains the same. Dashboard drilldowns help you to continue your analysis from a new perspective. + +For example, if you have a dashboard that shows the overall status of multiple data center, +you can create a drilldown that navigates from the overall status dashboard to a dashboard +that shows a single data center or server. + +[float] +[[url-drilldown]] +==== URL drilldowns + +beta[] URL drilldowns enable you to navigate from a dashboard to internal or external URLs. +Destination URLs can be dynamic, depending on the dashboard context or user interaction with a panel. +For example, if you have a dashboard that shows data from a Github repository, you can create a URL drilldown +that opens Github from the dashboard. + +Some panels support multiple interactions, also known as triggers. +The <> you use to create a <> depends on the trigger you choose. URL drilldowns support these types of triggers: + +* *Single click* — A single data point in the visualization. + +* *Range selection* — A range of values in a visualization. + +For example, *Single click* has `{{event.value}}` and *Range selection* has `{{event.from}}` and `{{event.to}}`. + +To disable URL drilldowns on your {kib} instance, disable the plugin: -Drilldowns are specific to the dashboard panel for which you create them—they are not shared across panels. A panel can have multiple drilldowns. +["source","yml"] +----------- +url_drilldown.enabled: false +----------- [float] -[[actions]] -=== Drilldown actions +[[dashboard-drilldown-supported-panels]] +=== Supported panels -Drilldowns are user-configurable {kib} actions that are stored with the dashboard metadata. -Kibana provides the following types of actions: +The following panels support dashboard and URL drilldowns. -[cols="2"] +[options="header"] |=== -a| <> +| Panel | Dashboard drilldown | URL drilldown -| Navigate to a dashboard. +| Lens +^| X +^| X -a| <> +| Area +^| X +^| X -| Navigate to external or internal URL. +| Controls +^| +^| + +| Data Table +^| X +^| X + +| Gauge +^| +^| + +| Goal +^| +^| + +| Heat map +^| X +^| X + +| Horizontal Bar +^| X +^| X + +| Line +^| X +^| X + +| Maps +^| X +^| + +| Markdown +^| +^| + +| Metric +^| +^| + +| Pie +^| X +^| X + +| TSVB +^| X +^| + +| Tag Cloud +^| X +^| X + +| Timelion +^| X +^| + +| Vega +^| X +^| + +| Vertical Bar +^| X +^| X |=== -[NOTE] -============================================== -Some action types are paid commercial features, while others are free. -For a comparison of the Elastic subscription levels, -see https://www.elastic.co/subscriptions[the subscription page]. -============================================== +[float] +[[drilldowns-example]] +=== Try it: Create a dashboard drilldown + +To create dashboard drilldowns, you create or locate the dashboards you want to connect, then configure the drilldown that allows you to easily open one dashboard from the other dashboard. + +image:images/drilldown_on_piechart.gif[Drilldown on pie chart that navigates to another dashboard] [float] -[[code-drilldowns]] -=== Code drilldowns -Third-party developers can create drilldowns. -Refer to {kib-repo}blob/{branch}/x-pack/examples/ui_actions_enhanced_examples[this example plugin] -to learn how to code drilldowns. +==== Create the dashboard -include::dashboard-drilldown.asciidoc[] -include::url-drilldown.asciidoc[] +. Add the *Sample web logs* data. + +. Create a new dashboard, then add the following panels: + +* *[Logs] Heatmap* +* *[Logs] Host, Visits, and Bytes Table* +* *[Logs] Total Requests and Bytes* +* *[Logs] Visitors by OS* ++ +If you don’t see data for a panel, try changing the <>. + +. Save the dashboard. In the *Title* field, enter `Host Overview`. + +. Open the *[Logs] Web traffic* dashboard. + +. Set a search and filter. ++ +[%hardbreaks] +Search: `extension.keyword: ("gz" or "css" or "deb")` +Filter: `geo.src: CN` + +[float] +==== Create the drilldown + +. In the toolbar, click *Edit*. + +. Open the *[Logs] Visitors by OS* panel menu, then select *Create drilldown*. + +. Give the drilldown a name, then select *Go to dashboard*. + +. From the *Choose a destination dashboard* dropdown, select *Host Overview*. + +. To carry over the filter, query, and date range, make sure that *Use filters and query from origin dashboard* and *Use date range from origin dashboard* are selected. ++ +[role="screenshot"] +image::images/drilldown_create.png[Create drilldown with entries for drilldown name and destination] + +. Click *Create drilldown*. ++ +The drilldown is stored as dashboard metadata. + +. Save the dashboard. ++ +If you fail to save the dashboard, the drilldown is lost when you navigate away from the dashboard. + +. In the *[Logs] Visitors by OS* panel, click *win 8*, then select the drilldown. ++ +[role="screenshot"] +image::images/drilldown_on_panel.png[Drilldown on pie chart that navigates to another dashboard] +. On the *Host Overview* dashboard, verify that the search query, filters, +and date range are carried over. + +[float] +[[create-a-url-drilldown]] +=== Try it: Create a URL drilldown + +beta[] To create URL drilldowns, you add <> to a URL template, which configures the bahavior of the drilldown. + +image:images/url_drilldown_go_to_github.gif[Drilldown on pie chart that navigates to Github] + +. Add the *Sample web logs* data. + +. Open the *[Logs] Web traffic* dashboard. This isn’t data from Github, but works for demonstration purposes. + +. In the toolbar, click *Edit*. + +. Open the *[Logs] Visitors by OS* panel menu, then select *Create drilldown*. + +.. In the *Name* field, enter `Show on Github`. + +.. Select *Go to URL*. + +.. Enter the URL template: ++ +[source, bash] +---- +https://github.com/elastic/kibana/issues?q=is:issue+is:open+{{event.value}} +---- ++ +The example URL navigates to {kib} issues on Github. `{{event.value}}` is substituted with a value associated with a selected pie slice. In *URL preview*, `{{event.value}}` is substituted with a <> value. ++ +[role="screenshot"] +image:images/url_drilldown_url_template.png[URL template input] + +.. Click *Create drilldown*. ++ +The drilldown is stored as dashboard metadata. + +. Save the dashboard. ++ +If you fail to save the dashboard, the drilldown is lost when you navigate away from the dashboard. + +. On the *[Logs] Visitors by OS* panel, click any chart slice, then select *Show on Github*. ++ +[role="screenshot"] +image:images/url_drilldown_popup.png[URL drilldown popup] + +. On the page that lists the issues in the {kib} repository, verify the slice value appears in Github. ++ +[role="screenshot"] +image:images/url_drilldown_github.png[Github] + +include::url-drilldown.asciidoc[] diff --git a/docs/user/dashboard/url-drilldown.asciidoc b/docs/user/dashboard/url-drilldown.asciidoc index 212e29898bd40..872d83bfd9009 100644 --- a/docs/user/dashboard/url-drilldown.asciidoc +++ b/docs/user/dashboard/url-drilldown.asciidoc @@ -1,96 +1,9 @@ -[[url-drilldown]] -=== URL drilldown +[[url_templating]] +=== URL templating beta[] -The URL drilldown allows you to navigate from a dashboard to an internal or external URL. -The destination URL can be dynamic, depending on the dashboard context or user’s interaction with a visualization. - -For example, you might have a dashboard that shows data from a Github repository. -You can create a drilldown that navigates from this dashboard to Github. - -[role="screenshot"] -image:images/url_drilldown_go_to_github.gif[Drilldown on pie chart that navigates to Github] - -NOTE: URL drilldown is available with the https://www.elastic.co/subscriptions[Gold subscription] and higher. - -[float] -[[url-drilldown-supported-panels]] -==== Supported panels - -The following panels support URL drilldowns: - -* Lens -* Area -* Data table -* Heat map -* Horizontal bar -* Line -* Pie -* Tag cloud -* Vertical bar - -[float] -[[try-it]] -==== Try it: Create a URL drilldown - -This example shows how to create the "Show on Github" drilldown shown above. - -. Add the sample web logs data set. -. Open the *[Logs] Web traffic* dashboard. This isn’t data from Github, but it should work for demonstration purposes. -. In the dashboard menu bar, click *Edit*. -. In *[Logs] Visitors by OS*, open the panel menu, and then select *Create drilldown*. -. Give the drilldown a name: *Show on Github*. -. Select a drilldown action: *Go to URL*. -+ -[role="screenshot"] -image:images/url_drilldown_pick_an_action.png[Action picker] -. Enter a URL template: -+ -[source, bash] ----- -https://github.com/elastic/kibana/issues?q=is:issue+is:open+{{event.value}} ----- -+ -This example URL navigates to {kib} issues on Github. `{{event.value}}` will be substituted with a value associated with a clicked pie slice. In _preview_ `{{event.value}}` is substituted with a <> value. -[role="screenshot"] -image:images/url_drilldown_url_template.png[URL template input] -. Click *Create drilldown*. -. Save the dashboard. -+ -If you don’t save the drilldown, and then navigate away, the drilldown is lost. - -. In *[Logs] Visitors by OS*, click any slice of the pie, and then select the drilldown *Show on Github*. -+ -[role="screenshot"] -image:images/url_drilldown_popup.png[URL drilldown popup] -+ -You are navigated to the issue list in the {kib} repository. Verify that value from a pie slice you’ve clicked on is carried over to Github. -+ -[role="screenshot"] -image:images/url_drilldown_github.png[Github] - -[float] -[[trigger-picker]] -==== Picking a trigger for a URL drilldown - -Some panels support multiple user interactions (called triggers) for which you can configure a URL drilldown. The list of supported variables in the URL template depends on the trigger you selected. -In the preceding example, you configured a URL drilldown on a pie chart. The only trigger that pie chart supports is clicking on a pie slice, so you didn’t have to pick a trigger. - -However, the sample *[Logs] Unique Visitors vs. Average Bytes* chart supports both clicking on a data point and selecting a range. When you create a URL drilldown for this chart, you have the following choices: - -[role="screenshot"] -image:images/url_drilldown_trigger_picker.png[Trigger picker: Single click and Range selection] - -Variables in the URL template differ per trigger. -For example, *Single click* has `{{event.value}}` and *Range selection* has `{{event.from}}` and `{{event.to}}`. -You can create multiple URL drilldowns per panel and attach them to different triggers. - -[float] -[[templating]] -==== URL templating language - -The URL template input uses Handlebars — a simple templating language. Handlebars templates look like regular text with embedded Handlebars expressions. +The URL template input uses https://handlebarsjs.com/guide/expressions.html#expressions[Handlebars] — a simple templating language. Handlebars templates look like regular text with embedded Handlebars expressions. [source, bash] ---- @@ -99,14 +12,13 @@ https://github.com/elastic/kibana/issues?q={{event.value}} A Handlebars expression is a `{{`, some contents, followed by a `}}`. When the drilldown is executed, these expressions are replaced by values from the dashboard and interaction context. -Refer to Handlebars https://ela.st/handlebars-docs#expressions[documentation] to learn about advanced use cases. - [[helpers]] -In addition to https://ela.st/handlebars-helpers[built-in] Handlebars helpers, you can use the following custom helpers: +In addition to https://handlebarsjs.com/guide/builtin-helpers.html[built-in] Handlebars helpers, you can use custom helpers. +Refer to Handlebars https://ela.st/handlebars-docs#expressions[documentation] to learn about advanced use cases. |=== -|Helper |Use case +|Custom helper |Use case |json a|Serialize variables in JSON format. @@ -133,7 +45,7 @@ a|Format dates. Supports relative dates expressions (for example, "now-15d"). R Example: -`{{ date event.from “YYYY MM DD”}}` + +`{{date event.from “YYYY MM DD”}}` + `{{date “now-15”}}` |formatNumber @@ -240,7 +152,7 @@ For example, `{{context.panel.filters}}` are previewed with the current filters *Event* variables are extracted during drilldown execution from a user interaction with a panel (for example, from a pie slice that the user clicked on). Because there is no user interaction with a panel in preview, there is no interaction context to use in a preview. -To work around this, {kib} provides a sample interaction that relies on a picked <>. +To work around this, {kib} provides a sample interaction that relies on a trigger. So in a preview, you might notice that `{{event.value}}` is replaced with `{{event.value}}` instead of with a sample from your data. Such previews can help you make sure that the structure of your URL template is valid. However, to ensure that the configured URL drilldown works as expected with your data, you have to save the dashboard and test in the panel. @@ -340,14 +252,3 @@ Tip: Consider using <> helper for date formatting. | Aggregation field behind the selected range, if available. |=== - -[float] -[[disable]] -==== Disable URL drilldown - -You can disable URL drilldown feature on your {kib} instance by disabling the plugin: - -["source","yml"] ------------ -url_drilldown.enabled: false ------------ diff --git a/package.json b/package.json index ade567c840da7..38cf150094677 100644 --- a/package.json +++ b/package.json @@ -140,8 +140,6 @@ "@slack/webhook": "^5.0.0", "@storybook/addons": "^6.0.16", "@turf/circle": "6.0.1", - "@types/pdfmake": "^0.1.15", - "@types/yauzl": "^2.9.1", "JSONStream": "1.3.5", "abort-controller": "^3.0.0", "abortcontroller-polyfill": "^1.4.0", @@ -345,8 +343,8 @@ "@babel/register": "^7.10.5", "@babel/traverse": "^7.11.5", "@babel/types": "^7.11.0", - "@cypress/snapshot": "^2.1.3", - "@cypress/webpack-preprocessor": "^5.4.1", + "@cypress/snapshot": "^2.1.7", + "@cypress/webpack-preprocessor": "^5.4.10", "@elastic/apm-rum": "^5.6.1", "@elastic/apm-rum-react": "^1.2.5", "@elastic/charts": "24.0.0", @@ -509,6 +507,7 @@ "@types/ora": "^1.3.5", "@types/papaparse": "^5.0.3", "@types/parse-link-header": "^1.0.0", + "@types/pdfmake": "^0.1.15", "@types/pegjs": "^0.10.1", "@types/pngjs": "^3.4.0", "@types/prettier": "^2.0.2", @@ -566,6 +565,7 @@ "@types/write-pkg": "^3.1.0", "@types/xml-crypto": "^1.4.1", "@types/xml2js": "^0.4.5", + "@types/yauzl": "^2.9.1", "@types/zen-observable": "^0.8.0", "@typescript-eslint/eslint-plugin": "^3.10.0", "@typescript-eslint/parser": "^3.10.0", @@ -609,9 +609,9 @@ "cpy": "^8.1.1", "cronstrue": "^1.51.0", "css-loader": "^3.4.2", - "cypress": "^5.4.0", + "cypress": "^5.5.0", "cypress-cucumber-preprocessor": "^2.5.2", - "cypress-multi-reporters": "^1.2.3", + "cypress-multi-reporters": "^1.4.0", "d3": "3.5.17", "d3-cloud": "1.2.5", "d3-scale": "1.0.7", @@ -633,7 +633,7 @@ "eslint-module-utils": "2.5.0", "eslint-plugin-babel": "^5.3.0", "eslint-plugin-ban": "^1.4.0", - "eslint-plugin-cypress": "^2.8.1", + "eslint-plugin-cypress": "^2.11.2", "eslint-plugin-eslint-comments": "^3.2.0", "eslint-plugin-import": "^2.19.1", "eslint-plugin-jest": "^24.0.2", @@ -663,10 +663,9 @@ "graphql-codegen-typescript-common": "^0.18.2", "graphql-codegen-typescript-resolvers": "^0.18.2", "graphql-codegen-typescript-server": "^0.18.2", - "grunt": "1.0.4", + "grunt": "1.3.0", "grunt-available-tasks": "^0.6.3", "grunt-babel": "^8.0.0", - "grunt-cli": "^1.2.0", "grunt-contrib-clean": "^1.1.0", "grunt-contrib-copy": "^1.0.0", "grunt-contrib-watch": "^1.1.0", diff --git a/packages/kbn-plugin-helpers/README.md b/packages/kbn-plugin-helpers/README.md index d7ed3106c1ceb..05eb9f5998737 100644 --- a/packages/kbn-plugin-helpers/README.md +++ b/packages/kbn-plugin-helpers/README.md @@ -4,13 +4,20 @@ Just some helpers for kibana plugin devs. ## Installation -To install the plugin helpers use `yarn` to link to the package from the Kibana project: +You don't actually need to install the plugin helpers, they are automatically inherited from the Kibana project by building your plugin within the Kibana repo. To use the plugin helpers just create the needed npm scripts on your plugin's `package.json` (as exemplified below) which +is already the case if you use the new `node scripts/generate_plugin` script. -```sh -yarn add --dev link:../../kibana/packages/kbn-plugin-helpers +```json +{ + "scripts" : { + "build": "yarn plugin-helpers build", + "plugin-helpers": "node ../../scripts/plugin_helpers", + "kbn": "node ../../scripts/kbn" + } +} ``` -This will link the package from the repository into your plugin, but the `plugin-helpers` executable won't be available in your project until you run bootstrap again. +This will make it easier to execute the `plugin-helpers` script from within your plugin repository. ```sh yarn kbn bootstrap @@ -18,24 +25,38 @@ yarn kbn bootstrap ## Usage -This simple CLI has several tasks that plugin devs can run from to easily debug, test, or package kibana plugins. +This simple CLI has a build task that plugin devs can run from to easily package Kibana plugins. + +Previously you could also use that tool to start and test your plugin. Currently you can run +your plugin along with Kibana running `yarn start` in the Kibana repository root folder. Finally to test +your plugin you should now configure and use your own tools. ```sh $ plugin-helpers help - Usage: plugin-helpers [options] [command] + Usage: plugin-helpers [command] [options] Commands: - - start Start kibana and have it include this plugin - build [options] [files...] Build a distributable archive - test Run the server and browser tests - test:mocha [files...] Run the server tests using mocha - - Options: - - -h, --help output usage information - -V, --version output the version number + build + Copies files from the source into a zip archive that can be distributed for + installation into production Kibana installs. The archive includes the non- + development npm dependencies and builds itself using raw files in the source + directory so make sure they are clean/up to date. The resulting archive can + be found at: + + build/{plugin.id}-{kibanaVersion}.zip + + Options: + --skip-archive Don't create the zip file, just create the build/kibana directory + --kibana-version, -v Kibana version that the + + + Global options: + --verbose, -v Log verbosely + --debug Log debug messages (less than verbose) + --quiet Only log errors + --silent Don't log anything + --help Show this message ``` @@ -55,21 +76,13 @@ All configuration setting listed below can simply can be included in the json co ## Global settings -### Settings for `start` - -Setting | Description -------- | ----------- -`includePlugins` | Intended to be used in a config file, an array of additional plugin paths to include, absolute or relative to the plugin root -`*` | Any options/flags included will be passed unmodified to the Kibana binary - ### Settings for `build` Setting | Description ------- | ----------- +`serverSourcePatterns` | Defines the files that are built with babel and written to your distributable for your server plugin. It is ignored if `kibana.json` has none `server: true` setting defined. `skipArchive` | Don't create the zip file, leave the build path alone -`buildDestination` | Target path for the build output, absolute or relative to the plugin root `skipInstallDependencies` | Don't install dependencies defined in package.json into build output -`buildVersion` | Version for the build output `kibanaVersion` | Kibana version for the build output (added to package.json) ## TypeScript support diff --git a/src/core/public/doc_links/doc_links_service.ts b/src/core/public/doc_links/doc_links_service.ts index 48187fe465392..6af14734444d1 100644 --- a/src/core/public/doc_links/doc_links_service.ts +++ b/src/core/public/doc_links/doc_links_service.ts @@ -39,9 +39,9 @@ export class DocLinksService { dashboard: { guide: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/dashboard.html`, drilldowns: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/drilldowns.html`, - drilldownsTriggerPicker: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/url-drilldown.html#trigger-picker`, - urlDrilldownTemplateSyntax: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/url-drilldown.html#templating`, - urlDrilldownVariables: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/url-drilldown.html#variables`, + drilldownsTriggerPicker: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/drilldowns.html#url-drilldown`, + urlDrilldownTemplateSyntax: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/url_templating-language.html`, + urlDrilldownVariables: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/url_templating-language.html#variables`, }, filebeat: { base: `${ELASTIC_WEBSITE_URL}guide/en/beats/filebeat/${DOC_LINK_VERSION}`, @@ -131,6 +131,7 @@ export class DocLinksService { management: { kibanaSearchSettings: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/advanced-options.html#kibana-search-settings`, dashboardSettings: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/advanced-options.html#kibana-dashboard-settings`, + visualizationSettings: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/advanced-options.html#kibana-visualization-settings`, }, visualize: { guide: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/visualize.html`, diff --git a/src/core/server/environment/environment_service.mock.ts b/src/core/server/environment/environment_service.mock.ts index a956e369ba4a7..3c579b0f68b00 100644 --- a/src/core/server/environment/environment_service.mock.ts +++ b/src/core/server/environment/environment_service.mock.ts @@ -17,8 +17,7 @@ * under the License. */ import type { PublicMethodsOf } from '@kbn/utility-types'; - -import { EnvironmentService, InternalEnvironmentServiceSetup } from './environment_service'; +import type { EnvironmentService, InternalEnvironmentServiceSetup } from './environment_service'; const createSetupContractMock = () => { const setupContract: jest.Mocked = { diff --git a/src/core/server/i18n/fs.ts b/src/core/server/i18n/fs.ts new file mode 100644 index 0000000000000..23d729504f81c --- /dev/null +++ b/src/core/server/i18n/fs.ts @@ -0,0 +1,23 @@ +/* + * 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 Fs from 'fs'; +import { promisify } from 'util'; + +export const readFile = promisify(Fs.readFile); diff --git a/src/core/server/i18n/get_kibana_translation_files.test.ts b/src/core/server/i18n/get_kibana_translation_files.test.ts new file mode 100644 index 0000000000000..737d8ed8bc4e2 --- /dev/null +++ b/src/core/server/i18n/get_kibana_translation_files.test.ts @@ -0,0 +1,81 @@ +/* + * 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 { getKibanaTranslationFiles } from './get_kibana_translation_files'; +import { getTranslationPaths } from './get_translation_paths'; + +const mockGetTranslationPaths = getTranslationPaths as jest.Mock; + +jest.mock('./get_translation_paths', () => ({ + getTranslationPaths: jest.fn().mockResolvedValue([]), +})); +jest.mock('../utils', () => ({ + fromRoot: jest.fn().mockImplementation((path: string) => path), +})); + +const locale = 'en'; + +describe('getKibanaTranslationPaths', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('calls getTranslationPaths against kibana root and kibana-extra', async () => { + await getKibanaTranslationFiles(locale, []); + + expect(mockGetTranslationPaths).toHaveBeenCalledTimes(2); + + expect(mockGetTranslationPaths).toHaveBeenCalledWith({ + cwd: '.', + nested: true, + }); + + expect(mockGetTranslationPaths).toHaveBeenCalledWith({ + cwd: '../kibana-extra', + nested: true, + }); + }); + + it('calls getTranslationPaths for each config returned in plugin.paths and plugins.scanDirs', async () => { + const pluginPaths = ['/path/to/pluginA', '/path/to/pluginB']; + + await getKibanaTranslationFiles(locale, pluginPaths); + + expect(mockGetTranslationPaths).toHaveBeenCalledTimes(2 + pluginPaths.length); + + pluginPaths.forEach((pluginPath) => { + expect(mockGetTranslationPaths).toHaveBeenCalledWith({ + cwd: pluginPath, + nested: false, + }); + }); + }); + + it('only return files for specified locale', async () => { + mockGetTranslationPaths.mockResolvedValueOnce(['/root/en.json', '/root/fr.json']); + mockGetTranslationPaths.mockResolvedValueOnce([ + '/kibana-extra/en.json', + '/kibana-extra/fr.json', + ]); + + const translationFiles = await getKibanaTranslationFiles('en', []); + + expect(translationFiles).toEqual(['/root/en.json', '/kibana-extra/en.json']); + }); +}); diff --git a/src/legacy/server/i18n/get_kibana_translation_paths.ts b/src/core/server/i18n/get_kibana_translation_files.ts similarity index 64% rename from src/legacy/server/i18n/get_kibana_translation_paths.ts rename to src/core/server/i18n/get_kibana_translation_files.ts index d7f77d3185ba4..dacb6a1e16a5c 100644 --- a/src/legacy/server/i18n/get_kibana_translation_paths.ts +++ b/src/core/server/i18n/get_kibana_translation_files.ts @@ -17,26 +17,27 @@ * under the License. */ -import { KibanaConfig } from '../kbn_server'; -import { fromRoot } from '../../../core/server/utils'; -import { I18N_RC } from './constants'; +import { basename } from 'path'; +import { fromRoot } from '../utils'; import { getTranslationPaths } from './get_translation_paths'; -export async function getKibanaTranslationPaths(config: Pick) { - return await Promise.all([ +export const getKibanaTranslationFiles = async ( + locale: string, + pluginPaths: string[] +): Promise => { + const translationPaths = await Promise.all([ getTranslationPaths({ cwd: fromRoot('.'), - glob: `*/${I18N_RC}`, + nested: true, }), - ...(config.get('plugins.paths') as string[]).map((cwd) => - getTranslationPaths({ cwd, glob: I18N_RC }) - ), - ...(config.get('plugins.scanDirs') as string[]).map((cwd) => - getTranslationPaths({ cwd, glob: `*/${I18N_RC}` }) - ), + ...pluginPaths.map((pluginPath) => getTranslationPaths({ cwd: pluginPath, nested: false })), getTranslationPaths({ cwd: fromRoot('../kibana-extra'), - glob: `*/${I18N_RC}`, + nested: true, }), ]); -} + + return ([] as string[]) + .concat(...translationPaths) + .filter((translationPath) => basename(translationPath, '.json') === locale); +}; diff --git a/src/core/server/i18n/get_translation_paths.test.mocks.ts b/src/core/server/i18n/get_translation_paths.test.mocks.ts new file mode 100644 index 0000000000000..f3b688062523c --- /dev/null +++ b/src/core/server/i18n/get_translation_paths.test.mocks.ts @@ -0,0 +1,26 @@ +/* + * 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 const globbyMock = jest.fn(); +jest.doMock('globby', () => globbyMock); + +export const readFileMock = jest.fn(); +jest.doMock('./fs', () => ({ + readFile: readFileMock, +})); diff --git a/src/core/server/i18n/get_translation_paths.test.ts b/src/core/server/i18n/get_translation_paths.test.ts new file mode 100644 index 0000000000000..c6af59da07fb5 --- /dev/null +++ b/src/core/server/i18n/get_translation_paths.test.ts @@ -0,0 +1,90 @@ +/* + * 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 { resolve, join } from 'path'; +import { globbyMock, readFileMock } from './get_translation_paths.test.mocks'; +import { getTranslationPaths } from './get_translation_paths'; + +describe('getTranslationPaths', () => { + beforeEach(() => { + globbyMock.mockReset(); + readFileMock.mockReset(); + + globbyMock.mockResolvedValue([]); + readFileMock.mockResolvedValue('{}'); + }); + + it('calls `globby` with the correct parameters', async () => { + getTranslationPaths({ cwd: '/some/cwd', nested: false }); + + expect(globbyMock).toHaveBeenCalledTimes(1); + expect(globbyMock).toHaveBeenCalledWith('.i18nrc.json', { cwd: '/some/cwd' }); + + globbyMock.mockClear(); + + await getTranslationPaths({ cwd: '/other/cwd', nested: true }); + + expect(globbyMock).toHaveBeenCalledTimes(1); + expect(globbyMock).toHaveBeenCalledWith('*/.i18nrc.json', { cwd: '/other/cwd' }); + }); + + it('calls `readFile` for each entry returned by `globby`', async () => { + const entries = [join('pathA', '.i18nrc.json'), join('pathB', '.i18nrc.json')]; + globbyMock.mockResolvedValue(entries); + + const cwd = '/kibana-extra'; + + await getTranslationPaths({ cwd, nested: true }); + + expect(readFileMock).toHaveBeenCalledTimes(2); + + expect(readFileMock).toHaveBeenNthCalledWith(1, resolve(cwd, entries[0]), 'utf8'); + expect(readFileMock).toHaveBeenNthCalledWith(2, resolve(cwd, entries[1]), 'utf8'); + }); + + it('returns the absolute path to the translation files', async () => { + const entries = ['.i18nrc.json']; + globbyMock.mockResolvedValue(entries); + + const i18nFileContent = { + translations: ['translations/en.json', 'translations/fr.json'], + }; + readFileMock.mockResolvedValue(JSON.stringify(i18nFileContent)); + + const cwd = '/cwd'; + + const translationPaths = await getTranslationPaths({ cwd, nested: true }); + + expect(translationPaths).toEqual([ + resolve(cwd, 'translations/en.json'), + resolve(cwd, 'translations/fr.json'), + ]); + }); + + it('throws if i18nrc parsing fails', async () => { + globbyMock.mockResolvedValue(['.i18nrc.json']); + readFileMock.mockRejectedValue(new Error('error parsing file')); + + await expect( + getTranslationPaths({ cwd: '/cwd', nested: true }) + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"Failed to parse .i18nrc.json file at /cwd/.i18nrc.json"` + ); + }); +}); diff --git a/src/legacy/server/i18n/get_translation_paths.ts b/src/core/server/i18n/get_translation_paths.ts similarity index 85% rename from src/legacy/server/i18n/get_translation_paths.ts rename to src/core/server/i18n/get_translation_paths.ts index a2a292e2278be..41d9dc4722e37 100644 --- a/src/legacy/server/i18n/get_translation_paths.ts +++ b/src/core/server/i18n/get_translation_paths.ts @@ -17,18 +17,18 @@ * under the License. */ -import { promisify } from 'util'; -import { readFile } from 'fs'; import { resolve, dirname } from 'path'; import globby from 'globby'; - -const readFileAsync = promisify(readFile); +import { readFile } from './fs'; interface I18NRCFileStructure { translations?: string[]; } -export async function getTranslationPaths({ cwd, glob }: { cwd: string; glob: string }) { +const I18N_RC = '.i18nrc.json'; + +export async function getTranslationPaths({ cwd, nested }: { cwd: string; nested: boolean }) { + const glob = nested ? `*/${I18N_RC}` : I18N_RC; const entries = await globby(glob, { cwd }); const translationPaths: string[] = []; @@ -36,7 +36,7 @@ export async function getTranslationPaths({ cwd, glob }: { cwd: string; glob: st const entryFullPath = resolve(cwd, entry); const pluginBasePath = dirname(entryFullPath); try { - const content = await readFileAsync(entryFullPath, 'utf8'); + const content = await readFile(entryFullPath, 'utf8'); const { translations } = JSON.parse(content) as I18NRCFileStructure; if (translations && translations.length) { translations.forEach((translation) => { diff --git a/src/core/server/i18n/i18n_config.ts b/src/core/server/i18n/i18n_config.ts new file mode 100644 index 0000000000000..f181c52dc4b06 --- /dev/null +++ b/src/core/server/i18n/i18n_config.ts @@ -0,0 +1,29 @@ +/* + * 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, TypeOf } from '@kbn/config-schema'; + +export const config = { + path: 'i18n', + schema: schema.object({ + locale: schema.string({ defaultValue: 'en' }), + }), +}; + +export type I18nConfigType = TypeOf; diff --git a/src/core/server/i18n/i18n_service.mock.ts b/src/core/server/i18n/i18n_service.mock.ts new file mode 100644 index 0000000000000..679751aefbf27 --- /dev/null +++ b/src/core/server/i18n/i18n_service.mock.ts @@ -0,0 +1,50 @@ +/* + * 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 { PublicMethodsOf } from '@kbn/utility-types'; +import type { I18nServiceSetup, I18nService } from './i18n_service'; + +const createSetupContractMock = () => { + const mock: jest.Mocked = { + getLocale: jest.fn(), + getTranslationFiles: jest.fn(), + }; + + mock.getLocale.mockReturnValue('en'); + mock.getTranslationFiles.mockReturnValue([]); + + return mock; +}; + +type I18nServiceContract = PublicMethodsOf; + +const createMock = () => { + const mock: jest.Mocked = { + setup: jest.fn(), + }; + + mock.setup.mockResolvedValue(createSetupContractMock()); + + return mock; +}; + +export const i18nServiceMock = { + create: createMock, + createSetupContract: createSetupContractMock, +}; diff --git a/src/core/server/i18n/i18n_service.test.mocks.ts b/src/core/server/i18n/i18n_service.test.mocks.ts new file mode 100644 index 0000000000000..23f97a1404fff --- /dev/null +++ b/src/core/server/i18n/i18n_service.test.mocks.ts @@ -0,0 +1,28 @@ +/* + * 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 const getKibanaTranslationFilesMock = jest.fn(); +jest.doMock('./get_kibana_translation_files', () => ({ + getKibanaTranslationFiles: getKibanaTranslationFilesMock, +})); + +export const initTranslationsMock = jest.fn(); +jest.doMock('./init_translations', () => ({ + initTranslations: initTranslationsMock, +})); diff --git a/src/core/server/i18n/i18n_service.test.ts b/src/core/server/i18n/i18n_service.test.ts new file mode 100644 index 0000000000000..87de39a92ab26 --- /dev/null +++ b/src/core/server/i18n/i18n_service.test.ts @@ -0,0 +1,84 @@ +/* + * 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 { getKibanaTranslationFilesMock, initTranslationsMock } from './i18n_service.test.mocks'; + +import { BehaviorSubject } from 'rxjs'; +import { I18nService } from './i18n_service'; + +import { configServiceMock } from '../config/mocks'; +import { mockCoreContext } from '../core_context.mock'; + +const getConfigService = (locale = 'en') => { + const configService = configServiceMock.create(); + configService.atPath.mockImplementation((path) => { + if (path === 'i18n') { + return new BehaviorSubject({ + locale, + }); + } + return new BehaviorSubject({}); + }); + return configService; +}; + +describe('I18nService', () => { + let service: I18nService; + let configService: ReturnType; + + beforeEach(() => { + jest.clearAllMocks(); + configService = getConfigService(); + + const coreContext = mockCoreContext.create({ configService }); + service = new I18nService(coreContext); + }); + + describe('#setup', () => { + it('calls `getKibanaTranslationFiles` with the correct parameters', async () => { + getKibanaTranslationFilesMock.mockResolvedValue([]); + + const pluginPaths = ['/pathA', '/pathB']; + await service.setup({ pluginPaths }); + + expect(getKibanaTranslationFilesMock).toHaveBeenCalledTimes(1); + expect(getKibanaTranslationFilesMock).toHaveBeenCalledWith('en', pluginPaths); + }); + + it('calls `initTranslations` with the correct parameters', async () => { + const translationFiles = ['/path/to/file', 'path/to/another/file']; + getKibanaTranslationFilesMock.mockResolvedValue(translationFiles); + + await service.setup({ pluginPaths: [] }); + + expect(initTranslationsMock).toHaveBeenCalledTimes(1); + expect(initTranslationsMock).toHaveBeenCalledWith('en', translationFiles); + }); + + it('returns accessors for locale and translation files', async () => { + const translationFiles = ['/path/to/file', 'path/to/another/file']; + getKibanaTranslationFilesMock.mockResolvedValue(translationFiles); + + const { getLocale, getTranslationFiles } = await service.setup({ pluginPaths: [] }); + + expect(getLocale()).toEqual('en'); + expect(getTranslationFiles()).toEqual(translationFiles); + }); + }); +}); diff --git a/src/core/server/i18n/i18n_service.ts b/src/core/server/i18n/i18n_service.ts new file mode 100644 index 0000000000000..fd32dd7fdd6ef --- /dev/null +++ b/src/core/server/i18n/i18n_service.ts @@ -0,0 +1,75 @@ +/* + * 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 { take } from 'rxjs/operators'; +import { Logger } from '../logging'; +import { IConfigService } from '../config'; +import { CoreContext } from '../core_context'; +import { config as i18nConfigDef, I18nConfigType } from './i18n_config'; +import { getKibanaTranslationFiles } from './get_kibana_translation_files'; +import { initTranslations } from './init_translations'; + +interface SetupDeps { + pluginPaths: string[]; +} + +/** + * @public + */ +export interface I18nServiceSetup { + /** + * Return the locale currently in use. + */ + getLocale(): string; + + /** + * Return the absolute paths to translation files currently in use. + */ + getTranslationFiles(): string[]; +} + +export class I18nService { + private readonly log: Logger; + private readonly configService: IConfigService; + + constructor(coreContext: CoreContext) { + this.log = coreContext.logger.get('i18n'); + this.configService = coreContext.configService; + } + + public async setup({ pluginPaths }: SetupDeps): Promise { + const i18nConfig = await this.configService + .atPath(i18nConfigDef.path) + .pipe(take(1)) + .toPromise(); + + const locale = i18nConfig.locale; + this.log.debug(`Using locale: ${locale}`); + + const translationFiles = await getKibanaTranslationFiles(locale, pluginPaths); + + this.log.debug(`Using translation files: [${translationFiles.join(', ')}]`); + await initTranslations(locale, translationFiles); + + return { + getLocale: () => locale, + getTranslationFiles: () => translationFiles, + }; + } +} diff --git a/src/core/server/i18n/index.ts b/src/core/server/i18n/index.ts new file mode 100644 index 0000000000000..2db3fdeb405d9 --- /dev/null +++ b/src/core/server/i18n/index.ts @@ -0,0 +1,21 @@ +/* + * 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 { config, I18nConfigType } from './i18n_config'; +export { I18nService, I18nServiceSetup } from './i18n_service'; diff --git a/src/core/server/i18n/init_translations.ts b/src/core/server/i18n/init_translations.ts new file mode 100644 index 0000000000000..94e6d41019ad2 --- /dev/null +++ b/src/core/server/i18n/init_translations.ts @@ -0,0 +1,31 @@ +/* + * 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, i18nLoader } from '@kbn/i18n'; + +export const initTranslations = async (locale: string, translationFiles: string[]) => { + i18nLoader.registerTranslationFiles(translationFiles); + const translations = await i18nLoader.getTranslationsByLocale(locale); + i18n.init( + Object.freeze({ + locale, + ...translations, + }) + ); +}; diff --git a/src/core/server/index.ts b/src/core/server/index.ts index 0adda4770639d..7b19c3a686757 100644 --- a/src/core/server/index.ts +++ b/src/core/server/index.ts @@ -64,6 +64,7 @@ import { MetricsServiceSetup, MetricsServiceStart } from './metrics'; import { StatusServiceSetup } from './status'; import { AppenderConfigType, appendersSchema, LoggingServiceSetup } from './logging'; import { CoreUsageDataStart } from './core_usage_data'; +import { I18nServiceSetup } from './i18n'; // Because of #79265 we need to explicity import, then export these types for // scripts/telemetry_check.js to work as expected @@ -336,6 +337,8 @@ export { MetricsServiceStart, } from './metrics'; +export { I18nServiceSetup } from './i18n'; + export { AppCategory } from '../types'; export { DEFAULT_APP_CATEGORIES } from '../utils'; @@ -421,6 +424,8 @@ export interface CoreSetup = KbnServer as any; @@ -75,6 +76,7 @@ beforeEach(() => { capabilities: capabilitiesServiceMock.createSetupContract(), context: contextServiceMock.createSetupContract(), elasticsearch: { legacy: {} } as any, + i18n: i18nServiceMock.createSetupContract(), uiSettings: uiSettingsServiceMock.createSetupContract(), http: { ...httpServiceMock.createInternalSetupContract(), diff --git a/src/core/server/legacy/legacy_service.ts b/src/core/server/legacy/legacy_service.ts index c42771179aba2..3111c8daf7981 100644 --- a/src/core/server/legacy/legacy_service.ts +++ b/src/core/server/legacy/legacy_service.ts @@ -251,6 +251,7 @@ export class LegacyService implements CoreService { csp: setupDeps.core.http.csp, getServerInfo: setupDeps.core.http.getServerInfo, }, + i18n: setupDeps.core.i18n, logging: { configure: (config$) => setupDeps.core.logging.configure([], config$), }, diff --git a/src/core/server/metrics/metrics_service.mock.ts b/src/core/server/metrics/metrics_service.mock.ts index 0d9e9af39317c..6faccc9c9da56 100644 --- a/src/core/server/metrics/metrics_service.mock.ts +++ b/src/core/server/metrics/metrics_service.mock.ts @@ -19,7 +19,7 @@ import { BehaviorSubject } from 'rxjs'; import type { PublicMethodsOf } from '@kbn/utility-types'; -import { MetricsService } from './metrics_service'; +import type { MetricsService } from './metrics_service'; import { InternalMetricsServiceSetup, InternalMetricsServiceStart, diff --git a/src/core/server/mocks.ts b/src/core/server/mocks.ts index 8ca0c82219ed4..03a0ae2d6443a 100644 --- a/src/core/server/mocks.ts +++ b/src/core/server/mocks.ts @@ -38,6 +38,7 @@ import { metricsServiceMock } from './metrics/metrics_service.mock'; import { environmentServiceMock } from './environment/environment_service.mock'; import { statusServiceMock } from './status/status_service.mock'; import { coreUsageDataServiceMock } from './core_usage_data/core_usage_data_service.mock'; +import { i18nServiceMock } from './i18n/i18n_service.mock'; export { configServiceMock } from './config/mocks'; export { httpServerMock } from './http/http_server.mocks'; @@ -57,6 +58,7 @@ export { statusServiceMock } from './status/status_service.mock'; export { contextServiceMock } from './context/context_service.mock'; export { capabilitiesServiceMock } from './capabilities/capabilities_service.mock'; export { coreUsageDataServiceMock } from './core_usage_data/core_usage_data_service.mock'; +export { i18nServiceMock } from './i18n/i18n_service.mock'; export function pluginInitializerContextConfigMock(config: T) { const globalConfig: SharedGlobalConfig = { @@ -136,6 +138,7 @@ function createCoreSetupMock({ context: contextServiceMock.createSetupContract(), elasticsearch: elasticsearchServiceMock.createSetup(), http: httpMock, + i18n: i18nServiceMock.createSetupContract(), savedObjects: savedObjectsServiceMock.createInternalSetupContract(), status: statusServiceMock.createSetupContract(), uiSettings: uiSettingsMock, @@ -172,6 +175,7 @@ function createInternalCoreSetupMock() { savedObjects: savedObjectsServiceMock.createInternalSetupContract(), status: statusServiceMock.createInternalSetupContract(), environment: environmentServiceMock.createSetupContract(), + i18n: i18nServiceMock.createSetupContract(), httpResources: httpResourcesMock.createSetupContract(), rendering: renderingMock.createSetupContract(), uiSettings: uiSettingsServiceMock.createSetupContract(), diff --git a/src/core/server/plugins/plugin_context.ts b/src/core/server/plugins/plugin_context.ts index 22e79741e854c..3b2634ddbe315 100644 --- a/src/core/server/plugins/plugin_context.ts +++ b/src/core/server/plugins/plugin_context.ts @@ -176,6 +176,7 @@ export function createPluginSetupContext( csp: deps.http.csp, getServerInfo: deps.http.getServerInfo, }, + i18n: deps.i18n, logging: { configure: (config$) => deps.logging.configure(['plugins', plugin.name], config$), }, diff --git a/src/core/server/plugins/plugins_service.mock.ts b/src/core/server/plugins/plugins_service.mock.ts index 14d6de889dd42..3ea8badb0f450 100644 --- a/src/core/server/plugins/plugins_service.mock.ts +++ b/src/core/server/plugins/plugins_service.mock.ts @@ -17,7 +17,7 @@ * under the License. */ import type { PublicMethodsOf } from '@kbn/utility-types'; -import { PluginsService, PluginsServiceSetup } from './plugins_service'; +import type { PluginsService, PluginsServiceSetup } from './plugins_service'; type PluginsServiceMock = jest.Mocked>; diff --git a/src/core/server/plugins/plugins_service.test.ts b/src/core/server/plugins/plugins_service.test.ts index 64a382e539fb0..02b82c17ed4fc 100644 --- a/src/core/server/plugins/plugins_service.test.ts +++ b/src/core/server/plugins/plugins_service.test.ts @@ -127,6 +127,7 @@ async function testSetup(options: { isDevClusterMaster?: boolean } = {}) { [mockPluginSystem] = MockPluginsSystem.mock.instances as any; mockPluginSystem.uiPlugins.mockReturnValue(new Map()); + mockPluginSystem.getPlugins.mockReturnValue([]); environmentSetup = environmentServiceMock.createSetupContract(); } @@ -469,6 +470,22 @@ describe('PluginsService', () => { deprecationProvider ); }); + + it('returns the paths of the plugins', async () => { + const pluginA = createPlugin('A', { path: '/plugin-A-path', configPath: 'pathA' }); + const pluginB = createPlugin('B', { path: '/plugin-B-path', configPath: 'pathB' }); + + mockDiscover.mockReturnValue({ + error$: from([]), + plugin$: from([]), + }); + + mockPluginSystem.getPlugins.mockReturnValue([pluginA, pluginB]); + + const { pluginPaths } = await pluginsService.discover({ environment: environmentSetup }); + + expect(pluginPaths).toEqual(['/plugin-A-path', '/plugin-B-path']); + }); }); describe('#generateUiPluginsConfigs()', () => { @@ -633,6 +650,7 @@ describe('PluginService when isDevClusterMaster is true', () => { await expect(pluginsService.discover({ environment: environmentSetup })).resolves .toMatchInlineSnapshot(` Object { + "pluginPaths": Array [], "pluginTree": undefined, "uiPlugins": Object { "browserConfigs": Map {}, diff --git a/src/core/server/plugins/plugins_service.ts b/src/core/server/plugins/plugins_service.ts index a1062bde7765f..5967e6d5358de 100644 --- a/src/core/server/plugins/plugins_service.ts +++ b/src/core/server/plugins/plugins_service.ts @@ -118,6 +118,7 @@ export class PluginsService implements CoreService plugin.path), uiPlugins: { internal: this.uiPluginInternalInfo, public: uiPlugins, diff --git a/src/core/server/plugins/plugins_system.test.ts b/src/core/server/plugins/plugins_system.test.ts index ae9267ca5cf60..89a3697ebe9cd 100644 --- a/src/core/server/plugins/plugins_system.test.ts +++ b/src/core/server/plugins/plugins_system.test.ts @@ -92,6 +92,15 @@ test('can be setup even without plugins', async () => { expect(pluginsSetup.size).toBe(0); }); +test('getPlugins returns the list of plugins', () => { + const pluginA = createPlugin('plugin-a'); + const pluginB = createPlugin('plugin-b'); + pluginsSystem.addPlugin(pluginA); + pluginsSystem.addPlugin(pluginB); + + expect(pluginsSystem.getPlugins()).toEqual([pluginA, pluginB]); +}); + test('getPluginDependencies returns dependency tree of symbols', () => { pluginsSystem.addPlugin(createPlugin('plugin-a', { required: ['no-dep'] })); pluginsSystem.addPlugin( diff --git a/src/core/server/plugins/plugins_system.ts b/src/core/server/plugins/plugins_system.ts index 72d2cfe158b37..23dc77b7bf673 100644 --- a/src/core/server/plugins/plugins_system.ts +++ b/src/core/server/plugins/plugins_system.ts @@ -42,6 +42,10 @@ export class PluginsSystem { this.plugins.set(plugin.name, plugin); } + public getPlugins() { + return [...this.plugins.values()]; + } + /** * @returns a ReadonlyMap of each plugin and an Array of its available dependencies * @internal diff --git a/src/core/server/rendering/__mocks__/rendering_service.ts b/src/core/server/rendering/__mocks__/rendering_service.ts index 01d084f9ae53c..00e617de82542 100644 --- a/src/core/server/rendering/__mocks__/rendering_service.ts +++ b/src/core/server/rendering/__mocks__/rendering_service.ts @@ -17,8 +17,8 @@ * under the License. */ import type { PublicMethodsOf } from '@kbn/utility-types'; -import { RenderingService as Service } from '../rendering_service'; -import { InternalRenderingServiceSetup } from '../types'; +import type { RenderingService as Service } from '../rendering_service'; +import type { InternalRenderingServiceSetup } from '../types'; import { mockRenderingServiceParams } from './params'; type IRenderingService = PublicMethodsOf; diff --git a/src/core/server/saved_objects/saved_objects_service.mock.ts b/src/core/server/saved_objects/saved_objects_service.mock.ts index c56cdabf6e4cd..85dbf4b5e8c6a 100644 --- a/src/core/server/saved_objects/saved_objects_service.mock.ts +++ b/src/core/server/saved_objects/saved_objects_service.mock.ts @@ -19,8 +19,7 @@ import { BehaviorSubject } from 'rxjs'; import type { PublicMethodsOf } from '@kbn/utility-types'; - -import { +import type { SavedObjectsService, InternalSavedObjectsServiceSetup, InternalSavedObjectsServiceStart, diff --git a/src/core/server/saved_objects/service/lib/search_dsl/query_params.test.ts b/src/core/server/saved_objects/service/lib/search_dsl/query_params.test.ts index 333f5caf72525..e78b944183df9 100644 --- a/src/core/server/saved_objects/service/lib/search_dsl/query_params.test.ts +++ b/src/core/server/saved_objects/service/lib/search_dsl/query_params.test.ts @@ -21,28 +21,64 @@ import { esKuery } from '../../../es_query'; type KueryNode = any; -import { typeRegistryMock } from '../../../saved_objects_type_registry.mock'; -import { ALL_NAMESPACES_STRING } from '../utils'; +import { ALL_NAMESPACES_STRING, DEFAULT_NAMESPACE_STRING } from '../utils'; +import { SavedObjectTypeRegistry } from '../../../saved_objects_type_registry'; import { getQueryParams, getClauseForReference } from './query_params'; -const registry = typeRegistryMock.create(); +const registerTypes = (registry: SavedObjectTypeRegistry) => { + registry.registerType({ + name: 'pending', + hidden: false, + namespaceType: 'single', + mappings: { + properties: { title: { type: 'text' } }, + }, + management: { + defaultSearchField: 'title', + }, + }); -const MAPPINGS = { - properties: { - pending: { properties: { title: { type: 'text' } } }, - saved: { + registry.registerType({ + name: 'saved', + hidden: false, + namespaceType: 'single', + mappings: { properties: { title: { type: 'text', fields: { raw: { type: 'keyword' } } }, obj: { properties: { key1: { type: 'text' } } }, }, }, - // mock registry returns isMultiNamespace=true for 'shared' type - shared: { properties: { name: { type: 'keyword' } } }, - // mock registry returns isNamespaceAgnostic=true for 'global' type - global: { properties: { name: { type: 'keyword' } } }, - }, + management: { + defaultSearchField: 'title', + }, + }); + + registry.registerType({ + name: 'shared', + hidden: false, + namespaceType: 'multiple', + mappings: { + properties: { name: { type: 'keyword' } }, + }, + management: { + defaultSearchField: 'name', + }, + }); + + registry.registerType({ + name: 'global', + hidden: false, + namespaceType: 'agnostic', + mappings: { + properties: { name: { type: 'keyword' } }, + }, + management: { + defaultSearchField: 'name', + }, + }); }; -const ALL_TYPES = Object.keys(MAPPINGS.properties); + +const ALL_TYPES = ['pending', 'saved', 'shared', 'global']; // get all possible subsets (combination) of all types const ALL_TYPE_SUBSETS = ALL_TYPES.reduce( (subsets, value) => subsets.concat(subsets.map((set) => [...set, value])), @@ -51,48 +87,57 @@ const ALL_TYPE_SUBSETS = ALL_TYPES.reduce( .filter((x) => x.length) // exclude empty set .map((x) => (x.length === 1 ? x[0] : x)); // if a subset is a single string, destructure it -const createTypeClause = (type: string, namespaces?: string[]) => { - if (registry.isMultiNamespace(type)) { - const array = [...(namespaces ?? ['default']), ALL_NAMESPACES_STRING]; - return { - bool: { - must: expect.arrayContaining([{ terms: { namespaces: array } }]), - must_not: [{ exists: { field: 'namespace' } }], - }, - }; - } else if (registry.isSingleNamespace(type)) { - const nonDefaultNamespaces = namespaces?.filter((n) => n !== 'default') ?? []; - const should: any = []; - if (nonDefaultNamespaces.length > 0) { - should.push({ terms: { namespace: nonDefaultNamespaces } }); - } - if (namespaces?.includes('default')) { - should.push({ bool: { must_not: [{ exists: { field: 'namespace' } }] } }); - } - return { - bool: { - must: [{ term: { type } }], - should: expect.arrayContaining(should), - minimum_should_match: 1, - must_not: [{ exists: { field: 'namespaces' } }], - }, - }; - } - // isNamespaceAgnostic - return { - bool: expect.objectContaining({ - must_not: [{ exists: { field: 'namespace' } }, { exists: { field: 'namespaces' } }], - }), - }; -}; - /** * Note: these tests cases are defined in the order they appear in the source code, for readability's sake */ describe('#getQueryParams', () => { - const mappings = MAPPINGS; + let registry: SavedObjectTypeRegistry; type Result = ReturnType; + beforeEach(() => { + registry = new SavedObjectTypeRegistry(); + registerTypes(registry); + }); + + const createTypeClause = (type: string, namespaces?: string[]) => { + if (registry.isMultiNamespace(type)) { + const array = [...(namespaces ?? [DEFAULT_NAMESPACE_STRING]), ALL_NAMESPACES_STRING]; + + return { + bool: { + must: namespaces?.includes(ALL_NAMESPACES_STRING) + ? [{ term: { type } }] + : [{ term: { type } }, { terms: { namespaces: array } }], + must_not: [{ exists: { field: 'namespace' } }], + }, + }; + } else if (registry.isSingleNamespace(type)) { + const nonDefaultNamespaces = namespaces?.filter((n) => n !== DEFAULT_NAMESPACE_STRING) ?? []; + const searchingAcrossAllNamespaces = namespaces?.includes(ALL_NAMESPACES_STRING) ?? false; + const should: any = []; + if (nonDefaultNamespaces.length > 0 && !searchingAcrossAllNamespaces) { + should.push({ terms: { namespace: nonDefaultNamespaces } }); + } + if (namespaces?.includes(DEFAULT_NAMESPACE_STRING)) { + should.push({ bool: { must_not: [{ exists: { field: 'namespace' } }] } }); + } + return { + bool: { + must: [{ term: { type } }], + should: expect.arrayContaining(should), + minimum_should_match: 1, + must_not: [{ exists: { field: 'namespaces' } }], + }, + }; + } + // isNamespaceAgnostic + return { + bool: expect.objectContaining({ + must_not: [{ exists: { field: 'namespace' } }, { exists: { field: 'namespaces' } }], + }), + }; + }; + describe('kueryNode filter clause', () => { const expectResult = (result: Result, expected: any) => { expect(result.query.bool.filter).toEqual(expect.arrayContaining([expected])); @@ -100,13 +145,13 @@ describe('#getQueryParams', () => { describe('`kueryNode` parameter', () => { it('does not include the clause when `kueryNode` is not specified', () => { - const result = getQueryParams({ mappings, registry, kueryNode: undefined }); + const result = getQueryParams({ registry, kueryNode: undefined }); expect(result.query.bool.filter).toHaveLength(1); }); it('includes the specified Kuery clause', () => { const test = (kueryNode: KueryNode) => { - const result = getQueryParams({ mappings, registry, kueryNode }); + const result = getQueryParams({ registry, kueryNode }); const expected = esKuery.toElasticsearchQuery(kueryNode); expect(result.query.bool.filter).toHaveLength(2); expectResult(result, expected); @@ -165,7 +210,6 @@ describe('#getQueryParams', () => { it('does not include the clause when `hasReference` is not specified', () => { const result = getQueryParams({ - mappings, registry, hasReference: undefined, }); @@ -176,7 +220,6 @@ describe('#getQueryParams', () => { it('creates a should clause for specified reference when operator is `OR`', () => { const hasReference = { id: 'foo', type: 'bar' }; const result = getQueryParams({ - mappings, registry, hasReference, hasReferenceOperator: 'OR', @@ -192,7 +235,6 @@ describe('#getQueryParams', () => { it('creates a must clause for specified reference when operator is `AND`', () => { const hasReference = { id: 'foo', type: 'bar' }; const result = getQueryParams({ - mappings, registry, hasReference, hasReferenceOperator: 'AND', @@ -210,7 +252,6 @@ describe('#getQueryParams', () => { { id: 'hello', type: 'dolly' }, ]; const result = getQueryParams({ - mappings, registry, hasReference, hasReferenceOperator: 'OR', @@ -229,7 +270,6 @@ describe('#getQueryParams', () => { { id: 'hello', type: 'dolly' }, ]; const result = getQueryParams({ - mappings, registry, hasReference, hasReferenceOperator: 'AND', @@ -244,7 +284,6 @@ describe('#getQueryParams', () => { it('defaults to `OR` when operator is not specified', () => { const hasReference = { id: 'foo', type: 'bar' }; const result = getQueryParams({ - mappings, registry, hasReference, }); @@ -278,14 +317,13 @@ describe('#getQueryParams', () => { }; it('searches for all known types when `type` is not specified', () => { - const result = getQueryParams({ mappings, registry, type: undefined }); + const result = getQueryParams({ registry, type: undefined }); expectResult(result, ...ALL_TYPES); }); it('searches for specified type/s', () => { const test = (typeOrTypes: string | string[]) => { const result = getQueryParams({ - mappings, registry, type: typeOrTypes, }); @@ -309,18 +347,17 @@ describe('#getQueryParams', () => { const test = (namespaces?: string[]) => { for (const typeOrTypes of ALL_TYPE_SUBSETS) { - const result = getQueryParams({ mappings, registry, type: typeOrTypes, namespaces }); + const result = getQueryParams({ registry, type: typeOrTypes, namespaces }); const types = Array.isArray(typeOrTypes) ? typeOrTypes : [typeOrTypes]; expectResult(result, ...types.map((x) => createTypeClause(x, namespaces))); } // also test with no specified type/s - const result = getQueryParams({ mappings, registry, type: undefined, namespaces }); + const result = getQueryParams({ registry, type: undefined, namespaces }); expectResult(result, ...ALL_TYPES.map((x) => createTypeClause(x, namespaces))); }; - it('normalizes and deduplicates provided namespaces', () => { + it('deduplicates provided namespaces', () => { const result = getQueryParams({ - mappings, registry, search: '*', namespaces: ['foo', '*', 'foo', 'bar', 'default'], @@ -328,7 +365,7 @@ describe('#getQueryParams', () => { expectResult( result, - ...ALL_TYPES.map((x) => createTypeClause(x, ['foo', 'default', 'bar'])) + ...ALL_TYPES.map((x) => createTypeClause(x, ['foo', '*', 'bar', 'default'])) ); }); @@ -360,7 +397,6 @@ describe('#getQueryParams', () => { it('supersedes `type` and `namespaces` parameters', () => { const result = getQueryParams({ - mappings, registry, type: ['pending', 'saved', 'shared', 'global'], namespaces: ['foo', 'bar', 'default'], @@ -381,148 +417,266 @@ describe('#getQueryParams', () => { }); }); - describe('search clause (query.bool.must.simple_query_string)', () => { - const search = 'foo*'; + describe('search clause (query.bool)', () => { + describe('when using simple search (query.bool.must.simple_query_string)', () => { + const search = 'foo'; - const expectResult = (result: Result, sqsClause: any) => { - expect(result.query.bool.must).toEqual([{ simple_query_string: sqsClause }]); - }; + const expectResult = (result: Result, sqsClause: any) => { + expect(result.query.bool.must).toEqual([{ simple_query_string: sqsClause }]); + }; - describe('`search` parameter', () => { - it('does not include clause when `search` is not specified', () => { - const result = getQueryParams({ - mappings, - registry, - search: undefined, + describe('`search` parameter', () => { + it('does not include clause when `search` is not specified', () => { + const result = getQueryParams({ + registry, + search: undefined, + }); + expect(result.query.bool.must).toBeUndefined(); }); - expect(result.query.bool.must).toBeUndefined(); - }); - it('creates a clause with query for specified search', () => { - const result = getQueryParams({ - mappings, - registry, - search, + it('creates a clause with query for specified search', () => { + const result = getQueryParams({ + registry, + search, + }); + expectResult(result, expect.objectContaining({ query: search })); }); - expectResult(result, expect.objectContaining({ query: search })); }); - }); - describe('`searchFields` and `rootSearchFields` parameters', () => { - const getExpectedFields = (searchFields: string[], typeOrTypes: string | string[]) => { - const types = Array.isArray(typeOrTypes) ? typeOrTypes : [typeOrTypes]; - return searchFields.map((x) => types.map((y) => `${y}.${x}`)).flat(); - }; + describe('`searchFields` and `rootSearchFields` parameters', () => { + const getExpectedFields = (searchFields: string[], typeOrTypes: string | string[]) => { + const types = Array.isArray(typeOrTypes) ? typeOrTypes : [typeOrTypes]; + return searchFields.map((x) => types.map((y) => `${y}.${x}`)).flat(); + }; - const test = ({ - searchFields, - rootSearchFields, - }: { - searchFields?: string[]; - rootSearchFields?: string[]; - }) => { - for (const typeOrTypes of ALL_TYPE_SUBSETS) { + const test = ({ + searchFields, + rootSearchFields, + }: { + searchFields?: string[]; + rootSearchFields?: string[]; + }) => { + for (const typeOrTypes of ALL_TYPE_SUBSETS) { + const result = getQueryParams({ + registry, + type: typeOrTypes, + search, + searchFields, + rootSearchFields, + }); + let fields = rootSearchFields || []; + if (searchFields) { + fields = fields.concat(getExpectedFields(searchFields, typeOrTypes)); + } + expectResult(result, expect.objectContaining({ fields })); + } + // also test with no specified type/s const result = getQueryParams({ - mappings, registry, - type: typeOrTypes, + type: undefined, search, searchFields, rootSearchFields, }); let fields = rootSearchFields || []; if (searchFields) { - fields = fields.concat(getExpectedFields(searchFields, typeOrTypes)); + fields = fields.concat(getExpectedFields(searchFields, ALL_TYPES)); } expectResult(result, expect.objectContaining({ fields })); - } - // also test with no specified type/s - const result = getQueryParams({ - mappings, - registry, - type: undefined, - search, - searchFields, - rootSearchFields, + }; + + it('throws an error if a raw search field contains a "." character', () => { + expect(() => + getQueryParams({ + registry, + type: undefined, + search, + searchFields: undefined, + rootSearchFields: ['foo', 'bar.baz'], + }) + ).toThrowErrorMatchingInlineSnapshot( + `"rootSearchFields entry \\"bar.baz\\" is invalid: cannot contain \\".\\" character"` + ); }); - let fields = rootSearchFields || []; - if (searchFields) { - fields = fields.concat(getExpectedFields(searchFields, ALL_TYPES)); - } - expectResult(result, expect.objectContaining({ fields })); - }; - it('throws an error if a raw search field contains a "." character', () => { - expect(() => - getQueryParams({ - mappings, + it('includes lenient flag and all fields when `searchFields` and `rootSearchFields` are not specified', () => { + const result = getQueryParams({ registry, - type: undefined, search, searchFields: undefined, - rootSearchFields: ['foo', 'bar.baz'], - }) - ).toThrowErrorMatchingInlineSnapshot( - `"rootSearchFields entry \\"bar.baz\\" is invalid: cannot contain \\".\\" character"` - ); + rootSearchFields: undefined, + }); + expectResult(result, expect.objectContaining({ lenient: true, fields: ['*'] })); + }); + + it('includes specified search fields for appropriate type/s', () => { + test({ searchFields: ['title'] }); + }); + + it('supports boosting', () => { + test({ searchFields: ['title^3'] }); + }); + + it('supports multiple search fields', () => { + test({ searchFields: ['title, title.raw'] }); + }); + + it('includes specified raw search fields', () => { + test({ rootSearchFields: ['_id'] }); + }); + + it('supports multiple raw search fields', () => { + test({ rootSearchFields: ['_id', 'originId'] }); + }); + + it('supports search fields and raw search fields', () => { + test({ searchFields: ['title'], rootSearchFields: ['_id'] }); + }); }); - it('includes lenient flag and all fields when `searchFields` and `rootSearchFields` are not specified', () => { - const result = getQueryParams({ - mappings, + describe('`defaultSearchOperator` parameter', () => { + it('does not include default_operator when `defaultSearchOperator` is not specified', () => { + const result = getQueryParams({ + registry, + search, + defaultSearchOperator: undefined, + }); + expectResult( + result, + expect.not.objectContaining({ default_operator: expect.anything() }) + ); + }); + + it('includes specified default operator', () => { + const defaultSearchOperator = 'AND'; + const result = getQueryParams({ + registry, + search, + defaultSearchOperator, + }); + expectResult( + result, + expect.objectContaining({ default_operator: defaultSearchOperator }) + ); + }); + }); + }); + + describe('when using prefix search (query.bool.should)', () => { + const searchQuery = 'foo*'; + + const getQueryParamForSearch = ({ + search, + searchFields, + type, + }: { + search?: string; + searchFields?: string[]; + type?: string[]; + }) => + getQueryParams({ registry, search, - searchFields: undefined, - rootSearchFields: undefined, + searchFields, + type, }); - expectResult(result, expect.objectContaining({ lenient: true, fields: ['*'] })); - }); - it('includes specified search fields for appropriate type/s', () => { - test({ searchFields: ['title'] }); - }); + it('uses a `should` clause instead of `must`', () => { + const result = getQueryParamForSearch({ search: searchQuery, searchFields: ['title'] }); - it('supports boosting', () => { - test({ searchFields: ['title^3'] }); + expect(result.query.bool.must).toBeUndefined(); + expect(result.query.bool.should).toEqual(expect.any(Array)); + expect(result.query.bool.should.length).toBeGreaterThanOrEqual(1); + expect(result.query.bool.minimum_should_match).toBe(1); }); - - it('supports multiple search fields', () => { - test({ searchFields: ['title, title.raw'] }); + it('includes the `simple_query_string` in the `should` clauses', () => { + const result = getQueryParamForSearch({ search: searchQuery, searchFields: ['title'] }); + expect(result.query.bool.should[0]).toEqual({ + simple_query_string: expect.objectContaining({ + query: searchQuery, + }), + }); }); - it('includes specified raw search fields', () => { - test({ rootSearchFields: ['_id'] }); + it('adds a should clause for each `searchFields` / `type` tuple', () => { + const result = getQueryParamForSearch({ + search: searchQuery, + searchFields: ['title', 'desc'], + type: ['saved', 'pending'], + }); + const shouldClauses = result.query.bool.should; + + expect(shouldClauses.length).toBe(5); + + const mppClauses = shouldClauses.slice(1); + + expect( + mppClauses.map((clause: any) => Object.keys(clause.match_phrase_prefix)[0]) + ).toEqual(['saved.title', 'pending.title', 'saved.desc', 'pending.desc']); }); - it('supports multiple raw search fields', () => { - test({ rootSearchFields: ['_id', 'originId'] }); + it('uses all registered types when `type` is not provided', () => { + const result = getQueryParamForSearch({ + search: searchQuery, + searchFields: ['title'], + type: undefined, + }); + const shouldClauses = result.query.bool.should; + + expect(shouldClauses.length).toBe(5); + + const mppClauses = shouldClauses.slice(1); + + expect( + mppClauses.map((clause: any) => Object.keys(clause.match_phrase_prefix)[0]) + ).toEqual(['pending.title', 'saved.title', 'shared.title', 'global.title']); }); - it('supports search fields and raw search fields', () => { - test({ searchFields: ['title'], rootSearchFields: ['_id'] }); + it('removes the prefix search wildcard from the query', () => { + const result = getQueryParamForSearch({ + search: searchQuery, + searchFields: ['title'], + type: ['saved'], + }); + const shouldClauses = result.query.bool.should; + const mppClauses = shouldClauses.slice(1); + + expect(mppClauses[0].match_phrase_prefix['saved.title'].query).toEqual('foo'); }); - }); - describe('`defaultSearchOperator` parameter', () => { - it('does not include default_operator when `defaultSearchOperator` is not specified', () => { - const result = getQueryParams({ - mappings, - registry, - search, - defaultSearchOperator: undefined, + it("defaults to the type's default search field when `searchFields` is not specified", () => { + const result = getQueryParamForSearch({ + search: searchQuery, + searchFields: undefined, + type: ['saved', 'global'], }); - expectResult(result, expect.not.objectContaining({ default_operator: expect.anything() })); + const shouldClauses = result.query.bool.should; + + expect(shouldClauses.length).toBe(3); + + const mppClauses = shouldClauses.slice(1); + + expect( + mppClauses.map((clause: any) => Object.keys(clause.match_phrase_prefix)[0]) + ).toEqual(['saved.title', 'global.name']); }); - it('includes specified default operator', () => { - const defaultSearchOperator = 'AND'; - const result = getQueryParams({ - mappings, - registry, - search, - defaultSearchOperator, + it('supports boosting', () => { + const result = getQueryParamForSearch({ + search: searchQuery, + searchFields: ['title^3', 'description'], + type: ['saved'], }); - expectResult(result, expect.objectContaining({ default_operator: defaultSearchOperator })); + const shouldClauses = result.query.bool.should; + + expect(shouldClauses.length).toBe(3); + + const mppClauses = shouldClauses.slice(1); + + expect(mppClauses.map((clause: any) => clause.match_phrase_prefix)).toEqual([ + { 'saved.title': { query: 'foo', boost: 3 } }, + { 'saved.description': { query: 'foo', boost: 1 } }, + ]); }); }); }); @@ -532,7 +686,6 @@ describe('#getQueryParams', () => { it(`throws for ${type} when namespaces is an empty array`, () => { expect(() => getQueryParams({ - mappings, registry, namespaces: [], }) diff --git a/src/core/server/saved_objects/service/lib/search_dsl/query_params.ts b/src/core/server/saved_objects/service/lib/search_dsl/query_params.ts index 8d4fe13b9bede..cb58db171681a 100644 --- a/src/core/server/saved_objects/service/lib/search_dsl/query_params.ts +++ b/src/core/server/saved_objects/service/lib/search_dsl/query_params.ts @@ -20,7 +20,6 @@ import { esKuery } from '../../../es_query'; type KueryNode = any; -import { getRootPropertiesObjects, IndexMapping } from '../../../mappings'; import { ISavedObjectTypeRegistry } from '../../../saved_objects_type_registry'; import { ALL_NAMESPACES_STRING, DEFAULT_NAMESPACE_STRING } from '../utils'; @@ -28,22 +27,17 @@ import { ALL_NAMESPACES_STRING, DEFAULT_NAMESPACE_STRING } from '../utils'; * Gets the types based on the type. Uses mappings to support * null type (all types), a single type string or an array */ -function getTypes(mappings: IndexMapping, type?: string | string[]) { +function getTypes(registry: ISavedObjectTypeRegistry, type?: string | string[]) { if (!type) { - return Object.keys(getRootPropertiesObjects(mappings)); + return registry.getAllTypes().map((registeredType) => registeredType.name); } - - if (Array.isArray(type)) { - return type; - } - - return [type]; + return Array.isArray(type) ? type : [type]; } /** * Get the field params based on the types, searchFields, and rootSearchFields */ -function getFieldsForTypes( +function getSimpleQueryStringTypeFields( types: string[], searchFields: string[] = [], rootSearchFields: string[] = [] @@ -81,34 +75,47 @@ function getClauseForType( if (namespaces.length === 0) { throw new Error('cannot specify empty namespaces array'); } + const searchAcrossAllNamespaces = namespaces.includes(ALL_NAMESPACES_STRING); + if (registry.isMultiNamespace(type)) { + const typeFilterClause = { term: { type } }; + + const namespacesFilterClause = { + terms: { namespaces: [...namespaces, ALL_NAMESPACES_STRING] }, + }; + + const must = searchAcrossAllNamespaces + ? [typeFilterClause] + : [typeFilterClause, namespacesFilterClause]; + return { bool: { - must: [ - { term: { type } }, - { terms: { namespaces: [...namespaces, ALL_NAMESPACES_STRING] } }, - ], + must, must_not: [{ exists: { field: 'namespace' } }], }, }; } else if (registry.isSingleNamespace(type)) { const should: Array> = []; const eligibleNamespaces = namespaces.filter((x) => x !== DEFAULT_NAMESPACE_STRING); - if (eligibleNamespaces.length > 0) { + if (eligibleNamespaces.length > 0 && !searchAcrossAllNamespaces) { should.push({ terms: { namespace: eligibleNamespaces } }); } if (namespaces.includes(DEFAULT_NAMESPACE_STRING)) { should.push({ bool: { must_not: [{ exists: { field: 'namespace' } }] } }); } - if (should.length === 0) { - // This is indicitive of a bug, and not user error. - throw new Error('unhandled search condition: expected at least 1 `should` clause.'); - } + + const shouldClauseProps = + should.length > 0 + ? { + should, + minimum_should_match: 1, + } + : {}; + return { bool: { must: [{ term: { type } }], - should, - minimum_should_match: 1, + ...shouldClauseProps, must_not: [{ exists: { field: 'namespaces' } }], }, }; @@ -130,7 +137,6 @@ export interface HasReferenceQueryParams { export type SearchOperator = 'AND' | 'OR'; interface QueryParams { - mappings: IndexMapping; registry: ISavedObjectTypeRegistry; namespaces?: string[]; type?: string | string[]; @@ -188,11 +194,14 @@ export function getClauseForReference(reference: HasReferenceQueryParams) { }; } +// A de-duplicated set of namespaces makes for a more effecient query. +const uniqNamespaces = (namespacesToNormalize?: string[]) => + namespacesToNormalize ? Array.from(new Set(namespacesToNormalize)) : undefined; + /** * Get the "query" related keys for the search body */ export function getQueryParams({ - mappings, registry, namespaces, type, @@ -206,7 +215,7 @@ export function getQueryParams({ kueryNode, }: QueryParams) { const types = getTypes( - mappings, + registry, typeToNamespacesMap ? Array.from(typeToNamespacesMap.keys()) : type ); @@ -214,35 +223,17 @@ export function getQueryParams({ hasReference = [hasReference]; } - // A de-duplicated set of namespaces makes for a more effecient query. - // - // Additonally, we treat the `*` namespace as the `default` namespace. - // In the Default Distribution, the `*` is automatically expanded to include all available namespaces. - // However, the OSS distribution (and certain configurations of the Default Distribution) can allow the `*` - // to pass through to the SO Repository, and eventually to this module. When this happens, we translate to `default`, - // since that is consistent with how a single-namespace search behaves in the OSS distribution. Leaving the wildcard in place - // would result in no results being returned, as the wildcard is treated as a literal, and not _actually_ as a wildcard. - // We had a good discussion around the tradeoffs here: https://github.com/elastic/kibana/pull/67644#discussion_r441055716 - const normalizeNamespaces = (namespacesToNormalize?: string[]) => - namespacesToNormalize - ? Array.from( - new Set(namespacesToNormalize.map((x) => (x === '*' ? DEFAULT_NAMESPACE_STRING : x))) - ) - : undefined; - const bool: any = { filter: [ ...(kueryNode != null ? [esKuery.toElasticsearchQuery(kueryNode)] : []), - ...(hasReference && hasReference.length - ? [getReferencesFilter(hasReference, hasReferenceOperator)] - : []), + ...(hasReference?.length ? [getReferencesFilter(hasReference, hasReferenceOperator)] : []), { bool: { should: types.map((shouldType) => { - const normalizedNamespaces = normalizeNamespaces( + const deduplicatedNamespaces = uniqNamespaces( typeToNamespacesMap ? typeToNamespacesMap.get(shouldType) : namespaces ); - return getClauseForType(registry, normalizedNamespaces, shouldType); + return getClauseForType(registry, deduplicatedNamespaces, shouldType); }), minimum_should_match: 1, }, @@ -251,16 +242,133 @@ export function getQueryParams({ }; if (search) { - bool.must = [ - { - simple_query_string: { - query: search, - ...getFieldsForTypes(types, searchFields, rootSearchFields), - ...(defaultSearchOperator ? { default_operator: defaultSearchOperator } : {}), - }, - }, - ]; + const useMatchPhrasePrefix = shouldUseMatchPhrasePrefix(search); + const simpleQueryStringClause = getSimpleQueryStringClause({ + search, + types, + searchFields, + rootSearchFields, + defaultSearchOperator, + }); + + if (useMatchPhrasePrefix) { + bool.should = [ + simpleQueryStringClause, + ...getMatchPhrasePrefixClauses({ search, searchFields, types, registry }), + ]; + bool.minimum_should_match = 1; + } else { + bool.must = [simpleQueryStringClause]; + } } return { query: { bool } }; } + +// we only want to add match_phrase_prefix clauses +// if the search is a prefix search +const shouldUseMatchPhrasePrefix = (search: string): boolean => { + return search.trim().endsWith('*'); +}; + +const getMatchPhrasePrefixClauses = ({ + search, + searchFields, + registry, + types, +}: { + search: string; + searchFields?: string[]; + types: string[]; + registry: ISavedObjectTypeRegistry; +}) => { + // need to remove the prefix search operator + const query = search.replace(/[*]$/, ''); + const mppFields = getMatchPhrasePrefixFields({ searchFields, types, registry }); + return mppFields.map(({ field, boost }) => { + return { + match_phrase_prefix: { + [field]: { + query, + boost, + }, + }, + }; + }); +}; + +interface FieldWithBoost { + field: string; + boost?: number; +} + +const getMatchPhrasePrefixFields = ({ + searchFields = [], + types, + registry, +}: { + searchFields?: string[]; + types: string[]; + registry: ISavedObjectTypeRegistry; +}): FieldWithBoost[] => { + const output: FieldWithBoost[] = []; + + searchFields = searchFields.filter((field) => field !== '*'); + let fields: string[]; + if (searchFields.length === 0) { + fields = types.reduce((typeFields, type) => { + const defaultSearchField = registry.getType(type)?.management?.defaultSearchField; + if (defaultSearchField) { + return [...typeFields, `${type}.${defaultSearchField}`]; + } + return typeFields; + }, [] as string[]); + } else { + fields = []; + for (const field of searchFields) { + fields = fields.concat(types.map((type) => `${type}.${field}`)); + } + } + + fields.forEach((rawField) => { + const [field, rawBoost] = rawField.split('^'); + let boost: number = 1; + if (rawBoost) { + try { + boost = parseInt(rawBoost, 10); + } catch (e) { + boost = 1; + } + } + if (isNaN(boost)) { + boost = 1; + } + output.push({ + field, + boost, + }); + }); + return output; +}; + +const getSimpleQueryStringClause = ({ + search, + types, + searchFields, + rootSearchFields, + defaultSearchOperator, +}: { + search: string; + types: string[]; + searchFields?: string[]; + rootSearchFields?: string[]; + defaultSearchOperator?: SearchOperator; +}) => { + return { + simple_query_string: { + query: search, + ...getSimpleQueryStringTypeFields(types, searchFields, rootSearchFields), + ...(defaultSearchOperator ? { default_operator: defaultSearchOperator } : {}), + }, + }; +}; diff --git a/src/core/server/saved_objects/service/lib/search_dsl/search_dsl.test.ts b/src/core/server/saved_objects/service/lib/search_dsl/search_dsl.test.ts index a9f26f71a3f2b..3522ab9ef1736 100644 --- a/src/core/server/saved_objects/service/lib/search_dsl/search_dsl.test.ts +++ b/src/core/server/saved_objects/service/lib/search_dsl/search_dsl.test.ts @@ -76,7 +76,6 @@ describe('getSearchDsl', () => { getSearchDsl(mappings, registry, opts); expect(getQueryParams).toHaveBeenCalledTimes(1); expect(getQueryParams).toHaveBeenCalledWith({ - mappings, registry, namespaces: opts.namespaces, type: opts.type, diff --git a/src/core/server/saved_objects/service/lib/search_dsl/search_dsl.ts b/src/core/server/saved_objects/service/lib/search_dsl/search_dsl.ts index d5da82e5617be..bddecc4d7f649 100644 --- a/src/core/server/saved_objects/service/lib/search_dsl/search_dsl.ts +++ b/src/core/server/saved_objects/service/lib/search_dsl/search_dsl.ts @@ -71,7 +71,6 @@ export function getSearchDsl( return { ...getQueryParams({ - mappings, registry, namespaces, type, diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index 52500673f7f31..88d7fecbcf502 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -480,6 +480,8 @@ export interface CoreSetup HttpServerInfo; } +// @public (undocumented) +export interface I18nServiceSetup { + getLocale(): string; + getTranslationFiles(): string[]; +} + // @public export type IBasePath = Pick; diff --git a/src/core/server/server.test.mocks.ts b/src/core/server/server.test.mocks.ts index fe299c6d11675..d2d6990dc5451 100644 --- a/src/core/server/server.test.mocks.ts +++ b/src/core/server/server.test.mocks.ts @@ -100,3 +100,9 @@ export const mockLoggingService = loggingServiceMock.create(); jest.doMock('./logging/logging_service', () => ({ LoggingService: jest.fn(() => mockLoggingService), })); + +import { i18nServiceMock } from './i18n/i18n_service.mock'; +export const mockI18nService = i18nServiceMock.create(); +jest.doMock('./i18n/i18n_service', () => ({ + I18nService: jest.fn(() => mockI18nService), +})); diff --git a/src/core/server/server.test.ts b/src/core/server/server.test.ts index 78703ceeec7ae..0c7ebbcb527ec 100644 --- a/src/core/server/server.test.ts +++ b/src/core/server/server.test.ts @@ -31,6 +31,7 @@ import { mockMetricsService, mockStatusService, mockLoggingService, + mockI18nService, } from './server.test.mocks'; import { BehaviorSubject } from 'rxjs'; @@ -49,6 +50,7 @@ beforeEach(() => { mockConfigService.atPath.mockReturnValue(new BehaviorSubject({ autoListen: true })); mockPluginsService.discover.mockResolvedValue({ pluginTree: { asOpaqueIds: new Map(), asNames: new Map() }, + pluginPaths: [], uiPlugins: { internal: new Map(), public: new Map(), browserConfigs: new Map() }, }); }); @@ -70,6 +72,7 @@ test('sets up services on "setup"', async () => { expect(mockMetricsService.setup).not.toHaveBeenCalled(); expect(mockStatusService.setup).not.toHaveBeenCalled(); expect(mockLoggingService.setup).not.toHaveBeenCalled(); + expect(mockI18nService.setup).not.toHaveBeenCalled(); await server.setup(); @@ -83,6 +86,7 @@ test('sets up services on "setup"', async () => { expect(mockMetricsService.setup).toHaveBeenCalledTimes(1); expect(mockStatusService.setup).toHaveBeenCalledTimes(1); expect(mockLoggingService.setup).toHaveBeenCalledTimes(1); + expect(mockI18nService.setup).toHaveBeenCalledTimes(1); }); test('injects legacy dependency to context#setup()', async () => { @@ -96,6 +100,7 @@ test('injects legacy dependency to context#setup()', async () => { ]); mockPluginsService.discover.mockResolvedValue({ pluginTree: { asOpaqueIds: pluginDependencies, asNames: new Map() }, + pluginPaths: [], uiPlugins: { internal: new Map(), public: new Map(), browserConfigs: new Map() }, }); @@ -185,6 +190,7 @@ test(`doesn't setup core services if config validation fails`, async () => { expect(mockMetricsService.setup).not.toHaveBeenCalled(); expect(mockStatusService.setup).not.toHaveBeenCalled(); expect(mockLoggingService.setup).not.toHaveBeenCalled(); + expect(mockI18nService.setup).not.toHaveBeenCalled(); }); test(`doesn't setup core services if legacy config validation fails`, async () => { @@ -207,6 +213,7 @@ test(`doesn't setup core services if legacy config validation fails`, async () = expect(mockMetricsService.setup).not.toHaveBeenCalled(); expect(mockStatusService.setup).not.toHaveBeenCalled(); expect(mockLoggingService.setup).not.toHaveBeenCalled(); + expect(mockI18nService.setup).not.toHaveBeenCalled(); }); test(`doesn't validate config if env.isDevClusterMaster is true`, async () => { diff --git a/src/core/server/server.ts b/src/core/server/server.ts index eaa03d11cab98..55ed88e55a9f5 100644 --- a/src/core/server/server.ts +++ b/src/core/server/server.ts @@ -16,11 +16,13 @@ * specific language governing permissions and limitations * under the License. */ + import apm from 'elastic-apm-node'; import { config as pathConfig } from '@kbn/utils'; import { mapToObject } from '@kbn/std'; import { ConfigService, Env, RawConfigurationProvider, coreDeprecationProvider } from './config'; import { CoreApp } from './core_app'; +import { I18nService } from './i18n'; import { ElasticsearchService } from './elasticsearch'; import { HttpService } from './http'; import { HttpResourcesService } from './http_resources'; @@ -29,10 +31,11 @@ import { LegacyService, ensureValidConfiguration } from './legacy'; import { Logger, LoggerFactory, LoggingService, ILoggingSystem } from './logging'; import { UiSettingsService } from './ui_settings'; import { PluginsService, config as pluginsConfig } from './plugins'; -import { SavedObjectsService } from '../server/saved_objects'; +import { SavedObjectsService } from './saved_objects'; import { MetricsService, opsConfig } from './metrics'; import { CapabilitiesService } from './capabilities'; import { EnvironmentService, config as pidConfig } from './environment'; +// do not try to shorten the import to `./status`, it will break server test mocking import { StatusService } from './status/status_service'; import { config as cspConfig } from './csp'; @@ -44,6 +47,7 @@ import { config as kibanaConfig } from './kibana_config'; import { savedObjectsConfig, savedObjectsMigrationConfig } from './saved_objects'; import { config as uiSettingsConfig } from './ui_settings'; import { config as statusConfig } from './status'; +import { config as i18nConfig } from './i18n'; import { ContextService } from './context'; import { RequestHandlerContext } from '.'; import { InternalCoreSetup, InternalCoreStart, ServiceConfigDescriptor } from './internal_types'; @@ -72,6 +76,7 @@ export class Server { private readonly logging: LoggingService; private readonly coreApp: CoreApp; private readonly coreUsageData: CoreUsageDataService; + private readonly i18n: I18nService; #pluginsInitialized?: boolean; private coreStart?: InternalCoreStart; @@ -103,6 +108,7 @@ export class Server { this.httpResources = new HttpResourcesService(core); this.logging = new LoggingService(core); this.coreUsageData = new CoreUsageDataService(core); + this.i18n = new I18nService(core); } public async setup() { @@ -112,7 +118,7 @@ export class Server { const environmentSetup = await this.environment.setup(); // Discover any plugins before continuing. This allows other systems to utilize the plugin dependency graph. - const { pluginTree, uiPlugins } = await this.plugins.discover({ + const { pluginTree, pluginPaths, uiPlugins } = await this.plugins.discover({ environment: environmentSetup, }); const legacyConfigSetup = await this.legacy.setupLegacyConfig(); @@ -125,6 +131,9 @@ export class Server { await ensureValidConfiguration(this.configService, legacyConfigSetup); } + // setup i18n prior to any other service, to have translations ready + const i18nServiceSetup = await this.i18n.setup({ pluginPaths }); + const contextServiceSetup = this.context.setup({ // We inject a fake "legacy plugin" with dependencies on every plugin so that legacy plugins: // 1) Can access context from any KP plugin @@ -190,6 +199,7 @@ export class Server { elasticsearch: elasticsearchServiceSetup, environment: environmentSetup, http: httpSetup, + i18n: i18nServiceSetup, savedObjects: savedObjectsSetup, status: statusSetup, uiSettings: uiSettingsSetup, @@ -302,6 +312,7 @@ export class Server { opsConfig, statusConfig, pidConfig, + i18nConfig, ]; this.configService.addDeprecationProvider(rootConfigPath, coreDeprecationProvider); diff --git a/src/core/server/status/status_service.mock.ts b/src/core/server/status/status_service.mock.ts index 0ee2d03229a78..42892ddbb490c 100644 --- a/src/core/server/status/status_service.mock.ts +++ b/src/core/server/status/status_service.mock.ts @@ -17,7 +17,7 @@ * under the License. */ import type { PublicMethodsOf } from '@kbn/utility-types'; -import { StatusService } from './status_service'; +import type { StatusService } from './status_service'; import { InternalStatusServiceSetup, StatusServiceSetup, diff --git a/src/core/server/ui_settings/ui_settings_service.mock.ts b/src/core/server/ui_settings/ui_settings_service.mock.ts index b1ed0dd188cde..818e9b43889e3 100644 --- a/src/core/server/ui_settings/ui_settings_service.mock.ts +++ b/src/core/server/ui_settings/ui_settings_service.mock.ts @@ -22,7 +22,7 @@ import { InternalUiSettingsServiceSetup, InternalUiSettingsServiceStart, } from './types'; -import { UiSettingsService } from './ui_settings_service'; +import type { UiSettingsService } from './ui_settings_service'; const createClientMock = () => { const mocked: jest.Mocked = { 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 0c17a3a614d60..f4e24e3a517c3 100644 --- a/src/core/server/ui_settings/ui_settings_service.test.ts +++ b/src/core/server/ui_settings/ui_settings_service.test.ts @@ -89,6 +89,20 @@ describe('uiSettings', () => { describe('#start', () => { describe('validation', () => { + it('throws if validation schema is not provided', async () => { + const { register } = await service.setup(setupDeps); + register({ + // @ts-expect-error schema is required key + custom: { + value: 42, + }, + }); + + await expect(service.start()).rejects.toMatchInlineSnapshot( + `[Error: Validation schema is not provided for [custom] UI Setting]` + ); + }); + it('validates registered definitions', async () => { const { register } = await service.setup(setupDeps); register({ @@ -125,6 +139,21 @@ describe('uiSettings', () => { `[Error: [ui settings overrides [custom]]: expected value of type [string] but got [number]]` ); }); + + it('do not throw on unknown overrides', async () => { + const coreContext = mockCoreContext.create(); + coreContext.configService.atPath.mockReturnValueOnce( + new BehaviorSubject({ + overrides: { + custom: 42, + }, + }) + ); + const customizedService = new UiSettingsService(coreContext); + await customizedService.setup(setupDeps); + + await customizedService.start(); + }); }); describe('#asScopedToClient', () => { diff --git a/src/core/server/ui_settings/ui_settings_service.ts b/src/core/server/ui_settings/ui_settings_service.ts index 25062490f5b6b..4f757d18ea7da 100644 --- a/src/core/server/ui_settings/ui_settings_service.ts +++ b/src/core/server/ui_settings/ui_settings_service.ts @@ -109,15 +109,17 @@ export class UiSettingsService private validatesDefinitions() { for (const [key, definition] of this.uiSettingsDefaults) { - if (definition.schema) { - definition.schema.validate(definition.value, {}, `ui settings defaults [${key}]`); + if (!definition.schema) { + throw new Error(`Validation schema is not provided for [${key}] UI Setting`); } + definition.schema.validate(definition.value, {}, `ui settings defaults [${key}]`); } } private validatesOverrides() { for (const [key, value] of Object.entries(this.overrides)) { const definition = this.uiSettingsDefaults.get(key); + // overrides might contain UiSettings for a disabled plugin if (definition?.schema) { definition.schema.validate(value, {}, `ui settings overrides [${key}]`); } diff --git a/src/dev/build/tasks/bin/scripts/kibana b/src/dev/build/tasks/bin/scripts/kibana index c606436c7b83f..a4fc5385500b5 100755 --- a/src/dev/build/tasks/bin/scripts/kibana +++ b/src/dev/build/tasks/bin/scripts/kibana @@ -14,7 +14,7 @@ while [ -h "$SCRIPT" ] ; do done DIR="$(dirname "${SCRIPT}")/.." -CONFIG_DIR=${KIBANA_PATH_CONF:-"$DIR/config"} +CONFIG_DIR=${KBN_PATH_CONF:-"$DIR/config"} NODE="${DIR}/node/bin/node" test -x "$NODE" if [ ! -x "$NODE" ]; then diff --git a/src/dev/build/tasks/os_packages/package_scripts/post_install.sh b/src/dev/build/tasks/os_packages/package_scripts/post_install.sh index 939226b565f79..728278dae746b 100644 --- a/src/dev/build/tasks/os_packages/package_scripts/post_install.sh +++ b/src/dev/build/tasks/os_packages/package_scripts/post_install.sh @@ -1,6 +1,11 @@ #!/bin/sh set -e +# source the default env file +if [ -f "<%= envFile %>" ]; then + . "<%= envFile %>" +fi + export KBN_PATH_CONF=${KBN_PATH_CONF:-<%= configDir %>} set_chmod() { @@ -71,4 +76,12 @@ if [ "$IS_UPGRADE" = "true" ]; then if command -v systemctl >/dev/null; then systemctl daemon-reload fi + + if [ "$RESTART_ON_UPGRADE" = "true" ]; then + echo -n "Restarting kibana service..." + if command -v systemctl >/dev/null; then + systemctl restart kibana.service || true + fi + echo " OK" + fi fi diff --git a/src/dev/build/tasks/os_packages/run_fpm.ts b/src/dev/build/tasks/os_packages/run_fpm.ts index e5de760ea11d0..f16eaea1daa2f 100644 --- a/src/dev/build/tasks/os_packages/run_fpm.ts +++ b/src/dev/build/tasks/os_packages/run_fpm.ts @@ -111,7 +111,8 @@ export async function runFpm( `dataDir=/var/lib/kibana`, '--template-value', `logDir=/var/log/kibana`, - + '--template-value', + `envFile=/etc/default/kibana`, // config and data directories are copied to /usr/share and /var/lib // below, so exclude them from the main package source located in // /usr/share/kibana/config. PATHS MUST BE RELATIVE, so drop the leading slash diff --git a/src/dev/build/tasks/os_packages/service_templates/systemd/etc/default/kibana b/src/dev/build/tasks/os_packages/service_templates/systemd/etc/default/kibana index 9c9f58ded350b..7d0c955964ae6 100644 --- a/src/dev/build/tasks/os_packages/service_templates/systemd/etc/default/kibana +++ b/src/dev/build/tasks/os_packages/service_templates/systemd/etc/default/kibana @@ -1,13 +1,5 @@ user="kibana" group="kibana" -chroot="/" -chdir="/" -nice="" - - -# If this is set to 1, then when `stop` is called, if the process has -# not exited within a reasonable time, SIGKILL will be sent next. -# The default behavior is to simply log a message "program stop failed; still running" -KILL_ON_STOP_TIMEOUT=0 KBN_PATH_CONF="/etc/kibana" +RESTART_ON_UPGRADE="true" diff --git a/src/dev/build/tasks/os_packages/service_templates/systemd/etc/systemd/system/kibana.service b/src/dev/build/tasks/os_packages/service_templates/systemd/etc/systemd/system/kibana.service index 05724db8799f3..7a1508d91b213 100644 --- a/src/dev/build/tasks/os_packages/service_templates/systemd/etc/systemd/system/kibana.service +++ b/src/dev/build/tasks/os_packages/service_templates/systemd/etc/systemd/system/kibana.service @@ -15,7 +15,7 @@ Environment=KBN_PATH_CONF=/etc/kibana EnvironmentFile=-/etc/default/kibana EnvironmentFile=-/etc/sysconfig/kibana -ExecStart=/usr/share/kibana/bin/kibana --logging.dest="/var/log/kibana/kibana.log" +ExecStart=/usr/share/kibana/bin/kibana --logging.dest="/var/log/kibana/kibana.log" --pid.file="/run/kibana/kibana.pid" Restart=on-failure RestartSec=3 diff --git a/src/dev/build/tasks/os_packages/service_templates/systemd/usr/lib/tmpfiles.d/kibana.conf b/src/dev/build/tasks/os_packages/service_templates/systemd/usr/lib/tmpfiles.d/kibana.conf index b5422df52fe11..fe033e30fbf58 100644 --- a/src/dev/build/tasks/os_packages/service_templates/systemd/usr/lib/tmpfiles.d/kibana.conf +++ b/src/dev/build/tasks/os_packages/service_templates/systemd/usr/lib/tmpfiles.d/kibana.conf @@ -1 +1 @@ -d /var/run/kibana 0755 kibana kibana - - \ No newline at end of file +d /run/kibana 0755 kibana kibana - - \ No newline at end of file diff --git a/src/legacy/server/config/schema.js b/src/legacy/server/config/schema.js index e1f03b8a08847..39df3990ff2ff 100644 --- a/src/legacy/server/config/schema.js +++ b/src/legacy/server/config/schema.js @@ -128,21 +128,12 @@ export default () => cGroupOverrides: HANDLED_IN_NEW_PLATFORM, }).default(), - // still used by the legacy i18n mixin - plugins: Joi.object({ - paths: Joi.array().items(Joi.string()).default([]), - scanDirs: Joi.array().items(Joi.string()).default([]), - initialize: Joi.boolean().default(true), - }).default(), - + plugins: HANDLED_IN_NEW_PLATFORM, path: HANDLED_IN_NEW_PLATFORM, stats: HANDLED_IN_NEW_PLATFORM, status: HANDLED_IN_NEW_PLATFORM, map: HANDLED_IN_NEW_PLATFORM, - - i18n: Joi.object({ - locale: Joi.string().default('en'), - }).default(), + i18n: HANDLED_IN_NEW_PLATFORM, // temporarily moved here from the (now deleted) kibana legacy plugin kibana: Joi.object({ diff --git a/src/legacy/server/i18n/get_kibana_translation_paths.test.ts b/src/legacy/server/i18n/get_kibana_translation_paths.test.ts deleted file mode 100644 index 0f202c4d433c0..0000000000000 --- a/src/legacy/server/i18n/get_kibana_translation_paths.test.ts +++ /dev/null @@ -1,58 +0,0 @@ -/* - * 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_RC } from './constants'; -import { fromRoot } from '../../../core/server/utils'; - -jest.mock('./get_translation_paths', () => ({ getTranslationPaths: jest.fn() })); -import { getKibanaTranslationPaths } from './get_kibana_translation_paths'; -import { getTranslationPaths as mockGetTranslationPaths } from './get_translation_paths'; - -describe('getKibanaTranslationPaths', () => { - const mockConfig = { get: jest.fn() }; - - beforeEach(() => { - jest.resetAllMocks(); - }); - - it('calls getTranslationPaths against kibana root and kibana-extra', async () => { - mockConfig.get.mockReturnValue([]); - await getKibanaTranslationPaths(mockConfig); - expect(mockGetTranslationPaths).toHaveBeenNthCalledWith(1, { - cwd: fromRoot('.'), - glob: `*/${I18N_RC}`, - }); - - expect(mockGetTranslationPaths).toHaveBeenNthCalledWith(2, { - cwd: fromRoot('../kibana-extra'), - glob: `*/${I18N_RC}`, - }); - }); - - it('calls getTranslationPaths for each config returned in plugin.paths and plugins.scanDirs', async () => { - mockConfig.get.mockReturnValueOnce(['a', 'b']).mockReturnValueOnce(['c']); - await getKibanaTranslationPaths(mockConfig); - expect(mockConfig.get).toHaveBeenNthCalledWith(1, 'plugins.paths'); - expect(mockConfig.get).toHaveBeenNthCalledWith(2, 'plugins.scanDirs'); - - expect(mockGetTranslationPaths).toHaveBeenNthCalledWith(2, { cwd: 'a', glob: I18N_RC }); - expect(mockGetTranslationPaths).toHaveBeenNthCalledWith(3, { cwd: 'b', glob: I18N_RC }); - expect(mockGetTranslationPaths).toHaveBeenNthCalledWith(4, { cwd: 'c', glob: `*/${I18N_RC}` }); - }); -}); diff --git a/src/legacy/server/i18n/i18n_mixin.ts b/src/legacy/server/i18n/i18n_mixin.ts deleted file mode 100644 index 0b3879073c164..0000000000000 --- a/src/legacy/server/i18n/i18n_mixin.ts +++ /dev/null @@ -1,63 +0,0 @@ -/* - * 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, i18nLoader } from '@kbn/i18n'; -import { basename } from 'path'; -import { Server } from '@hapi/hapi'; -import type { UsageCollectionSetup } from '../../../plugins/usage_collection/server'; -import { getKibanaTranslationPaths } from './get_kibana_translation_paths'; -import KbnServer, { KibanaConfig } from '../kbn_server'; -import { registerLocalizationUsageCollector } from './localization'; - -export async function i18nMixin( - kbnServer: KbnServer, - server: Server, - config: Pick -) { - const locale = config.get('i18n.locale') as string; - - const translationPaths = await getKibanaTranslationPaths(config); - - const currentTranslationPaths = ([] as string[]) - .concat(...translationPaths) - .filter((translationPath) => basename(translationPath, '.json') === locale); - i18nLoader.registerTranslationFiles(currentTranslationPaths); - - const translations = await i18nLoader.getTranslationsByLocale(locale); - i18n.init( - Object.freeze({ - locale, - ...translations, - }) - ); - - const getTranslationsFilePaths = () => currentTranslationPaths; - - server.decorate('server', 'getTranslationsFilePaths', getTranslationsFilePaths); - - if (kbnServer.newPlatform.setup.plugins.usageCollection) { - const { usageCollection } = kbnServer.newPlatform.setup.plugins as { - usageCollection: UsageCollectionSetup; - }; - registerLocalizationUsageCollector(usageCollection, { - getLocale: () => config.get('i18n.locale') as string, - getTranslationsFilePaths, - }); - } -} diff --git a/src/legacy/server/kbn_server.js b/src/legacy/server/kbn_server.js index e29563a7c6266..013da35d2acb7 100644 --- a/src/legacy/server/kbn_server.js +++ b/src/legacy/server/kbn_server.js @@ -31,7 +31,6 @@ import warningsMixin from './warnings'; import configCompleteMixin from './config/complete'; import { optimizeMixin } from '../../optimize'; import { uiMixin } from '../ui'; -import { i18nMixin } from './i18n'; /** * @typedef {import('./kbn_server').KibanaConfig} KibanaConfig @@ -82,9 +81,6 @@ export default class KbnServer { loggingMixin, warningsMixin, - // scan translations dirs, register locale files and initialize i18n engine. - i18nMixin, - // tell the config we are done loading plugins configCompleteMixin, diff --git a/src/plugins/data/common/constants.ts b/src/plugins/data/common/constants.ts index 43120583bd3a4..a70c5eedbf847 100644 --- a/src/plugins/data/common/constants.ts +++ b/src/plugins/data/common/constants.ts @@ -49,4 +49,5 @@ export const UI_SETTINGS = { INDEXPATTERN_PLACEHOLDER: 'indexPattern:placeholder', FILTERS_PINNED_BY_DEFAULT: 'filters:pinnedByDefault', FILTERS_EDITOR_SUGGEST_VALUES: 'filterEditor:suggestValues', + AUTOCOMPLETE_USE_TIMERANGE: 'autocomplete:useTimeRange', } as const; diff --git a/src/plugins/data/common/index_patterns/index_patterns/index_pattern.test.ts b/src/plugins/data/common/index_patterns/index_patterns/index_pattern.test.ts index 9085ae07bbe3e..145901509d1c5 100644 --- a/src/plugins/data/common/index_patterns/index_patterns/index_pattern.test.ts +++ b/src/plugins/data/common/index_patterns/index_patterns/index_pattern.test.ts @@ -196,6 +196,24 @@ describe('IndexPattern', () => { }); }); + describe('getFormatterForField', () => { + test('should return the default one for empty objects', () => { + indexPattern.setFieldFormat('scriptedFieldWithEmptyFormatter', {}); + expect( + indexPattern.getFormatterForField({ + name: 'scriptedFieldWithEmptyFormatter', + type: 'number', + esTypes: ['long'], + }) + ).toEqual( + expect.objectContaining({ + convert: expect.any(Function), + getConverterFor: expect.any(Function), + }) + ); + }); + }); + describe('toSpec', () => { test('should match snapshot', () => { const formatter = { diff --git a/src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts b/src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts index a0f27078543a9..4508d7b1d9082 100644 --- a/src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts +++ b/src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts @@ -291,15 +291,15 @@ export class IndexPattern implements IIndexPattern { getFormatterForField( field: IndexPatternField | IndexPatternField['spec'] | IFieldType ): FieldFormat { - const formatSpec = this.fieldFormatMap[field.name]; - if (formatSpec) { - return this.fieldFormats.getInstance(formatSpec.id, formatSpec.params); - } else { - return this.fieldFormats.getDefaultInstance( - field.type as KBN_FIELD_TYPES, - field.esTypes as ES_FIELD_TYPES[] - ); + const fieldFormat = this.getFormatterForFieldNoDefault(field.name); + if (fieldFormat) { + return fieldFormat; } + + return this.fieldFormats.getDefaultInstance( + field.type as KBN_FIELD_TYPES, + field.esTypes as ES_FIELD_TYPES[] + ); } /** @@ -308,7 +308,7 @@ export class IndexPattern implements IIndexPattern { */ getFormatterForFieldNoDefault(fieldname: string) { const formatSpec = this.fieldFormatMap[fieldname]; - if (formatSpec) { + if (formatSpec?.id) { return this.fieldFormats.getInstance(formatSpec.id, formatSpec.params); } } diff --git a/src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts b/src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts index aae9b89cdc61f..19c6e9c7b8a7a 100644 --- a/src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts +++ b/src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts @@ -98,11 +98,12 @@ export class IndexPatternsService { * Refresh cache of index pattern ids and titles */ private async refreshSavedObjectsCache() { - this.savedObjectsCache = await this.savedObjectsClient.find({ + const so = await this.savedObjectsClient.find({ type: 'index-pattern', fields: ['title'], perPage: 10000, }); + this.savedObjectsCache = so; } /** @@ -215,13 +216,13 @@ export class IndexPatternsService { * Get field list by providing { pattern } * @param options */ - getFieldsForWildcard = async (options: GetFieldsOptions = {}) => { + getFieldsForWildcard = async (options: GetFieldsOptions) => { const metaFields = await this.config.get(UI_SETTINGS.META_FIELDS); return this.apiClient.getFieldsForWildcard({ pattern: options.pattern, metaFields, type: options.type, - params: options.params || {}, + rollupIndex: options.rollupIndex, }); }; @@ -231,13 +232,13 @@ export class IndexPatternsService { */ getFieldsForIndexPattern = async ( indexPattern: IndexPattern | IndexPatternSpec, - options: GetFieldsOptions = {} + options?: GetFieldsOptions ) => this.getFieldsForWildcard({ - pattern: indexPattern.title as string, - ...options, type: indexPattern.type, - params: indexPattern.typeMeta && indexPattern.typeMeta.params, + rollupIndex: indexPattern?.typeMeta?.params?.rollup_index, + ...options, + pattern: indexPattern.title as string, }); /** @@ -374,10 +375,10 @@ export class IndexPatternsService { try { spec.fields = isFieldRefreshRequired ? await this.refreshFieldSpecMap(spec.fields || {}, id, spec.title as string, { - pattern: title, + pattern: title as string, metaFields: await this.config.get(UI_SETTINGS.META_FIELDS), type, - params: typeMeta && typeMeta.params, + rollupIndex: typeMeta?.params?.rollupIndex, }) : spec.fields; } catch (err) { diff --git a/src/plugins/data/common/index_patterns/types.ts b/src/plugins/data/common/index_patterns/types.ts index 3387bc3b3c19e..b381cc0963333 100644 --- a/src/plugins/data/common/index_patterns/types.ts +++ b/src/plugins/data/common/index_patterns/types.ts @@ -86,15 +86,22 @@ export interface SavedObjectsClientCommon { } export interface GetFieldsOptions { - pattern?: string; + pattern: string; type?: string; - params?: any; lookBack?: boolean; metaFields?: string[]; + rollupIndex?: string; +} + +export interface GetFieldsOptionsTimePattern { + pattern: string; + metaFields: string[]; + lookBack: number; + interval: string; } export interface IIndexPatternsApiClient { - getFieldsForTimePattern: (options: GetFieldsOptions) => Promise; + getFieldsForTimePattern: (options: GetFieldsOptionsTimePattern) => Promise; getFieldsForWildcard: (options: GetFieldsOptions) => Promise; } diff --git a/src/plugins/data/common/search/aggs/agg_config.test.ts b/src/plugins/data/common/search/aggs/agg_config.test.ts index f6fcc29805dc4..9bb47f5cb3575 100644 --- a/src/plugins/data/common/search/aggs/agg_config.test.ts +++ b/src/plugins/data/common/search/aggs/agg_config.test.ts @@ -680,16 +680,6 @@ describe('AggConfig', () => { const json = aggConfig.toExpressionAst()?.arguments.json; expect(json).toEqual([JSON.stringify(configStates.params.json)]); }); - - it(`returns undefined if an expressionName doesn't exist on the agg type`, () => { - const ac = new AggConfigs(indexPattern, [], { typesRegistry }); - const configStates = { - type: 'unknown type', - params: {}, - }; - const aggConfig = ac.createAggConfig(configStates); - expect(aggConfig.toExpressionAst()).toBe(undefined); - }); }); describe('#makeLabel', () => { diff --git a/src/plugins/data/common/search/aggs/agg_configs.test.ts b/src/plugins/data/common/search/aggs/agg_configs.test.ts index 803ccc70b98a7..d1c8e29a03cc7 100644 --- a/src/plugins/data/common/search/aggs/agg_configs.test.ts +++ b/src/plugins/data/common/search/aggs/agg_configs.test.ts @@ -150,6 +150,27 @@ describe('AggConfigs', () => { ); expect(ac.aggs).toHaveLength(1); }); + + it(`throws if trying to add an agg which doesn't have a type in the registry`, () => { + const configStates = [ + { + enabled: true, + type: 'histogram', + params: {}, + }, + ]; + + const ac = new AggConfigs(indexPattern, configStates, { typesRegistry }); + expect(() => + ac.createAggConfig({ + enabled: true, + type: 'oops', + params: {}, + }) + ).toThrowErrorMatchingInlineSnapshot( + `"Unable to find a registered agg type for \\"oops\\"."` + ); + }); }); describe('#getRequestAggs', () => { diff --git a/src/plugins/data/common/search/aggs/agg_configs.ts b/src/plugins/data/common/search/aggs/agg_configs.ts index 282e6f3b538a4..368f44f161132 100644 --- a/src/plugins/data/common/search/aggs/agg_configs.ts +++ b/src/plugins/data/common/search/aggs/agg_configs.ts @@ -18,6 +18,7 @@ */ import _ from 'lodash'; +import { i18n } from '@kbn/i18n'; import { Assign } from '@kbn/utility-types'; import { ISearchOptions, ISearchSource } from 'src/plugins/data/public'; @@ -122,15 +123,29 @@ export class AggConfigs { { addToAggConfigs = true } = {} ) => { const { type } = params; - let aggConfig; + const getType = (t: string) => { + const typeFromRegistry = this.typesRegistry.get(t); + + if (!typeFromRegistry) { + throw new Error( + i18n.translate('data.search.aggs.error.aggNotFound', { + defaultMessage: 'Unable to find a registered agg type for "{type}".', + values: { type: type as string }, + }) + ); + } + return typeFromRegistry; + }; + + let aggConfig; if (params instanceof AggConfig) { aggConfig = params; params.parent = this; } else { aggConfig = new AggConfig(this, { ...params, - type: typeof type === 'string' ? this.typesRegistry.get(type) : type, + type: typeof type === 'string' ? getType(type) : type, }); } diff --git a/src/plugins/expressions/common/expression_functions/specs/tests/__snapshots__/kibana.test.ts.snap b/src/plugins/data/common/search/expressions/__snapshots__/kibana.test.ts.snap similarity index 100% rename from src/plugins/expressions/common/expression_functions/specs/tests/__snapshots__/kibana.test.ts.snap rename to src/plugins/data/common/search/expressions/__snapshots__/kibana.test.ts.snap diff --git a/src/plugins/data/common/search/expressions/esaggs.ts b/src/plugins/data/common/search/expressions/esaggs.ts index 4f65babdcd360..47d97a81a67b1 100644 --- a/src/plugins/data/common/search/expressions/esaggs.ts +++ b/src/plugins/data/common/search/expressions/esaggs.ts @@ -17,11 +17,8 @@ * under the License. */ -import { - KibanaContext, - Datatable, - ExpressionFunctionDefinition, -} from '../../../../../plugins/expressions/common'; +import { Datatable, ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; +import { KibanaContext } from './kibana_context_type'; type Input = KibanaContext | null; type Output = Promise; diff --git a/src/plugins/data/common/search/expressions/index.ts b/src/plugins/data/common/search/expressions/index.ts index 25839a805d8c5..28d892d091956 100644 --- a/src/plugins/data/common/search/expressions/index.ts +++ b/src/plugins/data/common/search/expressions/index.ts @@ -17,5 +17,8 @@ * under the License. */ +export * from './kibana'; +export * from './kibana_context'; +export * from './kibana_context_type'; export * from './esaggs'; export * from './utils'; diff --git a/src/plugins/expressions/common/expression_functions/specs/tests/kibana.test.ts b/src/plugins/data/common/search/expressions/kibana.test.ts similarity index 92% rename from src/plugins/expressions/common/expression_functions/specs/tests/kibana.test.ts rename to src/plugins/data/common/search/expressions/kibana.test.ts index e5c4b92de4fdb..58fee1ee1f6a6 100644 --- a/src/plugins/expressions/common/expression_functions/specs/tests/kibana.test.ts +++ b/src/plugins/data/common/search/expressions/kibana.test.ts @@ -17,14 +17,14 @@ * under the License. */ +import { ExecutionContext } from 'src/plugins/expressions/common'; +import { kibana } from './kibana'; +import { ExpressionValueSearchContext } from './kibana_context_type'; import { functionWrapper } from './utils'; -import { kibana } from '../kibana'; -import { ExecutionContext } from '../../../execution/types'; -import { KibanaContext, ExpressionValueSearchContext } from '../../../expression_types'; describe('interpreter/functions#kibana', () => { const fn = functionWrapper(kibana); - let input: Partial; + let input: Partial; let search: ExpressionValueSearchContext; let context: ExecutionContext; diff --git a/src/plugins/expressions/common/expression_functions/specs/kibana.ts b/src/plugins/data/common/search/expressions/kibana.ts similarity index 82% rename from src/plugins/expressions/common/expression_functions/specs/kibana.ts rename to src/plugins/data/common/search/expressions/kibana.ts index 3ec4c23eab28d..c31219284760a 100644 --- a/src/plugins/expressions/common/expression_functions/specs/kibana.ts +++ b/src/plugins/data/common/search/expressions/kibana.ts @@ -18,8 +18,9 @@ */ import { i18n } from '@kbn/i18n'; -import { ExpressionFunctionDefinition } from '../types'; -import { ExpressionValueSearchContext } from '../../expression_types'; +import { ExecutionContext, ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; +import { Adapters } from 'src/plugins/inspector/common'; +import { ExpressionValueSearchContext, ExecutionContextSearch } from './kibana_context_type'; const toArray = (query: undefined | T | T[]): T[] => !query ? [] : Array.isArray(query) ? query : [query]; @@ -29,7 +30,8 @@ export type ExpressionFunctionKibana = ExpressionFunctionDefinition< // TODO: Get rid of the `null` type below. ExpressionValueSearchContext | null, object, - ExpressionValueSearchContext + ExpressionValueSearchContext, + ExecutionContext >; export const kibana: ExpressionFunctionKibana = { @@ -38,7 +40,7 @@ export const kibana: ExpressionFunctionKibana = { inputTypes: ['kibana_context', 'null'], - help: i18n.translate('expressions.functions.kibana.help', { + help: i18n.translate('data.search.functions.kibana.help', { defaultMessage: 'Gets kibana global context', }), diff --git a/src/plugins/expressions/common/expression_functions/specs/kibana_context.ts b/src/plugins/data/common/search/expressions/kibana_context.ts similarity index 84% rename from src/plugins/expressions/common/expression_functions/specs/kibana_context.ts rename to src/plugins/data/common/search/expressions/kibana_context.ts index 2b7d1b8ed9d76..7bf14ff02316e 100644 --- a/src/plugins/expressions/common/expression_functions/specs/kibana_context.ts +++ b/src/plugins/data/common/search/expressions/kibana_context.ts @@ -16,11 +16,13 @@ * specific language governing permissions and limitations * under the License. */ + import { uniqBy } from 'lodash'; import { i18n } from '@kbn/i18n'; -import { ExpressionFunctionDefinition } from '../../expression_functions'; -import { KibanaContext } from '../../expression_types'; -import { Query, uniqFilters } from '../../../../data/common'; +import { ExpressionFunctionDefinition, ExecutionContext } from 'src/plugins/expressions/common'; +import { Adapters } from 'src/plugins/inspector/common'; +import { Query, uniqFilters } from '../../query'; +import { ExecutionContextSearch, KibanaContext } from './kibana_context_type'; interface Arguments { q?: string | null; @@ -33,7 +35,8 @@ export type ExpressionFunctionKibanaContext = ExpressionFunctionDefinition< 'kibana_context', KibanaContext | null, Arguments, - Promise + Promise, + ExecutionContext >; const getParsedValue = (data: any, defaultValue: any) => @@ -49,7 +52,7 @@ export const kibanaContextFunction: ExpressionFunctionKibanaContext = { name: 'kibana_context', type: 'kibana_context', inputTypes: ['kibana_context', 'null'], - help: i18n.translate('expressions.functions.kibana_context.help', { + help: i18n.translate('data.search.functions.kibana_context.help', { defaultMessage: 'Updates kibana global context', }), args: { @@ -57,28 +60,28 @@ export const kibanaContextFunction: ExpressionFunctionKibanaContext = { types: ['string', 'null'], aliases: ['query', '_'], default: null, - help: i18n.translate('expressions.functions.kibana_context.q.help', { + help: i18n.translate('data.search.functions.kibana_context.q.help', { defaultMessage: 'Specify Kibana free form text query', }), }, filters: { types: ['string', 'null'], default: '"[]"', - help: i18n.translate('expressions.functions.kibana_context.filters.help', { + help: i18n.translate('data.search.functions.kibana_context.filters.help', { defaultMessage: 'Specify Kibana generic filters', }), }, timeRange: { types: ['string', 'null'], default: null, - help: i18n.translate('expressions.functions.kibana_context.timeRange.help', { + help: i18n.translate('data.search.functions.kibana_context.timeRange.help', { defaultMessage: 'Specify Kibana time range filter', }), }, savedSearchId: { types: ['string', 'null'], default: null, - help: i18n.translate('expressions.functions.kibana_context.savedSearchId.help', { + help: i18n.translate('data.search.functions.kibana_context.savedSearchId.help', { defaultMessage: 'Specify saved search ID to be used for queries and filters', }), }, diff --git a/src/plugins/expressions/common/expression_types/specs/kibana_context.ts b/src/plugins/data/common/search/expressions/kibana_context_type.ts similarity index 79% rename from src/plugins/expressions/common/expression_types/specs/kibana_context.ts rename to src/plugins/data/common/search/expressions/kibana_context_type.ts index 3af7b990429c0..a5b459a607991 100644 --- a/src/plugins/expressions/common/expression_types/specs/kibana_context.ts +++ b/src/plugins/data/common/search/expressions/kibana_context_type.ts @@ -17,8 +17,16 @@ * under the License. */ -import { ExpressionValueBoxed } from '../types'; -import { ExecutionContextSearch } from '../../execution/types'; +import { ExpressionValueBoxed } from 'src/plugins/expressions/common'; +import { Filter } from '../../es_query'; +import { Query, TimeRange } from '../../query'; + +// eslint-disable-next-line @typescript-eslint/consistent-type-definitions +export type ExecutionContextSearch = { + filters?: Filter[]; + query?: Query | Query[]; + timeRange?: TimeRange; +}; export type ExpressionValueSearchContext = ExpressionValueBoxed< 'kibana_context', diff --git a/src/plugins/data/common/search/expressions/utils/function_wrapper.ts b/src/plugins/data/common/search/expressions/utils/function_wrapper.ts new file mode 100644 index 0000000000000..b5e45d9ca1de0 --- /dev/null +++ b/src/plugins/data/common/search/expressions/utils/function_wrapper.ts @@ -0,0 +1,34 @@ +/* + * 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 { mapValues } from 'lodash'; +import { AnyExpressionFunctionDefinition, ExecutionContext } from 'src/plugins/expressions/common'; + +/** + * Takes a function spec and passes in default args, + * overriding with any provided args. + */ +export const functionWrapper = (spec: AnyExpressionFunctionDefinition) => { + const defaultArgs = mapValues(spec.args, (argSpec) => argSpec.default); + return ( + context: object | null, + args: Record = {}, + handlers: ExecutionContext = {} as ExecutionContext + ) => spec.fn(context, { ...defaultArgs, ...args }, handlers); +}; diff --git a/src/plugins/data/common/search/expressions/utils/index.ts b/src/plugins/data/common/search/expressions/utils/index.ts index 75c1809770c78..39f88b0f13fee 100644 --- a/src/plugins/data/common/search/expressions/utils/index.ts +++ b/src/plugins/data/common/search/expressions/utils/index.ts @@ -18,3 +18,4 @@ */ export * from './courier_inspector_stats'; +export * from './function_wrapper'; diff --git a/src/plugins/data/common/utils/index.ts b/src/plugins/data/common/utils/index.ts index 33989f3ad50a7..8b8686c51b9c1 100644 --- a/src/plugins/data/common/utils/index.ts +++ b/src/plugins/data/common/utils/index.ts @@ -19,4 +19,3 @@ /** @internal */ export { shortenDottedString } from './shorten_dotted_string'; -export { AbortError, toPromise, getCombinedSignal } from './abort_utils'; diff --git a/src/plugins/data/kibana.json b/src/plugins/data/kibana.json index 9cb9b1745373a..d6f2534bd5e3b 100644 --- a/src/plugins/data/kibana.json +++ b/src/plugins/data/kibana.json @@ -8,7 +8,7 @@ "uiActions" ], "optionalPlugins": ["usageCollection"], - "extraPublicDirs": ["common", "common/utils/abort_utils"], + "extraPublicDirs": ["common"], "requiredBundles": [ "usageCollection", "kibanaUtils", diff --git a/src/plugins/data/public/autocomplete/providers/query_suggestion_provider.ts b/src/plugins/data/public/autocomplete/providers/query_suggestion_provider.ts index 16500ac9e239a..da523e740c3d6 100644 --- a/src/plugins/data/public/autocomplete/providers/query_suggestion_provider.ts +++ b/src/plugins/data/public/autocomplete/providers/query_suggestion_provider.ts @@ -39,6 +39,7 @@ export interface QuerySuggestionGetFnArgs { selectionStart: number; selectionEnd: number; signal?: AbortSignal; + useTimeRange?: boolean; boolFilter?: any; } diff --git a/src/plugins/data/public/autocomplete/providers/value_suggestion_provider.test.ts b/src/plugins/data/public/autocomplete/providers/value_suggestion_provider.test.ts index 6b0c0f07cf6c9..0ef5b7db958e4 100644 --- a/src/plugins/data/public/autocomplete/providers/value_suggestion_provider.test.ts +++ b/src/plugins/data/public/autocomplete/providers/value_suggestion_provider.test.ts @@ -21,6 +21,26 @@ import { stubIndexPattern, stubFields } from '../../stubs'; import { setupValueSuggestionProvider, ValueSuggestionsGetFn } from './value_suggestion_provider'; import { IUiSettingsClient, CoreSetup } from 'kibana/public'; +jest.mock('../../services', () => ({ + getQueryService: () => ({ + timefilter: { + timefilter: { + createFilter: () => { + return { + time: 'fake', + }; + }, + getTime: () => { + return { + to: 'now', + from: 'now-15m', + }; + }, + }, + }, + }), +})); + describe('FieldSuggestions', () => { let getValueSuggestions: ValueSuggestionsGetFn; let http: any; @@ -94,6 +114,7 @@ describe('FieldSuggestions', () => { indexPattern: stubIndexPattern, field, query: '', + useTimeRange: false, }); expect(http.fetch).toHaveBeenCalled(); @@ -107,6 +128,7 @@ describe('FieldSuggestions', () => { indexPattern: stubIndexPattern, field, query: '', + useTimeRange: false, }; await getValueSuggestions(args); @@ -123,6 +145,7 @@ describe('FieldSuggestions', () => { indexPattern: stubIndexPattern, field, query: '', + useTimeRange: false, }; const { now } = Date; @@ -146,50 +169,76 @@ describe('FieldSuggestions', () => { indexPattern: stubIndexPattern, field: fields[0], query: '', + useTimeRange: false, }); await getValueSuggestions({ indexPattern: stubIndexPattern, field: fields[0], query: 'query', + useTimeRange: false, }); await getValueSuggestions({ indexPattern: stubIndexPattern, field: fields[1], query: '', + useTimeRange: false, }); await getValueSuggestions({ indexPattern: stubIndexPattern, field: fields[1], query: 'query', + useTimeRange: false, }); const customIndexPattern = { ...stubIndexPattern, title: 'customIndexPattern', + useTimeRange: false, }; await getValueSuggestions({ indexPattern: customIndexPattern, field: fields[0], query: '', + useTimeRange: false, }); await getValueSuggestions({ indexPattern: customIndexPattern, field: fields[0], query: 'query', + useTimeRange: false, }); await getValueSuggestions({ indexPattern: customIndexPattern, field: fields[1], query: '', + useTimeRange: false, }); await getValueSuggestions({ indexPattern: customIndexPattern, field: fields[1], query: 'query', + useTimeRange: false, }); expect(http.fetch).toHaveBeenCalledTimes(8); }); + + it('should apply timefilter', async () => { + const [field] = stubFields.filter( + ({ type, aggregatable }) => type === 'string' && aggregatable + ); + + await getValueSuggestions({ + indexPattern: stubIndexPattern, + field, + query: '', + useTimeRange: true, + }); + const callParams = http.fetch.mock.calls[0][1]; + + expect(JSON.parse(callParams.body).filters).toHaveLength(1); + expect(http.fetch).toHaveBeenCalled(); + }); }); }); diff --git a/src/plugins/data/public/autocomplete/providers/value_suggestion_provider.ts b/src/plugins/data/public/autocomplete/providers/value_suggestion_provider.ts index a6a45a26f06b3..fe9f939a0261d 100644 --- a/src/plugins/data/public/autocomplete/providers/value_suggestion_provider.ts +++ b/src/plugins/data/public/autocomplete/providers/value_suggestion_provider.ts @@ -17,15 +17,16 @@ * under the License. */ +import dateMath from '@elastic/datemath'; import { memoize } from 'lodash'; import { CoreSetup } from 'src/core/public'; -import { IIndexPattern, IFieldType, UI_SETTINGS } from '../../../common'; +import { IIndexPattern, IFieldType, UI_SETTINGS, buildQueryFromFilters } from '../../../common'; +import { getQueryService } from '../../services'; -function resolver(title: string, field: IFieldType, query: string, boolFilter: any) { +function resolver(title: string, field: IFieldType, query: string, filters: any[]) { // Only cache results for a minute const ttl = Math.floor(Date.now() / 1000 / 60); - - return [ttl, query, title, field.name, JSON.stringify(boolFilter)].join('|'); + return [ttl, query, title, field.name, JSON.stringify(filters)].join('|'); } export type ValueSuggestionsGetFn = (args: ValueSuggestionsGetFnArgs) => Promise; @@ -34,18 +35,31 @@ interface ValueSuggestionsGetFnArgs { indexPattern: IIndexPattern; field: IFieldType; query: string; + useTimeRange?: boolean; boolFilter?: any[]; signal?: AbortSignal; } +const getAutocompleteTimefilter = (indexPattern: IIndexPattern) => { + const { timefilter } = getQueryService().timefilter; + const timeRange = timefilter.getTime(); + + // Use a rounded timerange so that memoizing works properly + const roundedTimerange = { + from: dateMath.parse(timeRange.from)!.startOf('minute').toISOString(), + to: dateMath.parse(timeRange.to)!.endOf('minute').toISOString(), + }; + return timefilter.createFilter(indexPattern, roundedTimerange); +}; + export const getEmptyValueSuggestions = (() => Promise.resolve([])) as ValueSuggestionsGetFn; export const setupValueSuggestionProvider = (core: CoreSetup): ValueSuggestionsGetFn => { const requestSuggestions = memoize( - (index: string, field: IFieldType, query: string, boolFilter: any = [], signal?: AbortSignal) => + (index: string, field: IFieldType, query: string, filters: any = [], signal?: AbortSignal) => core.http.fetch(`/api/kibana/suggestions/values/${index}`, { method: 'POST', - body: JSON.stringify({ query, field: field.name, boolFilter }), + body: JSON.stringify({ query, field: field.name, filters }), signal, }), resolver @@ -55,12 +69,15 @@ export const setupValueSuggestionProvider = (core: CoreSetup): ValueSuggestionsG indexPattern, field, query, + useTimeRange, boolFilter, signal, }: ValueSuggestionsGetFnArgs): Promise => { const shouldSuggestValues = core!.uiSettings.get( UI_SETTINGS.FILTERS_EDITOR_SUGGEST_VALUES ); + useTimeRange = + useTimeRange ?? core!.uiSettings.get(UI_SETTINGS.AUTOCOMPLETE_USE_TIMERANGE); const { title } = indexPattern; if (field.type === 'boolean') { @@ -69,6 +86,9 @@ export const setupValueSuggestionProvider = (core: CoreSetup): ValueSuggestionsG return []; } - return await requestSuggestions(title, field, query, boolFilter, signal); + const timeFilter = useTimeRange ? getAutocompleteTimefilter(indexPattern) : undefined; + const filterQuery = timeFilter ? buildQueryFromFilters([timeFilter], indexPattern).filter : []; + const filters = [...(boolFilter ? boolFilter : []), ...filterQuery]; + return await requestSuggestions(title, field, query, filters, signal); }; }; diff --git a/src/plugins/data/public/index.ts b/src/plugins/data/public/index.ts index ce020a9742399..129addf3de70e 100644 --- a/src/plugins/data/public/index.ts +++ b/src/plugins/data/public/index.ts @@ -338,6 +338,12 @@ export { OptionedParamType, OptionedValueProp, ParsedInterval, + // expressions + ExecutionContextSearch, + ExpressionFunctionKibana, + ExpressionFunctionKibanaContext, + ExpressionValueSearchContext, + KibanaContext, // tabify TabbedAggColumn, TabbedAggRow, diff --git a/src/plugins/data/public/index_patterns/index_patterns/index_patterns_api_client.test.ts b/src/plugins/data/public/index_patterns/index_patterns/index_patterns_api_client.test.ts index 37ee80c2c29e4..8c48ee44fba9c 100644 --- a/src/plugins/data/public/index_patterns/index_patterns/index_patterns_api_client.test.ts +++ b/src/plugins/data/public/index_patterns/index_patterns/index_patterns_api_client.test.ts @@ -32,7 +32,12 @@ describe('IndexPatternsApiClient', () => { test('uses the right URI to fetch fields for time patterns', async function () { const expectedPath = '/api/index_patterns/_fields_for_time_pattern'; - await indexPatternsApiClient.getFieldsForTimePattern(); + await indexPatternsApiClient.getFieldsForTimePattern({ + pattern: 'blah', + metaFields: [], + lookBack: 5, + interval: '', + }); expect(fetchSpy).toHaveBeenCalledWith(expectedPath, expect.any(Object)); }); @@ -40,15 +45,7 @@ describe('IndexPatternsApiClient', () => { test('uses the right URI to fetch fields for wildcard', async function () { const expectedPath = '/api/index_patterns/_fields_for_wildcard'; - await indexPatternsApiClient.getFieldsForWildcard(); - - expect(fetchSpy).toHaveBeenCalledWith(expectedPath, expect.any(Object)); - }); - - test('uses the right URI to fetch fields for wildcard given a type', async function () { - const expectedPath = '/api/index_patterns/rollup/_fields_for_wildcard'; - - await indexPatternsApiClient.getFieldsForWildcard({ type: 'rollup' }); + await indexPatternsApiClient.getFieldsForWildcard({ pattern: 'blah' }); expect(fetchSpy).toHaveBeenCalledWith(expectedPath, expect.any(Object)); }); diff --git a/src/plugins/data/public/index_patterns/index_patterns/index_patterns_api_client.ts b/src/plugins/data/public/index_patterns/index_patterns/index_patterns_api_client.ts index 377a3f7f91a50..ca0f35d6612b2 100644 --- a/src/plugins/data/public/index_patterns/index_patterns/index_patterns_api_client.ts +++ b/src/plugins/data/public/index_patterns/index_patterns/index_patterns_api_client.ts @@ -19,7 +19,11 @@ import { HttpSetup } from 'src/core/public'; import { IndexPatternMissingIndices } from '../../../common/index_patterns/lib'; -import { GetFieldsOptions, IIndexPatternsApiClient } from '../../../common/index_patterns/types'; +import { + GetFieldsOptions, + IIndexPatternsApiClient, + GetFieldsOptionsTimePattern, +} from '../../../common/index_patterns/types'; const API_BASE_URL: string = `/api/index_patterns/`; @@ -48,7 +52,7 @@ export class IndexPatternsApiClient implements IIndexPatternsApiClient { return API_BASE_URL + path.filter(Boolean).map(encodeURIComponent).join('/'); } - getFieldsForTimePattern(options: GetFieldsOptions = {}) { + getFieldsForTimePattern(options: GetFieldsOptionsTimePattern) { const { pattern, lookBack, metaFields } = options; const url = this._getUrl(['_fields_for_time_pattern']); @@ -60,27 +64,12 @@ export class IndexPatternsApiClient implements IIndexPatternsApiClient { }).then((resp: any) => resp.fields); } - getFieldsForWildcard(options: GetFieldsOptions = {}) { - const { pattern, metaFields, type, params } = options; - - let url; - let query; - - if (type) { - url = this._getUrl([type, '_fields_for_wildcard']); - query = { - pattern, - meta_fields: metaFields, - params: JSON.stringify(params), - }; - } else { - url = this._getUrl(['_fields_for_wildcard']); - query = { - pattern, - meta_fields: metaFields, - }; - } - - return this._request(url, query).then((resp: any) => resp.fields); + getFieldsForWildcard({ pattern, metaFields, type, rollupIndex }: GetFieldsOptions) { + return this._request(this._getUrl(['_fields_for_wildcard']), { + pattern, + meta_fields: metaFields, + type, + rollup_index: rollupIndex, + }).then((resp: any) => resp.fields); } } diff --git a/src/plugins/data/public/plugin.ts b/src/plugins/data/public/plugin.ts index 5abf4d3648af7..afa8d935f367b 100644 --- a/src/plugins/data/public/plugin.ts +++ b/src/plugins/data/public/plugin.ts @@ -72,6 +72,7 @@ import { import { SavedObjectsClientPublicToCommon } from './index_patterns'; import { indexPatternLoad } from './index_patterns/expressions/load_index_pattern'; +import { UsageCollectionSetup } from '../../usage_collection/public'; declare module '../../ui_actions/public' { export interface ActionContextMapping { @@ -94,6 +95,7 @@ export class DataPublicPlugin private readonly fieldFormatsService: FieldFormatsService; private readonly queryService: QueryService; private readonly storage: IStorageWrapper; + private usageCollection: UsageCollectionSetup | undefined; constructor(initializerContext: PluginInitializerContext) { this.searchService = new SearchService(initializerContext); @@ -112,6 +114,8 @@ export class DataPublicPlugin expressions.registerFunction(esaggs); expressions.registerFunction(indexPatternLoad); + this.usageCollection = usageCollection; + const queryService = this.queryService.setup({ uiSettings: core.uiSettings, storage: this.storage, @@ -208,6 +212,7 @@ export class DataPublicPlugin core, data: dataServices, storage: this.storage, + trackUiMetric: this.usageCollection?.reportUiStats.bind(this.usageCollection, 'data_plugin'), }); return { diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md index d52edbe5b11dd..78b974758f8c0 100644 --- a/src/plugins/data/public/public.api.md +++ b/src/plugins/data/public/public.api.md @@ -6,6 +6,7 @@ import { $Values } from '@kbn/utility-types'; import { Action } from 'history'; +import { Adapters as Adapters_2 } from 'src/plugins/inspector/common'; import { ApiResponse } from '@elastic/elasticsearch'; import { ApiResponse as ApiResponse_2 } from '@elastic/elasticsearch/lib/Transport'; import { ApplicationStart } from 'kibana/public'; @@ -16,6 +17,7 @@ import { CoreSetup } from 'src/core/public'; import { CoreSetup as CoreSetup_2 } from 'kibana/public'; import { CoreStart } from 'kibana/public'; import { CoreStart as CoreStart_2 } from 'src/core/public'; +import { Datatable as Datatable_2 } from 'src/plugins/expressions/common'; import { DatatableColumn as DatatableColumn_2 } from 'src/plugins/expressions'; import { Ensure } from '@kbn/utility-types'; import { EnvironmentMode } from '@kbn/config'; @@ -26,8 +28,12 @@ import { EuiComboBoxProps } from '@elastic/eui'; import { EuiConfirmModalProps } from '@elastic/eui'; import { EuiGlobalToastListToast } from '@elastic/eui'; import { ExclusiveUnion } from '@elastic/eui'; +import { ExecutionContext } from 'src/plugins/expressions/common'; import { ExpressionAstFunction } from 'src/plugins/expressions/common'; +import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; +import { ExpressionFunctionDefinition as ExpressionFunctionDefinition_2 } from 'src/plugins/expressions/public'; import { ExpressionsSetup } from 'src/plugins/expressions/public'; +import { ExpressionValueBoxed } from 'src/plugins/expressions/common'; import { History } from 'history'; import { Href } from 'history'; import { IconType } from '@elastic/eui'; @@ -65,7 +71,7 @@ import { Required } from '@kbn/utility-types'; import * as Rx from 'rxjs'; import { SavedObject } from 'src/core/server'; import { SavedObject as SavedObject_2 } from 'src/core/public'; -import { SavedObjectReference as SavedObjectReference_2 } from 'src/core/types'; +import { SavedObjectReference } from 'src/core/types'; import { SavedObjectsClientContract } from 'src/core/public'; import { Search } from '@elastic/elasticsearch/api/requestParams'; import { SearchResponse } from 'elasticsearch'; @@ -78,9 +84,9 @@ import { TransportRequestPromise } from '@elastic/elasticsearch/lib/Transport'; import { TypeOf } from '@kbn/config-schema'; import { UiActionsSetup } from 'src/plugins/ui_actions/public'; import { UiActionsStart } from 'src/plugins/ui_actions/public'; +import { UiStatsMetricType } from '@kbn/analytics'; import { Unit } from '@elastic/datemath'; import { UnregisterCallback } from 'history'; -import { UnwrapPromiseOrReturn } from '@kbn/utility-types'; import { UserProvidedValues } from 'src/core/server/types'; // Warning: (ae-missing-release-tag) "ACTION_GLOBAL_APPLY_FILTER" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -525,7 +531,6 @@ export enum ES_FIELD_TYPES { // @public (undocumented) export const ES_SEARCH_STRATEGY = "es"; -// Warning: (ae-forgotten-export) The symbol "ExpressionFunctionDefinition" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "Input" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "Arguments" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "Output" needs to be exported by the entry point index.d.ts @@ -541,7 +546,7 @@ export type EsaggsExpressionFunctionDefinition = ExpressionFunctionDefinition<'e // Warning: (ae-missing-release-tag) "EsdslExpressionFunctionDefinition" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export type EsdslExpressionFunctionDefinition = ExpressionFunctionDefinition; +export type EsdslExpressionFunctionDefinition = ExpressionFunctionDefinition_2; // Warning: (ae-missing-release-tag) "esFilters" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -647,6 +652,15 @@ export type EsQuerySortValue = Record; +// Warning: (ae-missing-release-tag) "ExecutionContextSearch" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export type ExecutionContextSearch = { + filters?: Filter[]; + query?: Query | Query[]; + timeRange?: TimeRange; +}; + // Warning: (ae-missing-release-tag) "ExistsFilter" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -655,12 +669,28 @@ export type ExistsFilter = Filter & { exists?: FilterExistsProperty; }; +// Warning: (ae-missing-release-tag) "ExpressionFunctionKibana" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export type ExpressionFunctionKibana = ExpressionFunctionDefinition<'kibana', ExpressionValueSearchContext | null, object, ExpressionValueSearchContext, ExecutionContext>; + +// Warning: (ae-forgotten-export) The symbol "Arguments" needs to be exported by the entry point index.d.ts +// Warning: (ae-missing-release-tag) "ExpressionFunctionKibanaContext" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export type ExpressionFunctionKibanaContext = ExpressionFunctionDefinition<'kibana_context', KibanaContext | null, Arguments_2, Promise, ExecutionContext>; + +// Warning: (ae-missing-release-tag) "ExpressionValueSearchContext" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export type ExpressionValueSearchContext = ExpressionValueBoxed<'kibana_context', ExecutionContextSearch>; + // Warning: (ae-missing-release-tag) "extractReferences" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) export const extractSearchSourceReferences: (state: SearchSourceFields) => [SearchSourceFields & { indexRefName?: string; -}, SavedObjectReference_2[]]; +}, SavedObjectReference[]]; // Warning: (ae-missing-release-tag) "FieldFormat" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -1338,9 +1368,9 @@ export class IndexPatternsService { // (undocumented) getCache: () => Promise[] | null | undefined>; getDefault: () => Promise; - getFieldsForIndexPattern: (indexPattern: IndexPattern | IndexPatternSpec, options?: GetFieldsOptions) => Promise; + getFieldsForIndexPattern: (indexPattern: IndexPattern | IndexPatternSpec, options?: GetFieldsOptions | undefined) => Promise; // Warning: (ae-forgotten-export) The symbol "GetFieldsOptions" needs to be exported by the entry point index.d.ts - getFieldsForWildcard: (options?: GetFieldsOptions) => Promise; + getFieldsForWildcard: (options: GetFieldsOptions) => Promise; getIds: (refresh?: boolean) => Promise; getIdsWithTitle: (refresh?: boolean) => Promise SearchSourceFields; +}, references: SavedObjectReference[]) => SearchSourceFields; // Warning: (ae-missing-release-tag) "InputTimeRange" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -1510,6 +1540,11 @@ export enum KBN_FIELD_TYPES { UNKNOWN = "unknown" } +// Warning: (ae-missing-release-tag) "KibanaContext" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export type KibanaContext = ExpressionValueSearchContext; + // Warning: (ae-missing-release-tag) "KueryNode" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -1806,6 +1841,8 @@ export interface QuerySuggestionGetFnArgs { selectionStart: number; // (undocumented) signal?: AbortSignal; + // (undocumented) + useTimeRange?: boolean; } // Warning: (ae-missing-release-tag) "QuerySuggestionTypes" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -1962,8 +1999,8 @@ export const search: { // Warning: (ae-missing-release-tag) "SearchBar" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export const SearchBar: React.ComponentClass, "query" | "isLoading" | "filters" | "onRefresh" | "onRefreshChange" | "refreshInterval" | "indexPatterns" | "dataTestSubj" | "timeHistory" | "customSubmitButton" | "screenTitle" | "showQueryBar" | "showQueryInput" | "showFilterBar" | "showDatePicker" | "showAutoRefreshOnly" | "isRefreshPaused" | "dateRangeFrom" | "dateRangeTo" | "showSaveQuery" | "savedQuery" | "onQueryChange" | "onQuerySubmit" | "onSaved" | "onSavedQueryUpdated" | "onClearSavedQuery" | "indicateNoData" | "onFiltersUpdated">, any> & { - WrappedComponent: React.ComponentType & ReactIntl.InjectedIntlProps>; +export const SearchBar: React.ComponentClass, "query" | "isLoading" | "filters" | "onRefresh" | "onRefreshChange" | "refreshInterval" | "indexPatterns" | "dataTestSubj" | "screenTitle" | "showQueryInput" | "showDatePicker" | "showAutoRefreshOnly" | "dateRangeFrom" | "dateRangeTo" | "isRefreshPaused" | "customSubmitButton" | "timeHistory" | "indicateNoData" | "onFiltersUpdated" | "trackUiMetric" | "savedQuery" | "showSaveQuery" | "onClearSavedQuery" | "showQueryBar" | "showFilterBar" | "onQueryChange" | "onQuerySubmit" | "onSaved" | "onSavedQueryUpdated">, any> & { + WrappedComponent: React.ComponentType & ReactIntl.InjectedIntlProps>; }; // Warning: (ae-forgotten-export) The symbol "SearchBarOwnProps" needs to be exported by the entry point index.d.ts @@ -2275,6 +2312,7 @@ export const UI_SETTINGS: { readonly INDEXPATTERN_PLACEHOLDER: "indexPattern:placeholder"; readonly FILTERS_PINNED_BY_DEFAULT: "filters:pinnedByDefault"; readonly FILTERS_EDITOR_SUGGEST_VALUES: "filterEditor:suggestValues"; + readonly AUTOCOMPLETE_USE_TIMERANGE: "autocomplete:useTimeRange"; }; @@ -2321,21 +2359,21 @@ export const UI_SETTINGS: { // src/plugins/data/public/index.ts:236:27 - (ae-forgotten-export) The symbol "getFromSavedObject" needs to be exported by the entry point index.d.ts // src/plugins/data/public/index.ts:236:27 - (ae-forgotten-export) The symbol "flattenHitWrapper" needs to be exported by the entry point index.d.ts // src/plugins/data/public/index.ts:236:27 - (ae-forgotten-export) The symbol "formatHitProvider" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:387:20 - (ae-forgotten-export) The symbol "getRequestInspectorStats" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:387:20 - (ae-forgotten-export) The symbol "getResponseInspectorStats" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:387:20 - (ae-forgotten-export) The symbol "tabifyAggResponse" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:387:20 - (ae-forgotten-export) The symbol "tabifyGetColumns" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:389:1 - (ae-forgotten-export) The symbol "CidrMask" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:390:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:399:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:400:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:401:1 - (ae-forgotten-export) The symbol "Ipv4Address" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:402:1 - (ae-forgotten-export) The symbol "isDateHistogramBucketAggConfig" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:406:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:407:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:410:1 - (ae-forgotten-export) The symbol "parseInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:411:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:414:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:393:20 - (ae-forgotten-export) The symbol "getRequestInspectorStats" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:393:20 - (ae-forgotten-export) The symbol "getResponseInspectorStats" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:393:20 - (ae-forgotten-export) The symbol "tabifyAggResponse" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:393:20 - (ae-forgotten-export) The symbol "tabifyGetColumns" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:395:1 - (ae-forgotten-export) The symbol "CidrMask" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:396:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:405:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:406:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:407:1 - (ae-forgotten-export) The symbol "Ipv4Address" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:408:1 - (ae-forgotten-export) The symbol "isDateHistogramBucketAggConfig" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:412:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:413:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:416:1 - (ae-forgotten-export) The symbol "parseInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:417:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:420: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 // (No @packageDocumentation comment for this package) diff --git a/src/plugins/data/public/query/timefilter/timefilter.ts b/src/plugins/data/public/query/timefilter/timefilter.ts index 49a8c68f6916f..7278ceaaddcce 100644 --- a/src/plugins/data/public/query/timefilter/timefilter.ts +++ b/src/plugins/data/public/query/timefilter/timefilter.ts @@ -24,9 +24,14 @@ import { PublicMethodsOf } from '@kbn/utility-types'; import { areRefreshIntervalsDifferent, areTimeRangesDifferent } from './lib/diff_time_picker_vals'; import { getForceNow } from './lib/get_force_now'; import { TimefilterConfig, InputTimeRange, TimeRangeBounds } from './types'; -import { calculateBounds, getTime, RefreshInterval, TimeRange } from '../../../common'; +import { + calculateBounds, + getTime, + IIndexPattern, + RefreshInterval, + TimeRange, +} from '../../../common'; import { TimeHistoryContract } from './time_history'; -import { IndexPattern } from '../../index_patterns'; // TODO: remove! @@ -170,7 +175,7 @@ export class Timefilter { } }; - public createFilter = (indexPattern: IndexPattern, timeRange?: TimeRange) => { + public createFilter = (indexPattern: IIndexPattern, timeRange?: TimeRange) => { return getTime(indexPattern, timeRange ? timeRange : this._time, { forceNow: this.getForceNow(), }); diff --git a/src/plugins/data/public/search/errors/timeout_error.test.tsx b/src/plugins/data/public/search/errors/timeout_error.test.tsx index ad3384c389fbf..2547ee41cbbd7 100644 --- a/src/plugins/data/public/search/errors/timeout_error.test.tsx +++ b/src/plugins/data/public/search/errors/timeout_error.test.tsx @@ -23,7 +23,7 @@ import { coreMock } from '../../../../../core/public/mocks'; const startMock = coreMock.createStart(); import { mount } from 'enzyme'; -import { AbortError } from 'src/plugins/data/common'; +import { AbortError } from '../../../../kibana_utils/public'; describe('SearchTimeoutError', () => { beforeEach(() => { diff --git a/src/plugins/data/public/search/expressions/esdsl.ts b/src/plugins/data/public/search/expressions/esdsl.ts index 2efb21671b5b7..07e904928744f 100644 --- a/src/plugins/data/public/search/expressions/esdsl.ts +++ b/src/plugins/data/public/search/expressions/esdsl.ts @@ -18,15 +18,12 @@ */ import { i18n } from '@kbn/i18n'; -import { - KibanaContext, - ExpressionFunctionDefinition, -} from '../../../../../plugins/expressions/public'; +import { ExpressionFunctionDefinition } from 'src/plugins/expressions/public'; import { getSearchService, getUiSettings } from '../../services'; import { EsRawResponse } from './es_raw_response'; import { RequestStatistics, RequestAdapter } from '../../../../inspector/common'; -import { IEsSearchResponse } from '../../../common/search/es_search'; +import { IEsSearchResponse, KibanaContext } from '../../../common/search'; import { buildEsQuery, getEsQueryConfig } from '../../../common/es_query/es_query'; import { DataPublicPluginStart } from '../../types'; diff --git a/src/plugins/data/public/search/search_interceptor.test.ts b/src/plugins/data/public/search/search_interceptor.test.ts index 472caa5e4f45f..60274261da25f 100644 --- a/src/plugins/data/public/search/search_interceptor.test.ts +++ b/src/plugins/data/public/search/search_interceptor.test.ts @@ -21,7 +21,7 @@ import { CoreSetup, CoreStart } from '../../../../core/public'; import { coreMock } from '../../../../core/public/mocks'; import { IEsSearchRequest } from '../../common/search'; import { SearchInterceptor } from './search_interceptor'; -import { AbortError } from '../../common'; +import { AbortError } from '../../../kibana_utils/public'; import { SearchTimeoutError, PainlessError, TimeoutErrorMode } from './errors'; import { searchServiceMock } from './mocks'; import { ISearchStart } from '.'; diff --git a/src/plugins/data/public/search/search_interceptor.ts b/src/plugins/data/public/search/search_interceptor.ts index 3584d75ab86bb..78e65802bcf99 100644 --- a/src/plugins/data/public/search/search_interceptor.ts +++ b/src/plugins/data/public/search/search_interceptor.ts @@ -24,13 +24,11 @@ import { PublicMethodsOf } from '@kbn/utility-types'; import { CoreStart, CoreSetup, ToastsSetup } from 'kibana/public'; import { i18n } from '@kbn/i18n'; import { - AbortError, IKibanaSearchRequest, IKibanaSearchResponse, ISearchOptions, ES_SEARCH_STRATEGY, ISessionService, - getCombinedSignal, } from '../../common'; import { SearchUsageCollector } from './collectors'; import { @@ -43,6 +41,7 @@ import { getHttpError, } from './errors'; import { toMountPoint } from '../../../kibana_react/public'; +import { AbortError, getCombinedAbortSignal } from '../../../kibana_utils/public'; export interface SearchInterceptorDeps { http: CoreSetup['http']; @@ -170,7 +169,9 @@ export class SearchInterceptor { ...(abortSignal ? [abortSignal] : []), ]; - const { signal: combinedSignal, cleanup: cleanupCombinedSignal } = getCombinedSignal(signals); + const { signal: combinedSignal, cleanup: cleanupCombinedSignal } = getCombinedAbortSignal( + signals + ); const cleanup = () => { subscription.unsubscribe(); combinedSignal.removeEventListener('abort', cleanup); diff --git a/src/plugins/data/public/search/search_service.ts b/src/plugins/data/public/search/search_service.ts index e5a50077518af..96fb3f91ea85f 100644 --- a/src/plugins/data/public/search/search_service.ts +++ b/src/plugins/data/public/search/search_service.ts @@ -23,10 +23,13 @@ import { ISearchSetup, ISearchStart, SearchEnhancements } from './types'; import { handleResponse } from './fetch'; import { + kibana, + kibanaContext, + kibanaContextFunction, ISearchGeneric, - SearchSourceService, - SearchSourceDependencies, ISessionService, + SearchSourceDependencies, + SearchSourceService, } from '../../common/search'; import { getCallMsearch } from './legacy'; import { AggsService, AggsStartDependencies } from './aggs'; @@ -85,6 +88,10 @@ export class SearchService implements Plugin { session: this.sessionService, }); + expressions.registerFunction(kibana); + expressions.registerFunction(kibanaContextFunction); + expressions.registerType(kibanaContext); + expressions.registerFunction(esdsl); expressions.registerType(esRawResponse); diff --git a/src/plugins/data/public/ui/filter_bar/filter_bar.tsx b/src/plugins/data/public/ui/filter_bar/filter_bar.tsx index 8c009576ff280..194e253fd7b26 100644 --- a/src/plugins/data/public/ui/filter_bar/filter_bar.tsx +++ b/src/plugins/data/public/ui/filter_bar/filter_bar.tsx @@ -22,6 +22,7 @@ import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react'; import classNames from 'classnames'; import React, { useState } from 'react'; +import { METRIC_TYPE, UiStatsMetricType } from '@kbn/analytics'; import { FilterEditor } from './filter_editor'; import { FILTER_EDITOR_WIDTH, FilterItem } from './filter_item'; import { FilterOptions } from './filter_options'; @@ -45,6 +46,9 @@ interface Props { className: string; indexPatterns: IIndexPattern[]; intl: InjectedIntl; + appName: string; + // Track UI Metrics + trackUiMetric?: (metricType: UiStatsMetricType, eventName: string | string[]) => void; } function FilterBarUI(props: Props) { @@ -128,6 +132,9 @@ function FilterBarUI(props: Props) { function onAdd(filter: Filter) { setIsAddFilterPopoverOpen(false); + if (props.trackUiMetric) { + props.trackUiMetric(METRIC_TYPE.CLICK, `${props.appName}:filter_added`); + } const filters = [...props.filters, filter]; onFiltersUpdated(filters); } @@ -139,6 +146,9 @@ function FilterBarUI(props: Props) { } function onUpdate(i: number, filter: Filter) { + if (props.trackUiMetric) { + props.trackUiMetric(METRIC_TYPE.CLICK, `${props.appName}:filter_edited`); + } const filters = [...props.filters]; filters[i] = filter; onFiltersUpdated(filters); @@ -165,11 +175,17 @@ function FilterBarUI(props: Props) { } function onToggleAllNegated() { + if (props.trackUiMetric) { + props.trackUiMetric(METRIC_TYPE.CLICK, `${props.appName}:filter_invertInclusion`); + } const filters = props.filters.map(toggleFilterNegated); onFiltersUpdated(filters); } function onToggleAllDisabled() { + if (props.trackUiMetric) { + props.trackUiMetric(METRIC_TYPE.CLICK, `${props.appName}:filter_toggleAllDisabled`); + } const filters = props.filters.map(toggleFilterDisabled); onFiltersUpdated(filters); } diff --git a/src/plugins/data/public/ui/filter_bar/filter_editor/phrase_suggestor.tsx b/src/plugins/data/public/ui/filter_bar/filter_editor/phrase_suggestor.tsx index 719827a98cc63..c420734a43d41 100644 --- a/src/plugins/data/public/ui/filter_bar/filter_editor/phrase_suggestor.tsx +++ b/src/plugins/data/public/ui/filter_bar/filter_editor/phrase_suggestor.tsx @@ -85,6 +85,8 @@ export class PhraseSuggestorUI extends React.Com field, query, signal: this.abortController.signal, + // Show all results in filter bar autocomplete + useTimeRange: false, }); this.setState({ suggestions, isLoading: false }); diff --git a/src/plugins/data/public/ui/search_bar/create_search_bar.tsx b/src/plugins/data/public/ui/search_bar/create_search_bar.tsx index 48e2e8dab7580..f120aae920774 100644 --- a/src/plugins/data/public/ui/search_bar/create_search_bar.tsx +++ b/src/plugins/data/public/ui/search_bar/create_search_bar.tsx @@ -21,6 +21,7 @@ import _ from 'lodash'; import React, { useEffect, useRef } from 'react'; import { CoreStart } from 'src/core/public'; import { IStorageWrapper } from 'src/plugins/kibana_utils/public'; +import { UiStatsMetricType } from '@kbn/analytics'; import { KibanaContextProvider } from '../../../../kibana_react/public'; import { QueryStart, SavedQuery } from '../../query'; import { SearchBar, SearchBarOwnProps } from './'; @@ -35,6 +36,7 @@ interface StatefulSearchBarDeps { core: CoreStart; data: Omit; storage: IStorageWrapper; + trackUiMetric?: (metricType: UiStatsMetricType, eventName: string | string[]) => void; } export type StatefulSearchBarProps = SearchBarOwnProps & { @@ -119,7 +121,7 @@ const overrideDefaultBehaviors = (props: StatefulSearchBarProps) => { return props.useDefaultBehaviors ? {} : props; }; -export function createSearchBar({ core, storage, data }: StatefulSearchBarDeps) { +export function createSearchBar({ core, storage, data, trackUiMetric }: StatefulSearchBarDeps) { // App name should come from the core application service. // Until it's available, we'll ask the user to provide it for the pre-wired component. return (props: StatefulSearchBarProps) => { @@ -197,6 +199,7 @@ export function createSearchBar({ core, storage, data }: StatefulSearchBarDeps) onClearSavedQuery={defaultOnClearSavedQuery(props, clearSavedQuery)} onSavedQueryUpdated={defaultOnSavedQueryUpdated(props, setSavedQuery)} onSaved={defaultOnSavedQueryUpdated(props, setSavedQuery)} + trackUiMetric={trackUiMetric} {...overrideDefaultBehaviors(props)} /> diff --git a/src/plugins/data/public/ui/search_bar/search_bar.tsx b/src/plugins/data/public/ui/search_bar/search_bar.tsx index daa6fa0dd80ab..e77f58f572f33 100644 --- a/src/plugins/data/public/ui/search_bar/search_bar.tsx +++ b/src/plugins/data/public/ui/search_bar/search_bar.tsx @@ -24,6 +24,7 @@ import React, { Component } from 'react'; import ResizeObserver from 'resize-observer-polyfill'; import { get, isEqual } from 'lodash'; +import { METRIC_TYPE, UiStatsMetricType } from '@kbn/analytics'; import { withKibana, KibanaReactContextValue } from '../../../../kibana_react/public'; import QueryBarTopRow from '../query_string_input/query_bar_top_row'; @@ -78,6 +79,8 @@ export interface SearchBarOwnProps { onRefresh?: (payload: { dateRange: TimeRange }) => void; indicateNoData?: boolean; + // Track UI Metrics + trackUiMetric?: (metricType: UiStatsMetricType, eventName: string | string[]) => void; } export type SearchBarProps = SearchBarOwnProps & SearchBarInjectedDeps; @@ -331,6 +334,9 @@ class SearchBarUI extends Component { }, }); } + if (this.props.trackUiMetric) { + this.props.trackUiMetric(METRIC_TYPE.CLICK, `${this.services.appName}:query_submitted`); + } } ); }; @@ -432,6 +438,8 @@ class SearchBarUI extends Component { filters={this.props.filters!} onFiltersUpdated={this.props.onFiltersUpdated} indexPatterns={this.props.indexPatterns!} + appName={this.services.appName} + trackUiMetric={this.props.trackUiMetric} /> diff --git a/src/plugins/data/server/autocomplete/value_suggestions_route.ts b/src/plugins/data/server/autocomplete/value_suggestions_route.ts index 6a5b7d1d5b414..89ee0995f4140 100644 --- a/src/plugins/data/server/autocomplete/value_suggestions_route.ts +++ b/src/plugins/data/server/autocomplete/value_suggestions_route.ts @@ -45,7 +45,7 @@ export function registerValueSuggestionsRoute( { field: schema.string(), query: schema.string(), - boolFilter: schema.maybe(schema.any()), + filters: schema.maybe(schema.any()), }, { unknowns: 'allow' } ), @@ -53,7 +53,7 @@ export function registerValueSuggestionsRoute( }, async (context, request, response) => { const config = await config$.pipe(first()).toPromise(); - const { field: fieldName, query, boolFilter } = request.body; + const { field: fieldName, query, filters } = request.body; const { index } = request.params; const { client } = context.core.elasticsearch.legacy; const signal = getRequestAbortedSignal(request.events.aborted$); @@ -66,7 +66,7 @@ export function registerValueSuggestionsRoute( const indexPattern = await findIndexPatternById(context.core.savedObjects.client, index); const field = indexPattern && getFieldByName(fieldName, indexPattern); - const body = await getBody(autocompleteSearchOptions, field || fieldName, query, boolFilter); + const body = await getBody(autocompleteSearchOptions, field || fieldName, query, filters); try { const result = await client.callAsCurrentUser('search', { index, body }, { signal }); @@ -88,7 +88,7 @@ async function getBody( { timeout, terminate_after }: Record, field: IFieldType | string, query: string, - boolFilter: Filter[] = [] + filters: Filter[] = [] ) { const isFieldObject = (f: any): f is IFieldType => Boolean(f && f.name); @@ -108,7 +108,7 @@ async function getBody( terminate_after, query: { bool: { - filter: boolFilter, + filter: filters, }, }, aggs: { diff --git a/src/plugins/data/server/index.ts b/src/plugins/data/server/index.ts index 9a9b8b67730cc..e24869f5237ea 100644 --- a/src/plugins/data/server/index.ts +++ b/src/plugins/data/server/index.ts @@ -134,6 +134,8 @@ export { FieldDescriptor as IndexPatternFieldDescriptor, shouldReadFieldFromDocValues, // used only in logstash_fields fixture FieldDescriptor, + mergeCapabilitiesWithFields, + getCapabilitiesForRollupIndices, } from './index_patterns'; export { @@ -206,6 +208,12 @@ export { OptionedParamType, OptionedValueProp, ParsedInterval, + // expressions + ExecutionContextSearch, + ExpressionFunctionKibana, + ExpressionFunctionKibanaContext, + ExpressionValueSearchContext, + KibanaContext, // search ISearchOptions, IEsSearchRequest, diff --git a/src/plugins/data/server/index_patterns/fetcher/index.ts b/src/plugins/data/server/index_patterns/fetcher/index.ts index 19306696885db..5d8ef62df9627 100644 --- a/src/plugins/data/server/index_patterns/fetcher/index.ts +++ b/src/plugins/data/server/index_patterns/fetcher/index.ts @@ -18,4 +18,8 @@ */ export * from './index_patterns_fetcher'; -export { shouldReadFieldFromDocValues } from './lib'; +export { + shouldReadFieldFromDocValues, + mergeCapabilitiesWithFields, + getCapabilitiesForRollupIndices, +} from './lib'; diff --git a/src/plugins/data/server/index_patterns/fetcher/index_patterns_fetcher.ts b/src/plugins/data/server/index_patterns/fetcher/index_patterns_fetcher.ts index e75b8761984ec..24dad39088b8f 100644 --- a/src/plugins/data/server/index_patterns/fetcher/index_patterns_fetcher.ts +++ b/src/plugins/data/server/index_patterns/fetcher/index_patterns_fetcher.ts @@ -18,8 +18,15 @@ */ import { ElasticsearchClient } from 'kibana/server'; +import { keyBy } from 'lodash'; -import { getFieldCapabilities, resolveTimePattern, createNoMatchingIndicesError } from './lib'; +import { + getFieldCapabilities, + resolveTimePattern, + createNoMatchingIndicesError, + getCapabilitiesForRollupIndices, + mergeCapabilitiesWithFields, +} from './lib'; export interface FieldDescriptor { aggregatable: boolean; @@ -58,11 +65,44 @@ export class IndexPatternsFetcher { pattern: string | string[]; metaFields?: string[]; fieldCapsOptions?: { allow_no_indices: boolean }; + type?: string; + rollupIndex?: string; }): Promise { - const { pattern, metaFields, fieldCapsOptions } = options; - return await getFieldCapabilities(this.elasticsearchClient, pattern, metaFields, { - allow_no_indices: fieldCapsOptions ? fieldCapsOptions.allow_no_indices : this.allowNoIndices, - }); + const { pattern, metaFields, fieldCapsOptions, type, rollupIndex } = options; + const fieldCapsResponse = await getFieldCapabilities( + this.elasticsearchClient, + pattern, + metaFields, + { + allow_no_indices: fieldCapsOptions + ? fieldCapsOptions.allow_no_indices + : this.allowNoIndices, + } + ); + if (type === 'rollup' && rollupIndex) { + const rollupFields: FieldDescriptor[] = []; + const rollupIndexCapabilities = getCapabilitiesForRollupIndices( + ( + await this.elasticsearchClient.rollup.getRollupIndexCaps({ + index: rollupIndex, + }) + ).body + )[rollupIndex].aggs; + const fieldCapsResponseObj = keyBy(fieldCapsResponse, 'name'); + + // Keep meta fields + metaFields!.forEach( + (field: string) => + fieldCapsResponseObj[field] && rollupFields.push(fieldCapsResponseObj[field]) + ); + + return mergeCapabilitiesWithFields( + rollupIndexCapabilities, + fieldCapsResponseObj, + rollupFields + ); + } + return fieldCapsResponse; } /** diff --git a/src/legacy/server/i18n/constants.ts b/src/plugins/data/server/index_patterns/fetcher/lib/__tests__/fixtures/index.js similarity index 95% rename from src/legacy/server/i18n/constants.ts rename to src/plugins/data/server/index_patterns/fetcher/lib/__tests__/fixtures/index.js index a7a410dbcb5b3..d675702ae54e9 100644 --- a/src/legacy/server/i18n/constants.ts +++ b/src/plugins/data/server/index_patterns/fetcher/lib/__tests__/fixtures/index.js @@ -17,4 +17,4 @@ * under the License. */ -export const I18N_RC = '.i18nrc.json'; +export { jobs } from './jobs'; diff --git a/x-pack/plugins/rollup/server/lib/__tests__/fixtures/jobs.js b/src/plugins/data/server/index_patterns/fetcher/lib/__tests__/fixtures/jobs.js similarity index 65% rename from x-pack/plugins/rollup/server/lib/__tests__/fixtures/jobs.js rename to src/plugins/data/server/index_patterns/fetcher/lib/__tests__/fixtures/jobs.js index c03b7c33abe0a..39ebd9595eeaf 100644 --- a/x-pack/plugins/rollup/server/lib/__tests__/fixtures/jobs.js +++ b/src/plugins/data/server/index_patterns/fetcher/lib/__tests__/fixtures/jobs.js @@ -1,7 +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. + * 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 const jobs = [ diff --git a/x-pack/plugins/rollup/server/lib/__tests__/jobs_compatibility.js b/src/plugins/data/server/index_patterns/fetcher/lib/__tests__/jobs_compatibility.js similarity index 81% rename from x-pack/plugins/rollup/server/lib/__tests__/jobs_compatibility.js rename to src/plugins/data/server/index_patterns/fetcher/lib/__tests__/jobs_compatibility.js index a67f67de859f5..e3c93ac1f8616 100644 --- a/x-pack/plugins/rollup/server/lib/__tests__/jobs_compatibility.js +++ b/src/plugins/data/server/index_patterns/fetcher/lib/__tests__/jobs_compatibility.js @@ -1,8 +1,22 @@ /* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. + * 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 expect from '@kbn/expect'; import { areJobsCompatible, mergeJobConfigurations } from '../jobs_compatibility'; import { jobs } from './fixtures'; diff --git a/src/plugins/data/server/index_patterns/fetcher/lib/index.ts b/src/plugins/data/server/index_patterns/fetcher/lib/index.ts index 20e74d2b1a579..b2fd3a1a09a25 100644 --- a/src/plugins/data/server/index_patterns/fetcher/lib/index.ts +++ b/src/plugins/data/server/index_patterns/fetcher/lib/index.ts @@ -20,3 +20,5 @@ export { getFieldCapabilities, shouldReadFieldFromDocValues } from './field_capabilities'; export { resolveTimePattern } from './resolve_time_pattern'; export { createNoMatchingIndicesError } from './errors'; +export * from './merge_capabilities_with_fields'; +export * from './map_capabilities'; diff --git a/x-pack/plugins/rollup/server/lib/jobs_compatibility.ts b/src/plugins/data/server/index_patterns/fetcher/lib/jobs_compatibility.ts similarity index 79% rename from x-pack/plugins/rollup/server/lib/jobs_compatibility.ts rename to src/plugins/data/server/index_patterns/fetcher/lib/jobs_compatibility.ts index f5f54cf9a54e8..f21de8907ee24 100644 --- a/x-pack/plugins/rollup/server/lib/jobs_compatibility.ts +++ b/src/plugins/data/server/index_patterns/fetcher/lib/jobs_compatibility.ts @@ -1,7 +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. + * 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 { isEqual } from 'lodash'; diff --git a/src/plugins/data/server/index_patterns/fetcher/lib/map_capabilities.ts b/src/plugins/data/server/index_patterns/fetcher/lib/map_capabilities.ts new file mode 100644 index 0000000000000..6187174834012 --- /dev/null +++ b/src/plugins/data/server/index_patterns/fetcher/lib/map_capabilities.ts @@ -0,0 +1,37 @@ +/* + * 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 { mergeJobConfigurations } from './jobs_compatibility'; + +export function getCapabilitiesForRollupIndices(indices: { [key: string]: any }) { + const indexNames = Object.keys(indices); + const capabilities = {} as { [key: string]: any }; + + indexNames.forEach((index) => { + try { + capabilities[index] = mergeJobConfigurations(indices[index].rollup_jobs); + } catch (e) { + capabilities[index] = { + error: e.message, + }; + } + }); + + return capabilities; +} diff --git a/x-pack/plugins/rollup/server/lib/merge_capabilities_with_fields.ts b/src/plugins/data/server/index_patterns/fetcher/lib/merge_capabilities_with_fields.ts similarity index 70% rename from x-pack/plugins/rollup/server/lib/merge_capabilities_with_fields.ts rename to src/plugins/data/server/index_patterns/fetcher/lib/merge_capabilities_with_fields.ts index 51111e9e45d0a..dd69f4b7ff007 100644 --- a/x-pack/plugins/rollup/server/lib/merge_capabilities_with_fields.ts +++ b/src/plugins/data/server/index_patterns/fetcher/lib/merge_capabilities_with_fields.ts @@ -1,20 +1,30 @@ /* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. + * 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. */ // Merge rollup capabilities information with field information -export interface Field { - name?: string; - [key: string]: any; -} +import { FieldDescriptor } from '../index_patterns_fetcher'; export const mergeCapabilitiesWithFields = ( rollupIndexCapabilities: { [key: string]: any }, fieldsFromFieldCapsApi: { [key: string]: any }, - previousFields: Field[] = [] + previousFields: FieldDescriptor[] = [] ) => { const rollupFields = [...previousFields]; const rollupFieldNames: string[] = []; diff --git a/src/plugins/data/server/index_patterns/index.ts b/src/plugins/data/server/index_patterns/index.ts index 683d1c445fd72..3305b1bb9a92f 100644 --- a/src/plugins/data/server/index_patterns/index.ts +++ b/src/plugins/data/server/index_patterns/index.ts @@ -17,5 +17,11 @@ * under the License. */ export * from './utils'; -export { IndexPatternsFetcher, FieldDescriptor, shouldReadFieldFromDocValues } from './fetcher'; +export { + IndexPatternsFetcher, + FieldDescriptor, + shouldReadFieldFromDocValues, + mergeCapabilitiesWithFields, + getCapabilitiesForRollupIndices, +} from './fetcher'; export { IndexPatternsService, IndexPatternsServiceStart } from './index_patterns_service'; diff --git a/src/plugins/data/server/index_patterns/index_patterns_api_client.ts b/src/plugins/data/server/index_patterns/index_patterns_api_client.ts index 2dc6f40c5a6f1..21a3bf6e73e61 100644 --- a/src/plugins/data/server/index_patterns/index_patterns_api_client.ts +++ b/src/plugins/data/server/index_patterns/index_patterns_api_client.ts @@ -17,13 +17,30 @@ * under the License. */ -import { GetFieldsOptions, IIndexPatternsApiClient } from '../../common/index_patterns/types'; +import { ElasticsearchClient } from 'kibana/server'; +import { + GetFieldsOptions, + IIndexPatternsApiClient, + GetFieldsOptionsTimePattern, +} from '../../common/index_patterns/types'; +import { IndexPatternsFetcher } from './fetcher'; export class IndexPatternsApiServer implements IIndexPatternsApiClient { - async getFieldsForTimePattern(options: GetFieldsOptions = {}) { - throw new Error('IndexPatternsApiServer - getFieldsForTimePattern not defined'); + esClient: ElasticsearchClient; + constructor(elasticsearchClient: ElasticsearchClient) { + this.esClient = elasticsearchClient; } - async getFieldsForWildcard(options: GetFieldsOptions = {}) { - throw new Error('IndexPatternsApiServer - getFieldsForWildcard not defined'); + async getFieldsForWildcard({ pattern, metaFields, type, rollupIndex }: GetFieldsOptions) { + const indexPatterns = new IndexPatternsFetcher(this.esClient); + return await indexPatterns.getFieldsForWildcard({ + pattern, + metaFields, + type, + rollupIndex, + }); + } + async getFieldsForTimePattern(options: GetFieldsOptionsTimePattern) { + const indexPatterns = new IndexPatternsFetcher(this.esClient); + return await indexPatterns.getFieldsForTimePattern(options); } } diff --git a/src/plugins/data/server/index_patterns/index_patterns_service.ts b/src/plugins/data/server/index_patterns/index_patterns_service.ts index d665e3715fa72..af2d4d6a73e0f 100644 --- a/src/plugins/data/server/index_patterns/index_patterns_service.ts +++ b/src/plugins/data/server/index_patterns/index_patterns_service.ts @@ -17,7 +17,14 @@ * under the License. */ -import { CoreSetup, CoreStart, Plugin, Logger, SavedObjectsClientContract } from 'kibana/server'; +import { + CoreSetup, + CoreStart, + Plugin, + Logger, + SavedObjectsClientContract, + ElasticsearchClient, +} from 'kibana/server'; import { registerRoutes } from './routes'; import { indexPatternSavedObjectType } from '../saved_objects'; import { capabilitiesProvider } from './capabilities_provider'; @@ -29,7 +36,8 @@ import { SavedObjectsClientServerToCommon } from './saved_objects_client_wrapper export interface IndexPatternsServiceStart { indexPatternsServiceFactory: ( - savedObjectsClient: SavedObjectsClientContract + savedObjectsClient: SavedObjectsClientContract, + elasticsearchClient: ElasticsearchClient ) => Promise; } @@ -50,14 +58,17 @@ export class IndexPatternsService implements Plugin { + indexPatternsServiceFactory: async ( + savedObjectsClient: SavedObjectsClientContract, + elasticsearchClient: ElasticsearchClient + ) => { const uiSettingsClient = uiSettings.asScopedToClient(savedObjectsClient); const formats = await fieldFormats.fieldFormatServiceFactory(uiSettingsClient); return new IndexPatternsCommonService({ uiSettings: new UiSettingsServerToCommon(uiSettingsClient), savedObjectsClient: new SavedObjectsClientServerToCommon(savedObjectsClient), - apiClient: new IndexPatternsApiServer(), + apiClient: new IndexPatternsApiServer(elasticsearchClient), fieldFormats: formats, onError: (error) => { logger.error(error); diff --git a/src/plugins/data/server/index_patterns/routes.ts b/src/plugins/data/server/index_patterns/routes.ts index 041eb235d01e0..f8af52954fc61 100644 --- a/src/plugins/data/server/index_patterns/routes.ts +++ b/src/plugins/data/server/index_patterns/routes.ts @@ -42,13 +42,15 @@ export function registerRoutes(http: HttpServiceSetup) { meta_fields: schema.oneOf([schema.string(), schema.arrayOf(schema.string())], { defaultValue: [], }), + type: schema.maybe(schema.string()), + rollup_index: schema.maybe(schema.string()), }), }, }, async (context, request, response) => { const { asCurrentUser } = context.core.elasticsearch.client; const indexPatterns = new IndexPatternsFetcher(asCurrentUser); - const { pattern, meta_fields: metaFields } = request.query; + const { pattern, meta_fields: metaFields, type, rollup_index: rollupIndex } = request.query; let parsedFields: string[] = []; try { @@ -61,6 +63,8 @@ export function registerRoutes(http: HttpServiceSetup) { const fields = await indexPatterns.getFieldsForWildcard({ pattern, metaFields: parsedFields, + type, + rollupIndex, }); return response.ok({ diff --git a/src/plugins/data/server/plugin.ts b/src/plugins/data/server/plugin.ts index 88f24b7ca5a70..3ec4e7e64e382 100644 --- a/src/plugins/data/server/plugin.ts +++ b/src/plugins/data/server/plugin.ts @@ -96,7 +96,7 @@ export class DataServerPlugin core.uiSettings.register(getUiSettings()); const searchSetup = this.searchService.setup(core, { - registerFunction: expressions.registerFunction, + expressions, usageCollection, }); diff --git a/src/plugins/data/server/search/aggs/aggs_service.test.ts b/src/plugins/data/server/search/aggs/aggs_service.test.ts index cb4239cc339c4..e58420f6c2f07 100644 --- a/src/plugins/data/server/search/aggs/aggs_service.test.ts +++ b/src/plugins/data/server/search/aggs/aggs_service.test.ts @@ -17,7 +17,7 @@ * under the License. */ -import { KibanaRequest } from 'src/core/server'; +import { KibanaRequest, ElasticsearchClient } from 'src/core/server'; import { coreMock } from '../../../../../core/server/mocks'; import { expressionsPluginMock } from '../../../../../plugins/expressions/server/mocks'; @@ -63,7 +63,8 @@ describe('AggsService - server', () => { expect(start).toHaveProperty('asScopedToClient'); const contract = await start.asScopedToClient( - savedObjects.getScopedClient({} as KibanaRequest) + savedObjects.getScopedClient({} as KibanaRequest), + {} as ElasticsearchClient ); expect(contract).toHaveProperty('calculateAutoTimeExpression'); expect(contract).toHaveProperty('createAggConfigs'); @@ -74,7 +75,10 @@ describe('AggsService - server', () => { service.setup(setupDeps); const start = await service .start(startDeps) - .asScopedToClient(savedObjects.getScopedClient({} as KibanaRequest)); + .asScopedToClient( + savedObjects.getScopedClient({} as KibanaRequest), + {} as ElasticsearchClient + ); expect(start.types.get('terms').name).toBe('terms'); }); @@ -83,7 +87,10 @@ describe('AggsService - server', () => { service.setup(setupDeps); const start = await service .start(startDeps) - .asScopedToClient(savedObjects.getScopedClient({} as KibanaRequest)); + .asScopedToClient( + savedObjects.getScopedClient({} as KibanaRequest), + {} as ElasticsearchClient + ); const aggTypes = getAggTypes(); expect(start.types.getAll().buckets.length).toBe(aggTypes.buckets.length); @@ -103,7 +110,10 @@ describe('AggsService - server', () => { const start = await service .start(startDeps) - .asScopedToClient(savedObjects.getScopedClient({} as KibanaRequest)); + .asScopedToClient( + savedObjects.getScopedClient({} as KibanaRequest), + {} as ElasticsearchClient + ); const aggTypes = getAggTypes(); expect(start.types.getAll().buckets.length).toBe(aggTypes.buckets.length + 1); diff --git a/src/plugins/data/server/search/aggs/aggs_service.ts b/src/plugins/data/server/search/aggs/aggs_service.ts index c805c8af6694c..c23f748b1eeb5 100644 --- a/src/plugins/data/server/search/aggs/aggs_service.ts +++ b/src/plugins/data/server/search/aggs/aggs_service.ts @@ -19,7 +19,11 @@ import { pick } from 'lodash'; -import { UiSettingsServiceStart, SavedObjectsClientContract } from 'src/core/server'; +import { + UiSettingsServiceStart, + SavedObjectsClientContract, + ElasticsearchClient, +} from 'src/core/server'; import { ExpressionsServiceSetup } from 'src/plugins/expressions/common'; import { AggsCommonService, @@ -65,7 +69,10 @@ export class AggsService { public start({ fieldFormats, uiSettings, indexPatterns }: AggsStartDependencies): AggsStart { return { - asScopedToClient: async (savedObjectsClient: SavedObjectsClientContract) => { + asScopedToClient: async ( + savedObjectsClient: SavedObjectsClientContract, + elasticsearchClient: ElasticsearchClient + ) => { const uiSettingsClient = uiSettings.asScopedToClient(savedObjectsClient); const formats = await fieldFormats.fieldFormatServiceFactory(uiSettingsClient); @@ -82,8 +89,9 @@ export class AggsService { types, } = this.aggsCommonService.start({ getConfig, - getIndexPattern: (await indexPatterns.indexPatternsServiceFactory(savedObjectsClient)) - .get, + getIndexPattern: ( + await indexPatterns.indexPatternsServiceFactory(savedObjectsClient, elasticsearchClient) + ).get, isDefaultTimezone, }); diff --git a/src/plugins/data/server/search/aggs/types.ts b/src/plugins/data/server/search/aggs/types.ts index 1b21d948b25d9..2c28c970cbb84 100644 --- a/src/plugins/data/server/search/aggs/types.ts +++ b/src/plugins/data/server/search/aggs/types.ts @@ -17,11 +17,14 @@ * under the License. */ -import { SavedObjectsClientContract } from 'src/core/server'; +import { SavedObjectsClientContract, ElasticsearchClient } from 'src/core/server'; import { AggsCommonSetup, AggsStart as Start } from '../../../common'; export type AggsSetup = AggsCommonSetup; export interface AggsStart { - asScopedToClient: (savedObjectsClient: SavedObjectsClientContract) => Promise; + asScopedToClient: ( + savedObjectsClient: SavedObjectsClientContract, + elasticsearchClient: ElasticsearchClient + ) => Promise; } diff --git a/src/plugins/data/server/search/search_service.test.ts b/src/plugins/data/server/search/search_service.test.ts index 2b513be147e9d..0700afd8d6c83 100644 --- a/src/plugins/data/server/search/search_service.test.ts +++ b/src/plugins/data/server/search/search_service.test.ts @@ -44,7 +44,10 @@ describe('Search service', () => { it('exposes proper contract', async () => { const setup = plugin.setup(mockCoreSetup, ({ packageInfo: { version: '8' }, - registerFunction: jest.fn(), + expressions: { + registerFunction: jest.fn(), + registerType: jest.fn(), + }, } as unknown) as SearchServiceSetupDependencies); expect(setup).toHaveProperty('aggs'); expect(setup).toHaveProperty('registerSearchStrategy'); diff --git a/src/plugins/data/server/search/search_service.ts b/src/plugins/data/server/search/search_service.ts index c500c62914c0b..d8aa588719e3e 100644 --- a/src/plugins/data/server/search/search_service.ts +++ b/src/plugins/data/server/search/search_service.ts @@ -30,6 +30,7 @@ import { StartServicesAccessor, } from 'src/core/server'; import { first } from 'rxjs/operators'; +import { ExpressionsServerSetup } from 'src/plugins/expressions/server'; import { ISearchSetup, ISearchStart, @@ -38,7 +39,7 @@ import { SearchStrategyDependencies, } from './types'; -import { AggsService, AggsSetupDependencies } from './aggs'; +import { AggsService } from './aggs'; import { FieldFormatsStart } from '../field_formats'; import { IndexPatternsServiceStart } from '../index_patterns'; @@ -50,15 +51,18 @@ import { registerUsageCollector } from './collectors/register'; import { usageProvider } from './collectors/usage'; import { searchTelemetry } from '../saved_objects'; import { - IKibanaSearchRequest, - IKibanaSearchResponse, IEsSearchRequest, IEsSearchResponse, + IKibanaSearchRequest, + IKibanaSearchResponse, + ISearchClient, + ISearchOptions, + kibana, + kibanaContext, + kibanaContextFunction, SearchSourceDependencies, - SearchSourceService, searchSourceRequiredUiSettings, - ISearchOptions, - ISearchClient, + SearchSourceService, } from '../../common/search'; import { getShardDelayBucketAgg, @@ -77,7 +81,7 @@ type StrategyMap = Record>; /** @internal */ export interface SearchServiceSetupDependencies { - registerFunction: AggsSetupDependencies['registerFunction']; + expressions: ExpressionsServerSetup; usageCollection?: UsageCollectionSetup; } @@ -106,7 +110,7 @@ export class SearchService implements Plugin { public setup( core: CoreSetup<{}, DataPluginStart>, - { registerFunction, usageCollection }: SearchServiceSetupDependencies + { expressions, usageCollection }: SearchServiceSetupDependencies ): ISearchSetup { const usage = usageCollection ? usageProvider(core) : undefined; @@ -137,7 +141,11 @@ export class SearchService implements Plugin { registerUsageCollector(usageCollection, this.initializerContext); } - const aggs = this.aggsService.setup({ registerFunction }); + expressions.registerFunction(kibana); + expressions.registerFunction(kibanaContextFunction); + expressions.registerType(kibanaContext); + + const aggs = this.aggsService.setup({ registerFunction: expressions.registerFunction }); this.initializerContext.config .create() @@ -146,7 +154,7 @@ export class SearchService implements Plugin { .then((value) => { if (value.search.aggs.shardDelay.enabled) { aggs.types.registerBucket(SHARD_DELAY_AGG_NAME, getShardDelayBucketAgg); - registerFunction(aggShardDelay); + expressions.registerFunction(aggShardDelay); } }); @@ -169,7 +177,11 @@ export class SearchService implements Plugin { const { elasticsearch, savedObjects, uiSettings } = core; const asScoped = this.asScopedProvider(core); return { - aggs: this.aggsService.start({ fieldFormats, uiSettings, indexPatterns }), + aggs: this.aggsService.start({ + fieldFormats, + uiSettings, + indexPatterns, + }), getSearchStrategy: this.getSearchStrategy, asScoped, searchSource: { @@ -177,7 +189,8 @@ export class SearchService implements Plugin { const esClient = elasticsearch.client.asScoped(request); const savedObjectsClient = savedObjects.getScopedClient(request); const scopedIndexPatterns = await indexPatterns.indexPatternsServiceFactory( - savedObjectsClient + savedObjectsClient, + esClient.asCurrentUser ); const uiSettingsClient = uiSettings.asScopedToClient(savedObjectsClient); diff --git a/src/plugins/data/server/server.api.md b/src/plugins/data/server/server.api.md index f62a70c9e4ce1..2984ca336819a 100644 --- a/src/plugins/data/server/server.api.md +++ b/src/plugins/data/server/server.api.md @@ -5,6 +5,7 @@ ```ts import { $Values } from '@kbn/utility-types'; +import { Adapters } from 'src/plugins/inspector/common'; import { ApiResponse } from '@elastic/elasticsearch'; import { Assign } from '@kbn/utility-types'; import { BehaviorSubject } from 'rxjs'; @@ -13,14 +14,19 @@ import { CoreSetup } from 'src/core/server'; import { CoreSetup as CoreSetup_2 } from 'kibana/server'; import { CoreStart } from 'src/core/server'; import { CoreStart as CoreStart_2 } from 'kibana/server'; +import { Datatable } from 'src/plugins/expressions/common'; import { DatatableColumn } from 'src/plugins/expressions'; import { Duration } from 'moment'; -import { ElasticsearchClient } from 'kibana/server'; +import { ElasticsearchClient } from 'src/core/server'; +import { ElasticsearchClient as ElasticsearchClient_2 } from 'kibana/server'; import { Ensure } from '@kbn/utility-types'; import { EnvironmentMode } from '@kbn/config'; import { ErrorToastOptions } from 'src/core/public/notifications'; +import { ExecutionContext } from 'src/plugins/expressions/common'; import { ExpressionAstFunction } from 'src/plugins/expressions/common'; +import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; import { ExpressionsServerSetup } from 'src/plugins/expressions/server'; +import { ExpressionValueBoxed } from 'src/plugins/expressions/common'; import { ISavedObjectsRepository } from 'kibana/server'; import { IScopedClusterClient } from 'src/core/server'; import { ISearchOptions as ISearchOptions_2 } from 'src/plugins/data/public'; @@ -55,7 +61,6 @@ import { ToastInputFields } from 'src/core/public/notifications'; import { Type } from '@kbn/config-schema'; import { TypeOf } from '@kbn/config-schema'; import { Unit } from '@elastic/datemath'; -import { UnwrapPromiseOrReturn } from '@kbn/utility-types'; // Warning: (ae-forgotten-export) The symbol "AggConfigSerialized" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "AggConfigOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -224,7 +229,6 @@ export enum ES_FIELD_TYPES { // @public (undocumented) export const ES_SEARCH_STRATEGY = "es"; -// Warning: (ae-forgotten-export) The symbol "ExpressionFunctionDefinition" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "Input" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "Arguments" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "Output" needs to be exported by the entry point index.d.ts @@ -285,6 +289,31 @@ export interface EsQueryConfig { queryStringOptions: Record; } +// Warning: (ae-missing-release-tag) "ExecutionContextSearch" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export type ExecutionContextSearch = { + filters?: Filter[]; + query?: Query | Query[]; + timeRange?: TimeRange; +}; + +// Warning: (ae-missing-release-tag) "ExpressionFunctionKibana" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export type ExpressionFunctionKibana = ExpressionFunctionDefinition<'kibana', ExpressionValueSearchContext | null, object, ExpressionValueSearchContext, ExecutionContext>; + +// Warning: (ae-forgotten-export) The symbol "Arguments" needs to be exported by the entry point index.d.ts +// Warning: (ae-missing-release-tag) "ExpressionFunctionKibanaContext" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export type ExpressionFunctionKibanaContext = ExpressionFunctionDefinition<'kibana_context', KibanaContext | null, Arguments_2, Promise, ExecutionContext>; + +// Warning: (ae-missing-release-tag) "ExpressionValueSearchContext" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export type ExpressionValueSearchContext = ExpressionValueBoxed<'kibana_context', ExecutionContextSearch>; + // Warning: (ae-missing-release-tag) "FieldDescriptor" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -361,6 +390,15 @@ export type Filter = { query?: any; }; +// Warning: (ae-missing-release-tag) "getCapabilitiesForRollupIndices" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export function getCapabilitiesForRollupIndices(indices: { + [key: string]: any; +}): { + [key: string]: any; +}; + // Warning: (ae-forgotten-export) The symbol "IUiSettingsClient" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "getDefaultSearchParams" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -660,7 +698,7 @@ export const indexPatterns: { // // @public (undocumented) export class IndexPatternsFetcher { - constructor(elasticsearchClient: ElasticsearchClient, allowNoIndices?: boolean); + constructor(elasticsearchClient: ElasticsearchClient_2, allowNoIndices?: boolean); getFieldsForTimePattern(options: { pattern: string; metaFields: string[]; @@ -673,6 +711,8 @@ export class IndexPatternsFetcher { fieldCapsOptions?: { allow_no_indices: boolean; }; + type?: string; + rollupIndex?: string; }): Promise; } @@ -687,7 +727,7 @@ export class IndexPatternsService implements Plugin_3 Promise; + indexPatternsServiceFactory: (savedObjectsClient: SavedObjectsClientContract_2, elasticsearchClient: ElasticsearchClient_2) => Promise; }; } @@ -779,6 +819,11 @@ export enum KBN_FIELD_TYPES { UNKNOWN = "unknown" } +// Warning: (ae-missing-release-tag) "KibanaContext" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export type KibanaContext = ExpressionValueSearchContext; + // Warning: (ae-missing-release-tag) "KueryNode" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -791,6 +836,15 @@ export interface KueryNode { type: keyof NodeTypes; } +// Warning: (ae-missing-release-tag) "mergeCapabilitiesWithFields" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export const mergeCapabilitiesWithFields: (rollupIndexCapabilities: { + [key: string]: any; +}, fieldsFromFieldCapsApi: { + [key: string]: any; +}, previousFields?: FieldDescriptor[]) => FieldDescriptor[]; + // Warning: (ae-missing-release-tag) "METRIC_TYPES" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -885,7 +939,7 @@ export class Plugin implements Plugin_2 void; search: ISearchSetup; fieldFormats: { - register: (customFieldFormat: import("../public").FieldFormatInstanceType) => number; + register: (customFieldFormat: import("../common").FieldFormatInstanceType) => number; }; }; // (undocumented) @@ -894,7 +948,7 @@ export class Plugin implements Plugin_2 Promise; }; indexPatterns: { - indexPatternsServiceFactory: (savedObjectsClient: Pick) => Promise; + indexPatternsServiceFactory: (savedObjectsClient: Pick, elasticsearchClient: import("src/core/server").ElasticsearchClient) => Promise; }; search: ISearchStart>; }; @@ -1127,6 +1181,7 @@ export const UI_SETTINGS: { readonly INDEXPATTERN_PLACEHOLDER: "indexPattern:placeholder"; readonly FILTERS_PINNED_BY_DEFAULT: "filters:pinnedByDefault"; readonly FILTERS_EDITOR_SUGGEST_VALUES: "filterEditor:suggestValues"; + readonly AUTOCOMPLETE_USE_TIMERANGE: "autocomplete:useTimeRange"; }; // Warning: (ae-missing-release-tag) "usageProvider" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -1162,22 +1217,22 @@ export function usageProvider(core: CoreSetup_2): SearchUsage; // src/plugins/data/server/index.ts:101:26 - (ae-forgotten-export) The symbol "TruncateFormat" needs to be exported by the entry point index.d.ts // src/plugins/data/server/index.ts:127:27 - (ae-forgotten-export) The symbol "isFilterable" needs to be exported by the entry point index.d.ts // src/plugins/data/server/index.ts:127:27 - (ae-forgotten-export) The symbol "isNestedField" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:235:20 - (ae-forgotten-export) The symbol "getRequestInspectorStats" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:235:20 - (ae-forgotten-export) The symbol "getResponseInspectorStats" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:235:20 - (ae-forgotten-export) The symbol "tabifyAggResponse" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:235:20 - (ae-forgotten-export) The symbol "tabifyGetColumns" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:250:5 - (ae-forgotten-export) The symbol "getTotalLoaded" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:251:5 - (ae-forgotten-export) The symbol "toSnakeCase" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:255:1 - (ae-forgotten-export) The symbol "CidrMask" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:256:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:265:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:266:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:267:1 - (ae-forgotten-export) The symbol "Ipv4Address" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:271:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:272:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:276:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:279:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index_patterns/index_patterns_service.ts:50:14 - (ae-forgotten-export) The symbol "IndexPatternsService" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:243:20 - (ae-forgotten-export) The symbol "getRequestInspectorStats" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:243:20 - (ae-forgotten-export) The symbol "getResponseInspectorStats" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:243:20 - (ae-forgotten-export) The symbol "tabifyAggResponse" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:243:20 - (ae-forgotten-export) The symbol "tabifyGetColumns" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:258:5 - (ae-forgotten-export) The symbol "getTotalLoaded" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:259:5 - (ae-forgotten-export) The symbol "toSnakeCase" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:263:1 - (ae-forgotten-export) The symbol "CidrMask" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:264:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:273:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:274:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:275:1 - (ae-forgotten-export) The symbol "Ipv4Address" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:279:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:280:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:284:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:287:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index_patterns/index_patterns_service.ts:58:14 - (ae-forgotten-export) The symbol "IndexPatternsService" needs to be exported by the entry point index.d.ts // src/plugins/data/server/plugin.ts:88:66 - (ae-forgotten-export) The symbol "DataEnhancements" needs to be exported by the entry point index.d.ts // src/plugins/data/server/search/types.ts:104:5 - (ae-forgotten-export) The symbol "ISearchStartSearchSource" needs to be exported by the entry point index.d.ts diff --git a/src/plugins/data/server/ui_settings.ts b/src/plugins/data/server/ui_settings.ts index 763a086d7688d..9393700a0e771 100644 --- a/src/plugins/data/server/ui_settings.ts +++ b/src/plugins/data/server/ui_settings.ts @@ -684,5 +684,17 @@ export function getUiSettings(): Record> { }), schema: schema.boolean(), }, + [UI_SETTINGS.AUTOCOMPLETE_USE_TIMERANGE]: { + name: i18n.translate('data.advancedSettings.autocompleteIgnoreTimerange', { + defaultMessage: 'Use time range', + description: 'Restrict autocomplete results to the current time range', + }), + value: true, + description: i18n.translate('data.advancedSettings.autocompleteIgnoreTimerangeText', { + defaultMessage: + 'Disable this property to get autocomplete suggestions from your full dataset, rather than from the current time range.', + }), + schema: schema.boolean(), + }, }; } diff --git a/src/plugins/discover/public/application/angular/discover.js b/src/plugins/discover/public/application/angular/discover.js index 389eda90e00a1..9319c58db3e33 100644 --- a/src/plugins/discover/public/application/angular/discover.js +++ b/src/plugins/discover/public/application/angular/discover.js @@ -65,6 +65,7 @@ const { timefilter, toastNotifications, uiSettings: config, + trackUiMetric, } = getServices(); import { getRootBreadcrumbs, getSavedSearchBreadcrumbs } from '../helpers/breadcrumbs'; @@ -81,6 +82,9 @@ import { DOC_HIDE_TIME_COLUMN_SETTING, MODIFY_COLUMNS_ON_SWITCH, } from '../../../common'; +import { METRIC_TYPE } from '@kbn/analytics'; +import { SEARCH_SESSION_ID_QUERY_PARAM } from '../../url_generator'; +import { removeQueryParam, getQueryParams } from '../../../../kibana_utils/public'; const fetchStatuses = { UNINITIALIZED: 'uninitialized', @@ -89,6 +93,9 @@ const fetchStatuses = { ERROR: 'error', }; +const getSearchSessionIdFromURL = (history) => + getQueryParams(history.location)[SEARCH_SESSION_ID_QUERY_PARAM]; + const app = getAngularModule(); app.config(($routeProvider) => { @@ -206,6 +213,8 @@ function discoverController($element, $route, $scope, $timeout, $window, Promise }; const history = getHistory(); + // used for restoring background session + let isInitialSearch = true; const { appStateContainer, @@ -796,17 +805,30 @@ function discoverController($element, $route, $scope, $timeout, $window, Promise if (abortController) abortController.abort(); abortController = new AbortController(); - const sessionId = data.search.session.start(); + const searchSessionId = (() => { + const searchSessionIdFromURL = getSearchSessionIdFromURL(history); + if (searchSessionIdFromURL) { + if (isInitialSearch) { + data.search.session.restore(searchSessionIdFromURL); + isInitialSearch = false; + return searchSessionIdFromURL; + } else { + // navigating away from background search + removeQueryParam(history, SEARCH_SESSION_ID_QUERY_PARAM); + } + } + return data.search.session.start(); + })(); $scope .updateDataSource() .then(setupVisualization) .then(function () { $scope.fetchStatus = fetchStatuses.LOADING; - logInspectorRequest(); + logInspectorRequest({ searchSessionId }); return $scope.searchSource.fetch({ abortSignal: abortController.signal, - sessionId, + sessionId: searchSessionId, }); }) .then(onResults) @@ -898,7 +920,7 @@ function discoverController($element, $route, $scope, $timeout, $window, Promise $scope.fetchStatus = fetchStatuses.COMPLETE; } - function logInspectorRequest() { + function logInspectorRequest({ searchSessionId = null } = { searchSessionId: null }) { inspectorAdapters.requests.reset(); const title = i18n.translate('discover.inspectorRequestDataTitle', { defaultMessage: 'data', @@ -906,7 +928,7 @@ function discoverController($element, $route, $scope, $timeout, $window, Promise const description = i18n.translate('discover.inspectorRequestDescription', { defaultMessage: 'This request queries Elasticsearch to fetch the data for the search.', }); - inspectorRequest = inspectorAdapters.requests.start(title, { description }); + inspectorRequest = inspectorAdapters.requests.start(title, { description, searchSessionId }); inspectorRequest.stats(getRequestInspectorStats($scope.searchSource)); $scope.searchSource.getSearchRequestBody().then((body) => { inspectorRequest.json(body); @@ -990,6 +1012,9 @@ function discoverController($element, $route, $scope, $timeout, $window, Promise operation, $scope.indexPattern.id ); + if (trackUiMetric) { + trackUiMetric(METRIC_TYPE.CLICK, 'filter_added'); + } return filterManager.addFilters(newFilters); }; diff --git a/src/plugins/discover/public/url_generator.test.ts b/src/plugins/discover/public/url_generator.test.ts index a18ee486ab007..98b7625e63c72 100644 --- a/src/plugins/discover/public/url_generator.test.ts +++ b/src/plugins/discover/public/url_generator.test.ts @@ -212,6 +212,15 @@ describe('Discover url generator', () => { }); }); + test('can specify a search session id', async () => { + const { generator } = await setup(); + const url = await generator.createUrl({ + searchSessionId: '__test__', + }); + expect(url).toMatchInlineSnapshot(`"xyz/app/discover#/?_g=()&_a=()&searchSessionId=__test__"`); + expect(url).toContain('__test__'); + }); + describe('useHash property', () => { describe('when default useHash is set to false', () => { test('when using default, sets index pattern ID in the generated URL', async () => { diff --git a/src/plugins/discover/public/url_generator.ts b/src/plugins/discover/public/url_generator.ts index c7f2e2147e819..df9b16a4627ec 100644 --- a/src/plugins/discover/public/url_generator.ts +++ b/src/plugins/discover/public/url_generator.ts @@ -67,6 +67,11 @@ export interface DiscoverUrlGeneratorState { * whether to hash the data in the url to avoid url length issues. */ useHash?: boolean; + + /** + * Background search session id + */ + searchSessionId?: string; } interface Params { @@ -74,6 +79,8 @@ interface Params { useHash: boolean; } +export const SEARCH_SESSION_ID_QUERY_PARAM = 'searchSessionId'; + export class DiscoverUrlGenerator implements UrlGeneratorsDefinition { constructor(private readonly params: Params) {} @@ -88,6 +95,7 @@ export class DiscoverUrlGenerator savedSearchId, timeRange, useHash = this.params.useHash, + searchSessionId, }: DiscoverUrlGeneratorState): Promise => { const savedSearchPath = savedSearchId ? encodeURIComponent(savedSearchId) : ''; const appState: { @@ -111,6 +119,10 @@ export class DiscoverUrlGenerator url = setStateToKbnUrl('_g', queryState, { useHash }, url); url = setStateToKbnUrl('_a', appState, { useHash }, url); + if (searchSessionId) { + url = `${url}&${SEARCH_SESSION_ID_QUERY_PARAM}=${searchSessionId}`; + } + return url; }; } diff --git a/src/plugins/embeddable/public/public.api.md b/src/plugins/embeddable/public/public.api.md index 4406dded98547..6a2565edf2f67 100644 --- a/src/plugins/embeddable/public/public.api.md +++ b/src/plugins/embeddable/public/public.api.md @@ -80,6 +80,7 @@ import { TransportRequestParams } from '@elastic/elasticsearch/lib/Transport'; import { TransportRequestPromise } from '@elastic/elasticsearch/lib/Transport'; import { TypeOf } from '@kbn/config-schema'; import { UiComponent } from 'src/plugins/kibana_utils/public'; +import { UiStatsMetricType } from '@kbn/analytics'; import { UnregisterCallback } from 'history'; import { UserProvidedValues } from 'src/core/server/types'; diff --git a/src/plugins/es_ui_shared/public/url/attempt_to_uri_decode.test.ts b/src/plugins/es_ui_shared/public/url/attempt_to_uri_decode.test.ts index 15750c7667800..6654611faa18b 100644 --- a/src/plugins/es_ui_shared/public/url/attempt_to_uri_decode.test.ts +++ b/src/plugins/es_ui_shared/public/url/attempt_to_uri_decode.test.ts @@ -19,14 +19,51 @@ import { attemptToURIDecode } from './attempt_to_uri_decode'; +// this function doesn't work for % with other special chars or sequence %25 +// known issue https://github.com/elastic/kibana/issues/82440 test('decodes an encoded string', () => { - const encodedString = 'test%3F'; - expect(attemptToURIDecode(encodedString)).toBe('test?'); + const originalName = 'test;,/?:@&=+$#'; + const encodedName = encodeURIComponent(originalName); + // react router v5 automatically decodes route match params + const reactRouterDecoded = decodeURI(encodedName); + + expect(attemptToURIDecode(encodedName)).toBe(originalName); + expect(attemptToURIDecode(reactRouterDecoded)).toBe(originalName); }); -// react router partially decodes %25 sequence to % in match params -// https://github.com/elastic/kibana/pull/81664 test('ignores the error if a string is already decoded', () => { - const decodedString = 'test%'; - expect(attemptToURIDecode(decodedString)).toBe(decodedString); + const originalName = 'test%'; + + const encodedName = encodeURIComponent(originalName); + // react router v5 automatically decodes route match params + const reactRouterDecoded = decodeURI(encodedName); + + expect(attemptToURIDecode(encodedName)).toBe(originalName); + expect(attemptToURIDecode(reactRouterDecoded)).toBe(originalName); +}); + +test('returns wrong decoded value for %25 sequence', () => { + const originalName = 'test%25'; + + const encodedName = encodeURIComponent(originalName); + // react router v5 automatically decodes route match params + const reactRouterDecoded = decodeURI(encodedName); + + expect(attemptToURIDecode(encodedName)).toBe(originalName); + expect(attemptToURIDecode(reactRouterDecoded)).not.toBe(originalName); +}); + +test('returns wrong decoded value for % with other escaped characters', () => { + const originalName = 'test%?#'; + + const encodedName = encodeURIComponent(originalName); + // react router v5 automatically decodes route match params + const reactRouterDecoded = decodeURI(encodedName); + + expect(attemptToURIDecode(encodedName)).toBe(originalName); + expect(attemptToURIDecode(reactRouterDecoded)).not.toBe(originalName); +}); + +test("doesn't convert undefined to a string", () => { + expect(attemptToURIDecode(undefined)).toBeUndefined(); }); diff --git a/src/plugins/es_ui_shared/public/url/attempt_to_uri_decode.ts b/src/plugins/es_ui_shared/public/url/attempt_to_uri_decode.ts index 65444b83f77bb..37e4761106e44 100644 --- a/src/plugins/es_ui_shared/public/url/attempt_to_uri_decode.ts +++ b/src/plugins/es_ui_shared/public/url/attempt_to_uri_decode.ts @@ -19,12 +19,14 @@ /* * Use this function with any match params coming from react router to safely decode values. - * https://github.com/elastic/kibana/pull/81664 + * After an update to react router v6, this functions should be deprecated. + * Known issue for navigation with special characters in paths + * https://github.com/elastic/kibana/issues/82440 */ -export const attemptToURIDecode = (value: string) => { +export const attemptToURIDecode = (value?: string): string | undefined => { let result = value; try { - result = decodeURIComponent(value); + result = value ? decodeURIComponent(value) : value; } catch (e) { // do nothing } diff --git a/src/plugins/expressions/common/execution/execution.ts b/src/plugins/expressions/common/execution/execution.ts index ba115a7538604..50a469c338d73 100644 --- a/src/plugins/expressions/common/execution/execution.ts +++ b/src/plugins/expressions/common/execution/execution.ts @@ -22,8 +22,7 @@ import { keys, last, mapValues, reduce, zipObject } from 'lodash'; import { Executor } from '../executor'; import { createExecutionContainer, ExecutionContainer } from './container'; import { createError } from '../util'; -import { Defer, now } from '../../../kibana_utils/common'; -import { toPromise } from '../../../data/common/utils/abort_utils'; +import { abortSignalToPromise, Defer, now } from '../../../kibana_utils/common'; import { RequestAdapter, DataAdapter, Adapters } from '../../../inspector/common'; import { isExpressionValueError, ExpressionValueError } from '../expression_types/specs/error'; import { @@ -93,7 +92,7 @@ export class Execution< /** * Promise that rejects if/when abort controller sends "abort" signal. */ - private readonly abortRejection = toPromise(this.abortController.signal); + private readonly abortRejection = abortSignalToPromise(this.abortController.signal); /** * Races a given promise against the "abort" event of `abortController`. diff --git a/src/plugins/expressions/common/execution/types.ts b/src/plugins/expressions/common/execution/types.ts index 50475c3bd94ae..abe3e08fc20c2 100644 --- a/src/plugins/expressions/common/execution/types.ts +++ b/src/plugins/expressions/common/execution/types.ts @@ -17,16 +17,18 @@ * under the License. */ -import { ExpressionType } from '../expression_types'; +import { ExpressionType, SerializableState } from '../expression_types'; import { Adapters, DataAdapter, RequestAdapter } from '../../../inspector/common'; -import { TimeRange, Query, Filter } from '../../../data/common'; import { SavedObject, SavedObjectAttributes } from '../../../../core/public'; /** * `ExecutionContext` is an object available to all functions during a single execution; * it provides various methods to perform side-effects. */ -export interface ExecutionContext { +export interface ExecutionContext< + InspectorAdapters extends Adapters = Adapters, + ExecutionContextSearch extends SerializableState = SerializableState +> { /** * Get search context of the expression. */ @@ -79,9 +81,3 @@ export interface DefaultInspectorAdapters extends Adapters { requests: RequestAdapter; data: DataAdapter; } - -export interface ExecutionContextSearch { - filters?: Filter[]; - query?: Query | Query[]; - timeRange?: TimeRange; -} diff --git a/src/plugins/expressions/common/expression_functions/index.ts b/src/plugins/expressions/common/expression_functions/index.ts index b29e6b78b8f4d..094fbe83efd22 100644 --- a/src/plugins/expressions/common/expression_functions/index.ts +++ b/src/plugins/expressions/common/expression_functions/index.ts @@ -22,3 +22,4 @@ export * from './arguments'; export * from './expression_function_parameter'; export * from './expression_function'; export * from './specs'; +export * from './series_calculation_helpers'; diff --git a/src/plugins/expressions/common/expression_functions/specs/series_calculation_helpers.ts b/src/plugins/expressions/common/expression_functions/series_calculation_helpers.ts similarity index 97% rename from src/plugins/expressions/common/expression_functions/specs/series_calculation_helpers.ts rename to src/plugins/expressions/common/expression_functions/series_calculation_helpers.ts index 8ba9d527d4c59..99ad2098ab10a 100644 --- a/src/plugins/expressions/common/expression_functions/specs/series_calculation_helpers.ts +++ b/src/plugins/expressions/common/expression_functions/series_calculation_helpers.ts @@ -18,7 +18,7 @@ */ import { i18n } from '@kbn/i18n'; -import { Datatable, DatatableRow } from '../../expression_types'; +import { Datatable, DatatableRow } from '../expression_types'; /** * Returns a string identifying the group of a row by a list of columns to group by diff --git a/src/plugins/expressions/common/expression_functions/specs/cumulative_sum.ts b/src/plugins/expressions/common/expression_functions/specs/cumulative_sum.ts index 672abadd3c016..0d9547f70dd3b 100644 --- a/src/plugins/expressions/common/expression_functions/specs/cumulative_sum.ts +++ b/src/plugins/expressions/common/expression_functions/specs/cumulative_sum.ts @@ -20,7 +20,7 @@ import { i18n } from '@kbn/i18n'; import { ExpressionFunctionDefinition } from '../types'; import { Datatable } from '../../expression_types'; -import { buildResultColumns, getBucketIdentifier } from './series_calculation_helpers'; +import { buildResultColumns, getBucketIdentifier } from '../series_calculation_helpers'; export interface CumulativeSumArgs { by?: string[]; diff --git a/src/plugins/expressions/common/expression_functions/specs/derivative.ts b/src/plugins/expressions/common/expression_functions/specs/derivative.ts index 44ac198e2d17c..320a254bf94a9 100644 --- a/src/plugins/expressions/common/expression_functions/specs/derivative.ts +++ b/src/plugins/expressions/common/expression_functions/specs/derivative.ts @@ -20,7 +20,7 @@ import { i18n } from '@kbn/i18n'; import { ExpressionFunctionDefinition } from '../types'; import { Datatable } from '../../expression_types'; -import { buildResultColumns, getBucketIdentifier } from './series_calculation_helpers'; +import { buildResultColumns, getBucketIdentifier } from '../series_calculation_helpers'; export interface DerivativeArgs { by?: string[]; diff --git a/src/plugins/expressions/common/expression_functions/specs/index.ts b/src/plugins/expressions/common/expression_functions/specs/index.ts index 11706f65dbd32..10ccd014fd7bd 100644 --- a/src/plugins/expressions/common/expression_functions/specs/index.ts +++ b/src/plugins/expressions/common/expression_functions/specs/index.ts @@ -19,8 +19,6 @@ import { clog } from './clog'; import { font } from './font'; -import { kibana } from './kibana'; -import { kibanaContextFunction } from './kibana_context'; import { variableSet } from './var_set'; import { variable } from './var'; import { AnyExpressionFunctionDefinition } from '../types'; @@ -32,8 +30,6 @@ import { movingAverage } from './moving_average'; export const functionSpecs: AnyExpressionFunctionDefinition[] = [ clog, font, - kibana, - kibanaContextFunction, variableSet, variable, theme, @@ -44,8 +40,6 @@ export const functionSpecs: AnyExpressionFunctionDefinition[] = [ export * from './clog'; export * from './font'; -export * from './kibana'; -export * from './kibana_context'; export * from './var_set'; export * from './var'; export * from './theme'; diff --git a/src/plugins/expressions/common/expression_functions/specs/moving_average.ts b/src/plugins/expressions/common/expression_functions/specs/moving_average.ts index 00a4d8c45839e..c4887e0240ec0 100644 --- a/src/plugins/expressions/common/expression_functions/specs/moving_average.ts +++ b/src/plugins/expressions/common/expression_functions/specs/moving_average.ts @@ -20,7 +20,7 @@ import { i18n } from '@kbn/i18n'; import { ExpressionFunctionDefinition } from '../types'; import { Datatable } from '../../expression_types'; -import { buildResultColumns, getBucketIdentifier } from './series_calculation_helpers'; +import { buildResultColumns, getBucketIdentifier } from '../series_calculation_helpers'; export interface MovingAverageArgs { by?: string[]; diff --git a/src/plugins/expressions/common/expression_functions/specs/tests/var.test.ts b/src/plugins/expressions/common/expression_functions/specs/tests/var.test.ts index 762f34e3f5566..efcb5555cc63b 100644 --- a/src/plugins/expressions/common/expression_functions/specs/tests/var.test.ts +++ b/src/plugins/expressions/common/expression_functions/specs/tests/var.test.ts @@ -20,12 +20,11 @@ import { functionWrapper } from './utils'; import { variable } from '../var'; import { ExecutionContext } from '../../../execution/types'; -import { KibanaContext } from '../../../expression_types'; describe('expression_functions', () => { describe('var', () => { const fn = functionWrapper(variable); - let input: Partial; + let input: Partial>; let context: ExecutionContext; beforeEach(() => { diff --git a/src/plugins/expressions/common/expression_functions/specs/tests/var_set.test.ts b/src/plugins/expressions/common/expression_functions/specs/tests/var_set.test.ts index 365ae5b89baea..17d9c4ab98c1a 100644 --- a/src/plugins/expressions/common/expression_functions/specs/tests/var_set.test.ts +++ b/src/plugins/expressions/common/expression_functions/specs/tests/var_set.test.ts @@ -20,12 +20,11 @@ import { functionWrapper } from './utils'; import { variableSet } from '../var_set'; import { ExecutionContext } from '../../../execution/types'; -import { KibanaContext } from '../../../expression_types'; describe('expression_functions', () => { describe('var_set', () => { const fn = functionWrapper(variableSet); - let input: Partial; + let input: Partial>; let context: ExecutionContext; let variables: Record; diff --git a/src/plugins/expressions/common/expression_functions/types.ts b/src/plugins/expressions/common/expression_functions/types.ts index 4a93cfa9211ff..52e4b4d9facf3 100644 --- a/src/plugins/expressions/common/expression_functions/types.ts +++ b/src/plugins/expressions/common/expression_functions/types.ts @@ -24,8 +24,6 @@ import { ExecutionContext } from '../execution/types'; import { ExpressionFunctionClog, ExpressionFunctionFont, - ExpressionFunctionKibanaContext, - ExpressionFunctionKibana, ExpressionFunctionVarSet, ExpressionFunctionVar, ExpressionFunctionTheme, @@ -129,8 +127,6 @@ export type AnyExpressionFunctionDefinition = ExpressionFunctionDefinition< export interface ExpressionFunctionDefinitions { clog: ExpressionFunctionClog; font: ExpressionFunctionFont; - kibana_context: ExpressionFunctionKibanaContext; - kibana: ExpressionFunctionKibana; var_set: ExpressionFunctionVarSet; var: ExpressionFunctionVar; theme: ExpressionFunctionTheme; diff --git a/src/plugins/expressions/common/expression_types/specs/index.ts b/src/plugins/expressions/common/expression_types/specs/index.ts index 00c52a2545cd6..ec8dee1f7fc52 100644 --- a/src/plugins/expressions/common/expression_types/specs/index.ts +++ b/src/plugins/expressions/common/expression_types/specs/index.ts @@ -22,7 +22,6 @@ import { datatable } from './datatable'; import { error } from './error'; import { filter } from './filter'; import { image } from './image'; -import { kibanaContext } from './kibana_context'; import { nullType } from './null'; import { num } from './num'; import { number } from './number'; @@ -40,7 +39,6 @@ export const typeSpecs: AnyExpressionTypeDefinition[] = [ error, filter, image, - kibanaContext, nullType, num, number, @@ -57,7 +55,6 @@ export * from './datatable'; export * from './error'; export * from './filter'; export * from './image'; -export * from './kibana_context'; export * from './null'; export * from './num'; export * from './number'; diff --git a/src/plugins/expressions/common/service/expressions_services.ts b/src/plugins/expressions/common/service/expressions_services.ts index 01289ca1ae57a..c9cc0680360bb 100644 --- a/src/plugins/expressions/common/service/expressions_services.ts +++ b/src/plugins/expressions/common/service/expressions_services.ts @@ -26,7 +26,6 @@ import { AnyExpressionFunctionDefinition } from '../expression_functions'; import { SavedObjectReference } from '../../../../core/types'; import { PersistableStateService, SerializableState } from '../../../kibana_utils/common'; import { Adapters } from '../../../inspector/common/adapters'; -import { ExecutionContextSearch } from '../execution'; /** * The public contract that `ExpressionsService` provides to other plugins @@ -48,7 +47,7 @@ export type ExpressionsServiceSetup = Pick< >; export interface ExpressionExecutionParams { - searchContext?: ExecutionContextSearch; + searchContext?: SerializableState; variables?: Record; diff --git a/src/plugins/expressions/kibana.json b/src/plugins/expressions/kibana.json index 67bbf4b6e5454..23c7fe722fdb3 100644 --- a/src/plugins/expressions/kibana.json +++ b/src/plugins/expressions/kibana.json @@ -6,7 +6,6 @@ "extraPublicDirs": ["common", "common/fonts"], "requiredBundles": [ "kibanaUtils", - "inspector", - "data" + "inspector" ] } diff --git a/src/plugins/expressions/public/index.ts b/src/plugins/expressions/public/index.ts index 893d68238747d..385055bc2fdc2 100644 --- a/src/plugins/expressions/public/index.ts +++ b/src/plugins/expressions/public/index.ts @@ -66,7 +66,6 @@ export { ExpressionFunction, ExpressionFunctionDefinition, ExpressionFunctionDefinitions, - ExpressionFunctionKibana, ExpressionFunctionParameter, ExpressionImage, ExpressionRenderDefinition, @@ -81,7 +80,6 @@ export { ExpressionValueError, ExpressionValueNum, ExpressionValueRender, - ExpressionValueSearchContext, ExpressionValueUnboxed, ExpressionValueFilter, Font, @@ -96,8 +94,6 @@ export { InterpreterErrorType, IRegistry, isExpressionAstBuilder, - KIBANA_CONTEXT_NAME, - KibanaContext, KnownTypeToString, Overflow, parse, diff --git a/src/plugins/expressions/public/plugin.test.ts b/src/plugins/expressions/public/plugin.test.ts index d9dde1f6def68..0b21fe354d3d3 100644 --- a/src/plugins/expressions/public/plugin.test.ts +++ b/src/plugins/expressions/public/plugin.test.ts @@ -50,14 +50,6 @@ describe('ExpressionsPublicPlugin', () => { const bar = await setup.run('var_set name="foo" value="bar" | var name="foo"', null); expect(bar).toBe('bar'); }); - - test('kibana_context function is available', async () => { - const { setup } = await expressionsPluginMock.createPlugin(); - const result = await setup.run('kibana_context', null); - expect(result).toMatchObject({ - type: 'kibana_context', - }); - }); }); }); @@ -81,15 +73,6 @@ describe('ExpressionsPublicPlugin', () => { } `); }); - - test('"kibana" function return value of type "kibana_context"', async () => { - const { doStart } = await expressionsPluginMock.createPlugin(); - const start = await doStart(); - const execution = start.execute('kibana', null); - const result = await execution.getData(); - - expect((result as any).type).toBe('kibana_context'); - }); }); }); }); diff --git a/src/plugins/expressions/public/public.api.md b/src/plugins/expressions/public/public.api.md index 773d61ebe2e28..17f8e6255f6bb 100644 --- a/src/plugins/expressions/public/public.api.md +++ b/src/plugins/expressions/public/public.api.md @@ -130,15 +130,15 @@ export class Execution = StateContainer, ExecutionPureTransitions>; +// Warning: (ae-forgotten-export) The symbol "SerializableState" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "ExecutionContext" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public -export interface ExecutionContext { +export interface ExecutionContext { abortSignal: AbortSignal; // Warning: (ae-forgotten-export) The symbol "SavedObjectAttributes" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "SavedObject" needs to be exported by the entry point index.d.ts getSavedObject?: (type: string, id: string) => Promise>; - // Warning: (ae-forgotten-export) The symbol "ExecutionContextSearch" needs to be exported by the entry point index.d.ts getSearchContext: () => ExecutionContextSearch; getSearchSessionId: () => string | undefined; inspectorAdapters: InspectorAdapters; @@ -396,12 +396,6 @@ export interface ExpressionFunctionDefinitions { // // (undocumented) font: ExpressionFunctionFont; - // (undocumented) - kibana: ExpressionFunctionKibana; - // Warning: (ae-forgotten-export) The symbol "ExpressionFunctionKibanaContext" needs to be exported by the entry point index.d.ts - // - // (undocumented) - kibana_context: ExpressionFunctionKibanaContext; // Warning: (ae-forgotten-export) The symbol "ExpressionFunctionMovingAverage" needs to be exported by the entry point index.d.ts // // (undocumented) @@ -420,11 +414,6 @@ export interface ExpressionFunctionDefinitions { var_set: ExpressionFunctionVarSet; } -// Warning: (ae-missing-release-tag) "ExpressionFunctionKibana" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export type ExpressionFunctionKibana = ExpressionFunctionDefinition<'kibana', ExpressionValueSearchContext | null, object, ExpressionValueSearchContext>; - // Warning: (ae-missing-release-tag) "ExpressionFunctionParameter" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -789,11 +778,6 @@ export { ExpressionValueRender } export { ExpressionValueRender as Render } -// Warning: (ae-missing-release-tag) "ExpressionValueSearchContext" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export type ExpressionValueSearchContext = ExpressionValueBoxed<'kibana_context', ExecutionContextSearch>; - // Warning: (ae-missing-release-tag) "ExpressionValueUnboxed" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -908,7 +892,7 @@ export interface IExpressionLoaderParams { // (undocumented) onRenderError?: RenderErrorHandlerFnType; // (undocumented) - searchContext?: ExecutionContextSearch; + searchContext?: SerializableState_2; // (undocumented) searchSessionId?: string; // (undocumented) @@ -956,16 +940,6 @@ export interface IRegistry { // @public export function isExpressionAstBuilder(val: any): val is ExpressionAstExpressionBuilder; -// Warning: (ae-missing-release-tag) "KIBANA_CONTEXT_NAME" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export type KIBANA_CONTEXT_NAME = 'kibana_context'; - -// Warning: (ae-missing-release-tag) "KibanaContext" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export type KibanaContext = ExpressionValueSearchContext; - // Warning: (ae-missing-release-tag) "KnownTypeToString" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public diff --git a/src/plugins/expressions/public/types/index.ts b/src/plugins/expressions/public/types/index.ts index 1643b5734ef1a..4af36fea169a1 100644 --- a/src/plugins/expressions/public/types/index.ts +++ b/src/plugins/expressions/public/types/index.ts @@ -21,8 +21,8 @@ import { Adapters } from '../../../inspector/public'; import { IInterpreterRenderHandlers, ExpressionValue, - ExecutionContextSearch, ExpressionsService, + SerializableState, } from '../../common'; /** @@ -42,7 +42,7 @@ export interface ExpressionInterpreter { } export interface IExpressionLoaderParams { - searchContext?: ExecutionContextSearch; + searchContext?: SerializableState; context?: ExpressionValue; variables?: Record; // Enables debug tracking on each expression in the AST diff --git a/src/plugins/expressions/server/index.ts b/src/plugins/expressions/server/index.ts index cc22d4b500d97..287b91049b1ae 100644 --- a/src/plugins/expressions/server/index.ts +++ b/src/plugins/expressions/server/index.ts @@ -57,7 +57,6 @@ export { ExpressionFunction, ExpressionFunctionDefinition, ExpressionFunctionDefinitions, - ExpressionFunctionKibana, ExpressionFunctionParameter, ExpressionImage, ExpressionRenderDefinition, @@ -72,7 +71,6 @@ export { ExpressionValueError, ExpressionValueNum, ExpressionValueRender, - ExpressionValueSearchContext, ExpressionValueUnboxed, ExpressionValueFilter, Font, @@ -87,8 +85,6 @@ export { InterpreterErrorType, IRegistry, isExpressionAstBuilder, - KIBANA_CONTEXT_NAME, - KibanaContext, KnownTypeToString, Overflow, parse, diff --git a/src/plugins/expressions/server/server.api.md b/src/plugins/expressions/server/server.api.md index 27a3193bf7894..e5b499206ebdd 100644 --- a/src/plugins/expressions/server/server.api.md +++ b/src/plugins/expressions/server/server.api.md @@ -128,15 +128,15 @@ export class Execution = StateContainer, ExecutionPureTransitions>; +// Warning: (ae-forgotten-export) The symbol "SerializableState" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "ExecutionContext" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public -export interface ExecutionContext { +export interface ExecutionContext { abortSignal: AbortSignal; // Warning: (ae-forgotten-export) The symbol "SavedObjectAttributes" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "SavedObject" needs to be exported by the entry point index.d.ts getSavedObject?: (type: string, id: string) => Promise>; - // Warning: (ae-forgotten-export) The symbol "ExecutionContextSearch" needs to be exported by the entry point index.d.ts getSearchContext: () => ExecutionContextSearch; getSearchSessionId: () => string | undefined; inspectorAdapters: InspectorAdapters; @@ -368,12 +368,6 @@ export interface ExpressionFunctionDefinitions { // // (undocumented) font: ExpressionFunctionFont; - // (undocumented) - kibana: ExpressionFunctionKibana; - // Warning: (ae-forgotten-export) The symbol "ExpressionFunctionKibanaContext" needs to be exported by the entry point index.d.ts - // - // (undocumented) - kibana_context: ExpressionFunctionKibanaContext; // Warning: (ae-forgotten-export) The symbol "ExpressionFunctionMovingAverage" needs to be exported by the entry point index.d.ts // // (undocumented) @@ -392,11 +386,6 @@ export interface ExpressionFunctionDefinitions { var_set: ExpressionFunctionVarSet; } -// Warning: (ae-missing-release-tag) "ExpressionFunctionKibana" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export type ExpressionFunctionKibana = ExpressionFunctionDefinition<'kibana', ExpressionValueSearchContext | null, object, ExpressionValueSearchContext>; - // Warning: (ae-missing-release-tag) "ExpressionFunctionParameter" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -640,11 +629,6 @@ export { ExpressionValueRender } export { ExpressionValueRender as Render } -// Warning: (ae-missing-release-tag) "ExpressionValueSearchContext" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export type ExpressionValueSearchContext = ExpressionValueBoxed<'kibana_context', ExecutionContextSearch>; - // Warning: (ae-missing-release-tag) "ExpressionValueUnboxed" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -777,16 +761,6 @@ export interface IRegistry { // @public export function isExpressionAstBuilder(val: any): val is ExpressionAstExpressionBuilder; -// Warning: (ae-missing-release-tag) "KIBANA_CONTEXT_NAME" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export type KIBANA_CONTEXT_NAME = 'kibana_context'; - -// Warning: (ae-missing-release-tag) "KibanaContext" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export type KibanaContext = ExpressionValueSearchContext; - // Warning: (ae-missing-release-tag) "KnownTypeToString" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public diff --git a/src/plugins/home/public/application/components/home_app.js b/src/plugins/home/public/application/components/home_app.js index 7fe4f4351c35b..734100fe584ab 100644 --- a/src/plugins/home/public/application/components/home_app.js +++ b/src/plugins/home/public/application/components/home_app.js @@ -28,7 +28,7 @@ import { HashRouter as Router, Switch, Route } from 'react-router-dom'; import { getTutorial } from '../load_tutorials'; import { replaceTemplateStrings } from './tutorial/replace_template_strings'; import { getServices } from '../kibana_services'; -import { useMount } from 'react-use'; +import useMount from 'react-use/lib/useMount'; const RedirectToDefaultApp = () => { useMount(() => { diff --git a/src/plugins/kibana_usage_collection/server/__snapshots__/index.test.ts.snap b/src/plugins/kibana_usage_collection/server/__snapshots__/index.test.ts.snap index c479562795512..c782ce9c8cc84 100644 --- a/src/plugins/kibana_usage_collection/server/__snapshots__/index.test.ts.snap +++ b/src/plugins/kibana_usage_collection/server/__snapshots__/index.test.ts.snap @@ -13,3 +13,5 @@ exports[`kibana_usage_collection Runs the setup method without issues 5`] = `fal exports[`kibana_usage_collection Runs the setup method without issues 6`] = `true`; exports[`kibana_usage_collection Runs the setup method without issues 7`] = `false`; + +exports[`kibana_usage_collection Runs the setup method without issues 8`] = `true`; diff --git a/src/plugins/kibana_usage_collection/server/collectors/index.ts b/src/plugins/kibana_usage_collection/server/collectors/index.ts index 2408dc84c2e56..f3b7d8ca5eea0 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/index.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/index.ts @@ -24,3 +24,4 @@ export { registerKibanaUsageCollector } from './kibana'; export { registerOpsStatsCollector } from './ops_stats'; export { registerCspCollector } from './csp'; export { registerCoreUsageCollector } from './core'; +export { registerLocalizationUsageCollector } from './localization'; diff --git a/src/legacy/server/i18n/localization/file_integrity.test.mocks.ts b/src/plugins/kibana_usage_collection/server/collectors/localization/file_integrity.test.mocks.ts similarity index 100% rename from src/legacy/server/i18n/localization/file_integrity.test.mocks.ts rename to src/plugins/kibana_usage_collection/server/collectors/localization/file_integrity.test.mocks.ts diff --git a/src/legacy/server/i18n/localization/file_integrity.test.ts b/src/plugins/kibana_usage_collection/server/collectors/localization/file_integrity.test.ts similarity index 100% rename from src/legacy/server/i18n/localization/file_integrity.test.ts rename to src/plugins/kibana_usage_collection/server/collectors/localization/file_integrity.test.ts diff --git a/src/legacy/server/i18n/localization/file_integrity.ts b/src/plugins/kibana_usage_collection/server/collectors/localization/file_integrity.ts similarity index 100% rename from src/legacy/server/i18n/localization/file_integrity.ts rename to src/plugins/kibana_usage_collection/server/collectors/localization/file_integrity.ts diff --git a/src/legacy/server/i18n/localization/index.ts b/src/plugins/kibana_usage_collection/server/collectors/localization/index.ts similarity index 100% rename from src/legacy/server/i18n/localization/index.ts rename to src/plugins/kibana_usage_collection/server/collectors/localization/index.ts diff --git a/src/legacy/server/i18n/localization/telemetry_localization_collector.test.ts b/src/plugins/kibana_usage_collection/server/collectors/localization/telemetry_localization_collector.test.ts similarity index 100% rename from src/legacy/server/i18n/localization/telemetry_localization_collector.test.ts rename to src/plugins/kibana_usage_collection/server/collectors/localization/telemetry_localization_collector.test.ts diff --git a/src/legacy/server/i18n/localization/telemetry_localization_collector.ts b/src/plugins/kibana_usage_collection/server/collectors/localization/telemetry_localization_collector.ts similarity index 82% rename from src/legacy/server/i18n/localization/telemetry_localization_collector.ts rename to src/plugins/kibana_usage_collection/server/collectors/localization/telemetry_localization_collector.ts index fb837f5ae28df..f2b00bd629a07 100644 --- a/src/legacy/server/i18n/localization/telemetry_localization_collector.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/localization/telemetry_localization_collector.ts @@ -20,6 +20,7 @@ import { i18nLoader } from '@kbn/i18n'; import { size } from 'lodash'; import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; +import { I18nServiceSetup } from '../../../../../core/server'; import { getIntegrityHashes, Integrities } from './file_integrity'; export interface UsageStats { @@ -28,11 +29,6 @@ export interface UsageStats { labelsCount?: number; } -export interface LocalizationUsageCollectorHelpers { - getLocale: () => string; - getTranslationsFilePaths: () => string[]; -} - export async function getTranslationCount( loader: typeof i18nLoader, locale: string @@ -41,13 +37,10 @@ export async function getTranslationCount( return size(translations.messages); } -export function createCollectorFetch({ - getLocale, - getTranslationsFilePaths, -}: LocalizationUsageCollectorHelpers) { +export function createCollectorFetch({ getLocale, getTranslationFiles }: I18nServiceSetup) { return async function fetchUsageStats(): Promise { const locale = getLocale(); - const translationFilePaths: string[] = getTranslationsFilePaths(); + const translationFilePaths: string[] = getTranslationFiles(); const [labelsCount, integrities] = await Promise.all([ getTranslationCount(i18nLoader, locale), @@ -62,15 +55,14 @@ export function createCollectorFetch({ }; } -// TODO: Migrate out of the Legacy dir export function registerLocalizationUsageCollector( usageCollection: UsageCollectionSetup, - helpers: LocalizationUsageCollectorHelpers + i18n: I18nServiceSetup ) { const collector = usageCollection.makeUsageCollector({ type: 'localization', isReady: () => true, - fetch: createCollectorFetch(helpers), + fetch: createCollectorFetch(i18n), schema: { locale: { type: 'keyword' }, integrities: { DYNAMIC_KEY: { type: 'text' } }, diff --git a/src/plugins/kibana_usage_collection/server/plugin.ts b/src/plugins/kibana_usage_collection/server/plugin.ts index 198fdbb7a8703..16cb620351aaa 100644 --- a/src/plugins/kibana_usage_collection/server/plugin.ts +++ b/src/plugins/kibana_usage_collection/server/plugin.ts @@ -41,6 +41,7 @@ import { registerUiMetricUsageCollector, registerCspCollector, registerCoreUsageCollector, + registerLocalizationUsageCollector, } from './collectors'; interface KibanaUsageCollectionPluginsDepsSetup { @@ -104,5 +105,6 @@ export class KibanaUsageCollectionPlugin implements Plugin { ); registerCspCollector(usageCollection, coreSetup.http); registerCoreUsageCollector(usageCollection, getCoreUsageDataService); + registerLocalizationUsageCollector(usageCollection, coreSetup.i18n); } } diff --git a/src/plugins/data/common/utils/abort_utils.test.ts b/src/plugins/kibana_utils/common/abort_utils.test.ts similarity index 84% rename from src/plugins/data/common/utils/abort_utils.test.ts rename to src/plugins/kibana_utils/common/abort_utils.test.ts index 358f00e5e82bd..8ccbf752b4d1b 100644 --- a/src/plugins/data/common/utils/abort_utils.test.ts +++ b/src/plugins/kibana_utils/common/abort_utils.test.ts @@ -17,7 +17,7 @@ * under the License. */ -import { AbortError, toPromise, getCombinedSignal } from './abort_utils'; +import { AbortError, abortSignalToPromise, getCombinedAbortSignal } from './abort_utils'; jest.useFakeTimers(); @@ -37,11 +37,11 @@ describe('AbortUtils', () => { }); }); - describe('toPromise', () => { + describe('abortSignalToPromise', () => { describe('rejects', () => { test('should not reject if the signal does not abort', async () => { const controller = new AbortController(); - const promise = toPromise(controller.signal).promise; + const promise = abortSignalToPromise(controller.signal).promise; const whenRejected = jest.fn(); promise.catch(whenRejected); await flushPromises(); @@ -50,7 +50,7 @@ describe('AbortUtils', () => { test('should reject if the signal does abort', async () => { const controller = new AbortController(); - const promise = toPromise(controller.signal).promise; + const promise = abortSignalToPromise(controller.signal).promise; const whenRejected = jest.fn(); promise.catch(whenRejected); controller.abort(); @@ -61,13 +61,13 @@ describe('AbortUtils', () => { test('should expose cleanup handler', () => { const controller = new AbortController(); - const promise = toPromise(controller.signal); + const promise = abortSignalToPromise(controller.signal); expect(promise.cleanup).toBeDefined(); }); test('calling clean up handler prevents rejects', async () => { const controller = new AbortController(); - const { promise, cleanup } = toPromise(controller.signal); + const { promise, cleanup } = abortSignalToPromise(controller.signal); const whenRejected = jest.fn(); promise.catch(whenRejected); cleanup(); @@ -78,9 +78,9 @@ describe('AbortUtils', () => { }); }); - describe('getCombinedSignal', () => { + describe('getCombinedAbortSignal', () => { test('should return an AbortSignal', () => { - const signal = getCombinedSignal([]).signal; + const signal = getCombinedAbortSignal([]).signal; expect(signal).toBeInstanceOf(AbortSignal); }); @@ -89,7 +89,7 @@ describe('AbortUtils', () => { const controller2 = new AbortController(); setTimeout(() => controller1.abort(), 2000); setTimeout(() => controller2.abort(), 1000); - const signal = getCombinedSignal([controller1.signal, controller2.signal]).signal; + const signal = getCombinedAbortSignal([controller1.signal, controller2.signal]).signal; expect(signal.aborted).toBe(false); jest.advanceTimersByTime(500); await flushPromises(); @@ -101,7 +101,7 @@ describe('AbortUtils', () => { const controller2 = new AbortController(); setTimeout(() => controller1.abort(), 2000); setTimeout(() => controller2.abort(), 1000); - const signal = getCombinedSignal([controller1.signal, controller2.signal]).signal; + const signal = getCombinedAbortSignal([controller1.signal, controller2.signal]).signal; expect(signal.aborted).toBe(false); jest.advanceTimersByTime(1000); await flushPromises(); @@ -112,7 +112,7 @@ describe('AbortUtils', () => { const controller1 = new AbortController(); const controller2 = new AbortController(); controller1.abort(); - const signal = getCombinedSignal([controller1.signal, controller2.signal]).signal; + const signal = getCombinedAbortSignal([controller1.signal, controller2.signal]).signal; expect(signal.aborted).toBe(true); }); @@ -132,7 +132,7 @@ describe('AbortUtils', () => { const controller1 = createMockController(); const controller2 = createMockController(); - const { cleanup } = getCombinedSignal([ + const { cleanup } = getCombinedAbortSignal([ controller1.controller.signal, controller2.controller.signal, ]); @@ -150,7 +150,7 @@ describe('AbortUtils', () => { const controller1 = createMockController(); const controller2 = createMockController(); - getCombinedSignal([controller1.controller.signal, controller2.controller.signal]); + getCombinedAbortSignal([controller1.controller.signal, controller2.controller.signal]); expect(controller1.getTotalListeners()).toBe(1); expect(controller2.getTotalListeners()).toBe(1); diff --git a/src/plugins/data/common/utils/abort_utils.ts b/src/plugins/kibana_utils/common/abort_utils.ts similarity index 91% rename from src/plugins/data/common/utils/abort_utils.ts rename to src/plugins/kibana_utils/common/abort_utils.ts index 81f30b7454c7b..dd8db34931693 100644 --- a/src/plugins/data/common/utils/abort_utils.ts +++ b/src/plugins/kibana_utils/common/abort_utils.ts @@ -36,7 +36,9 @@ export class AbortError extends Error { * * @param signal The `AbortSignal` to generate the `Promise` from */ -export function toPromise(signal: AbortSignal): { promise: Promise; cleanup: () => void } { +export function abortSignalToPromise( + signal: AbortSignal +): { promise: Promise; cleanup: () => void } { let abortHandler: () => void; const cleanup = () => { if (abortHandler) { @@ -60,7 +62,7 @@ export function toPromise(signal: AbortSignal): { promise: Promise; clean * * @param signals */ -export function getCombinedSignal( +export function getCombinedAbortSignal( signals: AbortSignal[] ): { signal: AbortSignal; cleanup: () => void } { const controller = new AbortController(); @@ -69,7 +71,7 @@ export function getCombinedSignal( if (signals.some((signal) => signal.aborted)) { controller.abort(); } else { - const promises = signals.map((signal) => toPromise(signal)); + const promises = signals.map((signal) => abortSignalToPromise(signal)); cleanup = () => { promises.forEach((p) => p.cleanup()); controller.signal.removeEventListener('abort', cleanup); diff --git a/src/plugins/kibana_utils/common/index.ts b/src/plugins/kibana_utils/common/index.ts index e09290c811c7b..a49b7100594ba 100644 --- a/src/plugins/kibana_utils/common/index.ts +++ b/src/plugins/kibana_utils/common/index.ts @@ -24,6 +24,7 @@ export * from './ui'; export * from './state_containers'; export * from './typed_json'; export * from './errors'; +export { AbortError, abortSignalToPromise, getCombinedAbortSignal } from './abort_utils'; export { createGetterSetter, Get, Set } from './create_getter_setter'; export { distinctUntilChangedWithInitialValue } from './distinct_until_changed_with_initial_value'; export { url } from './url'; diff --git a/src/plugins/kibana_utils/public/index.ts b/src/plugins/kibana_utils/public/index.ts index 9ba42d39139da..46a0cc4a10f00 100644 --- a/src/plugins/kibana_utils/public/index.ts +++ b/src/plugins/kibana_utils/public/index.ts @@ -18,12 +18,15 @@ */ export { + AbortError, + abortSignalToPromise, calculateObjectHash, defer, Defer, fieldWildcardFilter, fieldWildcardMatcher, Get, + getCombinedAbortSignal, JsonArray, JsonObject, JsonValue, diff --git a/src/plugins/kibana_utils/server/index.ts b/src/plugins/kibana_utils/server/index.ts index bf3361d1e5369..d994a4940bdfd 100644 --- a/src/plugins/kibana_utils/server/index.ts +++ b/src/plugins/kibana_utils/server/index.ts @@ -18,10 +18,13 @@ */ export { + AbortError, + abortSignalToPromise, createGetterSetter, fieldWildcardFilter, fieldWildcardMatcher, Get, + getCombinedAbortSignal, Set, url, } from '../common'; diff --git a/src/plugins/maps_legacy/common/ems_defaults.ts b/src/plugins/maps_legacy/common/ems_defaults.ts new file mode 100644 index 0000000000000..583dca1dbf036 --- /dev/null +++ b/src/plugins/maps_legacy/common/ems_defaults.ts @@ -0,0 +1,25 @@ +/* + * 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. + */ + +// Default config for the elastic hosted EMS endpoints +export const DEFAULT_EMS_FILE_API_URL = 'https://vector.maps.elastic.co'; +export const DEFAULT_EMS_TILE_API_URL = 'https://tiles.maps.elastic.co'; +export const DEFAULT_EMS_LANDING_PAGE_URL = 'https://maps.elastic.co/v7.10'; +export const DEFAULT_EMS_FONT_LIBRARY_URL = + 'https://tiles.maps.elastic.co/fonts/{fontstack}/{range}.pbf'; diff --git a/src/legacy/server/i18n/index.ts b/src/plugins/maps_legacy/common/index.ts similarity index 95% rename from src/legacy/server/i18n/index.ts rename to src/plugins/maps_legacy/common/index.ts index a7ef49f44532c..12148ec1ec6b8 100644 --- a/src/legacy/server/i18n/index.ts +++ b/src/plugins/maps_legacy/common/index.ts @@ -17,4 +17,4 @@ * under the License. */ -export { i18nMixin } from './i18n_mixin'; +export * from './ems_defaults'; diff --git a/src/plugins/maps_legacy/config.ts b/src/plugins/maps_legacy/config.ts index f49d56dedd45f..68595944e68b3 100644 --- a/src/plugins/maps_legacy/config.ts +++ b/src/plugins/maps_legacy/config.ts @@ -21,18 +21,29 @@ import { schema, TypeOf } from '@kbn/config-schema'; import { configSchema as tilemapSchema } from '../tile_map/config'; import { configSchema as regionmapSchema } from '../region_map/config'; +import { + DEFAULT_EMS_FONT_LIBRARY_URL, + DEFAULT_EMS_LANDING_PAGE_URL, + DEFAULT_EMS_TILE_API_URL, + DEFAULT_EMS_FILE_API_URL, +} from './common/ems_defaults'; + export const configSchema = schema.object({ includeElasticMapsService: schema.boolean({ defaultValue: true }), proxyElasticMapsServiceInMaps: schema.boolean({ defaultValue: false }), tilemap: tilemapSchema, regionmap: regionmapSchema, manifestServiceUrl: schema.string({ defaultValue: '' }), - emsFileApiUrl: schema.string({ defaultValue: 'https://vector.maps.elastic.co' }), - emsTileApiUrl: schema.string({ defaultValue: 'https://tiles.maps.elastic.co' }), - emsLandingPageUrl: schema.string({ defaultValue: 'https://maps.elastic.co/v7.10' }), + + emsUrl: schema.string({ defaultValue: '' }), + + emsFileApiUrl: schema.string({ defaultValue: DEFAULT_EMS_FILE_API_URL }), + emsTileApiUrl: schema.string({ defaultValue: DEFAULT_EMS_TILE_API_URL }), + emsLandingPageUrl: schema.string({ defaultValue: DEFAULT_EMS_LANDING_PAGE_URL }), emsFontLibraryUrl: schema.string({ - defaultValue: 'https://tiles.maps.elastic.co/fonts/{fontstack}/{range}.pbf', + defaultValue: DEFAULT_EMS_FONT_LIBRARY_URL, }), + emsTileLayerId: schema.object({ bright: schema.string({ defaultValue: 'road_map' }), desaturated: schema.string({ defaultValue: 'road_map_desaturated' }), diff --git a/src/plugins/maps_legacy/kibana.json b/src/plugins/maps_legacy/kibana.json index d9bf33e661368..1499b3de446b5 100644 --- a/src/plugins/maps_legacy/kibana.json +++ b/src/plugins/maps_legacy/kibana.json @@ -5,5 +5,6 @@ "configPath": ["map"], "ui": true, "server": true, + "extraPublicDirs": ["common"], "requiredBundles": ["kibanaReact", "charts"] } diff --git a/src/plugins/maps_legacy/public/index.ts b/src/plugins/maps_legacy/public/index.ts index fe5338b890ec8..2654ded907cce 100644 --- a/src/plugins/maps_legacy/public/index.ts +++ b/src/plugins/maps_legacy/public/index.ts @@ -59,6 +59,7 @@ export { mapTooltipProvider, }; +export * from '../common'; export * from './common/types'; export { ORIGIN } from './common/constants/origin'; diff --git a/src/plugins/maps_legacy/public/map/service_settings.js b/src/plugins/maps_legacy/public/map/service_settings.js index 833304378402a..7a00456b89a92 100644 --- a/src/plugins/maps_legacy/public/map/service_settings.js +++ b/src/plugins/maps_legacy/public/map/service_settings.js @@ -128,7 +128,7 @@ export class ServiceSettings { allServices.push(tmsService); } - if (this._mapConfig.includeElasticMapsService) { + if (this._mapConfig.includeElasticMapsService && !this._mapConfig.emsUrl) { const servicesFromManifest = await this._emsClient.getTMSServices(); const strippedServiceFromManifest = await Promise.all( servicesFromManifest diff --git a/src/plugins/maps_legacy/server/index.ts b/src/plugins/maps_legacy/server/index.ts index 665b3b8986ef0..ba37df60f9357 100644 --- a/src/plugins/maps_legacy/server/index.ts +++ b/src/plugins/maps_legacy/server/index.ts @@ -30,6 +30,7 @@ export const config: PluginConfigDescriptor = { tilemap: true, regionmap: true, manifestServiceUrl: true, + emsUrl: true, emsFileApiUrl: true, emsTileApiUrl: true, emsLandingPageUrl: true, diff --git a/src/plugins/telemetry/schema/legacy_plugins.json b/src/plugins/telemetry/schema/legacy_plugins.json index 1a7c0ccb15082..d5b0514b64918 100644 --- a/src/plugins/telemetry/schema/legacy_plugins.json +++ b/src/plugins/telemetry/schema/legacy_plugins.json @@ -1,21 +1,3 @@ { - "properties": { - "localization": { - "properties": { - "locale": { - "type": "keyword" - }, - "integrities": { - "properties": { - "DYNAMIC_KEY": { - "type": "text" - } - } - }, - "labelsCount": { - "type": "long" - } - } - } - } + "properties": {} } diff --git a/src/plugins/telemetry/schema/oss_plugins.json b/src/plugins/telemetry/schema/oss_plugins.json index c840cbe8fc94d..a1eae69ffaed0 100644 --- a/src/plugins/telemetry/schema/oss_plugins.json +++ b/src/plugins/telemetry/schema/oss_plugins.json @@ -1581,6 +1581,23 @@ } } }, + "localization": { + "properties": { + "locale": { + "type": "keyword" + }, + "integrities": { + "properties": { + "DYNAMIC_KEY": { + "type": "text" + } + } + }, + "labelsCount": { + "type": "long" + } + } + }, "stack_management": { "properties": { "visualize:enableLabs": { diff --git a/src/plugins/ui_actions/public/public.api.md b/src/plugins/ui_actions/public/public.api.md index 8393f7480d4e4..3e40c94e116fb 100644 --- a/src/plugins/ui_actions/public/public.api.md +++ b/src/plugins/ui_actions/public/public.api.md @@ -233,7 +233,7 @@ export class UiActionsService { // // (undocumented) protected readonly actions: ActionRegistry; - readonly addTriggerAction: (triggerId: T, action: UiActionsActionDefinition | Action) => void; + readonly addTriggerAction: (triggerId: T, action: UiActionsActionDefinition | Action) => void; // (undocumented) readonly attachAction: (triggerId: T, actionId: string) => void; readonly clear: () => void; @@ -247,21 +247,21 @@ export class UiActionsService { readonly executionService: UiActionsExecutionService; readonly fork: () => UiActionsService; // (undocumented) - readonly getAction: >(id: string) => Action, "" | "ACTION_VISUALIZE_FIELD" | "ACTION_VISUALIZE_GEO_FIELD" | "ACTION_VISUALIZE_LENS_FIELD" | "ACTION_CUSTOMIZE_PANEL" | "ACTION_ADD_PANEL" | "openInspector" | "deletePanel" | "editPanel" | "togglePanel" | "replacePanel" | "clonePanel" | "addToFromLibrary" | "unlinkFromLibrary" | "ACTION_LIBRARY_NOTIFICATION" | "ACTION_GLOBAL_APPLY_FILTER" | "ACTION_SELECT_RANGE" | "ACTION_VALUE_CLICK">; + readonly getAction: >(id: string) => Action, "" | "ACTION_VISUALIZE_FIELD" | "ACTION_VISUALIZE_GEO_FIELD" | "ACTION_VISUALIZE_LENS_FIELD" | "ACTION_GLOBAL_APPLY_FILTER" | "ACTION_SELECT_RANGE" | "ACTION_VALUE_CLICK" | "ACTION_CUSTOMIZE_PANEL" | "ACTION_ADD_PANEL" | "openInspector" | "deletePanel" | "editPanel" | "togglePanel" | "replacePanel" | "clonePanel" | "addToFromLibrary" | "unlinkFromLibrary" | "ACTION_LIBRARY_NOTIFICATION">; // Warning: (ae-forgotten-export) The symbol "TriggerContract" needs to be exported by the entry point index.d.ts // // (undocumented) readonly getTrigger: (triggerId: T) => TriggerContract; // (undocumented) - readonly getTriggerActions: (triggerId: T) => Action[]; + readonly getTriggerActions: (triggerId: T) => Action[]; // (undocumented) - readonly getTriggerCompatibleActions: (triggerId: T, context: TriggerContextMapping[T]) => Promise[]>; + readonly getTriggerCompatibleActions: (triggerId: T, context: TriggerContextMapping[T]) => Promise[]>; // (undocumented) readonly hasAction: (actionId: string) => boolean; // Warning: (ae-forgotten-export) The symbol "ActionContext" needs to be exported by the entry point index.d.ts // // (undocumented) - readonly registerAction: >(definition: A) => Action, "" | "ACTION_VISUALIZE_FIELD" | "ACTION_VISUALIZE_GEO_FIELD" | "ACTION_VISUALIZE_LENS_FIELD" | "ACTION_CUSTOMIZE_PANEL" | "ACTION_ADD_PANEL" | "openInspector" | "deletePanel" | "editPanel" | "togglePanel" | "replacePanel" | "clonePanel" | "addToFromLibrary" | "unlinkFromLibrary" | "ACTION_LIBRARY_NOTIFICATION" | "ACTION_GLOBAL_APPLY_FILTER" | "ACTION_SELECT_RANGE" | "ACTION_VALUE_CLICK">; + readonly registerAction: >(definition: A) => Action, "" | "ACTION_VISUALIZE_FIELD" | "ACTION_VISUALIZE_GEO_FIELD" | "ACTION_VISUALIZE_LENS_FIELD" | "ACTION_GLOBAL_APPLY_FILTER" | "ACTION_SELECT_RANGE" | "ACTION_VALUE_CLICK" | "ACTION_CUSTOMIZE_PANEL" | "ACTION_ADD_PANEL" | "openInspector" | "deletePanel" | "editPanel" | "togglePanel" | "replacePanel" | "clonePanel" | "addToFromLibrary" | "unlinkFromLibrary" | "ACTION_LIBRARY_NOTIFICATION">; // (undocumented) readonly registerTrigger: (trigger: Trigger) => void; // Warning: (ae-forgotten-export) The symbol "TriggerRegistry" needs to be exported by the entry point index.d.ts diff --git a/src/plugins/usage_collection/server/routes/stats/stats.ts b/src/plugins/usage_collection/server/routes/stats/stats.ts index ef64d15fabc2d..d38250067053c 100644 --- a/src/plugins/usage_collection/server/routes/stats/stats.ts +++ b/src/plugins/usage_collection/server/routes/stats/stats.ts @@ -73,8 +73,9 @@ export function registerStatsRoute({ return collectorSet.toObject(usage); }; - const getClusterUuid = async (callCluster: LegacyAPICaller): Promise => { - const { cluster_uuid: uuid } = await callCluster('info', { filterPath: 'cluster_uuid' }); + const getClusterUuid = async (asCurrentUser: ElasticsearchClient): Promise => { + const { body } = await asCurrentUser.info({ filter_path: 'cluster_uuid' }); + const { cluster_uuid: uuid } = body; return uuid; }; @@ -103,7 +104,7 @@ export function registerStatsRoute({ let extended; if (isExtended) { const callCluster = context.core.elasticsearch.legacy.client.callAsCurrentUser; - const esClient = context.core.elasticsearch.client.asCurrentUser; + const { asCurrentUser } = context.core.elasticsearch.client; const savedObjectsClient = context.core.savedObjects.client; if (shouldGetUsage) { @@ -114,9 +115,12 @@ export function registerStatsRoute({ } const usagePromise = shouldGetUsage - ? getUsage(callCluster, esClient, savedObjectsClient) + ? getUsage(callCluster, asCurrentUser, savedObjectsClient) : Promise.resolve({}); - const [usage, clusterUuid] = await Promise.all([usagePromise, getClusterUuid(callCluster)]); + const [usage, clusterUuid] = await Promise.all([ + usagePromise, + getClusterUuid(asCurrentUser), + ]); let modifiedUsage = usage; if (isLegacy) { diff --git a/src/plugins/vis_default_editor/public/components/__snapshots__/agg.test.tsx.snap b/src/plugins/vis_default_editor/public/components/__snapshots__/agg.test.tsx.snap index 2a521bc01219c..26173cddb3716 100644 --- a/src/plugins/vis_default_editor/public/components/__snapshots__/agg.test.tsx.snap +++ b/src/plugins/vis_default_editor/public/components/__snapshots__/agg.test.tsx.snap @@ -17,12 +17,12 @@ exports[`DefaultEditorAgg component should init with the default set of props 1` extraAction={
{ const actionIcons = []; + const aggTitle = agg.type?.title?.toLowerCase(); if (showError) { actionIcons.push({ @@ -170,7 +172,8 @@ function DefaultEditorAgg({ color: 'danger', type: 'alert', tooltip: i18n.translate('visDefaultEditor.agg.errorsAriaLabel', { - defaultMessage: 'Aggregation has errors', + defaultMessage: '{schemaTitle} {aggTitle} aggregation has errors', + values: { aggTitle, schemaTitle }, }), dataTestSubj: 'hasErrorsAggregationIcon', }); @@ -184,7 +187,8 @@ function DefaultEditorAgg({ type: 'eye', onClick: () => onToggleEnableAgg(agg.id, false), tooltip: i18n.translate('visDefaultEditor.agg.disableAggButtonTooltip', { - defaultMessage: 'Disable aggregation', + defaultMessage: 'Disable {schemaTitle} {aggTitle} aggregation', + values: { aggTitle, schemaTitle }, }), dataTestSubj: 'toggleDisableAggregationBtn disable', }); @@ -196,7 +200,8 @@ function DefaultEditorAgg({ type: 'eyeClosed', onClick: () => onToggleEnableAgg(agg.id, true), tooltip: i18n.translate('visDefaultEditor.agg.enableAggButtonTooltip', { - defaultMessage: 'Enable aggregation', + defaultMessage: 'Enable {schemaTitle} {aggTitle} aggregation', + values: { aggTitle, schemaTitle }, }), dataTestSubj: 'toggleDisableAggregationBtn enable', }); @@ -206,7 +211,8 @@ function DefaultEditorAgg({ id: 'dragHandle', type: 'grab', tooltip: i18n.translate('visDefaultEditor.agg.modifyPriorityButtonTooltip', { - defaultMessage: 'Modify priority by dragging', + defaultMessage: 'Modify priority of {schemaTitle} {aggTitle} by dragging', + values: { aggTitle, schemaTitle }, }), dataTestSubj: 'dragHandleBtn', }); @@ -218,7 +224,8 @@ function DefaultEditorAgg({ type: 'cross', onClick: () => removeAgg(agg.id), tooltip: i18n.translate('visDefaultEditor.agg.removeDimensionButtonTooltip', { - defaultMessage: 'Remove dimension', + defaultMessage: 'Remove {schemaTitle} {aggTitle} aggregation', + values: { aggTitle, schemaTitle }, }), dataTestSubj: 'removeDimensionBtn', }); @@ -257,7 +264,7 @@ function DefaultEditorAgg({
); }; - const schemaTitle = getSchemaByName(schemas, agg.schema).title; + const buttonContent = ( <> {schemaTitle || agg.schema} {showDescription && {aggDescription}} diff --git a/src/plugins/vis_default_editor/public/components/agg_add.tsx b/src/plugins/vis_default_editor/public/components/agg_add.tsx index 46d5af8cec680..e78f2fcc4453c 100644 --- a/src/plugins/vis_default_editor/public/components/agg_add.tsx +++ b/src/plugins/vis_default_editor/public/components/agg_add.tsx @@ -56,22 +56,26 @@ function DefaultEditorAggAdd({ addSchema(schema); }; + const groupNameLabel = + groupName === AggGroupNames.Buckets + ? i18n.translate('visDefaultEditor.aggAdd.bucketLabel', { defaultMessage: 'bucket' }) + : i18n.translate('visDefaultEditor.aggAdd.metricLabel', { defaultMessage: 'metric' }); + const addButton = ( setIsPopoverOpen(!isPopoverOpen)} + aria-label={i18n.translate('visDefaultEditor.aggAdd.addGroupButtonLabel', { + defaultMessage: 'Add {groupNameLabel}', + values: { groupNameLabel }, + })} > ); - const groupNameLabel = - groupName === AggGroupNames.Buckets - ? i18n.translate('visDefaultEditor.aggAdd.bucketLabel', { defaultMessage: 'bucket' }) - : i18n.translate('visDefaultEditor.aggAdd.metricLabel', { defaultMessage: 'metric' }); - const isSchemaDisabled = (schema: Schema): boolean => { const count = group.filter((agg) => agg.schema === schema.name).length; return count >= schema.max; diff --git a/src/plugins/vis_default_editor/public/components/controls/date_ranges.tsx b/src/plugins/vis_default_editor/public/components/controls/date_ranges.tsx index 785ef1b83a23d..90ee1dd4f02ae 100644 --- a/src/plugins/vis_default_editor/public/components/controls/date_ranges.tsx +++ b/src/plugins/vis_default_editor/public/components/controls/date_ranges.tsx @@ -36,7 +36,7 @@ import dateMath from '@elastic/datemath'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; import { isEqual, omit } from 'lodash'; -import { useMount } from 'react-use'; +import useMount from 'react-use/lib/useMount'; import { DocLinksStart } from 'src/core/public'; import { useKibana } from '../../../../kibana_react/public'; diff --git a/src/plugins/vis_default_editor/public/components/controls/field.tsx b/src/plugins/vis_default_editor/public/components/controls/field.tsx index 9529adfe12720..cb6e9d9aa7ba8 100644 --- a/src/plugins/vis_default_editor/public/components/controls/field.tsx +++ b/src/plugins/vis_default_editor/public/components/controls/field.tsx @@ -19,7 +19,7 @@ import { get } from 'lodash'; import React, { useState, useCallback } from 'react'; -import { useMount } from 'react-use'; +import useMount from 'react-use/lib/useMount'; import { EuiComboBox, EuiComboBoxOptionOption, EuiFormRow } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; diff --git a/src/plugins/vis_default_editor/public/components/controls/filters.tsx b/src/plugins/vis_default_editor/public/components/controls/filters.tsx index b2e6373edfc10..4c5181ab316d1 100644 --- a/src/plugins/vis_default_editor/public/components/controls/filters.tsx +++ b/src/plugins/vis_default_editor/public/components/controls/filters.tsx @@ -21,7 +21,7 @@ import React, { useState, useEffect } from 'react'; import { omit, isEqual } from 'lodash'; import { htmlIdGenerator, EuiButton, EuiSpacer } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { useMount } from 'react-use'; +import useMount from 'react-use/lib/useMount'; import { Query, DataPublicPluginStart } from '../../../../data/public'; import { IUiSettingsClient } from '../../../../../core/public'; diff --git a/src/plugins/vis_default_editor/public/components/controls/order_by.tsx b/src/plugins/vis_default_editor/public/components/controls/order_by.tsx index 16aeafaab253b..000719318f107 100644 --- a/src/plugins/vis_default_editor/public/components/controls/order_by.tsx +++ b/src/plugins/vis_default_editor/public/components/controls/order_by.tsx @@ -20,7 +20,7 @@ import React from 'react'; import { EuiFormRow, EuiSelect } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { useMount } from 'react-use'; +import useMount from 'react-use/lib/useMount'; import { isCompatibleAggregation, diff --git a/src/plugins/vis_default_editor/public/components/controls/radius_ratio_option.tsx b/src/plugins/vis_default_editor/public/components/controls/radius_ratio_option.tsx index c2c21e7c1a058..00e6b6da88b05 100644 --- a/src/plugins/vis_default_editor/public/components/controls/radius_ratio_option.tsx +++ b/src/plugins/vis_default_editor/public/components/controls/radius_ratio_option.tsx @@ -21,7 +21,7 @@ import React, { useCallback } from 'react'; import { EuiFormRow, EuiIconTip, EuiRange, EuiSpacer } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { useMount } from 'react-use'; +import useMount from 'react-use/lib/useMount'; import { AggControlProps } from './agg_control_props'; diff --git a/src/plugins/vis_default_editor/public/components/controls/sub_metric.tsx b/src/plugins/vis_default_editor/public/components/controls/sub_metric.tsx index fc79ba703c2b4..4b0637edf4055 100644 --- a/src/plugins/vis_default_editor/public/components/controls/sub_metric.tsx +++ b/src/plugins/vis_default_editor/public/components/controls/sub_metric.tsx @@ -20,7 +20,7 @@ import React from 'react'; import { EuiFormLabel, EuiSpacer } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { useMount } from 'react-use'; +import useMount from 'react-use/lib/useMount'; import { AggParamType, IAggConfig, AggGroupNames } from '../../../../data/public'; import { useSubAggParamsHandlers } from './utils'; diff --git a/src/plugins/vis_default_editor/public/components/sidebar/controls.tsx b/src/plugins/vis_default_editor/public/components/sidebar/controls.tsx index df9818b237962..388ac39f06475 100644 --- a/src/plugins/vis_default_editor/public/components/sidebar/controls.tsx +++ b/src/plugins/vis_default_editor/public/components/sidebar/controls.tsx @@ -21,7 +21,7 @@ import React, { useCallback, useState } from 'react'; import { EuiFlexGroup, EuiFlexItem, EuiButton, EuiButtonEmpty, EuiToolTip } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; -import { useDebounce } from 'react-use'; +import useDebounce from 'react-use/lib/useDebounce'; import { Vis } from 'src/plugins/visualizations/public'; import { discardChanges, EditorAction } from './state'; diff --git a/src/plugins/vis_type_timelion/public/helpers/timelion_request_handler.ts b/src/plugins/vis_type_timelion/public/helpers/timelion_request_handler.ts index 1f0ac8b2b9392..7a630f36b51f4 100644 --- a/src/plugins/vis_type_timelion/public/helpers/timelion_request_handler.ts +++ b/src/plugins/vis_type_timelion/public/helpers/timelion_request_handler.ts @@ -18,8 +18,7 @@ */ import { i18n } from '@kbn/i18n'; -import { KIBANA_CONTEXT_NAME } from 'src/plugins/expressions/public'; -import { TimeRange, Filter, esQuery, Query } from '../../../data/public'; +import { KibanaContext, TimeRange, Filter, esQuery, Query } from '../../../data/public'; import { TimelionVisDependencies } from '../plugin'; import { getTimezone } from './get_timezone'; import { TimelionVisParams } from '../timelion_vis_fn'; @@ -59,7 +58,7 @@ export interface TimelionSuccessResponse { sheet: Sheet[]; stats: Stats; visType: string; - type: KIBANA_CONTEXT_NAME; + type: KibanaContext['type']; } export function getTimelionRequestHandler({ diff --git a/src/plugins/vis_type_timelion/public/timelion_vis_fn.ts b/src/plugins/vis_type_timelion/public/timelion_vis_fn.ts index a0cd410e197ff..2e8878b11e915 100644 --- a/src/plugins/vis_type_timelion/public/timelion_vis_fn.ts +++ b/src/plugins/vis_type_timelion/public/timelion_vis_fn.ts @@ -19,18 +19,14 @@ import { get } from 'lodash'; import { i18n } from '@kbn/i18n'; -import { - ExpressionFunctionDefinition, - KibanaContext, - Render, -} from 'src/plugins/expressions/public'; +import { ExpressionFunctionDefinition, Render } from 'src/plugins/expressions/public'; import { getTimelionRequestHandler, TimelionSuccessResponse, } from './helpers/timelion_request_handler'; import { TIMELION_VIS_NAME } from './timelion_vis_type'; import { TimelionVisDependencies } from './plugin'; -import { Filter, Query, TimeRange } from '../../data/common'; +import { KibanaContext, Filter, Query, TimeRange } from '../../data/public'; type Input = KibanaContext | null; type Output = Promise>; diff --git a/src/plugins/vis_type_timeseries/common/types.ts b/src/plugins/vis_type_timeseries/common/types.ts index 4520069244527..8973060848b41 100644 --- a/src/plugins/vis_type_timeseries/common/types.ts +++ b/src/plugins/vis_type_timeseries/common/types.ts @@ -18,8 +18,9 @@ */ import { TypeOf } from '@kbn/config-schema'; -import { metricsItems, panel, seriesItems } from './vis_schema'; +import { metricsItems, panel, seriesItems, visPayloadSchema } from './vis_schema'; export type SeriesItemsSchema = TypeOf; export type MetricsItemsSchema = TypeOf; export type PanelSchema = TypeOf; +export type VisPayload = TypeOf; diff --git a/src/plugins/vis_type_timeseries/common/vis_schema.ts b/src/plugins/vis_type_timeseries/common/vis_schema.ts index 40f776050617e..9ec5ae1424ae3 100644 --- a/src/plugins/vis_type_timeseries/common/vis_schema.ts +++ b/src/plugins/vis_type_timeseries/common/vis_schema.ts @@ -120,7 +120,7 @@ export const metricsItems = schema.object({ type: stringRequired, value: stringOptionalNullable, values: schema.maybe(schema.nullable(schema.arrayOf(schema.nullable(schema.string())))), - size: stringOptionalNullable, + size: stringOrNumberOptionalNullable, agg_with: stringOptionalNullable, order: stringOptionalNullable, order_by: stringOptionalNullable, @@ -273,4 +273,5 @@ export const visPayloadSchema = schema.object({ min: stringRequired, max: stringRequired, }), + sessionId: schema.maybe(schema.string()), }); diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/calculation.js b/src/plugins/vis_type_timeseries/public/application/components/aggs/calculation.js index bb3d39797656f..5bf4fb55ee5e5 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/aggs/calculation.js +++ b/src/plugins/vis_type_timeseries/public/application/components/aggs/calculation.js @@ -26,6 +26,7 @@ import { createChangeHandler } from '../lib/create_change_handler'; import { createSelectHandler } from '../lib/create_select_handler'; import { createTextHandler } from '../lib/create_text_handler'; import { CalculationVars, newVariable } from './vars'; +import { METRIC_TYPES } from '../../../../common/metric_types'; import { FormattedMessage } from '@kbn/i18n/react'; import { @@ -99,6 +100,7 @@ export function CalculationAgg(props) { onChange={handleChange} name="variables" model={model} + exclude={[METRIC_TYPES.TOP_HIT]} /> diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/cumulative_sum.js b/src/plugins/vis_type_timeseries/public/application/components/aggs/cumulative_sum.js index 11b3e303e7e00..0b879adbd37ae 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/aggs/cumulative_sum.js +++ b/src/plugins/vis_type_timeseries/public/application/components/aggs/cumulative_sum.js @@ -24,6 +24,7 @@ import { AggSelect } from './agg_select'; import { MetricSelect } from './metric_select'; import { createChangeHandler } from '../lib/create_change_handler'; import { createSelectHandler } from '../lib/create_select_handler'; +import { METRIC_TYPES } from '../../../../common/metric_types'; import { FormattedMessage } from '@kbn/i18n/react'; import { htmlIdGenerator, @@ -80,6 +81,7 @@ export function CumulativeSumAgg(props) { metrics={siblings} metric={model} value={model.field} + exclude={[METRIC_TYPES.TOP_HIT]} /> diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/derivative.js b/src/plugins/vis_type_timeseries/public/application/components/aggs/derivative.js index faf1a59adc4aa..fa1289dc74c72 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/aggs/derivative.js +++ b/src/plugins/vis_type_timeseries/public/application/components/aggs/derivative.js @@ -25,6 +25,7 @@ import { AggRow } from './agg_row'; import { createChangeHandler } from '../lib/create_change_handler'; import { createSelectHandler } from '../lib/create_select_handler'; import { createTextHandler } from '../lib/create_text_handler'; +import { METRIC_TYPES } from '../../../../common/metric_types'; import { htmlIdGenerator, EuiFlexGroup, @@ -91,6 +92,7 @@ export const DerivativeAgg = (props) => { metrics={siblings} metric={model} value={model.field} + exclude={[METRIC_TYPES.TOP_HIT]} fullWidth /> diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/moving_average.js b/src/plugins/vis_type_timeseries/public/application/components/aggs/moving_average.js index 316e0f9af43bd..fb945d2606bc8 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/aggs/moving_average.js +++ b/src/plugins/vis_type_timeseries/public/application/components/aggs/moving_average.js @@ -25,6 +25,7 @@ import { MetricSelect } from './metric_select'; import { createChangeHandler } from '../lib/create_change_handler'; import { createSelectHandler } from '../lib/create_select_handler'; import { createNumberHandler } from '../lib/create_number_handler'; +import { METRIC_TYPES } from '../../../../common/metric_types'; import { htmlIdGenerator, EuiFlexGroup, @@ -153,6 +154,7 @@ export const MovingAverageAgg = (props) => { metrics={siblings} metric={model} value={model.field} + exclude={[METRIC_TYPES.TOP_HIT]} /> diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/positive_only.js b/src/plugins/vis_type_timeseries/public/application/components/aggs/positive_only.js index 1999862f7aa0e..6ca5fa8e7447f 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/aggs/positive_only.js +++ b/src/plugins/vis_type_timeseries/public/application/components/aggs/positive_only.js @@ -24,6 +24,7 @@ import { MetricSelect } from './metric_select'; import { AggRow } from './agg_row'; import { createChangeHandler } from '../lib/create_change_handler'; import { createSelectHandler } from '../lib/create_select_handler'; +import { METRIC_TYPES } from '../../../../common/metric_types'; import { htmlIdGenerator, EuiFlexGroup, @@ -85,6 +86,7 @@ export const PositiveOnlyAgg = (props) => { metrics={siblings} metric={model} value={model.field} + exclude={[METRIC_TYPES.TOP_HIT]} /> diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/serial_diff.js b/src/plugins/vis_type_timeseries/public/application/components/aggs/serial_diff.js index 10b3d551bb89f..e3a0c74273539 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/aggs/serial_diff.js +++ b/src/plugins/vis_type_timeseries/public/application/components/aggs/serial_diff.js @@ -25,6 +25,7 @@ import { AggRow } from './agg_row'; import { createChangeHandler } from '../lib/create_change_handler'; import { createSelectHandler } from '../lib/create_select_handler'; import { createNumberHandler } from '../lib/create_number_handler'; +import { METRIC_TYPES } from '../../../../common/metric_types'; import { htmlIdGenerator, EuiFlexGroup, @@ -87,6 +88,7 @@ export const SerialDiffAgg = (props) => { metrics={siblings} metric={model} value={model.field} + exclude={[METRIC_TYPES.TOP_HIT]} /> diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/std_sibling.js b/src/plugins/vis_type_timeseries/public/application/components/aggs/std_sibling.js index 30e5c57ac90ba..bed5e9caa9f87 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/aggs/std_sibling.js +++ b/src/plugins/vis_type_timeseries/public/application/components/aggs/std_sibling.js @@ -25,6 +25,8 @@ import { AggSelect } from './agg_select'; import { createChangeHandler } from '../lib/create_change_handler'; import { createSelectHandler } from '../lib/create_select_handler'; import { createTextHandler } from '../lib/create_text_handler'; +import { METRIC_TYPES } from '../../../../common/metric_types'; + import { htmlIdGenerator, EuiFlexGroup, @@ -154,7 +156,7 @@ const StandardSiblingAggUi = (props) => { > diff --git a/src/plugins/vis_type_timeseries/public/metrics_fn.ts b/src/plugins/vis_type_timeseries/public/metrics_fn.ts index b573225feaab1..8652d703f963e 100644 --- a/src/plugins/vis_type_timeseries/public/metrics_fn.ts +++ b/src/plugins/vis_type_timeseries/public/metrics_fn.ts @@ -19,7 +19,8 @@ import { get } from 'lodash'; import { i18n } from '@kbn/i18n'; -import { ExpressionFunctionDefinition, KibanaContext, Render } from '../../expressions/public'; +import { KibanaContext } from '../../data/public'; +import { ExpressionFunctionDefinition, Render } from '../../expressions/public'; // @ts-ignore import { metricsRequestHandler } from './request_handler'; diff --git a/src/plugins/vis_type_timeseries/public/request_handler.js b/src/plugins/vis_type_timeseries/public/request_handler.js index e33d0e254f609..12b7f3d417ef6 100644 --- a/src/plugins/vis_type_timeseries/public/request_handler.js +++ b/src/plugins/vis_type_timeseries/public/request_handler.js @@ -32,7 +32,8 @@ export const metricsRequestHandler = async ({ const config = getUISettings(); const timezone = getTimezone(config); const uiStateObj = uiState.get(visParams.type, {}); - const parsedTimeRange = getDataStart().query.timefilter.timefilter.calculateBounds(timeRange); + const dataSearch = getDataStart(); + const parsedTimeRange = dataSearch.query.timefilter.timefilter.calculateBounds(timeRange); const scaledDataFormat = config.get('dateFormat:scaled'); const dateFormat = config.get('dateFormat'); @@ -53,6 +54,7 @@ export const metricsRequestHandler = async ({ panels: [visParams], state: uiStateObj, savedObjectId: savedObjectId || 'unsaved', + sessionId: dataSearch.search.session.getSessionId(), }), }); diff --git a/src/plugins/vis_type_timeseries/server/lib/get_fields.ts b/src/plugins/vis_type_timeseries/server/lib/get_fields.ts index b52188129f77f..dc49e280a2bb7 100644 --- a/src/plugins/vis_type_timeseries/server/lib/get_fields.ts +++ b/src/plugins/vis_type_timeseries/server/lib/get_fields.ts @@ -62,10 +62,12 @@ export async function getFields( let indexPatternString = indexPattern; if (!indexPatternString) { - const [{ savedObjects }, { data }] = await framework.core.getStartServices(); + const [{ savedObjects, elasticsearch }, { data }] = await framework.core.getStartServices(); const savedObjectsClient = savedObjects.getScopedClient(request); + const clusterClient = elasticsearch.client.asScoped(request).asCurrentUser; const indexPatternsService = await data.indexPatterns.indexPatternsServiceFactory( - savedObjectsClient + savedObjectsClient, + clusterClient ); const defaultIndexPattern = await indexPatternsService.getDefault(); indexPatternString = get(defaultIndexPattern, 'title', ''); diff --git a/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.test.js b/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.test.js index 9710f7daf69b6..2c38e883cd69f 100644 --- a/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.test.js +++ b/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.test.js @@ -28,6 +28,7 @@ describe('AbstractSearchStrategy', () => { beforeEach(() => { mockedFields = {}; req = { + payload: {}, pre: { indexPatternsService: { getFieldsForWildcard: jest.fn().mockReturnValue(mockedFields), @@ -60,6 +61,9 @@ describe('AbstractSearchStrategy', () => { const responses = await abstractSearchStrategy.search( { + payload: { + sessionId: 1, + }, requestContext: { search: { search: searchFn }, }, @@ -76,7 +80,9 @@ describe('AbstractSearchStrategy', () => { }, indexType: undefined, }, - {} + { + sessionId: 1, + } ); }); }); diff --git a/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.ts b/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.ts index eb22fcb1dd689..b1e21edf8b588 100644 --- a/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.ts +++ b/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.ts @@ -23,8 +23,10 @@ import { IUiSettingsClient, SavedObjectsClientContract, } from 'kibana/server'; + import { Framework } from '../../../plugin'; import { IndexPatternsFetcher } from '../../../../../data/server'; +import { VisPayload } from '../../../../common/types'; /** * ReqFacade is a regular KibanaRequest object extended with additional service @@ -32,17 +34,17 @@ import { IndexPatternsFetcher } from '../../../../../data/server'; * * This will be replaced by standard KibanaRequest and RequestContext objects in a later version. */ -export type ReqFacade = FakeRequest & { +export interface ReqFacade extends FakeRequest { requestContext: RequestHandlerContext; framework: Framework; - payload: unknown; + payload: T; pre: { indexPatternsService?: IndexPatternsFetcher; }; getUiSettingsService: () => IUiSettingsClient; getSavedObjectsClient: () => SavedObjectsClientContract; getEsShardTimeout: () => Promise; -}; +} export class AbstractSearchStrategy { public indexType?: string; @@ -53,8 +55,10 @@ export class AbstractSearchStrategy { this.additionalParams = additionalParams; } - async search(req: ReqFacade, bodies: any[], options = {}) { + async search(req: ReqFacade, bodies: any[], options = {}) { const requests: any[] = []; + const { sessionId } = req.payload; + bodies.forEach((body) => { requests.push( req.requestContext @@ -67,6 +71,7 @@ export class AbstractSearchStrategy { indexType: this.indexType, }, { + sessionId, ...options, } ) diff --git a/src/plugins/vis_type_vega/public/vega_fn.ts b/src/plugins/vis_type_vega/public/vega_fn.ts index 25d4e76c336b3..5a8113aeeea11 100644 --- a/src/plugins/vis_type_vega/public/vega_fn.ts +++ b/src/plugins/vis_type_vega/public/vega_fn.ts @@ -19,16 +19,12 @@ import { get } from 'lodash'; import { i18n } from '@kbn/i18n'; -import { - ExecutionContext, - ExpressionFunctionDefinition, - KibanaContext, - Render, -} from '../../expressions/public'; +import { ExecutionContextSearch } from '../../data/public'; +import { ExecutionContext, ExpressionFunctionDefinition, Render } from '../../expressions/public'; import { VegaVisualizationDependencies } from './plugin'; import { createVegaRequestHandler } from './vega_request_handler'; import { VegaInspectorAdapters } from './vega_inspector/index'; -import { TimeRange, Query } from '../../data/public'; +import { KibanaContext, TimeRange, Query } from '../../data/public'; import { VegaParser } from './data_model/vega_parser'; type Input = KibanaContext | null; @@ -51,7 +47,7 @@ export type VegaExpressionFunctionDefinition = ExpressionFunctionDefinition< Input, Arguments, Output, - ExecutionContext + ExecutionContext >; export const createVegaFn = ( diff --git a/src/plugins/visualizations/public/embeddable/_index.scss b/src/plugins/visualizations/public/embeddable/_index.scss index c1e3809657bfa..9703e90159f48 100644 --- a/src/plugins/visualizations/public/embeddable/_index.scss +++ b/src/plugins/visualizations/public/embeddable/_index.scss @@ -1,2 +1 @@ -@import 'visualize_lab_disabled'; @import 'embeddables'; diff --git a/src/plugins/visualizations/public/embeddable/_visualize_lab_disabled.scss b/src/plugins/visualizations/public/embeddable/_visualize_lab_disabled.scss deleted file mode 100644 index 914480ff8c777..0000000000000 --- a/src/plugins/visualizations/public/embeddable/_visualize_lab_disabled.scss +++ /dev/null @@ -1,13 +0,0 @@ -.visDisabledLabVisualization { - width: 100%; - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - text-align: center; -} - -.visDisabledLabVisualization__icon { - font-size: $euiFontSizeXL; -} - diff --git a/src/plugins/visualizations/public/embeddable/disabled_lab_visualization.tsx b/src/plugins/visualizations/public/embeddable/disabled_lab_visualization.tsx index 3d2af2c591a3c..ea7760f31d54c 100644 --- a/src/plugins/visualizations/public/embeddable/disabled_lab_visualization.tsx +++ b/src/plugins/visualizations/public/embeddable/disabled_lab_visualization.tsx @@ -17,29 +17,42 @@ * under the License. */ -import { FormattedMessage } from '@kbn/i18n/react'; +import { FormattedMessage, I18nProvider } from '@kbn/i18n/react'; +import { EuiEmptyPrompt, EuiLink } from '@elastic/eui'; import React from 'react'; +import { getDocLinks } from '../services'; export function DisabledLabVisualization({ title }: { title: string }) { + const advancedSettingsLink = getDocLinks().links.management.visualizationSettings; return ( -
- + ); } diff --git a/src/plugins/visualize/public/application/components/visualize_listing.tsx b/src/plugins/visualize/public/application/components/visualize_listing.tsx index 2edabbf46f9d8..718bd2ed343ce 100644 --- a/src/plugins/visualize/public/application/components/visualize_listing.tsx +++ b/src/plugins/visualize/public/application/components/visualize_listing.tsx @@ -21,7 +21,9 @@ import './visualize_listing.scss'; import React, { useCallback, useRef, useMemo, useEffect } from 'react'; import { i18n } from '@kbn/i18n'; -import { useUnmount, useMount } from 'react-use'; +import useUnmount from 'react-use/lib/useUnmount'; +import useMount from 'react-use/lib/useMount'; + import { useLocation } from 'react-router-dom'; import { SavedObjectsFindOptionsReference } from '../../../../../core/public'; diff --git a/test/api_integration/apis/saved_objects/find.js b/test/api_integration/apis/saved_objects/find.js index c2e36b4a669ff..8e8730b1e574a 100644 --- a/test/api_integration/apis/saved_objects/find.js +++ b/test/api_integration/apis/saved_objects/find.js @@ -160,7 +160,7 @@ export default function ({ getService }) { }); describe('wildcard namespace', () => { - it('should return 200 with individual responses from the default namespace', async () => + it('should return 200 with individual responses from the all namespaces', async () => await supertest .get('/api/saved_objects/_find?type=visualization&fields=title&namespaces=*') .expect(200) @@ -168,7 +168,7 @@ export default function ({ getService }) { expect(resp.body).to.eql({ page: 1, per_page: 20, - total: 1, + total: 2, saved_objects: [ { type: 'visualization', @@ -189,6 +189,27 @@ export default function ({ getService }) { ], updated_at: '2017-09-21T18:51:23.794Z', }, + { + attributes: { + title: 'Count of requests', + }, + id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab', + migrationVersion: { + visualization: '7.10.0', + }, + namespaces: ['foo-ns'], + references: [ + { + id: '91200a00-9efd-11e7-acb3-3dab96693fab', + name: 'kibanaSavedObjectMeta.searchSourceJSON.index', + type: 'index-pattern', + }, + ], + score: 0, + type: 'visualization', + updated_at: '2017-09-21T18:51:23.794Z', + version: 'WzYsMV0=', + }, ], }); expect(resp.body.saved_objects[0].migrationVersion).to.be.ok(); @@ -334,6 +355,70 @@ export default function ({ getService }) { }); }); + describe('searching for special characters', () => { + before(() => esArchiver.load('saved_objects/find_edgecases')); + after(() => esArchiver.unload('saved_objects/find_edgecases')); + + it('can search for objects with dashes', async () => + await supertest + .get('/api/saved_objects/_find') + .query({ + type: 'visualization', + search_fields: 'title', + search: 'my-vis*', + }) + .expect(200) + .then((resp) => { + const savedObjects = resp.body.saved_objects; + expect(savedObjects.map((so) => so.attributes.title)).to.eql(['my-visualization']); + })); + + it('can search with the prefix search character just after a special one', async () => + await supertest + .get('/api/saved_objects/_find') + .query({ + type: 'visualization', + search_fields: 'title', + search: 'my-*', + }) + .expect(200) + .then((resp) => { + const savedObjects = resp.body.saved_objects; + expect(savedObjects.map((so) => so.attributes.title)).to.eql(['my-visualization']); + })); + + it('can search for objects with asterisk', async () => + await supertest + .get('/api/saved_objects/_find') + .query({ + type: 'visualization', + search_fields: 'title', + search: 'some*vi*', + }) + .expect(200) + .then((resp) => { + const savedObjects = resp.body.saved_objects; + expect(savedObjects.map((so) => so.attributes.title)).to.eql(['some*visualization']); + })); + + it('can still search tokens by prefix', async () => + await supertest + .get('/api/saved_objects/_find') + .query({ + type: 'visualization', + search_fields: 'title', + search: 'visuali*', + }) + .expect(200) + .then((resp) => { + const savedObjects = resp.body.saved_objects; + expect(savedObjects.map((so) => so.attributes.title)).to.eql([ + 'my-visualization', + 'some*visualization', + ]); + })); + }); + describe('without kibana index', () => { before( async () => diff --git a/test/api_integration/fixtures/es_archiver/saved_objects/find_edgecases/data.json b/test/api_integration/fixtures/es_archiver/saved_objects/find_edgecases/data.json new file mode 100644 index 0000000000000..0c8b35fd3f499 --- /dev/null +++ b/test/api_integration/fixtures/es_archiver/saved_objects/find_edgecases/data.json @@ -0,0 +1,93 @@ +{ + "type": "doc", + "value": { + "index": ".kibana", + "id": "visualization:title-with-dash", + "source": { + "type": "visualization", + "updated_at": "2017-09-21T18:51:23.794Z", + "visualization": { + "title": "my-visualization", + "visState": "{}", + "uiStateJSON": "{\"spy\":{\"mode\":{\"name\":null,\"fill\":false}}}", + "description": "", + "version": 1, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"filter\":[],\"query\":{\"query\":\"\",\"language\":\"lucene\"}}" + } + }, + "references": [] + } + } +} + +{ + "type": "doc", + "value": { + "index": ".kibana", + "id": "visualization:title-with-asterisk", + "source": { + "type": "visualization", + "updated_at": "2017-09-21T18:51:23.794Z", + "visualization": { + "title": "some*visualization", + "visState": "{}", + "uiStateJSON": "{\"spy\":{\"mode\":{\"name\":null,\"fill\":false}}}", + "description": "", + "version": 1, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"filter\":[],\"query\":{\"query\":\"\",\"language\":\"lucene\"}}" + } + }, + "references": [] + } + } +} + + +{ + "type": "doc", + "value": { + "index": ".kibana", + "id": "visualization:noise-1", + "source": { + "type": "visualization", + "updated_at": "2017-09-21T18:51:23.794Z", + "visualization": { + "title": "Just some noise in the dataset", + "visState": "{}", + "uiStateJSON": "{\"spy\":{\"mode\":{\"name\":null,\"fill\":false}}}", + "description": "", + "version": 1, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"filter\":[],\"query\":{\"query\":\"\",\"language\":\"lucene\"}}" + } + }, + "references": [] + } + } +} + +{ + "type": "doc", + "value": { + "index": ".kibana", + "id": "visualization:noise-2", + "source": { + "type": "visualization", + "updated_at": "2017-09-21T18:51:23.794Z", + "visualization": { + "title": "Just some noise in the dataset", + "visState": "{}", + "uiStateJSON": "{\"spy\":{\"mode\":{\"name\":null,\"fill\":false}}}", + "description": "", + "version": 1, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"filter\":[],\"query\":{\"query\":\"\",\"language\":\"lucene\"}}" + } + }, + "references": [] + } + } +} + diff --git a/test/api_integration/fixtures/es_archiver/saved_objects/find_edgecases/mappings.json b/test/api_integration/fixtures/es_archiver/saved_objects/find_edgecases/mappings.json new file mode 100644 index 0000000000000..e601c43431437 --- /dev/null +++ b/test/api_integration/fixtures/es_archiver/saved_objects/find_edgecases/mappings.json @@ -0,0 +1,267 @@ +{ + "type": "index", + "value": { + "index": ".kibana", + "settings": { + "index": { + "number_of_shards": "1", + "number_of_replicas": "1" + } + }, + "mappings": { + "dynamic": "strict", + "properties": { + "config": { + "dynamic": "true", + "properties": { + "buildNum": { + "type": "keyword" + }, + "defaultIndex": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + } + } + }, + "dashboard": { + "properties": { + "description": { + "type": "text" + }, + "hits": { + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "optionsJSON": { + "type": "text" + }, + "panelsJSON": { + "type": "text" + }, + "refreshInterval": { + "properties": { + "display": { + "type": "keyword" + }, + "pause": { + "type": "boolean" + }, + "section": { + "type": "integer" + }, + "value": { + "type": "integer" + } + } + }, + "timeFrom": { + "type": "keyword" + }, + "timeRestore": { + "type": "boolean" + }, + "timeTo": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "uiStateJSON": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "index-pattern": { + "properties": { + "fieldFormatMap": { + "type": "text" + }, + "fields": { + "type": "text" + }, + "intervalName": { + "type": "keyword" + }, + "notExpandable": { + "type": "boolean" + }, + "sourceFilters": { + "type": "text" + }, + "timeFieldName": { + "type": "keyword" + }, + "title": { + "type": "text" + } + } + }, + "search": { + "properties": { + "columns": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "hits": { + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "sort": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "server": { + "properties": { + "uuid": { + "type": "keyword" + } + } + }, + "timelion-sheet": { + "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" + } + } + }, + "namespace": { + "type": "keyword" + }, + "references": { + "properties": { + "id": { + "type": "keyword" + }, + "name": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + }, + "type": "nested" + }, + "type": { + "type": "keyword" + }, + "updated_at": { + "type": "date" + }, + "url": { + "properties": { + "accessCount": { + "type": "long" + }, + "accessDate": { + "type": "date" + }, + "createDate": { + "type": "date" + }, + "url": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 2048 + } + } + } + } + }, + "visualization": { + "properties": { + "description": { + "type": "text" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "savedSearchId": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "uiStateJSON": { + "type": "text" + }, + "version": { + "type": "integer" + }, + "visState": { + "type": "text" + } + } + } + } + } + } +} diff --git a/test/functional/apps/management/_import_objects.ts b/test/functional/apps/management/_import_objects.ts index 4a7a85c738fc2..52428c944d666 100644 --- a/test/functional/apps/management/_import_objects.ts +++ b/test/functional/apps/management/_import_objects.ts @@ -37,7 +37,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { describe('import objects', function describeIndexTests() { describe('.ndjson file', () => { beforeEach(async function () { - // delete .kibana index and then wait for Kibana to re-create it await kibanaServer.uiSettings.replace({}); await PageObjects.settings.navigateTo(); await esArchiver.load('management'); @@ -471,16 +470,20 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { ); }); - it('should display an explicit error message when importing a file bigger than allowed', async () => { - await PageObjects.savedObjects.importFile( - path.join(__dirname, 'exports', '_import_too_big.ndjson') - ); + describe('when bigger than savedObjects.maxImportPayloadBytes (not Cloud)', function () { + // see --savedObjects.maxImportPayloadBytes in config file + this.tags(['skipCloud']); + it('should display an explicit error message when importing a file bigger than allowed', async () => { + await PageObjects.savedObjects.importFile( + path.join(__dirname, 'exports', '_import_too_big.ndjson') + ); - await PageObjects.savedObjects.checkImportError(); + await PageObjects.savedObjects.checkImportError(); - const errorText = await PageObjects.savedObjects.getImportErrorText(); + const errorText = await PageObjects.savedObjects.getImportErrorText(); - expect(errorText).to.contain(`Payload content length greater than maximum allowed`); + expect(errorText).to.contain(`Payload content length greater than maximum allowed`); + }); }); it('should display an explicit error message when importing an invalid file', async () => { diff --git a/test/functional/apps/management/_index_patterns_empty.ts b/test/functional/apps/management/_index_patterns_empty.ts index 4ae2e7836ac37..2a1d723f1a06e 100644 --- a/test/functional/apps/management/_index_patterns_empty.ts +++ b/test/functional/apps/management/_index_patterns_empty.ts @@ -22,6 +22,7 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getPageObjects, getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const kibanaServer = getService('kibanaServer'); + const log = getService('log'); const PageObjects = getPageObjects(['common', 'settings']); const testSubjects = getService('testSubjects'); const globalNav = getService('globalNav'); @@ -30,6 +31,8 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { describe('index pattern empty view', () => { before(async () => { await esArchiver.load('empty_kibana'); + await esArchiver.unload('logstash_functional'); + await esArchiver.unload('makelogs'); await kibanaServer.uiSettings.replace({}); await PageObjects.settings.navigateTo(); }); @@ -37,16 +40,26 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { after(async () => { await esArchiver.unload('empty_kibana'); await esArchiver.loadIfNeeded('makelogs'); - }); - - // create index pattern and return to verify list - it(`shows empty views`, async () => { // @ts-expect-error await es.transport.request({ - path: '/_all', + path: '/logstash-a', method: 'DELETE', }); + }); + + // create index pattern and return to verify list + it(`shows empty views`, async () => { await PageObjects.settings.clickKibanaIndexPatterns(); + log.debug( + `\n\nNOTE: If this test fails make sure there aren't any non-system indices in the _cat/indices output (use esArchiver.unload on them)` + ); + log.debug( + // @ts-expect-error + await es.transport.request({ + path: '/_cat/indices', + method: 'GET', + }) + ); await testSubjects.existOrFail('createAnyway'); // @ts-expect-error await es.transport.request({ diff --git a/test/functional/apps/management/_mgmt_import_saved_objects.js b/test/functional/apps/management/_mgmt_import_saved_objects.js index 3a9f8665fd33b..d479a7006d0f8 100644 --- a/test/functional/apps/management/_mgmt_import_saved_objects.js +++ b/test/functional/apps/management/_mgmt_import_saved_objects.js @@ -29,6 +29,7 @@ export default function ({ getService, getPageObjects }) { describe('mgmt saved objects', function describeIndexTests() { beforeEach(async function () { + await esArchiver.load('empty_kibana'); await esArchiver.load('discover'); await PageObjects.settings.navigateTo(); }); diff --git a/test/functional/apps/management/_scripted_fields.js b/test/functional/apps/management/_scripted_fields.js index 6da9ebed0538a..5ca01f239e762 100644 --- a/test/functional/apps/management/_scripted_fields.js +++ b/test/functional/apps/management/_scripted_fields.js @@ -198,35 +198,44 @@ export default function ({ getService, getPageObjects }) { }); it('should visualize scripted field in vertical bar chart', async function () { - const expectedChartValues = [ - ['14', '31'], - ['10', '29'], - ['7', '24'], - ['11', '24'], - ['12', '23'], - ['20', '23'], - ['19', '21'], - ['6', '20'], - ['17', '20'], - ['30', '20'], - ['13', '19'], - ['18', '18'], - ['16', '17'], - ['5', '16'], - ['8', '16'], - ['15', '14'], - ['3', '13'], - ['2', '12'], - ['9', '10'], - ['4', '9'], - ]; await filterBar.removeAllFilters(); await PageObjects.discover.clickFieldListItemVisualize(scriptedPainlessFieldName); await PageObjects.header.waitUntilLoadingHasFinished(); - await inspector.open(); - await inspector.setTablePageSize(50); - await inspector.expectTableData(expectedChartValues); + if (await PageObjects.common.isOss()) { + // OSS renders a vertical bar chart and we check the data in the Inspect panel + const expectedChartValues = [ + ['14', '31'], + ['10', '29'], + ['7', '24'], + ['11', '24'], + ['12', '23'], + ['20', '23'], + ['19', '21'], + ['6', '20'], + ['17', '20'], + ['30', '20'], + ['13', '19'], + ['18', '18'], + ['16', '17'], + ['5', '16'], + ['8', '16'], + ['15', '14'], + ['3', '13'], + ['2', '12'], + ['9', '10'], + ['4', '9'], + ]; + + await inspector.open(); + await inspector.setTablePageSize(50); + await inspector.expectTableData(expectedChartValues); + } else { + // verify Lens opens a visualization + expect(await testSubjects.getVisibleTextAll('lns-dimensionTrigger')).to.contain( + 'Average of ram_Pain1' + ); + } }); }); @@ -309,11 +318,19 @@ export default function ({ getService, getPageObjects }) { it('should visualize scripted field in vertical bar chart', async function () { await PageObjects.discover.clickFieldListItemVisualize(scriptedPainlessFieldName2); await PageObjects.header.waitUntilLoadingHasFinished(); - await inspector.open(); - await inspector.expectTableData([ - ['good', '359'], - ['bad', '27'], - ]); + if (await PageObjects.common.isOss()) { + // OSS renders a vertical bar chart and we check the data in the Inspect panel + await inspector.open(); + await inspector.expectTableData([ + ['good', '359'], + ['bad', '27'], + ]); + } else { + // verify Lens opens a visualization + expect(await testSubjects.getVisibleTextAll('lns-dimensionTrigger')).to.contain( + 'Top values of painString' + ); + } }); }); @@ -397,11 +414,19 @@ export default function ({ getService, getPageObjects }) { it('should visualize scripted field in vertical bar chart', async function () { await PageObjects.discover.clickFieldListItemVisualize(scriptedPainlessFieldName2); await PageObjects.header.waitUntilLoadingHasFinished(); - await inspector.open(); - await inspector.expectTableData([ - ['true', '359'], - ['false', '27'], - ]); + if (await PageObjects.common.isOss()) { + // OSS renders a vertical bar chart and we check the data in the Inspect panel + await inspector.open(); + await inspector.expectTableData([ + ['true', '359'], + ['false', '27'], + ]); + } else { + // verify Lens opens a visualization + expect(await testSubjects.getVisibleTextAll('lns-dimensionTrigger')).to.contain( + 'Top values of painBool' + ); + } }); }); @@ -488,30 +513,39 @@ export default function ({ getService, getPageObjects }) { it('should visualize scripted field in vertical bar chart', async function () { await PageObjects.discover.clickFieldListItemVisualize(scriptedPainlessFieldName2); await PageObjects.header.waitUntilLoadingHasFinished(); - await inspector.open(); - await inspector.setTablePageSize(50); - await inspector.expectTableData([ - ['2015-09-17 20:00', '1'], - ['2015-09-17 21:00', '1'], - ['2015-09-17 23:00', '1'], - ['2015-09-18 00:00', '1'], - ['2015-09-18 03:00', '1'], - ['2015-09-18 04:00', '1'], - ['2015-09-18 04:00', '1'], - ['2015-09-18 04:00', '1'], - ['2015-09-18 04:00', '1'], - ['2015-09-18 05:00', '1'], - ['2015-09-18 05:00', '1'], - ['2015-09-18 05:00', '1'], - ['2015-09-18 05:00', '1'], - ['2015-09-18 06:00', '1'], - ['2015-09-18 06:00', '1'], - ['2015-09-18 06:00', '1'], - ['2015-09-18 06:00', '1'], - ['2015-09-18 07:00', '1'], - ['2015-09-18 07:00', '1'], - ['2015-09-18 07:00', '1'], - ]); + + if (await PageObjects.common.isOss()) { + // OSS renders a vertical bar chart and we check the data in the Inspect panel + await inspector.open(); + await inspector.setTablePageSize(50); + await inspector.expectTableData([ + ['2015-09-17 20:00', '1'], + ['2015-09-17 21:00', '1'], + ['2015-09-17 23:00', '1'], + ['2015-09-18 00:00', '1'], + ['2015-09-18 03:00', '1'], + ['2015-09-18 04:00', '1'], + ['2015-09-18 04:00', '1'], + ['2015-09-18 04:00', '1'], + ['2015-09-18 04:00', '1'], + ['2015-09-18 05:00', '1'], + ['2015-09-18 05:00', '1'], + ['2015-09-18 05:00', '1'], + ['2015-09-18 05:00', '1'], + ['2015-09-18 06:00', '1'], + ['2015-09-18 06:00', '1'], + ['2015-09-18 06:00', '1'], + ['2015-09-18 06:00', '1'], + ['2015-09-18 07:00', '1'], + ['2015-09-18 07:00', '1'], + ['2015-09-18 07:00', '1'], + ]); + } else { + // verify Lens opens a visualization + expect(await testSubjects.getVisibleTextAll('lns-dimensionTrigger')).to.contain( + 'painDate' + ); + } }); }); }); diff --git a/test/functional/services/common/browser.ts b/test/functional/services/common/browser.ts index f5fb54c72177f..b3b7fd32eae19 100644 --- a/test/functional/services/common/browser.ts +++ b/test/functional/services/common/browser.ts @@ -216,13 +216,17 @@ export async function BrowserProvider({ getService }: FtrProviderContext) { * Does a drag-and-drop action from one point to another * https://seleniumhq.github.io/selenium/docs/api/javascript/module/selenium-webdriver/lib/input_exports_Actions.html#dragAndDrop * - * @param {{element: WebElementWrapper | {x: number, y: number}, offset: {x: number, y: number}}} from - * @param {{element: WebElementWrapper | {x: number, y: number}, offset: {x: number, y: number}}} to * @return {Promise} */ public async dragAndDrop( - from: { offset?: { x: any; y: any }; location: any }, - to: { offset?: { x: any; y: any }; location: any } + from: { + location: WebElementWrapper | { x?: number; y?: number }; + offset?: { x?: number; y?: number }; + }, + to: { + location: WebElementWrapper | { x?: number; y?: number }; + offset?: { x?: number; y?: number }; + } ) { // The offset should be specified in pixels relative to the center of the element's bounding box const getW3CPoint = (data: any) => { @@ -230,7 +234,11 @@ export async function BrowserProvider({ getService }: FtrProviderContext) { data.offset = {}; } return data.location instanceof WebElementWrapper - ? { x: data.offset.x || 0, y: data.offset.y || 0, origin: data.location._webElement } + ? { + x: data.offset.x || 0, + y: data.offset.y || 0, + origin: data.location._webElement, + } : { x: data.location.x, y: data.location.y, origin: Origin.POINTER }; }; @@ -240,6 +248,62 @@ export async function BrowserProvider({ getService }: FtrProviderContext) { return await this.getActions().move(startPoint).press().move(endPoint).release().perform(); } + /** + * Performs drag and drop for html5 native drag and drop implementation + * There's a bug in Chromedriver for html5 dnd that doesn't allow to use the method `dragAndDrop` defined above + * https://github.com/SeleniumHQ/selenium/issues/6235 + * This implementation simulates user's action by calling the drag and drop specific events directly. + * + * @param {string} from html selector + * @param {string} to html selector + * @return {Promise} + */ + public async html5DragAndDrop(from: string, to: string) { + await this.execute( + ` + function createEvent(typeOfEvent) { + const event = document.createEvent("CustomEvent"); + event.initCustomEvent(typeOfEvent, true, true, null); + event.dataTransfer = { + data: {}, + setData: function (key, value) { + this.data[key] = value; + }, + getData: function (key) { + return this.data[key]; + } + }; + return event; + } + function dispatchEvent(element, event, transferData) { + if (transferData !== undefined) { + event.dataTransfer = transferData; + } + if (element.dispatchEvent) { + element.dispatchEvent(event); + } else if (element.fireEvent) { + element.fireEvent("on" + event.type, event); + } + } + + const origin = document.querySelector(arguments[0]); + const target = document.querySelector(arguments[1]); + + const dragStartEvent = createEvent('dragstart'); + dispatchEvent(origin, dragStartEvent); + + setTimeout(() => { + const dropEvent = createEvent('drop'); + dispatchEvent(target, dropEvent, dragStartEvent.dataTransfer); + const dragEndEvent = createEvent('dragend'); + dispatchEvent(origin, dragEndEvent, dropEvent.dataTransfer); + }, 50); + `, + from, + to + ); + } + /** * Reloads the current browser window/frame. * https://seleniumhq.github.io/selenium/docs/api/javascript/module/selenium-webdriver/lib/webdriver_exports_Navigation.html#refresh diff --git a/test/plugin_functional/plugins/data_search/server/plugin.ts b/test/plugin_functional/plugins/data_search/server/plugin.ts index e016ef56802f3..ca22e82188403 100644 --- a/test/plugin_functional/plugins/data_search/server/plugin.ts +++ b/test/plugin_functional/plugins/data_search/server/plugin.ts @@ -58,14 +58,16 @@ export class DataSearchTestPlugin }, }, async (context, req, res) => { - const [{ savedObjects }, { data }] = await core.getStartServices(); + const [{ savedObjects, elasticsearch }, { data }] = await core.getStartServices(); const service = await data.search.searchSource.asScoped(req); + const clusterClient = elasticsearch.client.asScoped(req).asCurrentUser; const savedObjectsClient = savedObjects.getScopedClient(req); // Since the index pattern ID can change on each test run, we need // to look it up on the fly and insert it into the request. const indexPatterns = await data.indexPatterns.indexPatternsServiceFactory( - savedObjectsClient + savedObjectsClient, + clusterClient ); const ids = await indexPatterns.getIds(); // @ts-expect-error Force overwriting the request diff --git a/test/plugin_functional/plugins/index_patterns/server/plugin.ts b/test/plugin_functional/plugins/index_patterns/server/plugin.ts index a54502b740211..c05b71b834c70 100644 --- a/test/plugin_functional/plugins/index_patterns/server/plugin.ts +++ b/test/plugin_functional/plugins/index_patterns/server/plugin.ts @@ -36,13 +36,35 @@ export class IndexPatternsTestPlugin public setup(core: CoreSetup) { const router = core.http.createRouter(); + router.post( + { + path: '/api/index-patterns-plugin/create', + validate: { + body: schema.object({}, { unknowns: 'allow' }), + }, + }, + async (context, req, res) => { + const [{ savedObjects, elasticsearch }, { data }] = await core.getStartServices(); + const savedObjectsClient = savedObjects.getScopedClient(req); + const service = await data.indexPatterns.indexPatternsServiceFactory( + savedObjectsClient, + elasticsearch.client.asScoped(req).asCurrentUser + ); + const ids = await service.createAndSave(req.body); + return res.ok({ body: ids }); + } + ); + router.get( { path: '/api/index-patterns-plugin/get-all', validate: false }, async (context, req, res) => { - const [{ savedObjects }, { data }] = await core.getStartServices(); + const [{ savedObjects, elasticsearch }, { data }] = await core.getStartServices(); const savedObjectsClient = savedObjects.getScopedClient(req); - const service = await data.indexPatterns.indexPatternsServiceFactory(savedObjectsClient); - const ids = await service.getIds(); + const service = await data.indexPatterns.indexPatternsServiceFactory( + savedObjectsClient, + elasticsearch.client.asScoped(req).asCurrentUser + ); + const ids = await service.getIds(true); return res.ok({ body: ids }); } ); @@ -58,9 +80,12 @@ export class IndexPatternsTestPlugin }, async (context, req, res) => { const id = (req.params as Record).id; - const [{ savedObjects }, { data }] = await core.getStartServices(); + const [{ savedObjects, elasticsearch }, { data }] = await core.getStartServices(); const savedObjectsClient = savedObjects.getScopedClient(req); - const service = await data.indexPatterns.indexPatternsServiceFactory(savedObjectsClient); + const service = await data.indexPatterns.indexPatternsServiceFactory( + savedObjectsClient, + elasticsearch.client.asScoped(req).asCurrentUser + ); const ip = await service.get(id); return res.ok({ body: ip.toSpec() }); } @@ -76,10 +101,13 @@ export class IndexPatternsTestPlugin }, }, async (context, req, res) => { - const [{ savedObjects }, { data }] = await core.getStartServices(); + const [{ savedObjects, elasticsearch }, { data }] = await core.getStartServices(); const id = (req.params as Record).id; const savedObjectsClient = savedObjects.getScopedClient(req); - const service = await data.indexPatterns.indexPatternsServiceFactory(savedObjectsClient); + const service = await data.indexPatterns.indexPatternsServiceFactory( + savedObjectsClient, + elasticsearch.client.asScoped(req).asCurrentUser + ); const ip = await service.get(id); await service.updateSavedObject(ip); return res.ok(); @@ -96,10 +124,13 @@ export class IndexPatternsTestPlugin }, }, async (context, req, res) => { - const [{ savedObjects }, { data }] = await core.getStartServices(); + const [{ savedObjects, elasticsearch }, { data }] = await core.getStartServices(); const id = (req.params as Record).id; const savedObjectsClient = savedObjects.getScopedClient(req); - const service = await data.indexPatterns.indexPatternsServiceFactory(savedObjectsClient); + const service = await data.indexPatterns.indexPatternsServiceFactory( + savedObjectsClient, + elasticsearch.client.asScoped(req).asCurrentUser + ); await service.delete(id); return res.ok(); } diff --git a/test/plugin_functional/test_suites/data_plugin/index_patterns.ts b/test/plugin_functional/test_suites/data_plugin/index_patterns.ts index 2c846dc780311..2e94f61c7ded8 100644 --- a/test/plugin_functional/test_suites/data_plugin/index_patterns.ts +++ b/test/plugin_functional/test_suites/data_plugin/index_patterns.ts @@ -23,16 +23,25 @@ import '../../plugins/core_provider_plugin/types'; export default function ({ getService }: PluginFunctionalProviderContext) { const supertest = getService('supertest'); - // skipping the tests as it deletes index patterns created by other test causing unexpected failures - // https://github.com/elastic/kibana/issues/79886 - describe.skip('index patterns', function () { + describe('index patterns', function () { let indexPatternId = ''; - it('can get all ids', async () => { - const body = await (await supertest.get('/api/index-patterns-plugin/get-all').expect(200)) - .body; - indexPatternId = body[0]; - expect(body.length > 0).to.equal(true); + it('can create an index pattern', async () => { + const title = 'shakes*'; + const fieldFormats = { bytes: { id: 'bytes' } }; + const body = await ( + await supertest + .post('/api/index-patterns-plugin/create') + .set('kbn-xsrf', 'anything') + .send({ title, fieldFormats }) + .expect(200) + ).body; + + indexPatternId = body.id; + expect(body.id).not.empty(); + expect(body.title).to.equal(title); + expect(body.fields.length).to.equal(15); + expect(body.fieldFormatMap).to.eql(fieldFormats); }); it('can get index pattern by id', async () => { diff --git a/x-pack/.i18nrc.json b/x-pack/.i18nrc.json index 3b1e4faf80bce..2be68b797ba5f 100644 --- a/x-pack/.i18nrc.json +++ b/x-pack/.i18nrc.json @@ -28,7 +28,7 @@ "xpack.idxMgmt": "plugins/index_management", "xpack.indexLifecycleMgmt": "plugins/index_lifecycle_management", "xpack.infra": "plugins/infra", - "xpack.fleet": "plugins/ingest_manager", + "xpack.fleet": "plugins/fleet", "xpack.ingestPipelines": "plugins/ingest_pipelines", "xpack.lens": "plugins/lens", "xpack.licenseMgmt": "plugins/license_management", diff --git a/x-pack/examples/alerting_example/public/alert_types/astros.tsx b/x-pack/examples/alerting_example/public/alert_types/astros.tsx index 73c7dfea1263b..54f989b93e22f 100644 --- a/x-pack/examples/alerting_example/public/alert_types/astros.tsx +++ b/x-pack/examples/alerting_example/public/alert_types/astros.tsx @@ -127,9 +127,9 @@ export const PeopleinSpaceExpression: React.FunctionComponent - errs.map((e) => ( -

+ Object.entries(errors).map(([field, errs]: [string, string[]], fieldIndex) => + errs.map((e, index) => ( +

{field}:`: ${errs}`

)) diff --git a/x-pack/examples/alerting_example/server/alert_types/always_firing.ts b/x-pack/examples/alerting_example/server/alert_types/always_firing.ts index bb1cb0d97689b..d02406a23045e 100644 --- a/x-pack/examples/alerting_example/server/alert_types/always_firing.ts +++ b/x-pack/examples/alerting_example/server/alert_types/always_firing.ts @@ -5,25 +5,31 @@ */ import uuid from 'uuid'; -import { range } from 'lodash'; +import { range, random } from 'lodash'; import { AlertType } from '../../../../plugins/alerts/server'; import { DEFAULT_INSTANCES_TO_GENERATE, ALERTING_EXAMPLE_APP_ID } from '../../common/constants'; +const ACTION_GROUPS = [ + { id: 'small', name: 'small' }, + { id: 'medium', name: 'medium' }, + { id: 'large', name: 'large' }, +]; + export const alertType: AlertType = { id: 'example.always-firing', name: 'Always firing', - actionGroups: [{ id: 'default', name: 'default' }], - defaultActionGroupId: 'default', + actionGroups: ACTION_GROUPS, + defaultActionGroupId: 'small', async executor({ services, params: { instances = DEFAULT_INSTANCES_TO_GENERATE }, state }) { const count = (state.count ?? 0) + 1; range(instances) - .map(() => ({ id: uuid.v4() })) - .forEach((instance: { id: string }) => { + .map(() => ({ id: uuid.v4(), tshirtSize: ACTION_GROUPS[random(0, 2)].id! })) + .forEach((instance: { id: string; tshirtSize: string }) => { services .alertInstanceFactory(instance.id) .replaceState({ triggerdOnCycle: count }) - .scheduleActions('default'); + .scheduleActions(instance.tshirtSize); }); return { diff --git a/x-pack/plugins/actions/README.md b/x-pack/plugins/actions/README.md index 4fef9bc582d08..432a4bfff7a6b 100644 --- a/x-pack/plugins/actions/README.md +++ b/x-pack/plugins/actions/README.md @@ -69,18 +69,21 @@ Table of Contents - [`secrets`](#secrets-6) - [`params`](#params-6) - [`subActionParams (pushToService)`](#subactionparams-pushtoservice) + - [`subActionParams (getFields)`](#subactionparams-getfields-1) - [Jira](#jira) - [`config`](#config-7) - [`secrets`](#secrets-7) - [`params`](#params-7) - [`subActionParams (pushToService)`](#subactionparams-pushtoservice-1) - [`subActionParams (issueTypes)`](#subactionparams-issuetypes) + - [`subActionParams (getFields)`](#subactionparams-getfields-2) - [`subActionParams (pushToService)`](#subactionparams-pushtoservice-2) - [IBM Resilient](#ibm-resilient) - [`config`](#config-8) - [`secrets`](#secrets-8) - [`params`](#params-8) - [`subActionParams (pushToService)`](#subactionparams-pushtoservice-3) + - [`subActionParams (getFields)`](#subactionparams-getfields-3) - [Command Line Utility](#command-line-utility) - [Developing New Action Types](#developing-new-action-types) - [licensing](#licensing) @@ -563,7 +566,7 @@ The ServiceNow action uses the [V2 Table API](https://developer.servicenow.com/a | Property | Description | Type | | --------------- | ------------------------------------------------------------------------------------ | ------ | -| subAction | The sub action to perform. It can be `pushToService`, `handshake`, and `getIncident` | string | +| subAction | The sub action to perform. It can be `getFields`, `pushToService`, `handshake`, and `getIncident` | string | | subActionParams | The parameters of the sub action | object | #### `subActionParams (pushToService)` @@ -580,6 +583,10 @@ The ServiceNow action uses the [V2 Table API](https://developer.servicenow.com/a | urgency | The name of the urgency in ServiceNow. | string _(optional)_ | | impact | The name of the impact in ServiceNow. | string _(optional)_ | +#### `subActionParams (getFields)` + +No parameters for `getFields` sub-action. Provide an empty object `{}`. + --- ## Jira @@ -606,7 +613,7 @@ The Jira action uses the [V2 API](https://developer.atlassian.com/cloud/jira/pla | Property | Description | Type | | --------------- | ----------------------------------------------------------------------------------------------------------------------- | ------ | -| subAction | The sub action to perform. It can be `pushToService`, `handshake`, `getIncident`, `issueTypes`, and `fieldsByIssueType` | string | +| subAction | The sub action to perform. It can be `getFields`, `pushToService`, `handshake`, `getIncident`, `issueTypes`, and `fieldsByIssueType` | string | | subActionParams | The parameters of the sub action | object | #### `subActionParams (pushToService)` @@ -627,6 +634,10 @@ The Jira action uses the [V2 API](https://developer.atlassian.com/cloud/jira/pla No parameters for `issueTypes` sub-action. Provide an empty object `{}`. +#### `subActionParams (getFields)` + +No parameters for `getFields` sub-action. Provide an empty object `{}`. + #### `subActionParams (pushToService)` | Property | Description | Type | @@ -655,7 +666,7 @@ ID: `.resilient` | Property | Description | Type | | --------------- | ------------------------------------------------------------------------------------ | ------ | -| subAction | The sub action to perform. It can be `pushToService`, `handshake`, and `getIncident` | string | +| subAction | The sub action to perform. It can be `getFields`, `pushToService`, `handshake`, and `getIncident` | string | | subActionParams | The parameters of the sub action | object | #### `subActionParams (pushToService)` @@ -670,6 +681,10 @@ ID: `.resilient` | incidentTypes | An array with the ids of IBM Resilient incident types. | number[] _(optional)_ | | severityCode | IBM Resilient id of the severity code. | number _(optional)_ | +#### `subActionParams (getFields)` + +No parameters for `getFields` sub-action. Provide an empty object `{}`. + # Command Line Utility The [`kbn-action`](https://github.com/pmuellr/kbn-action) tool can be used to send HTTP requests to the Actions plugin. For instance, to create a Slack action from the `.slack` Action Type, use the following command: diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/api.test.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/api.test.ts index e8fa9f76df778..5a7617ada1bf0 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/jira/api.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/jira/api.test.ts @@ -15,804 +15,792 @@ describe('api', () => { beforeEach(() => { externalService = externalServiceMock.create(); - jest.clearAllMocks(); }); - afterEach(() => { - jest.clearAllMocks(); - }); - - describe('pushToService', () => { - describe('create incident - cases', () => { - test('it creates an incident', async () => { - const params = { ...apiParams, externalId: null }; - const res = await api.pushToService({ - externalService, - mapping, - params, - logger: mockedLogger, - }); - - expect(res).toEqual({ - id: 'incident-1', - title: 'CK-1', - pushedDate: '2020-04-27T10:59:46.202Z', - url: 'https://siem-kibana.atlassian.net/browse/CK-1', - comments: [ - { - commentId: 'case-comment-1', - pushedDate: '2020-04-27T10:59:46.202Z', - }, - { - commentId: 'case-comment-2', - pushedDate: '2020-04-27T10:59:46.202Z', - }, - ], - }); - }); - - test('it creates an incident without comments', async () => { - const params = { ...apiParams, externalId: null, comments: [] }; - const res = await api.pushToService({ - externalService, - mapping, - params, - logger: mockedLogger, - }); - - expect(res).toEqual({ - id: 'incident-1', - title: 'CK-1', - pushedDate: '2020-04-27T10:59:46.202Z', - url: 'https://siem-kibana.atlassian.net/browse/CK-1', - }); - }); - - test('it calls createIncident correctly', async () => { - const params = { ...apiParams, externalId: null }; - await api.pushToService({ externalService, mapping, params, logger: mockedLogger }); - - expect(externalService.createIncident).toHaveBeenCalledWith({ - incident: { - labels: ['kibana', 'elastic'], - priority: 'High', - issueType: '10006', - parent: null, - description: - 'Incident description (created at 2020-04-27T10:59:46.202Z by Elastic User)', - summary: 'Incident title (created at 2020-04-27T10:59:46.202Z by Elastic User)', - }, - }); - expect(externalService.updateIncident).not.toHaveBeenCalled(); - }); - - test('it calls createIncident correctly without mapping', async () => { - const params = { ...apiParams, externalId: null }; - await api.pushToService({ externalService, mapping: null, params, logger: mockedLogger }); - - expect(externalService.createIncident).toHaveBeenCalledWith({ - incident: { - description: 'Incident description', - summary: 'Incident title', - issueType: '10006', - labels: ['kibana', 'elastic'], - priority: 'High', - parent: null, - }, - }); - expect(externalService.updateIncident).not.toHaveBeenCalled(); + describe('create incident - cases', () => { + test('it creates an incident', async () => { + const params = { ...apiParams, externalId: null }; + const res = await api.pushToService({ + externalService, + mapping, + params, + logger: mockedLogger, }); - test('it calls createComment correctly', async () => { - const params = { ...apiParams, externalId: null }; - await api.pushToService({ externalService, mapping, params, logger: mockedLogger }); - expect(externalService.createComment).toHaveBeenCalledTimes(2); - expect(externalService.createComment).toHaveBeenNthCalledWith(1, { - incidentId: 'incident-1', - comment: { + expect(res).toEqual({ + id: 'incident-1', + title: 'CK-1', + pushedDate: '2020-04-27T10:59:46.202Z', + url: 'https://siem-kibana.atlassian.net/browse/CK-1', + comments: [ + { commentId: 'case-comment-1', - comment: 'A comment (added at 2020-04-27T10:59:46.202Z by Elastic User)', - createdAt: '2020-04-27T10:59:46.202Z', - createdBy: { - fullName: 'Elastic User', - username: 'elastic', - }, - updatedAt: '2020-04-27T10:59:46.202Z', - updatedBy: { - fullName: 'Elastic User', - username: 'elastic', - }, + pushedDate: '2020-04-27T10:59:46.202Z', }, - }); - - expect(externalService.createComment).toHaveBeenNthCalledWith(2, { - incidentId: 'incident-1', - comment: { + { commentId: 'case-comment-2', - comment: 'Another comment (added at 2020-04-27T10:59:46.202Z by Elastic User)', - createdAt: '2020-04-27T10:59:46.202Z', - createdBy: { - fullName: 'Elastic User', - username: 'elastic', - }, - updatedAt: '2020-04-27T10:59:46.202Z', - updatedBy: { - fullName: 'Elastic User', - username: 'elastic', - }, + pushedDate: '2020-04-27T10:59:46.202Z', }, - }); + ], }); + }); - test('it calls createComment correctly without mapping', async () => { - const params = { ...apiParams, externalId: null }; - await api.pushToService({ externalService, mapping: null, params, logger: mockedLogger }); - expect(externalService.createComment).toHaveBeenCalledTimes(2); - expect(externalService.createComment).toHaveBeenNthCalledWith(1, { - incidentId: 'incident-1', - comment: { - commentId: 'case-comment-1', - comment: 'A comment', - createdAt: '2020-04-27T10:59:46.202Z', - createdBy: { - fullName: 'Elastic User', - username: 'elastic', - }, - updatedAt: '2020-04-27T10:59:46.202Z', - updatedBy: { - fullName: 'Elastic User', - username: 'elastic', - }, - }, - }); + test('it creates an incident without comments', async () => { + const params = { ...apiParams, externalId: null, comments: [] }; + const res = await api.pushToService({ + externalService, + mapping, + params, + logger: mockedLogger, + }); - expect(externalService.createComment).toHaveBeenNthCalledWith(2, { - incidentId: 'incident-1', - comment: { - commentId: 'case-comment-2', - comment: 'Another comment', - createdAt: '2020-04-27T10:59:46.202Z', - createdBy: { - fullName: 'Elastic User', - username: 'elastic', - }, - updatedAt: '2020-04-27T10:59:46.202Z', - updatedBy: { - fullName: 'Elastic User', - username: 'elastic', - }, - }, - }); + expect(res).toEqual({ + id: 'incident-1', + title: 'CK-1', + pushedDate: '2020-04-27T10:59:46.202Z', + url: 'https://siem-kibana.atlassian.net/browse/CK-1', }); }); - describe('update incident', () => { - test('it updates an incident', async () => { - const res = await api.pushToService({ - externalService, - mapping, - params: apiParams, - logger: mockedLogger, - }); - - expect(res).toEqual({ - id: 'incident-1', - title: 'CK-1', - pushedDate: '2020-04-27T10:59:46.202Z', - url: 'https://siem-kibana.atlassian.net/browse/CK-1', - comments: [ - { - commentId: 'case-comment-1', - pushedDate: '2020-04-27T10:59:46.202Z', - }, - { - commentId: 'case-comment-2', - pushedDate: '2020-04-27T10:59:46.202Z', - }, - ], - }); - }); - - test('it updates an incident without comments', async () => { - const params = { ...apiParams, comments: [] }; - const res = await api.pushToService({ - externalService, - mapping, - params, - logger: mockedLogger, - }); - - expect(res).toEqual({ - id: 'incident-1', - title: 'CK-1', - pushedDate: '2020-04-27T10:59:46.202Z', - url: 'https://siem-kibana.atlassian.net/browse/CK-1', - }); - }); - - test('it calls updateIncident correctly', async () => { - const params = { ...apiParams }; - await api.pushToService({ externalService, mapping, params, logger: mockedLogger }); - - expect(externalService.updateIncident).toHaveBeenCalledWith({ - incidentId: 'incident-3', - incident: { - labels: ['kibana', 'elastic'], - priority: 'High', - issueType: '10006', - parent: null, - description: - 'Incident description (updated at 2020-04-27T10:59:46.202Z by Elastic User)', - summary: 'Incident title (updated at 2020-04-27T10:59:46.202Z by Elastic User)', + test('it calls createIncident correctly', async () => { + const params = { ...apiParams, externalId: null }; + await api.pushToService({ externalService, mapping, params, logger: mockedLogger }); + + expect(externalService.createIncident).toHaveBeenCalledWith({ + incident: { + labels: ['kibana', 'elastic'], + priority: 'High', + issueType: '10006', + parent: null, + description: 'Incident description (created at 2020-04-27T10:59:46.202Z by Elastic User)', + summary: 'Incident title (created at 2020-04-27T10:59:46.202Z by Elastic User)', + }, + }); + expect(externalService.updateIncident).not.toHaveBeenCalled(); + }); + + test('it calls createIncident correctly without mapping', async () => { + const params = { ...apiParams, externalId: null }; + await api.pushToService({ externalService, mapping: null, params, logger: mockedLogger }); + + expect(externalService.createIncident).toHaveBeenCalledWith({ + incident: { + description: 'Incident description', + summary: 'Incident title', + issueType: '10006', + labels: ['kibana', 'elastic'], + priority: 'High', + parent: null, + }, + }); + expect(externalService.updateIncident).not.toHaveBeenCalled(); + }); + + test('it calls createComment correctly', async () => { + const params = { ...apiParams, externalId: null }; + await api.pushToService({ externalService, mapping, params, logger: mockedLogger }); + expect(externalService.createComment).toHaveBeenCalledTimes(2); + expect(externalService.createComment).toHaveBeenNthCalledWith(1, { + incidentId: 'incident-1', + comment: { + commentId: 'case-comment-1', + comment: 'A comment (added at 2020-04-27T10:59:46.202Z by Elastic User)', + createdAt: '2020-04-27T10:59:46.202Z', + createdBy: { + fullName: 'Elastic User', + username: 'elastic', }, - }); - expect(externalService.createIncident).not.toHaveBeenCalled(); - }); - - test('it calls updateIncident correctly without mapping', async () => { - const params = { ...apiParams }; - await api.pushToService({ externalService, mapping: null, params, logger: mockedLogger }); - - expect(externalService.updateIncident).toHaveBeenCalledWith({ - incidentId: 'incident-3', - incident: { - description: 'Incident description', - summary: 'Incident title', - issueType: '10006', - labels: ['kibana', 'elastic'], - priority: 'High', - parent: null, + updatedAt: '2020-04-27T10:59:46.202Z', + updatedBy: { + fullName: 'Elastic User', + username: 'elastic', }, - }); - expect(externalService.createIncident).not.toHaveBeenCalled(); + }, }); - test('it calls createComment correctly', async () => { - const params = { ...apiParams }; - await api.pushToService({ externalService, mapping, params, logger: mockedLogger }); - expect(externalService.createComment).toHaveBeenCalledTimes(2); - expect(externalService.createComment).toHaveBeenNthCalledWith(1, { - incidentId: 'incident-1', - comment: { - commentId: 'case-comment-1', - comment: 'A comment (added at 2020-04-27T10:59:46.202Z by Elastic User)', - createdAt: '2020-04-27T10:59:46.202Z', - createdBy: { - fullName: 'Elastic User', - username: 'elastic', - }, - updatedAt: '2020-04-27T10:59:46.202Z', - updatedBy: { - fullName: 'Elastic User', - username: 'elastic', - }, + expect(externalService.createComment).toHaveBeenNthCalledWith(2, { + incidentId: 'incident-1', + comment: { + commentId: 'case-comment-2', + comment: 'Another comment (added at 2020-04-27T10:59:46.202Z by Elastic User)', + createdAt: '2020-04-27T10:59:46.202Z', + createdBy: { + fullName: 'Elastic User', + username: 'elastic', }, - }); - - expect(externalService.createComment).toHaveBeenNthCalledWith(2, { - incidentId: 'incident-1', - comment: { - commentId: 'case-comment-2', - comment: 'Another comment (added at 2020-04-27T10:59:46.202Z by Elastic User)', - createdAt: '2020-04-27T10:59:46.202Z', - createdBy: { - fullName: 'Elastic User', - username: 'elastic', - }, - updatedAt: '2020-04-27T10:59:46.202Z', - updatedBy: { - fullName: 'Elastic User', - username: 'elastic', - }, + updatedAt: '2020-04-27T10:59:46.202Z', + updatedBy: { + fullName: 'Elastic User', + username: 'elastic', }, - }); + }, }); + }); - test('it calls createComment correctly without mapping', async () => { - const params = { ...apiParams }; - await api.pushToService({ externalService, mapping: null, params, logger: mockedLogger }); - expect(externalService.createComment).toHaveBeenCalledTimes(2); - expect(externalService.createComment).toHaveBeenNthCalledWith(1, { - incidentId: 'incident-1', - comment: { - commentId: 'case-comment-1', - comment: 'A comment', - createdAt: '2020-04-27T10:59:46.202Z', - createdBy: { - fullName: 'Elastic User', - username: 'elastic', - }, - updatedAt: '2020-04-27T10:59:46.202Z', - updatedBy: { - fullName: 'Elastic User', - username: 'elastic', - }, + test('it calls createComment correctly without mapping', async () => { + const params = { ...apiParams, externalId: null }; + await api.pushToService({ externalService, mapping: null, params, logger: mockedLogger }); + expect(externalService.createComment).toHaveBeenCalledTimes(2); + expect(externalService.createComment).toHaveBeenNthCalledWith(1, { + incidentId: 'incident-1', + comment: { + commentId: 'case-comment-1', + comment: 'A comment', + createdAt: '2020-04-27T10:59:46.202Z', + createdBy: { + fullName: 'Elastic User', + username: 'elastic', + }, + updatedAt: '2020-04-27T10:59:46.202Z', + updatedBy: { + fullName: 'Elastic User', + username: 'elastic', }, - }); + }, + }); - expect(externalService.createComment).toHaveBeenNthCalledWith(2, { - incidentId: 'incident-1', - comment: { - commentId: 'case-comment-2', - comment: 'Another comment', - createdAt: '2020-04-27T10:59:46.202Z', - createdBy: { - fullName: 'Elastic User', - username: 'elastic', - }, - updatedAt: '2020-04-27T10:59:46.202Z', - updatedBy: { - fullName: 'Elastic User', - username: 'elastic', - }, + expect(externalService.createComment).toHaveBeenNthCalledWith(2, { + incidentId: 'incident-1', + comment: { + commentId: 'case-comment-2', + comment: 'Another comment', + createdAt: '2020-04-27T10:59:46.202Z', + createdBy: { + fullName: 'Elastic User', + username: 'elastic', + }, + updatedAt: '2020-04-27T10:59:46.202Z', + updatedBy: { + fullName: 'Elastic User', + username: 'elastic', }, - }); + }, }); }); + }); - describe('issueTypes', () => { - test('it returns the issue types correctly', async () => { - const res = await api.issueTypes({ - externalService, - params: {}, - }); - expect(res).toEqual([ + describe('update incident', () => { + test('it updates an incident', async () => { + const res = await api.pushToService({ + externalService, + mapping, + params: apiParams, + logger: mockedLogger, + }); + + expect(res).toEqual({ + id: 'incident-1', + title: 'CK-1', + pushedDate: '2020-04-27T10:59:46.202Z', + url: 'https://siem-kibana.atlassian.net/browse/CK-1', + comments: [ { - id: '10006', - name: 'Task', + commentId: 'case-comment-1', + pushedDate: '2020-04-27T10:59:46.202Z', }, { - id: '10007', - name: 'Bug', + commentId: 'case-comment-2', + pushedDate: '2020-04-27T10:59:46.202Z', }, - ]); + ], }); }); - describe('fieldsByIssueType', () => { - test('it returns the fields correctly', async () => { - const res = await api.fieldsByIssueType({ - externalService, - params: { id: '10006' }, - }); - expect(res).toEqual({ - summary: { allowedValues: [], defaultValue: {} }, - priority: { - allowedValues: [ - { - name: 'Medium', - id: '3', - }, - ], - defaultValue: { name: 'Medium', id: '3' }, - }, - }); + test('it updates an incident without comments', async () => { + const params = { ...apiParams, comments: [] }; + const res = await api.pushToService({ + externalService, + mapping, + params, + logger: mockedLogger, + }); + + expect(res).toEqual({ + id: 'incident-1', + title: 'CK-1', + pushedDate: '2020-04-27T10:59:46.202Z', + url: 'https://siem-kibana.atlassian.net/browse/CK-1', }); }); - describe('getIssues', () => { - test('it returns the issues correctly', async () => { - const res = await api.issues({ - externalService, - params: { title: 'Title test' }, - }); - expect(res).toEqual([ - { - id: '10267', - key: 'RJ-107', - title: 'Test title', - }, - ]); + test('it calls updateIncident correctly', async () => { + const params = { ...apiParams }; + await api.pushToService({ externalService, mapping, params, logger: mockedLogger }); + + expect(externalService.updateIncident).toHaveBeenCalledWith({ + incidentId: 'incident-3', + incident: { + labels: ['kibana', 'elastic'], + priority: 'High', + issueType: '10006', + parent: null, + description: 'Incident description (updated at 2020-04-27T10:59:46.202Z by Elastic User)', + summary: 'Incident title (updated at 2020-04-27T10:59:46.202Z by Elastic User)', + }, }); + expect(externalService.createIncident).not.toHaveBeenCalled(); }); - describe('getIssue', () => { - test('it returns the issue correctly', async () => { - const res = await api.issue({ - externalService, - params: { id: 'RJ-107' }, - }); - expect(res).toEqual({ - id: '10267', - key: 'RJ-107', - title: 'Test title', - }); + test('it calls updateIncident correctly without mapping', async () => { + const params = { ...apiParams }; + await api.pushToService({ externalService, mapping: null, params, logger: mockedLogger }); + + expect(externalService.updateIncident).toHaveBeenCalledWith({ + incidentId: 'incident-3', + incident: { + description: 'Incident description', + summary: 'Incident title', + issueType: '10006', + labels: ['kibana', 'elastic'], + priority: 'High', + parent: null, + }, }); + expect(externalService.createIncident).not.toHaveBeenCalled(); }); - describe('mapping variations', () => { - test('overwrite & append', async () => { - mapping.set('title', { - target: 'summary', - actionType: 'overwrite', - }); - - mapping.set('description', { - target: 'description', - actionType: 'append', - }); - - mapping.set('comments', { - target: 'comments', - actionType: 'append', - }); - - mapping.set('summary', { - target: 'title', - actionType: 'overwrite', - }); - - await api.pushToService({ - externalService, - mapping, - params: apiParams, - logger: mockedLogger, - }); - expect(externalService.updateIncident).toHaveBeenCalledWith({ - incidentId: 'incident-3', - incident: { - labels: ['kibana', 'elastic'], - priority: 'High', - issueType: '10006', - parent: null, - summary: 'Incident title (updated at 2020-04-27T10:59:46.202Z by Elastic User)', - description: - 'description from jira \r\nIncident description (updated at 2020-04-27T10:59:46.202Z by Elastic User)', - }, - }); - }); - - test('nothing & append', async () => { - mapping.set('title', { - target: 'summary', - actionType: 'nothing', - }); - - mapping.set('description', { - target: 'description', - actionType: 'append', - }); - - mapping.set('comments', { - target: 'comments', - actionType: 'append', - }); - - mapping.set('summary', { - target: 'title', - actionType: 'nothing', - }); - - await api.pushToService({ - externalService, - mapping, - params: apiParams, - logger: mockedLogger, - }); - expect(externalService.updateIncident).toHaveBeenCalledWith({ - incidentId: 'incident-3', - incident: { - labels: ['kibana', 'elastic'], - priority: 'High', - issueType: '10006', - parent: null, - description: - 'description from jira \r\nIncident description (updated at 2020-04-27T10:59:46.202Z by Elastic User)', + test('it calls createComment correctly', async () => { + const params = { ...apiParams }; + await api.pushToService({ externalService, mapping, params, logger: mockedLogger }); + expect(externalService.createComment).toHaveBeenCalledTimes(2); + expect(externalService.createComment).toHaveBeenNthCalledWith(1, { + incidentId: 'incident-1', + comment: { + commentId: 'case-comment-1', + comment: 'A comment (added at 2020-04-27T10:59:46.202Z by Elastic User)', + createdAt: '2020-04-27T10:59:46.202Z', + createdBy: { + fullName: 'Elastic User', + username: 'elastic', }, - }); - }); - - test('append & append', async () => { - mapping.set('title', { - target: 'summary', - actionType: 'append', - }); - - mapping.set('description', { - target: 'description', - actionType: 'append', - }); - - mapping.set('comments', { - target: 'comments', - actionType: 'append', - }); - - mapping.set('summary', { - target: 'title', - actionType: 'append', - }); - - await api.pushToService({ - externalService, - mapping, - params: apiParams, - logger: mockedLogger, - }); - expect(externalService.updateIncident).toHaveBeenCalledWith({ - incidentId: 'incident-3', - incident: { - labels: ['kibana', 'elastic'], - priority: 'High', - issueType: '10006', - parent: null, - summary: - 'title from jira \r\nIncident title (updated at 2020-04-27T10:59:46.202Z by Elastic User)', - description: - 'description from jira \r\nIncident description (updated at 2020-04-27T10:59:46.202Z by Elastic User)', + updatedAt: '2020-04-27T10:59:46.202Z', + updatedBy: { + fullName: 'Elastic User', + username: 'elastic', }, - }); - }); - - test('nothing & nothing', async () => { - mapping.set('title', { - target: 'summary', - actionType: 'nothing', - }); - - mapping.set('description', { - target: 'description', - actionType: 'nothing', - }); - - mapping.set('comments', { - target: 'comments', - actionType: 'append', - }); - - mapping.set('summary', { - target: 'title', - actionType: 'nothing', - }); - - await api.pushToService({ - externalService, - mapping, - params: apiParams, - logger: mockedLogger, - }); - expect(externalService.updateIncident).toHaveBeenCalledWith({ - incidentId: 'incident-3', - incident: { - labels: ['kibana', 'elastic'], - priority: 'High', - issueType: '10006', - parent: null, + }, + }); + + expect(externalService.createComment).toHaveBeenNthCalledWith(2, { + incidentId: 'incident-1', + comment: { + commentId: 'case-comment-2', + comment: 'Another comment (added at 2020-04-27T10:59:46.202Z by Elastic User)', + createdAt: '2020-04-27T10:59:46.202Z', + createdBy: { + fullName: 'Elastic User', + username: 'elastic', }, - }); - }); - - test('overwrite & nothing', async () => { - mapping.set('title', { - target: 'summary', - actionType: 'overwrite', - }); - - mapping.set('description', { - target: 'description', - actionType: 'nothing', - }); - - mapping.set('comments', { - target: 'comments', - actionType: 'append', - }); - - mapping.set('summary', { - target: 'title', - actionType: 'overwrite', - }); - - await api.pushToService({ - externalService, - mapping, - params: apiParams, - logger: mockedLogger, - }); - expect(externalService.updateIncident).toHaveBeenCalledWith({ - incidentId: 'incident-3', - incident: { - labels: ['kibana', 'elastic'], - priority: 'High', - issueType: '10006', - parent: null, - summary: 'Incident title (updated at 2020-04-27T10:59:46.202Z by Elastic User)', + updatedAt: '2020-04-27T10:59:46.202Z', + updatedBy: { + fullName: 'Elastic User', + username: 'elastic', }, - }); - }); - - test('overwrite & overwrite', async () => { - mapping.set('title', { - target: 'summary', - actionType: 'overwrite', - }); - - mapping.set('description', { - target: 'description', - actionType: 'overwrite', - }); - - mapping.set('comments', { - target: 'comments', - actionType: 'append', - }); - - mapping.set('summary', { - target: 'title', - actionType: 'overwrite', - }); - - await api.pushToService({ - externalService, - mapping, - params: apiParams, - logger: mockedLogger, - }); - expect(externalService.updateIncident).toHaveBeenCalledWith({ - incidentId: 'incident-3', - incident: { - labels: ['kibana', 'elastic'], - priority: 'High', - issueType: '10006', - parent: null, - summary: 'Incident title (updated at 2020-04-27T10:59:46.202Z by Elastic User)', - description: - 'Incident description (updated at 2020-04-27T10:59:46.202Z by Elastic User)', + }, + }); + }); + + test('it calls createComment correctly without mapping', async () => { + const params = { ...apiParams }; + await api.pushToService({ externalService, mapping: null, params, logger: mockedLogger }); + expect(externalService.createComment).toHaveBeenCalledTimes(2); + expect(externalService.createComment).toHaveBeenNthCalledWith(1, { + incidentId: 'incident-1', + comment: { + commentId: 'case-comment-1', + comment: 'A comment', + createdAt: '2020-04-27T10:59:46.202Z', + createdBy: { + fullName: 'Elastic User', + username: 'elastic', }, - }); - }); - - test('nothing & overwrite', async () => { - mapping.set('title', { - target: 'summary', - actionType: 'nothing', - }); - - mapping.set('description', { - target: 'description', - actionType: 'overwrite', - }); - - mapping.set('comments', { - target: 'comments', - actionType: 'append', - }); - - mapping.set('summary', { - target: 'title', - actionType: 'nothing', - }); - - await api.pushToService({ - externalService, - mapping, - params: apiParams, - logger: mockedLogger, - }); - expect(externalService.updateIncident).toHaveBeenCalledWith({ - incidentId: 'incident-3', - incident: { - labels: ['kibana', 'elastic'], - priority: 'High', - issueType: '10006', - parent: null, - description: - 'Incident description (updated at 2020-04-27T10:59:46.202Z by Elastic User)', + updatedAt: '2020-04-27T10:59:46.202Z', + updatedBy: { + fullName: 'Elastic User', + username: 'elastic', }, - }); - }); - - test('append & overwrite', async () => { - mapping.set('title', { - target: 'summary', - actionType: 'append', - }); - - mapping.set('description', { - target: 'description', - actionType: 'overwrite', - }); - - mapping.set('comments', { - target: 'comments', - actionType: 'append', - }); - - mapping.set('summary', { - target: 'title', - actionType: 'append', - }); - - await api.pushToService({ - externalService, - mapping, - params: apiParams, - logger: mockedLogger, - }); - expect(externalService.updateIncident).toHaveBeenCalledWith({ - incidentId: 'incident-3', - incident: { - labels: ['kibana', 'elastic'], - priority: 'High', - issueType: '10006', - parent: null, - summary: - 'title from jira \r\nIncident title (updated at 2020-04-27T10:59:46.202Z by Elastic User)', - description: - 'Incident description (updated at 2020-04-27T10:59:46.202Z by Elastic User)', + }, + }); + + expect(externalService.createComment).toHaveBeenNthCalledWith(2, { + incidentId: 'incident-1', + comment: { + commentId: 'case-comment-2', + comment: 'Another comment', + createdAt: '2020-04-27T10:59:46.202Z', + createdBy: { + fullName: 'Elastic User', + username: 'elastic', }, - }); - }); - - test('append & nothing', async () => { - mapping.set('title', { - target: 'summary', - actionType: 'append', - }); - - mapping.set('description', { - target: 'description', - actionType: 'nothing', - }); - - mapping.set('comments', { - target: 'comments', - actionType: 'append', - }); - - mapping.set('summary', { - target: 'title', - actionType: 'append', - }); - - await api.pushToService({ - externalService, - mapping, - params: apiParams, - logger: mockedLogger, - }); - expect(externalService.updateIncident).toHaveBeenCalledWith({ - incidentId: 'incident-3', - incident: { - labels: ['kibana', 'elastic'], - priority: 'High', - issueType: '10006', - parent: null, - summary: - 'title from jira \r\nIncident title (updated at 2020-04-27T10:59:46.202Z by Elastic User)', + updatedAt: '2020-04-27T10:59:46.202Z', + updatedBy: { + fullName: 'Elastic User', + username: 'elastic', }, - }); - }); - - test('comment nothing', async () => { - mapping.set('title', { - target: 'summary', - actionType: 'overwrite', - }); - - mapping.set('description', { - target: 'description', - actionType: 'nothing', - }); - - mapping.set('comments', { - target: 'comments', - actionType: 'nothing', - }); - - mapping.set('summary', { - target: 'title', - actionType: 'overwrite', - }); - - await api.pushToService({ - externalService, - mapping, - params: apiParams, - logger: mockedLogger, - }); - expect(externalService.createComment).not.toHaveBeenCalled(); + }, + }); + }); + }); + + describe('issueTypes', () => { + test('it returns the issue types correctly', async () => { + const res = await api.issueTypes({ + externalService, + params: {}, + }); + expect(res).toEqual([ + { + id: '10006', + name: 'Task', + }, + { + id: '10007', + name: 'Bug', + }, + ]); + }); + }); + + describe('fieldsByIssueType', () => { + test('it returns the fields correctly', async () => { + const res = await api.fieldsByIssueType({ + externalService, + params: { id: '10006' }, + }); + expect(res).toEqual({ + summary: { allowedValues: [], defaultValue: {} }, + priority: { + allowedValues: [ + { + name: 'Medium', + id: '3', + }, + ], + defaultValue: { name: 'Medium', id: '3' }, + }, + }); + }); + }); + + describe('getIssues', () => { + test('it returns the issues correctly', async () => { + const res = await api.issues({ + externalService, + params: { title: 'Title test' }, + }); + expect(res).toEqual([ + { + id: '10267', + key: 'RJ-107', + title: 'Test title', + }, + ]); + }); + }); + + describe('getIssue', () => { + test('it returns the issue correctly', async () => { + const res = await api.issue({ + externalService, + params: { id: 'RJ-107' }, + }); + expect(res).toEqual({ + id: '10267', + key: 'RJ-107', + title: 'Test title', + }); + }); + }); + + describe('mapping variations', () => { + test('overwrite & append', async () => { + mapping.set('title', { + target: 'summary', + actionType: 'overwrite', + }); + + mapping.set('description', { + target: 'description', + actionType: 'append', + }); + + mapping.set('comments', { + target: 'comments', + actionType: 'append', + }); + + mapping.set('summary', { + target: 'title', + actionType: 'overwrite', + }); + + await api.pushToService({ + externalService, + mapping, + params: apiParams, + logger: mockedLogger, + }); + expect(externalService.updateIncident).toHaveBeenCalledWith({ + incidentId: 'incident-3', + incident: { + labels: ['kibana', 'elastic'], + priority: 'High', + issueType: '10006', + parent: null, + summary: 'Incident title (updated at 2020-04-27T10:59:46.202Z by Elastic User)', + description: + 'description from jira \r\nIncident description (updated at 2020-04-27T10:59:46.202Z by Elastic User)', + }, + }); + }); + + test('nothing & append', async () => { + mapping.set('title', { + target: 'summary', + actionType: 'nothing', + }); + + mapping.set('description', { + target: 'description', + actionType: 'append', + }); + + mapping.set('comments', { + target: 'comments', + actionType: 'append', + }); + + mapping.set('summary', { + target: 'title', + actionType: 'nothing', + }); + + await api.pushToService({ + externalService, + mapping, + params: apiParams, + logger: mockedLogger, + }); + expect(externalService.updateIncident).toHaveBeenCalledWith({ + incidentId: 'incident-3', + incident: { + labels: ['kibana', 'elastic'], + priority: 'High', + issueType: '10006', + parent: null, + description: + 'description from jira \r\nIncident description (updated at 2020-04-27T10:59:46.202Z by Elastic User)', + }, + }); + }); + + test('append & append', async () => { + mapping.set('title', { + target: 'summary', + actionType: 'append', + }); + + mapping.set('description', { + target: 'description', + actionType: 'append', + }); + + mapping.set('comments', { + target: 'comments', + actionType: 'append', + }); + + mapping.set('summary', { + target: 'title', + actionType: 'append', + }); + + await api.pushToService({ + externalService, + mapping, + params: apiParams, + logger: mockedLogger, + }); + expect(externalService.updateIncident).toHaveBeenCalledWith({ + incidentId: 'incident-3', + incident: { + labels: ['kibana', 'elastic'], + priority: 'High', + issueType: '10006', + parent: null, + summary: + 'title from jira \r\nIncident title (updated at 2020-04-27T10:59:46.202Z by Elastic User)', + description: + 'description from jira \r\nIncident description (updated at 2020-04-27T10:59:46.202Z by Elastic User)', + }, + }); + }); + + test('nothing & nothing', async () => { + mapping.set('title', { + target: 'summary', + actionType: 'nothing', + }); + + mapping.set('description', { + target: 'description', + actionType: 'nothing', + }); + + mapping.set('comments', { + target: 'comments', + actionType: 'append', + }); + + mapping.set('summary', { + target: 'title', + actionType: 'nothing', + }); + + await api.pushToService({ + externalService, + mapping, + params: apiParams, + logger: mockedLogger, + }); + expect(externalService.updateIncident).toHaveBeenCalledWith({ + incidentId: 'incident-3', + incident: { + labels: ['kibana', 'elastic'], + priority: 'High', + issueType: '10006', + parent: null, + }, + }); + }); + + test('overwrite & nothing', async () => { + mapping.set('title', { + target: 'summary', + actionType: 'overwrite', + }); + + mapping.set('description', { + target: 'description', + actionType: 'nothing', + }); + + mapping.set('comments', { + target: 'comments', + actionType: 'append', + }); + + mapping.set('summary', { + target: 'title', + actionType: 'overwrite', + }); + + await api.pushToService({ + externalService, + mapping, + params: apiParams, + logger: mockedLogger, + }); + expect(externalService.updateIncident).toHaveBeenCalledWith({ + incidentId: 'incident-3', + incident: { + labels: ['kibana', 'elastic'], + priority: 'High', + issueType: '10006', + parent: null, + summary: 'Incident title (updated at 2020-04-27T10:59:46.202Z by Elastic User)', + }, + }); + }); + + test('overwrite & overwrite', async () => { + mapping.set('title', { + target: 'summary', + actionType: 'overwrite', + }); + + mapping.set('description', { + target: 'description', + actionType: 'overwrite', + }); + + mapping.set('comments', { + target: 'comments', + actionType: 'append', + }); + + mapping.set('summary', { + target: 'title', + actionType: 'overwrite', + }); + + await api.pushToService({ + externalService, + mapping, + params: apiParams, + logger: mockedLogger, + }); + expect(externalService.updateIncident).toHaveBeenCalledWith({ + incidentId: 'incident-3', + incident: { + labels: ['kibana', 'elastic'], + priority: 'High', + issueType: '10006', + parent: null, + summary: 'Incident title (updated at 2020-04-27T10:59:46.202Z by Elastic User)', + description: 'Incident description (updated at 2020-04-27T10:59:46.202Z by Elastic User)', + }, + }); + }); + + test('nothing & overwrite', async () => { + mapping.set('title', { + target: 'summary', + actionType: 'nothing', + }); + + mapping.set('description', { + target: 'description', + actionType: 'overwrite', + }); + + mapping.set('comments', { + target: 'comments', + actionType: 'append', + }); + + mapping.set('summary', { + target: 'title', + actionType: 'nothing', + }); + + await api.pushToService({ + externalService, + mapping, + params: apiParams, + logger: mockedLogger, + }); + expect(externalService.updateIncident).toHaveBeenCalledWith({ + incidentId: 'incident-3', + incident: { + labels: ['kibana', 'elastic'], + priority: 'High', + issueType: '10006', + parent: null, + description: 'Incident description (updated at 2020-04-27T10:59:46.202Z by Elastic User)', + }, + }); + }); + + test('append & overwrite', async () => { + mapping.set('title', { + target: 'summary', + actionType: 'append', + }); + + mapping.set('description', { + target: 'description', + actionType: 'overwrite', + }); + + mapping.set('comments', { + target: 'comments', + actionType: 'append', + }); + + mapping.set('summary', { + target: 'title', + actionType: 'append', + }); + + await api.pushToService({ + externalService, + mapping, + params: apiParams, + logger: mockedLogger, + }); + expect(externalService.updateIncident).toHaveBeenCalledWith({ + incidentId: 'incident-3', + incident: { + labels: ['kibana', 'elastic'], + priority: 'High', + issueType: '10006', + parent: null, + summary: + 'title from jira \r\nIncident title (updated at 2020-04-27T10:59:46.202Z by Elastic User)', + description: 'Incident description (updated at 2020-04-27T10:59:46.202Z by Elastic User)', + }, + }); + }); + + test('append & nothing', async () => { + mapping.set('title', { + target: 'summary', + actionType: 'append', + }); + + mapping.set('description', { + target: 'description', + actionType: 'nothing', + }); + + mapping.set('comments', { + target: 'comments', + actionType: 'append', + }); + + mapping.set('summary', { + target: 'title', + actionType: 'append', + }); + + await api.pushToService({ + externalService, + mapping, + params: apiParams, + logger: mockedLogger, + }); + expect(externalService.updateIncident).toHaveBeenCalledWith({ + incidentId: 'incident-3', + incident: { + labels: ['kibana', 'elastic'], + priority: 'High', + issueType: '10006', + parent: null, + summary: + 'title from jira \r\nIncident title (updated at 2020-04-27T10:59:46.202Z by Elastic User)', + }, + }); + }); + + test('comment nothing', async () => { + mapping.set('title', { + target: 'summary', + actionType: 'overwrite', + }); + + mapping.set('description', { + target: 'description', + actionType: 'nothing', + }); + + mapping.set('comments', { + target: 'comments', + actionType: 'nothing', + }); + + mapping.set('summary', { + target: 'title', + actionType: 'overwrite', + }); + + await api.pushToService({ + externalService, + mapping, + params: apiParams, + logger: mockedLogger, }); + expect(externalService.createComment).not.toHaveBeenCalled(); }); }); }); diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/api.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/api.ts index 679c1541964ce..feeb69b1d1a0e 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/jira/api.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/jira/api.ts @@ -17,6 +17,7 @@ import { PushToServiceApiParams, PushToServiceResponse, GetIssueHandlerArgs, + GetCommonFieldsHandlerArgs, } from './types'; // TODO: to remove, need to support Case @@ -39,6 +40,11 @@ const getIssueTypesHandler = async ({ externalService }: GetIssueTypesHandlerArg return res; }; +const getFieldsHandler = async ({ externalService }: GetCommonFieldsHandlerArgs) => { + const res = await externalService.getFields(); + return res; +}; + const getFieldsByIssueTypeHandler = async ({ externalService, params, @@ -157,6 +163,7 @@ const pushToServiceHandler = async ({ }; export const api: ExternalServiceApi = { + getFields: getFieldsHandler, handshake: handshakeHandler, pushToService: pushToServiceHandler, getIncident: getIncidentHandler, diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/index.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/index.ts index 9d6ff90c33700..c70c0810926f4 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/jira/index.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/jira/index.ts @@ -40,6 +40,7 @@ interface GetActionTypeParams { } const supportedSubActions: string[] = [ + 'getFields', 'pushToService', 'issueTypes', 'fieldsByIssueType', @@ -145,6 +146,13 @@ async function executor( }); } + if (subAction === 'getFields') { + data = await api.getFields({ + externalService, + params: subActionParams, + }); + } + if (subAction === 'issues') { const getIssuesParams = subActionParams as ExecutorSubActionGetIssuesParams; data = await api.issues({ diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/mocks.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/mocks.ts index b98eda799e3aa..87a0f156a0c2a 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/jira/mocks.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/jira/mocks.ts @@ -73,6 +73,20 @@ const createMock = (): jest.Mocked => { key: 'RJ-107', title: 'Test title', })), + getFields: jest.fn().mockImplementation(() => ({ + description: { + allowedValues: [], + defaultValue: {}, + required: true, + schema: { type: 'string' }, + }, + summary: { + allowedValues: [], + defaultValue: {}, + required: true, + schema: { type: 'string' }, + }, + })), }; service.createComment.mockImplementationOnce(() => @@ -97,7 +111,6 @@ const createMock = (): jest.Mocked => { const externalServiceMock = { create: createMock, }; - const mapping: Map> = new Map(); mapping.set('title', { diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/schema.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/schema.ts index 513ca2cf18e6c..70b60ada9c386 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/jira/schema.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/jira/schema.ts @@ -55,6 +55,7 @@ export const ExecutorSubActionGetIncidentParamsSchema = schema.object({ }); // Reserved for future implementation +export const ExecutorSubActionCommonFieldsParamsSchema = schema.object({}); export const ExecutorSubActionHandshakeParamsSchema = schema.object({}); export const ExecutorSubActionGetCapabilitiesParamsSchema = schema.object({}); export const ExecutorSubActionGetIssueTypesParamsSchema = schema.object({}); @@ -65,6 +66,10 @@ export const ExecutorSubActionGetIssuesParamsSchema = schema.object({ title: sch export const ExecutorSubActionGetIssueParamsSchema = schema.object({ id: schema.string() }); export const ExecutorParamsSchema = schema.oneOf([ + schema.object({ + subAction: schema.literal('getFields'), + subActionParams: ExecutorSubActionCommonFieldsParamsSchema, + }), schema.object({ subAction: schema.literal('getIncident'), subActionParams: ExecutorSubActionGetIncidentParamsSchema, diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/service.test.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/service.test.ts index fe4e135c76fc3..2165ba56428c9 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/jira/service.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/jira/service.test.ts @@ -57,8 +57,10 @@ const fieldsResponse = { id: '10006', name: 'Task', fields: { - summary: { fieldId: 'summary' }, + summary: { required: true, schema: { type: 'string' }, fieldId: 'summary' }, priority: { + required: false, + schema: { type: 'string' }, fieldId: 'priority', allowedValues: [ { @@ -198,7 +200,7 @@ describe('Jira service', () => { error.response = { data: { errors: { summary: 'Required field' } } }; throw error; }); - expect(service.getIncident('1')).rejects.toThrow( + await expect(service.getIncident('1')).rejects.toThrow( '[Action][Jira]: Unable to get incident with id 1. Error: An error has occurred Reason: Required field' ); }); @@ -348,7 +350,7 @@ describe('Jira service', () => { throw error; }); - expect( + await expect( service.createIncident({ incident: { summary: 'title', @@ -442,7 +444,7 @@ describe('Jira service', () => { throw error; }); - expect( + await expect( service.updateIncident({ incidentId: '1', incident: { @@ -526,7 +528,7 @@ describe('Jira service', () => { throw error; }); - expect( + await expect( service.createComment({ incidentId: '1', comment: { @@ -587,7 +589,7 @@ describe('Jira service', () => { throw error; }); - expect(service.getCapabilities()).rejects.toThrow( + await expect(service.getCapabilities()).rejects.toThrow( '[Action][Jira]: Unable to get capabilities. Error: An error has occurred. Reason: Could not get capabilities' ); }); @@ -657,7 +659,7 @@ describe('Jira service', () => { throw error; }); - expect(service.getIssueTypes()).rejects.toThrow( + await expect(service.getIssueTypes()).rejects.toThrow( '[Action][Jira]: Unable to get issue types. Error: An error has occurred. Reason: Could not get issue types' ); }); @@ -741,7 +743,7 @@ describe('Jira service', () => { throw error; }); - expect(service.getIssueTypes()).rejects.toThrow( + await expect(service.getIssueTypes()).rejects.toThrow( '[Action][Jira]: Unable to get issue types. Error: An error has occurred. Reason: Could not get issue types' ); }); @@ -765,6 +767,8 @@ describe('Jira service', () => { expect(res).toEqual({ priority: { + required: false, + schema: { type: 'string' }, allowedValues: [ { id: '1', name: 'Highest' }, { id: '2', name: 'High' }, @@ -774,7 +778,12 @@ describe('Jira service', () => { ], defaultValue: { id: '3', name: 'Medium' }, }, - summary: { allowedValues: [], defaultValue: {} }, + summary: { + required: true, + schema: { type: 'string' }, + allowedValues: [], + defaultValue: {}, + }, }); }); @@ -815,7 +824,7 @@ describe('Jira service', () => { throw error; }); - expect(service.getFieldsByIssueType('10006')).rejects.toThrow( + await expect(service.getFieldsByIssueType('10006')).rejects.toThrow( '[Action][Jira]: Unable to get fields. Error: An error has occurred. Reason: Could not get fields' ); }); @@ -837,8 +846,10 @@ describe('Jira service', () => { requestMock.mockImplementationOnce(() => ({ data: { values: [ - { fieldId: 'summary' }, + { required: true, schema: { type: 'string' }, fieldId: 'summary' }, { + required: false, + schema: { type: 'string' }, fieldId: 'priority', allowedValues: [ { @@ -859,10 +870,17 @@ describe('Jira service', () => { expect(res).toEqual({ priority: { + required: false, + schema: { type: 'string' }, allowedValues: [{ id: '3', name: 'Medium' }], defaultValue: { id: '3', name: 'Medium' }, }, - summary: { allowedValues: [], defaultValue: {} }, + summary: { + required: true, + schema: { type: 'string' }, + allowedValues: [], + defaultValue: {}, + }, }); }); @@ -881,8 +899,10 @@ describe('Jira service', () => { requestMock.mockImplementationOnce(() => ({ data: { values: [ - { fieldId: 'summary' }, + { required: true, schema: { type: 'string' }, fieldId: 'summary' }, { + required: true, + schema: { type: 'string' }, fieldId: 'priority', allowedValues: [ { @@ -927,7 +947,7 @@ describe('Jira service', () => { throw error; }); - expect(service.getFieldsByIssueType('10006')).rejects.toThrow( + await expect(service.getFieldsByIssueType('10006')).rejects.toThrowError( '[Action][Jira]: Unable to get fields. Error: An error has occurred. Reason: Could not get issue types' ); }); @@ -976,7 +996,7 @@ describe('Jira service', () => { throw error; }); - expect(service.getIssues('Test title')).rejects.toThrow( + await expect(service.getIssues('Test title')).rejects.toThrow( '[Action][Jira]: Unable to get issues. Error: An error has occurred. Reason: Could not get issue types' ); }); @@ -1020,9 +1040,128 @@ describe('Jira service', () => { throw error; }); - expect(service.getIssue('RJ-107')).rejects.toThrow( + await expect(service.getIssue('RJ-107')).rejects.toThrow( '[Action][Jira]: Unable to get issue with id RJ-107. Error: An error has occurred. Reason: Could not get issue types' ); }); }); + + describe('getFields', () => { + const callMocks = () => { + requestMock + .mockImplementationOnce(() => ({ + data: { + capabilities: { + 'list-project-issuetypes': + 'https://siem-kibana.atlassian.net/rest/capabilities/list-project-issuetypes', + 'list-issuetype-fields': + 'https://siem-kibana.atlassian.net/rest/capabilities/list-issuetype-fields', + }, + }, + })) + .mockImplementationOnce(() => ({ + data: { + values: issueTypesResponse.data.projects[0].issuetypes, + }, + })) + .mockImplementationOnce(() => ({ + data: { + capabilities: { + 'list-project-issuetypes': + 'https://siem-kibana.atlassian.net/rest/capabilities/list-project-issuetypes', + 'list-issuetype-fields': + 'https://siem-kibana.atlassian.net/rest/capabilities/list-issuetype-fields', + }, + }, + })) + .mockImplementationOnce(() => ({ + data: { + capabilities: { + 'list-project-issuetypes': + 'https://siem-kibana.atlassian.net/rest/capabilities/list-project-issuetypes', + 'list-issuetype-fields': + 'https://siem-kibana.atlassian.net/rest/capabilities/list-issuetype-fields', + }, + }, + })) + .mockImplementationOnce(() => ({ + data: { + values: [ + { required: true, schema: { type: 'string' }, fieldId: 'summary' }, + { required: true, schema: { type: 'string' }, fieldId: 'description' }, + { + required: false, + schema: { type: 'string' }, + fieldId: 'priority', + allowedValues: [ + { + name: 'Medium', + id: '3', + }, + ], + defaultValue: { + name: 'Medium', + id: '3', + }, + }, + ], + }, + })) + .mockImplementationOnce(() => ({ + data: { + values: [ + { required: true, schema: { type: 'string' }, fieldId: 'summary' }, + { required: true, schema: { type: 'string' }, fieldId: 'description' }, + ], + }, + })); + }; + beforeEach(() => { + jest.resetAllMocks(); + }); + test('it should call request with correct arguments', async () => { + callMocks(); + await service.getFields(); + const callUrls = [ + 'https://siem-kibana.atlassian.net/rest/capabilities', + 'https://siem-kibana.atlassian.net/rest/api/2/issue/createmeta/CK/issuetypes', + 'https://siem-kibana.atlassian.net/rest/capabilities', + 'https://siem-kibana.atlassian.net/rest/capabilities', + 'https://siem-kibana.atlassian.net/rest/api/2/issue/createmeta/CK/issuetypes/10006', + 'https://siem-kibana.atlassian.net/rest/api/2/issue/createmeta/CK/issuetypes/10007', + ]; + requestMock.mock.calls.forEach((call, i) => { + expect(call[0].url).toEqual(callUrls[i]); + }); + }); + test('it returns common fields correctly', async () => { + callMocks(); + const res = await service.getFields(); + expect(res).toEqual({ + description: { + allowedValues: [], + defaultValue: {}, + required: true, + schema: { type: 'string' }, + }, + summary: { + allowedValues: [], + defaultValue: {}, + required: true, + schema: { type: 'string' }, + }, + }); + }); + + test('it should throw an error', async () => { + requestMock.mockImplementation(() => { + const error: ResponseError = new Error('An error has occurred'); + error.response = { data: { errors: { summary: 'Required field' } } }; + throw error; + }); + await expect(service.getFields()).rejects.toThrow( + '[Action][Jira]: Unable to get capabilities. Error: An error has occurred. Reason: Required field' + ); + }); + }); }); diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/service.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/service.ts index f5347891f4f70..b3c5bb4a84de5 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/jira/service.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/jira/service.ts @@ -8,18 +8,20 @@ import axios from 'axios'; import { Logger } from '../../../../../../src/core/server'; import { - ExternalServiceCredentials, - ExternalService, + CreateCommentParams, CreateIncidentParams, - UpdateIncidentParams, - JiraPublicConfigurationType, - JiraSecretConfigurationType, + ExternalService, + ExternalServiceCommentResponse, + ExternalServiceCredentials, + ExternalServiceIncidentResponse, Fields, - CreateCommentParams, + FieldSchema, + GetCommonFieldsResponse, Incident, + JiraPublicConfigurationType, + JiraSecretConfigurationType, ResponseError, - ExternalServiceCommentResponse, - ExternalServiceIncidentResponse, + UpdateIncidentParams, } from './types'; import * as i18n from './translations'; @@ -127,14 +129,21 @@ export const createExternalService = ( issueTypes.map((type) => ({ id: type.id, name: type.name })); const normalizeFields = (fields: { - [key: string]: { allowedValues?: Array<{}>; defaultValue?: {} }; + [key: string]: { + allowedValues?: Array<{}>; + defaultValue?: {}; + required: boolean; + schema: FieldSchema; + }; }) => Object.keys(fields ?? {}).reduce((fieldsAcc, fieldKey) => { return { ...fieldsAcc, [fieldKey]: { + required: fields[fieldKey]?.required, allowedValues: fields[fieldKey]?.allowedValues ?? [], defaultValue: fields[fieldKey]?.defaultValue ?? {}, + schema: fields[fieldKey]?.schema, }, }; }, {}); @@ -326,7 +335,6 @@ export const createExternalService = ( const getIssueTypes = async () => { const capabilitiesResponse = await getCapabilities(); const supportsNewAPI = hasSupportForNewAPI(capabilitiesResponse); - try { if (!supportsNewAPI) { const res = await request({ @@ -366,7 +374,6 @@ export const createExternalService = ( const getFieldsByIssueType = async (issueTypeId: string) => { const capabilitiesResponse = await getCapabilities(); const supportsNewAPI = hasSupportForNewAPI(capabilitiesResponse); - try { if (!supportsNewAPI) { const res = await request({ @@ -378,6 +385,7 @@ export const createExternalService = ( }); const fields = res.data.projects[0]?.issuetypes[0]?.fields || {}; + return normalizeFields(fields); } else { const res = await request({ @@ -409,6 +417,30 @@ export const createExternalService = ( } }; + const getFields = async () => { + try { + const issueTypes = await getIssueTypes(); + const fieldsPerIssueType = await Promise.all( + issueTypes.map((issueType) => getFieldsByIssueType(issueType.id)) + ); + return fieldsPerIssueType.reduce((acc: GetCommonFieldsResponse, fieldTypesByIssue) => { + const currentListOfFields = Object.keys(acc); + return currentListOfFields.length === 0 + ? fieldTypesByIssue + : currentListOfFields.reduce( + (add: GetCommonFieldsResponse, field) => + Object.keys(fieldTypesByIssue).includes(field) + ? { ...add, [field]: acc[field] } + : add, + {} + ); + }, {}); + } catch (error) { + // errors that happen here would be thrown in the contained async calls + throw error; + } + }; + const getIssues = async (title: string) => { const query = `${searchUrl}?jql=${encodeURIComponent( `project="${projectKey}" and summary ~"${title}"` @@ -461,6 +493,7 @@ export const createExternalService = ( }; return { + getFields, getIncident, createIncident, updateIncident, diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/types.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/types.ts index 7d650a22fba1b..e142637010a98 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/jira/types.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/jira/types.ts @@ -79,11 +79,34 @@ export interface CreateCommentParams { comment: Comment; } +export interface FieldsSchema { + type: string; + [key: string]: string; +} + +export interface ExternalServiceFields { + clauseNames: string[]; + custom: boolean; + id: string; + key: string; + name: string; + navigatable: boolean; + orderable: boolean; + schema: FieldsSchema; + searchable: boolean; +} + export type GetIssueTypesResponse = Array<{ id: string; name: string }>; + +export interface FieldSchema { + type: string; + items?: string; +} export type GetFieldsByIssueTypeResponse = Record< string, - { allowedValues: Array<{}>; defaultValue: {} } + { allowedValues: Array<{}>; defaultValue: {}; required: boolean; schema: FieldSchema } >; +export type GetCommonFieldsResponse = GetFieldsByIssueTypeResponse; export type GetIssuesResponse = Array<{ id: string; key: string; title: string }>; export interface GetIssueResponse { @@ -93,15 +116,16 @@ export interface GetIssueResponse { } export interface ExternalService { - getIncident: (id: string) => Promise; - createIncident: (params: CreateIncidentParams) => Promise; - updateIncident: (params: UpdateIncidentParams) => Promise; createComment: (params: CreateCommentParams) => Promise; + createIncident: (params: CreateIncidentParams) => Promise; + getFields: () => Promise; getCapabilities: () => Promise; - getIssueTypes: () => Promise; getFieldsByIssueType: (issueTypeId: string) => Promise; - getIssues: (title: string) => Promise; + getIncident: (id: string) => Promise; getIssue: (id: string) => Promise; + getIssues: (title: string) => Promise; + getIssueTypes: () => Promise; + updateIncident: (params: UpdateIncidentParams) => Promise; } export interface PushToServiceApiParams extends ExecutorSubActionPushParams { @@ -157,6 +181,11 @@ export interface GetIssueTypesHandlerArgs { params: ExecutorSubActionGetIssueTypesParams; } +export interface GetCommonFieldsHandlerArgs { + externalService: ExternalService; + params: ExecutorSubActionGetIssueTypesParams; +} + export interface GetFieldsByIssueTypeHandlerArgs { externalService: ExternalService; params: ExecutorSubActionGetFieldsByIssueTypeParams; @@ -177,15 +206,16 @@ export interface GetIssueHandlerArgs { } export interface ExternalServiceApi { - handshake: (args: HandshakeApiHandlerArgs) => Promise; - pushToService: (args: PushToServiceApiHandlerArgs) => Promise; + getFields: (args: GetCommonFieldsHandlerArgs) => Promise; getIncident: (args: GetIncidentApiHandlerArgs) => Promise; + handshake: (args: HandshakeApiHandlerArgs) => Promise; issueTypes: (args: GetIssueTypesHandlerArgs) => Promise; + pushToService: (args: PushToServiceApiHandlerArgs) => Promise; fieldsByIssueType: ( args: GetFieldsByIssueTypeHandlerArgs ) => Promise; - issues: (args: GetIssuesHandlerArgs) => Promise; issue: (args: GetIssueHandlerArgs) => Promise; + issues: (args: GetIssuesHandlerArgs) => Promise; } export type JiraExecutorResultData = diff --git a/x-pack/plugins/actions/server/builtin_action_types/resilient/api.ts b/x-pack/plugins/actions/server/builtin_action_types/resilient/api.ts index 46d9c114297a9..29f2594d2b6f8 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/resilient/api.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/resilient/api.ts @@ -15,6 +15,7 @@ import { GetSeverityHandlerArgs, PushToServiceApiParams, PushToServiceResponse, + GetCommonFieldsHandlerArgs, } from './types'; // TODO: to remove, need to support Case @@ -32,6 +33,10 @@ const getIncidentHandler = async ({ params, }: GetIncidentApiHandlerArgs) => {}; +const getFieldsHandler = async ({ externalService }: GetCommonFieldsHandlerArgs) => { + const res = await externalService.getFields(); + return res; +}; const getIncidentTypesHandler = async ({ externalService }: GetIncidentTypesHandlerArgs) => { const res = await externalService.getIncidentTypes(); return res; @@ -136,9 +141,10 @@ const pushToServiceHandler = async ({ }; export const api: ExternalServiceApi = { - handshake: handshakeHandler, - pushToService: pushToServiceHandler, + getFields: getFieldsHandler, getIncident: getIncidentHandler, + handshake: handshakeHandler, incidentTypes: getIncidentTypesHandler, + pushToService: pushToServiceHandler, severity: getSeverityHandler, }; diff --git a/x-pack/plugins/actions/server/builtin_action_types/resilient/index.ts b/x-pack/plugins/actions/server/builtin_action_types/resilient/index.ts index 53285a2a350af..6203dda4120f5 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/resilient/index.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/resilient/index.ts @@ -25,6 +25,7 @@ import { ResilientExecutorResultData, ExecutorSubActionGetIncidentTypesParams, ExecutorSubActionGetSeverityParams, + ExecutorSubActionCommonFieldsParams, } from './types'; import * as i18n from './translations'; import { Logger } from '../../../../../../src/core/server'; @@ -37,7 +38,7 @@ interface GetActionTypeParams { configurationUtilities: ActionsConfigurationUtilities; } -const supportedSubActions: string[] = ['pushToService', 'incidentTypes', 'severity']; +const supportedSubActions: string[] = ['getFields', 'pushToService', 'incidentTypes', 'severity']; // action type definition export function getActionType( @@ -122,6 +123,14 @@ async function executor( logger.debug(`response push to service for incident id: ${data.id}`); } + if (subAction === 'getFields') { + const getFieldsParams = subActionParams as ExecutorSubActionCommonFieldsParams; + data = await api.getFields({ + externalService, + params: getFieldsParams, + }); + } + if (subAction === 'incidentTypes') { const incidentTypesParams = subActionParams as ExecutorSubActionGetIncidentTypesParams; data = await api.incidentTypes({ diff --git a/x-pack/plugins/actions/server/builtin_action_types/resilient/mocks.ts b/x-pack/plugins/actions/server/builtin_action_types/resilient/mocks.ts index 2e841728159a3..2b2a22a66b709 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/resilient/mocks.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/resilient/mocks.ts @@ -8,8 +8,275 @@ import { ExternalService, PushToServiceApiParams, ExecutorSubActionPushParams } import { MapRecord } from '../case/types'; +export const resilientFields = [ + { + id: 17, + name: 'name', + text: 'Name', + prefix: null, + type_id: 0, + tooltip: 'A unique name to identify this particular incident.', + input_type: 'text', + required: 'always', + hide_notification: false, + chosen: false, + default_chosen_by_server: false, + blank_option: false, + internal: true, + uuid: 'ad6ed4f2-8d87-4ba2-81fa-03568a9326cc', + operations: [ + 'equals', + 'not_equals', + 'contains', + 'not_contains', + 'changed', + 'changed_to', + 'not_changed_to', + 'has_a_value', + 'not_has_a_value', + ], + operation_perms: { + changed_to: { + show_in_manual_actions: false, + show_in_auto_actions: true, + show_in_notifications: true, + }, + has_a_value: { + show_in_manual_actions: true, + show_in_auto_actions: true, + show_in_notifications: true, + }, + not_changed_to: { + show_in_manual_actions: false, + show_in_auto_actions: true, + show_in_notifications: true, + }, + equals: { + show_in_manual_actions: true, + show_in_auto_actions: true, + show_in_notifications: true, + }, + changed: { + show_in_manual_actions: false, + show_in_auto_actions: true, + show_in_notifications: true, + }, + contains: { + show_in_manual_actions: true, + show_in_auto_actions: true, + show_in_notifications: true, + }, + not_contains: { + show_in_manual_actions: true, + show_in_auto_actions: true, + show_in_notifications: true, + }, + not_equals: { + show_in_manual_actions: true, + show_in_auto_actions: true, + show_in_notifications: true, + }, + not_has_a_value: { + show_in_manual_actions: true, + show_in_auto_actions: true, + show_in_notifications: true, + }, + }, + values: [], + perms: { + delete: false, + modify_name: false, + modify_values: false, + modify_blank: false, + modify_required: false, + modify_operations: false, + modify_chosen: false, + modify_default: false, + show_in_manual_actions: true, + show_in_auto_actions: true, + show_in_notifications: true, + show_in_scripts: true, + modify_type: ['text'], + sort: true, + }, + read_only: false, + changeable: true, + rich_text: false, + templates: [], + deprecated: false, + tags: [], + calculated: false, + is_tracked: false, + allow_default_value: false, + }, + { + id: 15, + name: 'description', + text: 'Description', + prefix: null, + type_id: 0, + tooltip: 'A free form text description of the incident.', + input_type: 'textarea', + hide_notification: false, + chosen: false, + default_chosen_by_server: false, + blank_option: false, + internal: true, + uuid: '420d70b1-98f9-4681-a20b-84f36a9e5e48', + operations: [ + 'equals', + 'not_equals', + 'contains', + 'not_contains', + 'changed', + 'changed_to', + 'not_changed_to', + 'has_a_value', + 'not_has_a_value', + ], + operation_perms: { + changed_to: { + show_in_manual_actions: false, + show_in_auto_actions: true, + show_in_notifications: true, + }, + has_a_value: { + show_in_manual_actions: true, + show_in_auto_actions: true, + show_in_notifications: true, + }, + not_changed_to: { + show_in_manual_actions: false, + show_in_auto_actions: true, + show_in_notifications: true, + }, + equals: { + show_in_manual_actions: true, + show_in_auto_actions: true, + show_in_notifications: true, + }, + changed: { + show_in_manual_actions: false, + show_in_auto_actions: true, + show_in_notifications: true, + }, + contains: { + show_in_manual_actions: true, + show_in_auto_actions: true, + show_in_notifications: true, + }, + not_contains: { + show_in_manual_actions: true, + show_in_auto_actions: true, + show_in_notifications: true, + }, + not_equals: { + show_in_manual_actions: true, + show_in_auto_actions: true, + show_in_notifications: true, + }, + not_has_a_value: { + show_in_manual_actions: true, + show_in_auto_actions: true, + show_in_notifications: true, + }, + }, + values: [], + perms: { + delete: false, + modify_name: false, + modify_values: false, + modify_blank: false, + modify_required: false, + modify_operations: false, + modify_chosen: false, + modify_default: false, + show_in_manual_actions: true, + show_in_auto_actions: true, + show_in_notifications: true, + show_in_scripts: true, + modify_type: ['textarea'], + sort: true, + }, + read_only: false, + changeable: true, + rich_text: true, + templates: [], + deprecated: false, + tags: [], + calculated: false, + is_tracked: false, + allow_default_value: false, + }, + { + id: 65, + name: 'create_date', + text: 'Date Created', + prefix: null, + type_id: 0, + tooltip: 'The date the incident was created. This field is read-only.', + input_type: 'datetimepicker', + hide_notification: false, + chosen: false, + default_chosen_by_server: false, + blank_option: false, + internal: true, + uuid: 'b4faf728-881a-4e8b-bf0b-d39b720392a1', + operations: ['due_within', 'overdue_by', 'has_a_value', 'not_has_a_value'], + operation_perms: { + has_a_value: { + show_in_manual_actions: true, + show_in_auto_actions: true, + show_in_notifications: true, + }, + not_has_a_value: { + show_in_manual_actions: true, + show_in_auto_actions: true, + show_in_notifications: true, + }, + due_within: { + show_in_manual_actions: true, + show_in_auto_actions: true, + show_in_notifications: true, + }, + overdue_by: { + show_in_manual_actions: true, + show_in_auto_actions: true, + show_in_notifications: true, + }, + }, + values: [], + perms: { + delete: false, + modify_name: false, + modify_values: false, + modify_blank: false, + modify_required: false, + modify_operations: false, + modify_chosen: false, + modify_default: false, + show_in_manual_actions: true, + show_in_auto_actions: true, + show_in_notifications: true, + show_in_scripts: true, + modify_type: ['datetimepicker'], + sort: true, + }, + read_only: true, + changeable: false, + rich_text: false, + templates: [], + deprecated: false, + tags: [], + calculated: false, + is_tracked: false, + allow_default_value: false, + }, +]; + const createMock = (): jest.Mocked => { const service = { + getFields: jest.fn().mockImplementation(() => Promise.resolve(resilientFields)), getIncident: jest.fn().mockImplementation(() => Promise.resolve({ id: '1', diff --git a/x-pack/plugins/actions/server/builtin_action_types/resilient/schema.ts b/x-pack/plugins/actions/server/builtin_action_types/resilient/schema.ts index b6e3a9525dfd4..c7ceba94140fb 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/resilient/schema.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/resilient/schema.ts @@ -53,11 +53,16 @@ export const ExecutorSubActionGetIncidentParamsSchema = schema.object({ }); // Reserved for future implementation +export const ExecutorSubActionCommonFieldsParamsSchema = schema.object({}); export const ExecutorSubActionHandshakeParamsSchema = schema.object({}); export const ExecutorSubActionGetIncidentTypesParamsSchema = schema.object({}); export const ExecutorSubActionGetSeverityParamsSchema = schema.object({}); export const ExecutorParamsSchema = schema.oneOf([ + schema.object({ + subAction: schema.literal('getFields'), + subActionParams: ExecutorSubActionCommonFieldsParamsSchema, + }), schema.object({ subAction: schema.literal('getIncident'), subActionParams: ExecutorSubActionGetIncidentParamsSchema, diff --git a/x-pack/plugins/actions/server/builtin_action_types/resilient/service.test.ts b/x-pack/plugins/actions/server/builtin_action_types/resilient/service.test.ts index 86ea352625a5b..ecf246cb8fe3c 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/resilient/service.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/resilient/service.test.ts @@ -11,7 +11,7 @@ import * as utils from '../lib/axios_utils'; import { ExternalService } from './types'; import { Logger } from '../../../../../../src/core/server'; import { loggingSystemMock } from '../../../../../../src/core/server/mocks'; -import { incidentTypes, severity } from './mocks'; +import { incidentTypes, resilientFields, severity } from './mocks'; const logger = loggingSystemMock.create().get() as jest.Mocked; @@ -231,7 +231,7 @@ describe('IBM Resilient service', () => { requestMock.mockImplementation(() => { throw new Error('An error has occurred'); }); - expect(service.getIncident('1')).rejects.toThrow( + await expect(service.getIncident('1')).rejects.toThrow( 'Unable to get incident with id 1. Error: An error has occurred' ); }); @@ -310,7 +310,7 @@ describe('IBM Resilient service', () => { throw new Error('An error has occurred'); }); - expect( + await expect( service.createIncident({ incident: { name: 'title', @@ -418,7 +418,7 @@ describe('IBM Resilient service', () => { test('it should throw an error', async () => { mockIncidentUpdate(true); - expect( + await expect( service.updateIncident({ incidentId: '1', incident: { @@ -502,7 +502,7 @@ describe('IBM Resilient service', () => { throw new Error('An error has occurred'); }); - expect( + await expect( service.createComment({ incidentId: '1', comment: { @@ -541,7 +541,7 @@ describe('IBM Resilient service', () => { throw new Error('An error has occurred'); }); - expect(service.getIncidentTypes()).rejects.toThrow( + await expect(service.getIncidentTypes()).rejects.toThrow( '[Action][IBM Resilient]: Unable to get incident types. Error: An error has occurred.' ); }); @@ -578,9 +578,40 @@ describe('IBM Resilient service', () => { throw new Error('An error has occurred'); }); - expect(service.getIncidentTypes()).rejects.toThrow( + await expect(service.getIncidentTypes()).rejects.toThrow( '[Action][IBM Resilient]: Unable to get incident types. Error: An error has occurred.' ); }); }); + + describe('getFields', () => { + test('it should call request with correct arguments', async () => { + requestMock.mockImplementation(() => ({ + data: resilientFields, + })); + await service.getFields(); + + expect(requestMock).toHaveBeenCalledWith({ + axios, + logger, + url: 'https://resilient.elastic.co/rest/orgs/201/types/incident/fields', + }); + }); + test('it returns common fields correctly', async () => { + requestMock.mockImplementation(() => ({ + data: resilientFields, + })); + const res = await service.getFields(); + expect(res).toEqual(resilientFields); + }); + + test('it should throw an error', async () => { + requestMock.mockImplementation(() => { + throw new Error('An error has occurred'); + }); + await expect(service.getFields()).rejects.toThrow( + 'Unable to get fields. Error: An error has occurred' + ); + }); + }); }); diff --git a/x-pack/plugins/actions/server/builtin_action_types/resilient/service.ts b/x-pack/plugins/actions/server/builtin_action_types/resilient/service.ts index 4bf1453641e42..a13204f8bb1d8 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/resilient/service.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/resilient/service.ts @@ -303,12 +303,27 @@ export const createExternalService = ( } }; + const getFields = async () => { + try { + const res = await request({ + axios: axiosInstance, + url: incidentFieldsUrl, + logger, + proxySettings, + }); + return res.data ?? []; + } catch (error) { + throw new Error(getErrorMessage(i18n.NAME, `Unable to get fields. Error: ${error.message}.`)); + } + }; + return { - getIncident, - createIncident, - updateIncident, createComment, + createIncident, + getFields, + getIncident, getIncidentTypes, getSeverity, + updateIncident, }; }; diff --git a/x-pack/plugins/actions/server/builtin_action_types/resilient/types.ts b/x-pack/plugins/actions/server/builtin_action_types/resilient/types.ts index ed622ee473b65..a70420b30a092 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/resilient/types.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/resilient/types.ts @@ -8,14 +8,15 @@ import { TypeOf } from '@kbn/config-schema'; import { - ExternalIncidentServiceConfigurationSchema, - ExternalIncidentServiceSecretConfigurationSchema, ExecutorParamsSchema, - ExecutorSubActionPushParamsSchema, + ExecutorSubActionCommonFieldsParamsSchema, ExecutorSubActionGetIncidentParamsSchema, - ExecutorSubActionHandshakeParamsSchema, ExecutorSubActionGetIncidentTypesParamsSchema, ExecutorSubActionGetSeverityParamsSchema, + ExecutorSubActionHandshakeParamsSchema, + ExecutorSubActionPushParamsSchema, + ExternalIncidentServiceConfigurationSchema, + ExternalIncidentServiceSecretConfigurationSchema, } from './schema'; import { ActionsConfigurationUtilities } from '../../actions_config'; @@ -31,6 +32,10 @@ export type ResilientSecretConfigurationType = TypeOf< typeof ExternalIncidentServiceSecretConfigurationSchema >; +export type ExecutorSubActionCommonFieldsParams = TypeOf< + typeof ExecutorSubActionCommonFieldsParamsSchema +>; + export type ExecutorParams = TypeOf; export type ExecutorSubActionPushParams = TypeOf; @@ -60,6 +65,14 @@ export interface ExternalServiceCommentResponse { } export type ExternalServiceParams = Record; +export interface ExternalServiceFields { + id: string; + input_type: string; + name: string; + read_only: boolean; + required?: string; +} +export type GetCommonFieldsResponse = ExternalServiceFields[]; export type Incident = Pick< ExecutorSubActionPushParams, @@ -86,12 +99,13 @@ export type GetIncidentTypesResponse = Array<{ id: string; name: string }>; export type GetSeverityResponse = Array<{ id: string; name: string }>; export interface ExternalService { - getIncident: (id: string) => Promise; - createIncident: (params: CreateIncidentParams) => Promise; - updateIncident: (params: UpdateIncidentParams) => Promise; createComment: (params: CreateCommentParams) => Promise; + createIncident: (params: CreateIncidentParams) => Promise; + getFields: () => Promise; + getIncident: (id: string) => Promise; getIncidentTypes: () => Promise; getSeverity: () => Promise; + updateIncident: (params: UpdateIncidentParams) => Promise; } export interface PushToServiceApiParams extends ExecutorSubActionPushParams { @@ -132,6 +146,11 @@ export interface HandshakeApiHandlerArgs extends ExternalServiceApiHandlerArgs { params: ExecutorSubActionHandshakeParams; } +export interface GetCommonFieldsHandlerArgs { + externalService: ExternalService; + params: ExecutorSubActionCommonFieldsParams; +} + export interface GetIncidentTypesHandlerArgs { externalService: ExternalService; params: ExecutorSubActionGetIncidentTypesParams; @@ -147,6 +166,7 @@ export interface PushToServiceResponse extends ExternalServiceIncidentResponse { } export interface ExternalServiceApi { + getFields: (args: GetCommonFieldsHandlerArgs) => Promise; handshake: (args: HandshakeApiHandlerArgs) => Promise; pushToService: (args: PushToServiceApiHandlerArgs) => Promise; getIncident: (args: GetIncidentApiHandlerArgs) => Promise; @@ -156,6 +176,7 @@ export interface ExternalServiceApi { export type ResilientExecutorResultData = | PushToServiceResponse + | GetCommonFieldsResponse | GetIncidentTypesResponse | GetSeverityResponse; diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/api.test.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/api.test.ts index d49c2f265d04f..4683b661e21da 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/api.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/api.test.ts @@ -5,7 +5,7 @@ */ import { Logger } from '../../../../../../src/core/server'; -import { externalServiceMock, mapping, apiParams } from './mocks'; +import { externalServiceMock, mapping, apiParams, serviceNowCommonFields } from './mocks'; import { ExternalService } from './types'; import { api } from './api'; let mockedLogger: jest.Mocked; @@ -15,634 +15,619 @@ describe('api', () => { beforeEach(() => { externalService = externalServiceMock.create(); - jest.clearAllMocks(); }); - afterEach(() => { - jest.clearAllMocks(); - }); + describe('create incident', () => { + test('it creates an incident', async () => { + const params = { ...apiParams, externalId: null }; + const res = await api.pushToService({ + externalService, + mapping, + params, + secrets: {}, + logger: mockedLogger, + }); - describe('pushToService', () => { - describe('create incident', () => { - test('it creates an incident', async () => { - const params = { ...apiParams, externalId: null }; - const res = await api.pushToService({ - externalService, - mapping, - params, - secrets: {}, - logger: mockedLogger, - }); - - expect(res).toEqual({ - id: 'incident-1', - title: 'INC01', - pushedDate: '2020-03-10T12:24:20.000Z', - url: 'https://instance.service-now.com/nav_to.do?uri=incident.do?sys_id=123', - comments: [ - { - commentId: 'case-comment-1', - pushedDate: '2020-03-10T12:24:20.000Z', - }, - { - commentId: 'case-comment-2', - pushedDate: '2020-03-10T12:24:20.000Z', - }, - ], - }); - }); - - test('it creates an incident without comments', async () => { - const params = { ...apiParams, externalId: null, comments: [] }; - const res = await api.pushToService({ - externalService, - mapping, - params, - secrets: {}, - logger: mockedLogger, - }); - - expect(res).toEqual({ - id: 'incident-1', - title: 'INC01', - pushedDate: '2020-03-10T12:24:20.000Z', - url: 'https://instance.service-now.com/nav_to.do?uri=incident.do?sys_id=123', - }); - }); - - test('it calls createIncident correctly', async () => { - const params = { ...apiParams, externalId: null, comments: [] }; - await api.pushToService({ - externalService, - mapping, - params, - secrets: { username: 'elastic', password: 'elastic' }, - logger: mockedLogger, - }); - - expect(externalService.createIncident).toHaveBeenCalledWith({ - incident: { - severity: '1', - urgency: '2', - impact: '3', - caller_id: 'elastic', - description: - 'Incident description (created at 2020-03-13T08:34:53.450Z by Elastic User)', - short_description: - 'Incident title (created at 2020-03-13T08:34:53.450Z by Elastic User)', - }, - }); - expect(externalService.updateIncident).not.toHaveBeenCalled(); - }); - - test('it calls updateIncident correctly when creating an incident and having comments', async () => { - const params = { ...apiParams, externalId: null }; - await api.pushToService({ - externalService, - mapping, - params, - secrets: {}, - logger: mockedLogger, - }); - expect(externalService.updateIncident).toHaveBeenCalledTimes(2); - expect(externalService.updateIncident).toHaveBeenNthCalledWith(1, { - incident: { - severity: '1', - urgency: '2', - impact: '3', - comments: 'A comment (added at 2020-03-13T08:34:53.450Z by Elastic User)', - description: - 'Incident description (created at 2020-03-13T08:34:53.450Z by Elastic User)', - short_description: - 'Incident title (created at 2020-03-13T08:34:53.450Z by Elastic User)', + expect(res).toEqual({ + id: 'incident-1', + title: 'INC01', + pushedDate: '2020-03-10T12:24:20.000Z', + url: 'https://instance.service-now.com/nav_to.do?uri=incident.do?sys_id=123', + comments: [ + { + commentId: 'case-comment-1', + pushedDate: '2020-03-10T12:24:20.000Z', }, - incidentId: 'incident-1', - }); - - expect(externalService.updateIncident).toHaveBeenNthCalledWith(2, { - incident: { - severity: '1', - urgency: '2', - impact: '3', - comments: 'Another comment (added at 2020-03-13T08:34:53.450Z by Elastic User)', - description: - 'Incident description (created at 2020-03-13T08:34:53.450Z by Elastic User)', - short_description: - 'Incident title (created at 2020-03-13T08:34:53.450Z by Elastic User)', + { + commentId: 'case-comment-2', + pushedDate: '2020-03-10T12:24:20.000Z', }, - incidentId: 'incident-1', - }); + ], }); }); - describe('update incident', () => { - test('it updates an incident', async () => { - const res = await api.pushToService({ - externalService, - mapping, - params: apiParams, - secrets: {}, - logger: mockedLogger, - }); - - expect(res).toEqual({ - id: 'incident-2', - title: 'INC02', - pushedDate: '2020-03-10T12:24:20.000Z', - url: 'https://instance.service-now.com/nav_to.do?uri=incident.do?sys_id=123', - comments: [ - { - commentId: 'case-comment-1', - pushedDate: '2020-03-10T12:24:20.000Z', - }, - { - commentId: 'case-comment-2', - pushedDate: '2020-03-10T12:24:20.000Z', - }, - ], - }); - }); - - test('it updates an incident without comments', async () => { - const params = { ...apiParams, comments: [] }; - const res = await api.pushToService({ - externalService, - mapping, - params, - secrets: {}, - logger: mockedLogger, - }); - - expect(res).toEqual({ - id: 'incident-2', - title: 'INC02', - pushedDate: '2020-03-10T12:24:20.000Z', - url: 'https://instance.service-now.com/nav_to.do?uri=incident.do?sys_id=123', - }); - }); - - test('it calls updateIncident correctly', async () => { - const params = { ...apiParams }; - await api.pushToService({ - externalService, - mapping, - params, - secrets: {}, - logger: mockedLogger, - }); - - expect(externalService.updateIncident).toHaveBeenCalledWith({ - incidentId: 'incident-3', - incident: { - severity: '1', - urgency: '2', - impact: '3', - description: - 'Incident description (updated at 2020-03-13T08:34:53.450Z by Elastic User)', - short_description: - 'Incident title (updated at 2020-03-13T08:34:53.450Z by Elastic User)', - }, - }); - expect(externalService.createIncident).not.toHaveBeenCalled(); - }); - - test('it calls updateIncident to create a comments correctly', async () => { - const params = { ...apiParams }; - await api.pushToService({ - externalService, - mapping, - params, - secrets: {}, - logger: mockedLogger, - }); - expect(externalService.updateIncident).toHaveBeenCalledTimes(3); - expect(externalService.updateIncident).toHaveBeenNthCalledWith(1, { - incident: { - severity: '1', - urgency: '2', - impact: '3', - description: - 'Incident description (updated at 2020-03-13T08:34:53.450Z by Elastic User)', - short_description: - 'Incident title (updated at 2020-03-13T08:34:53.450Z by Elastic User)', - }, - incidentId: 'incident-3', - }); - - expect(externalService.updateIncident).toHaveBeenNthCalledWith(2, { - incident: { - severity: '1', - urgency: '2', - impact: '3', - comments: 'A comment (added at 2020-03-13T08:34:53.450Z by Elastic User)', - description: - 'Incident description (updated at 2020-03-13T08:34:53.450Z by Elastic User)', - short_description: - 'Incident title (updated at 2020-03-13T08:34:53.450Z by Elastic User)', - }, - incidentId: 'incident-2', - }); + test('it creates an incident without comments', async () => { + const params = { ...apiParams, externalId: null, comments: [] }; + const res = await api.pushToService({ + externalService, + mapping, + params, + secrets: {}, + logger: mockedLogger, + }); + + expect(res).toEqual({ + id: 'incident-1', + title: 'INC01', + pushedDate: '2020-03-10T12:24:20.000Z', + url: 'https://instance.service-now.com/nav_to.do?uri=incident.do?sys_id=123', }); }); - describe('mapping variations', () => { - test('overwrite & append', async () => { - mapping.set('title', { - target: 'short_description', - actionType: 'overwrite', - }); - - mapping.set('description', { - target: 'description', - actionType: 'append', - }); - - mapping.set('comments', { - target: 'comments', - actionType: 'append', - }); - - mapping.set('short_description', { - target: 'title', - actionType: 'overwrite', - }); - - await api.pushToService({ - externalService, - mapping, - params: apiParams, - secrets: {}, - logger: mockedLogger, - }); - expect(externalService.updateIncident).toHaveBeenCalledWith({ - incidentId: 'incident-3', - incident: { - severity: '1', - urgency: '2', - impact: '3', - short_description: - 'Incident title (updated at 2020-03-13T08:34:53.450Z by Elastic User)', - description: - 'description from servicenow \r\nIncident description (updated at 2020-03-13T08:34:53.450Z by Elastic User)', - }, - }); - }); - - test('nothing & append', async () => { - mapping.set('title', { - target: 'short_description', - actionType: 'nothing', - }); - - mapping.set('description', { - target: 'description', - actionType: 'append', - }); - - mapping.set('comments', { - target: 'comments', - actionType: 'append', - }); - - mapping.set('short_description', { - target: 'title', - actionType: 'nothing', - }); - - await api.pushToService({ - externalService, - mapping, - params: apiParams, - secrets: {}, - logger: mockedLogger, - }); - expect(externalService.updateIncident).toHaveBeenCalledWith({ - incidentId: 'incident-3', - incident: { - severity: '1', - urgency: '2', - impact: '3', - description: - 'description from servicenow \r\nIncident description (updated at 2020-03-13T08:34:53.450Z by Elastic User)', - }, - }); - }); - - test('append & append', async () => { - mapping.set('title', { - target: 'short_description', - actionType: 'append', - }); - - mapping.set('description', { - target: 'description', - actionType: 'append', - }); - - mapping.set('comments', { - target: 'comments', - actionType: 'append', - }); - - mapping.set('short_description', { - target: 'title', - actionType: 'append', - }); - - await api.pushToService({ - externalService, - mapping, - params: apiParams, - secrets: {}, - logger: mockedLogger, - }); - expect(externalService.updateIncident).toHaveBeenCalledWith({ - incidentId: 'incident-3', - incident: { - severity: '1', - urgency: '2', - impact: '3', - short_description: - 'title from servicenow \r\nIncident title (updated at 2020-03-13T08:34:53.450Z by Elastic User)', - description: - 'description from servicenow \r\nIncident description (updated at 2020-03-13T08:34:53.450Z by Elastic User)', - }, - }); - }); - - test('nothing & nothing', async () => { - mapping.set('title', { - target: 'short_description', - actionType: 'nothing', - }); - - mapping.set('description', { - target: 'description', - actionType: 'nothing', - }); - - mapping.set('comments', { - target: 'comments', - actionType: 'append', - }); - - mapping.set('short_description', { - target: 'title', - actionType: 'nothing', - }); - - await api.pushToService({ - externalService, - mapping, - params: apiParams, - secrets: {}, - logger: mockedLogger, - }); - - expect(externalService.updateIncident).toHaveBeenCalledWith({ - incidentId: 'incident-3', - incident: { - severity: '1', - urgency: '2', - impact: '3', - }, - }); - }); - - test('overwrite & nothing', async () => { - mapping.set('title', { - target: 'short_description', - actionType: 'overwrite', - }); - - mapping.set('description', { - target: 'description', - actionType: 'nothing', - }); - - mapping.set('comments', { - target: 'comments', - actionType: 'append', - }); - - mapping.set('short_description', { - target: 'title', - actionType: 'overwrite', - }); - - await api.pushToService({ - externalService, - mapping, - params: apiParams, - secrets: {}, - logger: mockedLogger, - }); - expect(externalService.updateIncident).toHaveBeenCalledWith({ - incidentId: 'incident-3', - incident: { - severity: '1', - urgency: '2', - impact: '3', - short_description: - 'Incident title (updated at 2020-03-13T08:34:53.450Z by Elastic User)', - }, - }); - }); - - test('overwrite & overwrite', async () => { - mapping.set('title', { - target: 'short_description', - actionType: 'overwrite', - }); - - mapping.set('description', { - target: 'description', - actionType: 'overwrite', - }); - - mapping.set('comments', { - target: 'comments', - actionType: 'append', - }); - - mapping.set('short_description', { - target: 'title', - actionType: 'overwrite', - }); - - await api.pushToService({ - externalService, - mapping, - params: apiParams, - secrets: {}, - logger: mockedLogger, - }); - expect(externalService.updateIncident).toHaveBeenCalledWith({ - incidentId: 'incident-3', - incident: { - severity: '1', - urgency: '2', - impact: '3', - short_description: - 'Incident title (updated at 2020-03-13T08:34:53.450Z by Elastic User)', - description: - 'Incident description (updated at 2020-03-13T08:34:53.450Z by Elastic User)', - }, - }); - }); - - test('nothing & overwrite', async () => { - mapping.set('title', { - target: 'short_description', - actionType: 'nothing', - }); - - mapping.set('description', { - target: 'description', - actionType: 'overwrite', - }); - - mapping.set('comments', { - target: 'comments', - actionType: 'append', - }); - - mapping.set('short_description', { - target: 'title', - actionType: 'nothing', - }); - - await api.pushToService({ - externalService, - mapping, - params: apiParams, - secrets: {}, - logger: mockedLogger, - }); - expect(externalService.updateIncident).toHaveBeenCalledWith({ - incidentId: 'incident-3', - incident: { - severity: '1', - urgency: '2', - impact: '3', - description: - 'Incident description (updated at 2020-03-13T08:34:53.450Z by Elastic User)', - }, - }); - }); - - test('append & overwrite', async () => { - mapping.set('title', { - target: 'short_description', - actionType: 'append', - }); - - mapping.set('description', { - target: 'description', - actionType: 'overwrite', - }); - - mapping.set('comments', { - target: 'comments', - actionType: 'append', - }); - - mapping.set('short_description', { - target: 'title', - actionType: 'append', - }); - - await api.pushToService({ - externalService, - mapping, - params: apiParams, - secrets: {}, - logger: mockedLogger, - }); - expect(externalService.updateIncident).toHaveBeenCalledWith({ - incidentId: 'incident-3', - incident: { - severity: '1', - urgency: '2', - impact: '3', - short_description: - 'title from servicenow \r\nIncident title (updated at 2020-03-13T08:34:53.450Z by Elastic User)', - description: - 'Incident description (updated at 2020-03-13T08:34:53.450Z by Elastic User)', + test('it calls createIncident correctly', async () => { + const params = { ...apiParams, externalId: null, comments: [] }; + await api.pushToService({ + externalService, + mapping, + params, + secrets: { username: 'elastic', password: 'elastic' }, + logger: mockedLogger, + }); + + expect(externalService.createIncident).toHaveBeenCalledWith({ + incident: { + severity: '1', + urgency: '2', + impact: '3', + caller_id: 'elastic', + description: 'Incident description (created at 2020-03-13T08:34:53.450Z by Elastic User)', + short_description: 'Incident title (created at 2020-03-13T08:34:53.450Z by Elastic User)', + }, + }); + expect(externalService.updateIncident).not.toHaveBeenCalled(); + }); + + test('it calls updateIncident correctly when creating an incident and having comments', async () => { + const params = { ...apiParams, externalId: null }; + await api.pushToService({ + externalService, + mapping, + params, + secrets: {}, + logger: mockedLogger, + }); + expect(externalService.updateIncident).toHaveBeenCalledTimes(2); + expect(externalService.updateIncident).toHaveBeenNthCalledWith(1, { + incident: { + severity: '1', + urgency: '2', + impact: '3', + comments: 'A comment (added at 2020-03-13T08:34:53.450Z by Elastic User)', + description: 'Incident description (created at 2020-03-13T08:34:53.450Z by Elastic User)', + short_description: 'Incident title (created at 2020-03-13T08:34:53.450Z by Elastic User)', + }, + incidentId: 'incident-1', + }); + + expect(externalService.updateIncident).toHaveBeenNthCalledWith(2, { + incident: { + severity: '1', + urgency: '2', + impact: '3', + comments: 'Another comment (added at 2020-03-13T08:34:53.450Z by Elastic User)', + description: 'Incident description (created at 2020-03-13T08:34:53.450Z by Elastic User)', + short_description: 'Incident title (created at 2020-03-13T08:34:53.450Z by Elastic User)', + }, + incidentId: 'incident-1', + }); + }); + }); + + describe('update incident', () => { + test('it updates an incident', async () => { + const res = await api.pushToService({ + externalService, + mapping, + params: apiParams, + secrets: {}, + logger: mockedLogger, + }); + + expect(res).toEqual({ + id: 'incident-2', + title: 'INC02', + pushedDate: '2020-03-10T12:24:20.000Z', + url: 'https://instance.service-now.com/nav_to.do?uri=incident.do?sys_id=123', + comments: [ + { + commentId: 'case-comment-1', + pushedDate: '2020-03-10T12:24:20.000Z', }, - }); - }); - - test('append & nothing', async () => { - mapping.set('title', { - target: 'short_description', - actionType: 'append', - }); - - mapping.set('description', { - target: 'description', - actionType: 'nothing', - }); - - mapping.set('comments', { - target: 'comments', - actionType: 'append', - }); - - mapping.set('short_description', { - target: 'title', - actionType: 'append', - }); - - await api.pushToService({ - externalService, - mapping, - params: apiParams, - secrets: {}, - logger: mockedLogger, - }); - expect(externalService.updateIncident).toHaveBeenCalledWith({ - incidentId: 'incident-3', - incident: { - severity: '1', - urgency: '2', - impact: '3', - short_description: - 'title from servicenow \r\nIncident title (updated at 2020-03-13T08:34:53.450Z by Elastic User)', + { + commentId: 'case-comment-2', + pushedDate: '2020-03-10T12:24:20.000Z', }, - }); - }); - - test('comment nothing', async () => { - mapping.set('title', { - target: 'short_description', - actionType: 'overwrite', - }); - - mapping.set('description', { - target: 'description', - actionType: 'nothing', - }); - - mapping.set('comments', { - target: 'comments', - actionType: 'nothing', - }); - - mapping.set('short_description', { - target: 'title', - actionType: 'overwrite', - }); - - await api.pushToService({ - externalService, - mapping, - params: apiParams, - secrets: {}, - logger: mockedLogger, - }); - expect(externalService.updateIncident).toHaveBeenCalledTimes(1); + ], + }); + }); + + test('it updates an incident without comments', async () => { + const params = { ...apiParams, comments: [] }; + const res = await api.pushToService({ + externalService, + mapping, + params, + secrets: {}, + logger: mockedLogger, + }); + + expect(res).toEqual({ + id: 'incident-2', + title: 'INC02', + pushedDate: '2020-03-10T12:24:20.000Z', + url: 'https://instance.service-now.com/nav_to.do?uri=incident.do?sys_id=123', + }); + }); + + test('it calls updateIncident correctly', async () => { + const params = { ...apiParams }; + await api.pushToService({ + externalService, + mapping, + params, + secrets: {}, + logger: mockedLogger, + }); + + expect(externalService.updateIncident).toHaveBeenCalledWith({ + incidentId: 'incident-3', + incident: { + severity: '1', + urgency: '2', + impact: '3', + description: 'Incident description (updated at 2020-03-13T08:34:53.450Z by Elastic User)', + short_description: 'Incident title (updated at 2020-03-13T08:34:53.450Z by Elastic User)', + }, + }); + expect(externalService.createIncident).not.toHaveBeenCalled(); + }); + + test('it calls updateIncident to create a comments correctly', async () => { + const params = { ...apiParams }; + await api.pushToService({ + externalService, + mapping, + params, + secrets: {}, + logger: mockedLogger, + }); + expect(externalService.updateIncident).toHaveBeenCalledTimes(3); + expect(externalService.updateIncident).toHaveBeenNthCalledWith(1, { + incident: { + severity: '1', + urgency: '2', + impact: '3', + description: 'Incident description (updated at 2020-03-13T08:34:53.450Z by Elastic User)', + short_description: 'Incident title (updated at 2020-03-13T08:34:53.450Z by Elastic User)', + }, + incidentId: 'incident-3', + }); + + expect(externalService.updateIncident).toHaveBeenNthCalledWith(2, { + incident: { + severity: '1', + urgency: '2', + impact: '3', + comments: 'A comment (added at 2020-03-13T08:34:53.450Z by Elastic User)', + description: 'Incident description (updated at 2020-03-13T08:34:53.450Z by Elastic User)', + short_description: 'Incident title (updated at 2020-03-13T08:34:53.450Z by Elastic User)', + }, + incidentId: 'incident-2', + }); + }); + }); + + describe('mapping variations', () => { + test('overwrite & append', async () => { + mapping.set('title', { + target: 'short_description', + actionType: 'overwrite', + }); + + mapping.set('description', { + target: 'description', + actionType: 'append', + }); + + mapping.set('comments', { + target: 'comments', + actionType: 'append', + }); + + mapping.set('short_description', { + target: 'title', + actionType: 'overwrite', + }); + + await api.pushToService({ + externalService, + mapping, + params: apiParams, + secrets: {}, + logger: mockedLogger, + }); + expect(externalService.updateIncident).toHaveBeenCalledWith({ + incidentId: 'incident-3', + incident: { + severity: '1', + urgency: '2', + impact: '3', + short_description: 'Incident title (updated at 2020-03-13T08:34:53.450Z by Elastic User)', + description: + 'description from servicenow \r\nIncident description (updated at 2020-03-13T08:34:53.450Z by Elastic User)', + }, + }); + }); + + test('nothing & append', async () => { + mapping.set('title', { + target: 'short_description', + actionType: 'nothing', + }); + + mapping.set('description', { + target: 'description', + actionType: 'append', + }); + + mapping.set('comments', { + target: 'comments', + actionType: 'append', + }); + + mapping.set('short_description', { + target: 'title', + actionType: 'nothing', + }); + + await api.pushToService({ + externalService, + mapping, + params: apiParams, + secrets: {}, + logger: mockedLogger, + }); + expect(externalService.updateIncident).toHaveBeenCalledWith({ + incidentId: 'incident-3', + incident: { + severity: '1', + urgency: '2', + impact: '3', + description: + 'description from servicenow \r\nIncident description (updated at 2020-03-13T08:34:53.450Z by Elastic User)', + }, + }); + }); + + test('append & append', async () => { + mapping.set('title', { + target: 'short_description', + actionType: 'append', + }); + + mapping.set('description', { + target: 'description', + actionType: 'append', + }); + + mapping.set('comments', { + target: 'comments', + actionType: 'append', + }); + + mapping.set('short_description', { + target: 'title', + actionType: 'append', + }); + + await api.pushToService({ + externalService, + mapping, + params: apiParams, + secrets: {}, + logger: mockedLogger, + }); + expect(externalService.updateIncident).toHaveBeenCalledWith({ + incidentId: 'incident-3', + incident: { + severity: '1', + urgency: '2', + impact: '3', + short_description: + 'title from servicenow \r\nIncident title (updated at 2020-03-13T08:34:53.450Z by Elastic User)', + description: + 'description from servicenow \r\nIncident description (updated at 2020-03-13T08:34:53.450Z by Elastic User)', + }, + }); + }); + + test('nothing & nothing', async () => { + mapping.set('title', { + target: 'short_description', + actionType: 'nothing', + }); + + mapping.set('description', { + target: 'description', + actionType: 'nothing', + }); + + mapping.set('comments', { + target: 'comments', + actionType: 'append', + }); + + mapping.set('short_description', { + target: 'title', + actionType: 'nothing', + }); + + await api.pushToService({ + externalService, + mapping, + params: apiParams, + secrets: {}, + logger: mockedLogger, + }); + + expect(externalService.updateIncident).toHaveBeenCalledWith({ + incidentId: 'incident-3', + incident: { + severity: '1', + urgency: '2', + impact: '3', + }, + }); + }); + + test('overwrite & nothing', async () => { + mapping.set('title', { + target: 'short_description', + actionType: 'overwrite', + }); + + mapping.set('description', { + target: 'description', + actionType: 'nothing', + }); + + mapping.set('comments', { + target: 'comments', + actionType: 'append', + }); + + mapping.set('short_description', { + target: 'title', + actionType: 'overwrite', + }); + + await api.pushToService({ + externalService, + mapping, + params: apiParams, + secrets: {}, + logger: mockedLogger, + }); + expect(externalService.updateIncident).toHaveBeenCalledWith({ + incidentId: 'incident-3', + incident: { + severity: '1', + urgency: '2', + impact: '3', + short_description: 'Incident title (updated at 2020-03-13T08:34:53.450Z by Elastic User)', + }, + }); + }); + + test('overwrite & overwrite', async () => { + mapping.set('title', { + target: 'short_description', + actionType: 'overwrite', + }); + + mapping.set('description', { + target: 'description', + actionType: 'overwrite', + }); + + mapping.set('comments', { + target: 'comments', + actionType: 'append', + }); + + mapping.set('short_description', { + target: 'title', + actionType: 'overwrite', + }); + + await api.pushToService({ + externalService, + mapping, + params: apiParams, + secrets: {}, + logger: mockedLogger, + }); + expect(externalService.updateIncident).toHaveBeenCalledWith({ + incidentId: 'incident-3', + incident: { + severity: '1', + urgency: '2', + impact: '3', + short_description: 'Incident title (updated at 2020-03-13T08:34:53.450Z by Elastic User)', + description: 'Incident description (updated at 2020-03-13T08:34:53.450Z by Elastic User)', + }, + }); + }); + + test('nothing & overwrite', async () => { + mapping.set('title', { + target: 'short_description', + actionType: 'nothing', + }); + + mapping.set('description', { + target: 'description', + actionType: 'overwrite', + }); + + mapping.set('comments', { + target: 'comments', + actionType: 'append', + }); + + mapping.set('short_description', { + target: 'title', + actionType: 'nothing', + }); + + await api.pushToService({ + externalService, + mapping, + params: apiParams, + secrets: {}, + logger: mockedLogger, + }); + expect(externalService.updateIncident).toHaveBeenCalledWith({ + incidentId: 'incident-3', + incident: { + severity: '1', + urgency: '2', + impact: '3', + description: 'Incident description (updated at 2020-03-13T08:34:53.450Z by Elastic User)', + }, + }); + }); + + test('append & overwrite', async () => { + mapping.set('title', { + target: 'short_description', + actionType: 'append', + }); + + mapping.set('description', { + target: 'description', + actionType: 'overwrite', + }); + + mapping.set('comments', { + target: 'comments', + actionType: 'append', + }); + + mapping.set('short_description', { + target: 'title', + actionType: 'append', + }); + + await api.pushToService({ + externalService, + mapping, + params: apiParams, + secrets: {}, + logger: mockedLogger, + }); + expect(externalService.updateIncident).toHaveBeenCalledWith({ + incidentId: 'incident-3', + incident: { + severity: '1', + urgency: '2', + impact: '3', + short_description: + 'title from servicenow \r\nIncident title (updated at 2020-03-13T08:34:53.450Z by Elastic User)', + description: 'Incident description (updated at 2020-03-13T08:34:53.450Z by Elastic User)', + }, + }); + }); + + test('append & nothing', async () => { + mapping.set('title', { + target: 'short_description', + actionType: 'append', + }); + + mapping.set('description', { + target: 'description', + actionType: 'nothing', + }); + + mapping.set('comments', { + target: 'comments', + actionType: 'append', + }); + + mapping.set('short_description', { + target: 'title', + actionType: 'append', + }); + + await api.pushToService({ + externalService, + mapping, + params: apiParams, + secrets: {}, + logger: mockedLogger, + }); + expect(externalService.updateIncident).toHaveBeenCalledWith({ + incidentId: 'incident-3', + incident: { + severity: '1', + urgency: '2', + impact: '3', + short_description: + 'title from servicenow \r\nIncident title (updated at 2020-03-13T08:34:53.450Z by Elastic User)', + }, + }); + }); + + test('comment nothing', async () => { + mapping.set('title', { + target: 'short_description', + actionType: 'overwrite', + }); + + mapping.set('description', { + target: 'description', + actionType: 'nothing', + }); + + mapping.set('comments', { + target: 'comments', + actionType: 'nothing', + }); + + mapping.set('short_description', { + target: 'title', + actionType: 'overwrite', + }); + + await api.pushToService({ + externalService, + mapping, + params: apiParams, + secrets: {}, + logger: mockedLogger, + }); + expect(externalService.updateIncident).toHaveBeenCalledTimes(1); + }); + }); + + describe('getFields', () => { + test('it returns the fields correctly', async () => { + const res = await api.getFields({ + externalService, + params: {}, }); + expect(res).toEqual(serviceNowCommonFields); }); }); }); diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/api.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/api.ts index 6d12a3c92dac7..fbd8fdd635d70 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/api.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/api.ts @@ -12,6 +12,8 @@ import { PushToServiceApiParams, PushToServiceResponse, Incident, + GetCommonFieldsHandlerArgs, + GetCommonFieldsResponse, } from './types'; // TODO: to remove, need to support Case @@ -127,8 +129,16 @@ const pushToServiceHandler = async ({ return res; }; +const getFieldsHandler = async ({ + externalService, +}: GetCommonFieldsHandlerArgs): Promise => { + const res = await externalService.getFields(); + return res; +}; + export const api: ExternalServiceApi = { + getFields: getFieldsHandler, + getIncident: getIncidentHandler, handshake: handshakeHandler, pushToService: pushToServiceHandler, - getIncident: getIncidentHandler, }; diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.ts index 41a577918b18e..d1182b0d3b2fa 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.ts @@ -25,6 +25,8 @@ import { ServiceNowPublicConfigurationType, ServiceNowSecretConfigurationType, PushToServiceResponse, + ExecutorSubActionCommonFieldsParams, + ServiceNowExecutorResultData, } from './types'; // TODO: to remove, need to support Case @@ -63,7 +65,7 @@ export function getActionType( } // action executor - +const supportedSubActions: string[] = ['getFields', 'pushToService']; async function executor( { logger }: { logger: Logger }, execOptions: ActionTypeExecutorOptions< @@ -71,10 +73,10 @@ async function executor( ServiceNowSecretConfigurationType, ExecutorParams > -): Promise> { +): Promise> { const { actionId, config, params, secrets } = execOptions; const { subAction, subActionParams } = params; - let data: PushToServiceResponse | null = null; + let data: ServiceNowExecutorResultData | null = null; const externalService = createExternalService( { @@ -91,7 +93,7 @@ async function executor( throw new Error(errorMessage); } - if (subAction !== 'pushToService') { + if (!supportedSubActions.includes(subAction)) { const errorMessage = `[Action][ExternalService] subAction ${subAction} not implemented.`; logger.error(errorMessage); throw new Error(errorMessage); @@ -117,5 +119,13 @@ async function executor( logger.debug(`response push to service for incident id: ${data.id}`); } + if (subAction === 'getFields') { + const getFieldsParams = subActionParams as ExecutorSubActionCommonFieldsParams; + data = await api.getFields({ + externalService, + params: getFieldsParams, + }); + } + return { status: 'ok', data: data ?? {}, actionId }; } diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/mocks.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/mocks.ts index 7c2b1bd9d73c1..2351be36a50c4 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/mocks.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/mocks.ts @@ -7,8 +7,36 @@ import { ExternalService, PushToServiceApiParams, ExecutorSubActionPushParams } from './types'; import { MapRecord } from '../case/types'; +export const serviceNowCommonFields = [ + { + column_label: 'Close notes', + max_length: '4000', + element: 'close_notes', + }, + { + column_label: 'Description', + max_length: '4000', + element: 'description', + }, + { + column_label: 'Short description', + max_length: '160', + element: 'short_description', + }, + { + column_label: 'Created by', + max_length: '40', + element: 'sys_created_by', + }, + { + column_label: 'Updated by', + max_length: '40', + element: 'sys_updated_by', + }, +]; const createMock = (): jest.Mocked => { const service = { + getFields: jest.fn().mockImplementation(() => Promise.resolve(serviceNowCommonFields)), getIncident: jest.fn().mockImplementation(() => Promise.resolve({ short_description: 'title from servicenow', diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/schema.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/schema.ts index 0dd70ea36636e..77c48aab1f309 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/schema.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/schema.ts @@ -28,6 +28,7 @@ export const ExternalIncidentServiceSecretConfigurationSchema = schema.object( ); export const ExecutorSubActionSchema = schema.oneOf([ + schema.literal('getFields'), schema.literal('getIncident'), schema.literal('pushToService'), schema.literal('handshake'), @@ -53,8 +54,13 @@ export const ExecutorSubActionGetIncidentParamsSchema = schema.object({ // Reserved for future implementation export const ExecutorSubActionHandshakeParamsSchema = schema.object({}); +export const ExecutorSubActionCommonFieldsParamsSchema = schema.object({}); export const ExecutorParamsSchema = schema.oneOf([ + schema.object({ + subAction: schema.literal('getFields'), + subActionParams: ExecutorSubActionCommonFieldsParamsSchema, + }), schema.object({ subAction: schema.literal('getIncident'), subActionParams: ExecutorSubActionGetIncidentParamsSchema, diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/service.test.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/service.test.ts index 2adcdf561ce17..8ec80be1e2b09 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/service.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/service.test.ts @@ -11,6 +11,7 @@ import * as utils from '../lib/axios_utils'; import { ExternalService } from './types'; import { Logger } from '../../../../../../src/core/server'; import { loggingSystemMock } from '../../../../../../src/core/server/mocks'; +import { serviceNowCommonFields } from './mocks'; const logger = loggingSystemMock.create().get() as jest.Mocked; jest.mock('axios'); @@ -108,7 +109,7 @@ describe('ServiceNow service', () => { requestMock.mockImplementation(() => { throw new Error('An error has occurred'); }); - expect(service.getIncident('1')).rejects.toThrow( + await expect(service.getIncident('1')).rejects.toThrow( 'Unable to get incident with id 1. Error: An error has occurred' ); }); @@ -155,7 +156,7 @@ describe('ServiceNow service', () => { throw new Error('An error has occurred'); }); - expect( + await expect( service.createIncident({ incident: { short_description: 'title', description: 'desc' }, }) @@ -207,7 +208,7 @@ describe('ServiceNow service', () => { throw new Error('An error has occurred'); }); - expect( + await expect( service.updateIncident({ incidentId: '1', incident: { short_description: 'title', description: 'desc' }, @@ -234,4 +235,36 @@ describe('ServiceNow service', () => { }); }); }); + + describe('getFields', () => { + test('it should call request with correct arguments', async () => { + requestMock.mockImplementation(() => ({ + data: { result: serviceNowCommonFields }, + })); + await service.getFields(); + + expect(requestMock).toHaveBeenCalledWith({ + axios, + logger, + url: + 'https://dev102283.service-now.com/api/now/v2/table/sys_dictionary?sysparm_query=name=task^internal_type=string&active=true&read_only=false&sysparm_fields=max_length,element,column_label', + }); + }); + test('it returns common fields correctly', async () => { + requestMock.mockImplementation(() => ({ + data: { result: serviceNowCommonFields }, + })); + const res = await service.getFields(); + expect(res).toEqual(serviceNowCommonFields); + }); + + test('it should throw an error', async () => { + requestMock.mockImplementation(() => { + throw new Error('An error has occurred'); + }); + await expect(service.getFields()).rejects.toThrow( + 'Unable to get common fields. Error: An error has occurred' + ); + }); + }); }); diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/service.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/service.ts index 9b1da4b4007c6..57f7176e2353c 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/service.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/service.ts @@ -16,6 +16,7 @@ import { ProxySettings } from '../../types'; const API_VERSION = 'v2'; const INCIDENT_URL = `api/now/${API_VERSION}/table/incident`; +const SYS_DICTIONARY = `api/now/${API_VERSION}/table/sys_dictionary`; // Based on: https://docs.servicenow.com/bundle/orlando-platform-user-interface/page/use/navigation/reference/r_NavigatingByURLExamples.html const VIEW_INCIDENT_URL = `nav_to.do?uri=incident.do?sys_id=`; @@ -33,6 +34,7 @@ export const createExternalService = ( } const incidentUrl = `${url}/${INCIDENT_URL}`; + const fieldsUrl = `${url}/${SYS_DICTIONARY}?sysparm_query=name=task^internal_type=string&active=true&read_only=false&sysparm_fields=max_length,element,column_label`; const axiosInstance = axios.create({ auth: { username, password }, }); @@ -126,10 +128,28 @@ export const createExternalService = ( } }; + const getFields = async () => { + try { + const res = await request({ + axios: axiosInstance, + url: fieldsUrl, + logger, + proxySettings, + }); + + return res.data.result.length > 0 ? res.data.result : []; + } catch (error) { + throw new Error( + getErrorMessage(i18n.NAME, `Unable to get common fields. Error: ${error.message}`) + ); + } + }; + return { - getIncident, createIncident, - updateIncident, findIncidents, + getFields, + getIncident, + updateIncident, }; }; diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/types.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/types.ts index a6a0ac946fe96..0ee03f883ec05 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/types.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/types.ts @@ -8,12 +8,13 @@ import { TypeOf } from '@kbn/config-schema'; import { - ExternalIncidentServiceConfigurationSchema, - ExternalIncidentServiceSecretConfigurationSchema, ExecutorParamsSchema, - ExecutorSubActionPushParamsSchema, + ExecutorSubActionCommonFieldsParamsSchema, ExecutorSubActionGetIncidentParamsSchema, ExecutorSubActionHandshakeParamsSchema, + ExecutorSubActionPushParamsSchema, + ExternalIncidentServiceConfigurationSchema, + ExternalIncidentServiceSecretConfigurationSchema, } from './schema'; import { ActionsConfigurationUtilities } from '../../actions_config'; import { ExternalServiceCommentResponse } from '../case/types'; @@ -27,6 +28,12 @@ export type ServiceNowSecretConfigurationType = TypeOf< typeof ExternalIncidentServiceSecretConfigurationSchema >; +export type ExecutorSubActionCommonFieldsParams = TypeOf< + typeof ExecutorSubActionCommonFieldsParamsSchema +>; + +export type ServiceNowExecutorResultData = PushToServiceResponse | GetCommonFieldsResponse; + export interface CreateCommentRequest { [key: string]: string; } @@ -59,6 +66,7 @@ export interface PushToServiceResponse extends ExternalServiceIncidentResponse { export type ExternalServiceParams = Record; export interface ExternalService { + getFields: () => Promise; getIncident: (id: string) => Promise; createIncident: (params: ExternalServiceParams) => Promise; updateIncident: (params: ExternalServiceParams) => Promise; @@ -102,8 +110,24 @@ export interface GetIncidentApiHandlerArgs extends ExternalServiceApiHandlerArgs export interface HandshakeApiHandlerArgs extends ExternalServiceApiHandlerArgs { params: ExecutorSubActionHandshakeParams; } +export interface ExternalServiceFields { + column_label: string; + name: string; + internal_type: { + link: string; + value: string; + }; + max_length: string; + element: string; +} +export type GetCommonFieldsResponse = ExternalServiceFields[]; +export interface GetCommonFieldsHandlerArgs { + externalService: ExternalService; + params: ExecutorSubActionCommonFieldsParams; +} export interface ExternalServiceApi { + getFields: (args: GetCommonFieldsHandlerArgs) => Promise; handshake: (args: HandshakeApiHandlerArgs) => Promise; pushToService: (args: PushToServiceApiHandlerArgs) => Promise; getIncident: (args: GetIncidentApiHandlerArgs) => Promise; diff --git a/x-pack/plugins/apm/e2e/cypress/integration/apm.feature b/x-pack/plugins/apm/e2e/cypress/integration/apm.feature index 285615108266b..494a6b5fadb5b 100644 --- a/x-pack/plugins/apm/e2e/cypress/integration/apm.feature +++ b/x-pack/plugins/apm/e2e/cypress/integration/apm.feature @@ -3,5 +3,4 @@ Feature: APM Scenario: Transaction duration charts Given a user browses the APM UI application When the user inspects the opbeans-node service - Then should redirect to correct path with correct params - And should have correct y-axis ticks + Then should redirect to correct path with correct params \ No newline at end of file diff --git a/x-pack/plugins/apm/e2e/cypress/support/step_definitions/apm.ts b/x-pack/plugins/apm/e2e/cypress/support/step_definitions/apm.ts index 50c620dca9ddf..42c2bc7ffd318 100644 --- a/x-pack/plugins/apm/e2e/cypress/support/step_definitions/apm.ts +++ b/x-pack/plugins/apm/e2e/cypress/support/step_definitions/apm.ts @@ -29,16 +29,3 @@ Then(`should redirect to correct path with correct params`, () => { cy.url().should('contain', `/app/apm/services/opbeans-node/transactions`); cy.url().should('contain', `transactionType=request`); }); - -Then(`should have correct y-axis ticks`, () => { - const yAxisTick = - '[data-cy=transaction-duration-charts] .rv-xy-plot__axis--vertical .rv-xy-plot__axis__tick__text'; - - // wait for all loading to finish - cy.get('kbnLoadingIndicator').should('not.be.visible'); - - // literal assertions because snapshot() doesn't retry - cy.get(yAxisTick).eq(2).should('have.text', '55 ms'); - cy.get(yAxisTick).eq(1).should('have.text', '28 ms'); - cy.get(yAxisTick).eq(0).should('have.text', '0 ms'); -}); diff --git a/x-pack/plugins/apm/public/components/app/Home/alerting_popover_flyout/index.tsx b/x-pack/plugins/apm/public/application/action_menu/alerting_popover_flyout.tsx similarity index 83% rename from x-pack/plugins/apm/public/components/app/Home/alerting_popover_flyout/index.tsx rename to x-pack/plugins/apm/public/application/action_menu/alerting_popover_flyout.tsx index 7e6331c1fa3a8..394b4caea3e7b 100644 --- a/x-pack/plugins/apm/public/components/app/Home/alerting_popover_flyout/index.tsx +++ b/x-pack/plugins/apm/public/application/action_menu/alerting_popover_flyout.tsx @@ -5,16 +5,16 @@ */ import { - EuiButtonEmpty, EuiContextMenu, EuiContextMenuPanelDescriptor, + EuiHeaderLink, EuiPopover, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React, { useState } from 'react'; -import { AlertType } from '../../../../../common/alert_types'; -import { useApmPluginContext } from '../../../../hooks/useApmPluginContext'; -import { AlertingFlyout } from '../../../alerting/AlertingFlyout'; +import { IBasePath } from '../../../../../../src/core/public'; +import { AlertType } from '../../../common/alert_types'; +import { AlertingFlyout } from '../../components/alerting/AlertingFlyout'; const alertLabel = i18n.translate('xpack.apm.home.alertsMenu.alerts', { defaultMessage: 'Alerts', @@ -46,28 +46,32 @@ const CREATE_TRANSACTION_ERROR_RATE_ALERT_PANEL_ID = const CREATE_ERROR_COUNT_ALERT_PANEL_ID = 'create_error_count_panel'; interface Props { + basePath: IBasePath; canReadAlerts: boolean; canSaveAlerts: boolean; canReadAnomalies: boolean; + includeTransactionDuration: boolean; } -export function AlertingPopoverAndFlyout(props: Props) { - const { canSaveAlerts, canReadAlerts, canReadAnomalies } = props; - - const plugin = useApmPluginContext(); - +export function AlertingPopoverAndFlyout({ + basePath, + canSaveAlerts, + canReadAlerts, + canReadAnomalies, + includeTransactionDuration, +}: Props) { const [popoverOpen, setPopoverOpen] = useState(false); - const [alertType, setAlertType] = useState(null); const button = ( - setPopoverOpen(true)} + onClick={() => setPopoverOpen((prevState) => !prevState)} > {alertLabel} - + ); const panels: EuiContextMenuPanelDescriptor[] = [ @@ -98,7 +102,7 @@ export function AlertingPopoverAndFlyout(props: Props) { 'xpack.apm.home.alertsMenu.viewActiveAlerts', { defaultMessage: 'View active alerts' } ), - href: plugin.core.http.basePath.prepend( + href: basePath.prepend( '/app/management/insightsAndAlerting/triggersActions/alerts' ), icon: 'tableOfContents', @@ -113,6 +117,19 @@ export function AlertingPopoverAndFlyout(props: Props) { id: CREATE_TRANSACTION_DURATION_ALERT_PANEL_ID, title: transactionDurationLabel, items: [ + // threshold alerts + ...(includeTransactionDuration + ? [ + { + name: createThresholdAlertLabel, + onClick: () => { + setAlertType(AlertType.TransactionDuration); + setPopoverOpen(false); + }, + }, + ] + : []), + // anomaly alerts ...(canReadAnomalies ? [ diff --git a/x-pack/plugins/apm/public/components/shared/Links/apm/anomaly_detection_setup_link.test.tsx b/x-pack/plugins/apm/public/application/action_menu/anomaly_detection_setup_link.test.tsx similarity index 96% rename from x-pack/plugins/apm/public/components/shared/Links/apm/anomaly_detection_setup_link.test.tsx rename to x-pack/plugins/apm/public/application/action_menu/anomaly_detection_setup_link.test.tsx index a53468e2ad06c..b90f606d276eb 100644 --- a/x-pack/plugins/apm/public/components/shared/Links/apm/anomaly_detection_setup_link.test.tsx +++ b/x-pack/plugins/apm/public/application/action_menu/anomaly_detection_setup_link.test.tsx @@ -6,8 +6,8 @@ import React from 'react'; import { render, fireEvent, waitFor } from '@testing-library/react'; -import { MissingJobsAlert } from './AnomalyDetectionSetupLink'; -import * as hooks from '../../../../hooks/useFetcher'; +import { MissingJobsAlert } from './anomaly_detection_setup_link'; +import * as hooks from '../../hooks/useFetcher'; async function renderTooltipAnchor({ jobs, diff --git a/x-pack/plugins/apm/public/components/shared/Links/apm/AnomalyDetectionSetupLink.tsx b/x-pack/plugins/apm/public/application/action_menu/anomaly_detection_setup_link.tsx similarity index 67% rename from x-pack/plugins/apm/public/components/shared/Links/apm/AnomalyDetectionSetupLink.tsx rename to x-pack/plugins/apm/public/application/action_menu/anomaly_detection_setup_link.tsx index 368837b3c9411..d75446cb0dd48 100644 --- a/x-pack/plugins/apm/public/components/shared/Links/apm/AnomalyDetectionSetupLink.tsx +++ b/x-pack/plugins/apm/public/application/action_menu/anomaly_detection_setup_link.tsx @@ -3,19 +3,25 @@ * 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 { EuiButtonEmpty, EuiToolTip, EuiIcon } from '@elastic/eui'; +import { + EuiHeaderLink, + EuiIcon, + EuiLoadingSpinner, + EuiToolTip, +} from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { useApmPluginContext } from '../../../../hooks/useApmPluginContext'; -import { APIReturnType } from '../../../../services/rest/createCallApmApi'; -import { APMLink } from './APMLink'; +import React from 'react'; import { ENVIRONMENT_ALL, getEnvironmentLabel, -} from '../../../../../common/environment_filter_values'; -import { useUrlParams } from '../../../../hooks/useUrlParams'; -import { useFetcher, FETCH_STATUS } from '../../../../hooks/useFetcher'; -import { useLicense } from '../../../../hooks/useLicense'; +} from '../../../common/environment_filter_values'; +import { getAPMHref } from '../../components/shared/Links/apm/APMLink'; +import { useApmPluginContext } from '../../hooks/useApmPluginContext'; +import { FETCH_STATUS, useFetcher } from '../../hooks/useFetcher'; +import { useLicense } from '../../hooks/useLicense'; +import { useUrlParams } from '../../hooks/useUrlParams'; +import { APIReturnType } from '../../services/rest/createCallApmApi'; +import { units } from '../../style/variables'; export type AnomalyDetectionApiResponse = APIReturnType< '/api/apm/settings/anomaly-detection', @@ -27,24 +33,27 @@ const DEFAULT_DATA = { jobs: [], hasLegacyJobs: false }; export function AnomalyDetectionSetupLink() { const { uiFilters } = useUrlParams(); const environment = uiFilters.environment; - const plugin = useApmPluginContext(); - const canGetJobs = !!plugin.core.application.capabilities.ml?.canGetJobs; + const { core } = useApmPluginContext(); + const canGetJobs = !!core.application.capabilities.ml?.canGetJobs; const license = useLicense(); const hasValidLicense = license?.isActive && license?.hasAtLeast('platinum'); + const { basePath } = core.http; return ( - - - {ANOMALY_DETECTION_LINK_LABEL} - - {canGetJobs && hasValidLicense ? ( - ) : null} - + ) : ( + + )} + + {ANOMALY_DETECTION_LINK_LABEL} + + ); } @@ -56,8 +65,14 @@ export function MissingJobsAlert({ environment }: { environment?: string }) { { preservePreviousData: false, showToastOnError: false } ); + const defaultIcon = ; + + if (status === FETCH_STATUS.LOADING) { + return ; + } + if (status !== FETCH_STATUS.SUCCESS) { - return null; + return defaultIcon; } const isEnvironmentSelected = @@ -65,7 +80,7 @@ export function MissingJobsAlert({ environment }: { environment?: string }) { // there are jobs for at least one environment if (!isEnvironmentSelected && data.jobs.length > 0) { - return null; + return defaultIcon; } // there are jobs for the selected environment @@ -73,7 +88,7 @@ export function MissingJobsAlert({ environment }: { environment?: string }) { isEnvironmentSelected && data.jobs.some((job) => environment === job.environment) ) { - return null; + return defaultIcon; } return ( diff --git a/x-pack/plugins/apm/public/application/action_menu/index.tsx b/x-pack/plugins/apm/public/application/action_menu/index.tsx new file mode 100644 index 0000000000000..1713ef61fac1e --- /dev/null +++ b/x-pack/plugins/apm/public/application/action_menu/index.tsx @@ -0,0 +1,72 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiHeaderLink, EuiHeaderLinks } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import React from 'react'; +import { useParams } from 'react-router-dom'; +import { getAlertingCapabilities } from '../../components/alerting/get_alert_capabilities'; +import { getAPMHref } from '../../components/shared/Links/apm/APMLink'; +import { useApmPluginContext } from '../../hooks/useApmPluginContext'; +import { AlertingPopoverAndFlyout } from './alerting_popover_flyout'; +import { AnomalyDetectionSetupLink } from './anomaly_detection_setup_link'; + +export function ActionMenu() { + const { core, plugins } = useApmPluginContext(); + const { serviceName } = useParams<{ serviceName?: string }>(); + const { search } = window.location; + const { application, http } = core; + const { basePath } = http; + const { capabilities } = application; + const canAccessML = !!capabilities.ml?.canAccessML; + const { + isAlertingAvailable, + canReadAlerts, + canSaveAlerts, + canReadAnomalies, + } = getAlertingCapabilities(plugins, capabilities); + + function apmHref(path: string) { + return getAPMHref({ basePath, path, search }); + } + + function kibanaHref(path: string) { + return basePath.prepend(path); + } + + return ( + + + {i18n.translate('xpack.apm.settingsLinkLabel', { + defaultMessage: 'Settings', + })} + + {isAlertingAvailable && ( + + )} + {canAccessML && } + + {i18n.translate('xpack.apm.addDataButtonLabel', { + defaultMessage: 'Add data', + })} + + + ); +} diff --git a/x-pack/plugins/apm/public/application/application.test.tsx b/x-pack/plugins/apm/public/application/application.test.tsx index 97700b9bc96b7..75b7835c13151 100644 --- a/x-pack/plugins/apm/public/application/application.test.tsx +++ b/x-pack/plugins/apm/public/application/application.test.tsx @@ -53,6 +53,7 @@ describe('renderApp', () => { const params = { element: document.createElement('div'), history: createMemoryHistory(), + setHeaderActionMenu: () => {}, }; jest.spyOn(window, 'scrollTo').mockReturnValueOnce(undefined); createCallApmApi((core.http as unknown) as HttpSetup); diff --git a/x-pack/plugins/apm/public/application/csmApp.tsx b/x-pack/plugins/apm/public/application/csmApp.tsx index d8f54c7bfc94f..dfc3d6b4b9ec8 100644 --- a/x-pack/plugins/apm/public/application/csmApp.tsx +++ b/x-pack/plugins/apm/public/application/csmApp.tsx @@ -66,21 +66,23 @@ function CsmApp() { } export function CsmAppRoot({ + appMountParameters, core, deps, - history, config, corePlugins: { embeddable }, }: { + appMountParameters: AppMountParameters; core: CoreStart; deps: ApmPluginSetupDeps; - history: AppMountParameters['history']; config: ConfigSchema; corePlugins: ApmPluginStartDeps; }) { + const { history } = appMountParameters; const i18nCore = core.i18n; const plugins = deps; const apmPluginContextValue = { + appMountParameters, config, core, plugins, @@ -109,10 +111,12 @@ export function CsmAppRoot({ export const renderApp = ( core: CoreStart, deps: ApmPluginSetupDeps, - { element, history }: AppMountParameters, + appMountParameters: AppMountParameters, config: ConfigSchema, corePlugins: ApmPluginStartDeps ) => { + const { element } = appMountParameters; + createCallApmApi(core.http); // Automatically creates static index pattern and stores as saved object @@ -123,9 +127,9 @@ export const renderApp = ( ReactDOM.render( , diff --git a/x-pack/plugins/apm/public/application/index.tsx b/x-pack/plugins/apm/public/application/index.tsx index 4e3217ce17ed1..2e1f259bd8c42 100644 --- a/x-pack/plugins/apm/public/application/index.tsx +++ b/x-pack/plugins/apm/public/application/index.tsx @@ -22,7 +22,10 @@ import { import { AlertsContextProvider } from '../../../triggers_actions_ui/public'; import { routes } from '../components/app/Main/route_config'; import { ScrollToTopOnPathChange } from '../components/app/Main/ScrollToTopOnPathChange'; -import { ApmPluginContext } from '../context/ApmPluginContext'; +import { + ApmPluginContext, + ApmPluginContextValue, +} from '../context/ApmPluginContext'; import { LicenseProvider } from '../context/LicenseContext'; import { UrlParamsProvider } from '../context/UrlParamsContext'; import { useBreadcrumbs } from '../hooks/use_breadcrumbs'; @@ -64,23 +67,14 @@ function App() { } export function ApmAppRoot({ - core, - deps, - history, - config, + apmPluginContextValue, }: { - core: CoreStart; - deps: ApmPluginSetupDeps; - history: AppMountParameters['history']; - config: ConfigSchema; + apmPluginContextValue: ApmPluginContextValue; }) { + const { appMountParameters, core, plugins } = apmPluginContextValue; + const { history } = appMountParameters; const i18nCore = core.i18n; - const plugins = deps; - const apmPluginContextValue = { - config, - core, - plugins, - }; + return ( @@ -117,14 +111,21 @@ export function ApmAppRoot({ export const renderApp = ( core: CoreStart, - deps: ApmPluginSetupDeps, - { element, history }: AppMountParameters, + setupDeps: ApmPluginSetupDeps, + appMountParameters: AppMountParameters, config: ConfigSchema ) => { + const { element } = appMountParameters; + const apmPluginContextValue = { + appMountParameters, + config, + core, + plugins: setupDeps, + }; + // render APM feedback link in global help menu setHelpExtension(core); setReadonlyBadge(core); - createCallApmApi(core.http); // Automatically creates static index pattern and stores as saved object @@ -134,7 +135,7 @@ export const renderApp = ( }); ReactDOM.render( - , + , element ); return () => { diff --git a/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/Distribution/index.tsx b/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/Distribution/index.tsx index e17dd9a9eb038..a17bf7e93e466 100644 --- a/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/Distribution/index.tsx +++ b/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/Distribution/index.tsx @@ -4,31 +4,24 @@ * you may not use this file except in compliance with the Elastic License. */ +import { + Axis, + Chart, + HistogramBarSeries, + niceTimeFormatter, + Position, + ScaleType, + Settings, + SettingsSpec, + TooltipValue, +} from '@elastic/charts'; import { EuiTitle } from '@elastic/eui'; -import theme from '@elastic/eui/dist/eui_theme_light.json'; -import numeral from '@elastic/numeral'; -import { i18n } from '@kbn/i18n'; import d3 from 'd3'; -import { scaleUtc } from 'd3-scale'; -import { mean } from 'lodash'; import React from 'react'; import { asRelativeDateTimeRange } from '../../../../../common/utils/formatters'; -import { getTimezoneOffsetInMs } from '../../../shared/charts/CustomPlot/getTimezoneOffsetInMs'; -// @ts-expect-error -import Histogram from '../../../shared/charts/Histogram'; -import { EmptyMessage } from '../../../shared/EmptyMessage'; - -interface IBucket { - key: number; - count: number | undefined; -} - -// TODO: cleanup duplication of this in distribution/get_distribution.ts (ErrorDistributionAPIResponse) and transactions/distribution/index.ts (TransactionDistributionAPIResponse) -interface IDistribution { - noHits: boolean; - buckets: IBucket[]; - bucketSize: number; -} +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import type { ErrorDistributionAPIResponse } from '../../../../../server/lib/errors/distribution/get_distribution'; +import { useTheme } from '../../../../hooks/useTheme'; interface FormattedBucket { x0: number; @@ -37,13 +30,9 @@ interface FormattedBucket { } export function getFormattedBuckets( - buckets: IBucket[], + buckets: ErrorDistributionAPIResponse['buckets'], bucketSize: number -): FormattedBucket[] | null { - if (!buckets) { - return null; - } - +): FormattedBucket[] { return buckets.map(({ count, key }) => { return { x0: key, @@ -54,76 +43,66 @@ export function getFormattedBuckets( } interface Props { - distribution: IDistribution; + distribution: ErrorDistributionAPIResponse; title: React.ReactNode; } -const tooltipHeader = (bucket: FormattedBucket) => - asRelativeDateTimeRange(bucket.x0, bucket.x); - export function ErrorDistribution({ distribution, title }: Props) { + const theme = useTheme(); const buckets = getFormattedBuckets( distribution.buckets, distribution.bucketSize ); - if (!buckets) { - return ( - - ); - } - - const averageValue = mean(buckets.map((bucket) => bucket.y)) || 0; const xMin = d3.min(buckets, (d) => d.x0); - const xMax = d3.max(buckets, (d) => d.x); - const tickFormat = scaleUtc().domain([xMin, xMax]).tickFormat(); + const xMax = d3.max(buckets, (d) => d.x0); + + const xFormatter = niceTimeFormatter([xMin, xMax]); + + const tooltipProps: SettingsSpec['tooltip'] = { + headerFormatter: (tooltip: TooltipValue) => { + const serie = buckets.find((bucket) => bucket.x0 === tooltip.value); + if (serie) { + return asRelativeDateTimeRange(serie.x0, serie.x); + } + return `${tooltip.value}`; + }, + }; return (
{title} - bucket.x} - xType="time-utc" - formatX={(value: Date) => { - const time = value.getTime(); - return tickFormat(new Date(time - getTimezoneOffsetInMs(time))); - }} - buckets={buckets} - bucketSize={distribution.bucketSize} - formatYShort={(value: number) => - i18n.translate('xpack.apm.errorGroupDetails.occurrencesShortLabel', { - defaultMessage: '{occCount} occ.', - values: { occCount: value }, - }) - } - formatYLong={(value: number) => - i18n.translate('xpack.apm.errorGroupDetails.occurrencesLongLabel', { - defaultMessage: - '{occCount} {occCount, plural, one {occurrence} other {occurrences}}', - values: { occCount: value }, - }) - } - legends={[ - { - color: theme.euiColorVis1, - // 0a abbreviates large whole numbers with metric prefixes like: 1000 = 1k, 32000 = 32k, 1000000 = 1m - legendValue: numeral(averageValue).format('0a'), - title: i18n.translate('xpack.apm.errorGroupDetails.avgLabel', { - defaultMessage: 'Avg.', - }), - legendClickDisabled: true, - }, - ]} - /> +
+ + + + + + +
); } diff --git a/x-pack/plugins/apm/public/components/app/Home/__snapshots__/Home.test.tsx.snap b/x-pack/plugins/apm/public/components/app/Home/__snapshots__/Home.test.tsx.snap index 0a22604837b97..82fabff610191 100644 --- a/x-pack/plugins/apm/public/components/app/Home/__snapshots__/Home.test.tsx.snap +++ b/x-pack/plugins/apm/public/components/app/Home/__snapshots__/Home.test.tsx.snap @@ -4,6 +4,9 @@ exports[`Home component should render services 1`] = ` - {i18n.translate('xpack.apm.home.servicesTabLabel', { - defaultMessage: 'Services', - })} - - ), - render: () => , - name: 'services', - }, - { - link: ( - - {i18n.translate('xpack.apm.home.tracesTabLabel', { - defaultMessage: 'Traces', - })} - - ), - render: () => , - name: 'traces', - }, - ]; - - if (serviceMapEnabled) { - homeTabs.push({ - link: ( - - {i18n.translate('xpack.apm.home.serviceMapTabLabel', { - defaultMessage: 'Service Map', - })} - - ), - render: () => , - name: 'service-map', - }); - } - - return homeTabs; -} - -const SETTINGS_LINK_LABEL = i18n.translate('xpack.apm.settingsLinkLabel', { - defaultMessage: 'Settings', -}); +const homeTabs = [ + { + link: ( + + {i18n.translate('xpack.apm.home.servicesTabLabel', { + defaultMessage: 'Services', + })} + + ), + render: () => , + name: 'services', + }, + { + link: ( + + {i18n.translate('xpack.apm.home.tracesTabLabel', { + defaultMessage: 'Traces', + })} + + ), + render: () => , + name: 'traces', + }, + { + link: ( + + {i18n.translate('xpack.apm.home.serviceMapTabLabel', { + defaultMessage: 'Service Map', + })} + + ), + render: () => , + name: 'service-map', + }, +]; interface Props { tab: 'traces' | 'services' | 'service-map'; } export function Home({ tab }: Props) { - const { config, core, plugins } = useApmPluginContext(); - const capabilities = core.application.capabilities; - const canAccessML = !!capabilities.ml?.canAccessML; - const homeTabs = getHomeTabs(config); const selectedTab = homeTabs.find( (homeTab) => homeTab.name === tab ) as $ElementType; - const { - isAlertingAvailable, - canReadAlerts, - canSaveAlerts, - canReadAnomalies, - } = getAlertingCapabilities(plugins, core.application.capabilities); - return (
- - - -

APM

-
-
- - - - {SETTINGS_LINK_LABEL} - - - - {isAlertingAvailable && ( - - - - )} - {canAccessML && ( - - - - )} - - - -
+ +

APM

+
{homeTabs.map((homeTab) => ( diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/URLFilter/URLSearch/SelectableUrlList.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/URLFilter/URLSearch/SelectableUrlList.tsx index 7bd9b2c87814b..1d314e3a0e0d3 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/URLFilter/URLSearch/SelectableUrlList.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/URLFilter/URLSearch/SelectableUrlList.tsx @@ -31,7 +31,7 @@ import { i18n } from '@kbn/i18n'; import styled from 'styled-components'; import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json'; -import { useEvent } from 'react-use'; +import useEvent from 'react-use/lib/useEvent'; import { formatOptions, selectableRenderOptions, diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/EmbeddedMap.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/EmbeddedMap.tsx index 77afe92a8f521..b757635af1702 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/EmbeddedMap.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/EmbeddedMap.tsx @@ -77,6 +77,7 @@ export function EmbeddedMapComponent() { ); const input: MapEmbeddableInput = { + attributes: { title: '' }, id: uuid.v4(), filters: mapFilters, refreshConfig: { diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/__tests__/__mocks__/regions_layer.mock.ts b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/__tests__/__mocks__/regions_layer.mock.ts index e564332583375..6d259a5a2e48c 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/__tests__/__mocks__/regions_layer.mock.ts +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/__tests__/__mocks__/regions_layer.mock.ts @@ -21,6 +21,8 @@ export const mockLayerList = [ { leftField: 'iso2', right: { + applyGlobalQuery: true, + applyGlobalTime: true, type: 'ES_TERM_SOURCE', id: '3657625d-17b0-41ef-99ba-3a2b2938655c', indexPatternTitle: 'apm-*', @@ -38,7 +40,6 @@ export const mockLayerList = [ }, ], indexPatternId: 'apm_static_index_pattern_id', - applyGlobalQuery: true, }, }, ], @@ -46,7 +47,6 @@ export const mockLayerList = [ type: 'EMS_FILE', id: 'world_countries', tooltipProperties: ['name'], - applyGlobalQuery: true, }, style: { type: 'VECTOR', @@ -96,6 +96,8 @@ export const mockLayerList = [ { leftField: 'region_iso_code', right: { + applyGlobalQuery: true, + applyGlobalTime: true, type: 'ES_TERM_SOURCE', id: 'e62a1b9c-d7ff-4fd4-a0f6-0fdc44bb9e41', indexPatternTitle: 'apm-*', diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/useLayerList.ts b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/useLayerList.ts index bc45d58329f49..a1cdf7bb646e5 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/useLayerList.ts +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/useLayerList.ts @@ -43,6 +43,7 @@ const ES_TERM_SOURCE_COUNTRY: ESTermSourceDescriptor = { ], indexPatternId: APM_STATIC_INDEX_PATTERN_ID, applyGlobalQuery: true, + applyGlobalTime: true, }; const ES_TERM_SOURCE_REGION: ESTermSourceDescriptor = { @@ -56,6 +57,8 @@ const ES_TERM_SOURCE_REGION: ESTermSourceDescriptor = { language: 'kuery', }, indexPatternId: APM_STATIC_INDEX_PATTERN_ID, + applyGlobalQuery: true, + applyGlobalTime: true, }; const getWhereQuery = (serviceName: string) => { @@ -158,7 +161,6 @@ export function useLayerList() { type: 'EMS_FILE', id: 'world_countries', tooltipProperties: [COUNTRY_NAME], - applyGlobalQuery: true, }, style: getLayerStyle(TRANSACTION_DURATION_COUNTRY), id: 'e8d1d974-eed8-462f-be2c-f0004b7619b2', diff --git a/x-pack/plugins/apm/public/components/app/ServiceDetails/alerting_popover_flyout/index.tsx b/x-pack/plugins/apm/public/components/app/ServiceDetails/alerting_popover_flyout/index.tsx deleted file mode 100644 index 3a8d24f0a8b02..0000000000000 --- a/x-pack/plugins/apm/public/components/app/ServiceDetails/alerting_popover_flyout/index.tsx +++ /dev/null @@ -1,197 +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 { - EuiButtonEmpty, - EuiContextMenu, - EuiContextMenuPanelDescriptor, - EuiPopover, -} from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import React, { useState } from 'react'; -import { AlertType } from '../../../../../common/alert_types'; -import { useApmPluginContext } from '../../../../hooks/useApmPluginContext'; -import { AlertingFlyout } from '../../../alerting/AlertingFlyout'; - -const alertLabel = i18n.translate( - 'xpack.apm.serviceDetails.alertsMenu.alerts', - { defaultMessage: 'Alerts' } -); -const transactionDurationLabel = i18n.translate( - 'xpack.apm.serviceDetails.alertsMenu.transactionDuration', - { defaultMessage: 'Transaction duration' } -); -const transactionErrorRateLabel = i18n.translate( - 'xpack.apm.serviceDetails.alertsMenu.transactionErrorRate', - { defaultMessage: 'Transaction error rate' } -); -const errorCountLabel = i18n.translate( - 'xpack.apm.serviceDetails.alertsMenu.errorCount', - { defaultMessage: 'Error count' } -); -const createThresholdAlertLabel = i18n.translate( - 'xpack.apm.serviceDetails.alertsMenu.createThresholdAlert', - { defaultMessage: 'Create threshold alert' } -); -const createAnomalyAlertAlertLabel = i18n.translate( - 'xpack.apm.serviceDetails.alertsMenu.createAnomalyAlert', - { defaultMessage: 'Create anomaly alert' } -); - -const CREATE_TRANSACTION_DURATION_ALERT_PANEL_ID = - 'create_transaction_duration_panel'; -const CREATE_TRANSACTION_ERROR_RATE_ALERT_PANEL_ID = - 'create_transaction_error_rate_panel'; -const CREATE_ERROR_COUNT_ALERT_PANEL_ID = 'create_error_count_panel'; - -interface Props { - canReadAlerts: boolean; - canSaveAlerts: boolean; - canReadAnomalies: boolean; -} - -export function AlertingPopoverAndFlyout(props: Props) { - const { canSaveAlerts, canReadAlerts, canReadAnomalies } = props; - - const plugin = useApmPluginContext(); - - const [popoverOpen, setPopoverOpen] = useState(false); - - const [alertType, setAlertType] = useState(null); - - const button = ( - setPopoverOpen(true)} - > - {alertLabel} - - ); - - const panels: EuiContextMenuPanelDescriptor[] = [ - { - id: 0, - title: alertLabel, - items: [ - ...(canSaveAlerts - ? [ - { - name: transactionDurationLabel, - panel: CREATE_TRANSACTION_DURATION_ALERT_PANEL_ID, - }, - { - name: transactionErrorRateLabel, - panel: CREATE_TRANSACTION_ERROR_RATE_ALERT_PANEL_ID, - }, - { - name: errorCountLabel, - panel: CREATE_ERROR_COUNT_ALERT_PANEL_ID, - }, - ] - : []), - ...(canReadAlerts - ? [ - { - name: i18n.translate( - 'xpack.apm.serviceDetails.alertsMenu.viewActiveAlerts', - { defaultMessage: 'View active alerts' } - ), - href: plugin.core.http.basePath.prepend( - '/app/management/insightsAndAlerting/triggersActions/alerts' - ), - icon: 'tableOfContents', - }, - ] - : []), - ], - }, - - // transaction duration panel - { - id: CREATE_TRANSACTION_DURATION_ALERT_PANEL_ID, - title: transactionDurationLabel, - items: [ - // threshold alerts - { - name: createThresholdAlertLabel, - onClick: () => { - setAlertType(AlertType.TransactionDuration); - setPopoverOpen(false); - }, - }, - - // anomaly alerts - ...(canReadAnomalies - ? [ - { - name: createAnomalyAlertAlertLabel, - onClick: () => { - setAlertType(AlertType.TransactionDurationAnomaly); - setPopoverOpen(false); - }, - }, - ] - : []), - ], - }, - - // transaction error rate panel - { - id: CREATE_TRANSACTION_ERROR_RATE_ALERT_PANEL_ID, - title: transactionErrorRateLabel, - items: [ - // threshold alerts - { - name: createThresholdAlertLabel, - onClick: () => { - setAlertType(AlertType.TransactionErrorRate); - setPopoverOpen(false); - }, - }, - ], - }, - - // error alerts panel - { - id: CREATE_ERROR_COUNT_ALERT_PANEL_ID, - title: errorCountLabel, - items: [ - { - name: createThresholdAlertLabel, - onClick: () => { - setAlertType(AlertType.ErrorCount); - setPopoverOpen(false); - }, - }, - ], - }, - ]; - - return ( - <> - setPopoverOpen(false)} - panelPaddingSize="none" - anchorPosition="downRight" - > - - - { - if (!visible) { - setAlertType(null); - } - }} - /> - - ); -} diff --git a/x-pack/plugins/apm/public/components/app/ServiceDetails/index.tsx b/x-pack/plugins/apm/public/components/app/ServiceDetails/index.tsx index 8825702cafd51..aa5dcd5a5ea18 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceDetails/index.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceDetails/index.tsx @@ -4,19 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - EuiButtonEmpty, - EuiFlexGroup, - EuiFlexItem, - EuiTitle, -} from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; +import { EuiTitle } from '@elastic/eui'; import React from 'react'; import { RouteComponentProps } from 'react-router-dom'; -import { useApmPluginContext } from '../../../hooks/useApmPluginContext'; -import { getAlertingCapabilities } from '../../alerting/get_alert_capabilities'; import { ApmHeader } from '../../shared/ApmHeader'; -import { AlertingPopoverAndFlyout } from './alerting_popover_flyout'; import { ServiceDetailTabs } from './ServiceDetailTabs'; interface Props extends RouteComponentProps<{ serviceName: string }> { @@ -24,51 +15,15 @@ interface Props extends RouteComponentProps<{ serviceName: string }> { } export function ServiceDetails({ match, tab }: Props) { - const { core, plugins } = useApmPluginContext(); const { serviceName } = match.params; - const { - isAlertingAvailable, - canReadAlerts, - canSaveAlerts, - canReadAnomalies, - } = getAlertingCapabilities(plugins, core.application.capabilities); - - const ADD_DATA_LABEL = i18n.translate('xpack.apm.addDataButtonLabel', { - defaultMessage: 'Add data', - }); - return (
- - - -

{serviceName}

-
-
- {isAlertingAvailable && ( - - - - )} - - - {ADD_DATA_LABEL} - - -
+ +

{serviceName}

+
-
); diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/useRefDimensions.ts b/x-pack/plugins/apm/public/components/app/ServiceMap/useRefDimensions.ts index c8639b334f66a..fc478e27ccac3 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/useRefDimensions.ts +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/useRefDimensions.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import { useRef } from 'react'; -import { useWindowSize } from 'react-use'; +import useWindowSize from 'react-use/lib/useWindowSize'; export function useRefDimensions() { const ref = useRef(null); diff --git a/x-pack/plugins/apm/public/components/app/Settings/index.tsx b/x-pack/plugins/apm/public/components/app/Settings/index.tsx index e770116ac2759..c9c577285ee80 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/index.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/index.tsx @@ -14,6 +14,8 @@ import { import { i18n } from '@kbn/i18n'; import React, { ReactNode } from 'react'; import { RouteComponentProps } from 'react-router-dom'; +import { HeaderMenuPortal } from '../../../../../observability/public'; +import { ActionMenu } from '../../../application/action_menu'; import { useApmPluginContext } from '../../../hooks/useApmPluginContext'; import { getAPMHref } from '../../shared/Links/apm/APMLink'; import { HomeLink } from '../../shared/Links/apm/HomeLink'; @@ -23,7 +25,7 @@ interface SettingsProps extends RouteComponentProps<{}> { } export function Settings({ children, location }: SettingsProps) { - const { core } = useApmPluginContext(); + const { appMountParameters, core } = useApmPluginContext(); const { basePath } = core.http; const canAccessML = !!core.application.capabilities.ml?.canAccessML; const { search, pathname } = location; @@ -34,6 +36,11 @@ export function Settings({ children, location }: SettingsProps) { return ( <> + + + {i18n.translate('xpack.apm.settings.returnLinkLabel', { diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/Distribution/index.tsx b/x-pack/plugins/apm/public/components/app/TransactionDetails/Distribution/index.tsx index 67125d41635a9..bf1bda793179f 100644 --- a/x-pack/plugins/apm/public/components/app/TransactionDetails/Distribution/index.tsx +++ b/x-pack/plugins/apm/public/components/app/TransactionDetails/Distribution/index.tsx @@ -4,22 +4,37 @@ * you may not use this file except in compliance with the Elastic License. */ +import { + Axis, + Chart, + ElementClickListener, + GeometryValue, + HistogramBarSeries, + Position, + RectAnnotation, + ScaleType, + Settings, + SettingsSpec, + TooltipValue, + XYChartSeriesIdentifier, +} from '@elastic/charts'; import { EuiIconTip, EuiTitle } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import d3 from 'd3'; import { isEmpty } from 'lodash'; import React, { useCallback } from 'react'; import { ValuesType } from 'utility-types'; +import { useTheme } from '../../../../../../observability/public'; import { getDurationFormatter } from '../../../../../common/utils/formatters'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { TransactionDistributionAPIResponse } from '../../../../../server/lib/transactions/distribution'; +import type { TransactionDistributionAPIResponse } from '../../../../../server/lib/transactions/distribution'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { DistributionBucket } from '../../../../../server/lib/transactions/distribution/get_buckets'; +import type { DistributionBucket } from '../../../../../server/lib/transactions/distribution/get_buckets'; import { IUrlParams } from '../../../../context/UrlParamsContext/types'; -// @ts-expect-error -import Histogram from '../../../shared/charts/Histogram'; +import { FETCH_STATUS } from '../../../../hooks/useFetcher'; +import { unit } from '../../../../style/variables'; +import { ChartContainer } from '../../../shared/charts/chart_container'; import { EmptyMessage } from '../../../shared/EmptyMessage'; -import { LoadingStatePrompt } from '../../../shared/LoadingStatePrompt'; interface IChartPoint { x0: number; @@ -31,10 +46,10 @@ interface IChartPoint { } export function getFormattedBuckets( - buckets: DistributionBucket[], - bucketSize: number + buckets?: DistributionBucket[], + bucketSize?: number ) { - if (!buckets) { + if (!buckets || !bucketSize) { return []; } @@ -74,7 +89,7 @@ const getFormatYLong = (transactionType: string | undefined) => (t: number) => { 'xpack.apm.transactionDetails.transactionsDurationDistributionChart.requestTypeUnitLongLabel', { defaultMessage: - '{transCount, plural, =0 {# request} one {# request} other {# requests}}', + '{transCount, plural, =0 {request} one {request} other {requests}}', values: { transCount: t, }, @@ -84,7 +99,7 @@ const getFormatYLong = (transactionType: string | undefined) => (t: number) => { 'xpack.apm.transactionDetails.transactionsDurationDistributionChart.transactionTypeUnitLongLabel', { defaultMessage: - '{transCount, plural, =0 {# transaction} one {# transaction} other {# transactions}}', + '{transCount, plural, =0 {transaction} one {transaction} other {transactions}}', values: { transCount: t, }, @@ -95,21 +110,21 @@ const getFormatYLong = (transactionType: string | undefined) => (t: number) => { interface Props { distribution?: TransactionDistributionAPIResponse; urlParams: IUrlParams; - isLoading: boolean; + fetchStatus: FETCH_STATUS; bucketIndex: number; onBucketClick: ( bucket: ValuesType ) => void; } -export function TransactionDistribution(props: Props) { - const { - distribution, - urlParams: { transactionType }, - isLoading, - bucketIndex, - onBucketClick, - } = props; +export function TransactionDistribution({ + distribution, + urlParams: { transactionType }, + fetchStatus, + bucketIndex, + onBucketClick, +}: Props) { + const theme = useTheme(); /* eslint-disable-next-line react-hooks/exhaustive-deps */ const formatYShort = useCallback(getFormatYShort(transactionType), [ @@ -122,12 +137,10 @@ export function TransactionDistribution(props: Props) { ]); // no data in response - if (!distribution || distribution.noHits) { - // only show loading state if there is no data - else show stale data until new data has loaded - if (isLoading) { - return ; - } - + if ( + (!distribution || distribution.noHits) && + fetchStatus !== FETCH_STATUS.LOADING + ) { return ( { - return bucket.key === chartPoint.x0; - }); - - return clickedBucket; - } - const buckets = getFormattedBuckets( - distribution.buckets, - distribution.bucketSize + distribution?.buckets, + distribution?.bucketSize ); - const xMax = d3.max(buckets, (d) => d.x) || 0; + const xMin = d3.min(buckets, (d) => d.x0) || 0; + const xMax = d3.max(buckets, (d) => d.x0) || 0; const timeFormatter = getDurationFormatter(xMax); + const tooltipProps: SettingsSpec['tooltip'] = { + headerFormatter: (tooltip: TooltipValue) => { + const serie = buckets.find((bucket) => bucket.x0 === tooltip.value); + if (serie) { + const xFormatted = timeFormatter(serie.x); + const x0Formatted = timeFormatter(serie.x0); + return `${x0Formatted.value} - ${xFormatted.value} ${xFormatted.unit}`; + } + return `${timeFormatter(tooltip.value)}`; + }, + }; + + const onBarClick: ElementClickListener = (elements) => { + const chartPoint = elements[0][0] as GeometryValue; + const clickedBucket = distribution?.buckets.find((bucket) => { + return bucket.key === chartPoint.x; + }); + if (clickedBucket) { + onBucketClick(clickedBucket); + } + }; + + const selectedBucket = buckets[bucketIndex]; + return (
@@ -181,42 +211,66 @@ export function TransactionDistribution(props: Props) { /> - - { - const clickedBucket = getBucketFromChartPoint(chartPoint); - - if (clickedBucket) { - onBucketClick(clickedBucket); - } - }} - formatX={(time: number) => timeFormatter(time).formatted} - formatYShort={formatYShort} - formatYLong={formatYLong} - verticalLineHover={(point: IChartPoint) => - isEmpty(getBucketFromChartPoint(point)?.samples) - } - backgroundHover={(point: IChartPoint) => - !isEmpty(getBucketFromChartPoint(point)?.samples) - } - tooltipHeader={(point: IChartPoint) => { - const xFormatted = timeFormatter(point.x); - const x0Formatted = timeFormatter(point.x0); - return `${x0Formatted.value} - ${xFormatted.value} ${xFormatted.unit}`; - }} - tooltipFooter={(point: IChartPoint) => - isEmpty(getBucketFromChartPoint(point)?.samples) && - i18n.translate( - 'xpack.apm.transactionDetails.transactionsDurationDistributionChart.noSampleTooltip', - { - defaultMessage: 'No sample available for this bucket', - } - ) - } - /> + + + + {selectedBucket && ( + + )} + timeFormatter(time).formatted} + /> + formatYShort(value)} + /> + value} + minBarHeight={2} + id="transactionDurationDistribution" + name={(series: XYChartSeriesIdentifier) => { + const bucketCount = series.splitAccessors.get( + series.yAccessor + ) as number; + return formatYLong(bucketCount); + }} + splitSeriesAccessors={['y']} + xScaleType={ScaleType.Linear} + yScaleType={ScaleType.Linear} + xAccessor="x0" + yAccessors={['y']} + data={buckets} + color={theme.eui.euiColorVis1} + /> + +
); } diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/index.tsx b/x-pack/plugins/apm/public/components/app/TransactionDetails/index.tsx index efdd7b1f34221..e4c36b028e55c 100644 --- a/x-pack/plugins/apm/public/components/app/TransactionDetails/index.tsx +++ b/x-pack/plugins/apm/public/components/app/TransactionDetails/index.tsx @@ -52,7 +52,11 @@ export function TransactionDetails({ status: distributionStatus, } = useTransactionDistribution(urlParams); - const { data: transactionChartsData } = useTransactionCharts(); + const { + data: transactionChartsData, + status: transactionChartsStatus, + } = useTransactionCharts(); + const { waterfall, exceedsMax, status: waterfallStatus } = useWaterfall( urlParams ); @@ -121,6 +125,7 @@ export function TransactionDetails({ @@ -131,7 +136,7 @@ export function TransactionDetails({ { diff --git a/x-pack/plugins/apm/public/components/app/TransactionOverview/TransactionOverview.test.tsx b/x-pack/plugins/apm/public/components/app/TransactionOverview/TransactionOverview.test.tsx index b7d1b93600a73..c530a7e1489ad 100644 --- a/x-pack/plugins/apm/public/components/app/TransactionOverview/TransactionOverview.test.tsx +++ b/x-pack/plugins/apm/public/components/app/TransactionOverview/TransactionOverview.test.tsx @@ -4,12 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - fireEvent, - getByText, - queryByLabelText, - render, -} from '@testing-library/react'; +import { fireEvent, getByText, queryByLabelText } from '@testing-library/react'; import { createMemoryHistory } from 'history'; import { CoreStart } from 'kibana/public'; import React from 'react'; @@ -20,7 +15,10 @@ import { UrlParamsProvider } from '../../../context/UrlParamsContext'; import { IUrlParams } from '../../../context/UrlParamsContext/types'; import * as useFetcherHook from '../../../hooks/useFetcher'; import * as useServiceTransactionTypesHook from '../../../hooks/useServiceTransactionTypes'; -import { disableConsoleWarning } from '../../../utils/testHelpers'; +import { + disableConsoleWarning, + renderWithTheme, +} from '../../../utils/testHelpers'; import { fromQuery } from '../../shared/Links/url_helpers'; import { TransactionOverview } from './'; @@ -54,7 +52,7 @@ function setup({ jest.spyOn(useFetcherHook, 'useFetcher').mockReturnValue({} as any); - return render( + return renderWithTheme( diff --git a/x-pack/plugins/apm/public/components/app/TransactionOverview/index.tsx b/x-pack/plugins/apm/public/components/app/TransactionOverview/index.tsx index 5444d2d521f37..df9e673ed4847 100644 --- a/x-pack/plugins/apm/public/components/app/TransactionOverview/index.tsx +++ b/x-pack/plugins/apm/public/components/app/TransactionOverview/index.tsx @@ -22,7 +22,7 @@ import React, { useMemo } from 'react'; import { useLocation } from 'react-router-dom'; import { useTrackPageview } from '../../../../../observability/public'; import { Projection } from '../../../../common/projections'; -import { LegacyChartsSyncContextProvider as ChartsSyncContextProvider } from '../../../context/charts_sync_context'; +import { TRANSACTION_PAGE_LOAD } from '../../../../common/transaction_types'; import { IUrlParams } from '../../../context/UrlParamsContext/types'; import { useServiceTransactionTypes } from '../../../hooks/useServiceTransactionTypes'; import { useTransactionCharts } from '../../../hooks/useTransactionCharts'; @@ -33,11 +33,10 @@ import { ElasticDocsLink } from '../../shared/Links/ElasticDocsLink'; import { fromQuery, toQuery } from '../../shared/Links/url_helpers'; import { LocalUIFilters } from '../../shared/LocalUIFilters'; import { TransactionTypeFilter } from '../../shared/LocalUIFilters/TransactionTypeFilter'; +import { Correlations } from '../Correlations'; import { TransactionList } from './TransactionList'; import { useRedirect } from './useRedirect'; -import { TRANSACTION_PAGE_LOAD } from '../../../../common/transaction_types'; import { UserExperienceCallout } from './user_experience_callout'; -import { Correlations } from '../Correlations'; function getRedirectLocation({ urlParams, @@ -83,7 +82,10 @@ export function TransactionOverview({ serviceName }: TransactionOverviewProps) { }) ); - const { data: transactionCharts } = useTransactionCharts(); + const { + data: transactionCharts, + status: transactionChartsStatus, + } = useTransactionCharts(); useTrackPageview({ app: 'apm', path: 'transaction_overview' }); useTrackPageview({ app: 'apm', path: 'transaction_overview', delay: 15000 }); @@ -135,12 +137,11 @@ export function TransactionOverview({ serviceName }: TransactionOverviewProps) { )} - - - + @@ -190,7 +191,7 @@ export function TransactionOverview({ serviceName }: TransactionOverviewProps) { diff --git a/x-pack/plugins/apm/public/components/app/service_inventory/service_inventory.test.tsx b/x-pack/plugins/apm/public/components/app/service_inventory/service_inventory.test.tsx index d394c7db62554..247e91fb438ef 100644 --- a/x-pack/plugins/apm/public/components/app/service_inventory/service_inventory.test.tsx +++ b/x-pack/plugins/apm/public/components/app/service_inventory/service_inventory.test.tsx @@ -149,7 +149,7 @@ describe('ServiceInventory', () => { "Looks like you don't have any APM services installed. Let's add some!" ); - expect(gettingStartedMessage).not.toBeEmpty(); + expect(gettingStartedMessage).not.toBeEmptyDOMElement(); }); it('should render empty message, when list is empty and historical data is found', async () => { @@ -165,7 +165,7 @@ describe('ServiceInventory', () => { await waitFor(() => expect(httpGet).toHaveBeenCalledTimes(1)); const noServicesText = await findByText('No services found'); - expect(noServicesText).not.toBeEmpty(); + expect(noServicesText).not.toBeEmptyDOMElement(); }); describe('when legacy data is found', () => { diff --git a/x-pack/plugins/apm/public/components/app/service_overview/index.tsx b/x-pack/plugins/apm/public/components/app/service_overview/index.tsx index 342152b572f1e..016ee3daf6b51 100644 --- a/x-pack/plugins/apm/public/components/app/service_overview/index.tsx +++ b/x-pack/plugins/apm/public/components/app/service_overview/index.tsx @@ -11,7 +11,7 @@ import styled from 'styled-components'; import { useTrackPageview } from '../../../../../observability/public'; import { isRumAgentName } from '../../../../common/agent_name'; import { ChartsSyncContextProvider } from '../../../context/charts_sync_context'; -import { ErroneousTransactionsRateChart } from '../../shared/charts/erroneous_transactions_rate_chart'; +import { TransactionErrorRateChart } from '../../shared/charts/transaction_error_rate_chart'; import { ErrorOverviewLink } from '../../shared/Links/apm/ErrorOverviewLink'; import { ServiceMapLink } from '../../shared/Links/apm/ServiceMapLink'; import { TransactionOverviewLink } from '../../shared/Links/apm/TransactionOverviewLink'; @@ -125,19 +125,7 @@ export function ServiceOverview({ {!isRumAgentName(agentName) && ( - - -

- {i18n.translate( - 'xpack.apm.serviceOverview.errorRateChartTitle', - { - defaultMessage: 'Error rate', - } - )} -

-
- -
+
)} diff --git a/x-pack/plugins/apm/public/components/shared/ApmHeader/index.tsx b/x-pack/plugins/apm/public/components/shared/ApmHeader/index.tsx index 6674abe9b8ce5..96f170fa6a093 100644 --- a/x-pack/plugins/apm/public/components/shared/ApmHeader/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/ApmHeader/index.tsx @@ -6,13 +6,21 @@ import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; import React, { ReactNode } from 'react'; +import { HeaderMenuPortal } from '../../../../../observability/public'; +import { ActionMenu } from '../../../application/action_menu'; +import { useApmPluginContext } from '../../../hooks/useApmPluginContext'; import { DatePicker } from '../DatePicker'; import { EnvironmentFilter } from '../EnvironmentFilter'; import { KueryBar } from '../KueryBar'; export function ApmHeader({ children }: { children: ReactNode }) { + const { setHeaderActionMenu } = useApmPluginContext().appMountParameters; + return ( <> + + + {children} diff --git a/x-pack/plugins/apm/public/components/shared/KueryBar/get_bool_filter.ts b/x-pack/plugins/apm/public/components/shared/KueryBar/get_bool_filter.ts index 74d7ace20dae0..f0d12fd16bf7a 100644 --- a/x-pack/plugins/apm/public/components/shared/KueryBar/get_bool_filter.ts +++ b/x-pack/plugins/apm/public/components/shared/KueryBar/get_bool_filter.ts @@ -26,23 +26,7 @@ export function getBoolFilter({ serviceName?: string; urlParams: IUrlParams; }) { - const { start, end } = urlParams; - - if (!start || !end) { - throw new Error('Date range was not defined'); - } - - const boolFilter: ESFilter[] = [ - { - range: { - '@timestamp': { - gte: new Date(start).getTime(), - lte: new Date(end).getTime(), - format: 'epoch_millis', - }, - }, - }, - ]; + const boolFilter: ESFilter[] = []; if (serviceName) { boolFilter.push({ diff --git a/x-pack/plugins/apm/public/components/shared/KueryBar/index.tsx b/x-pack/plugins/apm/public/components/shared/KueryBar/index.tsx index 157e014bee424..dce8e49deec41 100644 --- a/x-pack/plugins/apm/public/components/shared/KueryBar/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/KueryBar/index.tsx @@ -111,6 +111,7 @@ export function KueryBar() { query: inputValue, selectionStart, selectionEnd: selectionStart, + useTimeRange: true, })) || [] ) .filter((suggestion) => !startsWith(suggestion.text, 'span.')) diff --git a/x-pack/plugins/apm/public/components/shared/Links/SetupInstructionsLink.tsx b/x-pack/plugins/apm/public/components/shared/Links/SetupInstructionsLink.tsx index a5bcec1501ad3..0ff73d91d7c5b 100644 --- a/x-pack/plugins/apm/public/components/shared/Links/SetupInstructionsLink.tsx +++ b/x-pack/plugins/apm/public/components/shared/Links/SetupInstructionsLink.tsx @@ -34,7 +34,7 @@ export function SetupInstructionsLink({ {SETUP_INSTRUCTIONS_LABEL} ) : ( - + {ADD_DATA_LABEL} )} diff --git a/x-pack/plugins/apm/public/components/shared/TransactionBreakdown/TransactionBreakdownGraph/index.tsx b/x-pack/plugins/apm/public/components/shared/TransactionBreakdown/TransactionBreakdownGraph/index.tsx index b908eb8da4d03..05cae589c19fc 100644 --- a/x-pack/plugins/apm/public/components/shared/TransactionBreakdown/TransactionBreakdownGraph/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/TransactionBreakdown/TransactionBreakdownGraph/index.tsx @@ -4,62 +4,113 @@ * you may not use this file except in compliance with the Elastic License. */ -import { throttle } from 'lodash'; -import React, { useMemo } from 'react'; +import { + AreaSeries, + Axis, + Chart, + niceTimeFormatter, + Placement, + Position, + ScaleType, + Settings, +} from '@elastic/charts'; +import moment from 'moment'; +import React, { useEffect } from 'react'; +import { useHistory } from 'react-router-dom'; import { asPercent } from '../../../../../common/utils/formatters'; -import { useUiTracker } from '../../../../../../observability/public'; -import { NOT_AVAILABLE_LABEL } from '../../../../../common/i18n'; -import { Maybe } from '../../../../../typings/common'; -import { Coordinate, TimeSeries } from '../../../../../typings/timeseries'; +import { TimeSeries } from '../../../../../typings/timeseries'; +import { FETCH_STATUS } from '../../../../hooks/useFetcher'; import { useUrlParams } from '../../../../hooks/useUrlParams'; -import { isValidCoordinateValue } from '../../../../utils/isValidCoordinateValue'; -import { getEmptySeries } from '../../charts/CustomPlot/getEmptySeries'; -import { TransactionLineChart } from '../../charts/TransactionCharts/TransactionLineChart'; +import { useChartsSync as useChartsSync2 } from '../../../../hooks/use_charts_sync'; +import { unit } from '../../../../style/variables'; +import { Annotations } from '../../charts/annotations'; +import { ChartContainer } from '../../charts/chart_container'; +import { onBrushEnd } from '../../charts/helper/helper'; + +const XY_HEIGHT = unit * 16; interface Props { - timeseries: TimeSeries[]; - noHits: boolean; + fetchStatus: FETCH_STATUS; + timeseries?: TimeSeries[]; } -const tickFormatY = (y: Maybe) => { - return asPercent(y ?? 0, 1); -}; +export function TransactionBreakdownGraph({ fetchStatus, timeseries }: Props) { + const history = useHistory(); + const chartRef = React.createRef(); + const { event, setEvent } = useChartsSync2(); + const { urlParams } = useUrlParams(); + const { start, end } = urlParams; -const formatTooltipValue = (coordinate: Coordinate) => { - return isValidCoordinateValue(coordinate.y) - ? asPercent(coordinate.y, 1) - : NOT_AVAILABLE_LABEL; -}; + useEffect(() => { + if (event.chartId !== 'timeSpentBySpan' && chartRef.current) { + chartRef.current.dispatchExternalPointerEvent(event); + } + }, [chartRef, event]); -function TransactionBreakdownGraph({ timeseries, noHits }: Props) { - const { urlParams } = useUrlParams(); - const { rangeFrom, rangeTo } = urlParams; - const trackApmEvent = useUiTracker({ app: 'apm' }); - const handleHover = useMemo( - () => - throttle(() => trackApmEvent({ metric: 'hover_breakdown_chart' }), 60000), - [trackApmEvent] - ); + const min = moment.utc(start).valueOf(); + const max = moment.utc(end).valueOf(); - const emptySeries = - rangeFrom && rangeTo - ? getEmptySeries( - new Date(rangeFrom).getTime(), - new Date(rangeTo).getTime() - ) - : []; + const xFormatter = niceTimeFormatter([min, max]); return ( - + + + onBrushEnd({ x, history })} + showLegend + showLegendExtra + legendPosition={Position.Bottom} + xDomain={{ min, max }} + flatLegend + onPointerUpdate={(currEvent: any) => { + setEvent(currEvent); + }} + externalPointerEvents={{ + tooltip: { visible: true, placement: Placement.Bottom }, + }} + /> + + asPercent(y ?? 0, 1)} + /> + + + + {timeseries?.length ? ( + timeseries.map((serie) => { + return ( + + ); + }) + ) : ( + // When timeseries is empty, loads an AreaSeries chart to show the default empty message. + + )} + + ); } - -export { TransactionBreakdownGraph }; diff --git a/x-pack/plugins/apm/public/components/shared/TransactionBreakdown/index.tsx b/x-pack/plugins/apm/public/components/shared/TransactionBreakdown/index.tsx index 55826497ca385..9b0c041aaf7b5 100644 --- a/x-pack/plugins/apm/public/components/shared/TransactionBreakdown/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/TransactionBreakdown/index.tsx @@ -5,16 +5,13 @@ */ import { EuiFlexGroup, EuiFlexItem, EuiPanel, EuiTitle } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { isEmpty } from 'lodash'; import React from 'react'; -import { FETCH_STATUS } from '../../../hooks/useFetcher'; import { useTransactionBreakdown } from '../../../hooks/useTransactionBreakdown'; import { TransactionBreakdownGraph } from './TransactionBreakdownGraph'; function TransactionBreakdown() { const { data, status } = useTransactionBreakdown(); const { timeseries } = data; - const noHits = isEmpty(timeseries) && status === FETCH_STATUS.SUCCESS; return ( @@ -29,7 +26,10 @@ function TransactionBreakdown() { - + diff --git a/x-pack/plugins/apm/public/components/shared/charts/Histogram/SingleRect.js b/x-pack/plugins/apm/public/components/shared/charts/Histogram/SingleRect.js deleted file mode 100644 index ca85ee961f5d8..0000000000000 --- a/x-pack/plugins/apm/public/components/shared/charts/Histogram/SingleRect.js +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React from 'react'; -import PropTypes from 'prop-types'; - -function SingleRect({ innerHeight, marginTop, style, x, width }) { - return ( - - ); -} - -SingleRect.requiresSVG = true; -SingleRect.propTypes = { - x: PropTypes.number.isRequired, -}; - -export default SingleRect; diff --git a/x-pack/plugins/apm/public/components/shared/charts/Histogram/__test__/Histogram.test.js b/x-pack/plugins/apm/public/components/shared/charts/Histogram/__test__/Histogram.test.js deleted file mode 100644 index 03fd039a3401e..0000000000000 --- a/x-pack/plugins/apm/public/components/shared/charts/Histogram/__test__/Histogram.test.js +++ /dev/null @@ -1,119 +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 React from 'react'; - -import d3 from 'd3'; -import { HistogramInner } from '../index'; -import response from './response.json'; -import { - disableConsoleWarning, - toJson, - mountWithTheme, -} from '../../../../../utils/testHelpers'; -import { getFormattedBuckets } from '../../../../app/TransactionDetails/Distribution/index'; -import { - asInteger, - getDurationFormatter, -} from '../../../../../../common/utils/formatters'; - -describe('Histogram', () => { - let mockConsole; - let wrapper; - - const onClick = jest.fn(); - - beforeAll(() => { - mockConsole = disableConsoleWarning('Warning: componentWillReceiveProps'); - }); - - afterAll(() => { - mockConsole.mockRestore(); - }); - - beforeEach(() => { - const buckets = getFormattedBuckets(response.buckets, response.bucketSize); - const xMax = d3.max(buckets, (d) => d.x); - const timeFormatter = getDurationFormatter(xMax); - - wrapper = mountWithTheme( - timeFormatter(time).formatted} - formatYShort={(t) => `${asInteger(t)} occ.`} - formatYLong={(t) => `${asInteger(t)} occurrences`} - tooltipHeader={(bucket) => { - const xFormatted = timeFormatter(bucket.x); - const x0Formatted = timeFormatter(bucket.x0); - return `${x0Formatted.value} - ${xFormatted.value} ${xFormatted.unit}`; - }} - width={800} - /> - ); - }); - - describe('Initially', () => { - it('should have default markup', () => { - expect(toJson(wrapper)).toMatchSnapshot(); - }); - - it('should not show tooltip', () => { - expect(wrapper.find('Tooltip').length).toBe(0); - }); - }); - - describe('when hovering over an empty bucket', () => { - beforeEach(() => { - wrapper.find('.rv-voronoi__cell').at(2).simulate('mouseOver'); - }); - - it('should not display tooltip', () => { - expect(wrapper.find('Tooltip').length).toBe(0); - }); - }); - - describe('when hovering over a non-empty bucket', () => { - beforeEach(() => { - wrapper.find('.rv-voronoi__cell').at(7).simulate('mouseOver'); - }); - - it('should display tooltip', () => { - const tooltips = wrapper.find('Tooltip'); - - expect(tooltips.length).toBe(1); - expect(tooltips.prop('header')).toBe('811 - 927 ms'); - expect(tooltips.prop('tooltipPoints')).toEqual([ - { value: '49 occurrences' }, - ]); - expect(tooltips.prop('x')).toEqual(869010); - expect(tooltips.prop('y')).toEqual(27.5); - }); - - it('should have correct markup for tooltip', () => { - const tooltips = wrapper.find('Tooltip'); - expect(toJson(tooltips)).toMatchSnapshot(); - }); - }); - - describe('when clicking on a non-empty bucket', () => { - beforeEach(() => { - wrapper.find('.rv-voronoi__cell').at(7).simulate('click'); - }); - - it('should call onClick with bucket', () => { - expect(onClick).toHaveBeenCalledWith({ - style: { cursor: 'pointer' }, - xCenter: 869010, - x0: 811076, - x: 926944, - y: 49, - }); - }); - }); -}); diff --git a/x-pack/plugins/apm/public/components/shared/charts/Histogram/__test__/__snapshots__/Histogram.test.js.snap b/x-pack/plugins/apm/public/components/shared/charts/Histogram/__test__/__snapshots__/Histogram.test.js.snap deleted file mode 100644 index a31b9735628ab..0000000000000 --- a/x-pack/plugins/apm/public/components/shared/charts/Histogram/__test__/__snapshots__/Histogram.test.js.snap +++ /dev/null @@ -1,1504 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Histogram Initially should have default markup 1`] = ` -.c0 { - -webkit-user-select: none; - -moz-user-select: none; - -ms-user-select: none; - user-select: none; - position: absolute; - top: 0; - left: 0; -} - -
- -
-
- - - - - - - - - - - - - 0 ms - - - - - - 500 ms - - - - - - 1,000 ms - - - - - - 1,500 ms - - - - - - 2,000 ms - - - - - - 2,500 ms - - - - - - 3,000 ms - - - - - - - - - - 0 occ. - - - - - - 28 occ. - - - - - - 55 occ. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
-
-
-`; - -exports[`Histogram when hovering over a non-empty bucket should have correct markup for tooltip 1`] = ` -.c0 { - margin: 0 16px; - -webkit-transform: translateY(-50%); - -ms-transform: translateY(-50%); - transform: translateY(-50%); - border: 1px solid #d3dae6; - background: #ffffff; - border-radius: 4px; - font-size: 14px; - color: #000000; -} - -.c1 { - background: #f5f7fa; - border-bottom: 1px solid #d3dae6; - border-radius: 4px 4px 0 0; - padding: 8px; - color: #98a2b3; -} - -.c2 { - margin: 8px; - margin-right: 16px; - font-size: 12px; -} - -.c4 { - color: #98a2b3; - margin: 8px; - font-size: 12px; -} - -.c3 { - color: #69707d; - font-size: 14px; -} - -
- -
- -
- 811 - 927 ms -
-
- -
- -
- 49 occurrences -
-
-
-
- -
- -
-
-
-`; diff --git a/x-pack/plugins/apm/public/components/shared/charts/Histogram/__test__/response.json b/x-pack/plugins/apm/public/components/shared/charts/Histogram/__test__/response.json deleted file mode 100644 index 302e105dfa997..0000000000000 --- a/x-pack/plugins/apm/public/components/shared/charts/Histogram/__test__/response.json +++ /dev/null @@ -1,106 +0,0 @@ -{ - "buckets": [ - { "key": 0, "count": 0 }, - { "key": 115868, "count": 0 }, - { "key": 231736, "count": 0 }, - { "key": 347604, "count": 0 }, - { "key": 463472, "count": 0 }, - { - "key": 579340, - "count": 8, - "samples": [ - { - "transactionId": "99437ee4-08d4-41f5-9b2b-93cc32ec3dfb" - } - ] - }, - { - "key": 695208, - "count": 23, - "samples": [ - { - "transactionId": "d327611b-e999-4942-a94f-c60208940180" - } - ] - }, - { - "key": 811076, - "count": 49, - "samples": [ - { - "transactionId": "99c50a5b-44b4-4289-a3d1-a2815d128192" - } - ] - }, - { - "key": 926944, - "count": 51, - "transactionId": "9706a1ec-23f5-4ce8-97e8-69ce35fb0a9a" - }, - { - "key": 1042812, - "count": 46, - "transactionId": "f8d360c3-dd5e-47b6-b082-9e0bf821d3b2" - }, - { - "key": 1158680, - "count": 13, - "samples": [ - { - "transactionId": "8486d3e2-7f15-48df-aa37-6ee9955adbd2" - } - ] - }, - { - "key": 1274548, - "count": 7, - "transactionId": "54b4b5a7-f065-4cab-9016-534e58f4fc0a" - }, - { - "key": 1390416, - "count": 4, - "transactionId": "8cfac2a3-38e7-4d3a-9792-d008b4bcb867" - }, - { - "key": 1506284, - "count": 3, - "transactionId": "ce3f3bd3-a37c-419e-bb9c-5db956ded149" - }, - { "key": 1622152, "count": 0 }, - { - "key": 1738020, - "count": 4, - "transactionId": "2300174b-85d8-40ba-a6cb-eeba2a49debf" - }, - { "key": 1853888, "count": 0 }, - { "key": 1969756, "count": 0 }, - { - "key": 2085624, - "count": 1, - "transactionId": "774955a4-2ba3-4461-81a6-65759db4805d" - }, - { "key": 2201492, "count": 0 }, - { "key": 2317360, "count": 0 }, - { "key": 2433228, "count": 0 }, - { "key": 2549096, "count": 0 }, - { "key": 2664964, "count": 0 }, - { - "key": 2780832, - "count": 1, - "transactionId": "035d1b9d-af71-46cf-8910-57bd4faf412d" - }, - { - "key": 2896700, - "count": 1, - "transactionId": "4a845b32-9de4-4796-8ef4-d7bbdedc9099" - }, - { "key": 3012568, "count": 0 }, - { - "key": 3128436, - "count": 1, - "transactionId": "68620ffb-7a1b-4f8e-b9bb-009fa5b092be" - } - ], - "bucketSize": 115868, - "defaultBucketIndex": 12 -} diff --git a/x-pack/plugins/apm/public/components/shared/charts/Histogram/index.js b/x-pack/plugins/apm/public/components/shared/charts/Histogram/index.js deleted file mode 100644 index 3b2109d68c613..0000000000000 --- a/x-pack/plugins/apm/public/components/shared/charts/Histogram/index.js +++ /dev/null @@ -1,319 +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 React, { PureComponent } from 'react'; -import d3 from 'd3'; -import { isEmpty } from 'lodash'; -import PropTypes from 'prop-types'; -import { scaleLinear } from 'd3-scale'; -import styled from 'styled-components'; -import SingleRect from './SingleRect'; -import { - XYPlot, - XAxis, - YAxis, - HorizontalGridLines, - VerticalRectSeries, - Voronoi, - makeWidthFlexible, - VerticalGridLines, -} from 'react-vis'; -import { unit } from '../../../../style/variables'; -import Tooltip from '../Tooltip'; -import theme from '@elastic/eui/dist/eui_theme_light.json'; -import { tint } from 'polished'; -import { getTimeTicksTZ, getDomainTZ } from '../helper/timezone'; -import Legends from '../CustomPlot/Legends'; -import StatusText from '../CustomPlot/StatusText'; -import { i18n } from '@kbn/i18n'; -import { isValidCoordinateValue } from '../../../../utils/isValidCoordinateValue'; - -const XY_HEIGHT = unit * 10; -const XY_MARGIN = { - top: unit, - left: unit * 5, - right: unit, - bottom: unit * 2, -}; - -const X_TICK_TOTAL = 8; - -// position absolutely to make sure that window resizing/zooming works -const ChartsWrapper = styled.div` - user-select: none; - position: absolute; - top: 0; - left: 0; -`; - -export class HistogramInner extends PureComponent { - constructor(props) { - super(props); - this.state = { - hoveredBucket: {}, - }; - } - - onClick = (bucket) => { - if (this.props.onClick) { - this.props.onClick(bucket); - } - }; - - onHover = (bucket) => { - this.setState({ hoveredBucket: bucket }); - }; - - onBlur = () => { - this.setState({ hoveredBucket: {} }); - }; - - getChartData(items, selectedItem) { - const yMax = d3.max(items, (d) => d.y); - const MINIMUM_BUCKET_SIZE = yMax * 0.02; - - return items.map((item) => { - const padding = (item.x - item.x0) / 20; - return { - ...item, - color: - item === selectedItem - ? theme.euiColorVis1 - : tint(0.5, theme.euiColorVis1), - x0: item.x0 + padding, - x: item.x - padding, - y: item.y > 0 ? Math.max(item.y, MINIMUM_BUCKET_SIZE) : 0, - }; - }); - } - - render() { - const { - backgroundHover, - bucketIndex, - buckets, - bucketSize, - formatX, - formatYShort, - formatYLong, - tooltipFooter, - tooltipHeader, - verticalLineHover, - width: XY_WIDTH, - height, - legends, - } = this.props; - const { hoveredBucket } = this.state; - if (isEmpty(buckets) || XY_WIDTH === 0) { - return null; - } - - const isTimeSeries = - this.props.xType === 'time' || this.props.xType === 'time-utc'; - - const xMin = d3.min(buckets, (d) => d.x0); - const xMax = d3.max(buckets, (d) => d.x); - const yMin = 0; - const yMax = d3.max(buckets, (d) => d.y); - const selectedBucket = buckets[bucketIndex]; - const chartData = this.getChartData(buckets, selectedBucket); - - const x = scaleLinear() - .domain([xMin, xMax]) - .range([XY_MARGIN.left, XY_WIDTH - XY_MARGIN.right]); - - const y = scaleLinear().domain([yMin, yMax]).range([XY_HEIGHT, 0]).nice(); - - const [xMinZone, xMaxZone] = getDomainTZ(xMin, xMax); - const xTickValues = isTimeSeries - ? getTimeTicksTZ({ - domain: [xMinZone, xMaxZone], - totalTicks: X_TICK_TOTAL, - width: XY_WIDTH, - }) - : undefined; - - const xDomain = x.domain(); - const yDomain = y.domain(); - const yTickValues = [0, yDomain[1] / 2, yDomain[1]]; - const shouldShowTooltip = - hoveredBucket.x > 0 && (hoveredBucket.y > 0 || isTimeSeries); - - const showVerticalLineHover = verticalLineHover(hoveredBucket); - const showBackgroundHover = backgroundHover(hoveredBucket); - - const hasValidCoordinates = buckets.some((bucket) => - isValidCoordinateValue(bucket.y) - ); - const noHits = this.props.noHits || !hasValidCoordinates; - - const xyPlotProps = { - dontCheckIfEmpty: true, - xType: this.props.xType, - width: XY_WIDTH, - height: XY_HEIGHT, - margin: XY_MARGIN, - xDomain: xDomain, - yDomain: yDomain, - }; - - const xAxisProps = { - style: { strokeWidth: '1px' }, - marginRight: 10, - tickSize: 0, - tickTotal: X_TICK_TOTAL, - tickFormat: formatX, - tickValues: xTickValues, - }; - - const emptyStateChart = ( - - - - - ); - - return ( -
- - {noHits ? ( - <>{emptyStateChart} - ) : ( - <> - - - - - - {showBackgroundHover && ( - - )} - - {shouldShowTooltip && ( - - )} - - {selectedBucket && ( - - )} - - - - {showVerticalLineHover && hoveredBucket?.x && ( - - )} - - { - return { - ...bucket, - xCenter: (bucket.x0 + bucket.x) / 2, - }; - })} - onClick={this.onClick} - onHover={this.onHover} - onBlur={this.onBlur} - x={(d) => x(d.xCenter)} - y={() => 1} - /> - - - {legends && ( - {}} - truncateLegends={false} - noHits={noHits} - /> - )} - - )} - -
- ); - } -} - -HistogramInner.propTypes = { - backgroundHover: PropTypes.func, - bucketIndex: PropTypes.number, - buckets: PropTypes.array.isRequired, - bucketSize: PropTypes.number.isRequired, - formatX: PropTypes.func, - formatYLong: PropTypes.func, - formatYShort: PropTypes.func, - onClick: PropTypes.func, - tooltipFooter: PropTypes.func, - tooltipHeader: PropTypes.func, - verticalLineHover: PropTypes.func, - width: PropTypes.number.isRequired, - height: PropTypes.number, - xType: PropTypes.string, - legends: PropTypes.array, - noHits: PropTypes.bool, -}; - -HistogramInner.defaultProps = { - backgroundHover: () => null, - formatYLong: (value) => value, - formatYShort: (value) => value, - tooltipFooter: () => null, - tooltipHeader: () => null, - verticalLineHover: () => null, - xType: 'linear', - noHits: false, - height: XY_HEIGHT, -}; - -export default makeWidthFlexible(HistogramInner); diff --git a/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/TransactionLineChart/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/TransactionLineChart/index.tsx deleted file mode 100644 index 2e4b51af00d6b..0000000000000 --- a/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/TransactionLineChart/index.tsx +++ /dev/null @@ -1,70 +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 React, { useCallback } from 'react'; -import { Coordinate, TimeSeries } from '../../../../../../typings/timeseries'; -import { useLegacyChartsSync as useChartsSync } from '../../../../../hooks/use_charts_sync'; -// @ts-expect-error -import CustomPlot from '../../CustomPlot'; - -interface Props { - series: TimeSeries[]; - truncateLegends?: boolean; - tickFormatY: (y: number) => React.ReactNode; - formatTooltipValue: (c: Coordinate) => React.ReactNode; - yMax?: string | number; - height?: number; - stacked?: boolean; - onHover?: () => void; - visibleLegendCount?: number; - onToggleLegend?: (disabledSeriesState: boolean[]) => void; -} - -function TransactionLineChart(props: Props) { - const { - series, - tickFormatY, - formatTooltipValue, - yMax = 'max', - height, - truncateLegends, - stacked = false, - onHover, - visibleLegendCount, - onToggleLegend, - } = props; - - const syncedChartsProps = useChartsSync(); - - // combine callback for syncedChartsProps.onHover and props.onHover - const combinedOnHover = useCallback( - (hoverX: number) => { - if (onHover) { - onHover(); - } - return syncedChartsProps.onHover(hoverX); - }, - [syncedChartsProps, onHover] - ); - - return ( - - ); -} - -export { TransactionLineChart }; diff --git a/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/index.tsx index b3c0c3b6de857..2a5948d0ebf0b 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/index.tsx @@ -20,104 +20,107 @@ import { TRANSACTION_REQUEST, TRANSACTION_ROUTE_CHANGE, } from '../../../../../common/transaction_types'; +import { asDecimal, tpmUnit } from '../../../../../common/utils/formatters'; import { Coordinate } from '../../../../../typings/timeseries'; +import { ChartsSyncContextProvider } from '../../../../context/charts_sync_context'; import { LicenseContext } from '../../../../context/LicenseContext'; import { IUrlParams } from '../../../../context/UrlParamsContext/types'; +import { FETCH_STATUS } from '../../../../hooks/useFetcher'; import { ITransactionChartData } from '../../../../selectors/chartSelectors'; -import { asDecimal, tpmUnit } from '../../../../../common/utils/formatters'; import { isValidCoordinateValue } from '../../../../utils/isValidCoordinateValue'; -import { ErroneousTransactionsRateChart } from '../erroneous_transactions_rate_chart/legacy'; import { TransactionBreakdown } from '../../TransactionBreakdown'; -import { - getResponseTimeTickFormatter, - getResponseTimeTooltipFormatter, -} from './helper'; +import { LineChart } from '../line_chart'; +import { TransactionErrorRateChart } from '../transaction_error_rate_chart/'; +import { getResponseTimeTickFormatter } from './helper'; import { MLHeader } from './ml_header'; -import { TransactionLineChart } from './TransactionLineChart'; import { useFormatter } from './use_formatter'; interface TransactionChartProps { charts: ITransactionChartData; urlParams: IUrlParams; + fetchStatus: FETCH_STATUS; } export function TransactionCharts({ charts, urlParams, + fetchStatus, }: TransactionChartProps) { const getTPMFormatter = (t: number) => { - const unit = tpmUnit(urlParams.transactionType); - return `${asDecimal(t)} ${unit}`; + return `${asDecimal(t)} ${tpmUnit(urlParams.transactionType)}`; }; - const getTPMTooltipFormatter = (p: Coordinate) => { - return isValidCoordinateValue(p.y) - ? getTPMFormatter(p.y) - : NOT_AVAILABLE_LABEL; + const getTPMTooltipFormatter = (y: Coordinate['y']) => { + return isValidCoordinateValue(y) ? getTPMFormatter(y) : NOT_AVAILABLE_LABEL; }; const { transactionType } = urlParams; const { responseTimeSeries, tpmSeries } = charts; - const { formatter, setDisabledSeriesState } = useFormatter( - responseTimeSeries - ); + const { formatter, toggleSerie } = useFormatter(responseTimeSeries); return ( <> - - - - - - - {responseTimeLabel(transactionType)} - - - - {(license) => ( - - )} - - - - - + + + + + + + + {responseTimeLabel(transactionType)} + + + + {(license) => ( + + )} + + + { + if (serie) { + toggleSerie(serie); + } + }} + /> + + - - - - {tpmLabel(transactionType)} - - - - - + + + + {tpmLabel(transactionType)} + + + + + - + - - - - - - - - + + + + + + + + + ); } diff --git a/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/use_formatter.test.tsx b/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/use_formatter.test.tsx index fc873cbda7bf2..958a5db6b66c9 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/use_formatter.test.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/use_formatter.test.tsx @@ -3,38 +3,17 @@ * 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 { SeriesIdentifier } from '@elastic/charts'; +import { renderHook } from '@testing-library/react-hooks'; +import { act } from 'react-test-renderer'; +import { toMicroseconds } from '../../../../../common/utils/formatters'; import { TimeSeries } from '../../../../../typings/timeseries'; import { useFormatter } from './use_formatter'; -import { render, fireEvent, act } from '@testing-library/react'; -import { toMicroseconds } from '../../../../../common/utils/formatters'; - -function MockComponent({ - timeSeries, - disabledSeries, - value, -}: { - timeSeries: TimeSeries[]; - disabledSeries: boolean[]; - value: number; -}) { - const { formatter, setDisabledSeriesState } = useFormatter(timeSeries); - - const onDisableSeries = () => { - setDisabledSeriesState(disabledSeries); - }; - - return ( -
- - {formatter(value).formatted} -
- ); -} describe('useFormatter', () => { const timeSeries = ([ { + title: 'avg', data: [ { x: 1, y: toMicroseconds(11, 'minutes') }, { x: 2, y: toMicroseconds(1, 'minutes') }, @@ -42,6 +21,7 @@ describe('useFormatter', () => { ], }, { + title: '95th percentile', data: [ { x: 1, y: toMicroseconds(120, 'seconds') }, { x: 2, y: toMicroseconds(1, 'minutes') }, @@ -49,6 +29,7 @@ describe('useFormatter', () => { ], }, { + title: '99th percentile', data: [ { x: 1, y: toMicroseconds(60, 'seconds') }, { x: 2, y: toMicroseconds(5, 'minutes') }, @@ -56,54 +37,47 @@ describe('useFormatter', () => { ], }, ] as unknown) as TimeSeries[]; + it('returns new formatter when disabled series state changes', () => { - const { getByText } = render( - - ); - expect(getByText('2.0 min')).toBeInTheDocument(); + const { result } = renderHook(() => useFormatter(timeSeries)); + expect( + result.current.formatter(toMicroseconds(120, 'seconds')).formatted + ).toEqual('2.0 min'); + act(() => { - fireEvent.click(getByText('disable series')); + result.current.toggleSerie({ + specId: 'avg', + } as SeriesIdentifier); }); - expect(getByText('120 s')).toBeInTheDocument(); + + expect( + result.current.formatter(toMicroseconds(120, 'seconds')).formatted + ).toEqual('120 s'); }); + it('falls back to the first formatter when disabled series is empty', () => { - const { getByText } = render( - - ); - expect(getByText('2.0 min')).toBeInTheDocument(); + const { result } = renderHook(() => useFormatter(timeSeries)); + expect( + result.current.formatter(toMicroseconds(120, 'seconds')).formatted + ).toEqual('2.0 min'); + act(() => { - fireEvent.click(getByText('disable series')); + result.current.toggleSerie({ + specId: 'avg', + } as SeriesIdentifier); }); - expect(getByText('2.0 min')).toBeInTheDocument(); - // const { formatter, setDisabledSeriesState } = useFormatter(timeSeries); - // expect(formatter(toMicroseconds(120, 'seconds'))).toEqual('2.0 min'); - // setDisabledSeriesState([true, true, false]); - // expect(formatter(toMicroseconds(120, 'seconds'))).toEqual('2.0 min'); - }); - it('falls back to the first formatter when disabled series is all true', () => { - const { getByText } = render( - - ); - expect(getByText('2.0 min')).toBeInTheDocument(); + + expect( + result.current.formatter(toMicroseconds(120, 'seconds')).formatted + ).toEqual('120 s'); + act(() => { - fireEvent.click(getByText('disable series')); + result.current.toggleSerie({ + specId: 'avg', + } as SeriesIdentifier); }); - expect(getByText('2.0 min')).toBeInTheDocument(); - // const { formatter, setDisabledSeriesState } = useFormatter(timeSeries); - // expect(formatter(toMicroseconds(120, 'seconds'))).toEqual('2.0 min'); - // setDisabledSeriesState([true, true, false]); - // expect(formatter(toMicroseconds(120, 'seconds'))).toEqual('2.0 min'); + expect( + result.current.formatter(toMicroseconds(120, 'seconds')).formatted + ).toEqual('2.0 min'); }); }); diff --git a/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/use_formatter.ts b/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/use_formatter.ts index d4694bc3caf1d..1475ec2934e95 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/use_formatter.ts +++ b/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/use_formatter.ts @@ -4,8 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { useState, Dispatch, SetStateAction } from 'react'; -import { isEmpty } from 'lodash'; +import { SeriesIdentifier } from '@elastic/charts'; +import { omit } from 'lodash'; +import { useState } from 'react'; import { getDurationFormatter, TimeFormatter, @@ -14,17 +15,36 @@ import { TimeSeries } from '../../../../../typings/timeseries'; import { getMaxY } from './helper'; export const useFormatter = ( - series: TimeSeries[] + series?: TimeSeries[] ): { formatter: TimeFormatter; - setDisabledSeriesState: Dispatch>; + toggleSerie: (disabledSerie: SeriesIdentifier) => void; } => { - const [disabledSeriesState, setDisabledSeriesState] = useState([]); - const visibleSeries = series.filter( - (serie, index) => disabledSeriesState[index] !== true + const [disabledSeries, setDisabledSeries] = useState< + Record + >({}); + + const visibleSeries = series?.filter( + (serie) => disabledSeries[serie.title] === undefined ); - const maxY = getMaxY(isEmpty(visibleSeries) ? series : visibleSeries); + + const maxY = getMaxY(visibleSeries || series || []); const formatter = getDurationFormatter(maxY); - return { formatter, setDisabledSeriesState }; + const toggleSerie = ({ specId }: SeriesIdentifier) => { + if (disabledSeries[specId] !== undefined) { + setDisabledSeries((prevState) => { + return omit(prevState, specId); + }); + } else { + setDisabledSeries((prevState) => { + return { ...prevState, [specId]: 0 }; + }); + } + }; + + return { + formatter, + toggleSerie, + }; }; diff --git a/x-pack/plugins/apm/public/components/shared/charts/annotations/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/annotations/index.tsx new file mode 100644 index 0000000000000..683c66b2a96fe --- /dev/null +++ b/x-pack/plugins/apm/public/components/shared/charts/annotations/index.tsx @@ -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. + */ + +import { + AnnotationDomainTypes, + LineAnnotation, + Position, +} from '@elastic/charts'; +import { EuiIcon } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import React from 'react'; +import { asAbsoluteDateTime } from '../../../../../common/utils/formatters'; +import { useTheme } from '../../../../hooks/useTheme'; +import { useAnnotations } from '../../../../hooks/use_annotations'; + +export function Annotations() { + const { annotations } = useAnnotations(); + const theme = useTheme(); + + if (!annotations.length) { + return null; + } + + const color = theme.eui.euiColorSecondary; + + return ( + ({ + dataValue: annotation['@timestamp'], + header: asAbsoluteDateTime(annotation['@timestamp']), + details: `${i18n.translate('xpack.apm.chart.annotation.version', { + defaultMessage: 'Version', + })} ${annotation.text}`, + }))} + style={{ line: { strokeWidth: 1, stroke: color, opacity: 1 } }} + marker={} + markerPosition={Position.Top} + /> + ); +} diff --git a/x-pack/plugins/apm/public/components/shared/charts/chart_container.test.tsx b/x-pack/plugins/apm/public/components/shared/charts/chart_container.test.tsx index 409cb69575ca9..c0e8f869ce647 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/chart_container.test.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/chart_container.test.tsx @@ -5,30 +5,97 @@ */ import { render } from '@testing-library/react'; import React from 'react'; +import { FETCH_STATUS } from '../../../hooks/useFetcher'; import { ChartContainer } from './chart_container'; describe('ChartContainer', () => { - describe('when isLoading is true', () => { - it('shows loading the indicator', () => { - const component = render( - + describe('loading indicator', () => { + it('shows loading when status equals to Loading or Pending and has no data', () => { + [FETCH_STATUS.PENDING, FETCH_STATUS.LOADING].map((status) => { + const { queryAllByTestId } = render( + +
My amazing component
+
+ ); + + expect(queryAllByTestId('loading')[0]).toBeInTheDocument(); + }); + }); + it('does not show loading when status equals to Loading or Pending and has data', () => { + [FETCH_STATUS.PENDING, FETCH_STATUS.LOADING].map((status) => { + const { queryAllByText } = render( + +
My amazing component
+
+ ); + expect(queryAllByText('My amazing component')[0]).toBeInTheDocument(); + }); + }); + }); + + describe('failure indicator', () => { + it('shows failure message when status equals to Failure and has data', () => { + const { getByText } = render( +
My amazing component
); - - expect(component.getByTestId('loading')).toBeInTheDocument(); + expect( + getByText( + 'An error happened when trying to fetch data. Please try again' + ) + ).toBeInTheDocument(); + }); + it('shows failure message when status equals to Failure and has no data', () => { + const { getByText } = render( + +
My amazing component
+
+ ); + expect( + getByText( + 'An error happened when trying to fetch data. Please try again' + ) + ).toBeInTheDocument(); }); }); - describe('when isLoading is false', () => { - it('does not show the loading indicator', () => { - const component = render( - + describe('render component', () => { + it('shows children component when status Success and has data', () => { + const { getByText } = render( +
My amazing component
); - - expect(component.queryByTestId('loading')).not.toBeInTheDocument(); + expect(getByText('My amazing component')).toBeInTheDocument(); + }); + it('shows children component when status Success and has no data', () => { + const { getByText } = render( + +
My amazing component
+
+ ); + expect(getByText('My amazing component')).toBeInTheDocument(); }); }); }); diff --git a/x-pack/plugins/apm/public/components/shared/charts/chart_container.tsx b/x-pack/plugins/apm/public/components/shared/charts/chart_container.tsx index a6f579308597f..b4486f1e9b94a 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/chart_container.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/chart_container.tsx @@ -3,27 +3,56 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { EuiLoadingChart } from '@elastic/eui'; + +import { EuiLoadingChart, EuiText } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; import React from 'react'; +import { FETCH_STATUS } from '../../../hooks/useFetcher'; interface Props { - isLoading: boolean; + hasData: boolean; + status: FETCH_STATUS; height: number; children: React.ReactNode; } -export function ChartContainer({ isLoading, children, height }: Props) { +export function ChartContainer({ children, height, status, hasData }: Props) { + if ( + !hasData && + (status === FETCH_STATUS.LOADING || status === FETCH_STATUS.PENDING) + ) { + return ; + } + + if (status === FETCH_STATUS.FAILURE) { + return ; + } + + return
{children}
; +} + +function LoadingChartPlaceholder({ height }: { height: number }) { return (
- {isLoading && } - {children} +
); } + +function FailedChartPlaceholder({ height }: { height: number }) { + return ( + + {i18n.translate('xpack.apm.chart.error', { + defaultMessage: + 'An error happened when trying to fetch data. Please try again', + })} + + ); +} diff --git a/x-pack/plugins/apm/public/components/shared/charts/erroneous_transactions_rate_chart/legacy.tsx b/x-pack/plugins/apm/public/components/shared/charts/erroneous_transactions_rate_chart/legacy.tsx deleted file mode 100644 index 29102f606414f..0000000000000 --- a/x-pack/plugins/apm/public/components/shared/charts/erroneous_transactions_rate_chart/legacy.tsx +++ /dev/null @@ -1,112 +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 { EuiPanel, EuiSpacer, EuiTitle } from '@elastic/eui'; -import theme from '@elastic/eui/dist/eui_theme_light.json'; -import { i18n } from '@kbn/i18n'; -import { max } from 'lodash'; -import React, { useCallback } from 'react'; -import { useParams } from 'react-router-dom'; -import { asPercent } from '../../../../../common/utils/formatters'; -import { useLegacyChartsSync as useChartsSync } from '../../../../hooks/use_charts_sync'; -import { useFetcher } from '../../../../hooks/useFetcher'; -import { useUrlParams } from '../../../../hooks/useUrlParams'; -import { callApmApi } from '../../../../services/rest/createCallApmApi'; -// @ts-expect-error -import CustomPlot from '../CustomPlot'; - -const tickFormatY = (y?: number | null) => { - return asPercent(y || 0, 1); -}; - -/** - * "Legacy" version of this chart using react-vis charts. See index.tsx for the - * Elastic Charts version. - * - * This will be removed with #70290. - */ -export function ErroneousTransactionsRateChart() { - const { serviceName } = useParams<{ serviceName?: string }>(); - const { urlParams, uiFilters } = useUrlParams(); - const syncedChartsProps = useChartsSync(); - - const { start, end, transactionType, transactionName } = urlParams; - - const { data } = useFetcher(() => { - if (serviceName && start && end) { - return callApmApi({ - pathname: - '/api/apm/services/{serviceName}/transaction_groups/error_rate', - params: { - path: { - serviceName, - }, - query: { - start, - end, - transactionType, - transactionName, - uiFilters: JSON.stringify(uiFilters), - }, - }, - }); - } - }, [serviceName, start, end, uiFilters, transactionType, transactionName]); - - const combinedOnHover = useCallback( - (hoverX: number) => { - return syncedChartsProps.onHover(hoverX); - }, - [syncedChartsProps] - ); - - const errorRates = data?.transactionErrorRate || []; - const maxRate = max(errorRates.map((errorRate) => errorRate.y)); - - return ( - - - - {i18n.translate('xpack.apm.errorRateChart.title', { - defaultMessage: 'Transaction error rate', - })} - - - - - Number.isFinite(y) ? tickFormatY(y) : 'N/A' - } - /> - - ); -} diff --git a/x-pack/plugins/apm/public/components/shared/charts/line_chart/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/line_chart/index.tsx index 3f2a08ecb7641..507acc49d89db 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/line_chart/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/line_chart/index.tsx @@ -20,15 +20,17 @@ import moment from 'moment'; import React, { useEffect } from 'react'; import { useHistory } from 'react-router-dom'; import { TimeSeries } from '../../../../../typings/timeseries'; +import { FETCH_STATUS } from '../../../../hooks/useFetcher'; import { useUrlParams } from '../../../../hooks/useUrlParams'; import { useChartsSync } from '../../../../hooks/use_charts_sync'; import { unit } from '../../../../style/variables'; +import { Annotations } from '../annotations'; import { ChartContainer } from '../chart_container'; import { onBrushEnd } from '../helper/helper'; interface Props { id: string; - isLoading: boolean; + fetchStatus: FETCH_STATUS; onToggleLegend?: LegendItemListener; timeseries: TimeSeries[]; /** @@ -38,18 +40,20 @@ interface Props { /** * Formatter for legend and tooltip values */ - yTickFormat: (y: number) => string; + yTickFormat?: (y: number) => string; + showAnnotations?: boolean; } const XY_HEIGHT = unit * 16; export function LineChart({ id, - isLoading, + fetchStatus, onToggleLegend, timeseries, yLabelFormat, yTickFormat, + showAnnotations = true, }: Props) { const history = useHistory(); const chartRef = React.createRef(); @@ -84,7 +88,7 @@ export function LineChart({ ); return ( - + onBrushEnd({ x, history })} @@ -115,11 +119,13 @@ export function LineChart({ id="y-axis" ticks={3} position={Position.Left} - tickFormat={yTickFormat} + tickFormat={yTickFormat ? yTickFormat : yLabelFormat} labelFormat={yLabelFormat} showGridLines /> + {showAnnotations && } + {timeseries.map((serie) => { return ( (); const { urlParams, uiFilters } = useUrlParams(); @@ -56,25 +61,32 @@ export function ErroneousTransactionsRateChart() { const errorRates = data?.transactionErrorRate || []; return ( - + + +

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

+
+ +
); } diff --git a/x-pack/plugins/apm/public/context/ApmPluginContext/MockApmPluginContext.tsx b/x-pack/plugins/apm/public/context/ApmPluginContext/MockApmPluginContext.tsx index 3b915045f54b6..25e7f23a00125 100644 --- a/x-pack/plugins/apm/public/context/ApmPluginContext/MockApmPluginContext.tsx +++ b/x-pack/plugins/apm/public/context/ApmPluginContext/MockApmPluginContext.tsx @@ -38,6 +38,7 @@ const mockCore = { application: { capabilities: { apm: {}, + ml: {}, }, currentAppId$: new Observable(), navigateToUrl: (url: string) => {}, @@ -93,7 +94,13 @@ const mockPlugin = { }, }, }; + +const mockAppMountParameters = { + setHeaderActionMenu: () => {}, +}; + export const mockApmPluginContextValue = { + appMountParameters: mockAppMountParameters, config: mockConfig, core: mockCore, plugins: mockPlugin, diff --git a/x-pack/plugins/apm/public/context/ApmPluginContext/index.tsx b/x-pack/plugins/apm/public/context/ApmPluginContext/index.tsx index 39d961f6a8164..44952e64db59c 100644 --- a/x-pack/plugins/apm/public/context/ApmPluginContext/index.tsx +++ b/x-pack/plugins/apm/public/context/ApmPluginContext/index.tsx @@ -4,12 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import { CoreStart } from 'kibana/public'; +import { AppMountParameters, CoreStart } from 'kibana/public'; import { createContext } from 'react'; import { ConfigSchema } from '../../'; import { ApmPluginSetupDeps } from '../../plugin'; export interface ApmPluginContextValue { + appMountParameters: AppMountParameters; config: ConfigSchema; core: CoreStart; plugins: ApmPluginSetupDeps; diff --git a/x-pack/plugins/apm/public/hooks/useTransactionBreakdown.ts b/x-pack/plugins/apm/public/hooks/useTransactionBreakdown.ts index 08d2300c3254a..0705383ecb0ca 100644 --- a/x-pack/plugins/apm/public/hooks/useTransactionBreakdown.ts +++ b/x-pack/plugins/apm/public/hooks/useTransactionBreakdown.ts @@ -15,7 +15,7 @@ export function useTransactionBreakdown() { uiFilters, } = useUrlParams(); - const { data = { timeseries: [] }, error, status } = useFetcher( + const { data = { timeseries: undefined }, error, status } = useFetcher( (callApmApi) => { if (serviceName && start && end && transactionType) { return callApmApi({ diff --git a/x-pack/plugins/apm/public/hooks/useTransactionDistribution.ts b/x-pack/plugins/apm/public/hooks/useTransactionDistribution.ts index a5096a314388c..8c76225d03486 100644 --- a/x-pack/plugins/apm/public/hooks/useTransactionDistribution.ts +++ b/x-pack/plugins/apm/public/hooks/useTransactionDistribution.ts @@ -10,7 +10,7 @@ import { IUrlParams } from '../context/UrlParamsContext/types'; import { useFetcher } from './useFetcher'; import { useUiFilters } from '../context/UrlParamsContext'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { TransactionDistributionAPIResponse } from '../../server/lib/transactions/distribution'; +import type { TransactionDistributionAPIResponse } from '../../server/lib/transactions/distribution'; import { toQuery, fromQuery } from '../components/shared/Links/url_helpers'; import { maybe } from '../../common/utils/maybe'; diff --git a/x-pack/plugins/apm/public/hooks/useTransactionList.ts b/x-pack/plugins/apm/public/hooks/useTransactionList.ts index 9c3a18b9c0d0d..b2c2cc30f78ec 100644 --- a/x-pack/plugins/apm/public/hooks/useTransactionList.ts +++ b/x-pack/plugins/apm/public/hooks/useTransactionList.ts @@ -14,8 +14,8 @@ type TransactionsAPIResponse = APIReturnType< '/api/apm/services/{serviceName}/transaction_groups' >; -const DEFAULT_RESPONSE: TransactionsAPIResponse = { - items: [], +const DEFAULT_RESPONSE: Partial = { + items: undefined, isAggregationAccurate: true, bucketSize: 0, }; diff --git a/x-pack/plugins/apm/public/hooks/use_annotations.ts b/x-pack/plugins/apm/public/hooks/use_annotations.ts new file mode 100644 index 0000000000000..2b1c2bec52b3d --- /dev/null +++ b/x-pack/plugins/apm/public/hooks/use_annotations.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 { useParams } from 'react-router-dom'; +import { callApmApi } from '../services/rest/createCallApmApi'; +import { useFetcher } from './useFetcher'; +import { useUrlParams } from './useUrlParams'; + +const INITIAL_STATE = { annotations: [] }; + +export function useAnnotations() { + const { serviceName } = useParams<{ serviceName?: string }>(); + const { urlParams, uiFilters } = useUrlParams(); + const { start, end } = urlParams; + const { environment } = uiFilters; + + const { data = INITIAL_STATE } = useFetcher(() => { + if (start && end && serviceName) { + return callApmApi({ + pathname: '/api/apm/services/{serviceName}/annotation/search', + params: { + path: { + serviceName, + }, + query: { + start, + end, + environment, + }, + }, + }); + } + }, [start, end, environment, serviceName]); + + return data; +} diff --git a/x-pack/plugins/apm/public/selectors/chartSelectors.ts b/x-pack/plugins/apm/public/selectors/chartSelectors.ts index 8c6093859f969..450f02f70c6a4 100644 --- a/x-pack/plugins/apm/public/selectors/chartSelectors.ts +++ b/x-pack/plugins/apm/public/selectors/chartSelectors.ts @@ -31,40 +31,37 @@ export interface ITpmBucket { } export interface ITransactionChartData { - tpmSeries: ITpmBucket[]; - responseTimeSeries: TimeSeries[]; + tpmSeries?: ITpmBucket[]; + responseTimeSeries?: TimeSeries[]; mlJobId: string | undefined; } -const INITIAL_DATA = { - apmTimeseries: { - responseTimes: { - avg: [], - p95: [], - p99: [], - }, - tpmBuckets: [], - overallAvgDuration: null, - }, +const INITIAL_DATA: Partial = { + apmTimeseries: undefined, anomalyTimeseries: undefined, }; export function getTransactionCharts( { transactionType }: IUrlParams, - { apmTimeseries, anomalyTimeseries }: TimeSeriesAPIResponse = INITIAL_DATA + charts = INITIAL_DATA ): ITransactionChartData { - const tpmSeries = getTpmSeries(apmTimeseries, transactionType); - - const responseTimeSeries = getResponseTimeSeries({ - apmTimeseries, - anomalyTimeseries, - }); + const { apmTimeseries, anomalyTimeseries } = charts; - return { - tpmSeries, - responseTimeSeries, + const transactionCharts: ITransactionChartData = { + tpmSeries: undefined, + responseTimeSeries: undefined, mlJobId: anomalyTimeseries?.jobId, }; + + if (apmTimeseries) { + transactionCharts.tpmSeries = getTpmSeries(apmTimeseries, transactionType); + + transactionCharts.responseTimeSeries = getResponseTimeSeries({ + apmTimeseries, + anomalyTimeseries, + }); + } + return transactionCharts; } export function getResponseTimeSeries({ diff --git a/x-pack/plugins/apm/server/lib/errors/distribution/get_buckets.ts b/x-pack/plugins/apm/server/lib/errors/distribution/get_buckets.ts index a42710947a792..b12dd73a20986 100644 --- a/x-pack/plugins/apm/server/lib/errors/distribution/get_buckets.ts +++ b/x-pack/plugins/apm/server/lib/errors/distribution/get_buckets.ts @@ -73,6 +73,6 @@ export async function getBuckets({ return { noHits: resp.hits.total.value === 0, - buckets, + buckets: resp.hits.total.value > 0 ? buckets : [], }; } diff --git a/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/index.ts b/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/index.ts index 2bfd3c94ed34c..d23d729db3924 100644 --- a/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/index.ts +++ b/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/index.ts @@ -5,20 +5,22 @@ */ import { ValuesType } from 'utility-types'; -import { APMBaseDoc } from '../../../../../typings/es_schemas/raw/apm_base_doc'; import { APMError } from '../../../../../typings/es_schemas/ui/apm_error'; -import { KibanaRequest } from '../../../../../../../../src/core/server'; +import { + KibanaRequest, + LegacyScopedClusterClient, +} from '../../../../../../../../src/core/server'; import { ProcessorEvent } from '../../../../../common/processor_event'; import { ESSearchRequest, ESSearchResponse, } from '../../../../../typings/elasticsearch'; import { ApmIndicesConfig } from '../../../settings/apm_indices/get_apm_indices'; -import { APMRequestHandlerContext } from '../../../../routes/typings'; import { addFilterToExcludeLegacyData } from './add_filter_to_exclude_legacy_data'; import { callClientWithDebug } from '../call_client_with_debug'; import { Transaction } from '../../../../../typings/es_schemas/ui/transaction'; import { Span } from '../../../../../typings/es_schemas/ui/span'; +import { Metric } from '../../../../../typings/es_schemas/ui/metric'; import { unpackProcessorEvents } from './unpack_processor_events'; export type APMEventESSearchRequest = Omit & { @@ -31,7 +33,7 @@ type TypeOfProcessorEvent = { [ProcessorEvent.error]: APMError; [ProcessorEvent.transaction]: Transaction; [ProcessorEvent.span]: Span; - [ProcessorEvent.metric]: APMBaseDoc; + [ProcessorEvent.metric]: Metric; [ProcessorEvent.onboarding]: unknown; [ProcessorEvent.sourcemap]: unknown; }[T]; @@ -51,20 +53,23 @@ type TypedSearchResponse< export type APMEventClient = ReturnType; export function createApmEventClient({ - context, + esClient, + debug, request, indices, options: { includeFrozen } = { includeFrozen: false }, }: { - context: APMRequestHandlerContext; + esClient: Pick< + LegacyScopedClusterClient, + 'callAsInternalUser' | 'callAsCurrentUser' + >; + debug: boolean; request: KibanaRequest; indices: ApmIndicesConfig; options: { includeFrozen: boolean; }; }) { - const client = context.core.elasticsearch.legacy.client; - return { search( params: TParams, @@ -77,14 +82,14 @@ export function createApmEventClient({ : withProcessorEventFilter; return callClientWithDebug({ - apiCaller: client.callAsCurrentUser, + apiCaller: esClient.callAsCurrentUser, operationName: 'search', params: { ...withPossibleLegacyDataFilter, ignore_throttled: !includeFrozen, }, request, - debug: context.params.query._debug, + debug, }); }, }; diff --git a/x-pack/plugins/apm/server/lib/helpers/setup_request.ts b/x-pack/plugins/apm/server/lib/helpers/setup_request.ts index 5e75535c678b3..363c4128137e0 100644 --- a/x-pack/plugins/apm/server/lib/helpers/setup_request.ts +++ b/x-pack/plugins/apm/server/lib/helpers/setup_request.ts @@ -88,7 +88,8 @@ export async function setupRequest( const coreSetupRequest = { indices, apmEventClient: createApmEventClient({ - context, + esClient: context.core.elasticsearch.legacy.client, + debug: context.params.query._debug, request, indices, options: { includeFrozen }, diff --git a/x-pack/plugins/apm/server/plugin.ts b/x-pack/plugins/apm/server/plugin.ts index d3341b6c1b163..44269b1775953 100644 --- a/x-pack/plugins/apm/server/plugin.ts +++ b/x-pack/plugins/apm/server/plugin.ts @@ -10,14 +10,17 @@ import { map, take } from 'rxjs/operators'; import { CoreSetup, CoreStart, + KibanaRequest, Logger, Plugin, PluginInitializerContext, + RequestHandlerContext, } from 'src/core/server'; import { APMConfig, APMXPackConfig, mergeConfigs } from '.'; import { APMOSSPluginSetup } from '../../../../src/plugins/apm_oss/server'; import { HomeServerPluginSetup } from '../../../../src/plugins/home/server'; import { UsageCollectionSetup } from '../../../../src/plugins/usage_collection/server'; +import { UI_SETTINGS } from '../../../../src/plugins/data/common'; import { ActionsPlugin } from '../../actions/server'; import { AlertingPlugin } from '../../alerts/server'; import { CloudSetup } from '../../cloud/server'; @@ -30,6 +33,7 @@ import { TaskManagerSetupContract } from '../../task_manager/server'; import { APM_FEATURE, registerFeaturesUsage } from './feature'; import { registerApmAlerts } from './lib/alerts/register_apm_alerts'; import { createApmTelemetry } from './lib/apm_telemetry'; +import { createApmEventClient } from './lib/helpers/create_es_client/create_apm_event_client'; import { getInternalSavedObjectsClient } from './lib/helpers/get_internal_saved_objects_client'; import { createApmAgentConfigurationIndex } from './lib/settings/agent_configuration/create_agent_config_index'; import { getApmIndices } from './lib/settings/apm_indices/get_apm_indices'; @@ -42,6 +46,11 @@ import { uiSettings } from './ui_settings'; export interface APMPluginSetup { config$: Observable; getApmIndices: () => ReturnType; + createApmEventClient: (params: { + debug?: boolean; + request: KibanaRequest; + context: RequestHandlerContext; + }) => Promise>; } export class APMPlugin implements Plugin { @@ -141,13 +150,41 @@ export class APMPlugin implements Plugin { }, }); + const boundGetApmIndices = async () => + getApmIndices({ + savedObjectsClient: await getInternalSavedObjectsClient(core), + config: await mergedConfig$.pipe(take(1)).toPromise(), + }); + return { config$: mergedConfig$, - getApmIndices: async () => - getApmIndices({ - savedObjectsClient: await getInternalSavedObjectsClient(core), - config: await mergedConfig$.pipe(take(1)).toPromise(), - }), + getApmIndices: boundGetApmIndices, + createApmEventClient: async ({ + request, + context, + debug, + }: { + debug?: boolean; + request: KibanaRequest; + context: RequestHandlerContext; + }) => { + const [indices, includeFrozen] = await Promise.all([ + boundGetApmIndices(), + context.core.uiSettings.client.get(UI_SETTINGS.SEARCH_INCLUDE_FROZEN), + ]); + + const esClient = context.core.elasticsearch.legacy.client; + + return createApmEventClient({ + debug: debug ?? false, + esClient, + request, + indices, + options: { + includeFrozen, + }, + }); + }, }; } diff --git a/x-pack/plugins/apm/typings/es_schemas/raw/apm_base_doc.ts b/x-pack/plugins/apm/typings/es_schemas/raw/apm_base_doc.ts index 19bd1129677dd..91335b39dab0f 100644 --- a/x-pack/plugins/apm/typings/es_schemas/raw/apm_base_doc.ts +++ b/x-pack/plugins/apm/typings/es_schemas/raw/apm_base_doc.ts @@ -4,6 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +import { Observer } from './fields/observer'; + // all documents types extend APMBaseDoc and inherit all properties export interface APMBaseDoc { '@timestamp': string; @@ -11,10 +13,10 @@ export interface APMBaseDoc { name: string; version: string; }; - timestamp: { us: number }; parent?: { id: string }; // parent ID is not available on root transactions trace?: { id: string }; labels?: { [key: string]: string | number | boolean; }; + observer?: Observer; } diff --git a/x-pack/plugins/apm/typings/es_schemas/raw/error_raw.ts b/x-pack/plugins/apm/typings/es_schemas/raw/error_raw.ts index b8eb79aabdcc5..6b366090931cd 100644 --- a/x-pack/plugins/apm/typings/es_schemas/raw/error_raw.ts +++ b/x-pack/plugins/apm/typings/es_schemas/raw/error_raw.ts @@ -13,9 +13,9 @@ import { Page } from './fields/page'; import { Process } from './fields/process'; import { Service } from './fields/service'; import { Stackframe } from './fields/stackframe'; +import { TimestampUs } from './fields/timestamp_us'; import { Url } from './fields/url'; import { User } from './fields/user'; -import { Observer } from './fields/observer'; interface Processor { name: 'error'; @@ -41,6 +41,7 @@ interface Log { export interface ErrorRaw extends APMBaseDoc { processor: Processor; + timestamp: TimestampUs; transaction?: { id: string; sampled?: boolean; @@ -66,5 +67,4 @@ export interface ErrorRaw extends APMBaseDoc { service: Service; url?: Url; user?: User; - observer?: Observer; } diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/types.ts b/x-pack/plugins/apm/typings/es_schemas/raw/fields/timestamp_us.ts similarity index 82% rename from x-pack/plugins/enterprise_search/public/applications/shared/types.ts rename to x-pack/plugins/apm/typings/es_schemas/raw/fields/timestamp_us.ts index 3fd1dcad0066e..20b35295e9ba5 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/types.ts +++ b/x-pack/plugins/apm/typings/es_schemas/raw/fields/timestamp_us.ts @@ -4,4 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -export { IFlashMessage } from './flash_messages'; +export interface TimestampUs { + us: number; +} diff --git a/x-pack/plugins/apm/typings/es_schemas/raw/metric_raw.ts b/x-pack/plugins/apm/typings/es_schemas/raw/metric_raw.ts new file mode 100644 index 0000000000000..b4a1954783db0 --- /dev/null +++ b/x-pack/plugins/apm/typings/es_schemas/raw/metric_raw.ts @@ -0,0 +1,99 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { APMBaseDoc } from './apm_base_doc'; +import { Container } from './fields/container'; +import { Kubernetes } from './fields/kubernetes'; + +type BaseMetric = APMBaseDoc & { + processor: { + name: 'metric'; + event: 'metric'; + }; +}; + +type BaseBreakdownMetric = BaseMetric & { + transaction: { + name: string; + type: string; + }; + span: { + self_time: { + count: number; + sum: { + us: number; + }; + }; + }; +}; + +type TransactionBreakdownMetric = BaseBreakdownMetric & { + transaction: { + duration: { + count: number; + sum: { + us: number; + }; + }; + breakdown: { + count: number; + }; + }; +}; + +type SpanBreakdownMetric = BaseBreakdownMetric & { + span: { + type: string; + subtype?: string; + }; +}; + +type SystemMetric = BaseMetric & { + system: unknown; + service: { + node?: { + name: string; + }; + }; +}; + +type CGroupMetric = SystemMetric; +type JVMMetric = SystemMetric & { + jvm: unknown; +}; + +type TransactionDurationMetric = BaseMetric & { + transaction: { + name: string; + type: string; + result?: string; + duration: { + histogram: { + values: number[]; + counts: number[]; + }; + }; + }; + service: { + name: string; + node?: { + name: string; + }; + environment?: string; + version?: string; + }; + container?: Container; + kubernetes?: Kubernetes; +}; + +export type MetricRaw = + | BaseMetric + | TransactionBreakdownMetric + | SpanBreakdownMetric + | TransactionDurationMetric + | SystemMetric + | CGroupMetric + | JVMMetric; diff --git a/x-pack/plugins/apm/typings/es_schemas/raw/span_raw.ts b/x-pack/plugins/apm/typings/es_schemas/raw/span_raw.ts index 5c2e391059783..dcb3dc02f6519 100644 --- a/x-pack/plugins/apm/typings/es_schemas/raw/span_raw.ts +++ b/x-pack/plugins/apm/typings/es_schemas/raw/span_raw.ts @@ -6,7 +6,7 @@ import { APMBaseDoc } from './apm_base_doc'; import { Stackframe } from './fields/stackframe'; -import { Observer } from './fields/observer'; +import { TimestampUs } from './fields/timestamp_us'; interface Processor { name: 'transaction'; @@ -48,9 +48,9 @@ export interface SpanRaw extends APMBaseDoc { headers?: Record; }; }; + timestamp: TimestampUs; transaction?: { id: string; }; - observer?: Observer; child?: { id: string[] }; } diff --git a/x-pack/plugins/apm/typings/es_schemas/raw/transaction_raw.ts b/x-pack/plugins/apm/typings/es_schemas/raw/transaction_raw.ts index cdfe4183c96f5..68db3bca94641 100644 --- a/x-pack/plugins/apm/typings/es_schemas/raw/transaction_raw.ts +++ b/x-pack/plugins/apm/typings/es_schemas/raw/transaction_raw.ts @@ -12,10 +12,10 @@ import { Kubernetes } from './fields/kubernetes'; import { Page } from './fields/page'; import { Process } from './fields/process'; import { Service } from './fields/service'; +import { TimestampUs } from './fields/timestamp_us'; import { Url } from './fields/url'; import { User } from './fields/user'; import { UserAgent } from './fields/user_agent'; -import { Observer } from './fields/observer'; interface Processor { name: 'transaction'; @@ -24,6 +24,7 @@ interface Processor { export interface TransactionRaw extends APMBaseDoc { processor: Processor; + timestamp: TimestampUs; trace: { id: string }; // trace is required transaction: { duration: { us: number }; @@ -63,5 +64,4 @@ export interface TransactionRaw extends APMBaseDoc { url?: Url; user?: User; user_agent?: UserAgent; - observer?: Observer; } diff --git a/x-pack/plugins/maps/public/routing/store_operations.ts b/x-pack/plugins/apm/typings/es_schemas/ui/metric.ts similarity index 66% rename from x-pack/plugins/maps/public/routing/store_operations.ts rename to x-pack/plugins/apm/typings/es_schemas/ui/metric.ts index 53ebbb3328ff9..eefb0923f16da 100644 --- a/x-pack/plugins/maps/public/routing/store_operations.ts +++ b/x-pack/plugins/apm/typings/es_schemas/ui/metric.ts @@ -4,8 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { createMapStore } from '../reducers/store'; +import { MetricRaw } from '../raw/metric_raw'; -const store = createMapStore(); - -export const getStore = () => store; +export type Metric = MetricRaw; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_map.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_map.ts index a64ff7da2aa19..7bb6e43b38d59 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_map.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_map.ts @@ -83,6 +83,7 @@ export function savedMap(): ExpressionFunctionDefinition< return { type: EmbeddableExpressionType, input: { + attributes: { title: '' }, id: args.id, filters: getQueryFilters(filters), timeRange: args.timerange || defaultTimeRange, diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/input_type_to_expression/map.test.ts b/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/input_type_to_expression/map.test.ts index d2c803a1ff208..635e0ec2d0dcb 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/input_type_to_expression/map.test.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/input_type_to_expression/map.test.ts @@ -9,6 +9,7 @@ import { MapEmbeddableInput } from '../../../../../../plugins/maps/public/embedd import { fromExpression, Ast } from '@kbn/interpreter/common'; const baseSavedMapInput = { + attributes: { title: '' }, id: 'embeddableId', filters: [], isLayerTOCOpen: false, diff --git a/x-pack/plugins/canvas/types/state.ts b/x-pack/plugins/canvas/types/state.ts index 60407b78ab5e3..03bb931dc9b26 100644 --- a/x-pack/plugins/canvas/types/state.ts +++ b/x-pack/plugins/canvas/types/state.ts @@ -4,12 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ +import { KibanaContext } from 'src/plugins/data/common'; import { Datatable, ExpressionValueFilter, ExpressionImage, ExpressionFunction, - KibanaContext, PointSeries, Render, Style, diff --git a/x-pack/plugins/cloud/kibana.json b/x-pack/plugins/cloud/kibana.json index 27b35bcbdd88b..9bca2f30bd23c 100644 --- a/x-pack/plugins/cloud/kibana.json +++ b/x-pack/plugins/cloud/kibana.json @@ -3,7 +3,7 @@ "version": "8.0.0", "kibanaVersion": "kibana", "configPath": ["xpack", "cloud"], - "optionalPlugins": ["usageCollection", "home"], + "optionalPlugins": ["usageCollection", "home", "security"], "server": true, "ui": true } diff --git a/x-pack/plugins/cloud/public/index.ts b/x-pack/plugins/cloud/public/index.ts index 39ef5f452c18b..680b2f1ad2bd6 100644 --- a/x-pack/plugins/cloud/public/index.ts +++ b/x-pack/plugins/cloud/public/index.ts @@ -7,7 +7,7 @@ import { PluginInitializerContext } from '../../../../src/core/public'; import { CloudPlugin } from './plugin'; -export { CloudSetup } from './plugin'; +export { CloudSetup, CloudConfigType } from './plugin'; export function plugin(initializerContext: PluginInitializerContext) { return new CloudPlugin(initializerContext); } diff --git a/x-pack/plugins/cloud/public/mocks.ts b/x-pack/plugins/cloud/public/mocks.ts new file mode 100644 index 0000000000000..bafebbca4ecdd --- /dev/null +++ b/x-pack/plugins/cloud/public/mocks.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +function createSetupMock() { + return { + cloudId: 'mock-cloud-id', + isCloudEnabled: true, + resetPasswordUrl: 'reset-password-url', + accountUrl: 'account-url', + }; +} + +export const cloudMock = { + createSetup: createSetupMock, +}; diff --git a/x-pack/plugins/cloud/public/plugin.ts b/x-pack/plugins/cloud/public/plugin.ts index 45005f3f5e422..bc410b89c30e7 100644 --- a/x-pack/plugins/cloud/public/plugin.ts +++ b/x-pack/plugins/cloud/public/plugin.ts @@ -6,40 +6,51 @@ import { CoreSetup, CoreStart, Plugin, PluginInitializerContext } from 'src/core/public'; import { i18n } from '@kbn/i18n'; +import { SecurityPluginStart } from '../../security/public'; import { getIsCloudEnabled } from '../common/is_cloud_enabled'; import { ELASTIC_SUPPORT_LINK } from '../common/constants'; import { HomePublicPluginSetup } from '../../../../src/plugins/home/public'; +import { createUserMenuLinks } from './user_menu_links'; -interface CloudConfigType { +export interface CloudConfigType { id?: string; resetPasswordUrl?: string; deploymentUrl?: string; + accountUrl?: string; } interface CloudSetupDependencies { home?: HomePublicPluginSetup; } +interface CloudStartDependencies { + security?: SecurityPluginStart; +} + export interface CloudSetup { cloudId?: string; cloudDeploymentUrl?: string; isCloudEnabled: boolean; + resetPasswordUrl?: string; + accountUrl?: string; } export class CloudPlugin implements Plugin { private config!: CloudConfigType; + private isCloudEnabled: boolean; constructor(private readonly initializerContext: PluginInitializerContext) { this.config = this.initializerContext.config.get(); + this.isCloudEnabled = false; } public async setup(core: CoreSetup, { home }: CloudSetupDependencies) { const { id, resetPasswordUrl, deploymentUrl } = this.config; - const isCloudEnabled = getIsCloudEnabled(id); + this.isCloudEnabled = getIsCloudEnabled(id); if (home) { - home.environment.update({ cloud: isCloudEnabled }); - if (isCloudEnabled) { + home.environment.update({ cloud: this.isCloudEnabled }); + if (this.isCloudEnabled) { home.tutorials.setVariable('cloud', { id, resetPasswordUrl }); } } @@ -47,11 +58,11 @@ export class CloudPlugin implements Plugin { return { cloudId: id, cloudDeploymentUrl: deploymentUrl, - isCloudEnabled, + isCloudEnabled: this.isCloudEnabled, }; } - public start(coreStart: CoreStart) { + public start(coreStart: CoreStart, { security }: CloudStartDependencies) { const { deploymentUrl } = this.config; coreStart.chrome.setHelpSupportUrl(ELASTIC_SUPPORT_LINK); if (deploymentUrl) { @@ -63,5 +74,10 @@ export class CloudPlugin implements Plugin { href: deploymentUrl, }); } + + if (security && this.isCloudEnabled) { + const userMenuLinks = createUserMenuLinks(this.config); + security.navControlService.addUserMenuLinks(userMenuLinks); + } } } diff --git a/x-pack/plugins/cloud/public/user_menu_links.ts b/x-pack/plugins/cloud/public/user_menu_links.ts new file mode 100644 index 0000000000000..15e2f14e885ba --- /dev/null +++ b/x-pack/plugins/cloud/public/user_menu_links.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 { i18n } from '@kbn/i18n'; +import { UserMenuLink } from '../../security/public'; +import { CloudConfigType } from '.'; + +export const createUserMenuLinks = (config: CloudConfigType): UserMenuLink[] => { + const { resetPasswordUrl, accountUrl } = config; + const userMenuLinks = [] as UserMenuLink[]; + + if (resetPasswordUrl) { + userMenuLinks.push({ + label: i18n.translate('xpack.cloud.userMenuLinks.profileLinkText', { + defaultMessage: 'Cloud profile', + }), + iconType: 'logoCloud', + href: resetPasswordUrl, + order: 100, + }); + } + + if (accountUrl) { + userMenuLinks.push({ + label: i18n.translate('xpack.cloud.userMenuLinks.accountLinkText', { + defaultMessage: 'Account & Billing', + }), + iconType: 'gear', + href: accountUrl, + order: 200, + }); + } + + return userMenuLinks; +}; diff --git a/x-pack/plugins/cloud/server/config.ts b/x-pack/plugins/cloud/server/config.ts index ff8a2c5acdf9a..eaa4ab7a482dd 100644 --- a/x-pack/plugins/cloud/server/config.ts +++ b/x-pack/plugins/cloud/server/config.ts @@ -23,6 +23,7 @@ const configSchema = schema.object({ apm: schema.maybe(apmConfigSchema), resetPasswordUrl: schema.maybe(schema.string()), deploymentUrl: schema.maybe(schema.string()), + accountUrl: schema.maybe(schema.string()), }); export type CloudConfigType = TypeOf; @@ -32,6 +33,7 @@ export const config: PluginConfigDescriptor = { id: true, resetPasswordUrl: true, deploymentUrl: true, + accountUrl: true, }, schema: configSchema, }; diff --git a/x-pack/plugins/data_enhanced/common/search/es_search/es_search_rxjs_utils.ts b/x-pack/plugins/data_enhanced/common/search/es_search/es_search_rxjs_utils.ts index 2d5d09fd0205e..f38bec6d1f59f 100644 --- a/x-pack/plugins/data_enhanced/common/search/es_search/es_search_rxjs_utils.ts +++ b/x-pack/plugins/data_enhanced/common/search/es_search/es_search_rxjs_utils.ts @@ -8,11 +8,11 @@ import { of, merge, timer, throwError } from 'rxjs'; import { takeWhile, switchMap, expand, mergeMap, tap } from 'rxjs/operators'; import { - AbortError, doSearch, IKibanaSearchResponse, isErrorResponse, } from '../../../../../../src/plugins/data/common'; +import { AbortError } from '../../../../../../src/plugins/kibana_utils/common'; import type { IKibanaSearchRequest } from '../../../../../../src/plugins/data/common'; import type { IAsyncSearchOptions } from '../../../common/search/types'; diff --git a/x-pack/plugins/data_enhanced/public/autocomplete/providers/kql_query_suggestion/value.ts b/x-pack/plugins/data_enhanced/public/autocomplete/providers/kql_query_suggestion/value.ts index 441e5a6f775dd..cab3a657b7b6b 100644 --- a/x-pack/plugins/data_enhanced/public/autocomplete/providers/kql_query_suggestion/value.ts +++ b/x-pack/plugins/data_enhanced/public/autocomplete/providers/kql_query_suggestion/value.ts @@ -27,7 +27,7 @@ const wrapAsSuggestions = (start: number, end: number, query: string, values: st export const setupGetValueSuggestions: KqlQuerySuggestionProvider = () => { return async ( - { indexPatterns, boolFilter, signal }, + { indexPatterns, boolFilter, useTimeRange, signal }, { start, end, prefix, suffix, fieldName, nestedPath } ): Promise => { const fullFieldName = nestedPath ? `${nestedPath}.${fieldName}` : fieldName; @@ -49,6 +49,7 @@ export const setupGetValueSuggestions: KqlQuerySuggestionProvider = () => { field, query, boolFilter, + useTimeRange, signal, }).then((valueSuggestions) => { const quotedValues = valueSuggestions.map((value) => diff --git a/x-pack/plugins/data_enhanced/public/search/search_interceptor.test.ts b/x-pack/plugins/data_enhanced/public/search/search_interceptor.test.ts index f47d2b39a89a9..044489d58eb0e 100644 --- a/x-pack/plugins/data_enhanced/public/search/search_interceptor.test.ts +++ b/x-pack/plugins/data_enhanced/public/search/search_interceptor.test.ts @@ -7,7 +7,8 @@ import type { MockedKeys } from '@kbn/utility-types/jest'; import { coreMock } from '../../../../../src/core/public/mocks'; import { EnhancedSearchInterceptor } from './search_interceptor'; import { CoreSetup, CoreStart } from 'kibana/public'; -import { AbortError, UI_SETTINGS } from '../../../../../src/plugins/data/common'; +import { UI_SETTINGS } from '../../../../../src/plugins/data/common'; +import { AbortError } from '../../../../../src/plugins/kibana_utils/public'; import { SearchTimeoutError } from 'src/plugins/data/public'; import { dataPluginMock } from '../../../../../src/plugins/data/public/mocks'; 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 4cafcdb29ae8d..d36741ae6576d 100644 --- a/x-pack/plugins/data_enhanced/public/search/search_interceptor.ts +++ b/x-pack/plugins/data_enhanced/public/search/search_interceptor.ts @@ -13,7 +13,7 @@ import { SearchInterceptorDeps, UI_SETTINGS, } from '../../../../../src/plugins/data/public'; -import { AbortError, toPromise } from '../../../../../src/plugins/data/common'; +import { AbortError, abortSignalToPromise } from '../../../../../src/plugins/kibana_utils/public'; import { IAsyncSearchRequest, @@ -70,7 +70,7 @@ export class EnhancedSearchInterceptor extends SearchInterceptor { abortSignal: options.abortSignal, timeout: this.searchTimeout, }); - const abortedPromise = toPromise(combinedSignal); + const abortedPromise = abortSignalToPromise(combinedSignal); const strategy = options?.strategy ?? ENHANCED_ES_SEARCH_STRATEGY; this.pendingCount$.next(this.pendingCount$.getValue() + 1); diff --git a/x-pack/plugins/enterprise_search/common/types/app_search.ts b/x-pack/plugins/enterprise_search/common/types/app_search.ts index 203b77834bc15..9f754412ec730 100644 --- a/x-pack/plugins/enterprise_search/common/types/app_search.ts +++ b/x-pack/plugins/enterprise_search/common/types/app_search.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -export interface IAccount { +export interface Account { accountId: string; onboardingComplete: boolean; role: { @@ -21,7 +21,7 @@ export interface IAccount { }; } -export interface IConfiguredLimits { +export interface ConfiguredLimits { engine: { maxDocumentByteSize: number; maxEnginesPerMetaEngine: number; diff --git a/x-pack/plugins/enterprise_search/common/types/index.ts b/x-pack/plugins/enterprise_search/common/types/index.ts index 1006d39138759..39d9aa8607bc2 100644 --- a/x-pack/plugins/enterprise_search/common/types/index.ts +++ b/x-pack/plugins/enterprise_search/common/types/index.ts @@ -5,39 +5,39 @@ */ import { - IAccount as IAppSearchAccount, - IConfiguredLimits as IAppSearchConfiguredLimits, + Account as AppSearchAccount, + ConfiguredLimits as AppSearchConfiguredLimits, } from './app_search'; import { - IWorkplaceSearchInitialData, - IConfiguredLimits as IWorkplaceSearchConfiguredLimits, + WorkplaceSearchInitialData, + ConfiguredLimits as WorkplaceSearchConfiguredLimits, } from './workplace_search'; -export interface IInitialAppData { +export interface InitialAppData { readOnlyMode?: boolean; ilmEnabled?: boolean; isFederatedAuth?: boolean; - configuredLimits?: IConfiguredLimits; + configuredLimits?: ConfiguredLimits; access?: { hasAppSearchAccess: boolean; hasWorkplaceSearchAccess: boolean; }; - appSearch?: IAppSearchAccount; - workplaceSearch?: IWorkplaceSearchInitialData; + appSearch?: AppSearchAccount; + workplaceSearch?: WorkplaceSearchInitialData; } -export interface IConfiguredLimits { - appSearch: IAppSearchConfiguredLimits; - workplaceSearch: IWorkplaceSearchConfiguredLimits; +export interface ConfiguredLimits { + appSearch: AppSearchConfiguredLimits; + workplaceSearch: WorkplaceSearchConfiguredLimits; } -export interface IMetaPage { +export interface MetaPage { current: number; size: number; total_pages: number; total_results: number; } -export interface IMeta { - page: IMetaPage; +export interface Meta { + page: MetaPage; } diff --git a/x-pack/plugins/enterprise_search/common/types/workplace_search.ts b/x-pack/plugins/enterprise_search/common/types/workplace_search.ts index 886597fcd9891..883cc6939b4bc 100644 --- a/x-pack/plugins/enterprise_search/common/types/workplace_search.ts +++ b/x-pack/plugins/enterprise_search/common/types/workplace_search.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -export interface IAccount { +export interface Account { id: string; groups: string[]; isAdmin: boolean; @@ -14,65 +14,19 @@ export interface IAccount { viewedOnboardingPage: boolean; } -export interface IOrganization { +export interface Organization { name: string; defaultOrgName: string; } -export interface IWorkplaceSearchInitialData { - organization: IOrganization; - account: IAccount; +export interface WorkplaceSearchInitialData { + organization: Organization; + account: Account; } -export interface IConfiguredLimits { +export interface ConfiguredLimits { customApiSource: { maxDocumentByteSize: number; totalFields: number; }; } - -export interface IGroup { - id: string; - name: string; - createdAt: string; - updatedAt: string; - contentSources: IContentSource[]; - users: IUser[]; - usersCount: number; - color?: string; -} - -export interface IGroupDetails extends IGroup { - contentSources: IContentSourceDetails[]; - canEditGroup: boolean; - canDeleteGroup: boolean; -} - -export interface IUser { - id: string; - name: string | null; - initials: string; - pictureUrl: string | null; - color: string; - email: string; - role?: string; - groupIds: string[]; -} - -export interface IContentSource { - id: string; - serviceType: string; - name: string; -} - -export interface IContentSourceDetails extends IContentSource { - status: string; - statusMessage: string; - documentCount: string; - isFederatedSource: boolean; - searchable: boolean; - supportedByLicense: boolean; - errorReason: number; - allowsReauth: boolean; - boost: number; -} diff --git a/x-pack/plugins/enterprise_search/public/applications/__mocks__/mount_async.mock.tsx b/x-pack/plugins/enterprise_search/public/applications/__mocks__/mount_async.mock.tsx index a33e116c7ca72..a3d817e1da904 100644 --- a/x-pack/plugins/enterprise_search/public/applications/__mocks__/mount_async.mock.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/__mocks__/mount_async.mock.tsx @@ -20,13 +20,13 @@ import { mountWithIntl } from './'; * const wrapper = mountAsync(); */ -interface IOptions { +interface Options { i18n?: boolean; } export const mountAsync = async ( children: React.ReactElement, - options: IOptions + options: Options ): Promise => { let wrapper: ReactWrapper | undefined; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/app_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/app_logic.ts index 932e84af45c2b..2b475073c6ea5 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/app_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/app_logic.ts @@ -6,24 +6,24 @@ import { kea, MakeLogicType } from 'kea'; -import { IInitialAppData } from '../../../common/types'; -import { IConfiguredLimits, IAccount, IRole } from './types'; +import { InitialAppData } from '../../../common/types'; +import { ConfiguredLimits, Account, Role } from './types'; import { getRoleAbilities } from './utils/role'; -export interface IAppValues { +interface AppValues { hasInitialized: boolean; ilmEnabled: boolean; - configuredLimits: Partial; - account: Partial; - myRole: Partial; + configuredLimits: Partial; + account: Partial; + myRole: Partial; } -export interface IAppActions { - initializeAppData(props: IInitialAppData): Required; +interface AppActions { + initializeAppData(props: InitialAppData): Required; setOnboardingComplete(): boolean; } -export const AppLogic = kea>({ +export const AppLogic = kea>({ path: ['enterprise_search', 'app_search', 'app_logic'], actions: { initializeAppData: (props) => props, diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/constants.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/constants.ts index decf1e2158744..ea4906ec08946 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/constants.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/constants.ts @@ -3,8 +3,14 @@ * 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 CREDENTIALS_TITLE = i18n.translate( + 'xpack.enterpriseSearch.appSearch.credentials.title', + { defaultMessage: 'Credentials' } +); + export enum ApiTokenTypes { Admin = 'admin', Private = 'private', diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials.tsx index c8eae8cc13f5f..72b02dfdc1f61 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials.tsx @@ -28,6 +28,7 @@ import { FlashMessages } from '../../../shared/flash_messages'; import { CredentialsLogic } from './credentials_logic'; import { externalUrl } from '../../../shared/enterprise_search_url/external_url'; +import { CREDENTIALS_TITLE } from './constants'; import { CredentialsList } from './credentials_list'; import { CredentialsFlyout } from './credentials_flyout'; @@ -47,21 +48,11 @@ export const Credentials: React.FC = () => { return ( <> - + -

- {i18n.translate('xpack.enterpriseSearch.appSearch.credentials.title', { - defaultMessage: 'Credentials', - })} -

+

{CREDENTIALS_TITLE}

diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/form_components/key_read_write_access.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/form_components/key_read_write_access.tsx index a02b00b6ad377..d96c57b3c8bc3 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/form_components/key_read_write_access.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/form_components/key_read_write_access.tsx @@ -10,7 +10,7 @@ import { EuiCheckbox, EuiText, EuiTitle, EuiSpacer, EuiPanel } from '@elastic/eu import { i18n } from '@kbn/i18n'; import { CredentialsLogic } from '../../credentials_logic'; -import { ITokenReadWrite } from '../../types'; +import { TokenReadWrite } from '../../types'; export const FormKeyReadWriteAccess: React.FC = () => { const { setTokenReadWrite } = useActions(CredentialsLogic); @@ -37,7 +37,7 @@ export const FormKeyReadWriteAccess: React.FC = () => { name="read" id="read" checked={activeApiToken.read} - onChange={(e) => setTokenReadWrite(e.target as ITokenReadWrite)} + onChange={(e) => setTokenReadWrite(e.target as TokenReadWrite)} label={i18n.translate( 'xpack.enterpriseSearch.appSearch.credentials.formReadWrite.readLabel', { defaultMessage: 'Read Access' } @@ -47,7 +47,7 @@ export const FormKeyReadWriteAccess: React.FC = () => { name="write" id="write" checked={activeApiToken.write} - onChange={(e) => setTokenReadWrite(e.target as ITokenReadWrite)} + onChange={(e) => setTokenReadWrite(e.target as TokenReadWrite)} label={i18n.translate( 'xpack.enterpriseSearch.appSearch.credentials.formReadWrite.writeLabel', { defaultMessage: 'Write Access' } diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/header.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/header.test.tsx index a8d9505136faa..803789ebee568 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/header.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/header.test.tsx @@ -11,12 +11,12 @@ import { shallow } from 'enzyme'; import { EuiFlyoutHeader } from '@elastic/eui'; import { ApiTokenTypes } from '../constants'; -import { IApiToken } from '../types'; +import { ApiToken } from '../types'; import { CredentialsFlyoutHeader } from './header'; describe('CredentialsFlyoutHeader', () => { - const apiToken: IApiToken = { + const apiToken: ApiToken = { name: '', type: ApiTokenTypes.Private, read: true, diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_list/credentials_list.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_list/credentials_list.test.tsx index 97d29b9333f4b..4f5ded0a3ccc1 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_list/credentials_list.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_list/credentials_list.test.tsx @@ -10,7 +10,7 @@ import React from 'react'; import { shallow } from 'enzyme'; import { EuiBasicTable, EuiCopy, EuiEmptyPrompt } from '@elastic/eui'; -import { IApiToken } from '../types'; +import { ApiToken } from '../types'; import { ApiTokenTypes } from '../constants'; import { HiddenText } from '../../../../shared/hidden_text'; @@ -18,7 +18,7 @@ import { Key } from './key'; import { CredentialsList } from './credentials_list'; describe('Credentials', () => { - const apiToken: IApiToken = { + const apiToken: ApiToken = { name: '', type: ApiTokenTypes.Private, read: true, @@ -77,7 +77,7 @@ describe('Credentials', () => { }); const wrapper = shallow(); const { items } = wrapper.find(EuiBasicTable).props(); - expect(items.map((i: IApiToken) => i.id)).toEqual([undefined, 1, 2]); + expect(items.map((i: ApiToken) => i.id)).toEqual([undefined, 1, 2]); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_list/credentials_list.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_list/credentials_list.tsx index f9752dca582e1..9240bade4975e 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_list/credentials_list.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_list/credentials_list.tsx @@ -14,7 +14,7 @@ import { i18n } from '@kbn/i18n'; import { CredentialsLogic } from '../credentials_logic'; import { Key } from './key'; import { HiddenText } from '../../../../shared/hidden_text'; -import { IApiToken } from '../types'; +import { ApiToken } from '../types'; import { TOKEN_TYPE_DISPLAY_NAMES } from '../constants'; import { apiTokenSort } from '../utils/api_token_sort'; import { getModeDisplayText, getEnginesDisplayText } from '../utils'; @@ -26,21 +26,21 @@ export const CredentialsList: React.FC = () => { const items = useMemo(() => apiTokens.slice().sort(apiTokenSort), [apiTokens]); - const columns: Array> = [ + const columns: Array> = [ { name: 'Name', width: '12%', - render: (token: IApiToken) => token.name, + render: (token: ApiToken) => token.name, }, { name: 'Type', width: '15%', - render: (token: IApiToken) => TOKEN_TYPE_DISPLAY_NAMES[token.type], + render: (token: ApiToken) => TOKEN_TYPE_DISPLAY_NAMES[token.type], }, { name: 'Key', width: '36%', - render: (token: IApiToken) => { + render: (token: ApiToken) => { const { key } = token; if (!key) return null; return ( @@ -64,12 +64,12 @@ export const CredentialsList: React.FC = () => { { name: 'Modes', width: '10%', - render: (token: IApiToken) => getModeDisplayText(token), + render: (token: ApiToken) => getModeDisplayText(token), }, { name: 'Engines', width: '18%', - render: (token: IApiToken) => getEnginesDisplayText(token), + render: (token: ApiToken) => getEnginesDisplayText(token), }, { actions: [ @@ -83,7 +83,7 @@ export const CredentialsList: React.FC = () => { type: 'icon', icon: 'pencil', color: 'primary', - onClick: (token: IApiToken) => showCredentialsForm(token), + onClick: (token: ApiToken) => showCredentialsForm(token), }, { name: i18n.translate('xpack.enterpriseSearch.actions.delete', { @@ -95,7 +95,7 @@ export const CredentialsList: React.FC = () => { type: 'icon', icon: 'trash', color: 'danger', - onClick: (token: IApiToken) => deleteApiKey(token.name), + onClick: (token: ApiToken) => deleteApiKey(token.name), }, ], }, @@ -108,7 +108,7 @@ export const CredentialsList: React.FC = () => { hidePerPageOptions: true, }; - const onTableChange = ({ page }: CriteriaWithPagination) => { + const onTableChange = ({ page }: CriteriaWithPagination) => { const { index: current } = page; fetchCredentials(current + 1); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_list/key.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_list/key.tsx index 5c0c24ec733a4..fa2d124cbccdf 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_list/key.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_list/key.tsx @@ -8,14 +8,14 @@ import React from 'react'; import { EuiButtonIcon } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -interface IProps { +interface Props { copy: () => void; toggleIsHidden: () => void; isHidden: boolean; text: React.ReactNode; } -export const Key: React.FC = ({ copy, toggleIsHidden, isHidden, text }) => { +export const Key: React.FC = ({ copy, toggleIsHidden, isHidden, text }) => { const hideIcon = isHidden ? 'eye' : 'eyeClosed'; const hideIconLabel = isHidden ? i18n.translate('xpack.enterpriseSearch.appSearch.credentials.showApiKey', { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_logic.test.ts index de79862b540ba..6523b4fb110b0 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_logic.test.ts @@ -88,8 +88,8 @@ describe('CredentialsLogic', () => { const credentialsDetails = { engines: [ - { name: 'engine1', type: 'indexed', language: 'english', result_fields: [] }, - { name: 'engine1', type: 'indexed', language: 'english', result_fields: [] }, + { name: 'engine1', type: 'indexed', language: 'english', result_fields: {} }, + { name: 'engine1', type: 'indexed', language: 'english', result_fields: {} }, ], }; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_logic.ts index 7b8b864b3a317..166cbae9a4512 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_logic.ts @@ -17,11 +17,11 @@ import { } from '../../../shared/flash_messages'; import { AppLogic } from '../../app_logic'; -import { IMeta } from '../../../../../common/types'; -import { IEngine } from '../../types'; -import { IApiToken, ICredentialsDetails, ITokenReadWrite } from './types'; +import { Meta } from '../../../../../common/types'; +import { Engine } from '../../types'; +import { ApiToken, CredentialsDetails, TokenReadWrite } from './types'; -export const defaultApiToken: IApiToken = { +export const defaultApiToken: ApiToken = { name: '', type: ApiTokenTypes.Private, read: true, @@ -29,21 +29,21 @@ export const defaultApiToken: IApiToken = { access_all_engines: true, }; -interface ICredentialsLogicActions { +interface CredentialsLogicActions { addEngineName(engineName: string): string; onApiKeyDelete(tokenName: string): string; - onApiTokenCreateSuccess(apiToken: IApiToken): IApiToken; + onApiTokenCreateSuccess(apiToken: ApiToken): ApiToken; onApiTokenError(formErrors: string[]): string[]; - onApiTokenUpdateSuccess(apiToken: IApiToken): IApiToken; + onApiTokenUpdateSuccess(apiToken: ApiToken): ApiToken; removeEngineName(engineName: string): string; setAccessAllEngines(accessAll: boolean): boolean; - setCredentialsData(meta: IMeta, apiTokens: IApiToken[]): { meta: IMeta; apiTokens: IApiToken[] }; - setCredentialsDetails(details: ICredentialsDetails): ICredentialsDetails; + setCredentialsData(meta: Meta, apiTokens: ApiToken[]): { meta: Meta; apiTokens: ApiToken[] }; + setCredentialsDetails(details: CredentialsDetails): CredentialsDetails; setNameInputBlurred(isBlurred: boolean): boolean; - setTokenReadWrite(tokenReadWrite: ITokenReadWrite): ITokenReadWrite; + setTokenReadWrite(tokenReadWrite: TokenReadWrite): TokenReadWrite; setTokenName(name: string): string; setTokenType(tokenType: string): string; - showCredentialsForm(apiToken?: IApiToken): IApiToken; + showCredentialsForm(apiToken?: ApiToken): ApiToken; hideCredentialsForm(): { value: boolean }; resetCredentials(): { value: boolean }; initializeCredentialsData(): { value: boolean }; @@ -54,25 +54,25 @@ interface ICredentialsLogicActions { onEngineSelect(engineName: string): string; } -interface ICredentialsLogicValues { - activeApiToken: IApiToken; +interface CredentialsLogicValues { + activeApiToken: ApiToken; activeApiTokenExists: boolean; activeApiTokenRawName: string; - apiTokens: IApiToken[]; + apiTokens: ApiToken[]; dataLoading: boolean; - engines: IEngine[]; + engines: Engine[]; formErrors: string[]; isCredentialsDataComplete: boolean; isCredentialsDetailsComplete: boolean; fullEngineAccessChecked: boolean; - meta: Partial; + meta: Partial; nameInputBlurred: boolean; shouldShowCredentialsForm: boolean; } -export const CredentialsLogic = kea< - MakeLogicType ->({ +type CredentialsLogicType = MakeLogicType; // If we leave this inline, Prettier does some horrifying indenting nonsense :/ + +export const CredentialsLogic = kea({ path: ['enterprise_search', 'app_search', 'credentials_logic'], actions: () => ({ addEngineName: (engineName) => engineName, @@ -267,7 +267,7 @@ export const CredentialsLogic = kea< onApiTokenChange: async () => { const { id, name, engines, type, read, write } = values.activeApiToken; - const data: IApiToken = { + const data: ApiToken = { name, type, }; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/index.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/index.ts index bceda234175a7..0fc64a43ffa85 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/index.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/index.ts @@ -5,3 +5,4 @@ */ export { Credentials } from './credentials'; +export { CREDENTIALS_TITLE } from './constants'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/types.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/types.ts index 9ca4d086d55c8..23f78b44c0db5 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/types.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/types.ts @@ -4,14 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ -import { IEngine } from '../../types'; +import { Engine } from '../../types'; import { ApiTokenTypes } from './constants'; -export interface ICredentialsDetails { - engines: IEngine[]; +export interface CredentialsDetails { + engines: Engine[]; } -export interface IApiToken { +export interface ApiToken { access_all_engines?: boolean; key?: string; engines?: string[]; @@ -22,7 +22,7 @@ export interface IApiToken { write?: boolean; } -export interface ITokenReadWrite { +export interface TokenReadWrite { name: 'read' | 'write'; checked: boolean; } diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/utils/api_token_sort.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/utils/api_token_sort.test.ts index 84818322b3570..2287125bb5eb8 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/utils/api_token_sort.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/utils/api_token_sort.test.ts @@ -7,10 +7,10 @@ import { apiTokenSort } from '.'; import { ApiTokenTypes } from '../constants'; -import { IApiToken } from '../types'; +import { ApiToken } from '../types'; describe('apiTokenSort', () => { - const apiToken: IApiToken = { + const apiToken: ApiToken = { name: '', type: ApiTokenTypes.Private, read: true, diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/utils/api_token_sort.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/utils/api_token_sort.ts index 80a46f30e0930..b9fb501ccabe2 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/utils/api_token_sort.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/utils/api_token_sort.ts @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { IApiToken } from '../types'; +import { ApiToken } from '../types'; -export const apiTokenSort = (apiTokenA: IApiToken, apiTokenB: IApiToken): number => { +export const apiTokenSort = (apiTokenA: ApiToken, apiTokenB: ApiToken): number => { if (!apiTokenA.id) { return -1; } diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/utils/get_engines_display_text.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/utils/get_engines_display_text.test.tsx index b06ed63f8616c..bee19a44facd3 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/utils/get_engines_display_text.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/utils/get_engines_display_text.test.tsx @@ -8,10 +8,10 @@ import React from 'react'; import { shallow } from 'enzyme'; import { getEnginesDisplayText } from './get_engines_display_text'; -import { IApiToken } from '../types'; +import { ApiToken } from '../types'; import { ApiTokenTypes } from '../constants'; -const apiToken: IApiToken = { +const apiToken: ApiToken = { name: '', type: ApiTokenTypes.Private, read: true, diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/utils/get_engines_display_text.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/utils/get_engines_display_text.tsx index 1b216c46307db..fb23551302f3b 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/utils/get_engines_display_text.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/utils/get_engines_display_text.tsx @@ -6,9 +6,9 @@ import React from 'react'; import { ApiTokenTypes, ALL } from '../constants'; -import { IApiToken } from '../types'; +import { ApiToken } from '../types'; -export const getEnginesDisplayText = (apiToken: IApiToken): JSX.Element | string => { +export const getEnginesDisplayText = (apiToken: ApiToken): JSX.Element | string => { const { type, access_all_engines: accessAll, engines = [] } = apiToken; if (type === ApiTokenTypes.Admin) { return '--'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/utils/get_mode_display_text.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/utils/get_mode_display_text.test.ts index b2083f22c8e1c..46b4c9b2c786c 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/utils/get_mode_display_text.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/utils/get_mode_display_text.test.ts @@ -5,11 +5,11 @@ */ import { ApiTokenTypes } from '../constants'; -import { IApiToken } from '../types'; +import { ApiToken } from '../types'; import { getModeDisplayText } from './get_mode_display_text'; -const apiToken: IApiToken = { +const apiToken: ApiToken = { name: '', type: ApiTokenTypes.Private, read: true, diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/utils/get_mode_display_text.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/utils/get_mode_display_text.ts index 9c8758d83882d..b150aa2cfc3be 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/utils/get_mode_display_text.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/utils/get_mode_display_text.ts @@ -5,9 +5,9 @@ */ import { ApiTokenTypes, READ_ONLY, READ_WRITE, SEARCH_DISPLAY, WRITE_ONLY } from '../constants'; -import { IApiToken } from '../types'; +import { ApiToken } from '../types'; -export const getModeDisplayText = (apiToken: IApiToken): string => { +export const getModeDisplayText = (apiToken: ApiToken): string => { const { read = false, write = false, type } = apiToken; switch (type) { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/constants.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/constants.ts index ee5b47eda490e..3c963e415f33b 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/constants.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/constants.ts @@ -9,10 +9,6 @@ import { i18n } from '@kbn/i18n'; // TODO: It's very likely that we'll move these i18n constants to their respective component // folders once those are migrated over. This is a temporary way of DRYing them out for now. -export const ENGINES_TITLE = i18n.translate('xpack.enterpriseSearch.appSearch.engines.title', { - defaultMessage: 'Engines', -}); - export const OVERVIEW_TITLE = i18n.translate( 'xpack.enterpriseSearch.appSearch.engine.overview.title', { defaultMessage: 'Overview' } diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_logic.test.ts new file mode 100644 index 0000000000000..13db440df739e --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_logic.test.ts @@ -0,0 +1,266 @@ +/* + * 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 { resetContext } from 'kea'; + +import { mockHttpValues } from '../../../__mocks__'; +jest.mock('../../../shared/http', () => ({ + HttpLogic: { values: mockHttpValues }, +})); +const { http } = mockHttpValues; + +import { EngineLogic } from './'; + +describe('EngineLogic', () => { + const mockEngineData = { + name: 'some-engine', + type: 'default', + created_at: 'some date timestamp', + language: null, + document_count: 1, + field_count: 1, + result_fields: { + id: { raw: {} }, + }, + unconfirmedFields: [], + unsearchedUnconfirmedFields: false, + sample: false, + isMeta: false, + invalidBoosts: false, + schema: {}, + apiTokens: [], + apiKey: 'some-key', + }; + + const DEFAULT_VALUES = { + dataLoading: true, + engine: {}, + engineName: '', + isMetaEngine: false, + isSampleEngine: false, + hasSchemaConflicts: false, + hasUnconfirmedSchemaFields: false, + engineNotFound: false, + }; + + const mount = (values?: object) => { + if (!values) { + resetContext({}); + } else { + resetContext({ + defaults: { + enterprise_search: { + app_search: { + engine_logic: { + ...values, + }, + }, + }, + }, + }); + } + EngineLogic.mount(); + }; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('has expected default values', () => { + mount(); + expect(EngineLogic.values).toEqual(DEFAULT_VALUES); + }); + + describe('actions', () => { + describe('setEngineData', () => { + describe('engine & dataLoading', () => { + it('should set engine to the provided value and dataLoading to false', () => { + mount(); + EngineLogic.actions.setEngineData(mockEngineData); + + expect(EngineLogic.values).toEqual({ + ...DEFAULT_VALUES, + engine: mockEngineData, + dataLoading: false, + }); + }); + }); + }); + + describe('setEngineName', () => { + describe('engineName', () => { + it('should be set to the provided value', () => { + mount(); + EngineLogic.actions.setEngineName('some-new-engine'); + + expect(EngineLogic.values).toEqual({ + ...DEFAULT_VALUES, + engineName: 'some-new-engine', + }); + }); + }); + }); + + describe('setIndexingStatus', () => { + describe('engine', () => { + it('should set the nested obj property to the provided value', () => { + mount({ engine: mockEngineData }); + const mockReindexJob = { + percentageComplete: 50, + numDocumentsWithErrors: 2, + activeReindexJobId: 123, + }; + EngineLogic.actions.setIndexingStatus(mockReindexJob); + + expect(EngineLogic.values).toEqual({ + ...DEFAULT_VALUES, + engine: { + ...mockEngineData, + activeReindexJob: mockReindexJob, + }, + }); + }); + }); + }); + + describe('setEngineNotFound', () => { + describe('engineNotFound', () => { + it('should be set to the provided value', () => { + mount(); + EngineLogic.actions.setEngineNotFound(true); + + expect(EngineLogic.values).toEqual({ + ...DEFAULT_VALUES, + engineNotFound: true, + }); + }); + }); + }); + + describe('clearEngine', () => { + describe('engine', () => { + it('should be reset to an empty obj', () => { + mount({ engine: mockEngineData }); + EngineLogic.actions.clearEngine(); + + expect(EngineLogic.values).toEqual({ + ...DEFAULT_VALUES, + engine: {}, + }); + }); + }); + + describe('dataLoading', () => { + it('should be set to true', () => { + mount({ dataLoading: false }); + EngineLogic.actions.clearEngine(); + + expect(EngineLogic.values).toEqual({ + ...DEFAULT_VALUES, + dataLoading: true, + }); + }); + }); + }); + + describe('initializeEngine', () => { + it('fetches and sets engine data', async () => { + mount({ engineName: 'some-engine' }); + jest.spyOn(EngineLogic.actions, 'setEngineData'); + const promise = Promise.resolve(mockEngineData); + http.get.mockReturnValueOnce(promise); + + EngineLogic.actions.initializeEngine(); + await promise; + + expect(http.get).toHaveBeenCalledWith('/api/app_search/engines/some-engine'); + expect(EngineLogic.actions.setEngineData).toHaveBeenCalledWith(mockEngineData); + }); + + it('handles errors', async () => { + mount(); + jest.spyOn(EngineLogic.actions, 'setEngineNotFound'); + const promise = Promise.reject('An error occured'); + http.get.mockReturnValue(promise); + + try { + EngineLogic.actions.initializeEngine(); + await promise; + } catch { + // Do nothing + } + expect(EngineLogic.actions.setEngineNotFound).toHaveBeenCalledWith(true); + }); + }); + }); + + describe('selectors', () => { + describe('isSampleEngine', () => { + it('should be set based on engine.sample', () => { + const mockSampleEngine = { ...mockEngineData, sample: true }; + mount({ engine: mockSampleEngine }); + + expect(EngineLogic.values).toEqual({ + ...DEFAULT_VALUES, + engine: mockSampleEngine, + isSampleEngine: true, + }); + }); + }); + + describe('isMetaEngine', () => { + it('should be set based on engine.type', () => { + const mockMetaEngine = { ...mockEngineData, type: 'meta' }; + mount({ engine: mockMetaEngine }); + + expect(EngineLogic.values).toEqual({ + ...DEFAULT_VALUES, + engine: mockMetaEngine, + isMetaEngine: true, + }); + }); + }); + + describe('hasSchemaConflicts', () => { + it('should be set based on engine.schemaConflicts', () => { + const mockSchemaEngine = { + ...mockEngineData, + schemaConflicts: { + someSchemaField: { + fieldTypes: { + number: ['some-engine'], + date: ['another-engine'], + }, + }, + }, + }; + mount({ engine: mockSchemaEngine }); + + expect(EngineLogic.values).toEqual({ + ...DEFAULT_VALUES, + engine: mockSchemaEngine, + hasSchemaConflicts: true, + }); + }); + }); + + describe('hasUnconfirmedSchemaFields', () => { + it('should be set based on engine.unconfirmedFields', () => { + const mockUnconfirmedFieldsEngine = { + ...mockEngineData, + unconfirmedFields: ['new_field_1', 'new_field_2'], + }; + mount({ engine: mockUnconfirmedFieldsEngine }); + + expect(EngineLogic.values).toEqual({ + ...DEFAULT_VALUES, + engine: mockUnconfirmedFieldsEngine, + hasUnconfirmedSchemaFields: true, + }); + }); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_logic.ts new file mode 100644 index 0000000000000..2e7595e3ee87b --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_logic.ts @@ -0,0 +1,101 @@ +/* + * 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 { kea, MakeLogicType } from 'kea'; + +import { HttpLogic } from '../../../shared/http'; + +import { IndexingStatus } from '../schema/types'; +import { EngineDetails } from './types'; + +interface EngineValues { + dataLoading: boolean; + engine: EngineDetails | {}; + engineName: string; + isMetaEngine: boolean; + isSampleEngine: boolean; + hasSchemaConflicts: boolean; + hasUnconfirmedSchemaFields: boolean; + engineNotFound: boolean; +} + +interface EngineActions { + setEngineData(engine: EngineDetails): { engine: EngineDetails }; + setEngineName(engineName: string): { engineName: string }; + setIndexingStatus(activeReindexJob: IndexingStatus): { activeReindexJob: IndexingStatus }; + setEngineNotFound(notFound: boolean): { notFound: boolean }; + clearEngine(): void; + initializeEngine(): void; +} + +export const EngineLogic = kea>({ + path: ['enterprise_search', 'app_search', 'engine_logic'], + actions: { + setEngineData: (engine) => ({ engine }), + setEngineName: (engineName) => ({ engineName }), + setIndexingStatus: (activeReindexJob) => ({ activeReindexJob }), + setEngineNotFound: (notFound) => ({ notFound }), + clearEngine: true, + initializeEngine: true, + }, + reducers: { + dataLoading: [ + true, + { + setEngineData: () => false, + clearEngine: () => true, + }, + ], + engine: [ + {}, + { + setEngineData: (_, { engine }) => engine, + clearEngine: () => ({}), + setIndexingStatus: (state, { activeReindexJob }) => ({ + ...state, + activeReindexJob, + }), + }, + ], + engineName: [ + '', + { + setEngineName: (_, { engineName }) => engineName, + }, + ], + engineNotFound: [ + false, + { + setEngineNotFound: (_, { notFound }) => notFound, + }, + ], + }, + selectors: ({ selectors }) => ({ + isMetaEngine: [() => [selectors.engine], (engine) => engine?.type === 'meta'], + isSampleEngine: [() => [selectors.engine], (engine) => !!engine?.sample], + hasSchemaConflicts: [ + () => [selectors.engine], + (engine) => !!(engine?.schemaConflicts && Object.keys(engine.schemaConflicts).length > 0), + ], + hasUnconfirmedSchemaFields: [ + () => [selectors.engine], + (engine) => engine?.unconfirmedFields?.length > 0, + ], + }), + listeners: ({ actions, values }) => ({ + initializeEngine: async () => { + const { engineName } = values; + const { http } = HttpLogic.values; + + try { + const response = await http.get(`/api/app_search/engines/${engineName}`); + actions.setEngineData(response); + } catch (error) { + actions.setEngineNotFound(true); + } + }, + }), +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_nav.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_nav.tsx index e5ee392b34c01..f92fefc7124b8 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_nav.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_nav.tsx @@ -30,8 +30,8 @@ import { ENGINE_API_LOGS_PATH, } from '../../routes'; import { getAppSearchUrl } from '../../../shared/enterprise_search_url'; +import { ENGINES_TITLE } from '../engines'; import { - ENGINES_TITLE, OVERVIEW_TITLE, ANALYTICS_TITLE, DOCUMENTS_TITLE, diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/index.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/index.ts index a3320ba5024ca..3d8f343312cc6 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/index.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/index.ts @@ -5,3 +5,4 @@ */ export { EngineRouter, EngineNav } from './engine_nav'; +export { EngineLogic } from './engine_logic'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/types.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/types.ts new file mode 100644 index 0000000000000..635d1136291aa --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/types.ts @@ -0,0 +1,43 @@ +/* + * 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 { ApiToken } from '../credentials/types'; +import { Schema, SchemaConflicts, IndexingStatus } from '../schema/types'; + +export interface Engine { + name: string; + type: string; + language: string | null; + result_fields: { + [key: string]: ResultField; + }; +} + +export interface EngineDetails extends Engine { + created_at: string; + document_count: number; + field_count: number; + unsearchedUnconfirmedFields: boolean; + apiTokens: ApiToken[]; + apiKey: string; + schema: Schema; + schemaConflicts?: SchemaConflicts; + unconfirmedFields?: string[]; + activeReindexJob?: IndexingStatus; + invalidBoosts: boolean; + sample?: boolean; + isMeta: boolean; + engine_count?: number; + includedEngines?: EngineDetails[]; +} + +interface ResultField { + raw: object; + snippet?: { + size: number; + fallback: boolean; + }; +} diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/assets/engine_icon.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/assets/engine_icon.tsx similarity index 100% rename from x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/assets/engine_icon.tsx rename to x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/assets/engine_icon.tsx diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/assets/meta_engine_icon.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/assets/meta_engine_icon.tsx similarity index 100% rename from x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/assets/meta_engine_icon.tsx rename to x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/assets/meta_engine_icon.tsx diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/empty_state.scss b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/empty_state.scss similarity index 100% rename from x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/empty_state.scss rename to x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/empty_state.scss diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/empty_state.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/empty_state.test.tsx similarity index 100% rename from x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/empty_state.test.tsx rename to x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/empty_state.test.tsx diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/empty_state.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/empty_state.tsx similarity index 96% rename from x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/empty_state.tsx rename to x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/empty_state.tsx index b7ed1cc895097..95142782d2272 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/empty_state.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/empty_state.tsx @@ -14,7 +14,7 @@ import { getAppSearchUrl } from '../../../../shared/enterprise_search_url'; import { SetAppSearchChrome as SetPageChrome } from '../../../../shared/kibana_chrome'; import { CREATE_ENGINES_PATH } from '../../../routes'; -import { EngineOverviewHeader } from './header'; +import { EnginesOverviewHeader } from './header'; import './empty_state.scss'; @@ -34,7 +34,7 @@ export const EmptyState: React.FC = () => { return ( <> - + { +describe('EnginesOverviewHeader', () => { it('renders', () => { - const wrapper = shallow(); + const wrapper = shallow(); expect(wrapper.find('h1')).toHaveLength(1); }); it('renders a launch app search button that sends telemetry on click', () => { - const wrapper = shallow(); + const wrapper = shallow(); const button = wrapper.find('[data-test-subj="launchButton"]'); expect(button.prop('href')).toBe('http://localhost:3002/as'); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/header.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/header.tsx similarity index 93% rename from x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/header.tsx rename to x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/header.tsx index 4bb69bafa0996..3faa74be11e98 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/header.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/header.tsx @@ -19,7 +19,7 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { TelemetryLogic } from '../../../../shared/telemetry'; import { getAppSearchUrl } from '../../../../shared/enterprise_search_url'; -export const EngineOverviewHeader: React.FC = () => { +export const EnginesOverviewHeader: React.FC = () => { const { sendAppSearchTelemetry } = useActions(TelemetryLogic); const buttonProps = { @@ -42,7 +42,7 @@ export const EngineOverviewHeader: React.FC = () => {

diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/index.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/index.ts similarity index 87% rename from x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/index.ts rename to x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/index.ts index 794053f184f8c..cb2e0f825d455 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/index.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/index.ts @@ -4,6 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -export { EngineOverviewHeader } from './header'; +export { EnginesOverviewHeader } from './header'; export { LoadingState } from './loading_state'; export { EmptyState } from './empty_state'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/loading_state.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/loading_state.test.tsx similarity index 100% rename from x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/loading_state.test.tsx rename to x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/loading_state.test.tsx diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/loading_state.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/loading_state.tsx similarity index 89% rename from x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/loading_state.tsx rename to x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/loading_state.tsx index 832a133ed43c7..8e2166d2ef105 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/loading_state.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/loading_state.tsx @@ -8,13 +8,13 @@ import React from 'react'; import { EuiPageContent, EuiSpacer, EuiLoadingContent } from '@elastic/eui'; import { SetAppSearchChrome as SetPageChrome } from '../../../../shared/kibana_chrome'; -import { EngineOverviewHeader } from './header'; +import { EnginesOverviewHeader } from './header'; export const LoadingState: React.FC = () => { return ( <> - + diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/constants.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/constants.ts new file mode 100644 index 0000000000000..12545c59b40a0 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/constants.ts @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export const ENGINES_TITLE = i18n.translate('xpack.enterpriseSearch.appSearch.engines.title', { + defaultMessage: 'Engines', +}); + +export const META_ENGINES_TITLE = i18n.translate( + 'xpack.enterpriseSearch.appSearch.metaEngines.title', + { defaultMessage: 'Meta Engines' } +); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.scss b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_overview.scss similarity index 96% rename from x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.scss rename to x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_overview.scss index 5e8a20ba425ad..c956f3da562f6 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.scss +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_overview.scss @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -.engineOverview { +.enginesOverview { padding: $euiSize; @include euiBreakpoint('m', 'l', 'xl') { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_overview.test.tsx similarity index 80% rename from x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.test.tsx rename to x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_overview.test.tsx index f87ea2d422780..61f783a8b6c2e 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_overview.test.tsx @@ -14,14 +14,14 @@ import { shallow, ReactWrapper } from 'enzyme'; import { mountAsync, mockHttpValues, setMockValues } from '../../../__mocks__'; import { LoadingState, EmptyState } from './components'; -import { EngineTable } from './engine_table'; +import { EnginesTable } from './engines_table'; -import { EngineOverview } from './'; +import { EnginesOverview } from './'; -describe('EngineOverview', () => { +describe('EnginesOverview', () => { describe('non-happy-path states', () => { it('isLoading', () => { - const wrapper = shallow(); + const wrapper = shallow(); expect(wrapper.find(LoadingState)).toHaveLength(1); }); @@ -36,7 +36,7 @@ describe('EngineOverview', () => { }), }, }); - const wrapper = await mountAsync(, { i18n: true }); + const wrapper = await mountAsync(, { i18n: true }); expect(wrapper.find(EmptyState)).toHaveLength(1); }); @@ -69,9 +69,9 @@ describe('EngineOverview', () => { }); it('renders and calls the engines API', async () => { - const wrapper = await mountAsync(, { i18n: true }); + const wrapper = await mountAsync(, { i18n: true }); - expect(wrapper.find(EngineTable)).toHaveLength(1); + expect(wrapper.find(EnginesTable)).toHaveLength(1); expect(mockApi).toHaveBeenNthCalledWith(1, '/api/app_search/engines', { query: { type: 'indexed', @@ -86,9 +86,9 @@ describe('EngineOverview', () => { hasPlatinumLicense: true, http: { ...mockHttpValues.http, get: mockApi }, }); - const wrapper = await mountAsync(, { i18n: true }); + const wrapper = await mountAsync(, { i18n: true }); - expect(wrapper.find(EngineTable)).toHaveLength(2); + expect(wrapper.find(EnginesTable)).toHaveLength(2); expect(mockApi).toHaveBeenNthCalledWith(2, '/api/app_search/engines', { query: { type: 'meta', @@ -100,10 +100,10 @@ describe('EngineOverview', () => { describe('pagination', () => { const getTablePagination = (wrapper: ReactWrapper) => - wrapper.find(EngineTable).prop('pagination'); + wrapper.find(EnginesTable).prop('pagination'); it('passes down page data from the API', async () => { - const wrapper = await mountAsync(, { i18n: true }); + const wrapper = await mountAsync(, { i18n: true }); const pagination = getTablePagination(wrapper); expect(pagination.totalEngines).toEqual(100); @@ -111,7 +111,7 @@ describe('EngineOverview', () => { }); it('re-polls the API on page change', async () => { - const wrapper = await mountAsync(, { i18n: true }); + const wrapper = await mountAsync(, { i18n: true }); await act(async () => getTablePagination(wrapper).onPaginate(5)); wrapper.update(); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_overview.tsx similarity index 78% rename from x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.tsx rename to x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_overview.tsx index c0fd254567910..0381c3806fec7 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_overview.tsx @@ -13,7 +13,6 @@ import { EuiTitle, EuiSpacer, } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; import { SetAppSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome'; import { SendAppSearchTelemetry as SendTelemetry } from '../../../shared/telemetry'; @@ -23,22 +22,22 @@ import { LicensingLogic } from '../../../shared/licensing'; import { EngineIcon } from './assets/engine_icon'; import { MetaEngineIcon } from './assets/meta_engine_icon'; +import { ENGINES_TITLE, META_ENGINES_TITLE } from './constants'; +import { EnginesOverviewHeader, LoadingState, EmptyState } from './components'; +import { EnginesTable } from './engines_table'; -import { EngineOverviewHeader, LoadingState, EmptyState } from './components'; -import { EngineTable } from './engine_table'; +import './engines_overview.scss'; -import './engine_overview.scss'; - -interface IGetEnginesParams { +interface GetEnginesParams { type: string; pageIndex: number; } -interface ISetEnginesCallbacks { +interface SetEnginesCallbacks { setResults: React.Dispatch>; setResultsTotal: React.Dispatch>; } -export const EngineOverview: React.FC = () => { +export const EnginesOverview: React.FC = () => { const { http } = useValues(HttpLogic); const { hasPlatinumLicense } = useValues(LicensingLogic); @@ -50,12 +49,12 @@ export const EngineOverview: React.FC = () => { const [metaEnginesPage, setMetaEnginesPage] = useState(1); const [metaEnginesTotal, setMetaEnginesTotal] = useState(0); - const getEnginesData = async ({ type, pageIndex }: IGetEnginesParams) => { + const getEnginesData = async ({ type, pageIndex }: GetEnginesParams) => { return await http.get('/api/app_search/engines', { query: { type, pageIndex }, }); }; - const setEnginesData = async (params: IGetEnginesParams, callbacks: ISetEnginesCallbacks) => { + const setEnginesData = async (params: GetEnginesParams, callbacks: SetEnginesCallbacks) => { const response = await getEnginesData(params); callbacks.setResults(response.results); @@ -88,21 +87,18 @@ export const EngineOverview: React.FC = () => { - - + +

- - {i18n.translate('xpack.enterpriseSearch.appSearch.enginesOverview.engines', { - defaultMessage: 'Engines', - })} + {ENGINES_TITLE}

- {

- - {i18n.translate('xpack.enterpriseSearch.appSearch.enginesOverview.metaEngines', { - defaultMessage: 'Meta Engines', - })} + {META_ENGINES_TITLE}

- { +describe('EnginesTable', () => { const onPaginate = jest.fn(); // onPaginate updates the engines API call upstream const wrapper = mountWithIntl( - { it('handles empty data', () => { const emptyWrapper = mountWithIntl( - {} }} /> + {} }} + /> ); const emptyTable = emptyWrapper.find(EuiBasicTable); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_table.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_table.tsx similarity index 91% rename from x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_table.tsx rename to x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_table.tsx index abeaf45e6aee8..9591bbda1f7c2 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_table.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_table.tsx @@ -16,28 +16,28 @@ import { getEngineRoute } from '../../routes'; import { ENGINES_PAGE_SIZE } from '../../../../../common/constants'; -export interface IEngineTableData { +interface EnginesTableData { name: string; created_at: string; document_count: number; field_count: number; } -export interface IEngineTablePagination { +interface EnginesTablePagination { totalEngines: number; pageIndex: number; onPaginate(pageIndex: number): void; } -export interface IEngineTableProps { - data: IEngineTableData[]; - pagination: IEngineTablePagination; +interface EnginesTableProps { + data: EnginesTableData[]; + pagination: EnginesTablePagination; } -export interface IOnChange { +interface OnChange { page: { index: number; }; } -export const EngineTable: React.FC = ({ +export const EnginesTable: React.FC = ({ data, pagination: { totalEngines, pageIndex, onPaginate }, }) => { @@ -52,7 +52,7 @@ export const EngineTable: React.FC = ({ }), }); - const columns: Array> = [ + const columns: Array> = [ { field: 'name', name: i18n.translate('xpack.enterpriseSearch.appSearch.enginesOverview.table.column.name', { @@ -144,7 +144,7 @@ export const EngineTable: React.FC = ({ totalItemCount: totalEngines, hidePerPageOptions: true, }} - onChange={({ page }: IOnChange) => { + onChange={({ page }: OnChange) => { const { index } = page; onPaginate(index + 1); // Note on paging - App Search's API pages start at 1, EuiBasicTables' pages start at 0 }} diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/index.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/index.ts new file mode 100644 index 0000000000000..76ca9239a3c7e --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { EnginesOverview } from './engines_overview'; +export { ENGINES_TITLE, META_ENGINES_TITLE } from './constants'; diff --git a/x-pack/plugins/maps/public/embeddable/merge_input_with_saved_map.d.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/constants.ts similarity index 57% rename from x-pack/plugins/maps/public/embeddable/merge_input_with_saved_map.d.ts rename to x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/constants.ts index 4ce4df02f6a39..133d8b604381b 100644 --- a/x-pack/plugins/maps/public/embeddable/merge_input_with_saved_map.d.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/constants.ts @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { MapEmbeddableInput } from './map_embeddable'; +import { i18n } from '@kbn/i18n'; -export function mergeInputWithSavedMap( - input: MapEmbeddableInput, - savedmap: unknown -): Partial; +export const ROLE_MAPPINGS_TITLE = i18n.translate( + 'xpack.enterpriseSearch.appSearch.roleMappings.title', + { defaultMessage: 'Role Mappings' } +); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/index.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/index.ts similarity index 82% rename from x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/index.ts rename to x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/index.ts index 48b7645dc39e8..662b91bb3e925 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/index.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/index.ts @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export { EngineOverview } from './engine_overview'; +export { ROLE_MAPPINGS_TITLE } from './constants'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/types.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/types.ts new file mode 100644 index 0000000000000..84f402dd3b95f --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/types.ts @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export type SchemaTypes = 'text' | 'number' | 'geolocation' | 'date'; + +export interface Schema { + [key: string]: SchemaTypes; +} + +// this is a mapping of schema field types ("string", "number", "geolocation", "date") to the names +// of source engines which utilize that type +export type SchemaConflictFieldTypes = { + [key in SchemaTypes]: string[]; +}; + +export interface SchemaConflict { + fieldTypes: SchemaConflictFieldTypes; + resolution?: string; +} + +// For now these values are ISchemaConflictFieldTypes, but in the near future will be ISchemaConflict +// once we implement schema conflict resolution +export interface SchemaConflicts { + [key: string]: SchemaConflictFieldTypes; +} + +export interface IndexingStatus { + percentageComplete: number; + numDocumentsWithErrors: number; + activeReindexJobId: number; +} diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/constants.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/constants.ts new file mode 100644 index 0000000000000..c7b22d740341c --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/constants.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export const SETTINGS_TITLE = i18n.translate('xpack.enterpriseSearch.appSearch.settings.title', { + defaultMessage: 'Settings', +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/index.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/index.ts index db74dcb1a1846..3e162a4a2a1bc 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/index.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/index.ts @@ -5,3 +5,4 @@ */ export { Settings } from './settings'; +export { SETTINGS_TITLE } from './constants'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/log_retention_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/log_retention_logic.test.ts new file mode 100644 index 0000000000000..c86d7e3e915e2 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/log_retention_logic.test.ts @@ -0,0 +1,368 @@ +/* + * 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 { resetContext } from 'kea'; + +import { mockHttpValues } from '../../../../__mocks__'; +jest.mock('../../../../shared/http', () => ({ + HttpLogic: { values: mockHttpValues }, +})); +const { http } = mockHttpValues; + +jest.mock('../../../../shared/flash_messages', () => ({ + flashAPIErrors: jest.fn(), +})); +import { flashAPIErrors } from '../../../../shared/flash_messages'; + +import { LogRetentionOptions } from './types'; +import { LogRetentionLogic } from './log_retention_logic'; + +describe('LogRetentionLogic', () => { + const TYPICAL_SERVER_LOG_RETENTION = { + analytics: { + disabled_at: null, + enabled: true, + retention_policy: { is_default: true, min_age_days: 180 }, + }, + api: { + disabled_at: null, + enabled: true, + retention_policy: { is_default: true, min_age_days: 180 }, + }, + }; + + const TYPICAL_CLIENT_LOG_RETENTION = { + analytics: { + disabledAt: null, + enabled: true, + retentionPolicy: { isDefault: true, minAgeDays: 180 }, + }, + api: { + disabledAt: null, + enabled: true, + retentionPolicy: { isDefault: true, minAgeDays: 180 }, + }, + }; + + const DEFAULT_VALUES = { + logRetention: null, + openedModal: null, + isLogRetentionUpdating: false, + }; + + const mount = (defaults?: object) => { + if (!defaults) { + resetContext({}); + } else { + resetContext({ + defaults: { + enterprise_search: { + app_search: { + log_retention_logic: { + ...defaults, + }, + }, + }, + }, + }); + } + LogRetentionLogic.mount(); + }; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('has expected default values', () => { + mount(); + expect(LogRetentionLogic.values).toEqual(DEFAULT_VALUES); + }); + + describe('actions', () => { + describe('setOpenedModal', () => { + describe('openedModal', () => { + it('should be set to the provided value', () => { + mount(); + + LogRetentionLogic.actions.setOpenedModal(LogRetentionOptions.Analytics); + + expect(LogRetentionLogic.values).toEqual({ + ...DEFAULT_VALUES, + openedModal: LogRetentionOptions.Analytics, + }); + }); + }); + }); + + describe('closeModals', () => { + describe('openedModal', () => { + it('resets openedModal to null', () => { + mount({ + openedModal: 'analytics', + }); + + LogRetentionLogic.actions.closeModals(); + + expect(LogRetentionLogic.values).toEqual({ + ...DEFAULT_VALUES, + openedModal: null, + }); + }); + }); + + describe('isLogRetentionUpdating', () => { + it('resets isLogRetentionUpdating to false', () => { + mount({ + isLogRetentionUpdating: true, + }); + + LogRetentionLogic.actions.closeModals(); + + expect(LogRetentionLogic.values).toEqual({ + ...DEFAULT_VALUES, + isLogRetentionUpdating: false, + }); + }); + }); + }); + + describe('clearLogRetentionUpdating', () => { + describe('isLogRetentionUpdating', () => { + it('resets isLogRetentionUpdating to false', () => { + mount({ + isLogRetentionUpdating: true, + }); + + LogRetentionLogic.actions.clearLogRetentionUpdating(); + + expect(LogRetentionLogic.values).toEqual({ + ...DEFAULT_VALUES, + isLogRetentionUpdating: false, + }); + }); + }); + }); + + describe('updateLogRetention', () => { + describe('logRetention', () => { + it('updates the logRetention values that are passed', () => { + mount({ + logRetention: {}, + }); + + LogRetentionLogic.actions.updateLogRetention({ + api: { + disabledAt: null, + enabled: true, + retentionPolicy: null, + }, + analytics: { + disabledAt: null, + enabled: true, + retentionPolicy: null, + }, + }); + + expect(LogRetentionLogic.values).toEqual({ + ...DEFAULT_VALUES, + logRetention: { + api: { + disabledAt: null, + enabled: true, + retentionPolicy: null, + }, + analytics: { + disabledAt: null, + enabled: true, + retentionPolicy: null, + }, + }, + }); + }); + }); + }); + + describe('saveLogRetention', () => { + beforeEach(() => { + mount(); + jest.spyOn(LogRetentionLogic.actions, 'clearLogRetentionUpdating'); + }); + + describe('openedModal', () => { + it('should be reset to null', () => { + mount({ + openedModal: LogRetentionOptions.Analytics, + }); + + LogRetentionLogic.actions.saveLogRetention(LogRetentionOptions.Analytics, true); + + expect(LogRetentionLogic.values).toEqual({ + ...DEFAULT_VALUES, + openedModal: null, + }); + }); + }); + + it('will call an API endpoint and update log retention', async () => { + jest.spyOn(LogRetentionLogic.actions, 'updateLogRetention'); + const promise = Promise.resolve(TYPICAL_SERVER_LOG_RETENTION); + http.put.mockReturnValue(promise); + + LogRetentionLogic.actions.saveLogRetention(LogRetentionOptions.Analytics, true); + + expect(http.put).toHaveBeenCalledWith('/api/app_search/log_settings', { + body: JSON.stringify({ + analytics: { + enabled: true, + }, + }), + }); + + await promise; + expect(LogRetentionLogic.actions.updateLogRetention).toHaveBeenCalledWith( + TYPICAL_CLIENT_LOG_RETENTION + ); + + expect(LogRetentionLogic.actions.clearLogRetentionUpdating).toHaveBeenCalled(); + }); + + it('handles errors', async () => { + const promise = Promise.reject('An error occured'); + http.put.mockReturnValue(promise); + + LogRetentionLogic.actions.saveLogRetention(LogRetentionOptions.Analytics, true); + + try { + await promise; + } catch { + // Do nothing + } + expect(flashAPIErrors).toHaveBeenCalledWith('An error occured'); + expect(LogRetentionLogic.actions.clearLogRetentionUpdating).toHaveBeenCalled(); + }); + }); + + describe('toggleLogRetention', () => { + describe('isLogRetentionUpdating', () => { + it('sets isLogRetentionUpdating to true', () => { + mount({ + isLogRetentionUpdating: false, + }); + + LogRetentionLogic.actions.toggleLogRetention(LogRetentionOptions.Analytics); + + expect(LogRetentionLogic.values).toEqual({ + ...DEFAULT_VALUES, + isLogRetentionUpdating: true, + }); + }); + }); + + it('will call setOpenedModal if already enabled', () => { + mount({ + logRetention: { + [LogRetentionOptions.Analytics]: { + enabled: true, + }, + }, + }); + jest.spyOn(LogRetentionLogic.actions, 'setOpenedModal'); + + LogRetentionLogic.actions.toggleLogRetention(LogRetentionOptions.Analytics); + + expect(LogRetentionLogic.actions.setOpenedModal).toHaveBeenCalledWith( + LogRetentionOptions.Analytics + ); + }); + }); + + describe('fetchLogRetention', () => { + describe('isLogRetentionUpdating', () => { + it('sets isLogRetentionUpdating to true', () => { + mount({ + isLogRetentionUpdating: false, + }); + + LogRetentionLogic.actions.fetchLogRetention(); + + expect(LogRetentionLogic.values).toEqual({ + ...DEFAULT_VALUES, + isLogRetentionUpdating: true, + }); + }); + }); + + it('will call an API endpoint and update log retention', async () => { + mount(); + jest.spyOn(LogRetentionLogic.actions, 'clearLogRetentionUpdating'); + jest + .spyOn(LogRetentionLogic.actions, 'updateLogRetention') + .mockImplementationOnce(() => {}); + + const promise = Promise.resolve(TYPICAL_SERVER_LOG_RETENTION); + http.get.mockReturnValue(promise); + + LogRetentionLogic.actions.fetchLogRetention(); + + expect(http.get).toHaveBeenCalledWith('/api/app_search/log_settings'); + await promise; + expect(LogRetentionLogic.actions.updateLogRetention).toHaveBeenCalledWith( + TYPICAL_CLIENT_LOG_RETENTION + ); + + expect(LogRetentionLogic.actions.clearLogRetentionUpdating).toHaveBeenCalled(); + }); + + it('handles errors', async () => { + mount(); + jest.spyOn(LogRetentionLogic.actions, 'clearLogRetentionUpdating'); + const promise = Promise.reject('An error occured'); + http.get.mockReturnValue(promise); + + LogRetentionLogic.actions.fetchLogRetention(); + + try { + await promise; + } catch { + // Do nothing + } + expect(flashAPIErrors).toHaveBeenCalledWith('An error occured'); + expect(LogRetentionLogic.actions.clearLogRetentionUpdating).toHaveBeenCalled(); + }); + }); + + it('will call saveLogRetention if NOT already enabled', () => { + mount({ + logRetention: { + [LogRetentionOptions.Analytics]: { + enabled: false, + }, + }, + }); + jest.spyOn(LogRetentionLogic.actions, 'saveLogRetention'); + + LogRetentionLogic.actions.toggleLogRetention(LogRetentionOptions.Analytics); + + expect(LogRetentionLogic.actions.saveLogRetention).toHaveBeenCalledWith( + LogRetentionOptions.Analytics, + true + ); + }); + + it('will do nothing if logRetention option is not yet set', () => { + mount({ + logRetention: {}, + }); + jest.spyOn(LogRetentionLogic.actions, 'saveLogRetention'); + jest.spyOn(LogRetentionLogic.actions, 'setOpenedModal'); + + LogRetentionLogic.actions.toggleLogRetention(LogRetentionOptions.API); + + expect(LogRetentionLogic.actions.saveLogRetention).not.toHaveBeenCalled(); + expect(LogRetentionLogic.actions.setOpenedModal).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/log_retention_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/log_retention_logic.ts new file mode 100644 index 0000000000000..31fc41213492d --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/log_retention_logic.ts @@ -0,0 +1,117 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { kea, MakeLogicType } from 'kea'; + +import { LogRetentionOptions, LogRetention, LogRetentionServer } from './types'; +import { HttpLogic } from '../../../../shared/http'; +import { flashAPIErrors } from '../../../../shared/flash_messages'; +import { convertLogRetentionFromServerToClient } from './utils/convert_log_retention'; + +interface LogRetentionActions { + clearLogRetentionUpdating(): { value: boolean }; + closeModals(): { value: boolean }; + fetchLogRetention(): { value: boolean }; + saveLogRetention( + option: LogRetentionOptions, + enabled: boolean + ): { option: LogRetentionOptions; enabled: boolean }; + setOpenedModal(option: LogRetentionOptions): { option: LogRetentionOptions }; + toggleLogRetention(option: LogRetentionOptions): { option: LogRetentionOptions }; + updateLogRetention(logRetention: LogRetention): { logRetention: LogRetention }; +} + +interface LogRetentionValues { + logRetention: LogRetention | null; + isLogRetentionUpdating: boolean; + openedModal: LogRetentionOptions | null; +} + +export const LogRetentionLogic = kea>({ + path: ['enterprise_search', 'app_search', 'log_retention_logic'], + actions: () => ({ + clearLogRetentionUpdating: true, + closeModals: true, + fetchLogRetention: true, + saveLogRetention: (option, enabled) => ({ enabled, option }), + setOpenedModal: (option) => ({ option }), + toggleLogRetention: (option) => ({ option }), + updateLogRetention: (logRetention) => ({ logRetention }), + }), + reducers: () => ({ + logRetention: [ + null, + { + updateLogRetention: (_, { logRetention }) => logRetention, + }, + ], + isLogRetentionUpdating: [ + false, + { + clearLogRetentionUpdating: () => false, + closeModals: () => false, + fetchLogRetention: () => true, + toggleLogRetention: () => true, + }, + ], + openedModal: [ + null, + { + closeModals: () => null, + saveLogRetention: () => null, + setOpenedModal: (_, { option }) => option, + }, + ], + }), + listeners: ({ actions, values }) => ({ + fetchLogRetention: async () => { + try { + const { http } = HttpLogic.values; + const response = await http.get('/api/app_search/log_settings'); + actions.updateLogRetention( + convertLogRetentionFromServerToClient(response as LogRetentionServer) + ); + } catch (e) { + flashAPIErrors(e); + } finally { + actions.clearLogRetentionUpdating(); + } + }, + saveLogRetention: async ({ enabled, option }) => { + const updateData = { [option]: { enabled } }; + + try { + const { http } = HttpLogic.values; + const response = await http.put('/api/app_search/log_settings', { + body: JSON.stringify(updateData), + }); + actions.updateLogRetention( + convertLogRetentionFromServerToClient(response as LogRetentionServer) + ); + } catch (e) { + flashAPIErrors(e); + } finally { + actions.clearLogRetentionUpdating(); + } + }, + toggleLogRetention: ({ option }) => { + const logRetention = values.logRetention?.[option]; + + // If the user has found a way to call this before we've retrieved + // log retention settings from the server, short circuit this and return early + if (!logRetention) { + return; + } + + const optionIsAlreadyEnabled = logRetention.enabled; + if (optionIsAlreadyEnabled) { + actions.setOpenedModal(option); + } else { + actions.saveLogRetention(option, true); + } + }, + }), +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/log_retention_panel.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/log_retention_panel.test.tsx new file mode 100644 index 0000000000000..c44418a1eb5a2 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/log_retention_panel.test.tsx @@ -0,0 +1,187 @@ +/* + * 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 '../../../../__mocks__/kea.mock'; +import '../../../../__mocks__/shallow_useeffect.mock'; + +import { setMockActions, setMockValues } from '../../../../__mocks__'; + +import React from 'react'; +import { shallow } from 'enzyme'; + +import { LogRetentionPanel } from './log_retention_panel'; +import { LogRetention } from './types'; + +describe('', () => { + const actions = { + fetchLogRetention: jest.fn(), + toggleLogRetention: jest.fn(), + }; + + beforeEach(() => { + jest.clearAllMocks(); + setMockActions(actions); + }); + + it('renders', () => { + const logRetentionPanel = shallow(); + expect(logRetentionPanel.find('[data-test-subj="LogRetentionPanel"]')).toHaveLength(1); + }); + + it('initializes data on mount', () => { + shallow(); + expect(actions.fetchLogRetention).toHaveBeenCalledTimes(1); + }); + + it('renders Analytics switch off when analytics log retention is false in LogRetentionLogic ', () => { + setMockValues({ + isLogRetentionUpdating: false, + logRetention: mockLogRetention({ + analytics: { + enabled: false, + }, + }), + }); + + const logRetentionPanel = shallow(); + expect( + logRetentionPanel.find('[data-test-subj="LogRetentionPanelAnalyticsSwitch"]').prop('checked') + ).toEqual(false); + }); + + it('renders Analytics switch on when analyticsLogRetention is true in LogRetentionLogic ', () => { + setMockValues({ + isLogRetentionUpdating: false, + logRetention: mockLogRetention({ + analytics: { + enabled: true, + }, + }), + }); + + const logRetentionPanel = shallow(); + expect( + logRetentionPanel.find('[data-test-subj="LogRetentionPanelAnalyticsSwitch"]').prop('checked') + ).toEqual(true); + }); + + it('renders API switch off when apiLogRetention is false in LogRetentionLogic ', () => { + setMockValues({ + isLogRetentionUpdating: false, + logRetention: mockLogRetention({ + api: { + enabled: false, + }, + }), + }); + + const logRetentionPanel = shallow(); + expect( + logRetentionPanel.find('[data-test-subj="LogRetentionPanelAPISwitch"]').prop('checked') + ).toEqual(false); + }); + + it('renders API switch on when apiLogRetention is true in LogRetentionLogic ', () => { + setMockValues({ + isLogRetentionUpdating: false, + logRetention: mockLogRetention({ + api: { + enabled: true, + }, + }), + }); + + const logRetentionPanel = shallow(); + expect( + logRetentionPanel.find('[data-test-subj="LogRetentionPanelAPISwitch"]').prop('checked') + ).toEqual(true); + }); + + it('enables both switches when isLogRetentionUpdating is false', () => { + setMockValues({ + isLogRetentionUpdating: false, + logRetention: mockLogRetention({}), + }); + const logRetentionPanel = shallow(); + expect( + logRetentionPanel.find('[data-test-subj="LogRetentionPanelAnalyticsSwitch"]').prop('disabled') + ).toEqual(false); + expect( + logRetentionPanel.find('[data-test-subj="LogRetentionPanelAPISwitch"]').prop('disabled') + ).toEqual(false); + }); + + it('disables both switches when isLogRetentionUpdating is true', () => { + setMockValues({ + isLogRetentionUpdating: true, + logRetention: mockLogRetention({}), + }); + const logRetentionPanel = shallow(); + + expect( + logRetentionPanel.find('[data-test-subj="LogRetentionPanelAnalyticsSwitch"]').prop('disabled') + ).toEqual(true); + expect( + logRetentionPanel.find('[data-test-subj="LogRetentionPanelAPISwitch"]').prop('disabled') + ).toEqual(true); + }); + + it('calls toggleLogRetention when analytics log retention option is changed', () => { + setMockValues({ + isLogRetentionUpdating: false, + logRetention: mockLogRetention({ + analytics: { + enabled: false, + }, + }), + }); + const logRetentionPanel = shallow(); + logRetentionPanel + .find('[data-test-subj="LogRetentionPanelAnalyticsSwitch"]') + .simulate('change'); + expect(actions.toggleLogRetention).toHaveBeenCalledWith('analytics'); + }); + + it('calls toggleLogRetention when api log retention option is changed', () => { + setMockValues({ + isLogRetentionUpdating: false, + logRetention: mockLogRetention({ + analytics: { + enabled: false, + }, + }), + }); + const logRetentionPanel = shallow(); + logRetentionPanel.find('[data-test-subj="LogRetentionPanelAPISwitch"]').simulate('change'); + expect(actions.toggleLogRetention).toHaveBeenCalledWith('api'); + }); +}); + +const mockLogRetention = (logRetention: Partial) => { + const baseLogRetention = { + analytics: { + disabledAt: null, + enabled: true, + retentionPolicy: { isDefault: true, minAgeDays: 180 }, + }, + api: { + disabledAt: null, + enabled: true, + retentionPolicy: { isDefault: true, minAgeDays: 180 }, + }, + }; + + return { + analytics: { + ...baseLogRetention.analytics, + ...logRetention.analytics, + }, + api: { + ...baseLogRetention.api, + ...logRetention.api, + }, + }; +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/log_retention_panel.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/log_retention_panel.tsx new file mode 100644 index 0000000000000..23572074b3c69 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/log_retention_panel.tsx @@ -0,0 +1,110 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useEffect } from 'react'; +import { i18n } from '@kbn/i18n'; + +import { EuiLink, EuiSpacer, EuiSwitch, EuiText, EuiTextColor, EuiTitle } from '@elastic/eui'; +import { useActions, useValues } from 'kea'; + +import { LogRetentionLogic } from './log_retention_logic'; +import { AnalyticsLogRetentionMessage, ApiLogRetentionMessage } from './messaging'; +import { LogRetentionOptions } from './types'; + +export const LogRetentionPanel: React.FC = () => { + const { toggleLogRetention, fetchLogRetention } = useActions(LogRetentionLogic); + + const { logRetention, isLogRetentionUpdating } = useValues(LogRetentionLogic); + + const hasILM = logRetention !== null; + const analyticsLogRetentionSettings = logRetention?.[LogRetentionOptions.Analytics]; + const apiLogRetentionSettings = logRetention?.[LogRetentionOptions.API]; + + useEffect(() => { + fetchLogRetention(); + }, []); + + return ( +
+ +

+ {i18n.translate('xpack.enterpriseSearch.appSearch.settings.logRetention.title', { + defaultMessage: 'Log Retention', + })} +

+
+ +

+ {i18n.translate('xpack.enterpriseSearch.appSearch.settings.logRetention.description', { + defaultMessage: 'Manage the default write settings for API Logs and Analytics.', + })}{' '} + + {i18n.translate('xpack.enterpriseSearch.appSearch.settings.logRetention.learnMore', { + defaultMessage: 'Learn more about retention settings.', + })} + +

+
+ + + + + {i18n.translate( + 'xpack.enterpriseSearch.appSearch.settings.logRetention.analytics.label', + { + defaultMessage: 'Analytics Logs', + } + )} + + {': '} + {hasILM && ( + + + + )} + + } + checked={!!analyticsLogRetentionSettings?.enabled} + onChange={() => toggleLogRetention(LogRetentionOptions.Analytics)} + disabled={isLogRetentionUpdating} + data-test-subj="LogRetentionPanelAnalyticsSwitch" + /> + + + + + + {i18n.translate( + 'xpack.enterpriseSearch.appSearch.settings.logRetention.api.label', + { + defaultMessage: 'API Logs', + } + )} + + {': '} + {hasILM && ( + + + + )} + + } + checked={!!apiLogRetentionSettings?.enabled} + onChange={() => toggleLogRetention(LogRetentionOptions.API)} + disabled={isLogRetentionUpdating} + data-test-subj="LogRetentionPanelAPISwitch" + /> + +
+ ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/messaging/constants.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/messaging/constants.ts new file mode 100644 index 0000000000000..6db087e092697 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/messaging/constants.ts @@ -0,0 +1,119 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +import { LogRetentionMessages } from './types'; +import { renderLogRetentionDate } from '.'; + +const ANALYTICS_NO_LOGGING = i18n.translate( + 'xpack.enterpriseSearch.appSearch.settings.logRetention.analytics.noLogging', + { + defaultMessage: 'Analytics collection has been disabled for all engines.', + } +); + +const ANALYTICS_NO_LOGGING_COLLECTED = (disabledAt: string) => + i18n.translate( + 'xpack.enterpriseSearch.appSearch.settings.logRetention.analytics.noLogging.collected', + { + defaultMessage: 'The last date analytics were collected was {disabledAt}.', + values: { disabledAt }, + } + ); + +const ANALYTICS_NO_LOGGING_NOT_COLLECTED = i18n.translate( + 'xpack.enterpriseSearch.appSearch.settings.logRetention.analytics.noLogging.notCollected', + { + defaultMessage: 'There are no analytics collected.', + } +); + +const ANALYTICS_ILM_DISABLED = i18n.translate( + 'xpack.enterpriseSearch.appSearch.settings.logRetention.analytics.ilmDisabled', + { + defaultMessage: "App Search isn't managing analytics retention.", + } +); + +const ANALYTICS_CUSTOM_POLICY = i18n.translate( + 'xpack.enterpriseSearch.appSearch.settings.logRetention.analytics.customPolicy', + { + defaultMessage: 'You have a custom analytics retention policy.', + } +); + +const ANALYTICS_STORED = (minAgeDays: number | null | undefined) => + i18n.translate('xpack.enterpriseSearch.appSearch.settings.logRetention.analytics.stored', { + defaultMessage: 'Your analytics are being stored for at least {minAgeDays} days.', + values: { minAgeDays }, + }); + +const API_NO_LOGGING = i18n.translate( + 'xpack.enterpriseSearch.appSearch.settings.logRetention.api.noLogging', + { + defaultMessage: 'API logging has been disabled for all engines.', + } +); + +const API_NO_LOGGING_COLLECTED = (disabledAt: string) => + i18n.translate('xpack.enterpriseSearch.appSearch.settings.logRetention.api.noLogging.collected', { + defaultMessage: 'The last date logs were collected was {disabledAt}.', + values: { disabledAt }, + }); + +const API_NO_LOGGING_NOT_COLLECTED = i18n.translate( + 'xpack.enterpriseSearch.appSearch.settings.logRetention.api.noLogging.notCollected', + { + defaultMessage: 'There are no logs collected.', + } +); + +const API_ILM_DISABLED = i18n.translate( + 'xpack.enterpriseSearch.appSearch.settings.logRetention.api.ilmDisabled', + { + defaultMessage: "App Search isn't managing API log retention.", + } +); + +const API_CUSTOM_POLICY = i18n.translate( + 'xpack.enterpriseSearch.appSearch.settings.logRetention.api.customPolicy', + { + defaultMessage: 'You have a custom API log retention policy.', + } +); + +const API_STORED = (minAgeDays: number | null | undefined) => + i18n.translate('xpack.enterpriseSearch.appSearch.settings.logRetention.api.stored', { + defaultMessage: 'Your logs are being stored for at least {minAgeDays} days.', + values: { minAgeDays }, + }); + +export const ANALYTICS_MESSAGES: LogRetentionMessages = { + noLogging: (_, logRetentionSettings) => + `${ANALYTICS_NO_LOGGING} ${ + logRetentionSettings.disabledAt + ? ANALYTICS_NO_LOGGING_COLLECTED(renderLogRetentionDate(logRetentionSettings.disabledAt)) + : ANALYTICS_NO_LOGGING_NOT_COLLECTED + }`, + ilmDisabled: ANALYTICS_ILM_DISABLED, + customPolicy: ANALYTICS_CUSTOM_POLICY, + defaultPolicy: (_, logRetentionSettings) => + ANALYTICS_STORED(logRetentionSettings.retentionPolicy?.minAgeDays), +}; + +export const API_MESSAGES: LogRetentionMessages = { + noLogging: (_, logRetentionSettings) => + `${API_NO_LOGGING} ${ + logRetentionSettings.disabledAt + ? API_NO_LOGGING_COLLECTED(renderLogRetentionDate(logRetentionSettings.disabledAt)) + : API_NO_LOGGING_NOT_COLLECTED + }`, + ilmDisabled: API_ILM_DISABLED, + customPolicy: API_CUSTOM_POLICY, + defaultPolicy: (_, logRetentionSettings) => + API_STORED(logRetentionSettings.retentionPolicy?.minAgeDays), +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/messaging/determine_tooltip_content.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/messaging/determine_tooltip_content.test.ts new file mode 100644 index 0000000000000..fbc2ccfbc8a52 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/messaging/determine_tooltip_content.test.ts @@ -0,0 +1,168 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { determineTooltipContent } from './determine_tooltip_content'; +import { ANALYTICS_MESSAGES, API_MESSAGES } from './constants'; + +describe('determineTooltipContent', () => { + const BASE_SETTINGS = { + disabledAt: null, + enabled: true, + retentionPolicy: null, + }; + + it('will return nothing if settings are not provided', () => { + expect(determineTooltipContent(ANALYTICS_MESSAGES, true)).toBeUndefined(); + }); + + describe('analytics messages', () => { + describe('when analytics logs are enabled', () => { + describe("and they're using the default policy", () => { + it('will render a retention policy message', () => { + expect( + determineTooltipContent(ANALYTICS_MESSAGES, true, { + ...BASE_SETTINGS, + enabled: true, + retentionPolicy: { + isDefault: true, + minAgeDays: 7, + }, + }) + ).toEqual('Your analytics are being stored for at least 7 days.'); + }); + }); + + describe('and there is a custom policy', () => { + it('will render a retention policy message', () => { + expect( + determineTooltipContent(ANALYTICS_MESSAGES, true, { + ...BASE_SETTINGS, + enabled: true, + retentionPolicy: { + isDefault: false, + minAgeDays: 7, + }, + }) + ).toEqual('You have a custom analytics retention policy.'); + }); + }); + }); + + describe('when analytics logs are disabled', () => { + describe('and there is no disabledAt date', () => { + it('will render a no logging message', () => { + expect( + determineTooltipContent(ANALYTICS_MESSAGES, true, { + ...BASE_SETTINGS, + enabled: false, + disabledAt: null, + }) + ).toEqual( + 'Analytics collection has been disabled for all engines. There are no analytics collected.' + ); + }); + }); + + describe('and there is a disabledAt date', () => { + it('will render a no logging message', () => { + expect( + determineTooltipContent(ANALYTICS_MESSAGES, true, { + ...BASE_SETTINGS, + enabled: false, + disabledAt: 'Thu, 05 Nov 2020 18:57:28 +0000', + }) + ).toEqual( + 'Analytics collection has been disabled for all engines. The last date analytics were collected was November 5, 2020.' + ); + }); + }); + }); + + describe('when ilm is disabled entirely', () => { + it('will render a no logging message', () => { + expect( + determineTooltipContent(ANALYTICS_MESSAGES, false, { + ...BASE_SETTINGS, + enabled: true, + }) + ).toEqual("App Search isn't managing analytics retention."); + }); + }); + }); + + describe('api messages', () => { + describe('when analytics logs are enabled', () => { + describe("and they're using the default policy", () => { + it('will render a retention policy message', () => { + expect( + determineTooltipContent(API_MESSAGES, true, { + ...BASE_SETTINGS, + enabled: true, + retentionPolicy: { + isDefault: true, + minAgeDays: 7, + }, + }) + ).toEqual('Your logs are being stored for at least 7 days.'); + }); + }); + + describe('and there is a custom policy', () => { + it('will render a retention policy message', () => { + expect( + determineTooltipContent(API_MESSAGES, true, { + ...BASE_SETTINGS, + enabled: true, + retentionPolicy: { + isDefault: false, + minAgeDays: 7, + }, + }) + ).toEqual('You have a custom API log retention policy.'); + }); + }); + }); + + describe('when analytics logs are disabled', () => { + describe('and there is no disabledAt date', () => { + it('will render a no logging message', () => { + expect( + determineTooltipContent(API_MESSAGES, true, { + ...BASE_SETTINGS, + enabled: false, + disabledAt: null, + }) + ).toEqual('API logging has been disabled for all engines. There are no logs collected.'); + }); + }); + + describe('and there is a disabledAt date', () => { + it('will render a no logging message', () => { + expect( + determineTooltipContent(API_MESSAGES, true, { + ...BASE_SETTINGS, + enabled: false, + disabledAt: 'Thu, 05 Nov 2020 18:57:28 +0000', + }) + ).toEqual( + 'API logging has been disabled for all engines. The last date logs were collected was November 5, 2020.' + ); + }); + }); + }); + + describe('when ilm is disabled entirely', () => { + it('will render a no logging message', () => { + expect( + determineTooltipContent(API_MESSAGES, false, { + ...BASE_SETTINGS, + enabled: true, + }) + ).toEqual("App Search isn't managing API log retention."); + }); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/messaging/determine_tooltip_content.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/messaging/determine_tooltip_content.ts new file mode 100644 index 0000000000000..385831dc511da --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/messaging/determine_tooltip_content.ts @@ -0,0 +1,46 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { LogRetentionSettings } from '../types'; +import { TMessageStringOrFunction, LogRetentionMessages } from './types'; + +export const determineTooltipContent = ( + messages: LogRetentionMessages, + ilmEnabled: boolean, + logRetentionSettings?: LogRetentionSettings +) => { + if (typeof logRetentionSettings === 'undefined') { + return; + } + + const renderOrReturnMessage = (message: TMessageStringOrFunction) => { + if (typeof message === 'function') { + return message(ilmEnabled, logRetentionSettings); + } + return message; + }; + + if (!logRetentionSettings.enabled) { + return renderOrReturnMessage(messages.noLogging); + } + if (logRetentionSettings.enabled && !ilmEnabled) { + return renderOrReturnMessage(messages.ilmDisabled); + } + if ( + logRetentionSettings.enabled && + ilmEnabled && + !logRetentionSettings.retentionPolicy?.isDefault + ) { + return renderOrReturnMessage(messages.customPolicy); + } + if ( + logRetentionSettings.enabled && + ilmEnabled && + logRetentionSettings.retentionPolicy?.isDefault + ) { + return renderOrReturnMessage(messages.defaultPolicy); + } +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/messaging/index.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/messaging/index.test.tsx new file mode 100644 index 0000000000000..b65ffc04ad700 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/messaging/index.test.tsx @@ -0,0 +1,74 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import '../../../../../__mocks__/kea.mock'; +import { setMockValues } from '../../../../../__mocks__'; + +import React from 'react'; +import { shallow } from 'enzyme'; + +import { AnalyticsLogRetentionMessage, ApiLogRetentionMessage, renderLogRetentionDate } from '.'; + +describe('LogRetentionMessaging', () => { + const LOG_RETENTION = { + analytics: { + disabledAt: null, + enabled: true, + retentionPolicy: { isDefault: true, minAgeDays: 180 }, + }, + api: { + disabledAt: null, + enabled: true, + retentionPolicy: { isDefault: true, minAgeDays: 180 }, + }, + }; + + describe('renderLogRetentionDate', () => { + it('renders a formatted date', () => { + expect(renderLogRetentionDate('Thu, 05 Nov 2020 18:57:28 +0000')).toEqual('November 5, 2020'); + }); + }); + + describe('AnalyticsLogRetentionMessage', () => { + it('renders', () => { + setMockValues({ + ilmEnabled: true, + logRetention: LOG_RETENTION, + }); + const wrapper = shallow(); + expect(wrapper.text()).toEqual('Your analytics are being stored for at least 180 days.'); + }); + + it('renders nothing if logRetention is null', () => { + setMockValues({ + ilmEnabled: true, + logRetention: null, + }); + const wrapper = shallow(); + expect(wrapper.isEmptyRender()).toEqual(true); + }); + }); + + describe('ApiLogRetentionMessage', () => { + it('renders', () => { + setMockValues({ + ilmEnabled: true, + logRetention: LOG_RETENTION, + }); + const wrapper = shallow(); + expect(wrapper.text()).toEqual('Your logs are being stored for at least 180 days.'); + }); + + it('renders nothing if logRetention is null', () => { + setMockValues({ + ilmEnabled: true, + logRetention: null, + }); + const wrapper = shallow(); + expect(wrapper.isEmptyRender()).toEqual(true); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/messaging/index.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/messaging/index.tsx new file mode 100644 index 0000000000000..21267738f61ad --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/messaging/index.tsx @@ -0,0 +1,46 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; + +import { useValues } from 'kea'; +import moment from 'moment'; + +import { AppLogic } from '../../../../app_logic'; +import { LogRetentionLogic } from '../log_retention_logic'; +import { LogRetentionOptions } from '../types'; + +import { determineTooltipContent } from './determine_tooltip_content'; +import { ANALYTICS_MESSAGES, API_MESSAGES } from './constants'; + +export const renderLogRetentionDate = (dateString: string) => + moment(dateString).format('MMMM D, YYYY'); + +export const AnalyticsLogRetentionMessage: React.FC = () => { + const { ilmEnabled } = useValues(AppLogic); + const { logRetention } = useValues(LogRetentionLogic); + if (!logRetention) return null; + + return ( + <> + {determineTooltipContent( + ANALYTICS_MESSAGES, + ilmEnabled, + logRetention[LogRetentionOptions.Analytics] + )} + + ); +}; + +export const ApiLogRetentionMessage: React.FC = () => { + const { ilmEnabled } = useValues(AppLogic); + const { logRetention } = useValues(LogRetentionLogic); + if (!logRetention) return null; + + return ( + <>{determineTooltipContent(API_MESSAGES, ilmEnabled, logRetention[LogRetentionOptions.API])} + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/messaging/types.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/messaging/types.ts new file mode 100644 index 0000000000000..8f86e397dfe26 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/messaging/types.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { LogRetentionSettings } from '../types'; + +export type TMessageStringOrFunction = + | string + | ((ilmEnabled: boolean, logRetentionSettings: LogRetentionSettings) => string); + +export interface LogRetentionMessages { + noLogging: TMessageStringOrFunction; + ilmDisabled: TMessageStringOrFunction; + customPolicy: TMessageStringOrFunction; + defaultPolicy: TMessageStringOrFunction; +} diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/types.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/types.ts new file mode 100644 index 0000000000000..f802efc72ac4d --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/types.ts @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export enum LogRetentionOptions { + Analytics = 'analytics', + API = 'api', +} + +export interface LogRetention { + [LogRetentionOptions.Analytics]: LogRetentionSettings; + [LogRetentionOptions.API]: LogRetentionSettings; +} + +export interface LogRetentionPolicy { + isDefault: boolean; + minAgeDays: number | null; +} + +export interface LogRetentionSettings { + disabledAt?: string | null; + enabled?: boolean; + retentionPolicy?: LogRetentionPolicy | null; +} + +export interface LogRetentionServer { + [LogRetentionOptions.Analytics]: LogRetentionServerSettings; + [LogRetentionOptions.API]: LogRetentionServerSettings; +} + +export interface LogRetentionServerPolicy { + is_default: boolean; + min_age_days: number | null; +} + +export interface LogRetentionServerSettings { + disabled_at: string | null; + enabled: boolean; + retention_policy: LogRetentionServerPolicy | null; +} diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/utils/convert_log_retention.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/utils/convert_log_retention.test.ts new file mode 100644 index 0000000000000..b49b2afe50831 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/utils/convert_log_retention.test.ts @@ -0,0 +1,64 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { convertLogRetentionFromServerToClient } from './convert_log_retention'; + +describe('convertLogRetentionFromServerToClient', () => { + it('converts log retention from server to client', () => { + expect( + convertLogRetentionFromServerToClient({ + analytics: { + disabled_at: null, + enabled: true, + retention_policy: { is_default: true, min_age_days: 180 }, + }, + api: { + disabled_at: null, + enabled: true, + retention_policy: { is_default: true, min_age_days: 180 }, + }, + }) + ).toEqual({ + analytics: { + disabledAt: null, + enabled: true, + retentionPolicy: { isDefault: true, minAgeDays: 180 }, + }, + api: { + disabledAt: null, + enabled: true, + retentionPolicy: { isDefault: true, minAgeDays: 180 }, + }, + }); + }); + + it('handles null retention policies and null min_age_days', () => { + expect( + convertLogRetentionFromServerToClient({ + analytics: { + disabled_at: null, + enabled: true, + retention_policy: null, + }, + api: { + disabled_at: null, + enabled: true, + retention_policy: { is_default: true, min_age_days: null }, + }, + }) + ).toEqual({ + analytics: { + disabledAt: null, + enabled: true, + retentionPolicy: null, + }, + api: { + disabledAt: null, + enabled: true, + retentionPolicy: { isDefault: true, minAgeDays: null }, + }, + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/utils/convert_log_retention.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/utils/convert_log_retention.ts new file mode 100644 index 0000000000000..f960bb8f193b4 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/utils/convert_log_retention.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. + */ + +import { + LogRetentionOptions, + LogRetention, + LogRetentionPolicy, + LogRetentionServer, + LogRetentionServerPolicy, + LogRetentionServerSettings, + LogRetentionSettings, +} from '../types'; + +export const convertLogRetentionFromServerToClient = ( + logRetention: LogRetentionServer +): LogRetention => ({ + [LogRetentionOptions.Analytics]: convertLogRetentionSettingsFromServerToClient( + logRetention[LogRetentionOptions.Analytics] + ), + [LogRetentionOptions.API]: convertLogRetentionSettingsFromServerToClient( + logRetention[LogRetentionOptions.API] + ), +}); + +const convertLogRetentionSettingsFromServerToClient = ({ + disabled_at: disabledAt, + enabled, + retention_policy: retentionPolicy, +}: LogRetentionServerSettings): LogRetentionSettings => ({ + disabledAt, + enabled, + retentionPolicy: + retentionPolicy === null ? null : convertLogRetentionPolicyFromServerToClient(retentionPolicy), +}); + +const convertLogRetentionPolicyFromServerToClient = ({ + min_age_days: minAgeDays, + is_default: isDefault, +}: LogRetentionServerPolicy): LogRetentionPolicy => ({ + isDefault, + minAgeDays, +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/settings.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/settings.tsx index e5e86f3e39734..7bd3683919bc1 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/settings.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/settings.tsx @@ -5,37 +5,37 @@ */ import React from 'react'; - -import { i18n } from '@kbn/i18n'; -import { EuiPageHeader, EuiPageHeaderSection, EuiPageContentBody, EuiTitle } from '@elastic/eui'; +import { + EuiPageHeader, + EuiPageHeaderSection, + EuiPageContent, + EuiPageContentBody, + EuiTitle, +} from '@elastic/eui'; import { SetAppSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome'; import { FlashMessages } from '../../../shared/flash_messages'; +import { LogRetentionPanel } from './log_retention/log_retention_panel'; + +import { SETTINGS_TITLE } from './'; export const Settings: React.FC = () => { return ( <> - + -

- {i18n.translate('xpack.enterpriseSearch.appSearch.settings.title', { - defaultMessage: 'Settings', - })} -

+

{SETTINGS_TITLE}

- - - + + + + + + ); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/index.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/index.test.tsx index 700b903efe59b..11387734e9f9e 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/index.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/index.test.tsx @@ -16,7 +16,7 @@ import { shallow } from 'enzyme'; import { Layout, SideNav, SideNavLink } from '../shared/layout'; import { SetupGuide } from './components/setup_guide'; import { ErrorConnecting } from './components/error_connecting'; -import { EngineOverview } from './components/engine_overview'; +import { EnginesOverview } from './components/engines'; import { EngineRouter } from './components/engine'; import { AppSearch, AppSearchUnconfigured, AppSearchConfigured, AppSearchNav } from './'; @@ -57,7 +57,7 @@ describe('AppSearchConfigured', () => { expect(wrapper.find(Layout)).toHaveLength(2); expect(wrapper.find(Layout).last().prop('readOnlyMode')).toBeFalsy(); - expect(wrapper.find(EngineOverview)).toHaveLength(1); + expect(wrapper.find(EnginesOverview)).toHaveLength(1); expect(wrapper.find(EngineRouter)).toHaveLength(1); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/index.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/index.tsx index f32b0b256b898..743cf63fb4bc3 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/index.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/index.tsx @@ -8,13 +8,11 @@ import React, { useEffect } from 'react'; import { Route, Redirect, Switch } from 'react-router-dom'; import { useActions, useValues } from 'kea'; -import { i18n } from '@kbn/i18n'; - import { getAppSearchUrl } from '../shared/enterprise_search_url'; import { KibanaLogic } from '../shared/kibana'; import { HttpLogic } from '../shared/http'; import { AppLogic } from './app_logic'; -import { IInitialAppData } from '../../../common/types'; +import { InitialAppData } from '../../../common/types'; import { APP_SEARCH_PLUGIN } from '../../../common/constants'; import { Layout, SideNav, SideNavLink } from '../shared/layout'; @@ -33,11 +31,12 @@ import { import { SetupGuide } from './components/setup_guide'; import { ErrorConnecting } from './components/error_connecting'; import { NotFound } from '../shared/not_found'; -import { EngineOverview } from './components/engine_overview'; -import { Settings } from './components/settings'; -import { Credentials } from './components/credentials'; +import { EnginesOverview, ENGINES_TITLE } from './components/engines'; +import { Settings, SETTINGS_TITLE } from './components/settings'; +import { Credentials, CREDENTIALS_TITLE } from './components/credentials'; +import { ROLE_MAPPINGS_TITLE } from './components/role_mappings'; -export const AppSearch: React.FC = (props) => { +export const AppSearch: React.FC = (props) => { const { config } = useValues(KibanaLogic); return !config.host ? : ; }; @@ -53,7 +52,7 @@ export const AppSearchUnconfigured: React.FC = () => ( ); -export const AppSearchConfigured: React.FC = (props) => { +export const AppSearchConfigured: React.FC = (props) => { const { initializeAppData } = useActions(AppLogic); const { hasInitialized } = useValues(AppLogic); const { errorConnecting, readOnlyMode } = useValues(HttpLogic); @@ -82,7 +81,7 @@ export const AppSearchConfigured: React.FC = (props) => { - + @@ -101,11 +100,11 @@ export const AppSearchConfigured: React.FC = (props) => { ); }; -interface IAppSearchNavProps { +interface AppSearchNavProps { subNav?: React.ReactNode; } -export const AppSearchNav: React.FC = ({ subNav }) => { +export const AppSearchNav: React.FC = ({ subNav }) => { const { myRole: { canViewSettings, canViewAccountCredentials, canViewRoleMappings }, } = useValues(AppLogic); @@ -113,29 +112,15 @@ export const AppSearchNav: React.FC = ({ subNav }) => { return ( - {i18n.translate('xpack.enterpriseSearch.appSearch.nav.engines', { - defaultMessage: 'Engines', - })} + {ENGINES_TITLE} - {canViewSettings && ( - - {i18n.translate('xpack.enterpriseSearch.appSearch.nav.settings', { - defaultMessage: 'Settings', - })} - - )} + {canViewSettings && {SETTINGS_TITLE}} {canViewAccountCredentials && ( - - {i18n.translate('xpack.enterpriseSearch.appSearch.nav.credentials', { - defaultMessage: 'Credentials', - })} - + {CREDENTIALS_TITLE} )} {canViewRoleMappings && ( - {i18n.translate('xpack.enterpriseSearch.appSearch.nav.roleMappings', { - defaultMessage: 'Role Mappings', - })} + {ROLE_MAPPINGS_TITLE} )} diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/types.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/types.ts index 568a0a3365982..7c22a45757a93 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/types.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/types.ts @@ -5,11 +5,5 @@ */ export * from '../../../common/types/app_search'; -export { IRole, TRole, TAbility } from './utils/role'; - -export interface IEngine { - name: string; - type: string; - language: string; - result_fields: object[]; -} +export { Role, RoleTypes, AbilityTypes } from './utils/role'; +export { Engine } from './components/engine/types'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/utils/role/index.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/utils/role/index.ts index a935fa657738c..21a80bc0c208f 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/utils/role/index.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/utils/role/index.ts @@ -4,18 +4,18 @@ * you may not use this file except in compliance with the Elastic License. */ -import { IAccount } from '../../types'; +import { Account } from '../../types'; -export type TRole = 'owner' | 'admin' | 'dev' | 'editor' | 'analyst'; -export type TAbility = 'manage' | 'edit' | 'view'; +export type RoleTypes = 'owner' | 'admin' | 'dev' | 'editor' | 'analyst'; +export type AbilityTypes = 'manage' | 'edit' | 'view'; -export interface IRole { +export interface Role { id: string; - roleType: TRole; - availableRoleTypes: TRole[]; + roleType: RoleTypes; + availableRoleTypes: RoleTypes[]; credentialTypes: string[]; canAccessAllEngines: boolean; - can(action: TAbility, subject: string): boolean; + can(action: AbilityTypes, subject: string): boolean; canViewMetaEngines: boolean; canViewAccountCredentials: boolean; canViewEngineAnalytics: boolean; @@ -48,10 +48,10 @@ export interface IRole { * Transforms the `role` data we receive from the Enterprise Search * server into a more convenient format for front-end use */ -export const getRoleAbilities = (role: IAccount['role']): IRole => { +export const getRoleAbilities = (role: Account['role']): Role => { // Role ability function helpers const myRole = { - can: (action: TAbility, subject: string): boolean => { + can: (action: AbilityTypes, subject: string): boolean => { return ( role?.ability?.manage?.includes(subject) || (Array.isArray(role.ability[action]) && role.ability[action].includes(subject)) @@ -63,8 +63,8 @@ export const getRoleAbilities = (role: IAccount['role']): IRole => { // Clone top-level role props, and move some props out of `ability` and into the top-level for convenience const topLevelProps = { id: role.id, - roleType: role.roleType as TRole, - availableRoleTypes: role.ability.availableRoleTypes as TRole[], + roleType: role.roleType as RoleTypes, + availableRoleTypes: role.ability.availableRoleTypes as RoleTypes[], credentialTypes: role.ability.credentialTypes, }; diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_card/product_card.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_card/product_card.tsx index ee778f49ef5b6..de553acf11f7b 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_card/product_card.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_card/product_card.tsx @@ -16,7 +16,7 @@ import { KibanaLogic } from '../../../shared/kibana'; import './product_card.scss'; -interface IProductCard { +interface ProductCardProps { // Expects product plugin constants (@see common/constants.ts) product: { ID: string; @@ -27,7 +27,7 @@ interface IProductCard { image: string; } -export const ProductCard: React.FC = ({ product, image }) => { +export const ProductCard: React.FC = ({ product, image }) => { const { sendEnterpriseSearchTelemetry } = useActions(TelemetryLogic); const { config } = useValues(KibanaLogic); diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_selector/product_selector.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_selector/product_selector.tsx index 235ececd8b6fc..579baf41f2ce3 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_selector/product_selector.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_selector/product_selector.tsx @@ -30,14 +30,14 @@ import { SetupGuideCta } from '../setup_guide'; import AppSearchImage from '../../assets/app_search.png'; import WorkplaceSearchImage from '../../assets/workplace_search.png'; -interface IProductSelectorProps { +interface ProductSelectorProps { access: { hasAppSearchAccess?: boolean; hasWorkplaceSearchAccess?: boolean; }; } -export const ProductSelector: React.FC = ({ access }) => { +export const ProductSelector: React.FC = ({ access }) => { const { hasAppSearchAccess, hasWorkplaceSearchAccess } = access; const { config } = useValues(KibanaLogic); diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/index.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/index.tsx index 048baabe6a1dd..b562cd577c56b 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/index.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/index.tsx @@ -9,7 +9,7 @@ import { Route, Switch } from 'react-router-dom'; import { useValues } from 'kea'; import { KibanaLogic } from '../shared/kibana'; -import { IInitialAppData } from '../../../common/types'; +import { InitialAppData } from '../../../common/types'; import { HttpLogic } from '../shared/http'; @@ -21,7 +21,7 @@ import { SetupGuide } from './components/setup_guide'; import './index.scss'; -export const EnterpriseSearch: React.FC = ({ access = {} }) => { +export const EnterpriseSearch: React.FC = ({ access = {} }) => { const { errorConnecting } = useValues(HttpLogic); const { config } = useValues(KibanaLogic); diff --git a/x-pack/plugins/enterprise_search/public/applications/index.tsx b/x-pack/plugins/enterprise_search/public/applications/index.tsx index b9c94e351089d..3436df851c8d8 100644 --- a/x-pack/plugins/enterprise_search/public/applications/index.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/index.tsx @@ -14,7 +14,7 @@ import { I18nProvider } from '@kbn/i18n/react'; import { AppMountParameters, CoreStart } from 'src/core/public'; import { PluginsStart, ClientConfigType, ClientData } from '../plugin'; -import { IInitialAppData } from '../../common/types'; +import { InitialAppData } from '../../common/types'; import { mountKibanaLogic } from './shared/kibana'; import { mountLicensingLogic } from './shared/licensing'; @@ -29,7 +29,7 @@ import { externalUrl } from './shared/enterprise_search_url'; */ export const renderApp = ( - App: React.FC, + App: React.FC, { params, core, plugins }: { params: AppMountParameters; core: CoreStart; plugins: PluginsStart }, { config, data }: { config: ClientConfigType; data: ClientData } ) => { diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/flash_messages_logic.ts b/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/flash_messages_logic.ts index 5a05a03adeb6b..7271a1dbbea39 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/flash_messages_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/flash_messages_logic.ts @@ -15,12 +15,12 @@ export interface IFlashMessage { description?: ReactNode; } -export interface IFlashMessagesValues { +interface FlashMessagesValues { messages: IFlashMessage[]; queuedMessages: IFlashMessage[]; historyListener: Function | null; } -export interface IFlashMessagesActions { +interface FlashMessagesActions { setFlashMessages(messages: IFlashMessage | IFlashMessage[]): { messages: IFlashMessage[] }; clearFlashMessages(): void; setQueuedMessages(messages: IFlashMessage | IFlashMessage[]): { messages: IFlashMessage[] }; @@ -31,7 +31,7 @@ export interface IFlashMessagesActions { const convertToArray = (messages: IFlashMessage | IFlashMessage[]) => !Array.isArray(messages) ? [messages] : messages; -export const FlashMessagesLogic = kea>({ +export const FlashMessagesLogic = kea>({ path: ['enterprise_search', 'flash_messages_logic'], actions: { setFlashMessages: (messages) => ({ messages: convertToArray(messages) }), diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/handle_api_errors.ts b/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/handle_api_errors.ts index 2bd04d1d87f7d..c4b287ee08354 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/handle_api_errors.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/handle_api_errors.ts @@ -17,7 +17,7 @@ import { FlashMessagesLogic, IFlashMessage } from './'; * `errors` property in the response's data, which will contain messages we can * display to the user. */ -interface IErrorResponse { +interface ErrorResponse { statusCode: number; error: string; message: string; @@ -25,17 +25,14 @@ interface IErrorResponse { errors: string[]; }; } -interface IOptions { +interface Options { isQueued?: boolean; } /** * Converts API/HTTP errors into user-facing Flash Messages */ -export const flashAPIErrors = ( - error: HttpResponse, - { isQueued }: IOptions = {} -) => { +export const flashAPIErrors = (error: HttpResponse, { isQueued }: Options = {}) => { const defaultErrorMessage = 'An unexpected error occurred'; const errorFlashMessages: IFlashMessage[] = Array.isArray(error?.body?.attributes?.errors) diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/index.ts b/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/index.ts index 21c1a60efa6b7..8792c26f9bad4 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/index.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/index.ts @@ -5,12 +5,6 @@ */ export { FlashMessages } from './flash_messages'; -export { - FlashMessagesLogic, - IFlashMessage, - IFlashMessagesValues, - IFlashMessagesActions, - mountFlashMessagesLogic, -} from './flash_messages_logic'; +export { FlashMessagesLogic, IFlashMessage, mountFlashMessagesLogic } from './flash_messages_logic'; export { flashAPIErrors } from './handle_api_errors'; export { setSuccessMessage, setErrorMessage, setQueuedSuccessMessage } from './set_message_helpers'; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/hidden_text/hidden_text.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/hidden_text/hidden_text.tsx index 9b0833dfce541..69176b8a139e7 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/hidden_text/hidden_text.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/hidden_text/hidden_text.tsx @@ -7,18 +7,18 @@ import React, { useState, ReactElement } from 'react'; import { i18n } from '@kbn/i18n'; -interface IChildrenProps { +interface ChildrenProps { toggle: () => void; isHidden: boolean; hiddenText: React.ReactNode; } -interface IProps { +interface Props { text: string; - children(props: IChildrenProps): ReactElement; + children(props: ChildrenProps): ReactElement; } -export const HiddenText: React.FC = ({ text, children }) => { +export const HiddenText: React.FC = ({ text, children }) => { const [isHidden, toggleIsHidden] = useState(true); const hiddenLabel = i18n.translate('xpack.enterpriseSearch.hiddenText', { diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/http/http_logic.ts b/x-pack/plugins/enterprise_search/public/applications/shared/http/http_logic.ts index d16e507bfb3bc..76cefa4bc5a2c 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/http/http_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/http/http_logic.ts @@ -10,20 +10,20 @@ import { HttpSetup, HttpInterceptorResponseError, HttpResponse } from 'src/core/ import { READ_ONLY_MODE_HEADER } from '../../../../common/constants'; -export interface IHttpValues { +interface HttpValues { http: HttpSetup; httpInterceptors: Function[]; errorConnecting: boolean; readOnlyMode: boolean; } -export interface IHttpActions { +interface HttpActions { initializeHttpInterceptors(): void; setHttpInterceptors(httpInterceptors: Function[]): { httpInterceptors: Function[] }; setErrorConnecting(errorConnecting: boolean): { errorConnecting: boolean }; setReadOnlyMode(readOnlyMode: boolean): { readOnlyMode: boolean }; } -export const HttpLogic = kea>({ +export const HttpLogic = kea>({ path: ['enterprise_search', 'http_logic'], actions: { initializeHttpInterceptors: () => null, @@ -108,12 +108,12 @@ export const HttpLogic = kea>({ /** * Mount/props helper */ -interface IHttpLogicProps { +interface HttpLogicProps { http: HttpSetup; errorConnecting?: boolean; readOnlyMode?: boolean; } -export const mountHttpLogic = (props: IHttpLogicProps) => { +export const mountHttpLogic = (props: HttpLogicProps) => { HttpLogic(props); const unmount = HttpLogic.mount(); return unmount; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/http/index.ts b/x-pack/plugins/enterprise_search/public/applications/shared/http/index.ts index 46a52415f8564..5e41ea032d503 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/http/index.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/http/index.ts @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export { HttpLogic, IHttpValues, IHttpActions, mountHttpLogic } from './http_logic'; +export { HttpLogic, mountHttpLogic } from './http_logic'; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/kibana/kibana_logic.ts b/x-pack/plugins/enterprise_search/public/applications/shared/kibana/kibana_logic.ts index 89ed07f302b03..28f500a2c8a39 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/kibana/kibana_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/kibana/kibana_logic.ts @@ -11,9 +11,9 @@ import { History } from 'history'; import { ApplicationStart, ChromeBreadcrumb } from 'src/core/public'; import { HttpLogic } from '../http'; -import { createHref, ICreateHrefOptions } from '../react_router_helpers'; +import { createHref, CreateHrefOptions } from '../react_router_helpers'; -interface IKibanaLogicProps { +interface KibanaLogicProps { config: { host?: string }; history: History; navigateToUrl: ApplicationStart['navigateToUrl']; @@ -21,17 +21,17 @@ interface IKibanaLogicProps { setDocTitle(title: string): void; renderHeaderActions(HeaderActions: FC): void; } -export interface IKibanaValues extends IKibanaLogicProps { - navigateToUrl(path: string, options?: ICreateHrefOptions): Promise; +export interface KibanaValues extends KibanaLogicProps { + navigateToUrl(path: string, options?: CreateHrefOptions): Promise; } -export const KibanaLogic = kea>({ +export const KibanaLogic = kea>({ path: ['enterprise_search', 'kibana_logic'], reducers: ({ props }) => ({ config: [props.config || {}, {}], history: [props.history, {}], navigateToUrl: [ - (url: string, options?: ICreateHrefOptions) => { + (url: string, options?: CreateHrefOptions) => { const deps = { history: props.history, http: HttpLogic.values.http }; const href = createHref(url, deps, options); return props.navigateToUrl(href); @@ -44,7 +44,7 @@ export const KibanaLogic = kea>({ }), }); -export const mountKibanaLogic = (props: IKibanaLogicProps) => { +export const mountKibanaLogic = (props: KibanaLogicProps) => { KibanaLogic(props); const unmount = KibanaLogic.mount(); return unmount; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/kibana_chrome/generate_breadcrumbs.ts b/x-pack/plugins/enterprise_search/public/applications/shared/kibana_chrome/generate_breadcrumbs.ts index e22334aeea371..25d4850425a6e 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/kibana_chrome/generate_breadcrumbs.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/kibana_chrome/generate_breadcrumbs.ts @@ -23,15 +23,15 @@ import { letBrowserHandleEvent, createHref } from '../react_router_helpers'; * Types */ -interface IBreadcrumb { +interface Breadcrumb { text: string; path?: string; // Used to navigate outside of the React Router basename, // i.e. if we need to go from App Search to Enterprise Search shouldNotCreateHref?: boolean; } -export type TBreadcrumbs = IBreadcrumb[]; -export type TBreadcrumbTrail = string[]; // A trail of breadcrumb text +export type Breadcrumbs = Breadcrumb[]; +export type BreadcrumbTrail = string[]; // A trail of breadcrumb text /** * Generate an array of breadcrumbs based on: @@ -50,7 +50,7 @@ export type TBreadcrumbTrail = string[]; // A trail of breadcrumb text * > Source Prioritization (linked to `/groups/{example-group-id}/source_prioritization`) */ -export const useGenerateBreadcrumbs = (trail: TBreadcrumbTrail): TBreadcrumbs => { +export const useGenerateBreadcrumbs = (trail: BreadcrumbTrail): Breadcrumbs => { const { history } = useValues(KibanaLogic); const pathArray = stripLeadingSlash(history.location.pathname).split('/'); @@ -65,7 +65,7 @@ export const useGenerateBreadcrumbs = (trail: TBreadcrumbTrail): TBreadcrumbs => * https://elastic.github.io/eui/#/navigation/breadcrumbs */ -export const useEuiBreadcrumbs = (breadcrumbs: TBreadcrumbs): EuiBreadcrumb[] => { +export const useEuiBreadcrumbs = (breadcrumbs: Breadcrumbs): EuiBreadcrumb[] => { const { navigateToUrl, history } = useValues(KibanaLogic); const { http } = useValues(HttpLogic); @@ -89,7 +89,7 @@ export const useEuiBreadcrumbs = (breadcrumbs: TBreadcrumbs): EuiBreadcrumb[] => * Product-specific breadcrumb helpers */ -export const useEnterpriseSearchBreadcrumbs = (breadcrumbs: TBreadcrumbs = []) => +export const useEnterpriseSearchBreadcrumbs = (breadcrumbs: Breadcrumbs = []) => useEuiBreadcrumbs([ { text: ENTERPRISE_SEARCH_PLUGIN.NAME, @@ -99,10 +99,10 @@ export const useEnterpriseSearchBreadcrumbs = (breadcrumbs: TBreadcrumbs = []) = ...breadcrumbs, ]); -export const useAppSearchBreadcrumbs = (breadcrumbs: TBreadcrumbs = []) => +export const useAppSearchBreadcrumbs = (breadcrumbs: Breadcrumbs = []) => useEnterpriseSearchBreadcrumbs([{ text: APP_SEARCH_PLUGIN.NAME, path: '/' }, ...breadcrumbs]); -export const useWorkplaceSearchBreadcrumbs = (breadcrumbs: TBreadcrumbs = []) => +export const useWorkplaceSearchBreadcrumbs = (breadcrumbs: Breadcrumbs = []) => useEnterpriseSearchBreadcrumbs([ { text: WORKPLACE_SEARCH_PLUGIN.NAME, path: '/' }, ...breadcrumbs, diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/kibana_chrome/generate_title.ts b/x-pack/plugins/enterprise_search/public/applications/shared/kibana_chrome/generate_title.ts index de5f72de79192..a0e34106fe2a2 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/kibana_chrome/generate_title.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/kibana_chrome/generate_title.ts @@ -15,24 +15,24 @@ import { * https://github.com/elastic/kibana/blob/master/docs/development/core/public/kibana-plugin-core-public.chromedoctitle.md */ -export type TTitle = string[]; +type Title = string[]; /** * Given an array of page titles, return a final formatted document title * @param pages - e.g., ['Curations', 'some Engine', 'App Search'] * @returns - e.g., 'Curations - some Engine - App Search' */ -export const generateTitle = (pages: TTitle) => pages.join(' - '); +export const generateTitle = (pages: Title) => pages.join(' - '); /** * Product-specific helpers */ -export const enterpriseSearchTitle = (page: TTitle = []) => +export const enterpriseSearchTitle = (page: Title = []) => generateTitle([...page, ENTERPRISE_SEARCH_PLUGIN.NAME]); -export const appSearchTitle = (page: TTitle = []) => +export const appSearchTitle = (page: Title = []) => generateTitle([...page, APP_SEARCH_PLUGIN.NAME]); -export const workplaceSearchTitle = (page: TTitle = []) => +export const workplaceSearchTitle = (page: Title = []) => generateTitle([...page, WORKPLACE_SEARCH_PLUGIN.NAME]); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/kibana_chrome/set_chrome.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/kibana_chrome/set_chrome.tsx index a43e7053bb1e1..0e694a3d2bbc8 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/kibana_chrome/set_chrome.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/kibana_chrome/set_chrome.tsx @@ -14,7 +14,7 @@ import { useEnterpriseSearchBreadcrumbs, useAppSearchBreadcrumbs, useWorkplaceSearchBreadcrumbs, - TBreadcrumbTrail, + BreadcrumbTrail, } from './generate_breadcrumbs'; import { enterpriseSearchTitle, appSearchTitle, workplaceSearchTitle } from './generate_title'; @@ -33,11 +33,11 @@ import { enterpriseSearchTitle, appSearchTitle, workplaceSearchTitle } from './g * Title output: Workplace Search - Elastic */ -interface ISetChromeProps { - trail?: TBreadcrumbTrail; +interface SetChromeProps { + trail?: BreadcrumbTrail; } -export const SetEnterpriseSearchChrome: React.FC = ({ trail = [] }) => { +export const SetEnterpriseSearchChrome: React.FC = ({ trail = [] }) => { const { setBreadcrumbs, setDocTitle } = useValues(KibanaLogic); const title = reverseArray(trail); @@ -54,7 +54,7 @@ export const SetEnterpriseSearchChrome: React.FC = ({ trail = [ return null; }; -export const SetAppSearchChrome: React.FC = ({ trail = [] }) => { +export const SetAppSearchChrome: React.FC = ({ trail = [] }) => { const { setBreadcrumbs, setDocTitle } = useValues(KibanaLogic); const title = reverseArray(trail); @@ -71,7 +71,7 @@ export const SetAppSearchChrome: React.FC = ({ trail = [] }) => return null; }; -export const SetWorkplaceSearchChrome: React.FC = ({ trail = [] }) => { +export const SetWorkplaceSearchChrome: React.FC = ({ trail = [] }) => { const { setBreadcrumbs, setDocTitle } = useValues(KibanaLogic); const title = reverseArray(trail); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/layout/layout.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/layout/layout.tsx index ef8216e8b6711..0ee7de242dfe2 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/layout/layout.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/layout/layout.tsx @@ -12,7 +12,7 @@ import { i18n } from '@kbn/i18n'; import './layout.scss'; -interface ILayoutProps { +interface LayoutProps { navigation: React.ReactNode; restrictWidth?: boolean; readOnlyMode?: boolean; @@ -23,7 +23,7 @@ export interface INavContext { } export const NavContext = React.createContext({}); -export const Layout: React.FC = ({ +export const Layout: React.FC = ({ children, navigation, restrictWidth, diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/layout/side_nav.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/layout/side_nav.tsx index facfd0bfcb16d..6c4e1d084c16d 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/layout/side_nav.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/layout/side_nav.tsx @@ -23,7 +23,7 @@ import './side_nav.scss'; * Side navigation - product & icon + links wrapper */ -interface ISideNavProps { +interface SideNavProps { // Expects product plugin constants (@see common/constants.ts) product: { NAME: string; @@ -31,7 +31,7 @@ interface ISideNavProps { }; } -export const SideNav: React.FC = ({ product, children }) => { +export const SideNav: React.FC = ({ product, children }) => { return (