diff --git a/.eslintrc.js b/.eslintrc.js index d5629f1d374702..ad9de04251e4c0 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 6da2d5d602186a..b7fb3ff04db710 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -27,29 +27,16 @@ /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 -# 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 @@ -143,14 +118,17 @@ # 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 @@ -159,6 +137,7 @@ /x-pack/test/functional/es_archives/maps/ @elastic/kibana-gis /x-pack/test/visual_regression/tests/maps/index.js @elastic/kibana-gis #CC# /src/plugins/maps_legacy/ @elastic/kibana-gis +#CC# /src/plugins/maps_oss/ @elastic/kibana-gis #CC# /x-pack/plugins/file_upload @elastic/kibana-gis #CC# /x-pack/plugins/maps_legacy_licensing @elastic/kibana-gis #CC# /src/plugins/home/server/tutorials @elastic/kibana-gis @@ -180,9 +159,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 +187,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 @@ -257,19 +215,13 @@ /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 @@ -294,17 +246,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 /docs/user/alerting/ @elastic/kibana-alerting-services /docs/management/alerting/ @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 #CC# /x-pack/plugins/stack_alerts @elastic/kibana-alerting-services # Enterprise Search @@ -319,7 +266,6 @@ x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json @elastic/kib /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 @@ -333,18 +279,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 @@ -361,6 +300,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 @@ -369,13 +309,6 @@ 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 diff --git a/docs/developer/best-practices/stability.asciidoc b/docs/developer/best-practices/stability.asciidoc index 348412e593d9e8..29be86f58317f1 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 e4bd49e12101bb..4cf667195153d4 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/development/core/public/kibana-plugin-core-public.uisettingsparams.md b/docs/development/core/public/kibana-plugin-core-public.uisettingsparams.md index e7facb4a109cd8..4a9fc940c596ff 100644 --- a/docs/development/core/public/kibana-plugin-core-public.uisettingsparams.md +++ b/docs/development/core/public/kibana-plugin-core-public.uisettingsparams.md @@ -19,6 +19,7 @@ export interface UiSettingsParams | [category](./kibana-plugin-core-public.uisettingsparams.category.md) | string[] | used to group the configured setting in the UI | | [deprecation](./kibana-plugin-core-public.uisettingsparams.deprecation.md) | DeprecationSettings | optional deprecation information. Used to generate a deprecation warning. | | [description](./kibana-plugin-core-public.uisettingsparams.description.md) | string | description provided to a user in UI | +| [metric](./kibana-plugin-core-public.uisettingsparams.metric.md) | {
type: UiStatsMetricType;
name: string;
} | Metric to track once this property changes | | [name](./kibana-plugin-core-public.uisettingsparams.name.md) | string | title in the UI | | [optionLabels](./kibana-plugin-core-public.uisettingsparams.optionlabels.md) | Record<string, string> | text labels for 'select' type UI element | | [options](./kibana-plugin-core-public.uisettingsparams.options.md) | string[] | array of permitted values for this setting | diff --git a/docs/development/core/public/kibana-plugin-core-public.uisettingsparams.metric.md b/docs/development/core/public/kibana-plugin-core-public.uisettingsparams.metric.md new file mode 100644 index 00000000000000..0855cfd77a46b4 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.uisettingsparams.metric.md @@ -0,0 +1,21 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [UiSettingsParams](./kibana-plugin-core-public.uisettingsparams.md) > [metric](./kibana-plugin-core-public.uisettingsparams.metric.md) + +## UiSettingsParams.metric property + +> Warning: This API is now obsolete. +> +> Temporary measure until https://github.com/elastic/kibana/issues/83084 is in place +> + +Metric to track once this property changes + +Signature: + +```typescript +metric?: { + type: UiStatsMetricType; + name: string; + }; +``` 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 00000000000000..cac878c1e44494 --- /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 7a733cc34daced..1171dbad570ce5 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 00000000000000..2fe8e564e7ce52 --- /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 00000000000000..81caed287454ee --- /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 00000000000000..f68b7877953e70 --- /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 68f5e729155567..adbb2460dc80aa 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/core/server/kibana-plugin-core-server.uisettingsparams.md b/docs/development/core/server/kibana-plugin-core-server.uisettingsparams.md index f134decb5102bd..7bcb996e98e167 100644 --- a/docs/development/core/server/kibana-plugin-core-server.uisettingsparams.md +++ b/docs/development/core/server/kibana-plugin-core-server.uisettingsparams.md @@ -19,6 +19,7 @@ export interface UiSettingsParams | [category](./kibana-plugin-core-server.uisettingsparams.category.md) | string[] | used to group the configured setting in the UI | | [deprecation](./kibana-plugin-core-server.uisettingsparams.deprecation.md) | DeprecationSettings | optional deprecation information. Used to generate a deprecation warning. | | [description](./kibana-plugin-core-server.uisettingsparams.description.md) | string | description provided to a user in UI | +| [metric](./kibana-plugin-core-server.uisettingsparams.metric.md) | {
type: UiStatsMetricType;
name: string;
} | Metric to track once this property changes | | [name](./kibana-plugin-core-server.uisettingsparams.name.md) | string | title in the UI | | [optionLabels](./kibana-plugin-core-server.uisettingsparams.optionlabels.md) | Record<string, string> | text labels for 'select' type UI element | | [options](./kibana-plugin-core-server.uisettingsparams.options.md) | string[] | array of permitted values for this setting | diff --git a/docs/development/core/server/kibana-plugin-core-server.uisettingsparams.metric.md b/docs/development/core/server/kibana-plugin-core-server.uisettingsparams.metric.md new file mode 100644 index 00000000000000..4d54bf9ae472b8 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.uisettingsparams.metric.md @@ -0,0 +1,21 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [UiSettingsParams](./kibana-plugin-core-server.uisettingsparams.md) > [metric](./kibana-plugin-core-server.uisettingsparams.metric.md) + +## UiSettingsParams.metric property + +> Warning: This API is now obsolete. +> +> Temporary measure until https://github.com/elastic/kibana/issues/83084 is in place +> + +Metric to track once this property changes + +Signature: + +```typescript +metric?: { + type: UiStatsMetricType; + name: string; + }; +``` 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 96e43ca8368915..de6f4563b678ac 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 00000000000000..a29cddd81d885a --- /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 bb45222d096a0d..b886aafcfc00fa 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" | "screenTitle" | "showQueryInput" | "showDatePicker" | "showAutoRefreshOnly" | "dateRangeFrom" | "dateRangeTo" | "isRefreshPaused" | "customSubmitButton" | "timeHistory" | "indicateNoData" | "onFiltersUpdated" | "savedQuery" | "showSaveQuery" | "onClearSavedQuery" | "showQueryBar" | "showFilterBar" | "onQueryChange" | "onQuerySubmit" | "onSaved" | "onSavedQueryUpdated">, 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 6ed20beb396f15..9a0c37c8edd383 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.ui_settings.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.ui_settings.md index 2d4ce75b956df3..c2edc64f292d29 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/glossary.asciidoc b/docs/glossary.asciidoc index be24402170bbee..ff03a601739615 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 720aecf44d9faf..00000000000000 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 abb52bd0b5b0a6..21614aa0f4e243 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 2800a5a2d25846..1528f404026f2f 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 bf5fd56ceba137..00000000000000 Binary files a/docs/maps/images/gs_create_new_map.png and /dev/null differ diff --git a/docs/maps/import-geospatial-data.asciidoc b/docs/maps/import-geospatial-data.asciidoc index ff0c9bf1f72ba3..fb4250368086e8 100644 --- a/docs/maps/import-geospatial-data.asciidoc +++ b/docs/maps/import-geospatial-data.asciidoc @@ -26,7 +26,7 @@ Choose an import tool based on the format of your geospatial data. *Upload GeoJSON* indexes GeoJSON features as a geo_point or geo_shape. -. <>. +. <>. . Click *Add layer*. . Select *Upload GeoJSON*. . Use the file chooser to select a GeoJSON file. diff --git a/docs/maps/maps-getting-started.asciidoc b/docs/maps/maps-getting-started.asciidoc index 5c6cd87b235e1f..32a81c8e65f565 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 09d9788cd37e0d..764a84d88b1d26 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/settings/ssl-settings.asciidoc b/docs/settings/ssl-settings.asciidoc deleted file mode 100644 index 3a0a474d9d597b..00000000000000 --- a/docs/settings/ssl-settings.asciidoc +++ /dev/null @@ -1,99 +0,0 @@ -[float] -=== {component} TLS/SSL settings -You can configure the following TLS/SSL settings. If the settings are not -configured, the default values are used. See -{ref}/security-settings.html[Default TLS/SSL Settings]. - -ifdef::server[] -+{ssl-prefix}.ssl.enabled+:: -Used to enable or disable TLS/SSL. The default is `false`. -endif::server[] - -+{ssl-prefix}.ssl.supported_protocols+:: -Supported protocols with versions. Valid protocols: `SSLv2Hello`, -`SSLv3`, `TLSv1`, `TLSv1.1`, `TLSv1.2`. Defaults to `TLSv1.2`, `TLSv1.1`, -`TLSv1`. Defaults to the value of `xpack.ssl.supported_protocols`. - -ifdef::server[] -+{ssl-prefix}.ssl.client_authentication+:: -Controls the server's behavior in regard to requesting a certificate -from client connections. Valid values are `required`, `optional`, and `none`. -`required` forces a client to present a certificate, while `optional` -requests a client certificate but the client is not required to present one. -ifndef::client-auth-default[] -Defaults to the value of `xpack.ssl.client_authentication`. -endif::client-auth-default[] -ifdef::client-auth-default[] -Defaults to +{client-auth-default}+. -endif::client-auth-default[] -endif::server[] - -ifdef::verifies[] -+{ssl-prefix}.ssl.verification_mode+:: -Controls the verification of certificates. Valid values are `none`, -`certificate`, and `full`. Defaults to the value of `xpack.ssl.verification_mode`. -endif::verifies[] - -+{ssl-prefix}.ssl.cipher_suites+:: -Supported cipher suites can be found in Oracle's http://docs.oracle.com/javase/8/docs/technotes/guides/security/SunProviders.html[ -Java Cryptography Architecture documentation]. Defaults to the value of -`xpack.ssl.cipher_suites`. - -[float] -==== {component} TLS/SSL key and trusted certificate settings - -The following settings are used to specify a private key, certificate, and the -trusted certificates that should be used when communicating over an SSL/TLS connection. -If none of the settings are specified, the default values are used. -See {ref}/security-settings.html[Default TLS/SSL settings]. - -ifdef::server[] -A private key and certificate must be configured. -endif::server[] -ifndef::server[] -A private key and certificate are optional and would be used if the server requires client authentication for PKI -authentication. -endif::server[] -If none of the settings bare specified, the defaults values are used. -See {ref}/security-settings.html[Default TLS/SSL settings]. - -[float] -===== PEM encoded files - -When using PEM encoded files, use the following settings: - -+{ssl-prefix}.ssl.key+:: -Path to a PEM encoded file containing the private key. - -+{ssl-prefix}.ssl.key_passphrase+:: -The passphrase that will be used to decrypt the private key. This value is -optional as the key may not be encrypted. - -+{ssl-prefix}.ssl.certificate+:: -Path to a PEM encoded file containing the certificate (or certificate chain) -that will be presented when requested. - -+{ssl-prefix}.ssl.certificate_authorities+:: -List of paths to the PEM encoded certificate files that should be trusted. - -[float] -===== Java keystore files - -When using Java keystore files (JKS), which contain the private key, certificate -and certificates that should be trusted, use the following settings: - -+{ssl-prefix}.ssl.keystore.path+:: -Path to the keystore that holds the private key and certificate. - -+{ssl-prefix}.ssl.keystore.password+:: -Password to the keystore. - -+{ssl-prefix}.ssl.keystore.key_password+:: -Password for the private key in the keystore. Defaults to the -same value as +{ssl-prefix}.ssl.keystore.password+. - -+{ssl-prefix}.ssl.truststore.path+:: -Path to the truststore file. - -+{ssl-prefix}.ssl.truststore.password+:: -Password to the truststore. diff --git a/docs/user/alerting/alert-types.asciidoc b/docs/user/alerting/alert-types.asciidoc index f71e43c5defc7b..7de5ff56228cc0 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 2b22b49375676f..53aef4aaa062ed 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 bdff7355d7467d..00000000000000 --- 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 e3d0e16630c5c8..ca788020d92862 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 212e29898bd405..cf92016e23f19d 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://ela.st/handlebars-docs#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://ela.st/handlebars-helpers[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 353d3dc85356c7..7edbcb03be542d 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", @@ -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", @@ -577,8 +577,8 @@ "angular-recursion": "^1.0.5", "angular-route": "^1.8.0", "angular-sortable-view": "^0.0.17", - "apidoc": "^0.20.1", - "apidoc-markdown": "^5.0.0", + "apidoc": "^0.25.0", + "apidoc-markdown": "^5.1.8", "apollo-link": "^1.2.3", "apollo-link-error": "^1.1.7", "apollo-link-state": "^0.4.1", diff --git a/packages/kbn-dev-utils/src/kbn_client/kbn_client_ui_settings.ts b/packages/kbn-dev-utils/src/kbn_client/kbn_client_ui_settings.ts index 6ee2d3bfe59b0c..cf3f8f25476cbf 100644 --- a/packages/kbn-dev-utils/src/kbn_client/kbn_client_ui_settings.ts +++ b/packages/kbn-dev-utils/src/kbn_client/kbn_client_ui_settings.ts @@ -102,6 +102,7 @@ export class KbnClientUiSettings { body: { changes: updates, }, + retries: 3, }); } diff --git a/packages/kbn-optimizer/limits.yml b/packages/kbn-optimizer/limits.yml index 701b7cab21600d..e326c8e2cac39e 100644 --- a/packages/kbn-optimizer/limits.yml +++ b/packages/kbn-optimizer/limits.yml @@ -102,3 +102,4 @@ pageLoadAssetSize: visualizations: 295025 visualize: 57431 watcher: 43598 + stackAlerts: 29684 diff --git a/packages/kbn-plugin-helpers/README.md b/packages/kbn-plugin-helpers/README.md index d7ed3106c1ceba..05eb9f59987372 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/chrome/ui/header/__snapshots__/collapsible_nav.test.tsx.snap b/src/core/public/chrome/ui/header/__snapshots__/collapsible_nav.test.tsx.snap index cf734f33cc3e4d..b01dd205440a94 100644 --- a/src/core/public/chrome/ui/header/__snapshots__/collapsible_nav.test.tsx.snap +++ b/src/core/public/chrome/ui/header/__snapshots__/collapsible_nav.test.tsx.snap @@ -131,7 +131,7 @@ exports[`CollapsibleNav renders links grouped by category 1`] = ` "category": Object { "euiIconType": "logoKibana", "id": "kibana", - "label": "Kibana", + "label": "Analytics", "order": 1000, }, "data-test-subj": "discover", @@ -187,7 +187,7 @@ exports[`CollapsibleNav renders links grouped by category 1`] = ` "category": Object { "euiIconType": "logoKibana", "id": "kibana", - "label": "Kibana", + "label": "Analytics", "order": 1000, }, "data-test-subj": "visualize", @@ -201,7 +201,7 @@ exports[`CollapsibleNav renders links grouped by category 1`] = ` "category": Object { "euiIconType": "logoKibana", "id": "kibana", - "label": "Kibana", + "label": "Analytics", "order": 1000, }, "data-test-subj": "dashboard", @@ -859,7 +859,7 @@ exports[`CollapsibleNav renders links grouped by category 1`] = ` isCollapsible={true} key="kibana" onToggle={[Function]} - title="Kibana" + title="Analytics" > - Kibana + Analytics @@ -971,7 +971,7 @@ exports[`CollapsibleNav renders links grouped by category 1`] = ` className="euiTitle euiTitle--xxsmall euiCollapsibleNavGroup__title" id="mockId__title" > - Kibana + Analytics @@ -996,7 +996,7 @@ exports[`CollapsibleNav renders links grouped by category 1`] = ` className="euiCollapsibleNavGroup__children" >
    - {lastCrumb.text} - - + +
    {lastCrumb.text}
    + +
    ); } - return ; } diff --git a/src/core/public/doc_links/doc_links_service.ts b/src/core/public/doc_links/doc_links_service.ts index 48187fe4653922..6af14734444d1d 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/public/public.api.md b/src/core/public/public.api.md index 781a50f849e241..c8add5a8ddf581 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -38,6 +38,7 @@ import { TransportRequestParams } from '@elastic/elasticsearch/lib/Transport'; import { TransportRequestPromise } from '@elastic/elasticsearch/lib/Transport'; import { Type } from '@kbn/config-schema'; import { TypeOf } from '@kbn/config-schema'; +import { UiStatsMetricType } from '@kbn/analytics'; import { UnregisterCallback } from 'history'; import { UserProvidedValues as UserProvidedValues_2 } from 'src/core/server/types'; @@ -1362,6 +1363,11 @@ export interface UiSettingsParams { // Warning: (ae-forgotten-export) The symbol "DeprecationSettings" needs to be exported by the entry point index.d.ts deprecation?: DeprecationSettings; description?: string; + // @deprecated + metric?: { + type: UiStatsMetricType; + name: string; + }; name?: string; optionLabels?: Record; options?: string[]; diff --git a/src/core/server/environment/environment_service.mock.ts b/src/core/server/environment/environment_service.mock.ts index a956e369ba4a72..3c579b0f68b009 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/legacy/server/i18n/index.ts b/src/core/server/i18n/fs.ts similarity index 88% rename from src/legacy/server/i18n/index.ts rename to src/core/server/i18n/fs.ts index a7ef49f44532c7..23d729504f81cd 100644 --- a/src/legacy/server/i18n/index.ts +++ b/src/core/server/i18n/fs.ts @@ -17,4 +17,7 @@ * under the License. */ -export { i18nMixin } from './i18n_mixin'; +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 00000000000000..737d8ed8bc4e2c --- /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 d7f77d3185ba40..dacb6a1e16a5c2 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 00000000000000..f3b688062523c3 --- /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 00000000000000..c6af59da07fb54 --- /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 a2a292e2278be3..41d9dc4722e374 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 00000000000000..f181c52dc4b065 --- /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 00000000000000..679751aefbf276 --- /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 00000000000000..23f97a1404fff3 --- /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 00000000000000..87de39a92ab263 --- /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 00000000000000..fd32dd7fdd6ef5 --- /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 00000000000000..2db3fdeb405d9e --- /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 00000000000000..94e6d41019ad2e --- /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 0adda4770639d7..7b19c3a6867579 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 c42771179aba22..3111c8daf79815 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 0d9e9af39317c6..6faccc9c9da56d 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 8ca0c82219ed43..03a0ae2d6443ae 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 22e79741e854c4..3b2634ddbe3152 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 14d6de889dd424..3ea8badb0f4504 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 64a382e539fb0f..02b82c17ed4fc0 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 a1062bde7765f0..5967e6d5358dec 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 ae9267ca5cf607..89a3697ebe9cd6 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 72d2cfe158b37b..23dc77b7bf6737 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 01d084f9ae53ce..00e617de825423 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 c56cdabf6e4cde..85dbf4b5e8c6a5 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 c35ec809fcf8d3..e78b944183df91 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 @@ -103,12 +103,11 @@ describe('#getQueryParams', () => { if (registry.isMultiNamespace(type)) { const array = [...(namespaces ?? [DEFAULT_NAMESPACE_STRING]), ALL_NAMESPACES_STRING]; - const namespacesClause = { terms: { namespaces: array } }; return { bool: { must: namespaces?.includes(ALL_NAMESPACES_STRING) - ? expect.not.arrayContaining([namespacesClause]) - : expect.arrayContaining([namespacesClause]), + ? [{ term: { type } }] + : [{ term: { type } }, { terms: { namespaces: array } }], must_not: [{ exists: { field: 'namespace' } }], }, }; 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 2ecba42e408e77..cb58db171681af 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 @@ -78,13 +78,19 @@ function getClauseForType( const searchAcrossAllNamespaces = namespaces.includes(ALL_NAMESPACES_STRING); if (registry.isMultiNamespace(type)) { - const namespacesFilterClause = searchAcrossAllNamespaces - ? {} - : { terms: { namespaces: [...namespaces, ALL_NAMESPACES_STRING] } }; + const typeFilterClause = { term: { type } }; + + const namespacesFilterClause = { + terms: { namespaces: [...namespaces, ALL_NAMESPACES_STRING] }, + }; + + const must = searchAcrossAllNamespaces + ? [typeFilterClause] + : [typeFilterClause, namespacesFilterClause]; return { bool: { - must: [{ term: { type } }, namespacesFilterClause], + must, must_not: [{ exists: { field: 'namespace' } }], }, }; diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index 52500673f7f31d..a03e5ec9acd275 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -160,6 +160,7 @@ import { TransportRequestParams } from '@elastic/elasticsearch/lib/Transport'; import { TransportRequestPromise } from '@elastic/elasticsearch/lib/Transport'; import { Type } from '@kbn/config-schema'; import { TypeOf } from '@kbn/config-schema'; +import { UiStatsMetricType } from '@kbn/analytics'; import { UpdateDocumentByQueryParams } from 'elasticsearch'; import { UpdateDocumentParams } from 'elasticsearch'; import { URL } from 'url'; @@ -480,6 +481,8 @@ export interface CoreSetup HttpServerInfo; } +// @public (undocumented) +export interface I18nServiceSetup { + getLocale(): string; + getTranslationFiles(): string[]; +} + // @public export type IBasePath = Pick; @@ -2738,6 +2747,11 @@ export interface UiSettingsParams { category?: string[]; deprecation?: DeprecationSettings; description?: string; + // @deprecated + metric?: { + type: UiStatsMetricType; + name: string; + }; name?: string; optionLabels?: Record; options?: string[]; diff --git a/src/core/server/server.test.mocks.ts b/src/core/server/server.test.mocks.ts index fe299c6d11675e..d2d6990dc54515 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 78703ceeec7ae2..0c7ebbcb527ec2 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 eaa03d11cab98a..55ed88e55a9f5b 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 0ee2d03229a780..42892ddbb490c2 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 b1ed0dd188cdec..818e9b43889e3e 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 0c17a3a614d60c..f4e24e3a517c36 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 25062490f5b6b5..4f757d18ea7da6 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/core/types/ui_settings.ts b/src/core/types/ui_settings.ts index ed1076b5719603..0b7a8e1efd9df7 100644 --- a/src/core/types/ui_settings.ts +++ b/src/core/types/ui_settings.ts @@ -17,6 +17,7 @@ * under the License. */ import { Type } from '@kbn/config-schema'; +import { UiStatsMetricType } from '@kbn/analytics'; /** * UI element type to represent the settings. @@ -80,6 +81,15 @@ export interface UiSettingsParams { * Used to validate value on write and read. */ schema: Type; + /** + * Metric to track once this property changes + * @deprecated + * Temporary measure until https://github.com/elastic/kibana/issues/83084 is in place + */ + metric?: { + type: UiStatsMetricType; + name: string; + }; } /** diff --git a/src/core/utils/default_app_categories.ts b/src/core/utils/default_app_categories.ts index 809aaddb741722..d31ed8a8eb3071 100644 --- a/src/core/utils/default_app_categories.ts +++ b/src/core/utils/default_app_categories.ts @@ -25,7 +25,7 @@ export const DEFAULT_APP_CATEGORIES: Record = Object.freeze kibana: { id: 'kibana', label: i18n.translate('core.ui.kibanaNavList.label', { - defaultMessage: 'Kibana', + defaultMessage: 'Analytics', }), euiIconType: 'logoKibana', order: 1000, diff --git a/src/dev/typescript/run_check_ts_projects_cli.ts b/src/dev/typescript/run_check_ts_projects_cli.ts index a095bef047711e..1d332badcd8aed 100644 --- a/src/dev/typescript/run_check_ts_projects_cli.ts +++ b/src/dev/typescript/run_check_ts_projects_cli.ts @@ -17,7 +17,7 @@ * under the License. */ -import { resolve } from 'path'; +import { resolve, relative } from 'path'; import execa from 'execa'; @@ -35,7 +35,7 @@ export async function runCheckTsProjectsCli() { }); const isNotInTsProject: File[] = []; - const isInMultipleTsProjects: File[] = []; + const isInMultipleTsProjects: string[] = []; for (const lineRaw of files.split('\n')) { const line = lineRaw.trim(); @@ -56,7 +56,11 @@ export async function runCheckTsProjectsCli() { isNotInTsProject.push(file); } if (projects.length > 1 && !file.isTypescriptAmbient()) { - isInMultipleTsProjects.push(file); + isInMultipleTsProjects.push( + ` - ${file.getRelativePath()}:\n${projects + .map((p) => ` - ${relative(process.cwd(), p.tsConfigPath)}`) + .join('\n')}` + ); } } @@ -74,10 +78,9 @@ export async function runCheckTsProjectsCli() { } if (isInMultipleTsProjects.length) { + const details = isInMultipleTsProjects.join('\n'); log.error( - `The following files belong to multiple tsconfig.json files listed in src/dev/typescript/projects.ts\n${isInMultipleTsProjects - .map((file) => ` - ${file.getRelativePath()}`) - .join('\n')}` + `The following files belong to multiple tsconfig.json files listed in src/dev/typescript/projects.ts\n${details}` ); } diff --git a/src/legacy/server/config/schema.js b/src/legacy/server/config/schema.js index e1f03b8a088471..39df3990ff2ff2 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 0f202c4d433c0c..00000000000000 --- 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 0b3879073c1647..00000000000000 --- 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 e29563a7c6266f..013da35d2acb7e 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/advanced_settings/kibana.json b/src/plugins/advanced_settings/kibana.json index 0e49fe17089f0b..df0d31a904c599 100644 --- a/src/plugins/advanced_settings/kibana.json +++ b/src/plugins/advanced_settings/kibana.json @@ -4,6 +4,6 @@ "server": true, "ui": true, "requiredPlugins": ["management"], - "optionalPlugins": ["home"], + "optionalPlugins": ["home", "usageCollection"], "requiredBundles": ["kibanaReact", "home"] } diff --git a/src/plugins/advanced_settings/public/management_app/advanced_settings.tsx b/src/plugins/advanced_settings/public/management_app/advanced_settings.tsx index afdd90959eabde..bbc27ca025ede6 100644 --- a/src/plugins/advanced_settings/public/management_app/advanced_settings.tsx +++ b/src/plugins/advanced_settings/public/management_app/advanced_settings.tsx @@ -22,6 +22,7 @@ import { Subscription } from 'rxjs'; import { Comparators, EuiFlexGroup, EuiFlexItem, EuiSpacer, Query } from '@elastic/eui'; import { useParams } from 'react-router-dom'; +import { UiStatsMetricType } from '@kbn/analytics'; import { CallOuts } from './components/call_outs'; import { Search } from './components/search'; import { Form } from './components/form'; @@ -39,6 +40,7 @@ interface AdvancedSettingsProps { dockLinks: DocLinksStart['links']; toasts: ToastsStart; componentRegistry: ComponentRegistry['start']; + trackUiMetric?: (metricType: UiStatsMetricType, eventName: string | string[]) => void; } interface AdvancedSettingsComponentProps extends AdvancedSettingsProps { @@ -241,6 +243,7 @@ export class AdvancedSettingsComponent extends Component< enableSaving={this.props.enableSaving} dockLinks={this.props.dockLinks} toasts={this.props.toasts} + trackUiMetric={this.props.trackUiMetric} /> { dockLinks={props.dockLinks} toasts={props.toasts} componentRegistry={props.componentRegistry} + trackUiMetric={props.trackUiMetric} /> ); }; diff --git a/src/plugins/advanced_settings/public/management_app/components/form/form.tsx b/src/plugins/advanced_settings/public/management_app/components/form/form.tsx index d243d85e12a665..c30768a262056e 100644 --- a/src/plugins/advanced_settings/public/management_app/components/form/form.tsx +++ b/src/plugins/advanced_settings/public/management_app/components/form/form.tsx @@ -36,6 +36,7 @@ import { import { FormattedMessage } from '@kbn/i18n/react'; import { isEmpty } from 'lodash'; import { i18n } from '@kbn/i18n'; +import { UiStatsMetricType } from '@kbn/analytics'; import { toMountPoint } from '../../../../../kibana_react/public'; import { DocLinksStart, ToastsStart } from '../../../../../../core/public'; @@ -56,6 +57,7 @@ interface FormProps { enableSaving: boolean; dockLinks: DocLinksStart['links']; toasts: ToastsStart; + trackUiMetric?: (metricType: UiStatsMetricType, eventName: string | string[]) => void; } interface FormState { @@ -149,7 +151,7 @@ export class Form extends PureComponent { if (!setting) { return; } - const { defVal, type, requiresPageReload } = setting; + const { defVal, type, requiresPageReload, metric } = setting; let valueToSave = value; let equalsToDefault = false; switch (type) { @@ -163,6 +165,11 @@ export class Form extends PureComponent { const isArray = Array.isArray(JSON.parse((defVal as string) || '{}')); valueToSave = valueToSave.trim(); valueToSave = valueToSave || (isArray ? '[]' : '{}'); + case 'boolean': + if (metric && this.props.trackUiMetric) { + const metricName = valueToSave ? `${metric.name}_on` : `${metric.name}_off`; + this.props.trackUiMetric(metric.type, metricName); + } default: equalsToDefault = valueToSave === defVal; } diff --git a/src/plugins/advanced_settings/public/management_app/lib/to_editable_config.ts b/src/plugins/advanced_settings/public/management_app/lib/to_editable_config.ts index 406bc35f826e84..e5a1ee1437a91a 100644 --- a/src/plugins/advanced_settings/public/management_app/lib/to_editable_config.ts +++ b/src/plugins/advanced_settings/public/management_app/lib/to_editable_config.ts @@ -75,6 +75,7 @@ export function toEditableConfig({ options: def.options, optionLabels: def.optionLabels, requiresPageReload: !!def.requiresPageReload, + metric: def.metric, }; return conf; diff --git a/src/plugins/advanced_settings/public/management_app/mount_management_section.tsx b/src/plugins/advanced_settings/public/management_app/mount_management_section.tsx index ab348451b1eef8..0b3d73cb28806f 100644 --- a/src/plugins/advanced_settings/public/management_app/mount_management_section.tsx +++ b/src/plugins/advanced_settings/public/management_app/mount_management_section.tsx @@ -30,6 +30,7 @@ import { ManagementAppMountParams } from '../../../management/public'; import { ComponentRegistry } from '../types'; import './index.scss'; +import { UsageCollectionSetup } from '../../../usage_collection/public'; const title = i18n.translate('advancedSettings.advancedSettingsLabel', { defaultMessage: 'Advanced Settings', @@ -49,12 +50,14 @@ const readOnlyBadge = { export async function mountManagementSection( getStartServices: StartServicesAccessor, params: ManagementAppMountParams, - componentRegistry: ComponentRegistry['start'] + componentRegistry: ComponentRegistry['start'], + usageCollection?: UsageCollectionSetup ) { params.setBreadcrumbs(crumb); const [{ uiSettings, notifications, docLinks, application, chrome }] = await getStartServices(); const canSave = application.capabilities.advancedSettings.save as boolean; + const trackUiMetric = usageCollection?.reportUiStats.bind(usageCollection, 'advanced_settings'); if (!canSave) { chrome.setBadge(readOnlyBadge); @@ -71,6 +74,7 @@ export async function mountManagementSection( dockLinks={docLinks.links} uiSettings={uiSettings} componentRegistry={componentRegistry} + trackUiMetric={trackUiMetric} /> diff --git a/src/plugins/advanced_settings/public/management_app/types.ts b/src/plugins/advanced_settings/public/management_app/types.ts index 6e243926f7d7dc..05e695f9985000 100644 --- a/src/plugins/advanced_settings/public/management_app/types.ts +++ b/src/plugins/advanced_settings/public/management_app/types.ts @@ -17,6 +17,7 @@ * under the License. */ +import { UiStatsMetricType } from '@kbn/analytics'; import { UiSettingsType, StringValidation, ImageValidation } from '../../../../core/public'; export interface FieldSetting { @@ -39,6 +40,10 @@ export interface FieldSetting { message: string; docLinksKey: string; }; + metric?: { + type: UiStatsMetricType; + name: string; + }; } // until eui searchbar and query are typed diff --git a/src/plugins/advanced_settings/public/plugin.ts b/src/plugins/advanced_settings/public/plugin.ts index 188b11177eaec6..165af48b2023ca 100644 --- a/src/plugins/advanced_settings/public/plugin.ts +++ b/src/plugins/advanced_settings/public/plugin.ts @@ -30,7 +30,10 @@ const title = i18n.translate('advancedSettings.advancedSettingsLabel', { export class AdvancedSettingsPlugin implements Plugin { - public setup(core: CoreSetup, { management, home }: AdvancedSettingsPluginSetup) { + public setup( + core: CoreSetup, + { management, home, usageCollection }: AdvancedSettingsPluginSetup + ) { const kibanaSection = management.sections.section.kibana; kibanaSection.registerApp({ @@ -41,7 +44,12 @@ export class AdvancedSettingsPlugin const { mountManagementSection } = await import( './management_app/mount_management_section' ); - return mountManagementSection(core.getStartServices, params, component.start); + return mountManagementSection( + core.getStartServices, + params, + component.start, + usageCollection + ); }, }); diff --git a/src/plugins/advanced_settings/public/types.ts b/src/plugins/advanced_settings/public/types.ts index cc59f52b1f30fb..bd5cb0e61fb042 100644 --- a/src/plugins/advanced_settings/public/types.ts +++ b/src/plugins/advanced_settings/public/types.ts @@ -21,6 +21,7 @@ import { ComponentRegistry } from './component_registry'; import { HomePublicPluginSetup } from '../../home/public'; import { ManagementSetup } from '../../management/public'; +import { UsageCollectionSetup } from '../../usage_collection/public'; export interface AdvancedSettingsSetup { component: ComponentRegistry['setup']; @@ -32,6 +33,7 @@ export interface AdvancedSettingsStart { export interface AdvancedSettingsPluginSetup { management: ManagementSetup; home?: HomePublicPluginSetup; + usageCollection?: UsageCollectionSetup; } export { ComponentRegistry }; diff --git a/src/plugins/data/common/constants.ts b/src/plugins/data/common/constants.ts index 43120583bd3a41..a70c5eedbf8478 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/public/autocomplete/providers/query_suggestion_provider.ts b/src/plugins/data/public/autocomplete/providers/query_suggestion_provider.ts index 16500ac9e239a0..da523e740c3d6d 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 6b0c0f07cf6c95..0ef5b7db958e45 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 a6a45a26f06b39..fe9f939a0261d2 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/plugin.ts b/src/plugins/data/public/plugin.ts index 5abf4d3648af72..afa8d935f367b3 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 31b05bd4763a24..78b974758f8c07 100644 --- a/src/plugins/data/public/public.api.md +++ b/src/plugins/data/public/public.api.md @@ -84,6 +84,7 @@ 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 { UserProvidedValues } from 'src/core/server/types'; @@ -1840,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) @@ -1996,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" | "screenTitle" | "showQueryInput" | "showDatePicker" | "showAutoRefreshOnly" | "dateRangeFrom" | "dateRangeTo" | "isRefreshPaused" | "customSubmitButton" | "timeHistory" | "indicateNoData" | "onFiltersUpdated" | "savedQuery" | "showSaveQuery" | "onClearSavedQuery" | "showQueryBar" | "showFilterBar" | "onQueryChange" | "onQuerySubmit" | "onSaved" | "onSavedQueryUpdated">, 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 @@ -2309,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"; }; diff --git a/src/plugins/data/public/query/timefilter/timefilter.ts b/src/plugins/data/public/query/timefilter/timefilter.ts index 49a8c68f6916f6..7278ceaaddcced 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/ui/filter_bar/filter_bar.tsx b/src/plugins/data/public/ui/filter_bar/filter_bar.tsx index 8c009576ff2805..194e253fd7b26c 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 719827a98cc634..c420734a43d41c 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 48e2e8dab7580c..f120aae9207748 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 daa6fa0dd80aba..e77f58f572f33e 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 6a5b7d1d5b4141..89ee0995f41408 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/server.api.md b/src/plugins/data/server/server.api.md index 131b3e4c39c6bb..bb7a8f58c926ce 100644 --- a/src/plugins/data/server/server.api.md +++ b/src/plugins/data/server/server.api.md @@ -60,6 +60,7 @@ import { ShardsResponse } from 'elasticsearch'; import { ToastInputFields } from 'src/core/public/notifications'; import { Type } from '@kbn/config-schema'; import { TypeOf } from '@kbn/config-schema'; +import { UiStatsMetricType } from '@kbn/analytics'; import { Unit } from '@elastic/datemath'; // Warning: (ae-forgotten-export) The symbol "AggConfigSerialized" needs to be exported by the entry point index.d.ts @@ -1181,6 +1182,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) diff --git a/src/plugins/data/server/ui_settings.ts b/src/plugins/data/server/ui_settings.ts index 763a086d7688d2..9393700a0e771f 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 389eda90e00a15..9319c58db3e33f 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 a18ee486ab0078..98b7625e63c72f 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 c7f2e2147e819c..df9b16a4627ec7 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/discover/server/ui_settings.ts b/src/plugins/discover/server/ui_settings.ts index 5447b982eef146..f45281ee62202c 100644 --- a/src/plugins/discover/server/ui_settings.ts +++ b/src/plugins/discover/server/ui_settings.ts @@ -21,6 +21,7 @@ import { i18n } from '@kbn/i18n'; import { schema } from '@kbn/config-schema'; import { UiSettingsParams } from 'kibana/server'; +import { METRIC_TYPE } from '@kbn/analytics'; import { DEFAULT_COLUMNS_SETTING, SAMPLE_SIZE_SETTING, @@ -170,9 +171,13 @@ export const uiSettings: Record = { }), value: true, description: i18n.translate('discover.advancedSettings.discover.modifyColumnsOnSwitchText', { - defaultMessage: 'Remove columns that not available in the new index pattern.', + defaultMessage: 'Remove columns that are not available in the new index pattern.', }), category: ['discover'], schema: schema.boolean(), + metric: { + type: METRIC_TYPE.CLICK, + name: 'discover:modifyColumnsOnSwitchTitle', + }, }, }; diff --git a/src/plugins/embeddable/public/public.api.md b/src/plugins/embeddable/public/public.api.md index 4406dded98547a..6a2565edf2f677 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/expressions/common/expression_functions/index.ts b/src/plugins/expressions/common/expression_functions/index.ts index b29e6b78b8f4d0..094fbe83efd224 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 8ba9d527d4c598..99ad2098ab10a0 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 672abadd3c0160..0d9547f70dd3b0 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 44ac198e2d17c1..320a254bf94a95 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/moving_average.ts b/src/plugins/expressions/common/expression_functions/specs/moving_average.ts index 00a4d8c45839e1..c4887e0240ec0c 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/home/public/application/components/home_app.js b/src/plugins/home/public/application/components/home_app.js index 7fe4f4351c35ba..734100fe584abc 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 c479562795512e..c782ce9c8cc84f 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 2408dc84c2e562..f3b7d8ca5eea08 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 fb837f5ae28df9..f2b00bd629a07b 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 198fdbb7a87037..16cb620351aaab 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/maps_legacy/common/ems_defaults.ts b/src/plugins/maps_legacy/common/ems_defaults.ts new file mode 100644 index 00000000000000..583dca1dbf036c --- /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/constants.ts b/src/plugins/maps_legacy/common/index.ts similarity index 95% rename from src/legacy/server/i18n/constants.ts rename to src/plugins/maps_legacy/common/index.ts index a7a410dbcb5b34..12148ec1ec6b81 100644 --- a/src/legacy/server/i18n/constants.ts +++ b/src/plugins/maps_legacy/common/index.ts @@ -17,4 +17,4 @@ * under the License. */ -export const I18N_RC = '.i18nrc.json'; +export * from './ems_defaults'; diff --git a/src/plugins/maps_legacy/config.ts b/src/plugins/maps_legacy/config.ts index f49d56dedd45f5..68595944e68b3f 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 d9bf33e6613684..1499b3de446b5b 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 fe5338b890ec83..2654ded907cce9 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 833304378402a1..7a00456b89a92e 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 665b3b8986ef0e..ba37df60f93579 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 1a7c0ccb15082f..d5b0514b64918c 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 c840cbe8fc94d5..a1eae69ffaed04 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/vis_default_editor/public/components/__snapshots__/agg.test.tsx.snap b/src/plugins/vis_default_editor/public/components/__snapshots__/agg.test.tsx.snap index 2a521bc01219cb..26173cddb37161 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 46d5af8cec6801..e78f2fcc4453cf 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 785ef1b83a23d9..90ee1dd4f02ae4 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 9529adfe127200..cb6e9d9aa7ba8c 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 b2e6373edfc101..4c5181ab316d1c 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 16aeafaab253ba..000719318f1075 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 c2c21e7c1a058b..00e6b6da88b057 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 fc79ba703c2b4a..4b0637edf40555 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 df9818b237962f..388ac39f064753 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_timeseries/common/vis_schema.ts b/src/plugins/vis_type_timeseries/common/vis_schema.ts index 27f09fb574b0f3..9ec5ae1424ae3c 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, 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 bb3d39797656f7..5bf4fb55ee5e52 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 11b3e303e7e00a..0b879adbd37aea 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 faf1a59adc4aac..fa1289dc74c721 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 316e0f9af43bdc..fb945d2606bc88 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 1999862f7aa0e0..6ca5fa8e7447f3 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 10b3d551bb89f7..e3a0c74273539e 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 30e5c57ac90ba8..bed5e9caa9f872 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/visualizations/public/embeddable/_index.scss b/src/plugins/visualizations/public/embeddable/_index.scss index c1e3809657bfa0..9703e90159f481 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 914480ff8c777e..00000000000000 --- 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 3d2af2c591a3ce..ea7760f31d54c9 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 2edabbf46f9d84..718bd2ed343ce9 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/functional/apps/discover/_doc_navigation.js b/test/functional/apps/discover/_doc_navigation.js index 31aef96918ffab..59c4371b95d807 100644 --- a/test/functional/apps/discover/_doc_navigation.js +++ b/test/functional/apps/discover/_doc_navigation.js @@ -20,7 +20,6 @@ import expect from '@kbn/expect'; export default function ({ getService, getPageObjects }) { - const log = getService('log'); const docTable = getService('docTable'); const filterBar = getService('filterBar'); const testSubjects = getService('testSubjects'); @@ -28,15 +27,12 @@ export default function ({ getService, getPageObjects }) { const esArchiver = getService('esArchiver'); const retry = getService('retry'); - // FLAKY: https://github.com/elastic/kibana/issues/78373 - describe.skip('doc link in discover', function contextSize() { + describe('doc link in discover', function contextSize() { beforeEach(async function () { - log.debug('load kibana index with default index pattern'); - await esArchiver.loadIfNeeded('discover'); - await esArchiver.loadIfNeeded('logstash_functional'); + await esArchiver.loadIfNeeded('discover'); + await PageObjects.timePicker.setDefaultAbsoluteRangeViaUiSettings(); await PageObjects.common.navigateToApp('discover'); - await PageObjects.timePicker.setDefaultAbsoluteRange(); await PageObjects.discover.waitForDocTableLoadingComplete(); }); diff --git a/test/functional/apps/discover/_field_data.js b/test/functional/apps/discover/_field_data.js index 118234d54626c7..d45b8f4841cb6a 100644 --- a/test/functional/apps/discover/_field_data.js +++ b/test/functional/apps/discover/_field_data.js @@ -27,8 +27,7 @@ export default function ({ getService, getPageObjects }) { const queryBar = getService('queryBar'); const PageObjects = getPageObjects(['common', 'header', 'discover', 'visualize', 'timePicker']); - // FLAKY: https://github.com/elastic/kibana/issues/78689 - describe.skip('discover tab', function describeIndexTests() { + describe('discover tab', function describeIndexTests() { this.tags('includeFirefox'); before(async function () { await esArchiver.loadIfNeeded('logstash_functional'); diff --git a/test/functional/apps/discover/_inspector.js b/test/functional/apps/discover/_inspector.js index fcb66fbd52cf73..900ad28e14e691 100644 --- a/test/functional/apps/discover/_inspector.js +++ b/test/functional/apps/discover/_inspector.js @@ -34,8 +34,7 @@ export default function ({ getService, getPageObjects }) { return hitsCountStatsRow[STATS_ROW_VALUE_INDEX]; } - // FLAKY: https://github.com/elastic/kibana/issues/39842 - describe.skip('inspect', () => { + describe('inspect', () => { before(async () => { await esArchiver.loadIfNeeded('logstash_functional'); await esArchiver.load('discover'); diff --git a/test/functional/apps/management/_index_patterns_empty.ts b/test/functional/apps/management/_index_patterns_empty.ts index 4ae2e7836ac376..2a1d723f1a06ee 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/fixtures/es_archiver/discover/data.json b/test/functional/fixtures/es_archiver/discover/data.json new file mode 100644 index 00000000000000..9158a3023fc5e3 --- /dev/null +++ b/test/functional/fixtures/es_archiver/discover/data.json @@ -0,0 +1,42 @@ +{ + "type": "doc", + "value": { + "id": "index-pattern:logstash-*", + "index": ".kibana", + "source": { + "index-pattern": { + "fields": "[{\"name\":\"@message\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"@message.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"@message\"}}},{\"name\":\"@tags\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"@tags.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"@tags\"}}},{\"name\":\"@timestamp\",\"type\":\"date\",\"esTypes\":[\"date\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"_id\",\"type\":\"string\",\"esTypes\":[\"_id\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_index\",\"type\":\"string\",\"esTypes\":[\"_index\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_score\",\"type\":\"number\",\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_source\",\"type\":\"_source\",\"esTypes\":[\"_source\"],\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_type\",\"type\":\"string\",\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"agent\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"agent.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"agent\"}}},{\"name\":\"bytes\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"clientip\",\"type\":\"ip\",\"esTypes\":[\"ip\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"extension\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"extension.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"extension\"}}},{\"name\":\"geo.coordinates\",\"type\":\"geo_point\",\"esTypes\":[\"geo_point\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.dest\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.src\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.srcdest\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"headings\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"headings.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"headings\"}}},{\"name\":\"host\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"host.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"host\"}}},{\"name\":\"id\",\"type\":\"number\",\"esTypes\":[\"integer\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"index\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"index.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"index\"}}},{\"name\":\"ip\",\"type\":\"ip\",\"esTypes\":[\"ip\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"links\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"links.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"links\"}}},{\"name\":\"machine.os\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"machine.os.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"machine.os\"}}},{\"name\":\"machine.ram\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"memory\",\"type\":\"number\",\"esTypes\":[\"double\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"meta.char\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"meta.related\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"meta.user.firstname\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"meta.user.lastname\",\"type\":\"number\",\"esTypes\":[\"integer\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"nestedField.child\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"nested\":{\"path\":\"nestedField\"}}},{\"name\":\"phpmemory\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"referer\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:modified_time\",\"type\":\"date\",\"esTypes\":[\"date\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:published_time\",\"type\":\"date\",\"esTypes\":[\"date\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:section\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.article:section.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.article:section\"}}},{\"name\":\"relatedContent.article:tag\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.article:tag.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.article:tag\"}}},{\"name\":\"relatedContent.og:description\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:description.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.og:description\"}}},{\"name\":\"relatedContent.og:image\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.og:image\"}}},{\"name\":\"relatedContent.og:image:height\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image:height.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.og:image:height\"}}},{\"name\":\"relatedContent.og:image:width\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image:width.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.og:image:width\"}}},{\"name\":\"relatedContent.og:site_name\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:site_name.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.og:site_name\"}}},{\"name\":\"relatedContent.og:title\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:title.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.og:title\"}}},{\"name\":\"relatedContent.og:type\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:type.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.og:type\"}}},{\"name\":\"relatedContent.og:url\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:url.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.og:url\"}}},{\"name\":\"relatedContent.twitter:card\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:card.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.twitter:card\"}}},{\"name\":\"relatedContent.twitter:description\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:description.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.twitter:description\"}}},{\"name\":\"relatedContent.twitter:image\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:image.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.twitter:image\"}}},{\"name\":\"relatedContent.twitter:site\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:site.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.twitter:site\"}}},{\"name\":\"relatedContent.twitter:title\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:title.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.twitter:title\"}}},{\"name\":\"relatedContent.url\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.url.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.url\"}}},{\"name\":\"request\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"request.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"request\"}}},{\"name\":\"response\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"response.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"response\"}}},{\"name\":\"spaces\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"spaces.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"spaces\"}}},{\"name\":\"type\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"url\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"url.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"url\"}}},{\"name\":\"utc_time\",\"type\":\"date\",\"esTypes\":[\"date\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"xss\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"xss.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"xss\"}}}]", + "timeFieldName": "@timestamp", + "title": "logstash-*" + }, + "type": "index-pattern" + } + } +} + +{ + "type": "doc", + "value": { + "id": "search:ab12e3c0-f231-11e6-9486-733b1ac9221a", + "index": ".kibana", + "source": { + "search": { + "columns": [ + "_source" + ], + "description": "A Saved Search Description", + "hits": 0, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\n \"index\": \"logstash-*\",\n \"highlightAll\": true,\n \"filter\": [],\n \"query\": {\n \"query_string\": {\n \"query\": \"*\",\n \"analyze_wildcard\": true\n }\n }\n}" + }, + "sort": [ + "@timestamp", + "desc" + ], + "title": "A Saved Search", + "version": 1 + }, + "type": "search" + } + } +} diff --git a/test/functional/services/common/browser.ts b/test/functional/services/common/browser.ts index f5fb54c72177fb..b3b7fd32eae19f 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/x-pack/plugins/alerts/server/alert_instance/alert_instance.ts b/x-pack/plugins/alerts/server/alert_instance/alert_instance.ts index 661fb75f81c007..01790b2a4a0c08 100644 --- a/x-pack/plugins/alerts/server/alert_instance/alert_instance.ts +++ b/x-pack/plugins/alerts/server/alert_instance/alert_instance.ts @@ -51,6 +51,10 @@ export class AlertInstance< return false; } + getLastScheduledActions() { + return this.meta.lastScheduledActions; + } + getScheduledActionOptions() { return this.scheduledExecutionOptions; } diff --git a/x-pack/plugins/alerts/server/task_runner/create_execution_handler.test.ts b/x-pack/plugins/alerts/server/task_runner/create_execution_handler.test.ts index 2f0754d34492fb..ed73fec24db261 100644 --- a/x-pack/plugins/alerts/server/task_runner/create_execution_handler.test.ts +++ b/x-pack/plugins/alerts/server/task_runner/create_execution_handler.test.ts @@ -113,6 +113,7 @@ test('enqueues execution per selected action', async () => { }, "kibana": Object { "alerting": Object { + "action_group_id": "default", "instance_id": "2", }, "saved_objects": Array [ diff --git a/x-pack/plugins/alerts/server/task_runner/create_execution_handler.ts b/x-pack/plugins/alerts/server/task_runner/create_execution_handler.ts index 21e642d228b4dc..f49310c42c247a 100644 --- a/x-pack/plugins/alerts/server/task_runner/create_execution_handler.ts +++ b/x-pack/plugins/alerts/server/task_runner/create_execution_handler.ts @@ -116,6 +116,7 @@ export function createExecutionHandler({ kibana: { alerting: { instance_id: alertInstanceId, + action_group_id: actionGroup, }, saved_objects: [ { rel: SAVED_OBJECT_REL_PRIMARY, type: 'alert', id: alertId, ...namespace }, diff --git a/x-pack/plugins/alerts/server/task_runner/task_runner.test.ts b/x-pack/plugins/alerts/server/task_runner/task_runner.test.ts index 4d0d69010914e2..859b6ec4362cef 100644 --- a/x-pack/plugins/alerts/server/task_runner/task_runner.test.ts +++ b/x-pack/plugins/alerts/server/task_runner/task_runner.test.ts @@ -184,11 +184,15 @@ describe('Task Runner', () => { expect(eventLogger.logEvent).toHaveBeenCalledTimes(1); expect(eventLogger.logEvent.mock.calls[0][0]).toMatchInlineSnapshot(` Object { + "@timestamp": "1970-01-01T00:00:00.000Z", "event": Object { "action": "execute", "outcome": "success", }, "kibana": Object { + "alerting": Object { + "status": "ok", + }, "saved_objects": Array [ Object { "id": "1", @@ -249,29 +253,13 @@ describe('Task Runner', () => { const eventLogger = taskRunnerFactoryInitializerParams.eventLogger; expect(eventLogger.logEvent).toHaveBeenCalledTimes(4); - expect(eventLogger.logEvent).toHaveBeenCalledWith({ - event: { - action: 'execute', - outcome: 'success', - }, - kibana: { - saved_objects: [ - { - id: '1', - namespace: undefined, - rel: 'primary', - type: 'alert', - }, - ], - }, - message: "alert executed: test:1: 'alert-name'", - }); - expect(eventLogger.logEvent).toHaveBeenCalledWith({ + expect(eventLogger.logEvent).toHaveBeenNthCalledWith(1, { event: { action: 'new-instance', }, kibana: { alerting: { + action_group_id: 'default', instance_id: '1', }, saved_objects: [ @@ -285,7 +273,7 @@ describe('Task Runner', () => { }, message: "test:1: 'alert-name' created new instance: '1'", }); - expect(eventLogger.logEvent).toHaveBeenCalledWith({ + expect(eventLogger.logEvent).toHaveBeenNthCalledWith(2, { event: { action: 'active-instance', }, @@ -305,13 +293,14 @@ describe('Task Runner', () => { }, message: "test:1: 'alert-name' active instance: '1' in actionGroup: 'default'", }); - expect(eventLogger.logEvent).toHaveBeenCalledWith({ + expect(eventLogger.logEvent).toHaveBeenNthCalledWith(3, { event: { action: 'execute-action', }, kibana: { alerting: { instance_id: '1', + action_group_id: 'default', }, saved_objects: [ { @@ -330,6 +319,27 @@ describe('Task Runner', () => { message: "alert: test:1: 'alert-name' instanceId: '1' scheduled actionGroup: 'default' action: action:1", }); + expect(eventLogger.logEvent).toHaveBeenNthCalledWith(4, { + '@timestamp': '1970-01-01T00:00:00.000Z', + event: { + action: 'execute', + outcome: 'success', + }, + kibana: { + alerting: { + status: 'active', + }, + saved_objects: [ + { + id: '1', + namespace: undefined, + rel: 'primary', + type: 'alert', + }, + ], + }, + message: "alert executed: test:1: 'alert-name'", + }); }); test('includes the apiKey in the request used to initialize the actionsClient', async () => { @@ -402,10 +412,13 @@ describe('Task Runner', () => { Array [ Object { "event": Object { - "action": "execute", - "outcome": "success", + "action": "new-instance", }, "kibana": Object { + "alerting": Object { + "action_group_id": "default", + "instance_id": "1", + }, "saved_objects": Array [ Object { "id": "1", @@ -415,17 +428,17 @@ describe('Task Runner', () => { }, ], }, - "message": "alert executed: test:1: 'alert-name'", + "message": "test:1: 'alert-name' created new instance: '1'", }, ], Array [ Object { "event": Object { - "action": "new-instance", + "action": "active-instance", }, "kibana": Object { "alerting": Object { - "action_group_id": undefined, + "action_group_id": "default", "instance_id": "1", }, "saved_objects": Array [ @@ -437,13 +450,13 @@ describe('Task Runner', () => { }, ], }, - "message": "test:1: 'alert-name' created new instance: '1'", + "message": "test:1: 'alert-name' active instance: '1' in actionGroup: 'default'", }, ], Array [ Object { "event": Object { - "action": "active-instance", + "action": "execute-action", }, "kibana": Object { "alerting": Object { @@ -457,19 +470,26 @@ describe('Task Runner', () => { "rel": "primary", "type": "alert", }, + Object { + "id": "1", + "namespace": undefined, + "type": "action", + }, ], }, - "message": "test:1: 'alert-name' active instance: '1' in actionGroup: 'default'", + "message": "alert: test:1: 'alert-name' instanceId: '1' scheduled actionGroup: 'default' action: action:1", }, ], Array [ Object { + "@timestamp": "1970-01-01T00:00:00.000Z", "event": Object { - "action": "execute-action", + "action": "execute", + "outcome": "success", }, "kibana": Object { "alerting": Object { - "instance_id": "1", + "status": "active", }, "saved_objects": Array [ Object { @@ -478,14 +498,9 @@ describe('Task Runner', () => { "rel": "primary", "type": "alert", }, - Object { - "id": "1", - "namespace": undefined, - "type": "action", - }, ], }, - "message": "alert: test:1: 'alert-name' instanceId: '1' scheduled actionGroup: 'default' action: action:1", + "message": "alert executed: test:1: 'alert-name'", }, ], ] @@ -498,6 +513,7 @@ describe('Task Runner', () => { executorServices.alertInstanceFactory('1').scheduleActions('default'); } ); + const date = new Date().toISOString(); const taskRunner = new TaskRunner( alertType, { @@ -505,8 +521,14 @@ describe('Task Runner', () => { state: { ...mockedTaskInstance.state, alertInstances: { - '1': { meta: {}, state: { bar: false } }, - '2': { meta: {}, state: { bar: false } }, + '1': { + meta: { lastScheduledActions: { group: 'default', date } }, + state: { bar: false }, + }, + '2': { + meta: { lastScheduledActions: { group: 'default', date } }, + state: { bar: false }, + }, }, }, }, @@ -545,10 +567,13 @@ describe('Task Runner', () => { Array [ Object { "event": Object { - "action": "execute", - "outcome": "success", + "action": "resolved-instance", }, "kibana": Object { + "alerting": Object { + "action_group_id": "default", + "instance_id": "2", + }, "saved_objects": Array [ Object { "id": "1", @@ -558,18 +583,18 @@ describe('Task Runner', () => { }, ], }, - "message": "alert executed: test:1: 'alert-name'", + "message": "test:1: 'alert-name' resolved instance: '2'", }, ], Array [ Object { "event": Object { - "action": "resolved-instance", + "action": "active-instance", }, "kibana": Object { "alerting": Object { - "action_group_id": undefined, - "instance_id": "2", + "action_group_id": "default", + "instance_id": "1", }, "saved_objects": Array [ Object { @@ -580,18 +605,19 @@ describe('Task Runner', () => { }, ], }, - "message": "test:1: 'alert-name' resolved instance: '2'", + "message": "test:1: 'alert-name' active instance: '1' in actionGroup: 'default'", }, ], Array [ Object { + "@timestamp": "1970-01-01T00:00:00.000Z", "event": Object { - "action": "active-instance", + "action": "execute", + "outcome": "success", }, "kibana": Object { "alerting": Object { - "action_group_id": "default", - "instance_id": "1", + "status": "active", }, "saved_objects": Array [ Object { @@ -602,7 +628,7 @@ describe('Task Runner', () => { }, ], }, - "message": "test:1: 'alert-name' active instance: '1' in actionGroup: 'default'", + "message": "alert executed: test:1: 'alert-name'", }, ], ] @@ -787,14 +813,19 @@ describe('Task Runner', () => { Array [ Array [ Object { + "@timestamp": "1970-01-01T00:00:00.000Z", "error": Object { "message": "OMG", }, "event": Object { "action": "execute", "outcome": "failure", + "reason": "execute", }, "kibana": Object { + "alerting": Object { + "status": "error", + }, "saved_objects": Array [ Object { "id": "1", @@ -834,6 +865,40 @@ describe('Task Runner', () => { "state": Object {}, } `); + + const eventLogger = taskRunnerFactoryInitializerParams.eventLogger; + expect(eventLogger.logEvent).toHaveBeenCalledTimes(1); + expect(eventLogger.logEvent.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + Object { + "@timestamp": "1970-01-01T00:00:00.000Z", + "error": Object { + "message": "OMG", + }, + "event": Object { + "action": "execute", + "outcome": "failure", + "reason": "decrypt", + }, + "kibana": Object { + "alerting": Object { + "status": "error", + }, + "saved_objects": Array [ + Object { + "id": "1", + "namespace": undefined, + "rel": "primary", + "type": "alert", + }, + ], + }, + "message": "test:1: execution failed", + }, + ], + ] + `); }); test('recovers gracefully when the Alert Task Runner throws an exception when getting internal Services', async () => { @@ -867,6 +932,40 @@ describe('Task Runner', () => { "state": Object {}, } `); + + const eventLogger = taskRunnerFactoryInitializerParams.eventLogger; + expect(eventLogger.logEvent).toHaveBeenCalledTimes(1); + expect(eventLogger.logEvent.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + Object { + "@timestamp": "1970-01-01T00:00:00.000Z", + "error": Object { + "message": "OMG", + }, + "event": Object { + "action": "execute", + "outcome": "failure", + "reason": "unknown", + }, + "kibana": Object { + "alerting": Object { + "status": "error", + }, + "saved_objects": Array [ + Object { + "id": "1", + "namespace": undefined, + "rel": "primary", + "type": "alert", + }, + ], + }, + "message": "test:1: execution failed", + }, + ], + ] + `); }); test('recovers gracefully when the Alert Task Runner throws an exception when fetching attributes', async () => { @@ -899,6 +998,40 @@ describe('Task Runner', () => { "state": Object {}, } `); + + const eventLogger = taskRunnerFactoryInitializerParams.eventLogger; + expect(eventLogger.logEvent).toHaveBeenCalledTimes(1); + expect(eventLogger.logEvent.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + Object { + "@timestamp": "1970-01-01T00:00:00.000Z", + "error": Object { + "message": "OMG", + }, + "event": Object { + "action": "execute", + "outcome": "failure", + "reason": "read", + }, + "kibana": Object { + "alerting": Object { + "status": "error", + }, + "saved_objects": Array [ + Object { + "id": "1", + "namespace": undefined, + "rel": "primary", + "type": "alert", + }, + ], + }, + "message": "test:1: execution failed", + }, + ], + ] + `); }); test('recovers gracefully when the Runner of a legacy Alert task which has no schedule throws an exception when fetching attributes', async () => { diff --git a/x-pack/plugins/alerts/server/task_runner/task_runner.ts b/x-pack/plugins/alerts/server/task_runner/task_runner.ts index 86bf7006e8d09e..5bccf5c497a60c 100644 --- a/x-pack/plugins/alerts/server/task_runner/task_runner.ts +++ b/x-pack/plugins/alerts/server/task_runner/task_runner.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import type { PublicMethodsOf } from '@kbn/utility-types'; -import { Dictionary, pickBy, mapValues, without } from 'lodash'; +import { Dictionary, pickBy, mapValues, without, cloneDeep } from 'lodash'; import { Logger, KibanaRequest } from '../../../../../src/core/server'; import { TaskRunnerContext } from './task_runner_factory'; import { ConcreteTaskInstance, throwUnrecoverableError } from '../../../task_manager/server'; @@ -40,6 +40,8 @@ import { partiallyUpdateAlert } from '../saved_objects'; const FALLBACK_RETRY_INTERVAL = '5m'; +type Event = Exclude; + interface AlertTaskRunResult { state: AlertTaskState; schedule: IntervalSchedule | undefined; @@ -153,7 +155,8 @@ export class TaskRunner { alert: SanitizedAlert, params: AlertExecutorOptions['params'], executionHandler: ReturnType, - spaceId: string + spaceId: string, + event: Event ): Promise { const { throttle, muteAll, mutedInstanceIds, name, tags, createdBy, updatedBy } = alert; const { @@ -166,24 +169,10 @@ export class TaskRunner { alertRawInstances, (rawAlertInstance) => new AlertInstance(rawAlertInstance) ); + const originalAlertInstances = cloneDeep(alertInstances); - const originalAlertInstanceIds = Object.keys(alertInstances); const eventLogger = this.context.eventLogger; const alertLabel = `${this.alertType.id}:${alertId}: '${name}'`; - const event: IEvent = { - event: { action: EVENT_LOG_ACTIONS.execute }, - kibana: { - saved_objects: [ - { - rel: SAVED_OBJECT_REL_PRIMARY, - type: 'alert', - id: alertId, - namespace, - }, - ], - }, - }; - eventLogger.startTiming(event); let updatedAlertTypeState: void | Record; try { @@ -205,21 +194,17 @@ export class TaskRunner { updatedBy, }); } catch (err) { - eventLogger.stopTiming(event); event.message = `alert execution failure: ${alertLabel}`; event.error = event.error || {}; event.error.message = err.message; event.event = event.event || {}; event.event.outcome = 'failure'; - eventLogger.logEvent(event); throw new ErrorWithReason(AlertExecutionStatusErrorReasons.Execute, err); } - eventLogger.stopTiming(event); event.message = `alert executed: ${alertLabel}`; event.event = event.event || {}; event.event.outcome = 'success'; - eventLogger.logEvent(event); // Cleanup alert instances that are no longer scheduling actions to avoid over populating the alertInstances object const instancesWithScheduledActions = pickBy(alertInstances, (alertInstance: AlertInstance) => @@ -227,7 +212,7 @@ export class TaskRunner { ); generateNewAndResolvedInstanceEvents({ eventLogger, - originalAlertInstanceIds, + originalAlertInstances, currentAlertInstances: instancesWithScheduledActions, alertId, alertLabel, @@ -261,7 +246,8 @@ export class TaskRunner { async validateAndExecuteAlert( services: Services, apiKey: RawAlert['apiKey'], - alert: SanitizedAlert + alert: SanitizedAlert, + event: Event ) { const { params: { alertId, spaceId }, @@ -278,10 +264,17 @@ export class TaskRunner { alert.actions, alert.params ); - return this.executeAlertInstances(services, alert, validatedParams, executionHandler, spaceId); + return this.executeAlertInstances( + services, + alert, + validatedParams, + executionHandler, + spaceId, + event + ); } - async loadAlertAttributesAndRun(): Promise> { + async loadAlertAttributesAndRun(event: Event): Promise> { const { params: { alertId, spaceId }, } = this.taskInstance; @@ -304,7 +297,7 @@ export class TaskRunner { return { state: await promiseResult( - this.validateAndExecuteAlert(services, apiKey, alert) + this.validateAndExecuteAlert(services, apiKey, alert, event) ), schedule: asOk( // fetch the alert again to ensure we return the correct schedule as it may have @@ -322,18 +315,65 @@ export class TaskRunner { schedule: taskSchedule, } = this.taskInstance; - const { state, schedule } = await errorAsAlertTaskRunResult(this.loadAlertAttributesAndRun()); - const namespace = spaceId === 'default' ? undefined : spaceId; + const namespace = this.context.spaceIdToNamespace(spaceId); + const eventLogger = this.context.eventLogger; + const event: IEvent = { + // explicitly set execute timestamp so it will be before other events + // generated here (new-instance, schedule-action, etc) + '@timestamp': new Date().toISOString(), + event: { action: EVENT_LOG_ACTIONS.execute }, + kibana: { + saved_objects: [ + { + rel: SAVED_OBJECT_REL_PRIMARY, + type: 'alert', + id: alertId, + namespace, + }, + ], + }, + }; + eventLogger.startTiming(event); + + const { state, schedule } = await errorAsAlertTaskRunResult( + this.loadAlertAttributesAndRun(event) + ); const executionStatus: AlertExecutionStatus = map( state, (alertTaskState: AlertTaskState) => executionStatusFromState(alertTaskState), (err: Error) => executionStatusFromError(err) ); + + // set the executionStatus date to same as event, if it's set + if (event.event?.start) { + executionStatus.lastExecutionDate = new Date(event.event.start); + } + this.logger.debug( `alertExecutionStatus for ${this.alertType.id}:${alertId}: ${JSON.stringify(executionStatus)}` ); + eventLogger.stopTiming(event); + event.kibana = event.kibana || {}; + event.kibana.alerting = event.kibana.alerting || {}; + event.kibana.alerting.status = executionStatus.status; + + // if executionStatus indicates an error, fill in fields in + // event from it + if (executionStatus.error) { + event.event = event.event || {}; + event.event.reason = executionStatus.error?.reason || 'unknown'; + event.event.outcome = 'failure'; + event.error = event.error || {}; + event.error.message = event.error.message || executionStatus.error.message; + if (!event.message) { + event.message = `${this.alertType.id}:${alertId}: execution failed`; + } + } + + eventLogger.logEvent(event); + const client = this.context.internalSavedObjectsRepository; const attributes = { executionStatus: alertExecutionStatusToRaw(executionStatus), @@ -381,7 +421,7 @@ export class TaskRunner { interface GenerateNewAndResolvedInstanceEventsParams { eventLogger: IEventLogger; - originalAlertInstanceIds: string[]; + originalAlertInstances: Dictionary; currentAlertInstances: Dictionary; alertId: string; alertLabel: string; @@ -389,26 +429,23 @@ interface GenerateNewAndResolvedInstanceEventsParams { } function generateNewAndResolvedInstanceEvents(params: GenerateNewAndResolvedInstanceEventsParams) { - const { - eventLogger, - alertId, - namespace, - currentAlertInstances, - originalAlertInstanceIds, - } = params; + const { eventLogger, alertId, namespace, currentAlertInstances, originalAlertInstances } = params; + const originalAlertInstanceIds = Object.keys(originalAlertInstances); const currentAlertInstanceIds = Object.keys(currentAlertInstances); const newIds = without(currentAlertInstanceIds, ...originalAlertInstanceIds); const resolvedIds = without(originalAlertInstanceIds, ...currentAlertInstanceIds); for (const id of resolvedIds) { + const actionGroup = originalAlertInstances[id].getLastScheduledActions()?.group; const message = `${params.alertLabel} resolved instance: '${id}'`; - logInstanceEvent(id, EVENT_LOG_ACTIONS.resolvedInstance, message); + logInstanceEvent(id, EVENT_LOG_ACTIONS.resolvedInstance, message, actionGroup); } for (const id of newIds) { + const actionGroup = currentAlertInstances[id].getScheduledActionOptions()?.actionGroup; const message = `${params.alertLabel} created new instance: '${id}'`; - logInstanceEvent(id, EVENT_LOG_ACTIONS.newInstance, message); + logInstanceEvent(id, EVENT_LOG_ACTIONS.newInstance, message, actionGroup); } for (const id of currentAlertInstanceIds) { @@ -425,7 +462,7 @@ function generateNewAndResolvedInstanceEvents(params: GenerateNewAndResolvedInst kibana: { alerting: { instance_id: instanceId, - action_group_id: group, + ...(group ? { action_group_id: group } : {}), }, saved_objects: [ { 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 7e6331c1fa3a8b..394b4caea3e7b9 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 a53468e2ad06c1..b90f606d276eb1 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 368837b3c94115..d75446cb0dd487 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 00000000000000..1713ef61fac1ee --- /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 97700b9bc96b77..75b7835c13151b 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 d8f54c7bfc94fd..dfc3d6b4b9ec8d 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 4e3217ce17ed11..2e1f259bd8c420 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/Home/__snapshots__/Home.test.tsx.snap b/x-pack/plugins/apm/public/components/app/Home/__snapshots__/Home.test.tsx.snap index 0a22604837b975..82fabff6101919 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 7bd9b2c87814b6..1d314e3a0e0d36 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 77afe92a8f5210..b757635af1702e 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 e5643325833759..6d259a5a2e48c7 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 bc45d58329f496..a1cdf7bb646e5d 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 3a8d24f0a8b02d..00000000000000 --- 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 8825702cafd519..aa5dcd5a5ea18b 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 c8639b334f66aa..fc478e27ccac39 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 e770116ac27593..c9c577285ee807 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/service_inventory/service_inventory.test.tsx b/x-pack/plugins/apm/public/components/app/service_inventory/service_inventory.test.tsx index d394c7db625545..247e91fb438ef5 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/shared/ApmHeader/index.tsx b/x-pack/plugins/apm/public/components/shared/ApmHeader/index.tsx index 6674abe9b8ce53..96f170fa6a093c 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 74d7ace20dae09..e7dd03db6b63cf 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 @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { ESFilter } from '../../../../../../typings/elasticsearch'; import { ERROR_GROUP_ID, PROCESSOR_EVENT, @@ -12,7 +13,6 @@ import { TRANSACTION_TYPE, } from '../../../../common/elasticsearch_fieldnames'; import { UIProcessorEvent } from '../../../../common/processor_event'; -import { ESFilter } from '../../../../typings/elasticsearch'; import { IUrlParams } from '../../../context/UrlParamsContext/types'; export function getBoolFilter({ @@ -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 157e014bee4247..dce8e49deec414 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/MachineLearningLinks/MLLink.test.tsx b/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLLink.test.tsx index be00364cab92e6..30d4bb34ea3455 100644 --- a/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLLink.test.tsx +++ b/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLLink.test.tsx @@ -19,6 +19,6 @@ test('MLLink produces the correct URL', async () => { ); expect(href).toMatchInlineSnapshot( - `"/app/ml/jobs?mlManagement=(groupIds:!(apm),jobId:!(something))&_g=(refreshInterval:(pause:!t,value:0),time:(from:now-5h,to:now-2h))"` + `"/app/ml/jobs?_a=(queryText:'id:(something)%20groups:(apm)')&_g=(refreshInterval:(pause:!t,value:0),time:(from:now-5h,to:now-2h))"` ); }); 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 a5bcec1501ad3f..0ff73d91d7c5b0 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/context/ApmPluginContext/MockApmPluginContext.tsx b/x-pack/plugins/apm/public/context/ApmPluginContext/MockApmPluginContext.tsx index 3b915045f54b65..25e7f23a00125a 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 39d961f6a81647..44952e64db59c6 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/utils/testHelpers.tsx b/x-pack/plugins/apm/public/utils/testHelpers.tsx index f990c4387ddf12..87dfeb95b63263 100644 --- a/x-pack/plugins/apm/public/utils/testHelpers.tsx +++ b/x-pack/plugins/apm/public/utils/testHelpers.tsx @@ -6,26 +6,26 @@ /* global jest */ -import React from 'react'; -import { ReactWrapper, mount, MountRendererProps } from 'enzyme'; +import { render, waitFor } from '@testing-library/react'; +import { mount, MountRendererProps, ReactWrapper } from 'enzyme'; import enzymeToJson from 'enzyme-to-json'; import { Location } from 'history'; import moment from 'moment'; import { Moment } from 'moment-timezone'; -import { render, waitFor } from '@testing-library/react'; +import React from 'react'; import { MemoryRouter } from 'react-router-dom'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { APMConfig } from '../../server'; -import { PromiseReturnType } from '../../typings/common'; -import { EuiThemeProvider } from '../../../observability/public'; import { ESFilter, - ESSearchResponse, ESSearchRequest, -} from '../../typings/elasticsearch'; + ESSearchResponse, +} from '../../../../typings/elasticsearch'; +import { EuiThemeProvider } from '../../../observability/public'; +import { PromiseReturnType } from '../../../observability/typings/common'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { APMConfig } from '../../server'; +import { UIFilters } from '../../typings/ui_filters'; import { MockApmPluginContextWrapper } from '../context/ApmPluginContext/MockApmPluginContext'; import { UrlParamsProvider } from '../context/UrlParamsContext'; -import { UIFilters } from '../../typings/ui_filters'; const originalConsoleWarn = console.warn; // eslint-disable-line no-console /** diff --git a/x-pack/plugins/apm/scripts/shared/get_es_client.ts b/x-pack/plugins/apm/scripts/shared/get_es_client.ts index 86dfd92190fdf1..912c8943b1edfc 100644 --- a/x-pack/plugins/apm/scripts/shared/get_es_client.ts +++ b/x-pack/plugins/apm/scripts/shared/get_es_client.ts @@ -6,7 +6,10 @@ import { Client } from '@elastic/elasticsearch'; import { ApiKeyAuth, BasicAuth } from '@elastic/elasticsearch/lib/pool'; -import { ESSearchResponse, ESSearchRequest } from '../../typings/elasticsearch'; +import { + ESSearchResponse, + ESSearchRequest, +} from '../../../../typings/elasticsearch'; export type ESClient = ReturnType; diff --git a/x-pack/plugins/apm/server/lib/alerts/register_error_count_alert_type.ts b/x-pack/plugins/apm/server/lib/alerts/register_error_count_alert_type.ts index ecda5b0e8504bf..464a737c50ea2a 100644 --- a/x-pack/plugins/apm/server/lib/alerts/register_error_count_alert_type.ts +++ b/x-pack/plugins/apm/server/lib/alerts/register_error_count_alert_type.ts @@ -9,6 +9,7 @@ import { isEmpty } from 'lodash'; import { Observable } from 'rxjs'; import { take } from 'rxjs/operators'; import { APMConfig } from '../..'; +import { ESSearchResponse } from '../../../../../typings/elasticsearch'; import { AlertingPlugin } from '../../../../alerts/server'; import { AlertType, ALERT_TYPES_CONFIG } from '../../../common/alert_types'; import { @@ -17,7 +18,6 @@ import { SERVICE_NAME, } from '../../../common/elasticsearch_fieldnames'; import { ProcessorEvent } from '../../../common/processor_event'; -import { ESSearchResponse } from '../../../typings/elasticsearch'; import { getEnvironmentUiFilterES } from '../helpers/convert_ui_filters/get_environment_ui_filter_es'; import { getApmIndices } from '../settings/apm_indices/get_apm_indices'; import { apmActionVariables } from './action_variables'; diff --git a/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_alert_type.ts b/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_alert_type.ts index d9e69c8f3b7d7c..602ee99970f8ae 100644 --- a/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_alert_type.ts +++ b/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_alert_type.ts @@ -7,21 +7,21 @@ import { schema } from '@kbn/config-schema'; import { Observable } from 'rxjs'; import { take } from 'rxjs/operators'; -import { getDurationFormatter } from '../../../common/utils/formatters'; -import { ProcessorEvent } from '../../../common/processor_event'; +import { APMConfig } from '../..'; +import { ESSearchResponse } from '../../../../../typings/elasticsearch'; +import { AlertingPlugin } from '../../../../alerts/server'; import { AlertType, ALERT_TYPES_CONFIG } from '../../../common/alert_types'; -import { ESSearchResponse } from '../../../typings/elasticsearch'; import { PROCESSOR_EVENT, + SERVICE_ENVIRONMENT, SERVICE_NAME, - TRANSACTION_TYPE, TRANSACTION_DURATION, - SERVICE_ENVIRONMENT, + TRANSACTION_TYPE, } from '../../../common/elasticsearch_fieldnames'; -import { AlertingPlugin } from '../../../../alerts/server'; -import { getApmIndices } from '../settings/apm_indices/get_apm_indices'; -import { APMConfig } from '../..'; +import { ProcessorEvent } from '../../../common/processor_event'; +import { getDurationFormatter } from '../../../common/utils/formatters'; import { getEnvironmentUiFilterES } from '../helpers/convert_ui_filters/get_environment_ui_filter_es'; +import { getApmIndices } from '../settings/apm_indices/get_apm_indices'; import { apmActionVariables } from './action_variables'; interface RegisterAlertParams { diff --git a/x-pack/plugins/apm/server/lib/alerts/register_transaction_error_rate_alert_type.ts b/x-pack/plugins/apm/server/lib/alerts/register_transaction_error_rate_alert_type.ts index 06b296db5a4853..0506e1b4c3aed6 100644 --- a/x-pack/plugins/apm/server/lib/alerts/register_transaction_error_rate_alert_type.ts +++ b/x-pack/plugins/apm/server/lib/alerts/register_transaction_error_rate_alert_type.ts @@ -5,25 +5,25 @@ */ import { schema } from '@kbn/config-schema'; +import { isEmpty } from 'lodash'; import { Observable } from 'rxjs'; import { take } from 'rxjs/operators'; -import { isEmpty } from 'lodash'; -import { asDecimalOrInteger } from '../../../common/utils/formatters'; -import { ProcessorEvent } from '../../../common/processor_event'; -import { EventOutcome } from '../../../common/event_outcome'; +import { APMConfig } from '../..'; +import { ESSearchResponse } from '../../../../../typings/elasticsearch'; +import { AlertingPlugin } from '../../../../alerts/server'; import { AlertType, ALERT_TYPES_CONFIG } from '../../../common/alert_types'; -import { ESSearchResponse } from '../../../typings/elasticsearch'; import { + EVENT_OUTCOME, PROCESSOR_EVENT, + SERVICE_ENVIRONMENT, SERVICE_NAME, TRANSACTION_TYPE, - EVENT_OUTCOME, - SERVICE_ENVIRONMENT, } from '../../../common/elasticsearch_fieldnames'; -import { AlertingPlugin } from '../../../../alerts/server'; -import { getApmIndices } from '../settings/apm_indices/get_apm_indices'; -import { APMConfig } from '../..'; +import { EventOutcome } from '../../../common/event_outcome'; +import { ProcessorEvent } from '../../../common/processor_event'; +import { asDecimalOrInteger } from '../../../common/utils/formatters'; import { getEnvironmentUiFilterES } from '../helpers/convert_ui_filters/get_environment_ui_filter_es'; +import { getApmIndices } from '../settings/apm_indices/get_apm_indices'; import { apmActionVariables } from './action_variables'; interface RegisterAlertParams { diff --git a/x-pack/plugins/apm/server/lib/apm_telemetry/collect_data_telemetry/index.ts b/x-pack/plugins/apm/server/lib/apm_telemetry/collect_data_telemetry/index.ts index e53a0e24b4723d..730645c609cb60 100644 --- a/x-pack/plugins/apm/server/lib/apm_telemetry/collect_data_telemetry/index.ts +++ b/x-pack/plugins/apm/server/lib/apm_telemetry/collect_data_telemetry/index.ts @@ -9,7 +9,7 @@ import { IndicesStatsParams, Client } from 'elasticsearch'; import { ESSearchRequest, ESSearchResponse, -} from '../../../../typings/elasticsearch'; +} from '../../../../../../typings/elasticsearch'; import { ApmIndicesConfig } from '../../settings/apm_indices/get_apm_indices'; import { tasks } from './tasks'; import { APMDataTelemetry } from '../types'; diff --git a/x-pack/plugins/apm/server/lib/apm_telemetry/collect_data_telemetry/tasks.ts b/x-pack/plugins/apm/server/lib/apm_telemetry/collect_data_telemetry/tasks.ts index ac82d353417b49..ec2eb7c8dd99a4 100644 --- a/x-pack/plugins/apm/server/lib/apm_telemetry/collect_data_telemetry/tasks.ts +++ b/x-pack/plugins/apm/server/lib/apm_telemetry/collect_data_telemetry/tasks.ts @@ -5,7 +5,7 @@ */ import { ValuesType } from 'utility-types'; import { flatten, merge, sortBy, sum, pickBy } from 'lodash'; -import { AggregationOptionsByType } from '../../../../typings/elasticsearch/aggregations'; +import { AggregationOptionsByType } from '../../../../../../typings/elasticsearch/aggregations'; import { ProcessorEvent } from '../../../../common/processor_event'; import { TelemetryTask } from '.'; import { AGENT_NAMES, RUM_AGENT_NAMES } from '../../../../common/agent_name'; 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 b12dd73a20986b..9e18c81e4aaeb4 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 @@ -4,12 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ProcessorEvent } from '../../../../common/processor_event'; -import { ESFilter } from '../../../../typings/elasticsearch'; +import { ESFilter } from '../../../../../../typings/elasticsearch'; import { ERROR_GROUP_ID, SERVICE_NAME, } from '../../../../common/elasticsearch_fieldnames'; +import { ProcessorEvent } from '../../../../common/processor_event'; import { rangeFilter } from '../../../../common/utils/range_filter'; import { Setup, SetupTimeRange } from '../../helpers/setup_request'; diff --git a/x-pack/plugins/apm/server/lib/errors/distribution/get_distribution.ts b/x-pack/plugins/apm/server/lib/errors/distribution/get_distribution.ts index dea518cad8e405..7866c993534518 100644 --- a/x-pack/plugins/apm/server/lib/errors/distribution/get_distribution.ts +++ b/x-pack/plugins/apm/server/lib/errors/distribution/get_distribution.ts @@ -4,10 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import { PromiseReturnType } from '../../../../typings/common'; +import { PromiseReturnType } from '../../../../../observability/typings/common'; import { Setup, SetupTimeRange } from '../../helpers/setup_request'; -import { getBuckets } from './get_buckets'; import { BUCKET_TARGET_COUNT } from '../../transactions/constants'; +import { getBuckets } from './get_buckets'; function getBucketSize({ start, end }: SetupTimeRange) { return Math.floor((end - start) / BUCKET_TARGET_COUNT); diff --git a/x-pack/plugins/apm/server/lib/errors/get_error_group.ts b/x-pack/plugins/apm/server/lib/errors/get_error_group.ts index 0fbc7720f7111f..37be72beedeb1c 100644 --- a/x-pack/plugins/apm/server/lib/errors/get_error_group.ts +++ b/x-pack/plugins/apm/server/lib/errors/get_error_group.ts @@ -4,13 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ProcessorEvent } from '../../../common/processor_event'; +import { PromiseReturnType } from '../../../../observability/typings/common'; import { ERROR_GROUP_ID, SERVICE_NAME, TRANSACTION_SAMPLED, } from '../../../common/elasticsearch_fieldnames'; -import { PromiseReturnType } from '../../../typings/common'; +import { ProcessorEvent } from '../../../common/processor_event'; import { rangeFilter } from '../../../common/utils/range_filter'; import { Setup, SetupTimeRange } from '../helpers/setup_request'; import { getTransaction } from '../transactions/get_transaction'; diff --git a/x-pack/plugins/apm/server/lib/errors/get_error_groups.ts b/x-pack/plugins/apm/server/lib/errors/get_error_groups.ts index 006d2fae3d4fb1..d734a1395fc5e4 100644 --- a/x-pack/plugins/apm/server/lib/errors/get_error_groups.ts +++ b/x-pack/plugins/apm/server/lib/errors/get_error_groups.ts @@ -4,6 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +import { SortOptions } from '../../../../../typings/elasticsearch/aggregations'; +import { PromiseReturnType } from '../../../../observability/typings/common'; import { ERROR_CULPRIT, ERROR_EXC_HANDLED, @@ -12,11 +14,9 @@ import { ERROR_GROUP_ID, ERROR_LOG_MESSAGE, } from '../../../common/elasticsearch_fieldnames'; -import { PromiseReturnType } from '../../../typings/common'; -import { Setup, SetupTimeRange } from '../helpers/setup_request'; import { getErrorGroupsProjection } from '../../projections/errors'; import { mergeProjection } from '../../projections/util/merge_projection'; -import { SortOptions } from '../../../typings/elasticsearch/aggregations'; +import { Setup, SetupTimeRange } from '../helpers/setup_request'; export type ErrorGroupListAPIResponse = PromiseReturnType< typeof getErrorGroups diff --git a/x-pack/plugins/apm/server/lib/helpers/convert_ui_filters/get_environment_ui_filter_es.ts b/x-pack/plugins/apm/server/lib/helpers/convert_ui_filters/get_environment_ui_filter_es.ts index ea8d02eb833cfd..dd9f0b0333c013 100644 --- a/x-pack/plugins/apm/server/lib/helpers/convert_ui_filters/get_environment_ui_filter_es.ts +++ b/x-pack/plugins/apm/server/lib/helpers/convert_ui_filters/get_environment_ui_filter_es.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ESFilter } from '../../../../typings/elasticsearch'; +import { ESFilter } from '../../../../../../typings/elasticsearch'; import { ENVIRONMENT_NOT_DEFINED, ENVIRONMENT_ALL, diff --git a/x-pack/plugins/apm/server/lib/helpers/convert_ui_filters/get_es_filter.ts b/x-pack/plugins/apm/server/lib/helpers/convert_ui_filters/get_es_filter.ts index 1b8f32d4de8b93..0592f13bdf54ff 100644 --- a/x-pack/plugins/apm/server/lib/helpers/convert_ui_filters/get_es_filter.ts +++ b/x-pack/plugins/apm/server/lib/helpers/convert_ui_filters/get_es_filter.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ESFilter } from '../../../../typings/elasticsearch'; +import { ESFilter } from '../../../../../../typings/elasticsearch'; import { UIFilters } from '../../../../typings/ui_filters'; import { getEnvironmentUiFilterES } from './get_environment_ui_filter_es'; import { diff --git a/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/add_filter_to_exclude_legacy_data.ts b/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/add_filter_to_exclude_legacy_data.ts index 494cd6cbf0eec0..921b46daee2a9d 100644 --- a/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/add_filter_to_exclude_legacy_data.ts +++ b/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/add_filter_to_exclude_legacy_data.ts @@ -9,7 +9,7 @@ import { OBSERVER_VERSION_MAJOR } from '../../../../../common/elasticsearch_fiel import { ESSearchRequest, ESFilter, -} from '../../../../../typings/elasticsearch'; +} from '../../../../../../../typings/elasticsearch'; /* Adds a range query to the ES request to exclude legacy data 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 9020cb1b9953a3..870997efb77dee 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,7 +5,6 @@ */ 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, @@ -15,12 +14,13 @@ import { ProcessorEvent } from '../../../../../common/processor_event'; import { ESSearchRequest, ESSearchResponse, -} from '../../../../../typings/elasticsearch'; +} from '../../../../../../../typings/elasticsearch'; import { ApmIndicesConfig } from '../../../settings/apm_indices/get_apm_indices'; 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 & { @@ -33,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]; diff --git a/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/unpack_processor_events.ts b/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/unpack_processor_events.ts index 736c7ad2d1089f..add522a5c7f84f 100644 --- a/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/unpack_processor_events.ts +++ b/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/unpack_processor_events.ts @@ -10,7 +10,7 @@ import { ProcessorEvent } from '../../../../../common/processor_event'; import { ESSearchRequest, ESFilter, -} from '../../../../../typings/elasticsearch'; +} from '../../../../../../../typings/elasticsearch'; import { APMEventESSearchRequest } from '.'; import { ApmIndicesConfig, diff --git a/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_internal_es_client/index.ts b/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_internal_es_client/index.ts index 072391606d574e..8e74a7992e9ea7 100644 --- a/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_internal_es_client/index.ts +++ b/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_internal_es_client/index.ts @@ -15,7 +15,7 @@ import { APMRequestHandlerContext } from '../../../../routes/typings'; import { ESSearchResponse, ESSearchRequest, -} from '../../../../../typings/elasticsearch'; +} from '../../../../../../../typings/elasticsearch'; import { callClientWithDebug } from '../call_client_with_debug'; // `type` was deprecated in 7.0 diff --git a/x-pack/plugins/apm/server/lib/helpers/get_internal_saved_objects_client.ts b/x-pack/plugins/apm/server/lib/helpers/get_internal_saved_objects_client.ts index e0bb9ad354f588..097538076de257 100644 --- a/x-pack/plugins/apm/server/lib/helpers/get_internal_saved_objects_client.ts +++ b/x-pack/plugins/apm/server/lib/helpers/get_internal_saved_objects_client.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import { CoreSetup } from 'src/core/server'; -import { PromiseReturnType } from '../../../typings/common'; +import { PromiseReturnType } from '../../../../observability/typings/common'; export type InternalSavedObjectsClient = PromiseReturnType< typeof getInternalSavedObjectsClient 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 363c4128137e02..65d36c8b36af88 100644 --- a/x-pack/plugins/apm/server/lib/helpers/setup_request.ts +++ b/x-pack/plugins/apm/server/lib/helpers/setup_request.ts @@ -4,20 +4,21 @@ * you may not use this file except in compliance with the Elastic License. */ -import moment from 'moment'; import { Logger } from 'kibana/server'; -import { isActivePlatinumLicense } from '../../../common/service_map'; -import { UI_SETTINGS } from '../../../../../../src/plugins/data/common'; -import { KibanaRequest } from '../../../../../../src/core/server'; +import moment from 'moment'; import { APMConfig } from '../..'; +import { KibanaRequest } from '../../../../../../src/core/server'; +import { UI_SETTINGS } from '../../../../../../src/plugins/data/common'; +import { ESFilter } from '../../../../../typings/elasticsearch'; +import { ProcessorEvent } from '../../../common/processor_event'; +import { isActivePlatinumLicense } from '../../../common/service_map'; +import { UIFilters } from '../../../typings/ui_filters'; +import { APMRequestHandlerContext } from '../../routes/typings'; import { - getApmIndices, ApmIndicesConfig, + getApmIndices, } from '../settings/apm_indices/get_apm_indices'; -import { ESFilter } from '../../../typings/elasticsearch'; import { getEsFilter } from './convert_ui_filters/get_es_filter'; -import { APMRequestHandlerContext } from '../../routes/typings'; -import { ProcessorEvent } from '../../../common/processor_event'; import { APMEventClient, createApmEventClient, @@ -26,7 +27,6 @@ import { APMInternalClient, createInternalESClient, } from './create_es_client/create_internal_es_client'; -import { UIFilters } from '../../../typings/ui_filters'; // Explicitly type Setup to prevent TS initialization errors // https://github.com/microsoft/TypeScript/issues/34933 diff --git a/x-pack/plugins/apm/server/lib/helpers/transaction_error_rate.ts b/x-pack/plugins/apm/server/lib/helpers/transaction_error_rate.ts index ccc7ba7e22b504..03a44e77ba2d3d 100644 --- a/x-pack/plugins/apm/server/lib/helpers/transaction_error_rate.ts +++ b/x-pack/plugins/apm/server/lib/helpers/transaction_error_rate.ts @@ -9,7 +9,7 @@ import { EventOutcome } from '../../../common/event_outcome'; import { AggregationOptionsByType, AggregationResultOf, -} from '../../../typings/elasticsearch/aggregations'; +} from '../../../../../typings/elasticsearch/aggregations'; import { getTransactionDurationFieldForAggregatedTransactions } from './aggregated_transactions'; export function getOutcomeAggregation({ diff --git a/x-pack/plugins/apm/server/lib/metrics/fetch_and_transform_metrics.ts b/x-pack/plugins/apm/server/lib/metrics/fetch_and_transform_metrics.ts index 3ccba8c7586dcf..57eae26593305b 100644 --- a/x-pack/plugins/apm/server/lib/metrics/fetch_and_transform_metrics.ts +++ b/x-pack/plugins/apm/server/lib/metrics/fetch_and_transform_metrics.ts @@ -4,15 +4,15 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Unionize, Overwrite } from 'utility-types'; -import { Setup, SetupTimeRange } from '../helpers/setup_request'; -import { getMetricsDateHistogramParams } from '../helpers/metrics'; -import { ChartBase } from './types'; -import { transformDataToMetricsChart } from './transform_metrics_chart'; +import { Overwrite, Unionize } from 'utility-types'; +import { AggregationOptionsByType } from '../../../../../typings/elasticsearch'; import { getMetricsProjection } from '../../projections/metrics'; import { mergeProjection } from '../../projections/util/merge_projection'; -import { AggregationOptionsByType } from '../../../typings/elasticsearch/aggregations'; import { APMEventESSearchRequest } from '../helpers/create_es_client/create_apm_event_client'; +import { getMetricsDateHistogramParams } from '../helpers/metrics'; +import { Setup, SetupTimeRange } from '../helpers/setup_request'; +import { transformDataToMetricsChart } from './transform_metrics_chart'; +import { ChartBase } from './types'; type MetricsAggregationMap = Unionize<{ min: AggregationOptionsByType['min']; diff --git a/x-pack/plugins/apm/server/lib/metrics/transform_metrics_chart.ts b/x-pack/plugins/apm/server/lib/metrics/transform_metrics_chart.ts index a191d5400e36cf..bcf8b412f350a0 100644 --- a/x-pack/plugins/apm/server/lib/metrics/transform_metrics_chart.ts +++ b/x-pack/plugins/apm/server/lib/metrics/transform_metrics_chart.ts @@ -4,10 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ import theme from '@elastic/eui/dist/eui_theme_light.json'; -import { ChartBase } from './types'; -import { ESSearchResponse } from '../../../typings/elasticsearch'; +import { ESSearchResponse } from '../../../../../typings/elasticsearch'; import { getVizColorForIndex } from '../../../common/viz_colors'; import { GenericMetricsRequest } from './fetch_and_transform_metrics'; +import { ChartBase } from './types'; export type GenericMetricsChart = ReturnType< typeof transformDataToMetricsChart diff --git a/x-pack/plugins/apm/server/lib/service_map/get_service_anomalies.ts b/x-pack/plugins/apm/server/lib/service_map/get_service_anomalies.ts index 5f4bc61af4c69d..8f8d7763970b7b 100644 --- a/x-pack/plugins/apm/server/lib/service_map/get_service_anomalies.ts +++ b/x-pack/plugins/apm/server/lib/service_map/get_service_anomalies.ts @@ -4,21 +4,21 @@ * you may not use this file except in compliance with the Elastic License. */ import Boom from '@hapi/boom'; +import { MlPluginSetup } from '../../../../ml/server'; +import { PromiseReturnType } from '../../../../observability/typings/common'; +import { + getSeverity, + ML_ERRORS, + ServiceAnomalyStats, +} from '../../../common/anomaly_detection'; +import { ENVIRONMENT_ALL } from '../../../common/environment_filter_values'; import { getServiceHealthStatus } from '../../../common/service_health_status'; -import { Setup, SetupTimeRange } from '../helpers/setup_request'; -import { PromiseReturnType } from '../../../typings/common'; import { TRANSACTION_PAGE_LOAD, TRANSACTION_REQUEST, } from '../../../common/transaction_types'; -import { - ServiceAnomalyStats, - getSeverity, - ML_ERRORS, -} from '../../../common/anomaly_detection'; import { getMlJobsWithAPMGroup } from '../anomaly_detection/get_ml_jobs_with_apm_group'; -import { ENVIRONMENT_ALL } from '../../../common/environment_filter_values'; -import { MlPluginSetup } from '../../../../ml/server'; +import { Setup, SetupTimeRange } from '../helpers/setup_request'; export const DEFAULT_ANOMALIES = { mlJobIds: [], serviceAnomalies: {} }; diff --git a/x-pack/plugins/apm/server/lib/service_map/get_service_map.ts b/x-pack/plugins/apm/server/lib/service_map/get_service_map.ts index 330bb936c9e889..ccebbfa44538a1 100644 --- a/x-pack/plugins/apm/server/lib/service_map/get_service_map.ts +++ b/x-pack/plugins/apm/server/lib/service_map/get_service_map.ts @@ -3,8 +3,9 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { chunk } from 'lodash'; import { Logger } from 'kibana/server'; +import { chunk } from 'lodash'; +import { PromiseReturnType } from '../../../../observability/typings/common'; import { AGENT_NAME, SERVICE_ENVIRONMENT, @@ -12,17 +13,16 @@ import { } from '../../../common/elasticsearch_fieldnames'; import { getServicesProjection } from '../../projections/services'; import { mergeProjection } from '../../projections/util/merge_projection'; -import { PromiseReturnType } from '../../../typings/common'; -import { Setup, SetupTimeRange } from '../helpers/setup_request'; import { getEnvironmentUiFilterES } from '../helpers/convert_ui_filters/get_environment_ui_filter_es'; -import { transformServiceMapResponses } from './transform_service_map_responses'; -import { getServiceMapFromTraceIds } from './get_service_map_from_trace_ids'; -import { getTraceSampleIds } from './get_trace_sample_ids'; +import { Setup, SetupTimeRange } from '../helpers/setup_request'; import { + DEFAULT_ANOMALIES, getServiceAnomalies, ServiceAnomaliesResponse, - DEFAULT_ANOMALIES, } from './get_service_anomalies'; +import { getServiceMapFromTraceIds } from './get_service_map_from_trace_ids'; +import { getTraceSampleIds } from './get_trace_sample_ids'; +import { transformServiceMapResponses } from './transform_service_map_responses'; export interface IEnvOptions { setup: Setup & SetupTimeRange; diff --git a/x-pack/plugins/apm/server/lib/service_map/get_service_map_service_node_info.ts b/x-pack/plugins/apm/server/lib/service_map/get_service_map_service_node_info.ts index 37b34641435fbc..82d339686f7ec2 100644 --- a/x-pack/plugins/apm/server/lib/service_map/get_service_map_service_node_info.ts +++ b/x-pack/plugins/apm/server/lib/service_map/get_service_map_service_node_info.ts @@ -4,33 +4,33 @@ * you may not use this file except in compliance with the Elastic License. */ +import { ESFilter } from '../../../../../typings/elasticsearch'; import { - TRANSACTION_REQUEST, - TRANSACTION_PAGE_LOAD, -} from '../../../common/transaction_types'; -import { - SERVICE_NAME, + METRIC_CGROUP_MEMORY_USAGE_BYTES, METRIC_SYSTEM_CPU_PERCENT, METRIC_SYSTEM_FREE_MEMORY, METRIC_SYSTEM_TOTAL_MEMORY, - METRIC_CGROUP_MEMORY_USAGE_BYTES, + SERVICE_NAME, TRANSACTION_TYPE, } from '../../../common/elasticsearch_fieldnames'; import { ProcessorEvent } from '../../../common/processor_event'; +import { + TRANSACTION_PAGE_LOAD, + TRANSACTION_REQUEST, +} from '../../../common/transaction_types'; import { rangeFilter } from '../../../common/utils/range_filter'; -import { ESFilter } from '../../../typings/elasticsearch'; +import { + getDocumentTypeFilterForAggregatedTransactions, + getProcessorEventForAggregatedTransactions, + getTransactionDurationFieldForAggregatedTransactions, +} from '../helpers/aggregated_transactions'; +import { getEnvironmentUiFilterES } from '../helpers/convert_ui_filters/get_environment_ui_filter_es'; import { Setup, SetupTimeRange } from '../helpers/setup_request'; import { percentCgroupMemoryUsedScript, percentSystemMemoryUsedScript, } from '../metrics/by_agent/shared/memory'; -import { - getProcessorEventForAggregatedTransactions, - getTransactionDurationFieldForAggregatedTransactions, - getDocumentTypeFilterForAggregatedTransactions, -} from '../helpers/aggregated_transactions'; import { getErrorRate } from '../transaction_groups/get_error_rate'; -import { getEnvironmentUiFilterES } from '../helpers/convert_ui_filters/get_environment_ui_filter_es'; interface Options { setup: Setup & SetupTimeRange; diff --git a/x-pack/plugins/apm/server/lib/service_map/get_trace_sample_ids.ts b/x-pack/plugins/apm/server/lib/service_map/get_trace_sample_ids.ts index 22c655bb96f503..db11ab2d9ce3a5 100644 --- a/x-pack/plugins/apm/server/lib/service_map/get_trace_sample_ids.ts +++ b/x-pack/plugins/apm/server/lib/service_map/get_trace_sample_ids.ts @@ -3,20 +3,20 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { uniq, take, sortBy } from 'lodash'; import Boom from '@hapi/boom'; -import { ProcessorEvent } from '../../../common/processor_event'; -import { Setup, SetupTimeRange } from '../helpers/setup_request'; -import { rangeFilter } from '../../../common/utils/range_filter'; -import { ESFilter } from '../../../typings/elasticsearch'; +import { sortBy, take, uniq } from 'lodash'; +import { ESFilter } from '../../../../../typings/elasticsearch'; import { - SERVICE_NAME, SERVICE_ENVIRONMENT, - TRACE_ID, + SERVICE_NAME, SPAN_DESTINATION_SERVICE_RESOURCE, + TRACE_ID, } from '../../../common/elasticsearch_fieldnames'; -import { getEnvironmentUiFilterES } from '../helpers/convert_ui_filters/get_environment_ui_filter_es'; +import { ProcessorEvent } from '../../../common/processor_event'; import { SERVICE_MAP_TIMEOUT_ERROR } from '../../../common/service_map'; +import { rangeFilter } from '../../../common/utils/range_filter'; +import { getEnvironmentUiFilterES } from '../helpers/convert_ui_filters/get_environment_ui_filter_es'; +import { Setup, SetupTimeRange } from '../helpers/setup_request'; const MAX_TRACES_TO_INSPECT = 1000; diff --git a/x-pack/plugins/apm/server/lib/services/annotations/get_derived_service_annotations.ts b/x-pack/plugins/apm/server/lib/services/annotations/get_derived_service_annotations.ts index b80e86d53f2922..367cf2faf6a8e0 100644 --- a/x-pack/plugins/apm/server/lib/services/annotations/get_derived_service_annotations.ts +++ b/x-pack/plugins/apm/server/lib/services/annotations/get_derived_service_annotations.ts @@ -4,19 +4,19 @@ * you may not use this file except in compliance with the Elastic License. */ import { isNumber } from 'lodash'; +import { ESFilter } from '../../../../../../typings/elasticsearch'; import { Annotation, AnnotationType } from '../../../../common/annotations'; -import { SetupTimeRange, Setup } from '../../helpers/setup_request'; -import { ESFilter } from '../../../../typings/elasticsearch'; -import { rangeFilter } from '../../../../common/utils/range_filter'; import { SERVICE_NAME, SERVICE_VERSION, } from '../../../../common/elasticsearch_fieldnames'; -import { getEnvironmentUiFilterES } from '../../helpers/convert_ui_filters/get_environment_ui_filter_es'; +import { rangeFilter } from '../../../../common/utils/range_filter'; import { getDocumentTypeFilterForAggregatedTransactions, getProcessorEventForAggregatedTransactions, } from '../../helpers/aggregated_transactions'; +import { getEnvironmentUiFilterES } from '../../helpers/convert_ui_filters/get_environment_ui_filter_es'; +import { Setup, SetupTimeRange } from '../../helpers/setup_request'; export async function getDerivedServiceAnnotations({ setup, diff --git a/x-pack/plugins/apm/server/lib/services/annotations/get_stored_annotations.ts b/x-pack/plugins/apm/server/lib/services/annotations/get_stored_annotations.ts index 6e3ae0181ddeef..623abf69302970 100644 --- a/x-pack/plugins/apm/server/lib/services/annotations/get_stored_annotations.ts +++ b/x-pack/plugins/apm/server/lib/services/annotations/get_stored_annotations.ts @@ -5,13 +5,13 @@ */ import { LegacyAPICaller, Logger } from 'kibana/server'; -import { SERVICE_NAME } from '../../../../common/elasticsearch_fieldnames'; -import { ESSearchResponse } from '../../../../typings/elasticsearch'; +import { ESSearchResponse } from '../../../../../../typings/elasticsearch'; +import { Annotation as ESAnnotation } from '../../../../../observability/common/annotations'; import { ScopedAnnotationsClient } from '../../../../../observability/server'; import { Annotation, AnnotationType } from '../../../../common/annotations'; -import { Annotation as ESAnnotation } from '../../../../../observability/common/annotations'; -import { SetupTimeRange, Setup } from '../../helpers/setup_request'; +import { SERVICE_NAME } from '../../../../common/elasticsearch_fieldnames'; import { getEnvironmentUiFilterES } from '../../helpers/convert_ui_filters/get_environment_ui_filter_es'; +import { Setup, SetupTimeRange } from '../../helpers/setup_request'; export async function getStoredAnnotations({ setup, diff --git a/x-pack/plugins/apm/server/lib/services/annotations/index.test.ts b/x-pack/plugins/apm/server/lib/services/annotations/index.test.ts index f30b77f1477100..16c46be78cb284 100644 --- a/x-pack/plugins/apm/server/lib/services/annotations/index.test.ts +++ b/x-pack/plugins/apm/server/lib/services/annotations/index.test.ts @@ -6,7 +6,7 @@ import { ESSearchRequest, ESSearchResponse, -} from '../../../../typings/elasticsearch'; +} from '../../../../../../typings/elasticsearch'; import { inspectSearchParams, SearchParamsMock, diff --git a/x-pack/plugins/apm/server/lib/services/get_services/get_services_items.ts b/x-pack/plugins/apm/server/lib/services/get_services/get_services_items.ts index 5ea3714e81b6fe..89915e798b7cd2 100644 --- a/x-pack/plugins/apm/server/lib/services/get_services/get_services_items.ts +++ b/x-pack/plugins/apm/server/lib/services/get_services/get_services_items.ts @@ -4,17 +4,17 @@ * you may not use this file except in compliance with the Elastic License. */ import { Logger } from '@kbn/logging'; +import { PromiseReturnType } from '../../../../../observability/typings/common'; import { joinByKey } from '../../../../common/utils/join_by_key'; -import { PromiseReturnType } from '../../../../typings/common'; -import { Setup, SetupTimeRange } from '../../helpers/setup_request'; import { getServicesProjection } from '../../../projections/services'; +import { Setup, SetupTimeRange } from '../../helpers/setup_request'; import { - getTransactionDurationAverages, getAgentNames, - getTransactionRates, - getTransactionErrorRates, getEnvironments, getHealthStatuses, + getTransactionDurationAverages, + getTransactionErrorRates, + getTransactionRates, } from './get_services_items_stats'; export type ServiceListAPIResponse = PromiseReturnType; diff --git a/x-pack/plugins/apm/server/lib/services/get_services/index.ts b/x-pack/plugins/apm/server/lib/services/get_services/index.ts index 5f39d6c836930d..9d450804e421d0 100644 --- a/x-pack/plugins/apm/server/lib/services/get_services/index.ts +++ b/x-pack/plugins/apm/server/lib/services/get_services/index.ts @@ -4,13 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import { isEmpty } from 'lodash'; import { Logger } from '@kbn/logging'; -import { PromiseReturnType } from '../../../../typings/common'; +import { isEmpty } from 'lodash'; +import { PromiseReturnType } from '../../../../../observability/typings/common'; import { Setup, SetupTimeRange } from '../../helpers/setup_request'; -import { hasHistoricalAgentData } from './has_historical_agent_data'; import { getLegacyDataStatus } from './get_legacy_data_status'; import { getServicesItems } from './get_services_items'; +import { hasHistoricalAgentData } from './has_historical_agent_data'; export type ServiceListAPIResponse = PromiseReturnType; diff --git a/x-pack/plugins/apm/server/lib/settings/agent_configuration/convert_settings_to_string.ts b/x-pack/plugins/apm/server/lib/settings/agent_configuration/convert_settings_to_string.ts index ab01a68733a7a9..6791bf85a7b6ac 100644 --- a/x-pack/plugins/apm/server/lib/settings/agent_configuration/convert_settings_to_string.ts +++ b/x-pack/plugins/apm/server/lib/settings/agent_configuration/convert_settings_to_string.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ESSearchHit } from '../../../../typings/elasticsearch'; +import { ESSearchHit } from '../../../../../../typings/elasticsearch'; import { AgentConfiguration } from '../../../../common/agent_configuration/configuration_types'; // needed for backwards compatability diff --git a/x-pack/plugins/apm/server/lib/settings/agent_configuration/find_exact_configuration.ts b/x-pack/plugins/apm/server/lib/settings/agent_configuration/find_exact_configuration.ts index 3a9623c7775166..bd10df76b85c77 100644 --- a/x-pack/plugins/apm/server/lib/settings/agent_configuration/find_exact_configuration.ts +++ b/x-pack/plugins/apm/server/lib/settings/agent_configuration/find_exact_configuration.ts @@ -4,13 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ +import { ESSearchHit } from '../../../../../../typings/elasticsearch'; +import { AgentConfiguration } from '../../../../common/agent_configuration/configuration_types'; import { - SERVICE_NAME, SERVICE_ENVIRONMENT, + SERVICE_NAME, } from '../../../../common/elasticsearch_fieldnames'; import { Setup } from '../../helpers/setup_request'; -import { AgentConfiguration } from '../../../../common/agent_configuration/configuration_types'; -import { ESSearchHit } from '../../../../typings/elasticsearch'; import { convertConfigSettingsToString } from './convert_settings_to_string'; export async function findExactConfiguration({ diff --git a/x-pack/plugins/apm/server/lib/settings/agent_configuration/search_configurations.ts b/x-pack/plugins/apm/server/lib/settings/agent_configuration/search_configurations.ts index 2585f88c2aa431..43ae4995ce672b 100644 --- a/x-pack/plugins/apm/server/lib/settings/agent_configuration/search_configurations.ts +++ b/x-pack/plugins/apm/server/lib/settings/agent_configuration/search_configurations.ts @@ -3,7 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { ESSearchHit } from '../../../../typings/elasticsearch'; +import { ESSearchHit } from '../../../../../../typings/elasticsearch'; import { SERVICE_NAME, SERVICE_ENVIRONMENT, diff --git a/x-pack/plugins/apm/server/lib/transaction_groups/correlations/get_correlations_for_slow_transactions.ts b/x-pack/plugins/apm/server/lib/transaction_groups/correlations/get_correlations_for_slow_transactions.ts index 3efc65afdfd280..76e595c928cf2c 100644 --- a/x-pack/plugins/apm/server/lib/transaction_groups/correlations/get_correlations_for_slow_transactions.ts +++ b/x-pack/plugins/apm/server/lib/transaction_groups/correlations/get_correlations_for_slow_transactions.ts @@ -4,9 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { asDuration } from '../../../../common/utils/formatters'; -import { ESFilter } from '../../../../typings/elasticsearch'; -import { rangeFilter } from '../../../../common/utils/range_filter'; +import { ESFilter } from '../../../../../../typings/elasticsearch'; import { SERVICE_NAME, TRANSACTION_DURATION, @@ -14,6 +12,8 @@ import { TRANSACTION_TYPE, } from '../../../../common/elasticsearch_fieldnames'; import { ProcessorEvent } from '../../../../common/processor_event'; +import { asDuration } from '../../../../common/utils/formatters'; +import { rangeFilter } from '../../../../common/utils/range_filter'; import { Setup, SetupTimeRange } from '../../helpers/setup_request'; import { getDurationForPercentile } from './get_duration_for_percentile'; import { diff --git a/x-pack/plugins/apm/server/lib/transaction_groups/correlations/get_duration_for_percentile.ts b/x-pack/plugins/apm/server/lib/transaction_groups/correlations/get_duration_for_percentile.ts index 37ee19ff40f62a..a94540df10fffb 100644 --- a/x-pack/plugins/apm/server/lib/transaction_groups/correlations/get_duration_for_percentile.ts +++ b/x-pack/plugins/apm/server/lib/transaction_groups/correlations/get_duration_for_percentile.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ESFilter } from '../../../../typings/elasticsearch'; +import { ESFilter } from '../../../../../../typings/elasticsearch'; import { TRANSACTION_DURATION } from '../../../../common/elasticsearch_fieldnames'; import { ProcessorEvent } from '../../../../common/processor_event'; import { Setup, SetupTimeRange } from '../../helpers/setup_request'; diff --git a/x-pack/plugins/apm/server/lib/transaction_groups/correlations/get_significant_terms_agg.ts b/x-pack/plugins/apm/server/lib/transaction_groups/correlations/get_significant_terms_agg.ts index 1cf0787c1d9704..c5ab8d8f1d111e 100644 --- a/x-pack/plugins/apm/server/lib/transaction_groups/correlations/get_significant_terms_agg.ts +++ b/x-pack/plugins/apm/server/lib/transaction_groups/correlations/get_significant_terms_agg.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ESFilter } from '../../../../typings/elasticsearch'; +import { ESFilter } from '../../../../../../typings/elasticsearch'; import { SignificantTermsScoring } from './scoring_rt'; export function getSignificantTermsAgg({ diff --git a/x-pack/plugins/apm/server/lib/transaction_groups/fetcher.ts b/x-pack/plugins/apm/server/lib/transaction_groups/fetcher.ts index 0a4d9748f25977..89fff260a7d231 100644 --- a/x-pack/plugins/apm/server/lib/transaction_groups/fetcher.ts +++ b/x-pack/plugins/apm/server/lib/transaction_groups/fetcher.ts @@ -3,24 +3,24 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { take, sortBy } from 'lodash'; -import { Unionize } from 'utility-types'; +import { sortBy, take } from 'lodash'; import moment from 'moment'; -import { joinByKey } from '../../../common/utils/join_by_key'; +import { Unionize } from 'utility-types'; +import { AggregationOptionsByType } from '../../../../../typings/elasticsearch'; +import { PromiseReturnType } from '../../../../observability/typings/common'; import { SERVICE_NAME, TRANSACTION_NAME, } from '../../../common/elasticsearch_fieldnames'; +import { joinByKey } from '../../../common/utils/join_by_key'; import { getTransactionGroupsProjection } from '../../projections/transaction_groups'; import { mergeProjection } from '../../projections/util/merge_projection'; -import { PromiseReturnType } from '../../../../observability/typings/common'; -import { AggregationOptionsByType } from '../../../typings/elasticsearch/aggregations'; import { Setup, SetupTimeRange } from '../helpers/setup_request'; import { getAverages, - getSums, - getPercentiles, getCounts, + getPercentiles, + getSums, } from './get_transaction_group_stats'; interface TopTransactionOptions { diff --git a/x-pack/plugins/apm/server/lib/transaction_groups/get_transaction_group_stats.ts b/x-pack/plugins/apm/server/lib/transaction_groups/get_transaction_group_stats.ts index 2550bd70c527d3..cfd3540446172b 100644 --- a/x-pack/plugins/apm/server/lib/transaction_groups/get_transaction_group_stats.ts +++ b/x-pack/plugins/apm/server/lib/transaction_groups/get_transaction_group_stats.ts @@ -6,7 +6,7 @@ import { merge } from 'lodash'; import { TRANSACTION_TYPE } from '../../../common/elasticsearch_fieldnames'; import { arrayUnionToCallable } from '../../../common/utils/array_union_to_callable'; -import { AggregationInputMap } from '../../../typings/elasticsearch/aggregations'; +import { AggregationInputMap } from '../../../../../typings/elasticsearch'; import { TransactionGroupRequestBase, TransactionGroupSetup } from './fetcher'; import { getTransactionDurationFieldForAggregatedTransactions } from '../helpers/aggregated_transactions'; diff --git a/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/fetcher.ts b/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/fetcher.ts index 8c999b445d7995..aad67c43f48e20 100644 --- a/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/fetcher.ts +++ b/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/fetcher.ts @@ -5,7 +5,7 @@ */ import { Logger } from 'kibana/server'; -import { ESSearchResponse } from '../../../../../typings/elasticsearch'; +import { ESSearchResponse } from '../../../../../../../typings/elasticsearch'; import { PromiseReturnType } from '../../../../../../observability/typings/common'; import { Setup, SetupTimeRange } from '../../../helpers/setup_request'; diff --git a/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/fetcher.ts b/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/fetcher.ts index e2edbbec63d475..a2da3977b81c76 100644 --- a/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/fetcher.ts +++ b/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/fetcher.ts @@ -4,22 +4,22 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ESFilter } from '../../../../../typings/elasticsearch'; +import { ESFilter } from '../../../../../../../typings/elasticsearch'; +import { PromiseReturnType } from '../../../../../../observability/typings/common'; import { SERVICE_NAME, TRANSACTION_NAME, TRANSACTION_RESULT, TRANSACTION_TYPE, } from '../../../../../common/elasticsearch_fieldnames'; -import { PromiseReturnType } from '../../../../../../observability/typings/common'; -import { getBucketSize } from '../../../helpers/get_bucket_size'; import { rangeFilter } from '../../../../../common/utils/range_filter'; -import { Setup, SetupTimeRange } from '../../../helpers/setup_request'; import { + getDocumentTypeFilterForAggregatedTransactions, getProcessorEventForAggregatedTransactions, getTransactionDurationFieldForAggregatedTransactions, - getDocumentTypeFilterForAggregatedTransactions, } from '../../../helpers/aggregated_transactions'; +import { getBucketSize } from '../../../helpers/get_bucket_size'; +import { Setup, SetupTimeRange } from '../../../helpers/setup_request'; export type ESResponse = PromiseReturnType; export function timeseriesFetcher({ diff --git a/x-pack/plugins/apm/server/lib/transactions/distribution/get_buckets/index.ts b/x-pack/plugins/apm/server/lib/transactions/distribution/get_buckets/index.ts index 34d01627a2869d..010acd09239a32 100644 --- a/x-pack/plugins/apm/server/lib/transactions/distribution/get_buckets/index.ts +++ b/x-pack/plugins/apm/server/lib/transactions/distribution/get_buckets/index.ts @@ -4,9 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import { ValuesType } from 'utility-types'; -import { PromiseReturnType } from '../../../../../typings/common'; -import { joinByKey } from '../../../../../common/utils/join_by_key'; -import { ProcessorEvent } from '../../../../../common/processor_event'; +import { PromiseReturnType } from '../../../../../../observability/typings/common'; import { SERVICE_NAME, TRACE_ID, @@ -16,13 +14,15 @@ import { TRANSACTION_SAMPLED, TRANSACTION_TYPE, } from '../../../../../common/elasticsearch_fieldnames'; +import { ProcessorEvent } from '../../../../../common/processor_event'; +import { joinByKey } from '../../../../../common/utils/join_by_key'; import { rangeFilter } from '../../../../../common/utils/range_filter'; -import { Setup, SetupTimeRange } from '../../../helpers/setup_request'; import { getDocumentTypeFilterForAggregatedTransactions, getProcessorEventForAggregatedTransactions, getTransactionDurationFieldForAggregatedTransactions, } from '../../../helpers/aggregated_transactions'; +import { Setup, SetupTimeRange } from '../../../helpers/setup_request'; function getHistogramAggOptions({ bucketSize, diff --git a/x-pack/plugins/apm/server/lib/ui_filters/get_environments.ts b/x-pack/plugins/apm/server/lib/ui_filters/get_environments.ts index b9f25e20f9f730..8109becbed45e3 100644 --- a/x-pack/plugins/apm/server/lib/ui_filters/get_environments.ts +++ b/x-pack/plugins/apm/server/lib/ui_filters/get_environments.ts @@ -4,16 +4,16 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ProcessorEvent } from '../../../common/processor_event'; +import { ESFilter } from '../../../../../typings/elasticsearch'; import { SERVICE_ENVIRONMENT, SERVICE_NAME, } from '../../../common/elasticsearch_fieldnames'; -import { rangeFilter } from '../../../common/utils/range_filter'; -import { Setup, SetupTimeRange } from '../helpers/setup_request'; import { ENVIRONMENT_NOT_DEFINED } from '../../../common/environment_filter_values'; -import { ESFilter } from '../../../typings/elasticsearch'; +import { ProcessorEvent } from '../../../common/processor_event'; +import { rangeFilter } from '../../../common/utils/range_filter'; import { getProcessorEventForAggregatedTransactions } from '../helpers/aggregated_transactions'; +import { Setup, SetupTimeRange } from '../helpers/setup_request'; export async function getEnvironments({ setup, diff --git a/x-pack/plugins/apm/server/projections/typings.ts b/x-pack/plugins/apm/server/projections/typings.ts index 332ac533e78c61..b6d6ffbb7ddbfc 100644 --- a/x-pack/plugins/apm/server/projections/typings.ts +++ b/x-pack/plugins/apm/server/projections/typings.ts @@ -4,11 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ESSearchBody } from '../../typings/elasticsearch'; import { AggregationOptionsByType, AggregationInputMap, -} from '../../typings/elasticsearch/aggregations'; + ESSearchBody, +} from '../../../../typings/elasticsearch'; import { APMEventESSearchRequest } from '../lib/helpers/create_es_client/create_apm_event_client'; export type Projection = Omit & { diff --git a/x-pack/plugins/apm/server/projections/util/merge_projection/index.ts b/x-pack/plugins/apm/server/projections/util/merge_projection/index.ts index ea7267dd337c25..2782b039958e1e 100644 --- a/x-pack/plugins/apm/server/projections/util/merge_projection/index.ts +++ b/x-pack/plugins/apm/server/projections/util/merge_projection/index.ts @@ -3,12 +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 { mergeWith, isPlainObject, cloneDeep } from 'lodash'; +import { cloneDeep, isPlainObject, mergeWith } from 'lodash'; import { DeepPartial } from 'utility-types'; -import { AggregationInputMap } from '../../../../typings/elasticsearch/aggregations'; -import { ESSearchBody } from '../../../../typings/elasticsearch'; -import { Projection } from '../../typings'; +import { + AggregationInputMap, + ESSearchBody, +} from '../../../../../../typings/elasticsearch'; import { APMEventESSearchRequest } from '../../../lib/helpers/create_es_client/create_apm_event_client'; +import { Projection } from '../../typings'; type PlainObject = Record; diff --git a/x-pack/plugins/apm/server/utils/test_helpers.tsx b/x-pack/plugins/apm/server/utils/test_helpers.tsx index 21b59dc516d060..9093b16fada0d5 100644 --- a/x-pack/plugins/apm/server/utils/test_helpers.tsx +++ b/x-pack/plugins/apm/server/utils/test_helpers.tsx @@ -3,14 +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 { APMConfig } from '../'; +import { PromiseReturnType } from '../../../observability/typings/common'; import { ESFilter, - ESSearchResponse, ESSearchRequest, -} from '../../typings/elasticsearch'; -import { PromiseReturnType } from '../../typings/common'; + ESSearchResponse, +} from '../../../../typings/elasticsearch'; import { UIFilters } from '../../typings/ui_filters'; -import { APMConfig } from '..'; interface Options { mockResponse?: ( diff --git a/x-pack/plugins/apm/typings/common.d.ts b/x-pack/plugins/apm/typings/common.d.ts index 754529a1985528..9133315c4c16a5 100644 --- a/x-pack/plugins/apm/typings/common.d.ts +++ b/x-pack/plugins/apm/typings/common.d.ts @@ -25,10 +25,4 @@ export type PromiseValueType = Value extends Promise ? Value : Value; -export type PromiseReturnType = Func extends ( - ...args: any[] -) => Promise - ? Value - : Func; - export type Maybe = T | null | undefined; 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 19bd1129677dd0..91335b39dab0fb 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 b8eb79aabdcc59..6b366090931cdd 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 3fd1dcad0066ec..20b35295e9ba5b 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 00000000000000..b4a1954783db02 --- /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 5c2e391059783b..dcb3dc02f65195 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 cdfe4183c96f57..68db3bca946413 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 53ebbb3328ff98..eefb0923f16da0 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 a64ff7da2aa196..7bb6e43b38d59e 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 d2c803a1ff2086..635e0ec2d0dcb8 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/cloud/kibana.json b/x-pack/plugins/cloud/kibana.json index 27b35bcbdd88b9..9bca2f30bd23cf 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 39ef5f452c18b8..680b2f1ad2bd65 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/apm/public/components/shared/Links/apm/SettingsLink.tsx b/x-pack/plugins/cloud/public/mocks.ts similarity index 50% rename from x-pack/plugins/apm/public/components/shared/Links/apm/SettingsLink.tsx rename to x-pack/plugins/cloud/public/mocks.ts index 80f3053b86f938..bafebbca4ecdd8 100644 --- a/x-pack/plugins/apm/public/components/shared/Links/apm/SettingsLink.tsx +++ b/x-pack/plugins/cloud/public/mocks.ts @@ -3,11 +3,16 @@ * 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 { APMLink, APMLinkExtendProps } from './APMLink'; -function SettingsLink(props: APMLinkExtendProps) { - return ; +function createSetupMock() { + return { + cloudId: 'mock-cloud-id', + isCloudEnabled: true, + resetPasswordUrl: 'reset-password-url', + accountUrl: 'account-url', + }; } -export { SettingsLink }; +export const cloudMock = { + createSetup: createSetupMock, +}; diff --git a/x-pack/plugins/cloud/public/plugin.ts b/x-pack/plugins/cloud/public/plugin.ts index 45005f3f5e4227..bc410b89c30e7a 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 00000000000000..15e2f14e885ba2 --- /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 ff8a2c5acdf9ab..eaa4ab7a482dd6 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/public/autocomplete/providers/kql_query_suggestion/value.ts b/x-pack/plugins/data_enhanced/public/autocomplete/providers/kql_query_suggestion/value.ts index 441e5a6f775dd5..cab3a657b7b6b0 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/enterprise_search/common/types/app_search.ts b/x-pack/plugins/enterprise_search/common/types/app_search.ts index 203b77834bc15d..9f754412ec730b 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 1006d391387593..39d9aa8607bc25 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 886597fcd98911..883cc6939b4bcb 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__/enzyme_rerender.mock.ts b/x-pack/plugins/enterprise_search/public/applications/__mocks__/enzyme_rerender.mock.ts new file mode 100644 index 00000000000000..1508f27c003777 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/__mocks__/enzyme_rerender.mock.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 { ShallowWrapper } from 'enzyme'; + +/** + * Quick and easy helper for re-rendering a React component in Enzyme + * after (e.g.) updating Kea values + */ +export const rerender = (wrapper: ShallowWrapper) => { + wrapper.setProps({}); // Re-renders + wrapper.update(); // Just in case +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/__mocks__/index.ts b/x-pack/plugins/enterprise_search/public/applications/__mocks__/index.ts index bdf555311d154e..e8944b15978a9f 100644 --- a/x-pack/plugins/enterprise_search/public/applications/__mocks__/index.ts +++ b/x-pack/plugins/enterprise_search/public/applications/__mocks__/index.ts @@ -15,4 +15,5 @@ export { mockAllValues, mockAllActions, setMockValues, setMockActions } from './ export { mountAsync } from './mount_async.mock'; export { mountWithIntl } from './mount_with_i18n.mock'; export { shallowWithIntl } from './shallow_with_i18n.mock'; +export { rerender } from './enzyme_rerender.mock'; // Note: shallow_useeffect must be imported directly as a file 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 a33e116c7ca726..a3d817e1da9048 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 932e84af45c2bf..2b475073c6ea53 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/credentials_flyout/form_components/key_engine_access.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/form_components/key_engine_access.test.tsx index b4b092f17a6aa9..d0bbf868aa90c7 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/form_components/key_engine_access.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/form_components/key_engine_access.test.tsx @@ -5,6 +5,7 @@ */ import { setMockValues, setMockActions } from '../../../../../__mocks__/kea.mock'; +import { rerender } from '../../../../../__mocks__'; import React from 'react'; import { shallow } from 'enzyme'; @@ -60,7 +61,7 @@ describe('FormKeyEngineAccess', () => { ...values, fullEngineAccessChecked: false, }); - wrapper.setProps({}); // Re-render + rerender(wrapper); expect(wrapper.find('#all_engines').prop('checked')).toEqual(false); expect(wrapper.find('#all_engines').prop('value')).toEqual('false'); 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 a02b00b6ad3770..d96c57b3c8bc3c 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 a8d9505136faab..803789ebee568d 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 97d29b9333f4b5..4f5ded0a3ccc17 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 f9752dca582e1c..9240bade4975e1 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 5c0c24ec733a47..fa2d124cbccdf6 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 de79862b540ba8..6523b4fb110b00 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 7b8b864b3a317c..166cbae9a45129 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/types.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/types.ts index 9ca4d086d55c81..23f78b44c0db57 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 84818322b35709..2287125bb5eb89 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 80a46f30e09308..b9fb501ccabe2d 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 b06ed63f8616c2..bee19a44facd39 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 1b216c46307db3..fb23551302f3bc 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 b2083f22c8e1c5..46b4c9b2c786cc 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 9c8758d83882d9..b150aa2cfc3bee 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/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 00000000000000..13db440df739e8 --- /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 00000000000000..2e7595e3ee87b4 --- /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.scss b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_nav.scss index d7740724204a7e..529fb9c4bd63eb 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_nav.scss +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_nav.scss @@ -15,3 +15,11 @@ margin-top: $euiSizeXS; } } + +.appSearchNavIcons { + // EUI override + &.euiFlexItem { + flex-grow: 0; + flex-direction: row; + } +} diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_nav.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_nav.test.tsx index 7bdc3c86a50d60..0d2ce654d4a0ab 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_nav.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_nav.test.tsx @@ -4,134 +4,189 @@ * you may not use this file except in compliance with the Elastic License. */ -import '../../../__mocks__/react_router_history.mock'; import { setMockValues } from '../../../__mocks__/kea.mock'; +import { rerender } from '../../../__mocks__'; import React from 'react'; -import { shallow, mount } from 'enzyme'; -import { Switch, useParams } from 'react-router-dom'; -import { EuiBadge } from '@elastic/eui'; +import { shallow } from 'enzyme'; +import { EuiBadge, EuiIcon } from '@elastic/eui'; -import { EngineRouter, EngineNav } from './'; +import { EngineNav } from './'; -describe('EngineRouter', () => { - it('renders a default engine overview', () => { - setMockValues({ myRole: {} }); - const wrapper = shallow(); - - expect(wrapper.find(Switch)).toHaveLength(1); - expect(wrapper.find('[data-test-subj="EngineOverviewTODO"]')).toHaveLength(1); - }); - - it('renders an analytics view', () => { - setMockValues({ myRole: { canViewEngineAnalytics: true } }); - const wrapper = shallow(); +describe('EngineNav', () => { + const values = { myRole: {}, engineName: 'some-engine', dataLoading: false, engine: {} }; - expect(wrapper.find('[data-test-subj="AnalyticsTODO"]')).toHaveLength(1); + beforeEach(() => { + setMockValues(values); }); -}); -describe('EngineNav', () => { - beforeEach(() => { - (useParams as jest.Mock).mockReturnValue({ engineName: 'some-engine' }); + it('does not render if async data is still loading', () => { + setMockValues({ ...values, dataLoading: true }); + const wrapper = shallow(); + expect(wrapper.isEmptyRender()).toBe(true); }); it('does not render without an engine name', () => { - setMockValues({ myRole: {} }); - (useParams as jest.Mock).mockReturnValue({ engineName: '' }); + setMockValues({ ...values, engineName: '' }); const wrapper = shallow(); expect(wrapper.isEmptyRender()).toBe(true); }); - it('renders an engine label', () => { - setMockValues({ myRole: {} }); - const wrapper = mount(); + it('renders an engine label and badges', () => { + setMockValues({ ...values, isSampleEngine: false, isMetaEngine: false }); + const wrapper = shallow(); + const label = wrapper.find('[data-test-subj="EngineLabel"]').find('.eui-textTruncate'); + + expect(label.text()).toEqual('SOME-ENGINE'); + expect(wrapper.find(EuiBadge)).toHaveLength(0); - const label = wrapper.find('[data-test-subj="EngineLabel"]').last(); - expect(label.text()).toEqual(expect.stringContaining('SOME-ENGINE')); + setMockValues({ ...values, isSampleEngine: true }); + rerender(wrapper); + expect(wrapper.find(EuiBadge).prop('children')).toEqual('SAMPLE ENGINE'); - // TODO: Test sample & meta engine conditional rendering - expect(label.find(EuiBadge).text()).toEqual('SAMPLE ENGINE'); + setMockValues({ ...values, isMetaEngine: true }); + rerender(wrapper); + expect(wrapper.find(EuiBadge).prop('children')).toEqual('META ENGINE'); }); it('renders a default engine overview link', () => { - setMockValues({ myRole: {} }); const wrapper = shallow(); expect(wrapper.find('[data-test-subj="EngineOverviewLink"]')).toHaveLength(1); }); it('renders an analytics link', () => { - setMockValues({ myRole: { canViewEngineAnalytics: true } }); + setMockValues({ ...values, myRole: { canViewEngineAnalytics: true } }); const wrapper = shallow(); expect(wrapper.find('[data-test-subj="EngineAnalyticsLink"]')).toHaveLength(1); }); it('renders a documents link', () => { - setMockValues({ myRole: { canViewEngineDocuments: true } }); + setMockValues({ ...values, myRole: { canViewEngineDocuments: true } }); const wrapper = shallow(); expect(wrapper.find('[data-test-subj="EngineDocumentsLink"]')).toHaveLength(1); }); it('renders a schema link', () => { - setMockValues({ myRole: { canViewEngineSchema: true } }); + setMockValues({ ...values, myRole: { canViewEngineSchema: true } }); const wrapper = shallow(); expect(wrapper.find('[data-test-subj="EngineSchemaLink"]')).toHaveLength(1); + }); + + describe('schema nav icons', () => { + const myRole = { canViewEngineSchema: true }; + + it('renders unconfirmed schema fields info icon', () => { + setMockValues({ ...values, myRole, hasUnconfirmedSchemaFields: true }); + const wrapper = shallow(); + expect(wrapper.find('[data-test-subj="EngineNavSchemaUnconfirmedFields"]')).toHaveLength(1); + }); - // TODO: Schema warning icon + it('renders schema conflicts alert icon', () => { + setMockValues({ ...values, myRole, hasSchemaConflicts: true }); + const wrapper = shallow(); + expect(wrapper.find('[data-test-subj="EngineNavSchemaConflicts"]')).toHaveLength(1); + }); }); - // TODO: Unskip when EngineLogic is migrated - it.skip('renders a crawler link', () => { - setMockValues({ myRole: { canViewEngineCrawler: true } }); - const wrapper = shallow(); - expect(wrapper.find('[data-test-subj="EngineCrawlerLink"]')).toHaveLength(1); + describe('crawler link', () => { + const myRole = { canViewEngineCrawler: true }; + + it('renders', () => { + setMockValues({ ...values, myRole }); + const wrapper = shallow(); + expect(wrapper.find('[data-test-subj="EngineCrawlerLink"]')).toHaveLength(1); + }); + + it('does not render for meta engines', () => { + setMockValues({ ...values, myRole, isMetaEngine: true }); + const wrapper = shallow(); + expect(wrapper.find('[data-test-subj="EngineCrawlerLink"]')).toHaveLength(0); + }); - // TODO: Test that the crawler link does NOT show up for meta/sample engines + it('does not render for sample engine', () => { + setMockValues({ ...values, myRole, isSampleEngine: true }); + const wrapper = shallow(); + expect(wrapper.find('[data-test-subj="EngineCrawlerLink"]')).toHaveLength(0); + }); }); - // TODO: Unskip when EngineLogic is migrated - it.skip('renders a meta engine source engines link', () => { - setMockValues({ myRole: { canViewMetaEngineSourceEngines: true } }); - const wrapper = shallow(); - expect(wrapper.find('[data-test-subj="MetaEngineEnginesLink"]')).toHaveLength(1); + describe('meta engine source engines link', () => { + const myRole = { canViewMetaEngineSourceEngines: true }; + + it('renders', () => { + setMockValues({ ...values, myRole, isMetaEngine: true }); + const wrapper = shallow(); + expect(wrapper.find('[data-test-subj="MetaEngineEnginesLink"]')).toHaveLength(1); + }); - // TODO: Test that the crawler link does NOT show up for non-meta engines + it('does not render for non meta engines', () => { + setMockValues({ ...values, myRole, isMetaEngine: false }); + const wrapper = shallow(); + expect(wrapper.find('[data-test-subj="MetaEngineEnginesLink"]')).toHaveLength(0); + }); }); it('renders a relevance tuning link', () => { - setMockValues({ myRole: { canManageEngineRelevanceTuning: true } }); + setMockValues({ ...values, myRole: { canManageEngineRelevanceTuning: true } }); const wrapper = shallow(); expect(wrapper.find('[data-test-subj="EngineRelevanceTuningLink"]')).toHaveLength(1); + }); + + describe('relevance tuning nav icons', () => { + const myRole = { canManageEngineRelevanceTuning: true }; + + it('renders unconfirmed schema fields info icon', () => { + const engine = { unsearchedUnconfirmedFields: true }; + setMockValues({ ...values, myRole, engine }); + const wrapper = shallow(); + expect( + wrapper.find('[data-test-subj="EngineNavRelevanceTuningUnsearchedFields"]') + ).toHaveLength(1); + }); + + it('renders schema conflicts alert icon', () => { + const engine = { invalidBoosts: true }; + setMockValues({ ...values, myRole, engine }); + const wrapper = shallow(); + expect(wrapper.find('[data-test-subj="EngineNavRelevanceTuningInvalidBoosts"]')).toHaveLength( + 1 + ); + }); - // TODO: Boost error icon + it('can render multiple icons', () => { + const engine = { invalidBoosts: true, unsearchedUnconfirmedFields: true }; + setMockValues({ ...values, myRole, engine }); + const wrapper = shallow(); + expect(wrapper.find(EuiIcon)).toHaveLength(2); + }); }); it('renders a synonyms link', () => { - setMockValues({ myRole: { canManageEngineSynonyms: true } }); + setMockValues({ ...values, myRole: { canManageEngineSynonyms: true } }); const wrapper = shallow(); expect(wrapper.find('[data-test-subj="EngineSynonymsLink"]')).toHaveLength(1); }); it('renders a curations link', () => { - setMockValues({ myRole: { canManageEngineCurations: true } }); + setMockValues({ ...values, myRole: { canManageEngineCurations: true } }); const wrapper = shallow(); expect(wrapper.find('[data-test-subj="EngineCurationsLink"]')).toHaveLength(1); }); it('renders a results settings link', () => { - setMockValues({ myRole: { canManageEngineResultSettings: true } }); + setMockValues({ ...values, myRole: { canManageEngineResultSettings: true } }); const wrapper = shallow(); expect(wrapper.find('[data-test-subj="EngineResultSettingsLink"]')).toHaveLength(1); }); it('renders a Search UI link', () => { - setMockValues({ myRole: { canManageEngineSearchUi: true } }); + setMockValues({ ...values, myRole: { canManageEngineSearchUi: true } }); const wrapper = shallow(); expect(wrapper.find('[data-test-subj="EngineSearchUILink"]')).toHaveLength(1); }); it('renders an API logs link', () => { - setMockValues({ myRole: { canViewEngineApiLogs: true } }); + setMockValues({ ...values, myRole: { canViewEngineApiLogs: true } }); const wrapper = shallow(); expect(wrapper.find('[data-test-subj="EngineAPILogsLink"]')).toHaveLength(1); }); 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 f92fefc7124b83..77aca8a71994da 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 @@ -5,18 +5,15 @@ */ import React from 'react'; -import { Route, Switch, useParams } from 'react-router-dom'; import { useValues } from 'kea'; -import { EuiText, EuiBadge } from '@elastic/eui'; +import { EuiText, EuiBadge, EuiIcon, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { SideNavLink, SideNavItem } from '../../../shared/layout'; -import { SetAppSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome'; import { AppLogic } from '../../app_logic'; import { getEngineRoute, - ENGINE_PATH, ENGINE_ANALYTICS_PATH, ENGINE_DOCUMENTS_PATH, ENGINE_SCHEMA_PATH, @@ -45,34 +42,10 @@ import { API_LOGS_TITLE, } from './constants'; -import './engine_nav.scss'; - -export const EngineRouter: React.FC = () => { - const { - myRole: { canViewEngineAnalytics }, - } = useValues(AppLogic); +import { EngineLogic } from './'; +import { EngineDetails } from './types'; - // TODO: EngineLogic - - const { engineName } = useParams() as { engineName: string }; - const engineBreadcrumb = [ENGINES_TITLE, engineName]; - - return ( - // TODO: Add more routes as we migrate them - - {canViewEngineAnalytics && ( - - -
    Just testing right now
    -
    - )} - - -
    Overview
    -
    -
    - ); -}; +import './engine_nav.scss'; export const EngineNav: React.FC = () => { const { @@ -91,14 +64,22 @@ export const EngineNav: React.FC = () => { }, } = useValues(AppLogic); - // TODO: Use EngineLogic - const isSampleEngine = true; - const isMetaEngine = false; - const { engineName } = useParams() as { engineName: string }; - const engineRoute = engineName && getEngineRoute(engineName); + const { + engineName, + dataLoading, + isSampleEngine, + isMetaEngine, + hasSchemaConflicts, + hasUnconfirmedSchemaFields, + engine, + } = useValues(EngineLogic); + if (dataLoading) return null; if (!engineName) return null; + const engineRoute = getEngineRoute(engineName); + const { invalidBoosts, unsearchedUnconfirmedFields } = engine as Required; + return ( <> @@ -143,8 +124,33 @@ export const EngineNav: React.FC = () => { to={getAppSearchUrl(engineRoute + ENGINE_SCHEMA_PATH)} data-test-subj="EngineSchemaLink" > - {SCHEMA_TITLE} - {/* TODO: Engine schema warning icon */} + + {SCHEMA_TITLE} + + {hasUnconfirmedSchemaFields && ( + + )} + {hasSchemaConflicts && ( + + )} + + )} {canViewEngineCrawler && !isMetaEngine && !isSampleEngine && ( @@ -171,8 +177,33 @@ export const EngineNav: React.FC = () => { to={getAppSearchUrl(engineRoute + ENGINE_RELEVANCE_TUNING_PATH)} data-test-subj="EngineRelevanceTuningLink" > - {RELEVANCE_TUNING_TITLE} - {/* TODO: invalid boosts error icon */} + + {RELEVANCE_TUNING_TITLE} + + {invalidBoosts && ( + + )} + {unsearchedUnconfirmedFields && ( + + )} + + )} {canManageEngineSynonyms && ( diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_router.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_router.test.tsx new file mode 100644 index 00000000000000..e38381cad32bab --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_router.test.tsx @@ -0,0 +1,81 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import '../../../__mocks__/react_router_history.mock'; +import { unmountHandler } from '../../../__mocks__/shallow_useeffect.mock'; +import { setMockValues, setMockActions } from '../../../__mocks__/kea.mock'; + +import React from 'react'; +import { shallow } from 'enzyme'; +import { Switch, Redirect, useParams } from 'react-router-dom'; + +jest.mock('../../../shared/flash_messages', () => ({ + setQueuedErrorMessage: jest.fn(), +})); +import { setQueuedErrorMessage } from '../../../shared/flash_messages'; + +import { EngineRouter } from './'; + +describe('EngineRouter', () => { + const values = { dataLoading: false, engineNotFound: false, myRole: {} }; + const actions = { setEngineName: jest.fn(), initializeEngine: jest.fn(), clearEngine: jest.fn() }; + + beforeEach(() => { + setMockValues(values); + setMockActions(actions); + }); + + describe('useEffect', () => { + beforeEach(() => { + (useParams as jest.Mock).mockReturnValue({ engineName: 'some-engine' }); + shallow(); + }); + + it('sets engineName based on the current route parameters', () => { + expect(actions.setEngineName).toHaveBeenCalledWith('some-engine'); + }); + + it('initializes/fetches engine API data', () => { + expect(actions.initializeEngine).toHaveBeenCalled(); + }); + + it('clears engine on unmount', () => { + unmountHandler(); + expect(actions.clearEngine).toHaveBeenCalled(); + }); + }); + + it('redirects to engines list and flashes an error if the engine param was not found', () => { + (useParams as jest.Mock).mockReturnValue({ engineName: '404-engine' }); + setMockValues({ ...values, engineNotFound: true }); + const wrapper = shallow(); + + expect(wrapper.find(Redirect).prop('to')).toEqual('/engines'); + expect(setQueuedErrorMessage).toHaveBeenCalledWith( + "No engine with name '404-engine' could be found." + ); + }); + + it('does not render if async data is still loading', () => { + setMockValues({ ...values, dataLoading: true }); + const wrapper = shallow(); + expect(wrapper.isEmptyRender()).toBe(true); + }); + + it('renders a default engine overview', () => { + const wrapper = shallow(); + + expect(wrapper.find(Switch)).toHaveLength(1); + expect(wrapper.find('[data-test-subj="EngineOverviewTODO"]')).toHaveLength(1); + }); + + it('renders an analytics view', () => { + setMockValues({ myRole: { canViewEngineAnalytics: true } }); + const wrapper = shallow(); + + expect(wrapper.find('[data-test-subj="AnalyticsTODO"]')).toHaveLength(1); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_router.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_router.tsx new file mode 100644 index 00000000000000..3e6856771f7d99 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_router.tsx @@ -0,0 +1,105 @@ +/* + * 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 { Route, Switch, Redirect, useParams } from 'react-router-dom'; +import { useValues, useActions } from 'kea'; + +import { i18n } from '@kbn/i18n'; + +import { SetAppSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome'; +import { setQueuedErrorMessage } from '../../../shared/flash_messages'; +import { AppLogic } from '../../app_logic'; + +// TODO: Uncomment and add more routes as we migrate them +import { + ENGINES_PATH, + ENGINE_PATH, + ENGINE_ANALYTICS_PATH, + // ENGINE_DOCUMENTS_PATH, + // ENGINE_SCHEMA_PATH, + // ENGINE_CRAWLER_PATH, + // META_ENGINE_SOURCE_ENGINES_PATH, + // ENGINE_RELEVANCE_TUNING_PATH, + // ENGINE_SYNONYMS_PATH, + // ENGINE_CURATIONS_PATH, + // ENGINE_RESULT_SETTINGS_PATH, + // ENGINE_SEARCH_UI_PATH, + // ENGINE_API_LOGS_PATH, +} from '../../routes'; +import { ENGINES_TITLE } from '../engines'; +import { + OVERVIEW_TITLE, + ANALYTICS_TITLE, + // DOCUMENTS_TITLE, + // SCHEMA_TITLE, + // CRAWLER_TITLE, + // RELEVANCE_TUNING_TITLE, + // SYNONYMS_TITLE, + // CURATIONS_TITLE, + // RESULT_SETTINGS_TITLE, + // SEARCH_UI_TITLE, + // API_LOGS_TITLE, +} from './constants'; + +import { EngineLogic } from './'; + +export const EngineRouter: React.FC = () => { + const { + myRole: { + canViewEngineAnalytics, + // canViewEngineDocuments, + // canViewEngineSchema, + // canViewEngineCrawler, + // canViewMetaEngineSourceEngines, + // canManageEngineSynonyms, + // canManageEngineCurations, + // canManageEngineRelevanceTuning, + // canManageEngineResultSettings, + // canManageEngineSearchUi, + // canViewEngineApiLogs, + }, + } = useValues(AppLogic); + + const { dataLoading, engineNotFound } = useValues(EngineLogic); + const { setEngineName, initializeEngine, clearEngine } = useActions(EngineLogic); + + const { engineName } = useParams() as { engineName: string }; + const engineBreadcrumb = [ENGINES_TITLE, engineName]; + + useEffect(() => { + setEngineName(engineName); + initializeEngine(); + return clearEngine; + }, [engineName]); + + if (engineNotFound) { + setQueuedErrorMessage( + i18n.translate('xpack.enterpriseSearch.appSearch.engine.notFound', { + defaultMessage: "No engine with name '{engineName}' could be found.", + values: { engineName }, + }) + ); + return ; + } + + if (dataLoading) return null; + + return ( + + {canViewEngineAnalytics && ( + + +
    Just testing right now
    +
    + )} + + +
    Overview
    +
    +
    + ); +}; 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 a3320ba5024ca3..4e7d81f73fb8de 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 @@ -4,4 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -export { EngineRouter, EngineNav } from './engine_nav'; +export { EngineRouter } from './engine_router'; +export { 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 00000000000000..635d1136291aa0 --- /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/engines/engines_overview.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_overview.tsx index 559fef695d63b5..0381c3806fec7e 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_overview.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_overview.tsx @@ -28,11 +28,11 @@ import { EnginesTable } from './engines_table'; import './engines_overview.scss'; -interface IGetEnginesParams { +interface GetEnginesParams { type: string; pageIndex: number; } -interface ISetEnginesCallbacks { +interface SetEnginesCallbacks { setResults: React.Dispatch>; setResultsTotal: React.Dispatch>; } @@ -49,12 +49,12 @@ export const EnginesOverview: 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); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_table.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_table.tsx index a9cf64b3dffda3..9591bbda1f7c25 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_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'; -interface IEnginesTableData { +interface EnginesTableData { name: string; created_at: string; document_count: number; field_count: number; } -interface IEnginesTablePagination { +interface EnginesTablePagination { totalEngines: number; pageIndex: number; onPaginate(pageIndex: number): void; } -interface IEnginesTableProps { - data: IEnginesTableData[]; - pagination: IEnginesTablePagination; +interface EnginesTableProps { + data: EnginesTableData[]; + pagination: EnginesTablePagination; } -interface IOnChange { +interface OnChange { page: { index: number; }; } -export const EnginesTable: React.FC = ({ +export const EnginesTable: React.FC = ({ data, pagination: { totalEngines, pageIndex, onPaginate }, }) => { @@ -52,7 +52,7 @@ export const EnginesTable: 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 EnginesTable: 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/schema/types.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/types.ts new file mode 100644 index 00000000000000..84f402dd3b95f2 --- /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/log_retention/generic_confirmation_modal.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/generic_confirmation_modal.test.tsx new file mode 100644 index 00000000000000..f1eac1a009ecfc --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/generic_confirmation_modal.test.tsx @@ -0,0 +1,81 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { shallow } from 'enzyme'; + +import { GenericConfirmationModal } from './generic_confirmation_modal'; + +describe('GenericConfirmationModal', () => { + let wrapper: any; + const onClose = jest.fn(); + const onSave = jest.fn(); + + beforeEach(() => { + jest.clearAllMocks(); + wrapper = shallow( + + ); + }); + + it('calls onSave callback when save is pressed', () => { + const button = wrapper.find('[data-test-subj="GenericConfirmationModalSave"]'); + button.simulate('click'); + expect(onSave).toHaveBeenCalled(); + }); + + it('calls onClose callback when Cancel is pressed', () => { + const button = wrapper.find('[data-test-subj="GenericConfirmationModalCancel"]'); + button.simulate('click'); + expect(onClose).toHaveBeenCalled(); + }); + + it('disables the Save button when the input is empty', () => { + const button = wrapper.find('[data-test-subj="GenericConfirmationModalSave"]'); + expect(button.prop('disabled')).toEqual(true); + }); + + it('disables the Save button when the input is not equal to the target', () => { + const input = wrapper.find('[data-test-subj="GenericConfirmationModalInput"]'); + input.prop('onChange')({ + target: { + value: 'NOT_GOOD', + }, + }); + + const button = wrapper.find('[data-test-subj="GenericConfirmationModalSave"]'); + expect(button.prop('disabled')).toEqual(true); + }); + + it('enables the Save button when the current input equals the target prop', () => { + const input = wrapper.find('[data-test-subj="GenericConfirmationModalInput"]'); + input.prop('onChange')({ + target: { + value: 'DISABLE', + }, + }); + const button = wrapper.find('[data-test-subj="GenericConfirmationModalSave"]'); + expect(button.prop('disabled')).toEqual(false); + }); + + it('is not case sensitive', () => { + const input = wrapper.find('[data-test-subj="GenericConfirmationModalInput"]'); + input.prop('onChange')({ + target: { + value: 'diSable', + }, + }); + const button = wrapper.find('[data-test-subj="GenericConfirmationModalSave"]'); + expect(button.prop('disabled')).toEqual(false); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/generic_confirmation_modal.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/generic_confirmation_modal.tsx new file mode 100644 index 00000000000000..6d802b0c5cfafa --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/generic_confirmation_modal.tsx @@ -0,0 +1,95 @@ +/* + * 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, { ReactNode, useState } from 'react'; +import { i18n } from '@kbn/i18n'; + +import { + EuiButton, + EuiButtonEmpty, + EuiFieldText, + EuiFormRow, + EuiModal, + EuiModalBody, + EuiModalFooter, + EuiModalHeader, + EuiModalHeaderTitle, + EuiSpacer, + EuiText, +} from '@elastic/eui'; + +interface GenericConfirmationModalProps { + description: ReactNode; + subheading: ReactNode; + target: string; + title: string; + onClose(): void; + onSave(): void; +} + +export const GenericConfirmationModal: React.FC = ({ + description, + onClose, + onSave, + subheading, + target, + title, +}) => { + const [inputValue, setInputValue] = useState(''); + + const onConfirm = () => { + setInputValue(''); + onSave(); + }; + + return ( + + + {title} + + + +

    + {subheading} +

    +

    {description}

    +
    + + + setInputValue(e.target.value)} + /> + +
    + + + {i18n.translate('xpack.enterpriseSearch.appSearch.settings.logRetention.modal.cancel', { + defaultMessage: 'Cancel', + })} + + + {i18n.translate('xpack.enterpriseSearch.appSearch.settings.logRetention.modal.save', { + defaultMessage: 'Save setting', + })} + + +
    + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/log_retention_confirmation_modal.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/log_retention_confirmation_modal.test.tsx new file mode 100644 index 00000000000000..d77089d52fbdd5 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/log_retention_confirmation_modal.test.tsx @@ -0,0 +1,129 @@ +/* + * 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 { setMockActions, setMockValues } from '../../../../__mocks__'; + +import React from 'react'; +import { shallow } from 'enzyme'; + +import { LogRetentionConfirmationModal } from './log_retention_confirmation_modal'; +import { LogRetentionOptions } from './types'; +import { GenericConfirmationModal } from './generic_confirmation_modal'; + +describe('', () => { + const actions = { + closeModals: jest.fn(), + saveLogRetention: jest.fn(), + }; + + const values = { + openedModal: null, + logRetention: { + analytics: { + enabled: true, + retentionPolicy: { + isDefault: true, + minAgeDays: 180, + }, + }, + api: { + enabled: true, + retentionPolicy: { + isDefault: true, + minAgeDays: 7, + }, + }, + }, + }; + + beforeEach(() => { + jest.clearAllMocks(); + setMockActions(actions); + setMockValues(values); + }); + + it('renders nothing by default', () => { + const logRetentionPanel = shallow(); + expect(logRetentionPanel.isEmptyRender()).toBe(true); + }); + + describe('analytics', () => { + it('renders the Analytics panel when openedModal is set to Analytics', () => { + setMockValues({ + ...values, + openedModal: LogRetentionOptions.Analytics, + }); + + const logRetentionPanel = shallow(); + expect( + logRetentionPanel.find('[data-test-subj="AnalyticsLogRetentionConfirmationModal"]').length + ).toBe(1); + }); + + it('calls saveLogRetention on save when showing analytics', () => { + setMockValues({ + ...values, + openedModal: LogRetentionOptions.Analytics, + }); + + const logRetentionPanel = shallow(); + const genericConfirmationModal = logRetentionPanel.find(GenericConfirmationModal); + genericConfirmationModal.prop('onSave')(); + expect(actions.saveLogRetention).toHaveBeenCalledWith(LogRetentionOptions.Analytics, false); + }); + + it('calls closeModals on close', () => { + setMockValues({ + ...values, + openedModal: LogRetentionOptions.Analytics, + }); + + const logRetentionPanel = shallow(); + const genericConfirmationModal = logRetentionPanel.find(GenericConfirmationModal); + genericConfirmationModal.prop('onClose')(); + expect(actions.closeModals).toHaveBeenCalled(); + }); + }); + + describe('api', () => { + it('renders the API panel when openedModal is set to API', () => { + setMockValues({ + ...values, + openedModal: LogRetentionOptions.API, + }); + + const logRetentionPanel = shallow(); + expect( + logRetentionPanel.find('[data-test-subj="APILogRetentionConfirmationModal"]').length + ).toBe(1); + }); + + it('calls saveLogRetention on save when showing api', () => { + setMockValues({ + ...values, + openedModal: LogRetentionOptions.API, + }); + + const logRetentionPanel = shallow(); + const genericConfirmationModal = logRetentionPanel.find(GenericConfirmationModal); + genericConfirmationModal.prop('onSave')(); + expect(actions.saveLogRetention).toHaveBeenCalledWith(LogRetentionOptions.API, false); + }); + + it('calls closeModals on close', () => { + setMockValues({ + ...values, + openedModal: LogRetentionOptions.API, + }); + + const logRetentionPanel = shallow(); + const genericConfirmationModal = logRetentionPanel.find(GenericConfirmationModal); + genericConfirmationModal.prop('onClose')(); + expect(actions.closeModals).toHaveBeenCalled(); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/log_retention_confirmation_modal.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/log_retention_confirmation_modal.tsx new file mode 100644 index 00000000000000..67421bb78fa71c --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/log_retention_confirmation_modal.tsx @@ -0,0 +1,137 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { i18n } from '@kbn/i18n'; + +import { EuiTextColor, EuiOverlayMask } from '@elastic/eui'; +import { useActions, useValues } from 'kea'; + +import { GenericConfirmationModal } from './generic_confirmation_modal'; +import { LogRetentionLogic } from './log_retention_logic'; + +import { LogRetentionOptions } from './types'; + +export const LogRetentionConfirmationModal: React.FC = () => { + const CANNOT_BE_RECOVERED_TEXT = i18n.translate( + 'xpack.enterpriseSearch.appSearch.settings.logRetention.modal.recovery', + { + defaultMessage: 'Once your data has been removed, it cannot be recovered.', + } + ); + + const DISABLE_TEXT = i18n.translate( + 'xpack.enterpriseSearch.appSearch.settings.logRetention.modal.disable', + { + defaultMessage: 'DISABLE', + } + ); + + const { closeModals, saveLogRetention } = useActions(LogRetentionLogic); + + const { logRetention, openedModal } = useValues(LogRetentionLogic); + + if (openedModal === null) { + return null; + } + + return ( + + {openedModal === LogRetentionOptions.Analytics && ( + +

    + {i18n.translate( + 'xpack.enterpriseSearch.appSearch.settings.logRetention.modal.analytics.description', + { + defaultMessage: + 'When disabling Analytics Logs, all your engines will immediately stop indexing Analytics Logs. Your existing data will be deleted in accordance with the storage timeframes outlined above.', + } + )} +

    +

    + + {CANNOT_BE_RECOVERED_TEXT} + +

    + + } + target={DISABLE_TEXT} + onClose={closeModals} + onSave={() => saveLogRetention(LogRetentionOptions.Analytics, false)} + /> + )} + {openedModal === LogRetentionOptions.API && ( + +

    + {i18n.translate( + 'xpack.enterpriseSearch.appSearch.settings.logRetention.modal.api.description', + { + defaultMessage: + 'When disabling API Logs, all your engines will immediately stop indexing API Logs. Your existing data will be deleted in accordance with the storage timeframes outlined above.', + } + )} +

    +

    + + {CANNOT_BE_RECOVERED_TEXT} + +

    + + } + target={DISABLE_TEXT} + onClose={closeModals} + onSave={() => saveLogRetention(LogRetentionOptions.API, false)} + /> + )} +
    + ); +}; 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 index 367c7b085123fc..c86d7e3e915e2d 100644 --- 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 @@ -17,7 +17,7 @@ jest.mock('../../../../shared/flash_messages', () => ({ })); import { flashAPIErrors } from '../../../../shared/flash_messages'; -import { ELogRetentionOptions } from './types'; +import { LogRetentionOptions } from './types'; import { LogRetentionLogic } from './log_retention_logic'; describe('LogRetentionLogic', () => { @@ -87,11 +87,11 @@ describe('LogRetentionLogic', () => { it('should be set to the provided value', () => { mount(); - LogRetentionLogic.actions.setOpenedModal(ELogRetentionOptions.Analytics); + LogRetentionLogic.actions.setOpenedModal(LogRetentionOptions.Analytics); expect(LogRetentionLogic.values).toEqual({ ...DEFAULT_VALUES, - openedModal: ELogRetentionOptions.Analytics, + openedModal: LogRetentionOptions.Analytics, }); }); }); @@ -194,10 +194,10 @@ describe('LogRetentionLogic', () => { describe('openedModal', () => { it('should be reset to null', () => { mount({ - openedModal: ELogRetentionOptions.Analytics, + openedModal: LogRetentionOptions.Analytics, }); - LogRetentionLogic.actions.saveLogRetention(ELogRetentionOptions.Analytics, true); + LogRetentionLogic.actions.saveLogRetention(LogRetentionOptions.Analytics, true); expect(LogRetentionLogic.values).toEqual({ ...DEFAULT_VALUES, @@ -211,7 +211,7 @@ describe('LogRetentionLogic', () => { const promise = Promise.resolve(TYPICAL_SERVER_LOG_RETENTION); http.put.mockReturnValue(promise); - LogRetentionLogic.actions.saveLogRetention(ELogRetentionOptions.Analytics, true); + LogRetentionLogic.actions.saveLogRetention(LogRetentionOptions.Analytics, true); expect(http.put).toHaveBeenCalledWith('/api/app_search/log_settings', { body: JSON.stringify({ @@ -233,7 +233,7 @@ describe('LogRetentionLogic', () => { const promise = Promise.reject('An error occured'); http.put.mockReturnValue(promise); - LogRetentionLogic.actions.saveLogRetention(ELogRetentionOptions.Analytics, true); + LogRetentionLogic.actions.saveLogRetention(LogRetentionOptions.Analytics, true); try { await promise; @@ -252,7 +252,7 @@ describe('LogRetentionLogic', () => { isLogRetentionUpdating: false, }); - LogRetentionLogic.actions.toggleLogRetention(ELogRetentionOptions.Analytics); + LogRetentionLogic.actions.toggleLogRetention(LogRetentionOptions.Analytics); expect(LogRetentionLogic.values).toEqual({ ...DEFAULT_VALUES, @@ -264,17 +264,17 @@ describe('LogRetentionLogic', () => { it('will call setOpenedModal if already enabled', () => { mount({ logRetention: { - [ELogRetentionOptions.Analytics]: { + [LogRetentionOptions.Analytics]: { enabled: true, }, }, }); jest.spyOn(LogRetentionLogic.actions, 'setOpenedModal'); - LogRetentionLogic.actions.toggleLogRetention(ELogRetentionOptions.Analytics); + LogRetentionLogic.actions.toggleLogRetention(LogRetentionOptions.Analytics); expect(LogRetentionLogic.actions.setOpenedModal).toHaveBeenCalledWith( - ELogRetentionOptions.Analytics + LogRetentionOptions.Analytics ); }); }); @@ -337,17 +337,17 @@ describe('LogRetentionLogic', () => { it('will call saveLogRetention if NOT already enabled', () => { mount({ logRetention: { - [ELogRetentionOptions.Analytics]: { + [LogRetentionOptions.Analytics]: { enabled: false, }, }, }); jest.spyOn(LogRetentionLogic.actions, 'saveLogRetention'); - LogRetentionLogic.actions.toggleLogRetention(ELogRetentionOptions.Analytics); + LogRetentionLogic.actions.toggleLogRetention(LogRetentionOptions.Analytics); expect(LogRetentionLogic.actions.saveLogRetention).toHaveBeenCalledWith( - ELogRetentionOptions.Analytics, + LogRetentionOptions.Analytics, true ); }); @@ -359,7 +359,7 @@ describe('LogRetentionLogic', () => { jest.spyOn(LogRetentionLogic.actions, 'saveLogRetention'); jest.spyOn(LogRetentionLogic.actions, 'setOpenedModal'); - LogRetentionLogic.actions.toggleLogRetention(ELogRetentionOptions.API); + 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 index 28830f2edb1d99..31fc41213492db 100644 --- 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 @@ -6,31 +6,31 @@ import { kea, MakeLogicType } from 'kea'; -import { ELogRetentionOptions, ILogRetention, ILogRetentionServer } from './types'; +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 ILogRetentionActions { +interface LogRetentionActions { clearLogRetentionUpdating(): { value: boolean }; closeModals(): { value: boolean }; fetchLogRetention(): { value: boolean }; saveLogRetention( - option: ELogRetentionOptions, + option: LogRetentionOptions, enabled: boolean - ): { option: ELogRetentionOptions; enabled: boolean }; - setOpenedModal(option: ELogRetentionOptions): { option: ELogRetentionOptions }; - toggleLogRetention(option: ELogRetentionOptions): { option: ELogRetentionOptions }; - updateLogRetention(logRetention: ILogRetention): { logRetention: ILogRetention }; + ): { option: LogRetentionOptions; enabled: boolean }; + setOpenedModal(option: LogRetentionOptions): { option: LogRetentionOptions }; + toggleLogRetention(option: LogRetentionOptions): { option: LogRetentionOptions }; + updateLogRetention(logRetention: LogRetention): { logRetention: LogRetention }; } -interface ILogRetentionValues { - logRetention: ILogRetention | null; +interface LogRetentionValues { + logRetention: LogRetention | null; isLogRetentionUpdating: boolean; - openedModal: ELogRetentionOptions | null; + openedModal: LogRetentionOptions | null; } -export const LogRetentionLogic = kea>({ +export const LogRetentionLogic = kea>({ path: ['enterprise_search', 'app_search', 'log_retention_logic'], actions: () => ({ clearLogRetentionUpdating: true, @@ -72,7 +72,7 @@ export const LogRetentionLogic = kea', () => { + 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 00000000000000..23572074b3c692 --- /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 00000000000000..6db087e092697e --- /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 00000000000000..fbc2ccfbc8a527 --- /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 00000000000000..385831dc511da9 --- /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 00000000000000..b65ffc04ad7009 --- /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 00000000000000..21267738f61ada --- /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 00000000000000..8f86e397dfe264 --- /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 index 7d4f30f88f38f9..f802efc72ac4d0 100644 --- 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 @@ -4,39 +4,39 @@ * you may not use this file except in compliance with the Elastic License. */ -export enum ELogRetentionOptions { +export enum LogRetentionOptions { Analytics = 'analytics', API = 'api', } -export interface ILogRetention { - [ELogRetentionOptions.Analytics]: ILogRetentionSettings; - [ELogRetentionOptions.API]: ILogRetentionSettings; +export interface LogRetention { + [LogRetentionOptions.Analytics]: LogRetentionSettings; + [LogRetentionOptions.API]: LogRetentionSettings; } -export interface ILogRetentionPolicy { +export interface LogRetentionPolicy { isDefault: boolean; minAgeDays: number | null; } -export interface ILogRetentionSettings { +export interface LogRetentionSettings { disabledAt?: string | null; enabled?: boolean; - retentionPolicy?: ILogRetentionPolicy | null; + retentionPolicy?: LogRetentionPolicy | null; } -export interface ILogRetentionServer { - [ELogRetentionOptions.Analytics]: ILogRetentionServerSettings; - [ELogRetentionOptions.API]: ILogRetentionServerSettings; +export interface LogRetentionServer { + [LogRetentionOptions.Analytics]: LogRetentionServerSettings; + [LogRetentionOptions.API]: LogRetentionServerSettings; } -export interface ILogRetentionServerPolicy { +export interface LogRetentionServerPolicy { is_default: boolean; min_age_days: number | null; } -export interface ILogRetentionServerSettings { +export interface LogRetentionServerSettings { disabled_at: string | null; enabled: boolean; - retention_policy: ILogRetentionServerPolicy | null; + retention_policy: LogRetentionServerPolicy | 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 index 1c0818fc286f2c..f960bb8f193b4c 100644 --- 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 @@ -5,23 +5,23 @@ */ import { - ELogRetentionOptions, - ILogRetention, - ILogRetentionPolicy, - ILogRetentionServer, - ILogRetentionServerPolicy, - ILogRetentionServerSettings, - ILogRetentionSettings, + LogRetentionOptions, + LogRetention, + LogRetentionPolicy, + LogRetentionServer, + LogRetentionServerPolicy, + LogRetentionServerSettings, + LogRetentionSettings, } from '../types'; export const convertLogRetentionFromServerToClient = ( - logRetention: ILogRetentionServer -): ILogRetention => ({ - [ELogRetentionOptions.Analytics]: convertLogRetentionSettingsFromServerToClient( - logRetention[ELogRetentionOptions.Analytics] + logRetention: LogRetentionServer +): LogRetention => ({ + [LogRetentionOptions.Analytics]: convertLogRetentionSettingsFromServerToClient( + logRetention[LogRetentionOptions.Analytics] ), - [ELogRetentionOptions.API]: convertLogRetentionSettingsFromServerToClient( - logRetention[ELogRetentionOptions.API] + [LogRetentionOptions.API]: convertLogRetentionSettingsFromServerToClient( + logRetention[LogRetentionOptions.API] ), }); @@ -29,7 +29,7 @@ const convertLogRetentionSettingsFromServerToClient = ({ disabled_at: disabledAt, enabled, retention_policy: retentionPolicy, -}: ILogRetentionServerSettings): ILogRetentionSettings => ({ +}: LogRetentionServerSettings): LogRetentionSettings => ({ disabledAt, enabled, retentionPolicy: @@ -39,7 +39,7 @@ const convertLogRetentionSettingsFromServerToClient = ({ const convertLogRetentionPolicyFromServerToClient = ({ min_age_days: minAgeDays, is_default: isDefault, -}: ILogRetentionServerPolicy): ILogRetentionPolicy => ({ +}: 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 13079bb380f135..dbd6627a3b9ce4 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 @@ -6,10 +6,18 @@ import React from 'react'; -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 { LogRetentionConfirmationModal } from './log_retention/log_retention_confirmation_modal'; import { SETTINGS_TITLE } from './'; @@ -24,9 +32,13 @@ export const Settings: React.FC = () => { - - - + + + + + + + ); }; 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 4571ef10286e49..743cf63fb4bc35 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 @@ -12,7 +12,7 @@ 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'; @@ -36,7 +36,7 @@ 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 ? : ; }; @@ -52,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); @@ -100,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); 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 568a0a33659824..7c22a45757a937 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 a935fa657738c2..21a80bc0c208fc 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 ee778f49ef5b60..de553acf11f7ba 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 235ececd8b6fca..579baf41f2ce31 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.test.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/index.test.tsx index 0e929c9191e0f5..7f638c64b445d8 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/index.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/index.test.tsx @@ -8,6 +8,7 @@ import React from 'react'; import { shallow } from 'enzyme'; import { setMockValues } from '../__mocks__/kea.mock'; +import { rerender } from '../__mocks__'; import { EnterpriseSearch } from './'; import { SetupGuide } from './components/setup_guide'; @@ -40,7 +41,7 @@ describe('EnterpriseSearch', () => { errorConnecting: true, config: { host: '' }, }); - wrapper.setProps({}); // Re-render + rerender(wrapper); expect(wrapper.find(ErrorConnecting)).toHaveLength(0); expect(wrapper.find(ProductSelector)).toHaveLength(1); 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 048baabe6a1ddc..b562cd577c56b5 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 b9c94e351089db..3436df851c8d81 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 5a05a03adeb6b0..7271a1dbbea395 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 2bd04d1d87f7db..c4b287ee08354f 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 21c1a60efa6b73..a109640f09bbec 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,11 @@ */ 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'; +export { + setSuccessMessage, + setErrorMessage, + setQueuedSuccessMessage, + setQueuedErrorMessage, +} from './set_message_helpers'; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/set_message_helpers.test.ts b/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/set_message_helpers.test.ts index 46027fdfb22b1a..c5ee8200c490dd 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/set_message_helpers.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/set_message_helpers.test.ts @@ -15,6 +15,7 @@ import { setSuccessMessage, setErrorMessage, setQueuedSuccessMessage, + setQueuedErrorMessage, } from './'; describe('Flash Message Helpers', () => { @@ -56,4 +57,15 @@ describe('Flash Message Helpers', () => { }, ]); }); + + it('setQueuedErrorMessage()', () => { + setQueuedErrorMessage(message); + + expect(FlashMessagesLogic.values.queuedMessages).toEqual([ + { + message, + type: 'error', + }, + ]); + }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/set_message_helpers.ts b/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/set_message_helpers.ts index 6abb540b7c14bb..cb73d54fd7b1e7 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/set_message_helpers.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/set_message_helpers.ts @@ -26,3 +26,10 @@ export const setQueuedSuccessMessage = (message: string) => { message, }); }; + +export const setQueuedErrorMessage = (message: string) => { + FlashMessagesLogic.actions.setQueuedMessages({ + type: 'error', + message, + }); +}; 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 9b0833dfce5412..69176b8a139e79 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 d16e507bfb3bc4..76cefa4bc5a2c6 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 46a52415f85643..5e41ea032d503b 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 89ed07f302b035..28f500a2c8a39f 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 e22334aeea3712..25d4850425a6e8 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 de5f72de791925..a0e34106fe2a2e 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 a43e7053bb1e19..0e694a3d2bbc85 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 ef8216e8b6711c..0ee7de242dfe23 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 facfd0bfcb16d5..6c4e1d084c16dc 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 (