diff --git a/.ci/end2end.groovy b/.ci/end2end.groovy index 0d9f5c9d92453..025836a90204c 100644 --- a/.ci/end2end.groovy +++ b/.ci/end2end.groovy @@ -41,7 +41,7 @@ pipeline { // Filter when to run based on the below reasons: // - On a PRs when: // - There are changes related to the APM UI project - // - only when the owners of those changes are members of the apm-ui team (new filter) + // - only when the owners of those changes are members of the given GitHub teams // - On merges to branches when: // - There are changes related to the APM UI project // - FORCE parameter is set to true. @@ -51,7 +51,7 @@ pipeline { apm_updated = isGitRegionMatch(patterns: [ "^x-pack/plugins/apm/.*" ]) } if (isPR()) { - def isMember = isMemberOf(user: env.CHANGE_AUTHOR, team: 'apm-ui') + def isMember = isMemberOf(user: env.CHANGE_AUTHOR, team: ['apm-ui', 'uptime']) setEnvVar('RUN_APM_E2E', params.FORCE || (apm_updated && isMember)) } else { setEnvVar('RUN_APM_E2E', params.FORCE || apm_updated) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index d2a8b110b503f..990567e6a7a18 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -85,6 +85,7 @@ # Exclude tutorial resources folder for now because they are not owned by Kibana app and most will move out soon /src/legacy/core_plugins/kibana/public/home/*.ts @elastic/kibana-core-ui /src/legacy/core_plugins/kibana/public/home/np_ready/ @elastic/kibana-core-ui +/x-pack/plugins/global_search_bar/ @elastic/kibana-core-ui # Observability UIs /x-pack/legacy/plugins/infra/ @elastic/logs-metrics-ui @@ -153,6 +154,7 @@ /x-pack/plugins/cloud/ @elastic/kibana-platform /x-pack/test/saved_objects_field_count/ @elastic/kibana-platform /packages/kbn-config-schema/ @elastic/kibana-platform +/packages/kbn-std/ @elastic/kibana-platform /src/legacy/server/config/ @elastic/kibana-platform /src/legacy/server/http/ @elastic/kibana-platform /src/legacy/server/logging/ @elastic/kibana-platform diff --git a/docs/developer/architecture/code-exploration.asciidoc b/docs/developer/architecture/code-exploration.asciidoc new file mode 100644 index 0000000000000..4a390336da34f --- /dev/null +++ b/docs/developer/architecture/code-exploration.asciidoc @@ -0,0 +1,598 @@ +//// + +NOTE: + This is an automatically generated file. Please do not edit directly. Instead, run the + following from within the kibana repository: + + node scripts/build_plugin_list_docs + + You can update the template within packages/kbn-dev-utils/target/plugin_list/generate_plugin_list.js + +//// + +[[code-exploration]] +== Exploring Kibana code + +The goals of our folder heirarchy are: + +- Easy for developers to know where to add new services, plugins and applications. +- Easy for developers to know where to find the code from services, plugins and applications. +- Easy to browse and understand our folder structure. + +To that aim, we strive to: + +- Avoid too many files in any given folder. +- Choose clear, unambigious folder names. +- Organize by domain. +- Every folder should contain a README that describes the contents of that folder. + +[discrete] +[[kibana-services-applications]] +=== Services and Applications + +[discrete] +==== src/plugins + +- {kib-repo}blob/{branch}/src/plugins/advanced_settings[advancedSettings] + +WARNING: Missing README. + + +- {kib-repo}blob/{branch}/src/plugins/apm_oss[apmOss] + +WARNING: Missing README. + + +- {kib-repo}blob/{branch}/src/plugins/bfetch/README.md[bfetch] + +bfetch allows to batch HTTP requests and streams responses back. + + +- {kib-repo}blob/{branch}/src/plugins/charts/README.md[charts] + +The Charts plugin is a way to create easier integration of shared colors, themes, types and other utilities across all Kibana charts and visualizations. + + +- {kib-repo}blob/{branch}/src/plugins/console[console] + +WARNING: Missing README. + + +- {kib-repo}blob/{branch}/src/plugins/dashboard/README.md[dashboard] + +Contains the dashboard application. + + +- {kib-repo}blob/{branch}/src/plugins/data/README.md[data] + +data plugin provides common data access services. + + +- {kib-repo}blob/{branch}/src/plugins/dev_tools/README.md[devTools] + +The ui/registry/dev_tools is removed in favor of the devTools plugin which exposes a register method in the setup contract. +Registering app works mostly the same as registering apps in core.application.register. +Routing will be handled by the id of the dev tool - your dev tool will be mounted when the URL matches /app/dev_tools#/. +This API doesn't support angular, for registering angular dev tools, bootstrap a local module on mount into the given HTML element. + + +- {kib-repo}blob/{branch}/src/plugins/discover/README.md[discover] + +Contains the Discover application and the saved search embeddable. + + +- {kib-repo}blob/{branch}/src/plugins/embeddable/README.md[embeddable] + +Embeddables are re-usable widgets that can be rendered in any environment or plugin. Developers can embed them directly in their plugin. End users can dynamically add them to any embeddable containers. + + +- {kib-repo}blob/{branch}/src/plugins/es_ui_shared/README.md[esUiShared] + +This plugin contains reusable code in the form of self-contained modules (or libraries). Each of these modules exports a set of functionality relevant to the domain of the module. + + +- {kib-repo}blob/{branch}/src/plugins/expressions/README.md[expressions] + +This plugin provides methods which will parse & execute an expression pipeline +string for you, as well as a series of registries for advanced users who might +want to incorporate their own functions, types, and renderers into the service +for use in their own application. + + +- {kib-repo}blob/{branch}/src/plugins/home/README.md[home] + +Moves the legacy ui/registry/feature_catalogue module for registering "features" that should be shown in the home page's feature catalogue to a service within a "home" plugin. The feature catalogue refered to here should not be confused with the "feature" plugin for registering features used to derive UI capabilities for feature controls. + + +- {kib-repo}blob/{branch}/src/plugins/index_pattern_management[indexPatternManagement] + +WARNING: Missing README. + + +- {kib-repo}blob/{branch}/src/plugins/input_control_vis/README.md[inputControlVis] + +Contains the input control visualization allowing to place custom filter controls on a dashboard. + + +- {kib-repo}blob/{branch}/src/plugins/inspector/README.md[inspector] + +The inspector is a contextual tool to gain insights into different elements +in Kibana, e.g. visualizations. It has the form of a flyout panel. + + +- {kib-repo}blob/{branch}/src/plugins/kibana_legacy/README.md[kibanaLegacy] + +This plugin will contain several helpers and services to integrate pieces of the legacy Kibana app with the new Kibana platform. + + +- {kib-repo}blob/{branch}/src/plugins/kibana_react/README.md[kibanaReact] + +Tools for building React applications in Kibana. + + +- {kib-repo}blob/{branch}/src/plugins/kibana_usage_collection/README.md[kibanaUsageCollection] + +This plugin registers the basic usage collectors from Kibana: + + +- {kib-repo}blob/{branch}/src/plugins/kibana_utils/README.md[kibanaUtils] + +Utilities for building Kibana plugins. + + +- {kib-repo}blob/{branch}/src/plugins/legacy_export[legacyExport] + +WARNING: Missing README. + + +- {kib-repo}blob/{branch}/src/plugins/management[management] + +WARNING: Missing README. + + +- {kib-repo}blob/{branch}/src/plugins/maps_legacy[mapsLegacy] + +WARNING: Missing README. + + +- {kib-repo}blob/{branch}/src/plugins/navigation/README.md[navigation] + +The navigation plugins exports the TopNavMenu component. +It also provides a stateful version of it on the start contract. + + +- {kib-repo}blob/{branch}/src/plugins/newsfeed[newsfeed] + +WARNING: Missing README. + + +- {kib-repo}blob/{branch}/src/plugins/region_map[regionMap] + +WARNING: Missing README. + + +- {kib-repo}blob/{branch}/src/plugins/saved_objects[savedObjects] + +WARNING: Missing README. + + +- {kib-repo}blob/{branch}/src/plugins/saved_objects_management[savedObjectsManagement] + +WARNING: Missing README. + + +- {kib-repo}blob/{branch}/src/plugins/share/README.md[share] + +Replaces the legacy ui/share module for registering share context menus. + + +- {kib-repo}blob/{branch}/src/plugins/telemetry/README.md[telemetry] + +Telemetry allows Kibana features to have usage tracked in the wild. The general term "telemetry" refers to multiple things: + + +- {kib-repo}blob/{branch}/src/plugins/telemetry_collection_manager/README.md[telemetryCollectionManager] + +Telemetry's collection manager to go through all the telemetry sources when fetching it before reporting. + + +- {kib-repo}blob/{branch}/src/plugins/telemetry_management_section/README.md[telemetryManagementSection] + +This plugin adds the Advanced Settings section for the Usage Data collection (aka Telemetry). + + +- {kib-repo}blob/{branch}/src/plugins/tile_map[tileMap] + +WARNING: Missing README. + + +- {kib-repo}blob/{branch}/src/plugins/timelion/README.md[timelion] + +Contains the deprecated timelion application. For the timelion visualization, +which also contains the timelion APIs and backend, look at the vis_type_timelion plugin. + + +- {kib-repo}blob/{branch}/src/plugins/ui_actions/README.md[uiActions] + +An API for: + + +- {kib-repo}blob/{branch}/src/plugins/usage_collection/README.md[usageCollection] + +Usage Collection allows collecting usage data for other services to consume (telemetry and monitoring). +To integrate with the telemetry services for usage collection of your feature, there are 2 steps: + + +- {kib-repo}blob/{branch}/src/plugins/vis_type_markdown/README.md[visTypeMarkdown] + +The markdown visualization that can be used to place text panels on dashboards. + + +- {kib-repo}blob/{branch}/src/plugins/vis_type_metric/README.md[visTypeMetric] + +Contains the metric visualization. + + +- {kib-repo}blob/{branch}/src/plugins/vis_type_table/README.md[visTypeTable] + +Contains the data table visualization, that allows presenting data in a simple table format. + + +- {kib-repo}blob/{branch}/src/plugins/vis_type_tagcloud/README.md[visTypeTagcloud] + +Contains the tagcloud visualization. + + +- {kib-repo}blob/{branch}/src/plugins/vis_type_timelion/README.md[visTypeTimelion] + +Contains the timelion visualization and the timelion backend. + + +- {kib-repo}blob/{branch}/src/plugins/vis_type_timeseries/README.md[visTypeTimeseries] + +Contains everything around TSVB (the editor, visualizatin implementations and backends). + + +- {kib-repo}blob/{branch}/src/plugins/vis_type_vega/README.md[visTypeVega] + +Contains the Vega visualization. + + +- {kib-repo}blob/{branch}/src/plugins/vis_type_vislib/README.md[visTypeVislib] + +Contains the vislib visualizations. These are the classical area/line/bar, pie, gauge/goal and +heatmap charts. + + +- {kib-repo}blob/{branch}/src/plugins/vis_type_xy/README.md[visTypeXy] + +Contains the new xy-axis chart using the elastic-charts library, which will eventually +replace the vislib xy-axis (bar, area, line) charts. + + +- {kib-repo}blob/{branch}/src/plugins/visualizations/README.md[visualizations] + +Contains most of the visualization infrastructure, e.g. the visualization type registry or the +visualization embeddable. + + +- {kib-repo}blob/{branch}/src/plugins/visualize/README.md[visualize] + +Contains the visualize application which includes the listing page and the app frame, +which will load the visualization's editor. + + +[discrete] +==== x-pack/plugins + +- {kib-repo}blob/{branch}/x-pack/plugins/actions/README.md[actions] + +The Kibana actions plugin provides a framework to create executable actions. You can: + + +- {kib-repo}blob/{branch}/x-pack/plugins/alerting_builtins/README.md[alertingBuiltins] + +This plugin provides alertTypes shipped with Kibana for use with the +the alerts plugin. When enabled, it will register +the built-in alertTypes with the alerting plugin, register associated HTTP +routes, etc. + + +- {kib-repo}blob/{branch}/x-pack/plugins/alerts/README.md[alerts] + +The Kibana alerting plugin provides a common place to set up alerts. You can: + + +- {kib-repo}blob/{branch}/x-pack/plugins/apm/readme.md[apm] + +To access an elasticsearch instance that has live data you have two options: + + +- {kib-repo}blob/{branch}/x-pack/plugins/audit_trail[auditTrail] + +WARNING: Missing README. + + +- {kib-repo}blob/{branch}/x-pack/plugins/beats_management/readme.md[beatsManagement] + +Notes: +Failure to have auth enabled in Kibana will make for a broken UI. UI-based errors not yet in place + + +- {kib-repo}blob/{branch}/x-pack/plugins/canvas/README.md[canvas] + +"Never look back. The past is done. The future is a blank canvas." ― Suzy Kassem, Rise Up and Salute the Sun + + +- {kib-repo}blob/{branch}/x-pack/plugins/case/README.md[case] + +Experimental Feature + + +- {kib-repo}blob/{branch}/x-pack/plugins/cloud[cloud] + +WARNING: Missing README. + + +- {kib-repo}blob/{branch}/x-pack/plugins/code[code] + +WARNING: Missing README. + + +- {kib-repo}blob/{branch}/x-pack/plugins/console_extensions[consoleExtensions] + +WARNING: Missing README. + + +- {kib-repo}blob/{branch}/x-pack/plugins/cross_cluster_replication/README.md[crossClusterReplication] + +You can run a local cluster and simulate a remote cluster within a single Kibana directory. + + +- {kib-repo}blob/{branch}/x-pack/plugins/dashboard_enhanced/README.md[dashboardEnhanced] + +Contains the enhancements to the OSS dashboard app. + + +- {kib-repo}blob/{branch}/x-pack/plugins/dashboard_mode/README.md[dashboardMode] + +The deprecated dashboard only mode. + + +- {kib-repo}blob/{branch}/x-pack/plugins/data_enhanced[dataEnhanced] + +WARNING: Missing README. + + +- {kib-repo}blob/{branch}/x-pack/plugins/discover_enhanced/README.md[discoverEnhanced] + +Contains the enhancements to the OSS discover app. + + +- {kib-repo}blob/{branch}/x-pack/plugins/embeddable_enhanced[embeddableEnhanced] + +WARNING: Missing README. + + +- {kib-repo}blob/{branch}/x-pack/plugins/encrypted_saved_objects/README.md[encryptedSavedObjects] + +The purpose of this plugin is to provide a way to encrypt/decrypt attributes on the custom Saved Objects that works with +security and spaces filtering as well as performing audit logging. + + +- {kib-repo}blob/{branch}/x-pack/plugins/enterprise_search/README.md[enterpriseSearch] + +This plugin's goal is to provide a Kibana user interface to the Enterprise Search solution's products (App Search and Workplace Search). In it's current MVP state, the plugin provides the following with the goal of gathering user feedback and raising product awareness: + + +- {kib-repo}blob/{branch}/x-pack/plugins/event_log/README.md[eventLog] + +The purpose of this plugin is to provide a way to persist a history of events +occuring in Kibana, initially just for the Make It Action project - alerts +and actions. + + +- {kib-repo}blob/{branch}/x-pack/plugins/features[features] + +WARNING: Missing README. + + +- {kib-repo}blob/{branch}/x-pack/plugins/file_upload[fileUpload] + +WARNING: Missing README. + + +- {kib-repo}blob/{branch}/x-pack/plugins/global_search/README.md[globalSearch] + +The GlobalSearch plugin provides an easy way to search for various objects, such as applications +or dashboards from the Kibana instance, from both server and client-side plugins + + +- {kib-repo}blob/{branch}/x-pack/plugins/global_search_bar/README.md[globalSearchBar] + +The GlobalSearchBar plugin provides a search interface for navigating Kibana. (It is the UI to the GlobalSearch plugin.) + + +- {kib-repo}blob/{branch}/x-pack/plugins/global_search_providers[globalSearchProviders] + +WARNING: Missing README. + + +- {kib-repo}blob/{branch}/x-pack/plugins/graph/README.md[graph] + +This is the main source folder of the Graph plugin. It contains all of the Kibana server and client source code. x-pack/test/functional/apps/graph contains additional functional tests. + + +- {kib-repo}blob/{branch}/x-pack/plugins/grokdebugger/README.md[grokdebugger] + +- {kib-repo}blob/{branch}/x-pack/plugins/index_lifecycle_management/README.md[indexLifecycleManagement] + +You can test that the Frozen badge, phase filtering, and lifecycle information is surfaced in +Index Management by running this series of requests in Console: + + +- {kib-repo}blob/{branch}/x-pack/plugins/index_management[indexManagement] + +WARNING: Missing README. + + +- {kib-repo}blob/{branch}/x-pack/plugins/infra/README.md[infra] + +This is the home of the infra plugin, which aims to provide a solution for +the infrastructure monitoring use-case within Kibana. + + +- {kib-repo}blob/{branch}/x-pack/plugins/ingest_manager/README.md[ingestManager] + +Fleet needs to have Elasticsearch API keys enabled, and also to have TLS enabled on kibana, (if you want to run Kibana without TLS you can provide the following config flag --xpack.ingestManager.fleet.tlsCheckDisabled=false) + + +- {kib-repo}blob/{branch}/x-pack/plugins/ingest_pipelines/README.md[ingestPipelines] + +The ingest_pipelines plugin provides Kibana support for Elasticsearch's ingest nodes. Please refer to the Elasticsearch documentation for more details. + + +- {kib-repo}blob/{branch}/x-pack/plugins/lens/readme.md[lens] + +Run all tests from the x-pack root directory + + +- {kib-repo}blob/{branch}/x-pack/plugins/license_management[licenseManagement] + +WARNING: Missing README. + + +- {kib-repo}blob/{branch}/x-pack/plugins/licensing/README.md[licensing] + +The licensing plugin retrieves license data from Elasticsearch at regular configurable intervals. + + +- {kib-repo}blob/{branch}/x-pack/plugins/lists/README.md[lists] + +README.md for developers working on the backend lists on how to get started +using the CURL scripts in the scripts folder. + + +- {kib-repo}blob/{branch}/x-pack/plugins/logstash[logstash] + +WARNING: Missing README. + + +- {kib-repo}blob/{branch}/x-pack/plugins/maps/README.md[maps] + +Visualize geo data from Elasticsearch or 3rd party geo-services. + + +- {kib-repo}blob/{branch}/x-pack/plugins/maps_legacy_licensing/README.md[mapsLegacyLicensing] + +This plugin provides access to the detailed tile map services from Elastic. + + +- {kib-repo}blob/{branch}/x-pack/plugins/ml[ml] + +WARNING: Missing README. + + +- {kib-repo}blob/{branch}/x-pack/plugins/monitoring[monitoring] + +WARNING: Missing README. + + +- {kib-repo}blob/{branch}/x-pack/plugins/observability/README.md[observability] + +This plugin provides shared components and services for use across observability solutions, as well as the observability landing page UI. + + +- {kib-repo}blob/{branch}/x-pack/plugins/oss_telemetry[ossTelemetry] + +WARNING: Missing README. + + +- {kib-repo}blob/{branch}/x-pack/plugins/painless_lab[painlessLab] + +WARNING: Missing README. + + +- {kib-repo}blob/{branch}/x-pack/plugins/remote_clusters[remoteClusters] + +WARNING: Missing README. + + +- {kib-repo}blob/{branch}/x-pack/plugins/reporting/README.md[reporting] + +An awesome Kibana reporting plugin + + +- {kib-repo}blob/{branch}/x-pack/plugins/rollup/README.md[rollup] + +Welcome to the Kibana rollup plugin! This plugin provides Kibana support for Elasticsearch's rollup feature. Please refer to the Elasticsearch documentation to understand rollup indices and how to create rollup jobs. + + +- {kib-repo}blob/{branch}/x-pack/plugins/searchprofiler[searchprofiler] + +WARNING: Missing README. + + +- {kib-repo}blob/{branch}/x-pack/plugins/security/README.md[security] + +See Configuring security in Kibana. + + +- {kib-repo}blob/{branch}/x-pack/plugins/security_solution/README.md[securitySolution] + +Welcome to the Kibana Security Solution plugin! This README will go over getting started with development and testing. + + +- {kib-repo}blob/{branch}/x-pack/plugins/snapshot_restore/README.md[snapshotRestore] + +or + + +- {kib-repo}blob/{branch}/x-pack/plugins/spaces[spaces] + +WARNING: Missing README. + + +- {kib-repo}blob/{branch}/x-pack/plugins/task_manager[taskManager] + +WARNING: Missing README. + + +- {kib-repo}blob/{branch}/x-pack/plugins/telemetry_collection_xpack/README.md[telemetryCollectionXpack] + +Gathers all usage collection, retrieving them from both: OSS and X-Pack plugins. + + +- {kib-repo}blob/{branch}/x-pack/plugins/transform[transform] + +WARNING: Missing README. + + +- {kib-repo}blob/{branch}/x-pack/plugins/translations[translations] + +WARNING: Missing README. + + +- {kib-repo}blob/{branch}/x-pack/plugins/triggers_actions_ui/README.md[triggers_actions_ui] + +The Kibana alerts and actions UI plugin provides a user interface for managing alerts and actions. +As a developer you can reuse and extend built-in alerts and actions UI functionality: + + +- {kib-repo}blob/{branch}/x-pack/plugins/ui_actions_enhanced/README.md[uiActionsEnhanced] + +- {kib-repo}blob/{branch}/x-pack/plugins/upgrade_assistant[upgradeAssistant] + +WARNING: Missing README. + + +- {kib-repo}blob/{branch}/x-pack/plugins/uptime/README.md[uptime] + +The purpose of this plugin is to provide users of Heartbeat more visibility of what's happening +in their infrastructure. + + +- {kib-repo}blob/{branch}/x-pack/plugins/watcher/README.md[watcher] + +This plugins adopts some conventions in addition to or in place of conventions in Kibana (at the time of the plugin's creation): + diff --git a/docs/developer/best-practices/images/state_inside_the_link.png b/docs/developer/best-practices/images/state_inside_the_link.png new file mode 100644 index 0000000000000..833478ccbda68 Binary files /dev/null and b/docs/developer/best-practices/images/state_inside_the_link.png differ diff --git a/docs/developer/best-practices/index.asciidoc b/docs/developer/best-practices/index.asciidoc index 42cee6ef0e58a..13ea010d0aa96 100644 --- a/docs/developer/best-practices/index.asciidoc +++ b/docs/developer/best-practices/index.asciidoc @@ -122,6 +122,14 @@ In addition, if users are relying on state stored in your app’s URL as part of your public contract, keep in mind that you may also need to provide backwards compatibility for bookmarked URLs. +[discrete] +=== Routing, Navigation and URL + +The {kib} platform provides a set of tools to help developers build consistent experience around routing and browser navigation. +Some of that tooling is inside `core`, some is available as part of various plugins. + +<> to get an idea of available tools and common approaches for handling routing and browser navigation. + [discrete] === Testing & stability @@ -131,6 +139,8 @@ Review: * <> * <> +include::navigation.asciidoc[leveloffset=+1] + include::stability.asciidoc[leveloffset=+1] include::security.asciidoc[leveloffset=+1] diff --git a/docs/developer/best-practices/navigation.asciidoc b/docs/developer/best-practices/navigation.asciidoc new file mode 100644 index 0000000000000..d01f2c2aa0f95 --- /dev/null +++ b/docs/developer/best-practices/navigation.asciidoc @@ -0,0 +1,226 @@ +[[kibana-navigation]] +== Routing, Navigation and URL + +The {kib} platform provides a set of tools to help developers build consistent experience around routing and browser navigation. +Some of that tooling is inside `core`, some is available as part of various plugins. + +The purpose of this guide is to give a high-level overview of available tools and to explain common approaches for handling routing and browser navigation. + +This guide covers following topics: + +* <> +* <> +* <> +* <> +* <> +* <> + +[[deep-linking]] +=== Deep-linking into {kib} apps + +Assuming you want to link from your app to *Discover*. When building such URL there are two things to consider: + +1. Prepending a proper `basePath`. +2. Specifying *Discover* state. + +==== Prepending a proper `basePath` + +To prepend {kib}'s `basePath` use {kib-repo}tree/{branch}/docs/development/core/public/kibana-plugin-core-public.ibasepath.prepend.md[core.http.basePath.prepend] helper: + +[source,typescript jsx] +---- +const discoverUrl = core.http.basePath.prepend(`/discover`); + +console.log(discoverUrl); // http://localhost:5601/bpr/s/space/app/discover +---- + +==== Specifying state + +**Consider a {kib} app URL a part of app's plugin contract:** + +. Avoid hardcoding other app's URL in your app's code. +. Avoid generating other app's state and serializing it into URL query params. + +[source,typescript jsx] +---- +// Avoid relying on other app's state structure in your app's code: +const discoverUrlWithSomeState = core.http.basePath.prepend(`/discover#/?_g=(filters:!(),refreshInterval:(pause:!t,value:0),time:(from:'2020-09-10T11:39:50.203Z',to:'2020-09-10T11:40:20.249Z'))&_a=(columns:!(_source),filters:!(),index:'90943e30-9a47-11e8-b64d-95841ca0b247',interval:auto,query:(language:kuery,query:''),sort:!())`); +---- + +Instead, each app should expose {kib-repo}tree/{branch}/src/plugins/share/public/url_generators/README.md[a URL generator]. +Other apps should use those URL generators for creating URLs. + +[source,typescript jsx] +---- +// Properly generated URL to *Discover* app. Generator code is owned by *Discover* app and available on *Discover*'s plugin contract. +const discoverUrl = discoverUrlGenerator.createUrl({filters, timeRange}); +---- + +To get a better idea, take a look at *Discover* URL generator {kib-repo}tree/{branch}/src/plugins/discover/public/url_generator.ts[implementation]. +It allows specifying various **Discover** app state pieces like: index pattern, filters, query, time range and more. + +There are two ways to access other's app URL generator in your code: + +1. From a plugin contract of a destination app *(preferred)*. +2. Using URL generator service instance on `share` plugin contract (in case an explicit plugin dependency is not possible). + +In case you want other apps to link to your app, then you should create a URL generator and expose it on your plugin's contract. + + +[[navigating-between-kibana-apps]] +=== Navigating between {kib} apps + +{kib} is a single page application and there is a set of simple rules developers should follow +to make sure there is no page reload when navigating from one place in {kib} to another. + +For example, navigation using native browser APIs would cause a full page reload. + +[source,js] +---- +const urlToADashboard = core.http.basePath.prepend(`/dashboard/my-dashboard`); + +// this would cause a full page reload: +window.location.href = urlToADashboard; +---- + +To navigate between different {kib} apps without a page reload there are APIs in `core`: + +* {kib-repo}tree/{branch}/docs/development/core/public/kibana-plugin-core-public.applicationstart.navigatetoapp.md[core.application.navigateToApp] +* {kib-repo}tree/{branch}/docs/development/core/public/kibana-plugin-core-public.applicationstart.navigatetourl.md[core.application.navigateToUrl] + +*Rendering a link to a different {kib} app on its own would also cause a full page reload:* + +[source,typescript jsx] +---- +const myLink = () => + Go to Dashboard; +---- + +A workaround could be to handle a click, prevent browser navigation and use `core.application.navigateToApp` API: + +[source,typescript jsx] +---- +const MySPALink = () => + { + e.preventDefault(); + core.application.navigateToApp('dashboard', { path: '/my-dashboard' }); + }} + > + Go to Dashboard + ; +---- + +As it would be too much boilerplate to do this for each {kib} link in your app, there is a handy wrapper that helps with it: +{kib-repo}tree/{branch}/src/plugins/kibana_react/public/app_links/redirect_app_link.tsx#L49[RedirectAppLinks]. + +[source,typescript jsx] +---- +const MyApp = () => + + {/*...*/} + {/* navigations using this link will happen in SPA friendly way */} + Go to Dashboard + {/*...*/} + +---- + +[[routing]] +=== Setting up internal app routing + +It is very common for {kib} apps to use React and React Router. +Common rules to follow in this scenario: + +* Set up `BrowserRouter` and not `HashRouter`. +* *Initialize your router with `history` instance provided by the `core`.* + +This is required to make sure `core` is aware of navigations triggered inside your app, so it could act accordingly when needed. + +* `Core`'s {kib-repo}tree/{branch}/docs/development/core/public/kibana-plugin-core-public.scopedhistory.md[ScopedHistory] instance. +* {kib-repo}tree/{branch}/docs/development/core/public/kibana-plugin-core-public.appmountparameters.history.md[Example usage] +* {kib-repo}tree/{branch}/test/plugin_functional/plugins/core_plugin_a/public/application.tsx#L120[Example plugin] + +Relative links will be resolved relative to your app's route (e.g.: `http://localhost5601/app/{your-app-id}`) +and setting up internal links in your app in SPA friendly way would look something like: + +[source,typescript jsx] +---- +import {Link} from 'react-router-dom'; + +const MyInternalLink = () => +---- + +[[history-and-location]] +=== Using history and browser location + +Try to avoid using `window.location` and `window.history` directly. + +Instead, consider using {kib-repo}tree/{branch}/docs/development/core/public/kibana-plugin-core-public.scopedhistory.md[ScopedHistory] +instance provided by `core`. + +* This way `core` will know about location changes triggered within your app, and it would act accordingly. +* Some plugins are listening to location changes. Triggering location change manually could lead to unpredictable and hard-to-catch bugs. + +Common use-case for using +`core`'s {kib-repo}tree/{branch}/docs/development/core/public/kibana-plugin-core-public.scopedhistory.md[ScopedHistory] directly: + +* Reading/writing query params or hash. +* Imperatively triggering internal navigations within your app. +* Listening to browser location changes. + + +[[state-sync]] +=== Syncing state with URL + +Historically {kib} apps store _a lot_ of application state in the URL. +The most common pattern that {kib} apps follow today is storing state in `_a` and `_g` query params in https://github.com/w33ble/rison-node#readme[rison] format. +[[query-params]] +Those query params follow the convention: + +* `_g` (*global*) - global UI state that should be shared and synced across multiple apps. common example from Analyze group apps: time range, refresh interval, *pinned* filters. +* `_a` (*application*) - UI state scoped to current app. + +NOTE: After migrating to KP platform we got navigations without page reloads. Since then there is no real need to follow `_g` and `_a` separation anymore. It's up you to decide if you want to follow this pattern or if you prefer a single query param or something else. The need for this separation earlier is explained in <>. + +There are utils to help you to implement such kind of state syncing. + +**When you should consider using state syncing utils:** + +* You want to sync your application state with URL in similar manner Analyze group applications do. +* You want to follow platform's <> out of the box. +* You want to support `state:storeInSessionStore` escape hatch for URL overflowing out of the box. +* You should also consider using them if you'd like to serialize state to different (not `rison`) format. Utils are composable, and you can implement your own `storage`. +* In case you want to sync part of your state with URL, but other part of it with browser storage. + +**When you shouldn't use state syncing utils:** + +* Adding a query param flag or simple key/value to the URL. + +Follow {kib-repo}tree/{branch}/src/plugins/kibana_utils/docs/state_sync#state-syncing-utilities[these] docs to learn more. + + +[[preserve-state]] +=== Preserving state between navigations + +Consider the scenario: + +1. You are in *Dashboard* app looking at a dashboard with some filters applied; +2. Navigate to *Discover* using in-app navigation; +3. Change the time filter' +4. Navigate to *Dashboard* using in-app navigation. + +You'd notice that you were navigated to *Dashboard* app with the *same state* that you left it with, +except that the time filter has changed to the one you applied on *Discover* app. + +Historically {kib} Analyze groups apps achieve that behavior relying on state in the URL. +If you'd have a closer look on a link in the navigation, +you'd notice that state is stored inside that link, and it also gets updated whenever relevant state changes happen: + +[role="screenshot"] +image:images/state_inside_the_link.png[State is stored inside the navigation link] + +This is where <> into `_a` and `_g` query params comes into play. What is considered a *global* state gets constantly updated in those navigation links. In the example above it was a time filter. +This is backed by {kib-repo}tree/{branch}/src/plugins/kibana_utils/public/state_management/url/kbn_url_tracker.ts#L57[KbnUrlTracker] util. You can use it to achieve similar behavior. + +NOTE: After migrating to KP navigation works without page reloads and all plugins are loaded simultaneously. +Hence, likely there are simpler ways to preserve state of your application, unless you want to do it through URL. diff --git a/docs/developer/plugin-list.asciidoc b/docs/developer/plugin-list.asciidoc index 275fdf8fb69ad..7727cd322181f 100644 --- a/docs/developer/plugin-list.asciidoc +++ b/docs/developer/plugin-list.asciidoc @@ -341,6 +341,10 @@ and actions. or dashboards from the Kibana instance, from both server and client-side plugins +|{kib-repo}blob/{branch}/x-pack/plugins/global_search_bar/README.md[globalSearchBar] +|The GlobalSearchBar plugin provides a search interface for navigating Kibana. (It is the UI to the GlobalSearch plugin.) + + |{kib-repo}blob/{branch}/x-pack/plugins/global_search_providers[globalSearchProviders] |WARNING: Missing README. diff --git a/docs/development/core/public/kibana-plugin-core-public.assertnever.md b/docs/development/core/public/kibana-plugin-core-public.assertnever.md deleted file mode 100644 index 8fefd4450d49b..0000000000000 --- a/docs/development/core/public/kibana-plugin-core-public.assertnever.md +++ /dev/null @@ -1,24 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [assertNever](./kibana-plugin-core-public.assertnever.md) - -## assertNever() function - -Can be used in switch statements to ensure we perform exhaustive checks, see https://www.typescriptlang.org/docs/handbook/advanced-types.html\#exhaustiveness-checking - -Signature: - -```typescript -export declare function assertNever(x: never): never; -``` - -## Parameters - -| Parameter | Type | Description | -| --- | --- | --- | -| x | never | | - -Returns: - -`never` - diff --git a/docs/development/core/public/kibana-plugin-core-public.chromenavcontrols.md b/docs/development/core/public/kibana-plugin-core-public.chromenavcontrols.md index bca69adeef66b..47365782599ed 100644 --- a/docs/development/core/public/kibana-plugin-core-public.chromenavcontrols.md +++ b/docs/development/core/public/kibana-plugin-core-public.chromenavcontrols.md @@ -30,6 +30,7 @@ chrome.navControls.registerLeft({ | Method | Description | | --- | --- | -| [registerLeft(navControl)](./kibana-plugin-core-public.chromenavcontrols.registerleft.md) | Register a nav control to be presented on the left side of the chrome header. | -| [registerRight(navControl)](./kibana-plugin-core-public.chromenavcontrols.registerright.md) | Register a nav control to be presented on the right side of the chrome header. | +| [registerCenter(navControl)](./kibana-plugin-core-public.chromenavcontrols.registercenter.md) | Register a nav control to be presented on the top-center side of the chrome header. | +| [registerLeft(navControl)](./kibana-plugin-core-public.chromenavcontrols.registerleft.md) | Register a nav control to be presented on the bottom-left side of the chrome header. | +| [registerRight(navControl)](./kibana-plugin-core-public.chromenavcontrols.registerright.md) | Register a nav control to be presented on the top-right side of the chrome header. | diff --git a/docs/development/core/public/kibana-plugin-core-public.chromenavcontrols.registercenter.md b/docs/development/core/public/kibana-plugin-core-public.chromenavcontrols.registercenter.md new file mode 100644 index 0000000000000..2f921050e58dd --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.chromenavcontrols.registercenter.md @@ -0,0 +1,24 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [ChromeNavControls](./kibana-plugin-core-public.chromenavcontrols.md) > [registerCenter](./kibana-plugin-core-public.chromenavcontrols.registercenter.md) + +## ChromeNavControls.registerCenter() method + +Register a nav control to be presented on the top-center side of the chrome header. + +Signature: + +```typescript +registerCenter(navControl: ChromeNavControl): void; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| navControl | ChromeNavControl | | + +Returns: + +`void` + diff --git a/docs/development/core/public/kibana-plugin-core-public.chromenavcontrols.registerleft.md b/docs/development/core/public/kibana-plugin-core-public.chromenavcontrols.registerleft.md index c5c78bf9fb1da..514c44bd9d710 100644 --- a/docs/development/core/public/kibana-plugin-core-public.chromenavcontrols.registerleft.md +++ b/docs/development/core/public/kibana-plugin-core-public.chromenavcontrols.registerleft.md @@ -4,7 +4,7 @@ ## ChromeNavControls.registerLeft() method -Register a nav control to be presented on the left side of the chrome header. +Register a nav control to be presented on the bottom-left side of the chrome header. Signature: diff --git a/docs/development/core/public/kibana-plugin-core-public.chromenavcontrols.registerright.md b/docs/development/core/public/kibana-plugin-core-public.chromenavcontrols.registerright.md index 12058f1d16ab9..eb56e0e38c6c9 100644 --- a/docs/development/core/public/kibana-plugin-core-public.chromenavcontrols.registerright.md +++ b/docs/development/core/public/kibana-plugin-core-public.chromenavcontrols.registerright.md @@ -4,7 +4,7 @@ ## ChromeNavControls.registerRight() method -Register a nav control to be presented on the right side of the chrome header. +Register a nav control to be presented on the top-right side of the chrome header. Signature: diff --git a/docs/development/core/public/kibana-plugin-core-public.chromestart.getnavtype_.md b/docs/development/core/public/kibana-plugin-core-public.chromestart.getnavtype_.md deleted file mode 100644 index 09864be43996d..0000000000000 --- a/docs/development/core/public/kibana-plugin-core-public.chromestart.getnavtype_.md +++ /dev/null @@ -1,17 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [ChromeStart](./kibana-plugin-core-public.chromestart.md) > [getNavType$](./kibana-plugin-core-public.chromestart.getnavtype_.md) - -## ChromeStart.getNavType$() method - -Get the navigation type TODO \#64541 Can delete - -Signature: - -```typescript -getNavType$(): Observable; -``` -Returns: - -`Observable` - diff --git a/docs/development/core/public/kibana-plugin-core-public.chromestart.md b/docs/development/core/public/kibana-plugin-core-public.chromestart.md index e983ad50d2afe..2594848ef0847 100644 --- a/docs/development/core/public/kibana-plugin-core-public.chromestart.md +++ b/docs/development/core/public/kibana-plugin-core-public.chromestart.md @@ -59,7 +59,6 @@ core.chrome.setHelpExtension(elem => { | [getHelpExtension$()](./kibana-plugin-core-public.chromestart.gethelpextension_.md) | Get an observable of the current custom help conttent | | [getIsNavDrawerLocked$()](./kibana-plugin-core-public.chromestart.getisnavdrawerlocked_.md) | Get an observable of the current locked state of the nav drawer. | | [getIsVisible$()](./kibana-plugin-core-public.chromestart.getisvisible_.md) | Get an observable of the current visibility state of the chrome. | -| [getNavType$()](./kibana-plugin-core-public.chromestart.getnavtype_.md) | Get the navigation type TODO \#64541 Can delete | | [removeApplicationClass(className)](./kibana-plugin-core-public.chromestart.removeapplicationclass.md) | Remove a className added with addApplicationClass(). If className is unknown it is ignored. | | [setAppTitle(appTitle)](./kibana-plugin-core-public.chromestart.setapptitle.md) | Sets the current app's title | | [setBadge(badge)](./kibana-plugin-core-public.chromestart.setbadge.md) | Override the current badge | diff --git a/docs/development/core/public/kibana-plugin-core-public.deepfreeze.md b/docs/development/core/public/kibana-plugin-core-public.deepfreeze.md deleted file mode 100644 index 7c879b659a852..0000000000000 --- a/docs/development/core/public/kibana-plugin-core-public.deepfreeze.md +++ /dev/null @@ -1,24 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [deepFreeze](./kibana-plugin-core-public.deepfreeze.md) - -## deepFreeze() function - -Apply Object.freeze to a value recursively and convert the return type to Readonly variant recursively - -Signature: - -```typescript -export declare function deepFreeze(object: T): RecursiveReadonly; -``` - -## Parameters - -| Parameter | Type | Description | -| --- | --- | --- | -| object | T | | - -Returns: - -`RecursiveReadonly` - diff --git a/docs/development/core/public/kibana-plugin-core-public.freezable.md b/docs/development/core/public/kibana-plugin-core-public.freezable.md deleted file mode 100644 index fee87dde25c28..0000000000000 --- a/docs/development/core/public/kibana-plugin-core-public.freezable.md +++ /dev/null @@ -1,14 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [Freezable](./kibana-plugin-core-public.freezable.md) - -## Freezable type - - -Signature: - -```typescript -export declare type Freezable = { - [k: string]: any; -} | any[]; -``` diff --git a/docs/development/core/public/kibana-plugin-core-public.getflattenedobject.md b/docs/development/core/public/kibana-plugin-core-public.getflattenedobject.md deleted file mode 100644 index 3ef9b6bf703eb..0000000000000 --- a/docs/development/core/public/kibana-plugin-core-public.getflattenedobject.md +++ /dev/null @@ -1,30 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [getFlattenedObject](./kibana-plugin-core-public.getflattenedobject.md) - -## getFlattenedObject() function - -Flattens a deeply nested object to a map of dot-separated paths pointing to all primitive values \*\*and arrays\*\* from `rootValue`. - -example: getFlattenedObject({ a: { b: 1, c: \[2,3\] } }) // => { 'a.b': 1, 'a.c': \[2,3\] } - -Signature: - -```typescript -export declare function getFlattenedObject(rootValue: Record): { - [key: string]: any; -}; -``` - -## Parameters - -| Parameter | Type | Description | -| --- | --- | --- | -| rootValue | Record<string, any> | | - -Returns: - -`{ - [key: string]: any; -}` - diff --git a/docs/development/core/public/kibana-plugin-core-public.isrelativeurl.md b/docs/development/core/public/kibana-plugin-core-public.isrelativeurl.md deleted file mode 100644 index 3c2ffa6340a97..0000000000000 --- a/docs/development/core/public/kibana-plugin-core-public.isrelativeurl.md +++ /dev/null @@ -1,24 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [isRelativeUrl](./kibana-plugin-core-public.isrelativeurl.md) - -## isRelativeUrl() function - -Determine if a url is relative. Any url including a protocol, hostname, or port is not considered relative. This means that absolute \*paths\* are considered to be relative \*urls\* - -Signature: - -```typescript -export declare function isRelativeUrl(candidatePath: string): boolean; -``` - -## Parameters - -| Parameter | Type | Description | -| --- | --- | --- | -| candidatePath | string | | - -Returns: - -`boolean` - diff --git a/docs/development/core/public/kibana-plugin-core-public.md b/docs/development/core/public/kibana-plugin-core-public.md index 08b12190ef638..f2bf72a597656 100644 --- a/docs/development/core/public/kibana-plugin-core-public.md +++ b/docs/development/core/public/kibana-plugin-core-public.md @@ -27,16 +27,6 @@ The plugin integrates with the core system via lifecycle events: `setup` | [AppNavLinkStatus](./kibana-plugin-core-public.appnavlinkstatus.md) | Status of the application's navLink. | | [AppStatus](./kibana-plugin-core-public.appstatus.md) | Accessibility status of an application. | -## Functions - -| Function | Description | -| --- | --- | -| [assertNever(x)](./kibana-plugin-core-public.assertnever.md) | Can be used in switch statements to ensure we perform exhaustive checks, see https://www.typescriptlang.org/docs/handbook/advanced-types.html\#exhaustiveness-checking | -| [deepFreeze(object)](./kibana-plugin-core-public.deepfreeze.md) | Apply Object.freeze to a value recursively and convert the return type to Readonly variant recursively | -| [getFlattenedObject(rootValue)](./kibana-plugin-core-public.getflattenedobject.md) | Flattens a deeply nested object to a map of dot-separated paths pointing to all primitive values \*\*and arrays\*\* from rootValue.example: getFlattenedObject({ a: { b: 1, c: \[2,3\] } }) // => { 'a.b': 1, 'a.c': \[2,3\] } | -| [isRelativeUrl(candidatePath)](./kibana-plugin-core-public.isrelativeurl.md) | Determine if a url is relative. Any url including a protocol, hostname, or port is not considered relative. This means that absolute \*paths\* are considered to be relative \*urls\* | -| [modifyUrl(url, urlModifier)](./kibana-plugin-core-public.modifyurl.md) | Takes a URL and a function that takes the meaningful parts of the URL as a key-value object, modifies some or all of the parts, and returns the modified parts formatted again as a url.Url Parts sent: - protocol - slashes (does the url have the //) - auth - hostname (just the name of the host, no port or auth information) - port - pathname (the path after the hostname, no query or hash, starts with a slash if there was a path) - query (always an object, even when no query on original url) - hashWhy? - The default url library in node produces several conflicting properties on the "parsed" output. Modifying any of these might lead to the modifications being ignored (depending on which property was modified) - It's not always clear whether to use path/pathname, host/hostname, so this tries to add helpful constraints | - ## Interfaces | Interface | Description | @@ -128,7 +118,6 @@ The plugin integrates with the core system via lifecycle events: `setup` | [ToastOptions](./kibana-plugin-core-public.toastoptions.md) | Options available for [IToasts](./kibana-plugin-core-public.itoasts.md) APIs. | | [UiSettingsParams](./kibana-plugin-core-public.uisettingsparams.md) | UiSettings parameters defined by the plugins. | | [UiSettingsState](./kibana-plugin-core-public.uisettingsstate.md) | | -| [URLMeaningfulParts](./kibana-plugin-core-public.urlmeaningfulparts.md) | We define our own typings because the current version of @types/node declares properties to be optional "hostname?: string". Although, parse call returns "hostname: null \| string". | | [UserProvidedValues](./kibana-plugin-core-public.userprovidedvalues.md) | Describes the values explicitly set by user. | ## Variables @@ -156,7 +145,6 @@ The plugin integrates with the core system via lifecycle events: `setup` | [ChromeHelpExtensionMenuLink](./kibana-plugin-core-public.chromehelpextensionmenulink.md) | | | [ChromeNavLinkUpdateableFields](./kibana-plugin-core-public.chromenavlinkupdateablefields.md) | | | [FatalErrorsStart](./kibana-plugin-core-public.fatalerrorsstart.md) | FatalErrors stop the Kibana Public Core and displays a fatal error screen with details about the Kibana build and the error. | -| [Freezable](./kibana-plugin-core-public.freezable.md) | | | [HandlerContextType](./kibana-plugin-core-public.handlercontexttype.md) | Extracts the type of the first argument of a [HandlerFunction](./kibana-plugin-core-public.handlerfunction.md) to represent the type of the context. | | [HandlerFunction](./kibana-plugin-core-public.handlerfunction.md) | A function that accepts a context object and an optional number of additional arguments. Used for the generic types in [IContextContainer](./kibana-plugin-core-public.icontextcontainer.md) | | [HandlerParameters](./kibana-plugin-core-public.handlerparameters.md) | Extracts the types of the additional arguments of a [HandlerFunction](./kibana-plugin-core-public.handlerfunction.md), excluding the [HandlerContextType](./kibana-plugin-core-public.handlercontexttype.md). | diff --git a/docs/development/core/public/kibana-plugin-core-public.modifyurl.md b/docs/development/core/public/kibana-plugin-core-public.modifyurl.md deleted file mode 100644 index b174f733a5c64..0000000000000 --- a/docs/development/core/public/kibana-plugin-core-public.modifyurl.md +++ /dev/null @@ -1,31 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [modifyUrl](./kibana-plugin-core-public.modifyurl.md) - -## modifyUrl() function - -Takes a URL and a function that takes the meaningful parts of the URL as a key-value object, modifies some or all of the parts, and returns the modified parts formatted again as a url. - -Url Parts sent: - protocol - slashes (does the url have the //) - auth - hostname (just the name of the host, no port or auth information) - port - pathname (the path after the hostname, no query or hash, starts with a slash if there was a path) - query (always an object, even when no query on original url) - hash - -Why? - The default url library in node produces several conflicting properties on the "parsed" output. Modifying any of these might lead to the modifications being ignored (depending on which property was modified) - It's not always clear whether to use path/pathname, host/hostname, so this tries to add helpful constraints - -Signature: - -```typescript -export declare function modifyUrl(url: string, urlModifier: (urlParts: URLMeaningfulParts) => Partial | void): string; -``` - -## Parameters - -| Parameter | Type | Description | -| --- | --- | --- | -| url | string | | -| urlModifier | (urlParts: URLMeaningfulParts) => Partial<URLMeaningfulParts> | void | | - -Returns: - -`string` - -The modified and reformatted url - diff --git a/docs/development/core/public/kibana-plugin-core-public.urlmeaningfulparts.auth.md b/docs/development/core/public/kibana-plugin-core-public.urlmeaningfulparts.auth.md deleted file mode 100644 index 238dd66885896..0000000000000 --- a/docs/development/core/public/kibana-plugin-core-public.urlmeaningfulparts.auth.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [URLMeaningfulParts](./kibana-plugin-core-public.urlmeaningfulparts.md) > [auth](./kibana-plugin-core-public.urlmeaningfulparts.auth.md) - -## URLMeaningfulParts.auth property - -Signature: - -```typescript -auth?: string | null; -``` diff --git a/docs/development/core/public/kibana-plugin-core-public.urlmeaningfulparts.hash.md b/docs/development/core/public/kibana-plugin-core-public.urlmeaningfulparts.hash.md deleted file mode 100644 index 161e7dc7ebfae..0000000000000 --- a/docs/development/core/public/kibana-plugin-core-public.urlmeaningfulparts.hash.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [URLMeaningfulParts](./kibana-plugin-core-public.urlmeaningfulparts.md) > [hash](./kibana-plugin-core-public.urlmeaningfulparts.hash.md) - -## URLMeaningfulParts.hash property - -Signature: - -```typescript -hash?: string | null; -``` diff --git a/docs/development/core/public/kibana-plugin-core-public.urlmeaningfulparts.hostname.md b/docs/development/core/public/kibana-plugin-core-public.urlmeaningfulparts.hostname.md deleted file mode 100644 index f1884718337b5..0000000000000 --- a/docs/development/core/public/kibana-plugin-core-public.urlmeaningfulparts.hostname.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [URLMeaningfulParts](./kibana-plugin-core-public.urlmeaningfulparts.md) > [hostname](./kibana-plugin-core-public.urlmeaningfulparts.hostname.md) - -## URLMeaningfulParts.hostname property - -Signature: - -```typescript -hostname?: string | null; -``` diff --git a/docs/development/core/public/kibana-plugin-core-public.urlmeaningfulparts.md b/docs/development/core/public/kibana-plugin-core-public.urlmeaningfulparts.md deleted file mode 100644 index 2816d4c7df541..0000000000000 --- a/docs/development/core/public/kibana-plugin-core-public.urlmeaningfulparts.md +++ /dev/null @@ -1,27 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [URLMeaningfulParts](./kibana-plugin-core-public.urlmeaningfulparts.md) - -## URLMeaningfulParts interface - -We define our own typings because the current version of @types/node declares properties to be optional "hostname?: string". Although, parse call returns "hostname: null \| string". - -Signature: - -```typescript -export interface URLMeaningfulParts -``` - -## Properties - -| Property | Type | Description | -| --- | --- | --- | -| [auth](./kibana-plugin-core-public.urlmeaningfulparts.auth.md) | string | null | | -| [hash](./kibana-plugin-core-public.urlmeaningfulparts.hash.md) | string | null | | -| [hostname](./kibana-plugin-core-public.urlmeaningfulparts.hostname.md) | string | null | | -| [pathname](./kibana-plugin-core-public.urlmeaningfulparts.pathname.md) | string | null | | -| [port](./kibana-plugin-core-public.urlmeaningfulparts.port.md) | string | null | | -| [protocol](./kibana-plugin-core-public.urlmeaningfulparts.protocol.md) | string | null | | -| [query](./kibana-plugin-core-public.urlmeaningfulparts.query.md) | ParsedQuery | | -| [slashes](./kibana-plugin-core-public.urlmeaningfulparts.slashes.md) | boolean | null | | - diff --git a/docs/development/core/public/kibana-plugin-core-public.urlmeaningfulparts.pathname.md b/docs/development/core/public/kibana-plugin-core-public.urlmeaningfulparts.pathname.md deleted file mode 100644 index 5ad21f004481c..0000000000000 --- a/docs/development/core/public/kibana-plugin-core-public.urlmeaningfulparts.pathname.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [URLMeaningfulParts](./kibana-plugin-core-public.urlmeaningfulparts.md) > [pathname](./kibana-plugin-core-public.urlmeaningfulparts.pathname.md) - -## URLMeaningfulParts.pathname property - -Signature: - -```typescript -pathname?: string | null; -``` diff --git a/docs/development/core/public/kibana-plugin-core-public.urlmeaningfulparts.port.md b/docs/development/core/public/kibana-plugin-core-public.urlmeaningfulparts.port.md deleted file mode 100644 index 2e70da2f17421..0000000000000 --- a/docs/development/core/public/kibana-plugin-core-public.urlmeaningfulparts.port.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [URLMeaningfulParts](./kibana-plugin-core-public.urlmeaningfulparts.md) > [port](./kibana-plugin-core-public.urlmeaningfulparts.port.md) - -## URLMeaningfulParts.port property - -Signature: - -```typescript -port?: string | null; -``` diff --git a/docs/development/core/public/kibana-plugin-core-public.urlmeaningfulparts.protocol.md b/docs/development/core/public/kibana-plugin-core-public.urlmeaningfulparts.protocol.md deleted file mode 100644 index cedc7f0b878e3..0000000000000 --- a/docs/development/core/public/kibana-plugin-core-public.urlmeaningfulparts.protocol.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [URLMeaningfulParts](./kibana-plugin-core-public.urlmeaningfulparts.md) > [protocol](./kibana-plugin-core-public.urlmeaningfulparts.protocol.md) - -## URLMeaningfulParts.protocol property - -Signature: - -```typescript -protocol?: string | null; -``` diff --git a/docs/development/core/public/kibana-plugin-core-public.urlmeaningfulparts.query.md b/docs/development/core/public/kibana-plugin-core-public.urlmeaningfulparts.query.md deleted file mode 100644 index a9541efe0882a..0000000000000 --- a/docs/development/core/public/kibana-plugin-core-public.urlmeaningfulparts.query.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [URLMeaningfulParts](./kibana-plugin-core-public.urlmeaningfulparts.md) > [query](./kibana-plugin-core-public.urlmeaningfulparts.query.md) - -## URLMeaningfulParts.query property - -Signature: - -```typescript -query: ParsedQuery; -``` diff --git a/docs/development/core/public/kibana-plugin-core-public.urlmeaningfulparts.slashes.md b/docs/development/core/public/kibana-plugin-core-public.urlmeaningfulparts.slashes.md deleted file mode 100644 index cb28a25f9e162..0000000000000 --- a/docs/development/core/public/kibana-plugin-core-public.urlmeaningfulparts.slashes.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [URLMeaningfulParts](./kibana-plugin-core-public.urlmeaningfulparts.md) > [slashes](./kibana-plugin-core-public.urlmeaningfulparts.slashes.md) - -## URLMeaningfulParts.slashes property - -Signature: - -```typescript -slashes?: boolean | null; -``` diff --git a/docs/development/core/server/kibana-plugin-core-server.assertnever.md b/docs/development/core/server/kibana-plugin-core-server.assertnever.md deleted file mode 100644 index c13c88df9b9bf..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.assertnever.md +++ /dev/null @@ -1,24 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [assertNever](./kibana-plugin-core-server.assertnever.md) - -## assertNever() function - -Can be used in switch statements to ensure we perform exhaustive checks, see https://www.typescriptlang.org/docs/handbook/advanced-types.html\#exhaustiveness-checking - -Signature: - -```typescript -export declare function assertNever(x: never): never; -``` - -## Parameters - -| Parameter | Type | Description | -| --- | --- | --- | -| x | never | | - -Returns: - -`never` - diff --git a/docs/development/core/server/kibana-plugin-core-server.deepfreeze.md b/docs/development/core/server/kibana-plugin-core-server.deepfreeze.md deleted file mode 100644 index 946050bff0585..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.deepfreeze.md +++ /dev/null @@ -1,24 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [deepFreeze](./kibana-plugin-core-server.deepfreeze.md) - -## deepFreeze() function - -Apply Object.freeze to a value recursively and convert the return type to Readonly variant recursively - -Signature: - -```typescript -export declare function deepFreeze(object: T): RecursiveReadonly; -``` - -## Parameters - -| Parameter | Type | Description | -| --- | --- | --- | -| object | T | | - -Returns: - -`RecursiveReadonly` - diff --git a/docs/development/core/server/kibana-plugin-core-server.freezable.md b/docs/development/core/server/kibana-plugin-core-server.freezable.md deleted file mode 100644 index 32ba89e8370c1..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.freezable.md +++ /dev/null @@ -1,14 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [Freezable](./kibana-plugin-core-server.freezable.md) - -## Freezable type - - -Signature: - -```typescript -export declare type Freezable = { - [k: string]: any; -} | any[]; -``` diff --git a/docs/development/core/server/kibana-plugin-core-server.getflattenedobject.md b/docs/development/core/server/kibana-plugin-core-server.getflattenedobject.md deleted file mode 100644 index 2e7850ca579f6..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.getflattenedobject.md +++ /dev/null @@ -1,30 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [getFlattenedObject](./kibana-plugin-core-server.getflattenedobject.md) - -## getFlattenedObject() function - -Flattens a deeply nested object to a map of dot-separated paths pointing to all primitive values \*\*and arrays\*\* from `rootValue`. - -example: getFlattenedObject({ a: { b: 1, c: \[2,3\] } }) // => { 'a.b': 1, 'a.c': \[2,3\] } - -Signature: - -```typescript -export declare function getFlattenedObject(rootValue: Record): { - [key: string]: any; -}; -``` - -## Parameters - -| Parameter | Type | Description | -| --- | --- | --- | -| rootValue | Record<string, any> | | - -Returns: - -`{ - [key: string]: any; -}` - diff --git a/docs/development/core/server/kibana-plugin-core-server.isrelativeurl.md b/docs/development/core/server/kibana-plugin-core-server.isrelativeurl.md deleted file mode 100644 index bff9eb05419be..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.isrelativeurl.md +++ /dev/null @@ -1,24 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [isRelativeUrl](./kibana-plugin-core-server.isrelativeurl.md) - -## isRelativeUrl() function - -Determine if a url is relative. Any url including a protocol, hostname, or port is not considered relative. This means that absolute \*paths\* are considered to be relative \*urls\* - -Signature: - -```typescript -export declare function isRelativeUrl(candidatePath: string): boolean; -``` - -## Parameters - -| Parameter | Type | Description | -| --- | --- | --- | -| candidatePath | string | | - -Returns: - -`boolean` - diff --git a/docs/development/core/server/kibana-plugin-core-server.md b/docs/development/core/server/kibana-plugin-core-server.md index c16600d1d0492..30b98b9f0553e 100644 --- a/docs/development/core/server/kibana-plugin-core-server.md +++ b/docs/development/core/server/kibana-plugin-core-server.md @@ -42,13 +42,8 @@ The plugin integrates with the core system via lifecycle events: `setup` | Function | Description | | --- | --- | -| [assertNever(x)](./kibana-plugin-core-server.assertnever.md) | Can be used in switch statements to ensure we perform exhaustive checks, see https://www.typescriptlang.org/docs/handbook/advanced-types.html\#exhaustiveness-checking | -| [deepFreeze(object)](./kibana-plugin-core-server.deepfreeze.md) | Apply Object.freeze to a value recursively and convert the return type to Readonly variant recursively | | [exportSavedObjectsToStream({ types, objects, search, savedObjectsClient, exportSizeLimit, includeReferencesDeep, excludeExportDetails, namespace, })](./kibana-plugin-core-server.exportsavedobjectstostream.md) | Generates sorted saved object stream to be used for export. See the [options](./kibana-plugin-core-server.savedobjectsexportoptions.md) for more detailed information. | -| [getFlattenedObject(rootValue)](./kibana-plugin-core-server.getflattenedobject.md) | Flattens a deeply nested object to a map of dot-separated paths pointing to all primitive values \*\*and arrays\*\* from rootValue.example: getFlattenedObject({ a: { b: 1, c: \[2,3\] } }) // => { 'a.b': 1, 'a.c': \[2,3\] } | | [importSavedObjectsFromStream({ readStream, objectLimit, overwrite, createNewCopies, savedObjectsClient, typeRegistry, namespace, })](./kibana-plugin-core-server.importsavedobjectsfromstream.md) | Import saved objects from given stream. See the [options](./kibana-plugin-core-server.savedobjectsimportoptions.md) for more detailed information. | -| [isRelativeUrl(candidatePath)](./kibana-plugin-core-server.isrelativeurl.md) | Determine if a url is relative. Any url including a protocol, hostname, or port is not considered relative. This means that absolute \*paths\* are considered to be relative \*urls\* | -| [modifyUrl(url, urlModifier)](./kibana-plugin-core-server.modifyurl.md) | Takes a URL and a function that takes the meaningful parts of the URL as a key-value object, modifies some or all of the parts, and returns the modified parts formatted again as a url.Url Parts sent: - protocol - slashes (does the url have the //) - auth - hostname (just the name of the host, no port or auth information) - port - pathname (the path after the hostname, no query or hash, starts with a slash if there was a path) - query (always an object, even when no query on original url) - hashWhy? - The default url library in node produces several conflicting properties on the "parsed" output. Modifying any of these might lead to the modifications being ignored (depending on which property was modified) - It's not always clear whether to use path/pathname, host/hostname, so this tries to add helpful constraints | | [resolveSavedObjectsImportErrors({ readStream, objectLimit, retries, savedObjectsClient, typeRegistry, namespace, createNewCopies, })](./kibana-plugin-core-server.resolvesavedobjectsimporterrors.md) | Resolve and return saved object import errors. See the [options](./kibana-plugin-core-server.savedobjectsresolveimporterrorsoptions.md) for more detailed informations. | ## Interfaces @@ -217,7 +212,6 @@ The plugin integrates with the core system via lifecycle events: `setup` | [UiSettingsParams](./kibana-plugin-core-server.uisettingsparams.md) | UiSettings parameters defined by the plugins. | | [UiSettingsServiceSetup](./kibana-plugin-core-server.uisettingsservicesetup.md) | | | [UiSettingsServiceStart](./kibana-plugin-core-server.uisettingsservicestart.md) | | -| [URLMeaningfulParts](./kibana-plugin-core-server.urlmeaningfulparts.md) | We define our own typings because the current version of @types/node declares properties to be optional "hostname?: string". Although, parse call returns "hostname: null \| string". | | [UserProvidedValues](./kibana-plugin-core-server.userprovidedvalues.md) | Describes the values explicitly set by user. | ## Variables @@ -246,7 +240,6 @@ The plugin integrates with the core system via lifecycle events: `setup` | [DestructiveRouteMethod](./kibana-plugin-core-server.destructiveroutemethod.md) | Set of HTTP methods changing the state of the server. | | [ElasticsearchClient](./kibana-plugin-core-server.elasticsearchclient.md) | Client used to query the elasticsearch cluster. | | [ElasticsearchClientConfig](./kibana-plugin-core-server.elasticsearchclientconfig.md) | Configuration options to be used to create a [cluster client](./kibana-plugin-core-server.iclusterclient.md) using the [createClient API](./kibana-plugin-core-server.elasticsearchservicestart.createclient.md) | -| [Freezable](./kibana-plugin-core-server.freezable.md) | | | [GetAuthHeaders](./kibana-plugin-core-server.getauthheaders.md) | Get headers to authenticate a user against Elasticsearch. | | [GetAuthState](./kibana-plugin-core-server.getauthstate.md) | Gets authentication state for a request. Returned by auth interceptor. | | [HandlerContextType](./kibana-plugin-core-server.handlercontexttype.md) | Extracts the type of the first argument of a [HandlerFunction](./kibana-plugin-core-server.handlerfunction.md) to represent the type of the context. | diff --git a/docs/development/core/server/kibana-plugin-core-server.modifyurl.md b/docs/development/core/server/kibana-plugin-core-server.modifyurl.md deleted file mode 100644 index fc0bc354a3ca3..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.modifyurl.md +++ /dev/null @@ -1,31 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [modifyUrl](./kibana-plugin-core-server.modifyurl.md) - -## modifyUrl() function - -Takes a URL and a function that takes the meaningful parts of the URL as a key-value object, modifies some or all of the parts, and returns the modified parts formatted again as a url. - -Url Parts sent: - protocol - slashes (does the url have the //) - auth - hostname (just the name of the host, no port or auth information) - port - pathname (the path after the hostname, no query or hash, starts with a slash if there was a path) - query (always an object, even when no query on original url) - hash - -Why? - The default url library in node produces several conflicting properties on the "parsed" output. Modifying any of these might lead to the modifications being ignored (depending on which property was modified) - It's not always clear whether to use path/pathname, host/hostname, so this tries to add helpful constraints - -Signature: - -```typescript -export declare function modifyUrl(url: string, urlModifier: (urlParts: URLMeaningfulParts) => Partial | void): string; -``` - -## Parameters - -| Parameter | Type | Description | -| --- | --- | --- | -| url | string | | -| urlModifier | (urlParts: URLMeaningfulParts) => Partial<URLMeaningfulParts> | void | | - -Returns: - -`string` - -The modified and reformatted url - diff --git a/docs/development/core/server/kibana-plugin-core-server.urlmeaningfulparts.auth.md b/docs/development/core/server/kibana-plugin-core-server.urlmeaningfulparts.auth.md deleted file mode 100644 index 0422738669a70..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.urlmeaningfulparts.auth.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [URLMeaningfulParts](./kibana-plugin-core-server.urlmeaningfulparts.md) > [auth](./kibana-plugin-core-server.urlmeaningfulparts.auth.md) - -## URLMeaningfulParts.auth property - -Signature: - -```typescript -auth?: string | null; -``` diff --git a/docs/development/core/server/kibana-plugin-core-server.urlmeaningfulparts.hash.md b/docs/development/core/server/kibana-plugin-core-server.urlmeaningfulparts.hash.md deleted file mode 100644 index 13a3f4a9c95c8..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.urlmeaningfulparts.hash.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [URLMeaningfulParts](./kibana-plugin-core-server.urlmeaningfulparts.md) > [hash](./kibana-plugin-core-server.urlmeaningfulparts.hash.md) - -## URLMeaningfulParts.hash property - -Signature: - -```typescript -hash?: string | null; -``` diff --git a/docs/development/core/server/kibana-plugin-core-server.urlmeaningfulparts.hostname.md b/docs/development/core/server/kibana-plugin-core-server.urlmeaningfulparts.hostname.md deleted file mode 100644 index 6631f6f6744c5..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.urlmeaningfulparts.hostname.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [URLMeaningfulParts](./kibana-plugin-core-server.urlmeaningfulparts.md) > [hostname](./kibana-plugin-core-server.urlmeaningfulparts.hostname.md) - -## URLMeaningfulParts.hostname property - -Signature: - -```typescript -hostname?: string | null; -``` diff --git a/docs/development/core/server/kibana-plugin-core-server.urlmeaningfulparts.md b/docs/development/core/server/kibana-plugin-core-server.urlmeaningfulparts.md deleted file mode 100644 index 257f7b4b634ab..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.urlmeaningfulparts.md +++ /dev/null @@ -1,27 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [URLMeaningfulParts](./kibana-plugin-core-server.urlmeaningfulparts.md) - -## URLMeaningfulParts interface - -We define our own typings because the current version of @types/node declares properties to be optional "hostname?: string". Although, parse call returns "hostname: null \| string". - -Signature: - -```typescript -export interface URLMeaningfulParts -``` - -## Properties - -| Property | Type | Description | -| --- | --- | --- | -| [auth](./kibana-plugin-core-server.urlmeaningfulparts.auth.md) | string | null | | -| [hash](./kibana-plugin-core-server.urlmeaningfulparts.hash.md) | string | null | | -| [hostname](./kibana-plugin-core-server.urlmeaningfulparts.hostname.md) | string | null | | -| [pathname](./kibana-plugin-core-server.urlmeaningfulparts.pathname.md) | string | null | | -| [port](./kibana-plugin-core-server.urlmeaningfulparts.port.md) | string | null | | -| [protocol](./kibana-plugin-core-server.urlmeaningfulparts.protocol.md) | string | null | | -| [query](./kibana-plugin-core-server.urlmeaningfulparts.query.md) | ParsedQuery | | -| [slashes](./kibana-plugin-core-server.urlmeaningfulparts.slashes.md) | boolean | null | | - diff --git a/docs/development/core/server/kibana-plugin-core-server.urlmeaningfulparts.pathname.md b/docs/development/core/server/kibana-plugin-core-server.urlmeaningfulparts.pathname.md deleted file mode 100644 index 8fee8c8e146ca..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.urlmeaningfulparts.pathname.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [URLMeaningfulParts](./kibana-plugin-core-server.urlmeaningfulparts.md) > [pathname](./kibana-plugin-core-server.urlmeaningfulparts.pathname.md) - -## URLMeaningfulParts.pathname property - -Signature: - -```typescript -pathname?: string | null; -``` diff --git a/docs/development/core/server/kibana-plugin-core-server.urlmeaningfulparts.port.md b/docs/development/core/server/kibana-plugin-core-server.urlmeaningfulparts.port.md deleted file mode 100644 index dcf3517d92ba2..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.urlmeaningfulparts.port.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [URLMeaningfulParts](./kibana-plugin-core-server.urlmeaningfulparts.md) > [port](./kibana-plugin-core-server.urlmeaningfulparts.port.md) - -## URLMeaningfulParts.port property - -Signature: - -```typescript -port?: string | null; -``` diff --git a/docs/development/core/server/kibana-plugin-core-server.urlmeaningfulparts.protocol.md b/docs/development/core/server/kibana-plugin-core-server.urlmeaningfulparts.protocol.md deleted file mode 100644 index 914dcd4e8a8a5..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.urlmeaningfulparts.protocol.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [URLMeaningfulParts](./kibana-plugin-core-server.urlmeaningfulparts.md) > [protocol](./kibana-plugin-core-server.urlmeaningfulparts.protocol.md) - -## URLMeaningfulParts.protocol property - -Signature: - -```typescript -protocol?: string | null; -``` diff --git a/docs/development/core/server/kibana-plugin-core-server.urlmeaningfulparts.query.md b/docs/development/core/server/kibana-plugin-core-server.urlmeaningfulparts.query.md deleted file mode 100644 index 358adcfd3d180..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.urlmeaningfulparts.query.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [URLMeaningfulParts](./kibana-plugin-core-server.urlmeaningfulparts.md) > [query](./kibana-plugin-core-server.urlmeaningfulparts.query.md) - -## URLMeaningfulParts.query property - -Signature: - -```typescript -query: ParsedQuery; -``` diff --git a/docs/development/core/server/kibana-plugin-core-server.urlmeaningfulparts.slashes.md b/docs/development/core/server/kibana-plugin-core-server.urlmeaningfulparts.slashes.md deleted file mode 100644 index d5b598167f2f2..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.urlmeaningfulparts.slashes.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [URLMeaningfulParts](./kibana-plugin-core-server.urlmeaningfulparts.md) > [slashes](./kibana-plugin-core-server.urlmeaningfulparts.slashes.md) - -## URLMeaningfulParts.slashes property - -Signature: - -```typescript -slashes?: boolean | null; -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.querystringinput.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.querystringinput.md index cf171d9ee9f37..e85747b8cc3d7 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.querystringinput.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.querystringinput.md @@ -7,5 +7,5 @@ Signature: ```typescript -QueryStringInput: React.FC> +QueryStringInput: React.FC> ``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.serialize.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.serialize.md index 73ba8eb66040b..496e1ae9677d8 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.serialize.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.serialize.md @@ -15,13 +15,13 @@ Using `createSearchSource`, the instance can be re-created. ```typescript serialize(): { searchSourceJSON: string; - references: import("../../../../../core/public").SavedObjectReference[]; + references: import("../../../../../core/types").SavedObjectReference[]; }; ``` Returns: `{ searchSourceJSON: string; - references: import("../../../../../core/public").SavedObjectReference[]; + references: import("../../../../../core/types").SavedObjectReference[]; }` diff --git a/examples/alerting_example/tsconfig.json b/examples/alerting_example/tsconfig.json index 09c130aca4642..214e4b78a9a70 100644 --- a/examples/alerting_example/tsconfig.json +++ b/examples/alerting_example/tsconfig.json @@ -11,5 +11,8 @@ "common/**/*.ts", "../../typings/**/*", ], - "exclude": [] + "exclude": [], + "references": [ + { "path": "../../src/core/tsconfig.json" } + ] } diff --git a/examples/bfetch_explorer/tsconfig.json b/examples/bfetch_explorer/tsconfig.json index 798a9c222c5ab..86b35c5e4943f 100644 --- a/examples/bfetch_explorer/tsconfig.json +++ b/examples/bfetch_explorer/tsconfig.json @@ -11,5 +11,8 @@ "server/**/*.ts", "../../typings/**/*", ], - "exclude": [] + "exclude": [], + "references": [ + { "path": "../../src/core/tsconfig.json" } + ] } diff --git a/examples/dashboard_embeddable_examples/tsconfig.json b/examples/dashboard_embeddable_examples/tsconfig.json index 798a9c222c5ab..86b35c5e4943f 100644 --- a/examples/dashboard_embeddable_examples/tsconfig.json +++ b/examples/dashboard_embeddable_examples/tsconfig.json @@ -11,5 +11,8 @@ "server/**/*.ts", "../../typings/**/*", ], - "exclude": [] + "exclude": [], + "references": [ + { "path": "../../src/core/tsconfig.json" } + ] } diff --git a/examples/developer_examples/tsconfig.json b/examples/developer_examples/tsconfig.json index 798a9c222c5ab..86b35c5e4943f 100644 --- a/examples/developer_examples/tsconfig.json +++ b/examples/developer_examples/tsconfig.json @@ -11,5 +11,8 @@ "server/**/*.ts", "../../typings/**/*", ], - "exclude": [] + "exclude": [], + "references": [ + { "path": "../../src/core/tsconfig.json" } + ] } diff --git a/examples/embeddable_examples/tsconfig.json b/examples/embeddable_examples/tsconfig.json index caeed2c1a434f..78098339c16f5 100644 --- a/examples/embeddable_examples/tsconfig.json +++ b/examples/embeddable_examples/tsconfig.json @@ -12,5 +12,8 @@ "server/**/*.ts", "../../typings/**/*" ], - "exclude": [] + "exclude": [], + "references": [ + { "path": "../../src/core/tsconfig.json" } + ] } diff --git a/examples/embeddable_explorer/tsconfig.json b/examples/embeddable_explorer/tsconfig.json index 798a9c222c5ab..86b35c5e4943f 100644 --- a/examples/embeddable_explorer/tsconfig.json +++ b/examples/embeddable_explorer/tsconfig.json @@ -11,5 +11,8 @@ "server/**/*.ts", "../../typings/**/*", ], - "exclude": [] + "exclude": [], + "references": [ + { "path": "../../src/core/tsconfig.json" } + ] } diff --git a/examples/routing_example/tsconfig.json b/examples/routing_example/tsconfig.json index 761a5c4da65ba..54ac800019f82 100644 --- a/examples/routing_example/tsconfig.json +++ b/examples/routing_example/tsconfig.json @@ -12,5 +12,8 @@ "common/**/*.ts", "../../typings/**/*", ], - "exclude": [] + "exclude": [], + "references": [ + { "path": "../../src/core/tsconfig.json" } + ] } diff --git a/examples/search_examples/tsconfig.json b/examples/search_examples/tsconfig.json index 8bec69ca40ccc..2b7d86d76a8a5 100644 --- a/examples/search_examples/tsconfig.json +++ b/examples/search_examples/tsconfig.json @@ -12,5 +12,8 @@ "server/**/*.ts", "../../typings/**/*", ], - "exclude": [] + "exclude": [], + "references": [ + { "path": "../../src/core/tsconfig.json" } + ] } diff --git a/examples/state_containers_examples/tsconfig.json b/examples/state_containers_examples/tsconfig.json index 007322e2d9525..6cfb9f9dc2321 100644 --- a/examples/state_containers_examples/tsconfig.json +++ b/examples/state_containers_examples/tsconfig.json @@ -12,5 +12,8 @@ "common/**/*.ts", "../../typings/**/*" ], - "exclude": [] + "exclude": [], + "references": [ + { "path": "../../src/core/tsconfig.json" } + ] } diff --git a/examples/ui_action_examples/tsconfig.json b/examples/ui_action_examples/tsconfig.json index 798a9c222c5ab..86b35c5e4943f 100644 --- a/examples/ui_action_examples/tsconfig.json +++ b/examples/ui_action_examples/tsconfig.json @@ -11,5 +11,8 @@ "server/**/*.ts", "../../typings/**/*", ], - "exclude": [] + "exclude": [], + "references": [ + { "path": "../../src/core/tsconfig.json" } + ] } diff --git a/examples/ui_actions_explorer/tsconfig.json b/examples/ui_actions_explorer/tsconfig.json index 119209114a7bb..782b9cd57fa7b 100644 --- a/examples/ui_actions_explorer/tsconfig.json +++ b/examples/ui_actions_explorer/tsconfig.json @@ -10,5 +10,8 @@ "public/**/*.tsx", "../../typings/**/*", ], - "exclude": [] + "exclude": [], + "references": [ + { "path": "../../src/core/tsconfig.json" } + ] } diff --git a/examples/url_generators_examples/tsconfig.json b/examples/url_generators_examples/tsconfig.json index 327b4642a8e7f..8aef3328b4222 100644 --- a/examples/url_generators_examples/tsconfig.json +++ b/examples/url_generators_examples/tsconfig.json @@ -11,5 +11,8 @@ "server/**/*.ts", "../../typings/**/*" ], - "exclude": [] + "exclude": [], + "references": [ + { "path": "../../src/core/tsconfig.json" } + ] } diff --git a/examples/url_generators_explorer/tsconfig.json b/examples/url_generators_explorer/tsconfig.json index 327b4642a8e7f..8aef3328b4222 100644 --- a/examples/url_generators_explorer/tsconfig.json +++ b/examples/url_generators_explorer/tsconfig.json @@ -11,5 +11,8 @@ "server/**/*.ts", "../../typings/**/*" ], - "exclude": [] + "exclude": [], + "references": [ + { "path": "../../src/core/tsconfig.json" } + ] } diff --git a/package.json b/package.json index 664585405a039..8994f327d3e65 100644 --- a/package.json +++ b/package.json @@ -141,6 +141,7 @@ "@kbn/i18n": "1.0.0", "@kbn/interpreter": "1.0.0", "@kbn/pm": "1.0.0", + "@kbn/std": "1.0.0", "@kbn/telemetry-tools": "1.0.0", "@kbn/test-subj-selector": "0.2.1", "@kbn/ui-framework": "1.0.0", diff --git a/packages/kbn-std/README.md b/packages/kbn-std/README.md new file mode 100644 index 0000000000000..dfd98287ada4b --- /dev/null +++ b/packages/kbn-std/README.md @@ -0,0 +1,3 @@ +# `@kbn/std` — Kibana standard library + +This package is a set of utilities that can be used both on server-side and client-side. \ No newline at end of file diff --git a/packages/kbn-std/package.json b/packages/kbn-std/package.json new file mode 100644 index 0000000000000..4c67706b45d27 --- /dev/null +++ b/packages/kbn-std/package.json @@ -0,0 +1,21 @@ +{ + "name": "@kbn/std", + "main": "./target/index.js", + "types": "./target/index.d.ts", + "version": "1.0.0", + "license": "Apache-2.0", + "private": true, + "scripts": { + "build": "tsc", + "kbn:bootstrap": "yarn build" + }, + "devDependencies": { + "typescript": "4.0.2", + "tsd": "^0.13.1" + }, + "dependencies": { + "@kbn/utility-types": "1.0.0", + "lodash": "^4.17.15", + "query-string": "5.1.1" + } +} diff --git a/src/core/utils/__snapshots__/get.test.ts.snap b/packages/kbn-std/src/__snapshots__/get.test.ts.snap similarity index 100% rename from src/core/utils/__snapshots__/get.test.ts.snap rename to packages/kbn-std/src/__snapshots__/get.test.ts.snap diff --git a/src/core/utils/assert_never.ts b/packages/kbn-std/src/assert_never.ts similarity index 100% rename from src/core/utils/assert_never.ts rename to packages/kbn-std/src/assert_never.ts diff --git a/src/core/utils/deep_freeze.test.ts b/packages/kbn-std/src/deep_freeze.test.ts similarity index 100% rename from src/core/utils/deep_freeze.test.ts rename to packages/kbn-std/src/deep_freeze.test.ts diff --git a/src/core/utils/deep_freeze.ts b/packages/kbn-std/src/deep_freeze.ts similarity index 100% rename from src/core/utils/deep_freeze.ts rename to packages/kbn-std/src/deep_freeze.ts diff --git a/src/core/utils/get.test.ts b/packages/kbn-std/src/get.test.ts similarity index 100% rename from src/core/utils/get.test.ts rename to packages/kbn-std/src/get.test.ts diff --git a/src/core/utils/get.ts b/packages/kbn-std/src/get.ts similarity index 100% rename from src/core/utils/get.ts rename to packages/kbn-std/src/get.ts diff --git a/src/core/utils/get_flattened_object.test.ts b/packages/kbn-std/src/get_flattened_object.test.ts similarity index 100% rename from src/core/utils/get_flattened_object.test.ts rename to packages/kbn-std/src/get_flattened_object.test.ts diff --git a/src/core/utils/get_flattened_object.ts b/packages/kbn-std/src/get_flattened_object.ts similarity index 100% rename from src/core/utils/get_flattened_object.ts rename to packages/kbn-std/src/get_flattened_object.ts diff --git a/packages/kbn-std/src/index.ts b/packages/kbn-std/src/index.ts new file mode 100644 index 0000000000000..7cf70a0e28e2c --- /dev/null +++ b/packages/kbn-std/src/index.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. + */ + +export { assertNever } from './assert_never'; +export { deepFreeze, Freezable } from './deep_freeze'; +export { get } from './get'; +export { mapToObject } from './map_to_object'; +export { merge } from './merge'; +export { pick } from './pick'; +export { withTimeout } from './promise'; +export { isRelativeUrl, modifyUrl, URLMeaningfulParts } from './url'; +export { unset } from './unset'; +export { getFlattenedObject } from './get_flattened_object'; diff --git a/src/core/utils/map_to_object.ts b/packages/kbn-std/src/map_to_object.ts similarity index 100% rename from src/core/utils/map_to_object.ts rename to packages/kbn-std/src/map_to_object.ts diff --git a/src/core/utils/map_utils.test.ts b/packages/kbn-std/src/map_utils.test.ts similarity index 100% rename from src/core/utils/map_utils.test.ts rename to packages/kbn-std/src/map_utils.test.ts diff --git a/src/core/utils/map_utils.ts b/packages/kbn-std/src/map_utils.ts similarity index 100% rename from src/core/utils/map_utils.ts rename to packages/kbn-std/src/map_utils.ts diff --git a/src/core/utils/merge.test.ts b/packages/kbn-std/src/merge.test.ts similarity index 100% rename from src/core/utils/merge.test.ts rename to packages/kbn-std/src/merge.test.ts diff --git a/src/core/utils/merge.ts b/packages/kbn-std/src/merge.ts similarity index 98% rename from src/core/utils/merge.ts rename to packages/kbn-std/src/merge.ts index 43878c27b1e19..c0de50544a34e 100644 --- a/src/core/utils/merge.ts +++ b/packages/kbn-std/src/merge.ts @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -import { isPlainObject } from 'lodash'; +import isPlainObject from 'lodash/isPlainObject'; /** * Deeply merges two objects, omitting undefined values, and not deeply merging Arrays. * diff --git a/src/core/utils/pick.ts b/packages/kbn-std/src/pick.ts similarity index 100% rename from src/core/utils/pick.ts rename to packages/kbn-std/src/pick.ts diff --git a/src/core/utils/promise.test.ts b/packages/kbn-std/src/promise.test.ts similarity index 100% rename from src/core/utils/promise.test.ts rename to packages/kbn-std/src/promise.test.ts diff --git a/src/core/utils/promise.ts b/packages/kbn-std/src/promise.ts similarity index 100% rename from src/core/utils/promise.ts rename to packages/kbn-std/src/promise.ts diff --git a/src/core/utils/unset.test.ts b/packages/kbn-std/src/unset.test.ts similarity index 100% rename from src/core/utils/unset.test.ts rename to packages/kbn-std/src/unset.test.ts diff --git a/src/core/utils/unset.ts b/packages/kbn-std/src/unset.ts similarity index 100% rename from src/core/utils/unset.ts rename to packages/kbn-std/src/unset.ts diff --git a/src/core/utils/url.test.ts b/packages/kbn-std/src/url.test.ts similarity index 100% rename from src/core/utils/url.test.ts rename to packages/kbn-std/src/url.test.ts diff --git a/src/core/utils/url.ts b/packages/kbn-std/src/url.ts similarity index 100% rename from src/core/utils/url.ts rename to packages/kbn-std/src/url.ts diff --git a/packages/kbn-std/tsconfig.json b/packages/kbn-std/tsconfig.json new file mode 100644 index 0000000000000..5c86ad17a90e9 --- /dev/null +++ b/packages/kbn-std/tsconfig.json @@ -0,0 +1,16 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "declaration": true, + "declarationDir": "./target", + "outDir": "./target", + "stripInternal": true, + "declarationMap": true, + "types": ["jest", "node"] + }, + "include": [ + "./src/**/*.ts", + "../../typings/query_string.d.ts" + ], + "exclude": ["target"] +} diff --git a/packages/kbn-std/yarn.lock b/packages/kbn-std/yarn.lock new file mode 120000 index 0000000000000..3f82ebc9cdbae --- /dev/null +++ b/packages/kbn-std/yarn.lock @@ -0,0 +1 @@ +../../yarn.lock \ No newline at end of file diff --git a/packages/kbn-test/src/functional_test_runner/lib/config/schema.ts b/packages/kbn-test/src/functional_test_runner/lib/config/schema.ts index e1d3bf1a8d901..701171876ad2c 100644 --- a/packages/kbn-test/src/functional_test_runner/lib/config/schema.ts +++ b/packages/kbn-test/src/functional_test_runner/lib/config/schema.ts @@ -262,7 +262,7 @@ export const schema = Joi.object() // settings for the find service layout: Joi.object() .keys({ - fixedHeaderHeight: Joi.number().default(50), + fixedHeaderHeight: Joi.number().default(100), }) .default(), diff --git a/packages/kbn-utility-types/index.ts b/packages/kbn-utility-types/index.ts index 6ccfeb8ab052c..d3fb5bdf36194 100644 --- a/packages/kbn-utility-types/index.ts +++ b/packages/kbn-utility-types/index.ts @@ -98,3 +98,15 @@ export type PublicKeys = keyof T; * Returns an object with public keys only. */ export type PublicContract = Pick>; + +/** + * Returns public method names + */ +export type MethodKeysOf = { + [K in keyof T]: T[K] extends (...args: any[]) => any ? K : never; +}[keyof T]; + +/** + * Returns an object with public methods only. + */ +export type PublicMethodsOf = Pick>; diff --git a/packages/kbn-utility-types/tsconfig.json b/packages/kbn-utility-types/tsconfig.json index 03cace5b9cb2c..79cf423fe78ac 100644 --- a/packages/kbn-utility-types/tsconfig.json +++ b/packages/kbn-utility-types/tsconfig.json @@ -7,7 +7,6 @@ "stripInternal": true, "declarationMap": true, "types": [ - "jest", "node" ] }, diff --git a/src/core/public/_variables.scss b/src/core/public/_variables.scss new file mode 100644 index 0000000000000..8c054e770bd4b --- /dev/null +++ b/src/core/public/_variables.scss @@ -0,0 +1,3 @@ +@import '@elastic/eui/src/global_styling/variables/header'; + +$kbnHeaderOffset: $euiHeaderHeightCompensation * 2; diff --git a/src/core/public/application/capabilities/capabilities_service.mock.ts b/src/core/public/application/capabilities/capabilities_service.mock.ts index 54aaa31e08859..ee37854762666 100644 --- a/src/core/public/application/capabilities/capabilities_service.mock.ts +++ b/src/core/public/application/capabilities/capabilities_service.mock.ts @@ -16,8 +16,10 @@ * specific language governing permissions and limitations * under the License. */ + +import { deepFreeze } from '@kbn/std'; +import type { PublicMethodsOf } from '@kbn/utility-types'; import { CapabilitiesService, CapabilitiesStart } from './capabilities_service'; -import { deepFreeze } from '../../../utils'; const createStartContractMock = (): jest.Mocked => ({ capabilities: deepFreeze({ diff --git a/src/core/public/application/capabilities/capabilities_service.tsx b/src/core/public/application/capabilities/capabilities_service.tsx index 7304a8e5a66bc..1164164aec4c5 100644 --- a/src/core/public/application/capabilities/capabilities_service.tsx +++ b/src/core/public/application/capabilities/capabilities_service.tsx @@ -17,9 +17,9 @@ * under the License. */ import { RecursiveReadonly } from '@kbn/utility-types'; +import { deepFreeze } from '@kbn/std'; import { Capabilities } from '../../../types/capabilities'; -import { deepFreeze } from '../../../utils'; import { HttpStart } from '../../http'; interface StartDeps { diff --git a/src/core/public/application/test_types.ts b/src/core/public/application/test_types.ts index 64012f0c0b6c1..6d29b77d92f36 100644 --- a/src/core/public/application/test_types.ts +++ b/src/core/public/application/test_types.ts @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ - +import type { PublicMethodsOf } from '@kbn/utility-types'; import { AppUnmount, Mounter } from './types'; import { ApplicationService } from './application_service'; diff --git a/src/core/public/application/ui/app_container.tsx b/src/core/public/application/ui/app_container.tsx index f668cf851da55..089d1cf3f3ced 100644 --- a/src/core/public/application/ui/app_container.tsx +++ b/src/core/public/application/ui/app_container.tsx @@ -94,8 +94,10 @@ export const AppContainer: FunctionComponent = ({ // eslint-disable-next-line no-console console.error(e); } finally { - setShowSpinner(false); - setIsMounting(false); + if (elementRef.current) { + setShowSpinner(false); + setIsMounting(false); + } } }; diff --git a/src/core/public/chrome/chrome_service.mock.ts b/src/core/public/chrome/chrome_service.mock.ts index 5862ee7175f71..9cd96763d2e79 100644 --- a/src/core/public/chrome/chrome_service.mock.ts +++ b/src/core/public/chrome/chrome_service.mock.ts @@ -17,14 +17,8 @@ * under the License. */ import { BehaviorSubject } from 'rxjs'; -import { - ChromeBadge, - ChromeBrand, - ChromeBreadcrumb, - ChromeService, - InternalChromeStart, - NavType, -} from './'; +import type { PublicMethodsOf } from '@kbn/utility-types'; +import { ChromeBadge, ChromeBrand, ChromeBreadcrumb, ChromeService, InternalChromeStart } from './'; const createStartContractMock = () => { const startContract: DeeplyMockedKeys = { @@ -50,8 +44,10 @@ const createStartContractMock = () => { }, navControls: { registerLeft: jest.fn(), + registerCenter: jest.fn(), registerRight: jest.fn(), getLeft$: jest.fn(), + getCenter$: jest.fn(), getRight$: jest.fn(), }, setAppTitle: jest.fn(), @@ -70,7 +66,6 @@ const createStartContractMock = () => { setHelpExtension: jest.fn(), setHelpSupportUrl: jest.fn(), getIsNavDrawerLocked$: jest.fn(), - getNavType$: jest.fn(), getCustomNavLink$: jest.fn(), setCustomNavLink: jest.fn(), }; @@ -83,7 +78,6 @@ const createStartContractMock = () => { startContract.getCustomNavLink$.mockReturnValue(new BehaviorSubject(undefined)); startContract.getHelpExtension$.mockReturnValue(new BehaviorSubject(undefined)); startContract.getIsNavDrawerLocked$.mockReturnValue(new BehaviorSubject(false)); - startContract.getNavType$.mockReturnValue(new BehaviorSubject('modern' as NavType)); return startContract; }; diff --git a/src/core/public/chrome/chrome_service.tsx b/src/core/public/chrome/chrome_service.tsx index b96c34cd9fbe8..b01f120b81305 100644 --- a/src/core/public/chrome/chrome_service.tsx +++ b/src/core/public/chrome/chrome_service.tsx @@ -37,7 +37,6 @@ import { ChromeNavControls, NavControlsService } from './nav_controls'; import { ChromeNavLinks, NavLinksService, ChromeNavLink } from './nav_links'; import { ChromeRecentlyAccessed, RecentlyAccessedService } from './recently_accessed'; import { Header } from './ui'; -import { NavType } from './ui/header'; import { ChromeHelpExtensionMenuLink } from './ui/header/header_help_menu'; export { ChromeNavControls, ChromeRecentlyAccessed, ChromeDocTitle }; @@ -172,10 +171,6 @@ export class ChromeService { const getIsNavDrawerLocked$ = isNavDrawerLocked$.pipe(takeUntil(this.stop$)); - // TODO #64541 - // Can delete - const getNavType$ = uiSettings.get$('pageNavigation').pipe(takeUntil(this.stop$)); - const isIE = () => { const ua = window.navigator.userAgent; const msie = ua.indexOf('MSIE '); // IE 10 or older @@ -241,10 +236,10 @@ export class ChromeService { navLinks$={navLinks.getNavLinks$()} recentlyAccessed$={recentlyAccessed.get$()} navControlsLeft$={navControls.getLeft$()} + navControlsCenter$={navControls.getCenter$()} navControlsRight$={navControls.getRight$()} onIsLockedUpdate={setIsNavDrawerLocked} isLocked$={getIsNavDrawerLocked$} - navType$={getNavType$} /> ), @@ -305,8 +300,6 @@ export class ChromeService { getIsNavDrawerLocked$: () => getIsNavDrawerLocked$, - getNavType$: () => getNavType$, - getCustomNavLink$: () => customNavLink$.pipe(takeUntil(this.stop$)), setCustomNavLink: (customNavLink?: ChromeNavLink) => { @@ -468,13 +461,6 @@ export interface ChromeStart { * Get an observable of the current locked state of the nav drawer. */ getIsNavDrawerLocked$(): Observable; - - /** - * Get the navigation type - * TODO #64541 - * Can delete - */ - getNavType$(): Observable; } /** @internal */ diff --git a/src/core/public/chrome/nav_controls/nav_controls_service.ts b/src/core/public/chrome/nav_controls/nav_controls_service.ts index 167948e01cb36..2638f40c77dc4 100644 --- a/src/core/public/chrome/nav_controls/nav_controls_service.ts +++ b/src/core/public/chrome/nav_controls/nav_controls_service.ts @@ -45,14 +45,18 @@ export interface ChromeNavControl { * @public */ export interface ChromeNavControls { - /** Register a nav control to be presented on the left side of the chrome header. */ + /** Register a nav control to be presented on the bottom-left side of the chrome header. */ registerLeft(navControl: ChromeNavControl): void; - /** Register a nav control to be presented on the right side of the chrome header. */ + /** Register a nav control to be presented on the top-right side of the chrome header. */ registerRight(navControl: ChromeNavControl): void; + /** Register a nav control to be presented on the top-center side of the chrome header. */ + registerCenter(navControl: ChromeNavControl): void; /** @internal */ getLeft$(): Observable; /** @internal */ getRight$(): Observable; + /** @internal */ + getCenter$(): Observable; } /** @internal */ @@ -62,6 +66,7 @@ export class NavControlsService { public start() { const navControlsLeft$ = new BehaviorSubject>(new Set()); const navControlsRight$ = new BehaviorSubject>(new Set()); + const navControlsCenter$ = new BehaviorSubject>(new Set()); return { // In the future, registration should be moved to the setup phase. This @@ -72,6 +77,9 @@ export class NavControlsService { registerRight: (navControl: ChromeNavControl) => navControlsRight$.next(new Set([...navControlsRight$.value.values(), navControl])), + registerCenter: (navControl: ChromeNavControl) => + navControlsCenter$.next(new Set([...navControlsCenter$.value.values(), navControl])), + getLeft$: () => navControlsLeft$.pipe( map((controls) => sortBy([...controls.values()], 'order')), @@ -82,6 +90,11 @@ export class NavControlsService { map((controls) => sortBy([...controls.values()], 'order')), takeUntil(this.stop$) ), + getCenter$: () => + navControlsCenter$.pipe( + map((controls) => sortBy([...controls.values()], 'order')), + takeUntil(this.stop$) + ), }; } diff --git a/src/core/public/chrome/nav_links/nav_link.ts b/src/core/public/chrome/nav_links/nav_link.ts index 4b82e0ced4505..80f0265819c40 100644 --- a/src/core/public/chrome/nav_links/nav_link.ts +++ b/src/core/public/chrome/nav_links/nav_link.ts @@ -17,7 +17,7 @@ * under the License. */ -import { pick } from '../../../utils'; +import { pick } from '@kbn/std'; import { AppCategory } from '../../'; /** 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 a770ece8496e4..86cacfe98f767 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 @@ -121,7 +121,7 @@ exports[`CollapsibleNav renders links grouped by category 1`] = ` homeHref="/" id="collapsibe-nav" isLocked={false} - isOpen={true} + isNavOpen={true} navLinks$={ BehaviorSubject { "_isScalar": false, @@ -2105,7 +2105,7 @@ exports[`CollapsibleNav renders the default nav 1`] = ` homeHref="/" id="collapsibe-nav" isLocked={false} - isOpen={false} + isNavOpen={false} navLinks$={ BehaviorSubject { "_isScalar": false, @@ -2339,6 +2339,7 @@ exports[`CollapsibleNav renders the default nav 2`] = ` homeHref="/" id="collapsibe-nav" isLocked={false} + isNavOpen={false} isOpen={true} navLinks$={ BehaviorSubject { @@ -2454,461 +2455,9 @@ exports[`CollapsibleNav renders the default nav 2`] = ` data-test-subj="collapsibleNav" id="collapsibe-nav" isDocked={false} - isOpen={true} + isOpen={false} onClose={[Function]} - > - - - -
- -
-
- + /> `; @@ -3025,6 +2574,7 @@ exports[`CollapsibleNav renders the default nav 3`] = ` homeHref="/" id="collapsibe-nav" isLocked={true} + isNavOpen={false} isOpen={true} navLinks$={ BehaviorSubject { @@ -3140,7 +2690,7 @@ exports[`CollapsibleNav renders the default nav 3`] = ` data-test-subj="collapsibleNav" id="collapsibe-nav" isDocked={true} - isOpen={true} + isOpen={false} onClose={[Function]} > - -
-
-
- - -`; - -exports[`Header renders 2`] = ` -
+ -
- -
- -
- -
- - - -
-
- -
+ - - - -
- - - - -
- - , + ], + }, + Object { + "borders": "none", + "items": Array [ + -
- - - - - - - - - , ], - "thrownError": null, - } - } - /> - -
- -
+ }, + Object { + "borders": "none", + "items": Array [ , + , + ], + }, + ] + } + theme="dark" + > +
+ +
+ + + + +
+ +
+ +
+
+
+
+ +
+ +
+ - - - - } - closePopover={[Function]} - data-test-subj="helpMenuButton" - display="inlineBlock" - hasArrow={true} - id="headerHelpMenu" - isOpen={false} - ownFocus={true} - panelPaddingSize="m" - repositionOnScroll={true} - > - -
-
- - - -
-
-
-
- -
-
-
- -
-
-
- - - - -
-
-`; - -exports[`Header renders 3`] = ` -
- -
-
-
- -
- -
- -
- -
- - - -
-
- -
- - - - -
- - - - -
- - -
- - - - - - - - - - -
- -
- - - - - - } - closePopover={[Function]} - data-test-subj="helpMenuButton" - display="inlineBlock" - hasArrow={true} - id="headerHelpMenu" - isOpen={false} - ownFocus={true} - panelPaddingSize="m" - repositionOnScroll={true} - > - -
-
- - - -
-
-
-
-
-
-
-
- -
-
-
- - - - - -
- -
-
-
-
-
-
-`; - -exports[`Header renders 4`] = ` -
- -
-
-
- -
- -
- -
- - -
- - - -
-
-
- -
- - - - -
- - - - -
- - -
- - - - - - - - - - -
- -
- - + + + + } + closePopover={[Function]} + data-test-subj="helpMenuButton" + display="inlineBlock" + hasArrow={true} + id="headerHelpMenu" + isOpen={false} + ownFocus={true} + panelPaddingSize="m" + repositionOnScroll={true} + > + +
+
+ + + +
+
+
+
+
+
+
+
+ +
+ +
+
+
+
+
+ + +
+ +
+ +
+ + + +
+
+ +
+ + +
+ + + + + + + + + + +
+ +
+ - - - - } - closePopover={[Function]} - data-test-subj="helpMenuButton" - display="inlineBlock" - hasArrow={true} - id="headerHelpMenu" - isOpen={false} - ownFocus={true} - panelPaddingSize="m" - repositionOnScroll={true} - > - -
-
- - - -
-
-
-
- - -
-
- -
-
-
- - + +
+ +
+ +
+ +
+ - - + - - - - + close + + + + + + + + + + + + `; diff --git a/src/core/public/chrome/ui/header/_index.scss b/src/core/public/chrome/ui/header/_index.scss index e3b73abbcabc2..44cd864278325 100644 --- a/src/core/public/chrome/ui/header/_index.scss +++ b/src/core/public/chrome/ui/header/_index.scss @@ -1,14 +1,5 @@ @include euiHeaderAffordForFixed; -// TODO #64541 -// Delete this block -.chrHeaderWrapper:not(.headerWrapper) { - width: 100%; - position: fixed; - top: 0; - z-index: 10; -} - .chrHeaderHelpMenu__version { text-transform: none; } diff --git a/src/core/public/chrome/ui/header/collapsible_nav.test.tsx b/src/core/public/chrome/ui/header/collapsible_nav.test.tsx index d4325e0caf88c..e33e76a45580e 100644 --- a/src/core/public/chrome/ui/header/collapsible_nav.test.tsx +++ b/src/core/public/chrome/ui/header/collapsible_nav.test.tsx @@ -59,7 +59,7 @@ function mockProps() { basePath: httpServiceMock.createSetupContract({ basePath: '/test' }).basePath, id: 'collapsibe-nav', isLocked: false, - isOpen: false, + isNavOpen: false, homeHref: '/', navLinks$: new BehaviorSubject([]), recentlyAccessed$: new BehaviorSubject([]), @@ -123,7 +123,7 @@ describe('CollapsibleNav', () => { const component = mount( { const component = mount( @@ -147,9 +147,9 @@ describe('CollapsibleNav', () => { clickGroup(component, 'kibana'); clickGroup(component, 'recentlyViewed'); expectShownNavLinksCount(component, 1); - component.setProps({ isOpen: false }); + component.setProps({ isNavOpen: false }); expectNavIsClosed(component); - component.setProps({ isOpen: true }); + component.setProps({ isNavOpen: true }); expectShownNavLinksCount(component, 1); }); @@ -160,14 +160,14 @@ describe('CollapsibleNav', () => { const component = mount( ); component.setProps({ closeNav: () => { - component.setProps({ isOpen: false }); + component.setProps({ isNavOpen: false }); onClose(); }, }); @@ -175,11 +175,11 @@ describe('CollapsibleNav', () => { component.find('[data-test-subj="collapsibleNavGroup-recentlyViewed"] a').simulate('click'); expect(onClose.callCount).toEqual(1); expectNavIsClosed(component); - component.setProps({ isOpen: true }); + component.setProps({ isNavOpen: true }); component.find('[data-test-subj="collapsibleNavGroup-kibana"] a').simulate('click'); expect(onClose.callCount).toEqual(2); expectNavIsClosed(component); - component.setProps({ isOpen: true }); + component.setProps({ isNavOpen: true }); component.find('[data-test-subj="collapsibleNavGroup-noCategory"] a').simulate('click'); expect(onClose.callCount).toEqual(3); expectNavIsClosed(component); diff --git a/src/core/public/chrome/ui/header/collapsible_nav.tsx b/src/core/public/chrome/ui/header/collapsible_nav.tsx index a5f42c0949562..01cdb9c38881a 100644 --- a/src/core/public/chrome/ui/header/collapsible_nav.tsx +++ b/src/core/public/chrome/ui/header/collapsible_nav.tsx @@ -79,7 +79,7 @@ interface Props { basePath: HttpStart['basePath']; id: string; isLocked: boolean; - isOpen: boolean; + isNavOpen: boolean; homeHref: string; navLinks$: Rx.Observable; recentlyAccessed$: Rx.Observable; @@ -94,7 +94,7 @@ export function CollapsibleNav({ basePath, id, isLocked, - isOpen, + isNavOpen, homeHref, storage = window.localStorage, onIsLockedUpdate, @@ -129,7 +129,7 @@ export function CollapsibleNav({ aria-label={i18n.translate('core.ui.primaryNav.screenReaderLabel', { defaultMessage: 'Primary', })} - isOpen={isOpen} + isOpen={isNavOpen} isDocked={isLocked} onClose={closeNav} > diff --git a/src/core/public/chrome/ui/header/header.test.tsx b/src/core/public/chrome/ui/header/header.test.tsx index 04eb256f30f37..7309b9af49388 100644 --- a/src/core/public/chrome/ui/header/header.test.tsx +++ b/src/core/public/chrome/ui/header/header.test.tsx @@ -21,7 +21,6 @@ import React from 'react'; import { act } from 'react-dom/test-utils'; import { BehaviorSubject } from 'rxjs'; import { mountWithIntl } from 'test_utils/enzyme_helpers'; -import { NavType } from '.'; import { httpServiceMock } from '../../../http/http_service.mock'; import { applicationServiceMock } from '../../../mocks'; import { Header } from './header'; @@ -51,10 +50,10 @@ function mockProps() { helpExtension$: new BehaviorSubject(undefined), helpSupportUrl$: new BehaviorSubject(''), navControlsLeft$: new BehaviorSubject([]), + navControlsCenter$: new BehaviorSubject([]), navControlsRight$: new BehaviorSubject([]), basePath: http.basePath, isLocked$: new BehaviorSubject(false), - navType$: new BehaviorSubject('modern' as NavType), loadingCount$: new BehaviorSubject(0), onIsLockedUpdate: () => {}, }; @@ -71,7 +70,6 @@ describe('Header', () => { const isVisible$ = new BehaviorSubject(false); const breadcrumbs$ = new BehaviorSubject([{ text: 'test' }]); const isLocked$ = new BehaviorSubject(false); - const navType$ = new BehaviorSubject('modern' as NavType); const navLinks$ = new BehaviorSubject([ { id: 'kibana', title: 'kibana', baseUrl: '', href: '' }, ]); @@ -92,22 +90,19 @@ describe('Header', () => { navLinks$={navLinks$} recentlyAccessed$={recentlyAccessed$} isLocked$={isLocked$} - navType$={navType$} customNavLink$={customNavLink$} /> ); - expect(component).toMatchSnapshot(); + expect(component.find('EuiHeader').exists()).toBeFalsy(); act(() => isVisible$.next(true)); component.update(); - expect(component).toMatchSnapshot(); + expect(component.find('EuiHeader').exists()).toBeTruthy(); + expect(component.find('nav[aria-label="Primary"]').exists()).toBeFalsy(); act(() => isLocked$.next(true)); component.update(); - expect(component).toMatchSnapshot(); - - act(() => navType$.next('legacy' as NavType)); - component.update(); + expect(component.find('nav[aria-label="Primary"]').exists()).toBeTruthy(); expect(component).toMatchSnapshot(); }); }); diff --git a/src/core/public/chrome/ui/header/header.tsx b/src/core/public/chrome/ui/header/header.tsx index c0b3fc72930dc..7ec03ea4c6da6 100644 --- a/src/core/public/chrome/ui/header/header.tsx +++ b/src/core/public/chrome/ui/header/header.tsx @@ -23,8 +23,6 @@ import { EuiHeaderSectionItem, EuiHeaderSectionItemButton, EuiIcon, - EuiNavDrawer, - EuiShowFor, htmlIdGenerator, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; @@ -43,14 +41,14 @@ import { import { InternalApplicationStart } from '../../../application/types'; import { HttpStart } from '../../../http'; import { ChromeHelpExtension } from '../../chrome_service'; -import { NavType, OnIsLockedUpdate } from './'; +import { OnIsLockedUpdate } from './'; import { CollapsibleNav } from './collapsible_nav'; import { HeaderBadge } from './header_badge'; import { HeaderBreadcrumbs } from './header_breadcrumbs'; import { HeaderHelpMenu } from './header_help_menu'; import { HeaderLogo } from './header_logo'; import { HeaderNavControls } from './header_nav_controls'; -import { NavDrawer } from './nav_drawer'; +import { HeaderActionMenu } from './header_action_menu'; export interface HeaderProps { kibanaVersion: string; @@ -68,27 +66,14 @@ export interface HeaderProps { helpExtension$: Observable; helpSupportUrl$: Observable; navControlsLeft$: Observable; + navControlsCenter$: Observable; navControlsRight$: Observable; basePath: HttpStart['basePath']; isLocked$: Observable; - navType$: Observable; loadingCount$: ReturnType; onIsLockedUpdate: OnIsLockedUpdate; } -function renderMenuTrigger(toggleOpen: () => void) { - return ( - - - - ); -} - export function Header({ kibanaVersion, kibanaDocLink, @@ -98,125 +83,116 @@ export function Header({ homeHref, ...observables }: HeaderProps) { - const isVisible = useObservable(observables.isVisible$, true); - const navType = useObservable(observables.navType$, 'modern'); + const isVisible = useObservable(observables.isVisible$, false); const isLocked = useObservable(observables.isLocked$, false); - const [isOpen, setIsOpen] = useState(false); + const [isNavOpen, setIsNavOpen] = useState(false); if (!isVisible) { return ; } - const navDrawerRef = createRef(); const toggleCollapsibleNavRef = createRef(); const navId = htmlIdGenerator()(); - const className = classnames( - 'chrHeaderWrapper', // TODO #64541 - delete this - 'hide-for-sharing', - { - 'chrHeaderWrapper--navIsLocked': isLocked, - headerWrapper: navType === 'modern', - } - ); + const className = classnames('hide-for-sharing', 'headerGlobalNav'); return ( <>
- - - {navType === 'modern' ? ( +
+ , + ], + borders: 'none', + }, + { + ...(observables.navControlsCenter$ && { + items: [], + }), + borders: 'none', + }, + { + items: [ + , + , + ], + borders: 'none', + }, + ]} + /> + + + setIsOpen(!isOpen)} - aria-expanded={isOpen} - aria-pressed={isOpen} + onClick={() => setIsNavOpen(!isNavOpen)} + aria-expanded={isNavOpen} + aria-pressed={isNavOpen} aria-controls={navId} ref={toggleCollapsibleNavRef} > - ) : ( - // TODO #64541 - // Delete this block - - - {renderMenuTrigger(() => navDrawerRef.current?.toggleOpen())} - - - )} - - - + - - + + - + - + - - - - + + + + + + +
- -
-
- {navType === 'modern' ? ( - { - setIsOpen(false); - if (toggleCollapsibleNavRef.current) { - toggleCollapsibleNavRef.current.focus(); - } - }} - customNavLink$={observables.customNavLink$} - /> - ) : ( - // TODO #64541 - // Delete this block - - )} + { + setIsNavOpen(false); + if (toggleCollapsibleNavRef.current) { + toggleCollapsibleNavRef.current.focus(); + } + }} + customNavLink$={observables.customNavLink$} + />
); diff --git a/src/core/public/chrome/ui/header/header_action_menu.test.tsx b/src/core/public/chrome/ui/header/header_action_menu.test.tsx new file mode 100644 index 0000000000000..a124c8ab66969 --- /dev/null +++ b/src/core/public/chrome/ui/header/header_action_menu.test.tsx @@ -0,0 +1,139 @@ +/* + * 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 React from 'react'; +import { mount, ReactWrapper } from 'enzyme'; +import { act } from 'react-dom/test-utils'; +import { BehaviorSubject } from 'rxjs'; +import { HeaderActionMenu } from './header_action_menu'; +import { MountPoint, UnmountCallback } from '../../../types'; + +type MockedUnmount = jest.MockedFunction; + +describe('HeaderActionMenu', () => { + let component: ReactWrapper; + let menuMount$: BehaviorSubject; + let unmounts: Record; + + beforeEach(() => { + menuMount$ = new BehaviorSubject(undefined); + unmounts = {}; + }); + + const refresh = () => { + new Promise(async (resolve) => { + if (component) { + act(() => { + component.update(); + }); + } + setImmediate(() => resolve(component)); // flushes any pending promises + }); + }; + + const createMountPoint = (id: string, content: string = id): MountPoint => ( + root + ): MockedUnmount => { + const container = document.createElement('DIV'); + // eslint-disable-next-line no-unsanitized/property + container.innerHTML = content; + root.appendChild(container); + const unmount = jest.fn(() => container.remove()); + unmounts[id] = unmount; + return unmount; + }; + + it('mounts the current value of the provided observable', async () => { + component = mount(); + + act(() => { + menuMount$.next(createMountPoint('FOO')); + }); + await refresh(); + + expect(component.html()).toMatchInlineSnapshot( + `"
FOO
"` + ); + }); + + it('clears the content of the component when emitting undefined', async () => { + component = mount(); + + act(() => { + menuMount$.next(createMountPoint('FOO')); + }); + await refresh(); + + expect(component.html()).toMatchInlineSnapshot( + `"
FOO
"` + ); + + act(() => { + menuMount$.next(undefined); + }); + await refresh(); + + expect(component.html()).toMatchInlineSnapshot( + `"
"` + ); + }); + + it('updates the dom when a new mount point is emitted', async () => { + component = mount(); + + act(() => { + menuMount$.next(createMountPoint('FOO')); + }); + await refresh(); + + expect(component.html()).toMatchInlineSnapshot( + `"
FOO
"` + ); + + act(() => { + menuMount$.next(createMountPoint('BAR')); + }); + await refresh(); + + expect(component.html()).toMatchInlineSnapshot( + `"
BAR
"` + ); + }); + + it('calls the previous mount point `unmount` when mounting a new mount point', async () => { + component = mount(); + + act(() => { + menuMount$.next(createMountPoint('FOO')); + }); + await refresh(); + + expect(Object.keys(unmounts)).toEqual(['FOO']); + expect(unmounts.FOO).not.toHaveBeenCalled(); + + act(() => { + menuMount$.next(createMountPoint('BAR')); + }); + await refresh(); + + expect(Object.keys(unmounts)).toEqual(['FOO', 'BAR']); + expect(unmounts.FOO).toHaveBeenCalledTimes(1); + expect(unmounts.BAR).not.toHaveBeenCalled(); + }); +}); diff --git a/src/core/public/chrome/ui/header/header_action_menu.tsx b/src/core/public/chrome/ui/header/header_action_menu.tsx new file mode 100644 index 0000000000000..3a7a09608ba66 --- /dev/null +++ b/src/core/public/chrome/ui/header/header_action_menu.tsx @@ -0,0 +1,64 @@ +/* + * 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 React, { FC, useRef, useLayoutEffect, useState } from 'react'; +import { Observable } from 'rxjs'; +import { MountPoint, UnmountCallback } from '../../../types'; + +interface HeaderActionMenuProps { + actionMenu$: Observable; +} + +export const HeaderActionMenu: FC = ({ actionMenu$ }) => { + // useObservable relies on useState under the hood. The signature is type SetStateAction = S | ((prevState: S) => S); + // As we got a Observable here, React's setState setter assume he's getting a `(prevState: S) => S` signature, + // therefore executing the mount method, causing everything to crash. + // piping the observable before calling `useObservable` causes the effect to always having a new reference, as + // the piped observable is a new instance on every render, causing infinite loops. + // this is why we use `useLayoutEffect` manually here. + const [mounter, setMounter] = useState<{ mount: MountPoint | undefined }>({ mount: undefined }); + useLayoutEffect(() => { + const s = actionMenu$.subscribe((value) => { + setMounter({ mount: value }); + }); + return () => s.unsubscribe(); + }, [actionMenu$]); + + const elementRef = useRef(null); + const unmountRef = useRef(null); + + useLayoutEffect(() => { + if (unmountRef.current) { + unmountRef.current(); + unmountRef.current = null; + } + + if (mounter.mount && elementRef.current) { + try { + unmountRef.current = mounter.mount(elementRef.current); + } catch (e) { + // TODO: use client-side logger when feature is implemented + // eslint-disable-next-line no-console + console.error(e); + } + } + }, [mounter]); + + return
; +}; diff --git a/src/core/public/chrome/ui/header/header_logo.tsx b/src/core/public/chrome/ui/header/header_logo.tsx index 9bec946b6b76e..dee93ecb1a804 100644 --- a/src/core/public/chrome/ui/header/header_logo.tsx +++ b/src/core/public/chrome/ui/header/header_logo.tsx @@ -105,6 +105,8 @@ export function HeaderLogo({ href, navigateToApp, ...observables }: Props) { aria-label={i18n.translate('core.ui.chrome.headerGlobalNav.goHomePageIconAriaLabel', { defaultMessage: 'Go to home page', })} - /> + > + Elastic + ); } diff --git a/src/core/public/chrome/ui/header/header_nav_controls.tsx b/src/core/public/chrome/ui/header/header_nav_controls.tsx index 0941f7b27b662..8d9d8097fd8e3 100644 --- a/src/core/public/chrome/ui/header/header_nav_controls.tsx +++ b/src/core/public/chrome/ui/header/header_nav_controls.tsx @@ -26,7 +26,7 @@ import { HeaderExtension } from './header_extension'; interface Props { navControls$: Observable; - side: 'left' | 'right'; + side?: 'left' | 'right'; } export function HeaderNavControls({ navControls$, side }: Props) { @@ -41,7 +41,10 @@ export function HeaderNavControls({ navControls$, side }: Props) { return ( <> {navControls.map((navControl: ChromeNavControl, index: number) => ( - + ))} diff --git a/src/core/public/chrome/ui/header/nav_drawer.tsx b/src/core/public/chrome/ui/header/nav_drawer.tsx deleted file mode 100644 index fc080fbafc303..0000000000000 --- a/src/core/public/chrome/ui/header/nav_drawer.tsx +++ /dev/null @@ -1,83 +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 { EuiHorizontalRule, EuiNavDrawer, EuiNavDrawerGroup } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import React from 'react'; -import { useObservable } from 'react-use'; -import { Observable } from 'rxjs'; -import { ChromeNavLink, ChromeRecentlyAccessedHistoryItem, CoreStart } from '../../..'; -import { InternalApplicationStart } from '../../../application/types'; -import { HttpStart } from '../../../http'; -import { OnIsLockedUpdate } from './'; -import { createEuiListItem, createRecentNavLink } from './nav_link'; -import { RecentLinks } from './recent_links'; - -export interface Props { - appId$: InternalApplicationStart['currentAppId$']; - basePath: HttpStart['basePath']; - isLocked?: boolean; - navLinks$: Observable; - recentlyAccessed$: Observable; - navigateToApp: CoreStart['application']['navigateToApp']; - onIsLockedUpdate?: OnIsLockedUpdate; -} - -function NavDrawerRenderer( - { isLocked, onIsLockedUpdate, basePath, navigateToApp, ...observables }: Props, - ref: React.Ref -) { - const appId = useObservable(observables.appId$, ''); - const navLinks = useObservable(observables.navLinks$, []).filter((link) => !link.hidden); - const recentNavLinks = useObservable(observables.recentlyAccessed$, []).map((link) => - createRecentNavLink(link, navLinks, basePath) - ); - - return ( - - {RecentLinks({ recentNavLinks })} - - - createEuiListItem({ - link, - appId, - basePath, - navigateToApp, - dataTestSubj: 'navDrawerAppsMenuLink', - }) - )} - aria-label={i18n.translate('core.ui.primaryNavList.screenReaderLabel', { - defaultMessage: 'Primary navigation links', - })} - /> - - ); -} - -export const NavDrawer = React.forwardRef(NavDrawerRenderer); diff --git a/src/core/public/context/context_service.mock.ts b/src/core/public/context/context_service.mock.ts index eb55ced69dc04..3f9f647e1f7d0 100644 --- a/src/core/public/context/context_service.mock.ts +++ b/src/core/public/context/context_service.mock.ts @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ - +import type { PublicMethodsOf } from '@kbn/utility-types'; import { ContextService, ContextSetup } from './context_service'; import { contextMock } from '../../utils/context.mock'; diff --git a/src/core/public/core_app/core_app.mock.ts b/src/core/public/core_app/core_app.mock.ts index b0e3871a40bf5..b2af81b3630a3 100644 --- a/src/core/public/core_app/core_app.mock.ts +++ b/src/core/public/core_app/core_app.mock.ts @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ - +import type { PublicMethodsOf } from '@kbn/utility-types'; import { CoreApp } from './core_app'; type CoreAppContract = PublicMethodsOf; diff --git a/src/core/public/core_system.ts b/src/core/public/core_system.ts index 006d0036f7a12..a1d6f9c988b27 100644 --- a/src/core/public/core_system.ts +++ b/src/core/public/core_system.ts @@ -17,6 +17,7 @@ * under the License. */ +import { pick } from '@kbn/std'; import { CoreId } from '../server'; import { PackageInfo, EnvironmentMode } from '../server/types'; import { CoreSetup, CoreStart } from '.'; @@ -35,7 +36,6 @@ import { OverlayService } from './overlays'; import { PluginsService } from './plugins'; import { UiSettingsService } from './ui_settings'; import { ApplicationService } from './application'; -import { pick } from '../utils/'; import { DocLinksService } from './doc_links'; import { RenderingService } from './rendering'; import { SavedObjectsService } from './saved_objects'; diff --git a/src/core/public/doc_links/doc_links_service.mock.ts b/src/core/public/doc_links/doc_links_service.mock.ts index 105c13f96cef6..07e29edad8974 100644 --- a/src/core/public/doc_links/doc_links_service.mock.ts +++ b/src/core/public/doc_links/doc_links_service.mock.ts @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ - +import type { PublicMethodsOf } from '@kbn/utility-types'; import { injectedMetadataServiceMock } from '../injected_metadata/injected_metadata_service.mock'; import { DocLinksService, DocLinksStart } from './doc_links_service'; diff --git a/src/core/public/doc_links/doc_links_service.ts b/src/core/public/doc_links/doc_links_service.ts index fae7a272c9635..47f58a3a9fcbf 100644 --- a/src/core/public/doc_links/doc_links_service.ts +++ b/src/core/public/doc_links/doc_links_service.ts @@ -17,8 +17,8 @@ * under the License. */ +import { deepFreeze } from '@kbn/std'; import { InjectedMetadataSetup } from '../injected_metadata'; -import { deepFreeze } from '../../utils'; interface StartDeps { injectedMetadata: InjectedMetadataSetup; diff --git a/src/core/public/fatal_errors/fatal_errors_service.mock.ts b/src/core/public/fatal_errors/fatal_errors_service.mock.ts index 6d9876f787fa9..326ce9a3923e9 100644 --- a/src/core/public/fatal_errors/fatal_errors_service.mock.ts +++ b/src/core/public/fatal_errors/fatal_errors_service.mock.ts @@ -16,6 +16,7 @@ * specific language governing permissions and limitations * under the License. */ +import type { PublicMethodsOf } from '@kbn/utility-types'; import { FatalErrorsService, FatalErrorsSetup } from './fatal_errors_service'; const createSetupContractMock = () => { diff --git a/src/core/public/http/base_path.ts b/src/core/public/http/base_path.ts index ac85d71c793fe..5d9eb51023b78 100644 --- a/src/core/public/http/base_path.ts +++ b/src/core/public/http/base_path.ts @@ -35,7 +35,7 @@ * under the License. */ -import { modifyUrl } from '../../utils'; +import { modifyUrl } from '@kbn/std'; export class BasePath { constructor( diff --git a/src/core/public/http/http_service.mock.ts b/src/core/public/http/http_service.mock.ts index 1111fd39ec78e..68533159765fb 100644 --- a/src/core/public/http/http_service.mock.ts +++ b/src/core/public/http/http_service.mock.ts @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ - +import type { PublicMethodsOf } from '@kbn/utility-types'; import { HttpService } from './http_service'; import { HttpSetup } from './types'; import { BehaviorSubject } from 'rxjs'; diff --git a/src/core/public/http/loading_count_service.mock.ts b/src/core/public/http/loading_count_service.mock.ts index 79928aa4b160d..2ff635e815296 100644 --- a/src/core/public/http/loading_count_service.mock.ts +++ b/src/core/public/http/loading_count_service.mock.ts @@ -19,6 +19,7 @@ import { LoadingCountSetup, LoadingCountService } from './loading_count_service'; import { BehaviorSubject } from 'rxjs'; +import type { PublicMethodsOf } from '@kbn/utility-types'; const createSetupContractMock = () => { const setupContract: jest.Mocked = { diff --git a/src/core/public/i18n/i18n_service.mock.ts b/src/core/public/i18n/i18n_service.mock.ts index 1a9ca0869d7c1..a91710fbed780 100644 --- a/src/core/public/i18n/i18n_service.mock.ts +++ b/src/core/public/i18n/i18n_service.mock.ts @@ -18,6 +18,8 @@ */ import React from 'react'; +import type { PublicMethodsOf } from '@kbn/utility-types'; + import { I18nService, I18nStart } from './i18n_service'; const PassThroughComponent = ({ children }: { children: React.ReactNode }) => children; diff --git a/src/core/public/index.scss b/src/core/public/index.scss index c2ad2841d5a77..6ba9254e5d381 100644 --- a/src/core/public/index.scss +++ b/src/core/public/index.scss @@ -1,6 +1,6 @@ +@import './variables'; @import './core'; @import './chrome/index'; @import './overlays/index'; @import './rendering/index'; @import './styles/index'; - diff --git a/src/core/public/index.ts b/src/core/public/index.ts index a9774dafd2340..24d19e2d32074 100644 --- a/src/core/public/index.ts +++ b/src/core/public/index.ts @@ -68,7 +68,6 @@ import { UiSettingsState, IUiSettingsClient } from './ui_settings'; import { ApplicationSetup, Capabilities, ApplicationStart } from './application'; import { DocLinksStart } from './doc_links'; import { SavedObjectsStart } from './saved_objects'; -export { PackageInfo, EnvironmentMode } from '../server/types'; import { IContextContainer, IContextProvider, @@ -78,17 +77,9 @@ import { HandlerParameters, } from './context'; +export { PackageInfo, EnvironmentMode } from '../server/types'; export { CoreContext, CoreSystem } from './core_system'; -export { - DEFAULT_APP_CATEGORIES, - getFlattenedObject, - URLMeaningfulParts, - modifyUrl, - isRelativeUrl, - Freezable, - deepFreeze, - assertNever, -} from '../utils'; +export { DEFAULT_APP_CATEGORIES } from '../utils'; export { AppCategory, UiSettingsParams, diff --git a/src/core/public/injected_metadata/injected_metadata_service.mock.ts b/src/core/public/injected_metadata/injected_metadata_service.mock.ts index 3bb4358406246..33d04eedebb07 100644 --- a/src/core/public/injected_metadata/injected_metadata_service.mock.ts +++ b/src/core/public/injected_metadata/injected_metadata_service.mock.ts @@ -16,6 +16,7 @@ * specific language governing permissions and limitations * under the License. */ +import type { PublicMethodsOf } from '@kbn/utility-types'; import { InjectedMetadataService, InjectedMetadataSetup } from './injected_metadata_service'; const createSetupContractMock = () => { diff --git a/src/core/public/injected_metadata/injected_metadata_service.ts b/src/core/public/injected_metadata/injected_metadata_service.ts index 23630a5bcf228..5b51bc823d166 100644 --- a/src/core/public/injected_metadata/injected_metadata_service.ts +++ b/src/core/public/injected_metadata/injected_metadata_service.ts @@ -18,6 +18,7 @@ */ import { get } from 'lodash'; +import { deepFreeze } from '@kbn/std'; import { DiscoveredPlugin, PluginName } from '../../server'; import { EnvironmentMode, @@ -25,7 +26,6 @@ import { UiSettingsParams, UserProvidedValues, } from '../../server/types'; -import { deepFreeze } from '../../utils/'; import { AppCategory } from '../'; export interface InjectedPluginMetadata { diff --git a/src/core/public/integrations/integrations_service.mock.ts b/src/core/public/integrations/integrations_service.mock.ts index 4f6ca0fb68459..88458ebb747fe 100644 --- a/src/core/public/integrations/integrations_service.mock.ts +++ b/src/core/public/integrations/integrations_service.mock.ts @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ - +import type { PublicMethodsOf } from '@kbn/utility-types'; import { IntegrationsService } from './integrations_service'; type IntegrationsServiceContract = PublicMethodsOf; diff --git a/src/core/public/notifications/notifications_service.mock.ts b/src/core/public/notifications/notifications_service.mock.ts index 464f47e20aa3b..990ab479d35c3 100644 --- a/src/core/public/notifications/notifications_service.mock.ts +++ b/src/core/public/notifications/notifications_service.mock.ts @@ -16,6 +16,7 @@ * specific language governing permissions and limitations * under the License. */ +import type { PublicMethodsOf } from '@kbn/utility-types'; import { NotificationsService, NotificationsSetup, diff --git a/src/core/public/overlays/banners/banners_service.mock.ts b/src/core/public/overlays/banners/banners_service.mock.ts index 14041b2720877..acd38af383d67 100644 --- a/src/core/public/overlays/banners/banners_service.mock.ts +++ b/src/core/public/overlays/banners/banners_service.mock.ts @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ - +import type { PublicMethodsOf } from '@kbn/utility-types'; import { OverlayBannersStart, OverlayBannersService } from './banners_service'; const createStartContractMock = () => { diff --git a/src/core/public/overlays/flyout/flyout_service.mock.ts b/src/core/public/overlays/flyout/flyout_service.mock.ts index 91544500713d6..90dcb308de1b9 100644 --- a/src/core/public/overlays/flyout/flyout_service.mock.ts +++ b/src/core/public/overlays/flyout/flyout_service.mock.ts @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ - +import type { PublicMethodsOf } from '@kbn/utility-types'; import { FlyoutService, OverlayFlyoutStart } from './flyout_service'; const createStartContractMock = () => { diff --git a/src/core/public/overlays/modal/modal_service.mock.ts b/src/core/public/overlays/modal/modal_service.mock.ts index 5ac49874dcf93..0af207d168fae 100644 --- a/src/core/public/overlays/modal/modal_service.mock.ts +++ b/src/core/public/overlays/modal/modal_service.mock.ts @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ - +import type { PublicMethodsOf } from '@kbn/utility-types'; import { ModalService, OverlayModalStart } from './modal_service'; const createStartContractMock = () => { diff --git a/src/core/public/overlays/overlay_service.mock.ts b/src/core/public/overlays/overlay_service.mock.ts index e29247494034f..66ba36b20b45c 100644 --- a/src/core/public/overlays/overlay_service.mock.ts +++ b/src/core/public/overlays/overlay_service.mock.ts @@ -16,6 +16,7 @@ * specific language governing permissions and limitations * under the License. */ +import type { PublicMethodsOf } from '@kbn/utility-types'; import { OverlayService, OverlayStart } from './overlay_service'; import { overlayBannersServiceMock } from './banners/banners_service.mock'; import { overlayFlyoutServiceMock } from './flyout/flyout_service.mock'; diff --git a/src/core/public/plugins/plugins_service.mock.ts b/src/core/public/plugins/plugins_service.mock.ts index 900f20422b826..0625d2c3ec4b2 100644 --- a/src/core/public/plugins/plugins_service.mock.ts +++ b/src/core/public/plugins/plugins_service.mock.ts @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ - +import type { PublicMethodsOf } from '@kbn/utility-types'; import { PluginsService, PluginsServiceSetup } from './plugins_service'; const createSetupContractMock = () => { diff --git a/src/core/public/plugins/plugins_service.ts b/src/core/public/plugins/plugins_service.ts index f9bc40ca52601..87219f4c0bdd3 100644 --- a/src/core/public/plugins/plugins_service.ts +++ b/src/core/public/plugins/plugins_service.ts @@ -17,6 +17,7 @@ * under the License. */ +import { withTimeout } from '@kbn/std'; import { PluginName, PluginOpaqueId } from '../../server'; import { CoreService } from '../../types'; import { CoreContext } from '../core_system'; @@ -28,7 +29,6 @@ import { } from './plugin_context'; import { InternalCoreSetup, InternalCoreStart } from '../core_system'; import { InjectedPluginMetadata } from '../injected_metadata'; -import { withTimeout } from '../../utils'; const Sec = 1000; /** @internal */ diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index d90b8f780b674..d0b9e115bd524 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -7,7 +7,6 @@ import { Action } from 'history'; import { ApiResponse } from '@elastic/elasticsearch/lib/Transport'; import Boom from 'boom'; -import { ErrorToastOptions as ErrorToastOptions_2 } from 'src/core/public/notifications'; import { EuiBreadcrumb } from '@elastic/eui'; import { EuiButtonEmptyProps } from '@elastic/eui'; import { EuiConfirmModalProps } from '@elastic/eui'; @@ -22,15 +21,13 @@ import { Location } from 'history'; import { LocationDescriptorObject } from 'history'; import { MaybePromise } from '@kbn/utility-types'; import { Observable } from 'rxjs'; -import { ParsedQuery } from 'query-string'; import { Path } from 'history'; +import { PublicMethodsOf } from '@kbn/utility-types'; import { PublicUiSettingsParams as PublicUiSettingsParams_2 } from 'src/core/server/types'; import React from 'react'; import { RecursiveReadonly } from '@kbn/utility-types'; import * as Rx from 'rxjs'; -import { SavedObject as SavedObject_2 } from 'src/core/server'; import { ShallowPromise } from '@kbn/utility-types'; -import { ToastInputFields as ToastInputFields_2 } from 'src/core/public/notifications'; import { TransportRequestOptions } from '@elastic/elasticsearch/lib/Transport'; import { TransportRequestParams } from '@elastic/elasticsearch/lib/Transport'; import { TransportRequestPromise } from '@elastic/elasticsearch/lib/Transport'; @@ -187,9 +184,6 @@ export type AppUpdatableFields = Pick Partial | undefined; -// @public -export function assertNever(x: never): never; - // @public export interface Capabilities { [key: string]: Record>; @@ -272,10 +266,13 @@ export interface ChromeNavControl { // @public export interface ChromeNavControls { + // @internal (undocumented) + getCenter$(): Observable; // @internal (undocumented) getLeft$(): Observable; // @internal (undocumented) getRight$(): Observable; + registerCenter(navControl: ChromeNavControl): void; registerLeft(navControl: ChromeNavControl): void; registerRight(navControl: ChromeNavControl): void; } @@ -345,7 +342,6 @@ export interface ChromeStart { getHelpExtension$(): Observable; getIsNavDrawerLocked$(): Observable; getIsVisible$(): Observable; - getNavType$(): Observable; navControls: ChromeNavControls; navLinks: ChromeNavLinks; recentlyAccessed: ChromeRecentlyAccessed; @@ -444,9 +440,6 @@ export class CoreSystem { stop(): void; } -// @public -export function deepFreeze(object: T): RecursiveReadonly; - // @internal (undocumented) export const DEFAULT_APP_CATEGORIES: Readonly<{ kibana: { @@ -616,16 +609,6 @@ export interface FatalErrorsSetup { // @public export type FatalErrorsStart = FatalErrorsSetup; -// @public (undocumented) -export type Freezable = { - [k: string]: any; -} | any[]; - -// @public -export function getFlattenedObject(rootValue: Record): { - [key: string]: any; -}; - // @public export type HandlerContextType> = T extends HandlerFunction ? U : never; @@ -837,9 +820,6 @@ export interface ImageValidation { }; } -// @public -export function isRelativeUrl(candidatePath: string): boolean; - // @public export type IToasts = Pick; @@ -868,9 +848,6 @@ export interface IUiSettingsClient { set: (key: string, value: any) => Promise; } -// @public -export function modifyUrl(url: string, urlModifier: (urlParts: URLMeaningfulParts) => Partial | void): string; - // @public export type MountPoint = (element: T) => UnmountCallback; @@ -1447,26 +1424,6 @@ export type UnmountCallback = () => void; // @public export const URL_MAX_LENGTH: number; -// @public -export interface URLMeaningfulParts { - // (undocumented) - auth?: string | null; - // (undocumented) - hash?: string | null; - // (undocumented) - hostname?: string | null; - // (undocumented) - pathname?: string | null; - // (undocumented) - port?: string | null; - // (undocumented) - protocol?: string | null; - // (undocumented) - query: ParsedQuery; - // (undocumented) - slashes?: boolean | null; -} - // @public export interface UserProvidedValues { // (undocumented) diff --git a/src/core/public/rendering/_base.scss b/src/core/public/rendering/_base.scss index 211e9c03beea5..b806ac270331d 100644 --- a/src/core/public/rendering/_base.scss +++ b/src/core/public/rendering/_base.scss @@ -1,5 +1,4 @@ -@import '@elastic/eui/src/global_styling/variables/header'; -@import '@elastic/eui/src/components/nav_drawer/variables'; +@include euiHeaderAffordForFixed($kbnHeaderOffset); /** * stretch the root element of the Kibana application to set the base-size that @@ -12,74 +11,11 @@ min-height: 100%; } -// TODO #64541 -// Delete this block -.chrHeaderWrapper:not(.headerWrapper) ~ .app-wrapper { +.app-wrapper { display: flex; flex-flow: column nowrap; - position: absolute; - left: 0; - top: 0; - right: 0; - bottom: 0; - z-index: 5; margin: 0 auto; - - &:not(.hidden-chrome) { - top: $euiHeaderChildSize; - left: $euiHeaderChildSize; - - // HOTFIX: Temporary fix for flyouts not inside portals - // SASSTODO: Find an actual solution - .euiFlyout { - top: $euiHeaderChildSize; - height: calc(100% - #{$euiHeaderChildSize}); - } - - @include euiBreakpoint('xs', 's') { - left: 0; - } - } - - /** - * 1. Dirty, but we need to override the .kbnGlobalNav-isOpen state - * when we're looking at the log-in screen. - */ - &.hidden-chrome { - left: 0 !important; /* 1 */ - } - - .navbar-right { - margin-right: 0; - } -} - -// TODO #64541 -// Delete this block -@include euiBreakpoint('xl') { - .chrHeaderWrapper--navIsLocked:not(.headerWrapper) { - ~ .app-wrapper:not(.hidden-chrome) { - // Shrink the content from the left so it's no longer overlapped by the nav drawer (ALWAYS) - left: $euiNavDrawerWidthExpanded !important; // sass-lint:disable-line no-important - transition: left $euiAnimSpeedFast $euiAnimSlightResistance; - } - } -} - -// TODO #64541 -// Remove .headerWrapper and header conditionals -.headerWrapper ~ .app-wrapper, -:not(header) ~ .app-wrapper { - display: flex; - flex-flow: column nowrap; - margin: 0 auto; - min-height: calc(100vh - #{$euiHeaderHeightCompensation}); - - @include internetExplorerOnly { - // IE specific bug with min-height in flex container, described in the next link - // https://github.com/philipwalton/flexbugs#3-min-height-on-a-flex-container-wont-apply-to-its-flex-items - height: calc(100vh - #{$euiHeaderHeightCompensation}); - } + min-height: calc(100vh - #{$kbnHeaderOffset}); &.hidden-chrome { min-height: 100vh; diff --git a/src/core/public/rendering/rendering_service.mock.ts b/src/core/public/rendering/rendering_service.mock.ts index bb4723e69ab5e..803a7dae91a40 100644 --- a/src/core/public/rendering/rendering_service.mock.ts +++ b/src/core/public/rendering/rendering_service.mock.ts @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ - +import type { PublicMethodsOf } from '@kbn/utility-types'; import { RenderingService } from './rendering_service'; type RenderingServiceContract = PublicMethodsOf; diff --git a/src/core/public/saved_objects/saved_objects_client.ts b/src/core/public/saved_objects/saved_objects_client.ts index 351020004b0e7..5a8949ca2f55f 100644 --- a/src/core/public/saved_objects/saved_objects_client.ts +++ b/src/core/public/saved_objects/saved_objects_client.ts @@ -19,6 +19,7 @@ import { cloneDeep, pick, throttle } from 'lodash'; import { resolve as resolveUrl } from 'url'; +import type { PublicMethodsOf } from '@kbn/utility-types'; import { SavedObject, diff --git a/src/core/public/styles/_base.scss b/src/core/public/styles/_base.scss index 427c6b7735435..bfb07c1b51427 100644 --- a/src/core/public/styles/_base.scss +++ b/src/core/public/styles/_base.scss @@ -7,17 +7,6 @@ // Application Layout -// chrome-context -// TODO #64541 -// Delete this block -.chrHeaderWrapper:not(.headerWrapper) .content { - display: flex; - flex-flow: row nowrap; - width: 100%; - height: 100%; - overflow: hidden; -} - .application, .app-container { > * { diff --git a/src/core/public/ui_settings/ui_settings_service.mock.ts b/src/core/public/ui_settings/ui_settings_service.mock.ts index e1f7eeff93471..1921c26e57971 100644 --- a/src/core/public/ui_settings/ui_settings_service.mock.ts +++ b/src/core/public/ui_settings/ui_settings_service.mock.ts @@ -17,6 +17,7 @@ * under the License. */ import * as Rx from 'rxjs'; +import type { PublicMethodsOf } from '@kbn/utility-types'; import { UiSettingsService } from './'; import { IUiSettingsClient } from './types'; diff --git a/src/core/server/audit_trail/audit_trail_service.mock.ts b/src/core/server/audit_trail/audit_trail_service.mock.ts index d63b3539e5cdc..4c9c064840750 100644 --- a/src/core/server/audit_trail/audit_trail_service.mock.ts +++ b/src/core/server/audit_trail/audit_trail_service.mock.ts @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ - +import type { PublicMethodsOf } from '@kbn/utility-types'; import { AuditTrailSetup, AuditTrailStart, Auditor } from './types'; import { AuditTrailService } from './audit_trail_service'; diff --git a/src/core/server/capabilities/capabilities_service.mock.ts b/src/core/server/capabilities/capabilities_service.mock.ts index 3f31eca8339d8..7d134f9592dc7 100644 --- a/src/core/server/capabilities/capabilities_service.mock.ts +++ b/src/core/server/capabilities/capabilities_service.mock.ts @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ - +import type { PublicMethodsOf } from '@kbn/utility-types'; import { CapabilitiesService, CapabilitiesSetup, CapabilitiesStart } from './capabilities_service'; const createSetupContractMock = () => { diff --git a/src/core/server/config/config_service.ts b/src/core/server/config/config_service.ts index bceba420bb6ce..d77ee980b0491 100644 --- a/src/core/server/config/config_service.ts +++ b/src/core/server/config/config_service.ts @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ - +import type { PublicMethodsOf } from '@kbn/utility-types'; import { Type } from '@kbn/config-schema'; import { isEqual } from 'lodash'; import { BehaviorSubject, combineLatest, Observable } from 'rxjs'; diff --git a/src/core/server/config/deprecation/deprecation_factory.ts b/src/core/server/config/deprecation/deprecation_factory.ts index cbc9984924c5d..0598347d2cffc 100644 --- a/src/core/server/config/deprecation/deprecation_factory.ts +++ b/src/core/server/config/deprecation/deprecation_factory.ts @@ -17,10 +17,10 @@ * under the License. */ -import { set } from '@elastic/safer-lodash-set'; import { get } from 'lodash'; +import { set } from '@elastic/safer-lodash-set'; +import { unset } from '@kbn/std'; import { ConfigDeprecation, ConfigDeprecationLogger, ConfigDeprecationFactory } from './types'; -import { unset } from '../../../utils'; const _rename = ( config: Record, diff --git a/src/core/server/config/object_to_config_adapter.ts b/src/core/server/config/object_to_config_adapter.ts index 50b31722dceeb..c4d6ac02ccf05 100644 --- a/src/core/server/config/object_to_config_adapter.ts +++ b/src/core/server/config/object_to_config_adapter.ts @@ -17,10 +17,10 @@ * under the License. */ -import { set } from '@elastic/safer-lodash-set'; import { cloneDeep, get, has } from 'lodash'; +import { set } from '@elastic/safer-lodash-set'; +import { getFlattenedObject } from '@kbn/std'; -import { getFlattenedObject } from '../../utils'; import { Config, ConfigPath } from './'; /** diff --git a/src/core/server/config/raw_config_service.mock.ts b/src/core/server/config/raw_config_service.mock.ts index fdcb17395aaad..73a3b5cc9e4d0 100644 --- a/src/core/server/config/raw_config_service.mock.ts +++ b/src/core/server/config/raw_config_service.mock.ts @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ - +import type { PublicMethodsOf } from '@kbn/utility-types'; import { RawConfigService } from './raw_config_service'; import { Observable, of } from 'rxjs'; diff --git a/src/core/server/context/context_service.mock.ts b/src/core/server/context/context_service.mock.ts index eb55ced69dc04..a8d895acad624 100644 --- a/src/core/server/context/context_service.mock.ts +++ b/src/core/server/context/context_service.mock.ts @@ -16,6 +16,7 @@ * specific language governing permissions and limitations * under the License. */ +import type { PublicMethodsOf } from '@kbn/utility-types'; import { ContextService, ContextSetup } from './context_service'; import { contextMock } from '../../utils/context.mock'; diff --git a/src/core/server/core_context.mock.ts b/src/core/server/core_context.mock.ts index f870d30528df4..2b887358818e3 100644 --- a/src/core/server/core_context.mock.ts +++ b/src/core/server/core_context.mock.ts @@ -16,7 +16,6 @@ * specific language governing permissions and limitations * under the License. */ - import { CoreContext } from './core_context'; import { getEnvOptions } from './config/__mocks__/env'; import { Env, IConfigService } from './config'; diff --git a/src/core/server/elasticsearch/client/mocks.ts b/src/core/server/elasticsearch/client/mocks.ts index 2f2ca08fee6f2..6fb3dc090bfb4 100644 --- a/src/core/server/elasticsearch/client/mocks.ts +++ b/src/core/server/elasticsearch/client/mocks.ts @@ -16,7 +16,6 @@ * specific language governing permissions and limitations * under the License. */ - import { Client, ApiResponse } from '@elastic/elasticsearch'; import { TransportRequestPromise } from '@elastic/elasticsearch/lib/Transport'; import { ElasticsearchClient } from './types'; @@ -75,7 +74,7 @@ export type ElasticsearchClientMock = DeeplyMockedKeys; const createClientMock = (): ElasticsearchClientMock => (createInternalClientMock() as unknown) as ElasticsearchClientMock; -interface ScopedClusterClientMock { +export interface ScopedClusterClientMock { asInternalUser: ElasticsearchClientMock; asCurrentUser: ElasticsearchClientMock; } diff --git a/src/core/server/elasticsearch/elasticsearch_service.mock.ts b/src/core/server/elasticsearch/elasticsearch_service.mock.ts index 26186efc286bf..ad80928d2fe5e 100644 --- a/src/core/server/elasticsearch/elasticsearch_service.mock.ts +++ b/src/core/server/elasticsearch/elasticsearch_service.mock.ts @@ -18,6 +18,8 @@ */ import { BehaviorSubject } from 'rxjs'; +import type { PublicMethodsOf } from '@kbn/utility-types'; + import { ILegacyClusterClient, ILegacyCustomClusterClient } from './legacy'; import { elasticsearchClientMock, @@ -32,7 +34,7 @@ import { InternalElasticsearchServiceSetup, ElasticsearchStatusMeta } from './ty import { NodesVersionCompatibility } from './version_check/ensure_es_version'; import { ServiceStatus, ServiceStatusLevels } from '../status'; -interface MockedElasticSearchServiceSetup { +export interface MockedElasticSearchServiceSetup { legacy: { config$: BehaviorSubject; createClient: jest.Mock; diff --git a/src/core/server/elasticsearch/elasticsearch_service.ts b/src/core/server/elasticsearch/elasticsearch_service.ts index 2cc065aaaaeb1..5d07840e8bda7 100644 --- a/src/core/server/elasticsearch/elasticsearch_service.ts +++ b/src/core/server/elasticsearch/elasticsearch_service.ts @@ -19,9 +19,9 @@ import { Observable, Subject } from 'rxjs'; import { first, map, shareReplay, takeUntil } from 'rxjs/operators'; +import { merge } from '@kbn/std'; import { CoreService } from '../../types'; -import { merge } from '../../utils'; import { CoreContext } from '../core_context'; import { Logger } from '../logging'; import { diff --git a/src/core/server/elasticsearch/legacy/elasticsearch_client_config.ts b/src/core/server/elasticsearch/legacy/elasticsearch_client_config.ts index 3dbc1c2c647a9..6896c0a2e301f 100644 --- a/src/core/server/elasticsearch/legacy/elasticsearch_client_config.ts +++ b/src/core/server/elasticsearch/legacy/elasticsearch_client_config.ts @@ -22,7 +22,7 @@ import { cloneDeep } from 'lodash'; import { Duration } from 'moment'; import { checkServerIdentity } from 'tls'; import url from 'url'; -import { pick } from '../../../utils'; +import { pick } from '@kbn/std'; import { Logger } from '../../logging'; import { ElasticsearchConfig } from '../elasticsearch_config'; diff --git a/src/core/server/environment/environment_service.mock.ts b/src/core/server/environment/environment_service.mock.ts index 8bf726b4a6388..a956e369ba4a7 100644 --- a/src/core/server/environment/environment_service.mock.ts +++ b/src/core/server/environment/environment_service.mock.ts @@ -16,6 +16,7 @@ * specific language governing permissions and limitations * under the License. */ +import type { PublicMethodsOf } from '@kbn/utility-types'; import { EnvironmentService, InternalEnvironmentServiceSetup } from './environment_service'; diff --git a/src/core/server/environment/environment_service.test.ts b/src/core/server/environment/environment_service.test.ts index 06fd250ebe4f9..f6cffb5e26a9e 100644 --- a/src/core/server/environment/environment_service.test.ts +++ b/src/core/server/environment/environment_service.test.ts @@ -21,6 +21,7 @@ import { BehaviorSubject } from 'rxjs'; import { EnvironmentService } from './environment_service'; import { resolveInstanceUuid } from './resolve_uuid'; import { createDataFolder } from './create_data_folder'; +import { writePidFile } from './write_pid_file'; import { CoreContext } from '../core_context'; import { configServiceMock } from '../config/config_service.mock'; @@ -35,12 +36,20 @@ jest.mock('./create_data_folder', () => ({ createDataFolder: jest.fn(), })); +jest.mock('./write_pid_file', () => ({ + writePidFile: jest.fn(), +})); + const pathConfig = { data: 'data-folder', }; const serverConfig = { uuid: 'SOME_UUID', }; +const pidConfig = { + file: '/pid/file', + exclusive: 'false', +}; const getConfigService = () => { const configService = configServiceMock.create(); @@ -51,6 +60,9 @@ const getConfigService = () => { if (path === 'server') { return new BehaviorSubject(serverConfig); } + if (path === 'pid') { + return new BehaviorSubject(pidConfig); + } return new BehaviorSubject({}); }); return configService; @@ -76,7 +88,7 @@ describe('UuidService', () => { expect(resolveInstanceUuid).toHaveBeenCalledWith({ pathConfig, serverConfig, - logger: logger.get('uuid'), + logger: logger.get('environment'), }); }); @@ -86,7 +98,17 @@ describe('UuidService', () => { expect(createDataFolder).toHaveBeenCalledTimes(1); expect(createDataFolder).toHaveBeenCalledWith({ pathConfig, - logger: logger.get('uuid'), + logger: logger.get('environment'), + }); + }); + + it('calls writePidFile with correct parameters', async () => { + const service = new EnvironmentService(coreContext); + await service.setup(); + expect(writePidFile).toHaveBeenCalledTimes(1); + expect(writePidFile).toHaveBeenCalledWith({ + pidConfig, + logger: logger.get('environment'), }); }); diff --git a/src/core/server/environment/environment_service.ts b/src/core/server/environment/environment_service.ts index 50d872c51f109..b051dfbe4c7f2 100644 --- a/src/core/server/environment/environment_service.ts +++ b/src/core/server/environment/environment_service.ts @@ -23,8 +23,10 @@ import { CoreContext } from '../core_context'; import { Logger } from '../logging'; import { IConfigService } from '../config'; import { HttpConfigType, config as httpConfigDef } from '../http'; +import { PidConfigType, config as pidConfigDef } from './pid_config'; import { resolveInstanceUuid } from './resolve_uuid'; import { createDataFolder } from './create_data_folder'; +import { writePidFile } from './write_pid_file'; /** * @internal @@ -43,17 +45,24 @@ export class EnvironmentService { private uuid: string = ''; constructor(core: CoreContext) { - this.log = core.logger.get('uuid'); + this.log = core.logger.get('environment'); this.configService = core.configService; } public async setup() { - const [pathConfig, serverConfig] = await Promise.all([ + const [pathConfig, serverConfig, pidConfig] = await Promise.all([ this.configService.atPath(pathConfigDef.path).pipe(take(1)).toPromise(), this.configService.atPath(httpConfigDef.path).pipe(take(1)).toPromise(), + this.configService.atPath(pidConfigDef.path).pipe(take(1)).toPromise(), ]); + // was present in the legacy `pid` file. + process.on('unhandledRejection', (reason) => { + this.log.warn(`Detected an unhandled Promise rejection.\n${reason}`); + }); + await createDataFolder({ pathConfig, logger: this.log }); + await writePidFile({ pidConfig, logger: this.log }); this.uuid = await resolveInstanceUuid({ pathConfig, diff --git a/src/core/server/environment/fs.ts b/src/core/server/environment/fs.ts index dc040ccb73615..b79c70dbee280 100644 --- a/src/core/server/environment/fs.ts +++ b/src/core/server/environment/fs.ts @@ -23,3 +23,4 @@ import { promisify } from 'util'; export const readFile = promisify(Fs.readFile); export const writeFile = promisify(Fs.writeFile); export const mkdir = promisify(Fs.mkdir); +export const exists = promisify(Fs.exists); diff --git a/src/core/server/environment/index.ts b/src/core/server/environment/index.ts index 57a26d5ea3c79..92b57c6af2fff 100644 --- a/src/core/server/environment/index.ts +++ b/src/core/server/environment/index.ts @@ -18,3 +18,4 @@ */ export { EnvironmentService, InternalEnvironmentServiceSetup } from './environment_service'; +export { config, PidConfigType } from './pid_config'; diff --git a/src/core/server/environment/pid_config.ts b/src/core/server/environment/pid_config.ts new file mode 100644 index 0000000000000..ee9963016717e --- /dev/null +++ b/src/core/server/environment/pid_config.ts @@ -0,0 +1,30 @@ +/* + * 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 { TypeOf, schema } from '@kbn/config-schema'; + +export const config = { + path: 'pid', + schema: schema.object({ + file: schema.maybe(schema.string()), + exclusive: schema.boolean({ defaultValue: false }), + }), +}; + +export type PidConfigType = TypeOf; diff --git a/src/core/server/environment/write_pid_file.test.ts b/src/core/server/environment/write_pid_file.test.ts new file mode 100644 index 0000000000000..f9eb78a486970 --- /dev/null +++ b/src/core/server/environment/write_pid_file.test.ts @@ -0,0 +1,144 @@ +/* + * 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 { writeFile, exists } from './fs'; +import { writePidFile } from './write_pid_file'; +import { loggingSystemMock } from '../logging/logging_system.mock'; + +jest.mock('./fs', () => ({ + writeFile: jest.fn(), + exists: jest.fn(), +})); + +const writeFileMock = writeFile as jest.MockedFunction; +const existsMock = exists as jest.MockedFunction; + +const pid = String(process.pid); + +describe('writePidFile', () => { + let logger: ReturnType; + + beforeEach(() => { + logger = loggingSystemMock.createLogger(); + jest.spyOn(process, 'once'); + + writeFileMock.mockImplementation(() => Promise.resolve()); + existsMock.mockImplementation(() => Promise.resolve(false)); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + const allLogs = () => + Object.entries(loggingSystemMock.collect(logger)).reduce((messages, [key, value]) => { + return [...messages, ...(key === 'log' ? [] : (value as any[]).map(([msg]) => [key, msg]))]; + }, [] as any[]); + + it('does nothing if `pid.file` is not set', async () => { + await writePidFile({ + pidConfig: { + file: undefined, + exclusive: false, + }, + logger, + }); + expect(writeFile).not.toHaveBeenCalled(); + expect(process.once).not.toHaveBeenCalled(); + expect(allLogs()).toMatchInlineSnapshot(`Array []`); + }); + + it('writes the pid file to `pid.file`', async () => { + existsMock.mockResolvedValue(false); + + await writePidFile({ + pidConfig: { + file: '/pid-file', + exclusive: false, + }, + logger, + }); + + expect(writeFile).toHaveBeenCalledTimes(1); + expect(writeFile).toHaveBeenCalledWith('/pid-file', pid); + + expect(process.once).toHaveBeenCalledTimes(2); + expect(process.once).toHaveBeenCalledWith('exit', expect.any(Function)); + expect(process.once).toHaveBeenCalledWith('SIGINT', expect.any(Function)); + + expect(allLogs()).toMatchInlineSnapshot(` + Array [ + Array [ + "debug", + "wrote pid file to /pid-file", + ], + ] + `); + }); + + it('throws an error if the file exists and `pid.exclusive is true`', async () => { + existsMock.mockResolvedValue(true); + + await expect( + writePidFile({ + pidConfig: { + file: '/pid-file', + exclusive: true, + }, + logger, + }) + ).rejects.toThrowErrorMatchingInlineSnapshot(`"pid file already exists at /pid-file"`); + + expect(writeFile).not.toHaveBeenCalled(); + expect(process.once).not.toHaveBeenCalled(); + expect(allLogs()).toMatchInlineSnapshot(`Array []`); + }); + + it('logs a warning if the file exists and `pid.exclusive` is false', async () => { + existsMock.mockResolvedValue(true); + + await writePidFile({ + pidConfig: { + file: '/pid-file', + exclusive: false, + }, + logger, + }); + + expect(writeFile).toHaveBeenCalledTimes(1); + expect(writeFile).toHaveBeenCalledWith('/pid-file', pid); + + expect(process.once).toHaveBeenCalledTimes(2); + expect(process.once).toHaveBeenCalledWith('exit', expect.any(Function)); + expect(process.once).toHaveBeenCalledWith('SIGINT', expect.any(Function)); + + expect(allLogs()).toMatchInlineSnapshot(` + Array [ + Array [ + "debug", + "wrote pid file to /pid-file", + ], + Array [ + "warn", + "pid file already exists at /pid-file", + ], + ] + `); + }); +}); diff --git a/src/core/server/environment/write_pid_file.ts b/src/core/server/environment/write_pid_file.ts new file mode 100644 index 0000000000000..6ee20af02d7b0 --- /dev/null +++ b/src/core/server/environment/write_pid_file.ts @@ -0,0 +1,64 @@ +/* + * 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 { unlinkSync as unlink } from 'fs'; +import once from 'lodash/once'; +import { Logger } from '../logging'; +import { writeFile, exists } from './fs'; +import { PidConfigType } from './pid_config'; + +export const writePidFile = async ({ + pidConfig, + logger, +}: { + pidConfig: PidConfigType; + logger: Logger; +}) => { + const path = pidConfig.file; + if (!path) { + return; + } + + const pid = String(process.pid); + + if (await exists(path)) { + const message = `pid file already exists at ${path}`; + if (pidConfig.exclusive) { + throw new Error(message); + } else { + logger.warn(message, { path, pid }); + } + } + + await writeFile(path, pid); + + logger.debug(`wrote pid file to ${path}`, { path, pid }); + + const clean = once(() => { + unlink(path); + }); + + process.once('exit', clean); // for "natural" exits + process.once('SIGINT', () => { + // for Ctrl-C exits + clean(); + // resend SIGINT + process.kill(process.pid, 'SIGINT'); + }); +}; diff --git a/src/core/server/http/base_path_service.ts b/src/core/server/http/base_path_service.ts index 093d73b2da3bf..059eb36f42dd5 100644 --- a/src/core/server/http/base_path_service.ts +++ b/src/core/server/http/base_path_service.ts @@ -16,9 +16,9 @@ * specific language governing permissions and limitations * under the License. */ -import { ensureRawRequest, KibanaRequest, LegacyRequest } from './router'; +import { modifyUrl } from '@kbn/std'; -import { modifyUrl } from '../../utils'; +import { ensureRawRequest, KibanaRequest, LegacyRequest } from './router'; /** * Access or manipulate the Kibana base path diff --git a/src/core/server/http/http_service.mock.ts b/src/core/server/http/http_service.mock.ts index 676cee1954c59..3e38f6a6d384d 100644 --- a/src/core/server/http/http_service.mock.ts +++ b/src/core/server/http/http_service.mock.ts @@ -18,6 +18,8 @@ */ import { Server } from 'hapi'; +import type { PublicMethodsOf } from '@kbn/utility-types'; + import { CspConfig } from '../csp'; import { mockRouter, RouterMock } from './router/router.mock'; import { configMock } from '../config/config.mock'; diff --git a/src/core/server/http/http_service.ts b/src/core/server/http/http_service.ts index c2fd653918171..82b141c8e50dd 100644 --- a/src/core/server/http/http_service.ts +++ b/src/core/server/http/http_service.ts @@ -20,9 +20,9 @@ import { Observable, Subscription, combineLatest } from 'rxjs'; import { first, map } from 'rxjs/operators'; import { Server } from 'hapi'; +import { pick } from '@kbn/std'; import { CoreService } from '../../types'; -import { pick } from '../../utils'; import { Logger, LoggerFactory } from '../logging'; import { ContextSetup } from '../context'; import { Env } from '../config'; diff --git a/src/core/server/http/integration_tests/lifecycle_handlers.test.ts b/src/core/server/http/integration_tests/lifecycle_handlers.test.ts index a1401ba73813b..8d70b5c3ad119 100644 --- a/src/core/server/http/integration_tests/lifecycle_handlers.test.ts +++ b/src/core/server/http/integration_tests/lifecycle_handlers.test.ts @@ -20,7 +20,6 @@ import supertest from 'supertest'; import { BehaviorSubject } from 'rxjs'; import { ByteSizeValue } from '@kbn/config-schema'; -import pkg from '../../../../../package.json'; import { createHttpServer } from '../test_utils'; import { HttpService } from '../http_service'; @@ -30,6 +29,9 @@ import { IRouter, RouteRegistrar } from '../router'; import { configServiceMock } from '../../config/config_service.mock'; import { contextServiceMock } from '../../context/context_service.mock'; +// eslint-disable-next-line @typescript-eslint/no-var-requires +const pkg = require('../../../../../package.json'); + const actualVersion = pkg.version; const versionHeader = 'kbn-version'; const xsrfHeader = 'kbn-xsrf'; diff --git a/src/core/server/http/router/headers.ts b/src/core/server/http/router/headers.ts index f27f5e937b2c0..498dd153a43dd 100644 --- a/src/core/server/http/router/headers.ts +++ b/src/core/server/http/router/headers.ts @@ -17,8 +17,7 @@ * under the License. */ import { IncomingHttpHeaders } from 'http'; - -import { pick } from '../../../utils'; +import { pick } from '@kbn/std'; /** * Creates a Union type of all known keys of a given interface. diff --git a/src/core/server/http/router/request.ts b/src/core/server/http/router/request.ts index 76f8761a7e998..e04f8585981b5 100644 --- a/src/core/server/http/router/request.ts +++ b/src/core/server/http/router/request.ts @@ -23,8 +23,8 @@ import { Request, RouteOptionsApp, ApplicationState } from 'hapi'; import { Observable, fromEvent, merge } from 'rxjs'; import { shareReplay, first, takeUntil } from 'rxjs/operators'; import { RecursiveReadonly } from '@kbn/utility-types'; +import { deepFreeze } from '@kbn/std'; -import { deepFreeze } from '../../../utils'; import { Headers } from './headers'; import { RouteMethod, RouteConfigOptions, validBodyOutput, isSafeMethod } from './route'; import { KibanaSocket, IKibanaSocket } from './socket'; diff --git a/src/core/server/index.ts b/src/core/server/index.ts index d127471348d9f..01797d073ae2e 100644 --- a/src/core/server/index.ts +++ b/src/core/server/index.ts @@ -322,16 +322,7 @@ export { MetricsServiceSetup, } from './metrics'; -export { - DEFAULT_APP_CATEGORIES, - getFlattenedObject, - URLMeaningfulParts, - modifyUrl, - isRelativeUrl, - Freezable, - deepFreeze, - assertNever, -} from '../utils'; +export { DEFAULT_APP_CATEGORIES } from '../utils'; export { SavedObject, diff --git a/src/core/server/legacy/cli.js b/src/core/server/legacy/cli.js new file mode 100644 index 0000000000000..28e14d28eecd3 --- /dev/null +++ b/src/core/server/legacy/cli.js @@ -0,0 +1,20 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export { startRepl } from '../../../cli/repl'; diff --git a/src/core/server/legacy/cluster_manager.js b/src/core/server/legacy/cluster_manager.js new file mode 100644 index 0000000000000..3c51fd6869a09 --- /dev/null +++ b/src/core/server/legacy/cluster_manager.js @@ -0,0 +1,20 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export { ClusterManager } from '../../../cli/cluster/cluster_manager'; diff --git a/src/core/server/legacy/config/get_unused_config_keys.ts b/src/core/server/legacy/config/get_unused_config_keys.ts index d10c574f04974..c15c3b270df05 100644 --- a/src/core/server/legacy/config/get_unused_config_keys.ts +++ b/src/core/server/legacy/config/get_unused_config_keys.ts @@ -18,8 +18,8 @@ */ import { difference } from 'lodash'; +import { getFlattenedObject } from '@kbn/std'; import { unset } from '../../../../legacy/utils'; -import { getFlattenedObject } from '../../../utils'; import { hasConfigPathIntersection } from '../../config'; import { LegacyPluginSpec, LegacyConfig, LegacyVars } from '../types'; diff --git a/src/core/server/legacy/legacy_service.mock.ts b/src/core/server/legacy/legacy_service.mock.ts index c27f5be04d965..ab501bd6bb53b 100644 --- a/src/core/server/legacy/legacy_service.mock.ts +++ b/src/core/server/legacy/legacy_service.mock.ts @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ - +import type { PublicMethodsOf } from '@kbn/utility-types'; import { LegacyService } from './legacy_service'; import { LegacyConfig, LegacyServiceDiscoverPlugins, LegacyServiceSetupDeps } from './types'; diff --git a/src/core/server/legacy/legacy_service.test.ts b/src/core/server/legacy/legacy_service.test.ts index d0492ea88c5e8..f3ce89f83a610 100644 --- a/src/core/server/legacy/legacy_service.test.ts +++ b/src/core/server/legacy/legacy_service.test.ts @@ -18,8 +18,7 @@ */ jest.mock('../../../legacy/server/kbn_server'); -jest.mock('../../../cli/cluster/cluster_manager'); - +jest.mock('./cluster_manager'); import { findLegacyPluginSpecsMock, logLegacyThirdPartyPluginDeprecationWarningMock, @@ -27,7 +26,8 @@ import { import { BehaviorSubject, throwError } from 'rxjs'; -import { ClusterManager as MockClusterManager } from '../../../cli/cluster/cluster_manager'; +// @ts-expect-error js file to remove TS dependency on cli +import { ClusterManager as MockClusterManager } from './cluster_manager'; import KbnServer from '../../../legacy/server/kbn_server'; import { Config, Env, ObjectToConfigAdapter } from '../config'; import { getEnvOptions } from '../config/__mocks__/env'; diff --git a/src/core/server/legacy/legacy_service.ts b/src/core/server/legacy/legacy_service.ts index 1ad874fef6920..fd3e3a694e6ae 100644 --- a/src/core/server/legacy/legacy_service.ts +++ b/src/core/server/legacy/legacy_service.ts @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ - +import type { PublicMethodsOf } from '@kbn/utility-types'; import { combineLatest, ConnectableObservable, EMPTY, Observable, Subscription } from 'rxjs'; import { first, map, publishReplay, tap } from 'rxjs/operators'; @@ -233,7 +233,7 @@ export class LegacyService implements CoreService { : EMPTY; // eslint-disable-next-line @typescript-eslint/no-var-requires - const { ClusterManager } = require('../../../cli/cluster/cluster_manager'); + const { ClusterManager } = require('./cluster_manager'); return new ClusterManager( this.coreContext.env.cliArgs, config, @@ -368,7 +368,7 @@ export class LegacyService implements CoreService { // We only want one REPL. if (this.coreContext.env.cliArgs.repl && process.env.kbnWorkerType === 'server') { // eslint-disable-next-line @typescript-eslint/no-var-requires - require('../../../cli/repl').startRepl(kbnServer); + require('./cli').startRepl(kbnServer); } const { autoListen } = await this.httpConfig$.pipe(first()).toPromise(); diff --git a/src/core/server/legacy/plugins/collect_ui_exports.js b/src/core/server/legacy/plugins/collect_ui_exports.js new file mode 100644 index 0000000000000..842ab554d79d1 --- /dev/null +++ b/src/core/server/legacy/plugins/collect_ui_exports.js @@ -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. + */ + +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +export { collectUiExports } from '../../../../legacy/ui/ui_exports/collect_ui_exports'; diff --git a/src/core/server/legacy/plugins/find_legacy_plugin_specs.ts b/src/core/server/legacy/plugins/find_legacy_plugin_specs.ts index 82e04496ffc3e..cb4277b130a88 100644 --- a/src/core/server/legacy/plugins/find_legacy_plugin_specs.ts +++ b/src/core/server/legacy/plugins/find_legacy_plugin_specs.ts @@ -25,12 +25,12 @@ import { defaultConfig, // @ts-expect-error } from '../../../../legacy/plugin_discovery/find_plugin_specs.js'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { collectUiExports as collectLegacyUiExports } from '../../../../legacy/ui/ui_exports/collect_ui_exports'; +// @ts-expect-error +import { collectUiExports as collectLegacyUiExports } from './collect_ui_exports'; import { LoggerFactory } from '../../logging'; import { PackageInfo } from '../../config'; -import { LegacyPluginSpec, LegacyPluginPack, LegacyConfig } from '../types'; +import { LegacyUiExports, LegacyPluginSpec, LegacyPluginPack, LegacyConfig } from '../types'; export async function findLegacyPluginSpecs( settings: unknown, @@ -123,7 +123,7 @@ export async function findLegacyPluginSpecs( spec$.pipe(toArray()), log$.pipe(toArray()) ).toPromise(); - const uiExports = collectLegacyUiExports(pluginSpecs); + const uiExports: LegacyUiExports = collectLegacyUiExports(pluginSpecs); return { disabledPluginSpecs, diff --git a/src/core/server/logging/appenders/appenders.ts b/src/core/server/logging/appenders/appenders.ts index edfce4988275a..9c19ee2bd8be5 100644 --- a/src/core/server/logging/appenders/appenders.ts +++ b/src/core/server/logging/appenders/appenders.ts @@ -18,8 +18,8 @@ */ import { schema } from '@kbn/config-schema'; +import { assertNever } from '@kbn/std'; -import { assertNever } from '../../../utils'; import { LegacyAppender, LegacyAppenderConfig, diff --git a/src/core/server/logging/layouts/layouts.ts b/src/core/server/logging/layouts/layouts.ts index 124c007bae104..03e8adbee6311 100644 --- a/src/core/server/logging/layouts/layouts.ts +++ b/src/core/server/logging/layouts/layouts.ts @@ -18,8 +18,8 @@ */ import { schema } from '@kbn/config-schema'; +import { assertNever } from '@kbn/std'; -import { assertNever } from '../../../utils'; import { LogRecord } from '../log_record'; import { JsonLayout, JsonLayoutConfigType } from './json_layout'; import { PatternLayout, PatternLayoutConfigType } from './pattern_layout'; diff --git a/src/core/server/logging/log_level.ts b/src/core/server/logging/log_level.ts index 577239ddae8e5..165e56e632d6d 100644 --- a/src/core/server/logging/log_level.ts +++ b/src/core/server/logging/log_level.ts @@ -17,7 +17,7 @@ * under the License. */ -import { assertNever } from '../../utils'; +import { assertNever } from '@kbn/std'; /** * Possible log level string values. diff --git a/src/core/server/logging/logging_service.mock.ts b/src/core/server/logging/logging_service.mock.ts index 21edbe670eaec..6a721e134feed 100644 --- a/src/core/server/logging/logging_service.mock.ts +++ b/src/core/server/logging/logging_service.mock.ts @@ -16,6 +16,7 @@ * specific language governing permissions and limitations * under the License. */ +import type { PublicMethodsOf } from '@kbn/utility-types'; import { LoggingService, diff --git a/src/core/server/logging/logging_system.ts b/src/core/server/logging/logging_system.ts index 8aadab83bf716..a3970b1720950 100644 --- a/src/core/server/logging/logging_system.ts +++ b/src/core/server/logging/logging_system.ts @@ -16,6 +16,7 @@ * specific language governing permissions and limitations * under the License. */ +import type { PublicMethodsOf } from '@kbn/utility-types'; import { Appenders, DisposableAppender } from './appenders/appenders'; import { BufferAppender } from './appenders/buffer/buffer_appender'; import { LogLevel } from './log_level'; diff --git a/src/core/server/metrics/metrics_service.mock.ts b/src/core/server/metrics/metrics_service.mock.ts index 2af653004a479..caa7acc001db3 100644 --- a/src/core/server/metrics/metrics_service.mock.ts +++ b/src/core/server/metrics/metrics_service.mock.ts @@ -17,6 +17,8 @@ * under the License. */ import { BehaviorSubject } from 'rxjs'; +import type { PublicMethodsOf } from '@kbn/utility-types'; + import { MetricsService } from './metrics_service'; import { InternalMetricsServiceSetup, diff --git a/src/core/server/plugins/plugin_context.ts b/src/core/server/plugins/plugin_context.ts index 6c8be76302633..8d17300965680 100644 --- a/src/core/server/plugins/plugin_context.ts +++ b/src/core/server/plugins/plugin_context.ts @@ -20,6 +20,7 @@ import { map, shareReplay } from 'rxjs/operators'; import { combineLatest } from 'rxjs'; import { PathConfigType, config as pathConfig } from '@kbn/utils'; +import { pick, deepFreeze } from '@kbn/std'; import { CoreContext } from '../core_context'; import { PluginWrapper } from './plugin'; import { PluginsServiceSetupDeps, PluginsServiceStartDeps } from './plugins_service'; @@ -34,7 +35,6 @@ import { ElasticsearchConfigType, config as elasticsearchConfig, } from '../elasticsearch/elasticsearch_config'; -import { pick, deepFreeze } from '../../utils'; import { CoreSetup, CoreStart } from '..'; export interface InstanceInfo { diff --git a/src/core/server/plugins/plugins_service.mock.ts b/src/core/server/plugins/plugins_service.mock.ts index a40566767ddae..14d6de889dd42 100644 --- a/src/core/server/plugins/plugins_service.mock.ts +++ b/src/core/server/plugins/plugins_service.mock.ts @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ - +import type { PublicMethodsOf } from '@kbn/utility-types'; import { PluginsService, PluginsServiceSetup } from './plugins_service'; type PluginsServiceMock = jest.Mocked>; diff --git a/src/core/server/plugins/plugins_service.ts b/src/core/server/plugins/plugins_service.ts index 30cd47c9d44e1..e8fe42ee491ca 100644 --- a/src/core/server/plugins/plugins_service.ts +++ b/src/core/server/plugins/plugins_service.ts @@ -20,9 +20,10 @@ import Path from 'path'; import { Observable } from 'rxjs'; import { filter, first, map, mergeMap, tap, toArray } from 'rxjs/operators'; +import { pick } from '@kbn/std'; + import { CoreService } from '../../types'; import { CoreContext } from '../core_context'; - import { Logger } from '../logging'; import { discover, PluginDiscoveryError, PluginDiscoveryErrorType } from './discovery'; import { PluginWrapper } from './plugin'; @@ -31,7 +32,6 @@ import { PluginsConfig, PluginsConfigType } from './plugins_config'; import { PluginsSystem } from './plugins_system'; import { InternalCoreSetup, InternalCoreStart } from '../internal_types'; import { IConfigService } from '../config'; -import { pick } from '../../utils'; import { InternalEnvironmentServiceSetup } from '../environment'; /** @internal */ diff --git a/src/core/server/plugins/plugins_system.ts b/src/core/server/plugins/plugins_system.ts index b2acd9a6fd04b..72d2cfe158b37 100644 --- a/src/core/server/plugins/plugins_system.ts +++ b/src/core/server/plugins/plugins_system.ts @@ -17,13 +17,13 @@ * under the License. */ +import { withTimeout } from '@kbn/std'; import { CoreContext } from '../core_context'; import { Logger } from '../logging'; import { PluginWrapper } from './plugin'; import { DiscoveredPlugin, PluginName } from './types'; import { createPluginSetupContext, createPluginStartContext } from './plugin_context'; import { PluginsServiceSetupDeps, PluginsServiceStartDeps } from './plugins_service'; -import { withTimeout } from '../../utils'; import { PluginDependencies } from '.'; const Sec = 1000; diff --git a/src/core/server/rendering/__mocks__/rendering_service.ts b/src/core/server/rendering/__mocks__/rendering_service.ts index 2e35568873c9a..179a09b8619b0 100644 --- a/src/core/server/rendering/__mocks__/rendering_service.ts +++ b/src/core/server/rendering/__mocks__/rendering_service.ts @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ - +import type { PublicMethodsOf } from '@kbn/utility-types'; import { RenderingService as Service } from '../rendering_service'; import { InternalRenderingServiceSetup } from '../types'; import { mockRenderingServiceParams } from './params'; diff --git a/src/core/server/saved_objects/es_query.js b/src/core/server/saved_objects/es_query.js new file mode 100644 index 0000000000000..68d582e3cae09 --- /dev/null +++ b/src/core/server/saved_objects/es_query.js @@ -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. + */ +// a temporary file to remove circular deps in TS code between platform & data plugin +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +export { esKuery } from '../../../plugins/data/server'; diff --git a/src/core/server/saved_objects/migrations/kibana/kibana_migrator.mock.ts b/src/core/server/saved_objects/migrations/kibana/kibana_migrator.mock.ts index fae33bc050dee..23d8c4518d3ab 100644 --- a/src/core/server/saved_objects/migrations/kibana/kibana_migrator.mock.ts +++ b/src/core/server/saved_objects/migrations/kibana/kibana_migrator.mock.ts @@ -16,6 +16,7 @@ * specific language governing permissions and limitations * under the License. */ +import type { PublicMethodsOf } from '@kbn/utility-types'; import { KibanaMigrator, KibanaMigratorStatus } from './kibana_migrator'; import { buildActiveMappings } from '../core'; 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 e3d44c20dd190..bd76658c21731 100644 --- a/src/core/server/saved_objects/saved_objects_service.mock.ts +++ b/src/core/server/saved_objects/saved_objects_service.mock.ts @@ -18,6 +18,7 @@ */ import { BehaviorSubject } from 'rxjs'; +import type { PublicMethodsOf } from '@kbn/utility-types'; import { SavedObjectsService, diff --git a/src/core/server/saved_objects/saved_objects_type_registry.ts b/src/core/server/saved_objects/saved_objects_type_registry.ts index d0035294226ea..bb840e459bf22 100644 --- a/src/core/server/saved_objects/saved_objects_type_registry.ts +++ b/src/core/server/saved_objects/saved_objects_type_registry.ts @@ -17,7 +17,7 @@ * under the License. */ -import { deepFreeze } from '../../utils'; +import { deepFreeze } from '@kbn/std'; import { SavedObjectsType } from './types'; /** diff --git a/src/core/server/saved_objects/service/lib/filter_utils.test.ts b/src/core/server/saved_objects/service/lib/filter_utils.test.ts index 60e8aa0afdda4..0608035ce51a2 100644 --- a/src/core/server/saved_objects/service/lib/filter_utils.test.ts +++ b/src/core/server/saved_objects/service/lib/filter_utils.test.ts @@ -16,8 +16,8 @@ * specific language governing permissions and limitations * under the License. */ -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { esKuery } from '../../../../../plugins/data/server'; +// @ts-expect-error no ts +import { esKuery } from '../../es_query'; import { validateFilterKueryNode, validateConvertFilterToKueryNode } from './filter_utils'; diff --git a/src/core/server/saved_objects/service/lib/filter_utils.ts b/src/core/server/saved_objects/service/lib/filter_utils.ts index d19f06d74e419..be36807f0d02b 100644 --- a/src/core/server/saved_objects/service/lib/filter_utils.ts +++ b/src/core/server/saved_objects/service/lib/filter_utils.ts @@ -21,8 +21,9 @@ import { set } from '@elastic/safer-lodash-set'; import { get } from 'lodash'; import { SavedObjectsErrorHelpers } from './errors'; import { IndexMapping } from '../../mappings'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { esKuery, KueryNode } from '../../../../../plugins/data/server'; +// @ts-expect-error no ts +import { esKuery } from '../../es_query'; +type KueryNode = any; const astFunctionType = ['is', 'range', 'nested']; diff --git a/src/core/server/saved_objects/service/lib/repository.test.js b/src/core/server/saved_objects/service/lib/repository.test.js index 7d30875b90796..352ce4c1c16eb 100644 --- a/src/core/server/saved_objects/service/lib/repository.test.js +++ b/src/core/server/saved_objects/service/lib/repository.test.js @@ -25,8 +25,8 @@ import { encodeHitVersion } from '../../version'; import { SavedObjectTypeRegistry } from '../../saved_objects_type_registry'; import { DocumentMigrator } from '../../migrations/core/document_migrator'; import { elasticsearchClientMock } from '../../../elasticsearch/client/mocks'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { nodeTypes } from '../../../../../plugins/data/common/es_query'; +import { esKuery } from '../../es_query'; +const { nodeTypes } = esKuery; jest.mock('./search_dsl/search_dsl', () => ({ getSearchDsl: jest.fn() })); 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 85c47029e36d5..4adc92df31805 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 @@ -17,8 +17,9 @@ * under the License. */ -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { esKuery, KueryNode } from '../../../../../../plugins/data/server'; +// @ts-expect-error no ts +import { esKuery } from '../../../es_query'; +type KueryNode = any; import { typeRegistryMock } from '../../../saved_objects_type_registry.mock'; import { getQueryParams } from './query_params'; 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 3ff72a86c2f89..642d51c70766e 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 @@ -16,8 +16,9 @@ * specific language governing permissions and limitations * under the License. */ -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { esKuery, KueryNode } from '../../../../../../plugins/data/server'; +// @ts-expect-error no ts +import { esKuery } from '../../../es_query'; +type KueryNode = any; import { getRootPropertiesObjects, IndexMapping } from '../../../mappings'; import { ISavedObjectTypeRegistry } from '../../../saved_objects_type_registry'; diff --git a/src/core/server/saved_objects/service/lib/search_dsl/search_dsl.ts b/src/core/server/saved_objects/service/lib/search_dsl/search_dsl.ts index ddf20606800c8..aa79a10b2a9be 100644 --- a/src/core/server/saved_objects/service/lib/search_dsl/search_dsl.ts +++ b/src/core/server/saved_objects/service/lib/search_dsl/search_dsl.ts @@ -22,10 +22,10 @@ import Boom from 'boom'; import { IndexMapping } from '../../../mappings'; import { getQueryParams } from './query_params'; import { getSortingParams } from './sorting_params'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { KueryNode } from '../../../../../../plugins/data/server'; import { ISavedObjectTypeRegistry } from '../../../saved_objects_type_registry'; +type KueryNode = any; + interface GetSearchDslOptions { type: string | string[]; search?: string; diff --git a/src/core/server/saved_objects/types.ts b/src/core/server/saved_objects/types.ts index 50c118ca64ffb..1885f5ec50139 100644 --- a/src/core/server/saved_objects/types.ts +++ b/src/core/server/saved_objects/types.ts @@ -35,8 +35,7 @@ export { import { SavedObject } from '../../types'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { KueryNode } from '../../../plugins/data/common'; +type KueryNode = any; export { SavedObjectAttributes, diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index 5053d1cd646f1..05eb1b8d42386 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -40,7 +40,6 @@ import { DeleteScriptParams } from 'elasticsearch'; import { DeleteTemplateParams } from 'elasticsearch'; import { DetailedPeerCertificate } from 'tls'; import { Duration } from 'moment'; -import { ErrorToastOptions } from 'src/core/public/notifications'; import { ExistsParams } from 'elasticsearch'; import { ExplainParams } from 'elasticsearch'; import { FieldStatsParams } from 'elasticsearch'; @@ -106,7 +105,6 @@ import { NodesInfoParams } from 'elasticsearch'; import { NodesStatsParams } from 'elasticsearch'; import { ObjectType } from '@kbn/config-schema'; import { Observable } from 'rxjs'; -import { ParsedQuery } from 'query-string'; import { PathConfigType } from '@kbn/utils'; import { PeerCertificate } from 'tls'; import { PingParams } from 'elasticsearch'; @@ -120,7 +118,6 @@ import { RenderSearchTemplateParams } from 'elasticsearch'; import { Request } from 'hapi'; import { ResponseObject } from 'hapi'; import { ResponseToolkit } from 'hapi'; -import { SavedObject as SavedObject_2 } from 'src/core/server'; import { SchemaTypeError } from '@kbn/config-schema'; import { ScrollParams } from 'elasticsearch'; import { SearchParams } from 'elasticsearch'; @@ -144,7 +141,6 @@ import { TasksCancelParams } from 'elasticsearch'; import { TasksGetParams } from 'elasticsearch'; import { TasksListParams } from 'elasticsearch'; import { TermvectorsParams } from 'elasticsearch'; -import { ToastInputFields } from 'src/core/public/notifications'; import { TransportRequestOptions } from '@elastic/elasticsearch/lib/Transport'; import { TransportRequestParams } from '@elastic/elasticsearch/lib/Transport'; import { TransportRequestPromise } from '@elastic/elasticsearch/lib/Transport'; @@ -161,9 +157,6 @@ import { Url } from 'url'; // @public (undocumented) export type AppenderConfigType = ConsoleAppenderConfig | FileAppenderConfig | LegacyAppenderConfig; -// @public -export function assertNever(x: never): never; - // @public @deprecated (undocumented) export interface AssistanceAPIResponse { // (undocumented) @@ -505,9 +498,6 @@ export interface CustomHttpResponseOptions(object: T): RecursiveReadonly; - // @internal (undocumented) export const DEFAULT_APP_CATEGORIES: Readonly<{ kibana: { @@ -717,11 +707,6 @@ export interface FakeRequest { headers: Headers; } -// @public (undocumented) -export type Freezable = { - [k: string]: any; -} | any[]; - // @public export type GetAuthHeaders = (request: KibanaRequest | LegacyRequest) => AuthHeaders | undefined; @@ -731,11 +716,6 @@ export type GetAuthState = (request: KibanaRequest | LegacyRequest) state: T; }; -// @public -export function getFlattenedObject(rootValue: Record): { - [key: string]: any; -}; - // @public (undocumented) export interface GetResponse { // (undocumented) @@ -967,9 +947,6 @@ export interface IScopedClusterClient { readonly asInternalUser: ElasticsearchClient; } -// @public -export function isRelativeUrl(candidatePath: string): boolean; - // @public export interface IUiSettingsClient { get: (key: string) => Promise; @@ -1544,9 +1521,6 @@ export type MIGRATION_ASSISTANCE_INDEX_ACTION = 'upgrade' | 'reindex'; // @public @deprecated (undocumented) export type MIGRATION_DEPRECATION_LEVEL = 'none' | 'info' | 'warning' | 'critical'; -// @public -export function modifyUrl(url: string, urlModifier: (urlParts: URLMeaningfulParts) => Partial | void): string; - // @public export type MutatingOperationRefreshSetting = boolean | 'wait_for'; @@ -2849,26 +2823,6 @@ export interface UiSettingsServiceStart { // @public export type UiSettingsType = 'undefined' | 'json' | 'markdown' | 'number' | 'select' | 'boolean' | 'string' | 'array' | 'image'; -// @public -export interface URLMeaningfulParts { - // (undocumented) - auth?: string | null; - // (undocumented) - hash?: string | null; - // (undocumented) - hostname?: string | null; - // (undocumented) - pathname?: string | null; - // (undocumented) - port?: string | null; - // (undocumented) - protocol?: string | null; - // (undocumented) - query: ParsedQuery; - // (undocumented) - slashes?: boolean | null; -} - // @public export interface UserProvidedValues { // (undocumented) diff --git a/src/core/server/server.ts b/src/core/server/server.ts index 2316ebf250e8f..c689e2cb70cc9 100644 --- a/src/core/server/server.ts +++ b/src/core/server/server.ts @@ -18,6 +18,7 @@ */ 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 { AuditTrailService } from './audit_trail'; @@ -32,7 +33,7 @@ import { PluginsService, config as pluginsConfig } from './plugins'; import { SavedObjectsService } from '../server/saved_objects'; import { MetricsService, opsConfig } from './metrics'; import { CapabilitiesService } from './capabilities'; -import { EnvironmentService } from './environment'; +import { EnvironmentService, config as pidConfig } from './environment'; import { StatusService } from './status/status_service'; import { config as cspConfig } from './csp'; @@ -44,7 +45,6 @@ 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 { mapToObject } from '../utils'; import { ContextService } from './context'; import { RequestHandlerContext } from '.'; import { InternalCoreSetup, InternalCoreStart, ServiceConfigDescriptor } from './internal_types'; @@ -310,6 +310,7 @@ export class Server { uiSettingsConfig, opsConfig, statusConfig, + pidConfig, ]; 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 42b3eecdca310..930ee2970cf55 100644 --- a/src/core/server/status/status_service.mock.ts +++ b/src/core/server/status/status_service.mock.ts @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ - +import type { PublicMethodsOf } from '@kbn/utility-types'; import { StatusService } from './status_service'; import { InternalStatusServiceSetup, diff --git a/src/core/server/status/types.ts b/src/core/server/status/types.ts index f884b80316fa8..9fa33a8c6d40c 100644 --- a/src/core/server/status/types.ts +++ b/src/core/server/status/types.ts @@ -18,7 +18,7 @@ */ import { Observable } from 'rxjs'; -import { deepFreeze } from '../../utils'; +import { deepFreeze } from '@kbn/std'; import { PluginName } from '../plugins'; /** diff --git a/src/core/server/ui_settings/settings/navigation.ts b/src/core/server/ui_settings/settings/navigation.ts index 6483e86a1395a..ec825a2779f38 100644 --- a/src/core/server/ui_settings/settings/navigation.ts +++ b/src/core/server/ui_settings/settings/navigation.ts @@ -19,8 +19,8 @@ import { schema } from '@kbn/config-schema'; import { i18n } from '@kbn/i18n'; +import { isRelativeUrl } from '@kbn/std'; import { UiSettingsParams } from '../../../types'; -import { isRelativeUrl } from '../../../utils'; export const getNavigationSettings = (): Record => { return { 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 83cea6d7ab3e2..b1ed0dd188cde 100644 --- a/src/core/server/ui_settings/ui_settings_service.mock.ts +++ b/src/core/server/ui_settings/ui_settings_service.mock.ts @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ - +import type { PublicMethodsOf } from '@kbn/utility-types'; import { IUiSettingsClient, InternalUiSettingsServiceSetup, diff --git a/src/core/server/ui_settings/ui_settings_service.ts b/src/core/server/ui_settings/ui_settings_service.ts index 8598cf7a62287..25062490f5b6b 100644 --- a/src/core/server/ui_settings/ui_settings_service.ts +++ b/src/core/server/ui_settings/ui_settings_service.ts @@ -19,10 +19,11 @@ import { Observable } from 'rxjs'; import { first } from 'rxjs/operators'; +import { mapToObject } from '@kbn/std'; + import { CoreService } from '../../types'; import { CoreContext } from '../core_context'; import { Logger } from '../logging'; - import { SavedObjectsClientContract } from '../saved_objects/types'; import { InternalSavedObjectsServiceSetup } from '../saved_objects'; import { InternalHttpServiceSetup } from '../http'; @@ -33,7 +34,6 @@ import { InternalUiSettingsServiceStart, UiSettingsParams, } from './types'; -import { mapToObject } from '../../utils/'; import { uiSettingsType } from './saved_objects'; import { registerRoutes } from './routes'; import { getCoreSettings } from './settings'; diff --git a/src/core/tsconfig.json b/src/core/tsconfig.json new file mode 100644 index 0000000000000..b8780321e11dd --- /dev/null +++ b/src/core/tsconfig.json @@ -0,0 +1,22 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "composite": true, + "outDir": "./target", + "emitDeclarationOnly": true, + "declaration": true, + "declarationMap": true + }, + "include": [ + "public/**/*", + "server/**/*", + "types/**/*", + "test_helpers/**/*", + "utils/**/*", + "index.ts", + "typings.ts" + ], + "references": [ + { "path": "../test_utils/" } + ] +} diff --git a/src/core/typings.ts b/src/core/typings.ts new file mode 100644 index 0000000000000..a84e1c01d2bd2 --- /dev/null +++ b/src/core/typings.ts @@ -0,0 +1,55 @@ +/* + * 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. + */ + +declare module 'query-string' { + type ArrayFormat = 'bracket' | 'index' | 'none'; + + export interface ParseOptions { + arrayFormat?: ArrayFormat; + sort: ((itemLeft: string, itemRight: string) => number) | false; + } + + export interface ParsedQuery { + [key: string]: T | T[] | null | undefined; + } + + export function parse(str: string, options?: ParseOptions): ParsedQuery; + + export function parseUrl(str: string, options?: ParseOptions): { url: string; query: any }; + + export interface StringifyOptions { + strict?: boolean; + encode?: boolean; + arrayFormat?: ArrayFormat; + sort: ((itemLeft: string, itemRight: string) => number) | false; + } + + export function stringify(obj: object, options?: StringifyOptions): string; + + export function extract(str: string): string; +} + +type DeeplyMockedKeys = { + [P in keyof T]: T[P] extends (...args: any[]) => any + ? jest.MockInstance, Parameters> + : DeeplyMockedKeys; +} & + T; + +type MockedKeys = { [P in keyof T]: jest.Mocked }; diff --git a/src/core/utils/context.ts b/src/core/utils/context.ts index 941bbceb0cd92..f28d3330b8e36 100644 --- a/src/core/utils/context.ts +++ b/src/core/utils/context.ts @@ -19,8 +19,8 @@ import { flatten } from 'lodash'; import { ShallowPromise } from '@kbn/utility-types'; -import { pick } from '.'; -import { CoreId, PluginOpaqueId } from '../server'; +import { pick } from '@kbn/std'; +import type { CoreId, PluginOpaqueId } from '../server'; /** * Make all properties in T optional, except for the properties whose keys are in the union K diff --git a/src/core/utils/index.ts b/src/core/utils/index.ts index a6df0992f6cc6..c620e4e5ee155 100644 --- a/src/core/utils/index.ts +++ b/src/core/utils/index.ts @@ -17,15 +17,12 @@ * under the License. */ -export * from './assert_never'; -export * from './context'; -export * from './deep_freeze'; -export * from './get'; -export * from './map_to_object'; -export * from './merge'; -export * from './pick'; -export * from './promise'; -export * from './url'; -export * from './unset'; -export * from './get_flattened_object'; -export * from './default_app_categories'; +export { + ContextContainer, + HandlerContextType, + HandlerFunction, + HandlerParameters, + IContextContainer, + IContextProvider, +} from './context'; +export { DEFAULT_APP_CATEGORIES } from './default_app_categories'; diff --git a/src/dev/typescript/projects.ts b/src/dev/typescript/projects.ts index a1a94b668173d..4d1e549e192b6 100644 --- a/src/dev/typescript/projects.ts +++ b/src/dev/typescript/projects.ts @@ -25,6 +25,7 @@ import { Project } from './project'; export const PROJECTS = [ new Project(resolve(REPO_ROOT, 'tsconfig.json')), new Project(resolve(REPO_ROOT, 'src/test_utils/tsconfig.json')), + new Project(resolve(REPO_ROOT, 'src/core/tsconfig.json')), new Project(resolve(REPO_ROOT, 'test/tsconfig.json'), { name: 'kibana/test' }), new Project(resolve(REPO_ROOT, 'x-pack/tsconfig.json')), new Project(resolve(REPO_ROOT, 'x-pack/test/tsconfig.json'), { name: 'x-pack/test' }), diff --git a/src/dev/typescript/run_type_check_cli.ts b/src/dev/typescript/run_type_check_cli.ts index e1fca23274a5a..00968b7259a30 100644 --- a/src/dev/typescript/run_type_check_cli.ts +++ b/src/dev/typescript/run_type_check_cli.ts @@ -98,7 +98,7 @@ export async function runTypeCheckCli() { } execInProjects(log, projects, process.execPath, (project) => [ - '--max-old-space-size=4096', + '--max-old-space-size=5120', require.resolve('typescript/bin/tsc'), ...['--project', project.tsConfigPath], ...tscArgs, diff --git a/src/legacy/server/config/schema.js b/src/legacy/server/config/schema.js index ce7a500a00dc8..f8736fb30f90e 100644 --- a/src/legacy/server/config/schema.js +++ b/src/legacy/server/config/schema.js @@ -42,10 +42,7 @@ export default () => basePathProxyTarget: Joi.number().default(5603), }).default(), - pid: Joi.object({ - file: Joi.string(), - exclusive: Joi.boolean().default(false), - }).default(), + pid: HANDLED_IN_NEW_PLATFORM, csp: HANDLED_IN_NEW_PLATFORM, diff --git a/src/legacy/server/i18n/index.ts b/src/legacy/server/i18n/index.ts index e895f83fe6901..cb86c3220bec1 100644 --- a/src/legacy/server/i18n/index.ts +++ b/src/legacy/server/i18n/index.ts @@ -21,6 +21,7 @@ import { i18n, i18nLoader } from '@kbn/i18n'; import { basename } from 'path'; import { Server } from 'hapi'; import { fromRoot } from '../../../core/server/utils'; +import type { UsageCollectionSetup } from '../../../plugins/usage_collection/server'; import { getTranslationPaths } from './get_translations_path'; import { I18N_RC } from './constants'; import KbnServer, { KibanaConfig } from '../kbn_server'; @@ -64,7 +65,10 @@ export async function i18nMixin(kbnServer: KbnServer, server: Server, config: Ki server.decorate('server', 'getTranslationsFilePaths', getTranslationsFilePaths); if (kbnServer.newPlatform.setup.plugins.usageCollection) { - registerLocalizationUsageCollector(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.d.ts b/src/legacy/server/kbn_server.d.ts index 663542618375a..8827dc53c5275 100644 --- a/src/legacy/server/kbn_server.d.ts +++ b/src/legacy/server/kbn_server.d.ts @@ -19,7 +19,6 @@ import { Server } from 'hapi'; -import { TelemetryCollectionManagerPluginSetup } from 'src/plugins/telemetry_collection_manager/server'; import { CoreSetup, CoreStart, @@ -35,8 +34,6 @@ import { LegacyConfig, ILegacyInternals } from '../../core/server/legacy'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { UiPlugins } from '../../core/server/plugins'; import { ElasticsearchPlugin } from '../core_plugins/elasticsearch'; -import { UsageCollectionSetup } from '../../plugins/usage_collection/server'; -import { HomeServerPluginSetup } from '../../plugins/home/server'; // lot of legacy code was assuming this type only had these two methods export type KibanaConfig = Pick; @@ -60,9 +57,6 @@ declare module 'hapi' { type KbnMixinFunc = (kbnServer: KbnServer, server: Server, config: any) => Promise | void; export interface PluginsSetup { - usageCollection: UsageCollectionSetup; - telemetryCollectionManager: TelemetryCollectionManagerPluginSetup; - home: HomeServerPluginSetup; [key: string]: object; } diff --git a/src/legacy/server/kbn_server.js b/src/legacy/server/kbn_server.js index a5eefd140c8fa..24d00abb99a05 100644 --- a/src/legacy/server/kbn_server.js +++ b/src/legacy/server/kbn_server.js @@ -29,7 +29,6 @@ import { coreMixin } from './core'; import { loggingMixin } from './logging'; import warningsMixin from './warnings'; import { statusMixin } from './status'; -import pidMixin from './pid'; import configCompleteMixin from './config/complete'; import { optimizeMixin } from '../../optimize'; import * as Plugins from './plugins'; @@ -93,9 +92,6 @@ export default class KbnServer { warningsMixin, statusMixin, - // writes pid file - pidMixin, - // scan translations dirs, register locale files and initialize i18n engine. i18nMixin, diff --git a/src/legacy/server/pid/index.js b/src/legacy/server/pid/index.js deleted file mode 100644 index d7b9da1292252..0000000000000 --- a/src/legacy/server/pid/index.js +++ /dev/null @@ -1,73 +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 _ from 'lodash'; -import Boom from 'boom'; -import Bluebird from 'bluebird'; -import { unlinkSync as unlink } from 'fs'; -const writeFile = Bluebird.promisify(require('fs').writeFile); - -export default Bluebird.method(function (kbnServer, server, config) { - const path = config.get('pid.file'); - if (!path) return; - - const pid = String(process.pid); - - return writeFile(path, pid, { flag: 'wx' }) - .catch(function (err) { - if (err.code !== 'EEXIST') throw err; - - const message = `pid file already exists at ${path}`; - const metadata = { - path: path, - pid: pid, - }; - - if (config.get('pid.exclusive')) { - throw Boom.internal(message, { message, ...metadata }); - } else { - server.log(['pid', 'warning'], message, metadata); - } - - return writeFile(path, pid); - }) - .then(function () { - server.logWithMetadata(['pid', 'debug'], `wrote pid file to ${path}`, { - path: path, - pid: pid, - }); - - const clean = _.once(function () { - unlink(path); - }); - - process.once('exit', clean); // for "natural" exits - process.once('SIGINT', function () { - // for Ctrl-C exits - clean(); - - // resend SIGINT - process.kill(process.pid, 'SIGINT'); - }); - - process.on('unhandledRejection', function (reason) { - server.log(['warning'], `Detected an unhandled Promise rejection.\n${reason}`); - }); - }); -}); 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 bbc3f3632bf64..8c9e3847844d9 100644 --- a/src/plugins/advanced_settings/public/management_app/advanced_settings.tsx +++ b/src/plugins/advanced_settings/public/management_app/advanced_settings.tsx @@ -121,7 +121,7 @@ export class AdvancedSettingsComponent extends Component< setTimeout(() => { const id = hash.replace('#', ''); const element = document.getElementById(id); - const globalNavOffset = document.getElementById('headerGlobalNav')?.offsetHeight || 0; + const globalNavOffset = document.getElementById('globalHeaderBars')?.offsetHeight || 0; if (element) { element.scrollIntoView(); diff --git a/src/plugins/advanced_settings/public/management_app/components/_index.scss b/src/plugins/advanced_settings/public/management_app/components/_index.scss deleted file mode 100644 index d2d2e38947f76..0000000000000 --- a/src/plugins/advanced_settings/public/management_app/components/_index.scss +++ /dev/null @@ -1 +0,0 @@ -@import './form/index'; diff --git a/src/plugins/advanced_settings/public/management_app/components/form/_form.scss b/src/plugins/advanced_settings/public/management_app/components/form/_form.scss deleted file mode 100644 index 8d768d200fdd2..0000000000000 --- a/src/plugins/advanced_settings/public/management_app/components/form/_form.scss +++ /dev/null @@ -1,15 +0,0 @@ -@import '@elastic/eui/src/global_styling/variables/header'; -@import '@elastic/eui/src/components/nav_drawer/variables'; - -// TODO #64541 -// Delete this whole file -.mgtAdvancedSettingsForm__bottomBar { - margin-left: $euiNavDrawerWidthCollapsed; - z-index: 9; // Puts it inuder the nav drawer when expanded - &--pushForNav { - margin-left: $euiNavDrawerWidthExpanded; - } - @include euiBreakpoint('xs', 's') { - margin-left: 0; - } -} diff --git a/src/plugins/advanced_settings/public/management_app/components/form/_index.scss b/src/plugins/advanced_settings/public/management_app/components/form/_index.scss deleted file mode 100644 index 2ef4ef1d20ce9..0000000000000 --- a/src/plugins/advanced_settings/public/management_app/components/form/_index.scss +++ /dev/null @@ -1 +0,0 @@ -@import './form'; 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 5533f684870d9..0378d816fd2c3 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 @@ -18,7 +18,6 @@ */ import React, { PureComponent, Fragment } from 'react'; -import classNames from 'classnames'; import { EuiFlexGroup, @@ -45,7 +44,6 @@ import { Field, getEditableValue } from '../field'; import { FieldSetting, SettingsChanges, FieldState } from '../../types'; type Category = string; -const NAV_IS_LOCKED_KEY = 'core.chrome.isLocked'; interface FormProps { settings: Record; @@ -326,23 +324,8 @@ export class Form extends PureComponent { renderBottomBar = () => { const areChangesInvalid = this.areChangesInvalid(); - - // TODO #64541 - // Delete these classes - let bottomBarClasses = ''; - const pageNav = this.props.settings.general.find( - (setting) => setting.name === 'pageNavigation' - ); - - if (pageNav?.value === 'legacy') { - bottomBarClasses = classNames('mgtAdvancedSettingsForm__bottomBar', { - // eslint-disable-next-line @typescript-eslint/naming-convention - 'mgtAdvancedSettingsForm__bottomBar--pushForNav': - localStorage.getItem(NAV_IS_LOCKED_KEY) === 'true', - }); - } return ( - + ScopedHistory; + setHeaderActionMenu: AppMountParameters['setHeaderActionMenu']; savedObjects: SavedObjectsStart; restorePreviousUrl: () => void; } diff --git a/src/plugins/dashboard/public/application/dashboard_app_controller.tsx b/src/plugins/dashboard/public/application/dashboard_app_controller.tsx index 92d6f2ed91dde..dd5eb1ee5ccaa 100644 --- a/src/plugins/dashboard/public/application/dashboard_app_controller.tsx +++ b/src/plugins/dashboard/public/application/dashboard_app_controller.tsx @@ -153,6 +153,7 @@ export class DashboardAppController { i18n: i18nStart, }, history, + setHeaderActionMenu, kbnUrlStateStorage, usageCollection, navigation, @@ -709,7 +710,13 @@ export class DashboardAppController { }; const dashboardNavBar = document.getElementById('dashboardChrome'); const updateNavBar = () => { - ReactDOM.render(, dashboardNavBar); + ReactDOM.render( + , + dashboardNavBar + ); }; const unmountNavBar = () => { diff --git a/src/plugins/dashboard/public/attribute_service/attribute_service.tsx b/src/plugins/dashboard/public/attribute_service/attribute_service.tsx index a36363d22d87d..84df05154fb63 100644 --- a/src/plugins/dashboard/public/attribute_service/attribute_service.tsx +++ b/src/plugins/dashboard/public/attribute_service/attribute_service.tsx @@ -57,7 +57,7 @@ export interface AttributeServiceOptions { type: string, attributes: A, savedObjectId?: string - ) => Promise<{ id: string }>; + ) => Promise<{ id?: string } | { error: Error }>; customUnwrapMethod?: (savedObject: SimpleSavedObject) => A; } @@ -124,7 +124,10 @@ export class AttributeService< newAttributes, savedObjectId ); - return { ...originalInput, savedObjectId: savedItem.id } as RefType; + if ('id' in savedItem) { + return { ...originalInput, savedObjectId: savedItem.id } as RefType; + } + return { ...originalInput } as RefType; } if (savedObjectId) { @@ -208,7 +211,6 @@ export class AttributeService< return { error }; } }; - if (saveOptions && (saveOptions as { showSaveModal: boolean }).showSaveModal) { this.showSaveModal( ; + +const createStartContract = (): DashboardStart => { + // @ts-ignore + const startContract: DashboardStart = { + getAttributeService: jest.fn(), + }; + + return startContract; +}; + +export const dashboardPluginMock = { + createStartContract, +}; diff --git a/src/plugins/dashboard/public/plugin.tsx b/src/plugins/dashboard/public/plugin.tsx index 49584f62215ea..5a45229a58a7d 100644 --- a/src/plugins/dashboard/public/plugin.tsx +++ b/src/plugins/dashboard/public/plugin.tsx @@ -310,7 +310,7 @@ export class DashboardPlugin id: DashboardConstants.DASHBOARDS_ID, title: 'Dashboard', order: 2500, - euiIconType: 'dashboardApp', + euiIconType: 'logoKibana', defaultPath: `#${DashboardConstants.LANDING_PAGE_PATH}`, updater$: this.appStateUpdater, category: DEFAULT_APP_CATEGORIES.kibana, @@ -352,6 +352,7 @@ export class DashboardPlugin localStorage: new Storage(localStorage), usageCollection, scopedHistory: () => this.currentHistory!, + setHeaderActionMenu: params.setHeaderActionMenu, savedObjects, restorePreviousUrl, }; diff --git a/src/plugins/data/common/es_query/kuery/node_types/types.ts b/src/plugins/data/common/es_query/kuery/node_types/types.ts index 6d3019e75d483..894bd1812f084 100644 --- a/src/plugins/data/common/es_query/kuery/node_types/types.ts +++ b/src/plugins/data/common/es_query/kuery/node_types/types.ts @@ -76,6 +76,7 @@ export interface NamedArgTypeBuildNode { } interface WildcardType { + wildcardSymbol: string; buildNode: (value: string) => WildcardTypeBuildNode | KueryNode; test: (node: any, string: string) => boolean; toElasticsearchQuery: (node: any) => string; diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md index fa5d3cd85f430..a8fbbcb08d358 100644 --- a/src/plugins/data/public/public.api.md +++ b/src/plugins/data/public/public.api.md @@ -28,7 +28,6 @@ import { ExpressionAstFunction } from 'src/plugins/expressions/common'; import { ExpressionsSetup } from 'src/plugins/expressions/public'; import { History } from 'history'; import { Href } from 'history'; -import { HttpStart } from 'src/core/public'; import { IconType } from '@elastic/eui'; import { InjectedIntl } from '@kbn/i18n/react'; import { ISearchOptions as ISearchOptions_2 } from 'src/plugins/data/public'; @@ -49,6 +48,7 @@ import { Path } from 'history'; import { Plugin as Plugin_2 } from 'src/core/public'; import { PluginInitializerContext as PluginInitializerContext_2 } from 'src/core/public'; import { PopoverAnchorPosition } from '@elastic/eui'; +import { PublicMethodsOf } from '@kbn/utility-types'; import { PublicUiSettingsParams } from 'src/core/server/types'; import React from 'react'; import * as React_2 from 'react'; @@ -1685,7 +1685,7 @@ export interface QueryStateChange extends QueryStateChangePartial { // Warning: (ae-missing-release-tag) "QueryStringInput" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export const QueryStringInput: React.FC>; +export const QueryStringInput: React.FC>; // @public (undocumented) export type QuerySuggestion = QuerySuggestionBasic | QuerySuggestionField; @@ -2017,7 +2017,7 @@ export class SearchSource { onRequestStart(handler: (searchSource: SearchSource, options?: ISearchOptions) => Promise): void; serialize(): { searchSourceJSON: string; - references: import("../../../../../core/public").SavedObjectReference[]; + references: import("../../../../../core/types").SavedObjectReference[]; }; setField(field: K, value: SearchSourceFields[K]): this; setFields(newFields: SearchSourceFields): this; diff --git a/src/plugins/data/public/search/fetch/types.ts b/src/plugins/data/public/search/fetch/types.ts index 224a597766909..cdf10d8f1a1b0 100644 --- a/src/plugins/data/public/search/fetch/types.ts +++ b/src/plugins/data/public/search/fetch/types.ts @@ -17,9 +17,9 @@ * under the License. */ -import { HttpStart } from 'src/core/public'; -import { BehaviorSubject } from 'rxjs'; +import { SearchResponse } from 'elasticsearch'; import { GetConfigFn } from '../../../common'; +import { LegacyFetchHandlers } from '../legacy/types'; /** * @internal @@ -31,9 +31,17 @@ import { GetConfigFn } from '../../../common'; export type SearchRequest = Record; export interface FetchHandlers { - config: { get: GetConfigFn }; - http: HttpStart; - loadingCount$: BehaviorSubject; + getConfig: GetConfigFn; + /** + * Callback which can be used to hook into responses, modify them, or perform + * side effects like displaying UI errors on the client. + */ + onResponse: (request: SearchRequest, response: SearchResponse) => SearchResponse; + /** + * These handlers are only used by the legacy defaultSearchStrategy and can be removed + * once that strategy has been deprecated. + */ + legacy: LegacyFetchHandlers; } export interface SearchError { diff --git a/src/plugins/data/public/search/legacy/call_client.test.ts b/src/plugins/data/public/search/legacy/call_client.test.ts index 943a02d22088d..0a7913b0a734f 100644 --- a/src/plugins/data/public/search/legacy/call_client.test.ts +++ b/src/plugins/data/public/search/legacy/call_client.test.ts @@ -17,18 +17,13 @@ * under the License. */ -import { coreMock } from '../../../../../core/public/mocks'; import { callClient } from './call_client'; import { SearchStrategySearchParams } from './types'; import { defaultSearchStrategy } from './default_search_strategy'; import { FetchHandlers } from '../fetch'; -import { handleResponse } from '../fetch/handle_response'; import { BehaviorSubject } from 'rxjs'; const mockAbortFn = jest.fn(); -jest.mock('../fetch/handle_response', () => ({ - handleResponse: jest.fn((request, response) => response), -})); jest.mock('./default_search_strategy', () => { return { @@ -50,32 +45,36 @@ jest.mock('./default_search_strategy', () => { }); describe('callClient', () => { + const handleResponse = jest.fn().mockImplementation((req, res) => res); + const handlers = { + getConfig: jest.fn(), + onResponse: handleResponse, + legacy: { + callMsearch: jest.fn(), + loadingCount$: new BehaviorSubject(0), + }, + } as FetchHandlers; + beforeEach(() => { - (handleResponse as jest.Mock).mockClear(); + handleResponse.mockClear(); }); test('Passes the additional arguments it is given to the search strategy', () => { const searchRequests = [{ _searchStrategyId: 0 }]; - const args = { - http: coreMock.createStart().http, - legacySearchService: {}, - config: { get: jest.fn() }, - loadingCount$: new BehaviorSubject(0), - } as FetchHandlers; - callClient(searchRequests, [], args); + callClient(searchRequests, [], handlers); expect(defaultSearchStrategy.search).toBeCalled(); expect((defaultSearchStrategy.search as any).mock.calls[0][0]).toEqual({ searchRequests, - ...args, + ...handlers, }); }); test('Returns the responses in the original order', async () => { const searchRequests = [{ _searchStrategyId: 1 }, { _searchStrategyId: 0 }]; - const responses = await Promise.all(callClient(searchRequests, [], {} as FetchHandlers)); + const responses = await Promise.all(callClient(searchRequests, [], handlers)); expect(responses[0]).toEqual({ id: searchRequests[0]._searchStrategyId }); expect(responses[1]).toEqual({ id: searchRequests[1]._searchStrategyId }); @@ -84,7 +83,7 @@ describe('callClient', () => { test('Calls handleResponse with each request and response', async () => { const searchRequests = [{ _searchStrategyId: 0 }, { _searchStrategyId: 1 }]; - const responses = callClient(searchRequests, [], {} as FetchHandlers); + const responses = callClient(searchRequests, [], handlers); await Promise.all(responses); expect(handleResponse).toBeCalledTimes(2); @@ -105,7 +104,7 @@ describe('callClient', () => { }, ]; - callClient(searchRequests, requestOptions, {} as FetchHandlers); + callClient(searchRequests, requestOptions, handlers); abortController.abort(); expect(mockAbortFn).toBeCalled(); diff --git a/src/plugins/data/public/search/legacy/call_client.ts b/src/plugins/data/public/search/legacy/call_client.ts index d66796b9427a1..b87affdd59c54 100644 --- a/src/plugins/data/public/search/legacy/call_client.ts +++ b/src/plugins/data/public/search/legacy/call_client.ts @@ -19,7 +19,7 @@ import { SearchResponse } from 'elasticsearch'; import { ISearchOptions } from 'src/plugins/data/common'; -import { FetchHandlers, handleResponse } from '../fetch'; +import { FetchHandlers } from '../fetch'; import { defaultSearchStrategy } from './default_search_strategy'; import { SearchRequest } from '../index'; @@ -42,7 +42,7 @@ export function callClient( }); searchRequests.forEach((request, i) => { - const response = searching.then((results) => handleResponse(request, results[i])); + const response = searching.then((results) => fetchHandlers.onResponse(request, results[i])); const { abortSignal = null } = requestOptionsMap.get(request) || {}; if (abortSignal) abortSignal.addEventListener('abort', abort); requestResponseMap.set(request, response); diff --git a/src/plugins/data/public/search/legacy/call_msearch.ts b/src/plugins/data/public/search/legacy/call_msearch.ts new file mode 100644 index 0000000000000..fd4f8a07919f8 --- /dev/null +++ b/src/plugins/data/public/search/legacy/call_msearch.ts @@ -0,0 +1,37 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { HttpStart } from 'src/core/public'; +import { LegacyFetchHandlers } from './types'; + +/** + * Wrapper for calling the internal msearch endpoint from the client. + * This is needed to abstract away differences in the http service + * between client & server. + * + * @internal + */ +export function getCallMsearch({ http }: { http: HttpStart }): LegacyFetchHandlers['callMsearch'] { + return async ({ body, signal }) => { + return http.post('/internal/_msearch', { + body: JSON.stringify(body), + signal, + }); + }; +} diff --git a/src/plugins/data/public/search/legacy/default_search_strategy.test.ts b/src/plugins/data/public/search/legacy/default_search_strategy.test.ts index e74ab49131430..ad59e5c6c9625 100644 --- a/src/plugins/data/public/search/legacy/default_search_strategy.test.ts +++ b/src/plugins/data/public/search/legacy/default_search_strategy.test.ts @@ -19,8 +19,9 @@ import { HttpStart } from 'src/core/public'; import { coreMock } from '../../../../../core/public/mocks'; +import { getCallMsearch } from './call_msearch'; import { defaultSearchStrategy } from './default_search_strategy'; -import { SearchStrategySearchParams } from './types'; +import { LegacyFetchHandlers, SearchStrategySearchParams } from './types'; import { BehaviorSubject } from 'rxjs'; const { search } = defaultSearchStrategy; @@ -44,11 +45,12 @@ describe('defaultSearchStrategy', function () { index: { title: 'foo' }, }, ], - http, - config: { - get: jest.fn(), - }, - loadingCount$: new BehaviorSubject(0) as any, + getConfig: jest.fn(), + onResponse: (req, res) => res, + legacy: { + callMsearch: getCallMsearch({ http }), + loadingCount$: new BehaviorSubject(0) as any, + } as jest.Mocked, }; }); diff --git a/src/plugins/data/public/search/legacy/default_search_strategy.ts b/src/plugins/data/public/search/legacy/default_search_strategy.ts index cbcd0da20207f..bed86cb75cca6 100644 --- a/src/plugins/data/public/search/legacy/default_search_strategy.ts +++ b/src/plugins/data/public/search/legacy/default_search_strategy.ts @@ -29,12 +29,14 @@ export const defaultSearchStrategy: SearchStrategyProvider = { }, }; -function msearch({ searchRequests, config, http, loadingCount$ }: SearchStrategySearchParams) { +function msearch({ searchRequests, getConfig, legacy }: SearchStrategySearchParams) { + const { callMsearch, loadingCount$ } = legacy; + const requests = searchRequests.map(({ index, body }) => { return { header: { index: index.title || index, - preference: getPreference(config.get), + preference: getPreference(getConfig), }, body, }; @@ -55,12 +57,11 @@ function msearch({ searchRequests, config, http, loadingCount$ }: SearchStrategy } }; - const searching = http - .post('/internal/_msearch', { - body: JSON.stringify({ searches: requests }), - signal: abortController.signal, - }) - .then(({ body }) => body?.responses) + const searching = callMsearch({ + body: { searches: requests }, + signal: abortController.signal, + }) + .then((res: any) => res?.body?.responses) .finally(() => cleanup()); return { diff --git a/src/plugins/data/public/search/legacy/fetch_soon.test.ts b/src/plugins/data/public/search/legacy/fetch_soon.test.ts index d38a41cf5ffbc..7243ab158009a 100644 --- a/src/plugins/data/public/search/legacy/fetch_soon.test.ts +++ b/src/plugins/data/public/search/legacy/fetch_soon.test.ts @@ -67,25 +67,21 @@ describe('fetchSoon', () => { }); test('should execute asap if config is set to not batch searches', () => { - const config = { - get: getConfigStub({ [UI_SETTINGS.COURIER_BATCH_SEARCHES]: false }), - }; + const getConfig = getConfigStub({ [UI_SETTINGS.COURIER_BATCH_SEARCHES]: false }); const request = {}; const options = {}; - fetchSoon(request, options, { config } as FetchHandlers); + fetchSoon(request, options, { getConfig } as FetchHandlers); expect(callClient).toBeCalled(); }); test('should delay by 50ms if config is set to batch searches', () => { - const config = { - get: getConfigStub({ [UI_SETTINGS.COURIER_BATCH_SEARCHES]: true }), - }; + const getConfig = getConfigStub({ [UI_SETTINGS.COURIER_BATCH_SEARCHES]: true }); const request = {}; const options = {}; - fetchSoon(request, options, { config } as FetchHandlers); + fetchSoon(request, options, { getConfig } as FetchHandlers); expect(callClient).not.toBeCalled(); jest.advanceTimersByTime(0); @@ -95,14 +91,12 @@ describe('fetchSoon', () => { }); test('should send a batch of requests to callClient', () => { - const config = { - get: getConfigStub({ [UI_SETTINGS.COURIER_BATCH_SEARCHES]: true }), - }; + const getConfig = getConfigStub({ [UI_SETTINGS.COURIER_BATCH_SEARCHES]: true }); const requests = [{ foo: 1 }, { foo: 2 }]; const options = [{ bar: 1 }, { bar: 2 }]; requests.forEach((request, i) => { - fetchSoon(request, options[i] as ISearchOptions, { config } as FetchHandlers); + fetchSoon(request, options[i] as ISearchOptions, { getConfig } as FetchHandlers); }); jest.advanceTimersByTime(50); @@ -112,13 +106,11 @@ describe('fetchSoon', () => { }); test('should return the response to the corresponding call for multiple batched requests', async () => { - const config = { - get: getConfigStub({ [UI_SETTINGS.COURIER_BATCH_SEARCHES]: true }), - }; + const getConfig = getConfigStub({ [UI_SETTINGS.COURIER_BATCH_SEARCHES]: true }); const requests = [{ _mockResponseId: 'foo' }, { _mockResponseId: 'bar' }]; const promises = requests.map((request) => { - return fetchSoon(request, {}, { config } as FetchHandlers); + return fetchSoon(request, {}, { getConfig } as FetchHandlers); }); jest.advanceTimersByTime(50); const results = await Promise.all(promises); @@ -127,18 +119,16 @@ describe('fetchSoon', () => { }); test('should wait for the previous batch to start before starting a new batch', () => { - const config = { - get: getConfigStub({ [UI_SETTINGS.COURIER_BATCH_SEARCHES]: true }), - }; + const getConfig = getConfigStub({ [UI_SETTINGS.COURIER_BATCH_SEARCHES]: true }); const firstBatch = [{ foo: 1 }, { foo: 2 }]; const secondBatch = [{ bar: 1 }, { bar: 2 }]; firstBatch.forEach((request) => { - fetchSoon(request, {}, { config } as FetchHandlers); + fetchSoon(request, {}, { getConfig } as FetchHandlers); }); jest.advanceTimersByTime(50); secondBatch.forEach((request) => { - fetchSoon(request, {}, { config } as FetchHandlers); + fetchSoon(request, {}, { getConfig } as FetchHandlers); }); expect(callClient).toBeCalledTimes(1); diff --git a/src/plugins/data/public/search/legacy/fetch_soon.ts b/src/plugins/data/public/search/legacy/fetch_soon.ts index 37c3827bb7bba..1c0573aa895d7 100644 --- a/src/plugins/data/public/search/legacy/fetch_soon.ts +++ b/src/plugins/data/public/search/legacy/fetch_soon.ts @@ -32,7 +32,7 @@ export async function fetchSoon( options: ISearchOptions, fetchHandlers: FetchHandlers ) { - const msToDelay = fetchHandlers.config.get(UI_SETTINGS.COURIER_BATCH_SEARCHES) ? 50 : 0; + const msToDelay = fetchHandlers.getConfig(UI_SETTINGS.COURIER_BATCH_SEARCHES) ? 50 : 0; return delayedFetch(request, options, fetchHandlers, msToDelay); } diff --git a/src/plugins/data/public/search/legacy/types.ts b/src/plugins/data/public/search/legacy/types.ts index ed17db464feff..740bc22a7485c 100644 --- a/src/plugins/data/public/search/legacy/types.ts +++ b/src/plugins/data/public/search/legacy/types.ts @@ -17,10 +17,20 @@ * under the License. */ +import { BehaviorSubject } from 'rxjs'; import { SearchResponse } from 'elasticsearch'; import { FetchHandlers } from '../fetch'; import { SearchRequest } from '..'; +// @internal +export interface LegacyFetchHandlers { + callMsearch: (params: { + body: SearchRequest; + signal: AbortSignal; + }) => Promise>>; + loadingCount$: BehaviorSubject; +} + export interface SearchStrategySearchParams extends FetchHandlers { searchRequests: SearchRequest[]; } diff --git a/src/plugins/data/public/search/search_service.ts b/src/plugins/data/public/search/search_service.ts index 6b73761c5a437..c41e1f78ee74e 100644 --- a/src/plugins/data/public/search/search_service.ts +++ b/src/plugins/data/public/search/search_service.ts @@ -21,6 +21,8 @@ import { Plugin, CoreSetup, CoreStart } from 'src/core/public'; import { BehaviorSubject } from 'rxjs'; import { ISearchSetup, ISearchStart, SearchEnhancements } from './types'; +import { handleResponse } from './fetch'; +import { getCallMsearch } from './legacy/call_msearch'; import { createSearchSource, SearchSource, SearchSourceDependencies } from './search_source'; import { AggsService, AggsStartDependencies } from './aggs'; import { IndexPatternsContract } from '../index_patterns/index_patterns'; @@ -49,7 +51,7 @@ export class SearchService implements Plugin { private usageCollector?: SearchUsageCollector; public setup( - { http, getStartServices, injectedMetadata, notifications, uiSettings }: CoreSetup, + { http, getStartServices, notifications, uiSettings }: CoreSetup, { expressions, usageCollection }: SearchServiceSetupDependencies ): ISearchSetup { this.usageCollector = createUsageCollector(getStartServices, usageCollection); @@ -82,7 +84,7 @@ export class SearchService implements Plugin { } public start( - { application, http, injectedMetadata, notifications, uiSettings }: CoreStart, + { application, http, notifications, uiSettings }: CoreStart, { fieldFormats, indexPatterns }: SearchServiceStartDependencies ): ISearchStart { const search = ((request, options) => { @@ -95,8 +97,11 @@ export class SearchService implements Plugin { const searchSourceDependencies: SearchSourceDependencies = { getConfig: uiSettings.get.bind(uiSettings), search, - http, - loadingCount$, + onResponse: handleResponse, + legacy: { + callMsearch: getCallMsearch({ http }), + loadingCount$, + }, }; return { diff --git a/src/plugins/data/public/search/search_source/create_search_source.test.ts b/src/plugins/data/public/search/search_source/create_search_source.test.ts index bc1c7c06c8806..6b6cfb0c9b1ca 100644 --- a/src/plugins/data/public/search/search_source/create_search_source.test.ts +++ b/src/plugins/data/public/search/search_source/create_search_source.test.ts @@ -22,7 +22,6 @@ import { SearchSourceDependencies } from './search_source'; import { IIndexPattern } from '../../../common/index_patterns'; import { IndexPatternsContract } from '../../index_patterns/index_patterns'; import { Filter } from '../../../common/es_query/filters'; -import { coreMock } from '../../../../../core/public/mocks'; import { BehaviorSubject } from 'rxjs'; describe('createSearchSource', () => { @@ -35,8 +34,11 @@ describe('createSearchSource', () => { dependencies = { getConfig: jest.fn(), search: jest.fn(), - http: coreMock.createStart().http, - loadingCount$: new BehaviorSubject(0), + onResponse: (req, res) => res, + legacy: { + callMsearch: jest.fn(), + loadingCount$: new BehaviorSubject(0), + }, }; indexPatternContractMock = ({ diff --git a/src/plugins/data/public/search/search_source/mocks.ts b/src/plugins/data/public/search/search_source/mocks.ts index adf53bee33fe1..f582861e37c15 100644 --- a/src/plugins/data/public/search/search_source/mocks.ts +++ b/src/plugins/data/public/search/search_source/mocks.ts @@ -18,7 +18,7 @@ */ import { BehaviorSubject } from 'rxjs'; -import { httpServiceMock, uiSettingsServiceMock } from '../../../../../core/public/mocks'; +import { uiSettingsServiceMock } from '../../../../../core/public/mocks'; import { ISearchSource, SearchSource } from './search_source'; import { SearchSourceFields } from './types'; @@ -54,6 +54,9 @@ export const createSearchSourceMock = (fields?: SearchSourceFields) => new SearchSource(fields, { getConfig: uiSettingsServiceMock.createStartContract().get, search: jest.fn(), - http: httpServiceMock.createStartContract(), - loadingCount$: new BehaviorSubject(0), + onResponse: jest.fn().mockImplementation((req, res) => res), + legacy: { + callMsearch: jest.fn(), + loadingCount$: new BehaviorSubject(0), + }, }); diff --git a/src/plugins/data/public/search/search_source/search_source.test.ts b/src/plugins/data/public/search/search_source/search_source.test.ts index 282a33e6d01f7..d9a9fb2f4fef3 100644 --- a/src/plugins/data/public/search/search_source/search_source.test.ts +++ b/src/plugins/data/public/search/search_source/search_source.test.ts @@ -22,7 +22,6 @@ import { GetConfigFn } from 'src/plugins/data/common'; import { SearchSource, SearchSourceDependencies } from './search_source'; import { IndexPattern, SortDirection } from '../..'; import { fetchSoon } from '../legacy'; -import { coreMock } from '../../../../../core/public/mocks'; jest.mock('../legacy', () => ({ fetchSoon: jest.fn().mockResolvedValue({}), @@ -68,8 +67,11 @@ describe('SearchSource', () => { searchSourceDependencies = { getConfig: jest.fn(), search: mockSearchMethod, - http: coreMock.createStart().http, - loadingCount$: new BehaviorSubject(0), + onResponse: (req, res) => res, + legacy: { + callMsearch: jest.fn(), + loadingCount$: new BehaviorSubject(0), + }, }; }); diff --git a/src/plugins/data/public/search/search_source/search_source.ts b/src/plugins/data/public/search/search_source/search_source.ts index a39898e6a9f52..4afee223454e4 100644 --- a/src/plugins/data/public/search/search_source/search_source.ts +++ b/src/plugins/data/public/search/search_source/search_source.ts @@ -72,19 +72,12 @@ import { setWith } from '@elastic/safer-lodash-set'; import { uniqueId, uniq, extend, pick, difference, omit, isObject, keys, isFunction } from 'lodash'; import { map } from 'rxjs/operators'; -import { HttpStart } from 'src/core/public'; -import { BehaviorSubject } from 'rxjs'; import { normalizeSortRequest } from './normalize_sort_request'; import { filterDocvalueFields } from './filter_docvalue_fields'; import { fieldWildcardFilter } from '../../../../kibana_utils/common'; import { IIndexPattern, ISearchGeneric } from '../..'; import { SearchSourceOptions, SearchSourceFields } from './types'; -import { - RequestFailure, - handleResponse, - getSearchParamsFromRequest, - SearchRequest, -} from '../fetch'; +import { FetchHandlers, RequestFailure, getSearchParamsFromRequest, SearchRequest } from '../fetch'; import { getEsQueryConfig, @@ -94,7 +87,6 @@ import { ISearchOptions, } from '../../../common'; import { getHighlightRequest } from '../../../common/field_formats'; -import { GetConfigFn } from '../../../common/types'; import { fetchSoon } from '../legacy'; import { extractReferences } from './extract_references'; @@ -114,11 +106,8 @@ export const searchSourceRequiredUiSettings = [ UI_SETTINGS.SORT_OPTIONS, ]; -export interface SearchSourceDependencies { - getConfig: GetConfigFn; +export interface SearchSourceDependencies extends FetchHandlers { search: ISearchGeneric; - http: HttpStart; - loadingCount$: BehaviorSubject; } /** @public **/ @@ -321,14 +310,14 @@ export class SearchSource { * @return {Observable>} */ private fetch$(searchRequest: SearchRequest, options: ISearchOptions) { - const { search, getConfig } = this.dependencies; + const { search, getConfig, onResponse } = this.dependencies; const params = getSearchParamsFromRequest(searchRequest, { getConfig, }); return search({ params, indexType: searchRequest.indexType }, options).pipe( - map(({ rawResponse }) => handleResponse(searchRequest, rawResponse)) + map(({ rawResponse }) => onResponse(searchRequest, rawResponse)) ); } @@ -337,7 +326,7 @@ export class SearchSource { * @return {Promise>} */ private async legacyFetch(searchRequest: SearchRequest, options: ISearchOptions) { - const { http, getConfig, loadingCount$ } = this.dependencies; + const { getConfig, legacy, onResponse } = this.dependencies; return await fetchSoon( searchRequest, @@ -346,9 +335,9 @@ export class SearchSource { ...options, }, { - http, - config: { get: getConfig }, - loadingCount$, + getConfig, + onResponse, + legacy, } ); } diff --git a/src/plugins/data/public/ui/shard_failure_modal/shard_failure_description.tsx b/src/plugins/data/public/ui/shard_failure_modal/shard_failure_description.tsx index 3606bfbaeb1f9..1f124291669ec 100644 --- a/src/plugins/data/public/ui/shard_failure_modal/shard_failure_description.tsx +++ b/src/plugins/data/public/ui/shard_failure_modal/shard_failure_description.tsx @@ -17,9 +17,9 @@ * under the License. */ import React from 'react'; +import { getFlattenedObject } from '@kbn/std'; import { EuiCodeBlock, EuiDescriptionList, EuiSpacer } from '@elastic/eui'; import { ShardFailure } from './shard_failure_types'; -import { getFlattenedObject } from '../../../../../core/public'; import { ShardFailureDescriptionHeader } from './shard_failure_description_header'; /** diff --git a/src/plugins/dev_tools/public/index.scss b/src/plugins/dev_tools/public/index.scss index c9d8dc7470656..4bec602ea42db 100644 --- a/src/plugins/dev_tools/public/index.scss +++ b/src/plugins/dev_tools/public/index.scss @@ -16,10 +16,6 @@ } } -.devApp { - height: 100%; -} - .devAppWrapper { display: flex; flex-direction: column; diff --git a/src/plugins/dev_tools/public/plugin.ts b/src/plugins/dev_tools/public/plugin.ts index fcc6a57361a94..8c4743c93fab3 100644 --- a/src/plugins/dev_tools/public/plugin.ts +++ b/src/plugins/dev_tools/public/plugin.ts @@ -60,7 +60,7 @@ export class DevToolsPlugin implements Plugin { defaultMessage: 'Dev Tools', }), updater$: this.appStateUpdater, - euiIconType: 'devToolsApp', + euiIconType: 'logoElastic', order: 9010, category: DEFAULT_APP_CATEGORIES.management, mount: async (params: AppMountParameters) => { diff --git a/src/plugins/discover/public/application/angular/discover.html b/src/plugins/discover/public/application/angular/discover.html index 94f13e1cd8132..e0e452aaa41c5 100644 --- a/src/plugins/discover/public/application/angular/discover.html +++ b/src/plugins/discover/public/application/angular/discover.html @@ -5,6 +5,7 @@

{{screenTitle}}

(uiActions = pluginUiActions); export const getUiActions = () => uiActions; +export const [getHeaderActionMenuMounter, setHeaderActionMenuMounter] = createGetterSetter< + AppMountParameters['setHeaderActionMenu'] +>('headerActionMenuMounter'); + export const [getUrlTracker, setUrlTracker] = createGetterSetter<{ setTrackedUrl: (url: string) => void; restorePreviousUrl: () => void; diff --git a/src/plugins/discover/public/plugin.ts b/src/plugins/discover/public/plugin.ts index b6960c8a20abf..dd9b57b568e42 100644 --- a/src/plugins/discover/public/plugin.ts +++ b/src/plugins/discover/public/plugin.ts @@ -54,6 +54,7 @@ import { setUrlTracker, setAngularModule, setServices, + setHeaderActionMenuMounter, setUiActions, setScopedHistory, getScopedHistory, @@ -240,7 +241,7 @@ export class DiscoverPlugin title: 'Discover', updater$: this.appStateUpdater.asObservable(), order: 1000, - euiIconType: 'discoverApp', + euiIconType: 'logoKibana', defaultPath: '#/', category: DEFAULT_APP_CATEGORIES.kibana, mount: async (params: AppMountParameters) => { @@ -251,6 +252,7 @@ export class DiscoverPlugin throw Error('Discover plugin method initializeInnerAngular is undefined'); } setScopedHistory(params.history); + setHeaderActionMenuMounter(params.setHeaderActionMenu); syncHistoryLocations(); appMounted(); const { @@ -264,6 +266,7 @@ export class DiscoverPlugin params.element.classList.add('dscAppWrapper'); const unmount = await renderApp(innerAngularName, params.element); return () => { + params.element.classList.remove('dscAppWrapper'); unmount(); appUnMounted(); }; diff --git a/src/plugins/embeddable/public/lib/embeddables/default_embeddable_factory_provider.ts b/src/plugins/embeddable/public/lib/embeddables/default_embeddable_factory_provider.ts index 570a78fc41ea9..b22f16c94aff8 100644 --- a/src/plugins/embeddable/public/lib/embeddables/default_embeddable_factory_provider.ts +++ b/src/plugins/embeddable/public/lib/embeddables/default_embeddable_factory_provider.ts @@ -37,11 +37,11 @@ export const defaultEmbeddableFactoryProvider = < getExplicitInput: def.getExplicitInput ? def.getExplicitInput.bind(def) : () => Promise.resolve({}), - createFromSavedObject: - def.createFromSavedObject ?? - ((savedObjectId: string, input: Partial, parent?: IContainer) => { - throw new Error(`Creation from saved object not supported by type ${def.type}`); - }), + createFromSavedObject: def.createFromSavedObject + ? def.createFromSavedObject.bind(def) + : (savedObjectId: string, input: Partial, parent?: IContainer) => { + throw new Error(`Creation from saved object not supported by type ${def.type}`); + }, create: def.create.bind(def), type: def.type, isEditable: def.isEditable.bind(def), diff --git a/src/plugins/kibana_legacy/public/angular/kbn_top_nav.js b/src/plugins/kibana_legacy/public/angular/kbn_top_nav.js index b3fbe8baadec3..c34e2487b32d4 100644 --- a/src/plugins/kibana_legacy/public/angular/kbn_top_nav.js +++ b/src/plugins/kibana_legacy/public/angular/kbn_top_nav.js @@ -74,6 +74,7 @@ export function createTopNavDirective() { export const createTopNavHelper = ({ TopNavMenu }) => (reactDirective) => { return reactDirective(TopNavMenu, [ ['config', { watchDepth: 'value' }], + ['setMenuMountPoint', { watchDepth: 'reference' }], ['disabledButtons', { watchDepth: 'reference' }], ['query', { watchDepth: 'reference' }], diff --git a/src/plugins/kibana_react/README.md b/src/plugins/kibana_react/README.md index 3389af9f1800b..adbdb628ea9dd 100644 --- a/src/plugins/kibana_react/README.md +++ b/src/plugins/kibana_react/README.md @@ -2,7 +2,6 @@ Tools for building React applications in Kibana. - ## Context You can create React context that holds Core or plugin services that your plugin depends on. @@ -51,7 +50,6 @@ import { KibanaContextProvider } from 'kibana-react'; ``` - ## Accessing context Using `useKibana` hook. @@ -61,11 +59,7 @@ import { useKibana } from 'kibana-react'; const Demo = () => { const kibana = useKibana(); - return ( -
- {kibana.services.uiSettings.get('theme:darkMode') ? 'dark' : 'light'} -
- ); + return
{kibana.services.uiSettings.get('theme:darkMode') ? 'dark' : 'light'}
; }; ``` @@ -75,11 +69,7 @@ Using `withKibana()` higher order component. import { withKibana } from 'kibana-react'; const Demo = ({ kibana }) => { - return ( -
- {kibana.services.uiSettings.get('theme:darkMode') ? 'dark' : 'light'} -
- ); + return
{kibana.services.uiSettings.get('theme:darkMode') ? 'dark' : 'light'}
; }; export default withKibana(Demo); @@ -92,21 +82,17 @@ import { UseKibana } from 'kibana-react'; const Demo = () => { return ( - {kibana => -
- {kibana.services.uiSettings.get('theme:darkMode') ? 'dark' : 'light'} -
- }
+ + {(kibana) =>
{kibana.services.uiSettings.get('theme:darkMode') ? 'dark' : 'light'}
} +
); }; ``` - ## `uiSettings` service Wrappers around Core's `uiSettings` service. - ### `useUiSetting` hook `useUiSetting` synchronously returns the latest setting from `CoreStart['uiSettings']` service. @@ -116,11 +102,7 @@ import { useUiSetting } from 'kibana-react'; const Demo = () => { const darkMode = useUiSetting('theme:darkMode'); - return ( -
- {darkMode ? 'dark' : 'light'} -
- ); + return
{darkMode ? 'dark' : 'light'}
; }; ``` @@ -130,7 +112,6 @@ const Demo = () => { useUiSetting(key: string, defaultValue: T): T; ``` - ### `useUiSetting$` hook `useUiSetting$` synchronously returns the latest setting from `CoreStart['uiSettings']` service and @@ -141,11 +122,7 @@ import { useUiSetting$ } from 'kibana-react'; const Demo = () => { const [darkMode] = useUiSetting$('theme:darkMode'); - return ( -
- {darkMode ? 'dark' : 'light'} -
- ); + return
{darkMode ? 'dark' : 'light'}
; }; ``` @@ -155,7 +132,6 @@ const Demo = () => { useUiSetting$(key: string, defaultValue: T): [T, (newValue: T) => void]; ``` - ## `overlays` service Wrapper around Core's `overlays` service, allows you to display React modals and flyouts @@ -166,13 +142,11 @@ import { createKibanaReactContext } from 'kibana-react'; class MyPlugin { start(core) { - const { value: { overlays } } = createKibanaReactContext(core); + const { + value: { overlays }, + } = createKibanaReactContext(core); - overlays.openModal( -
- Hello world! -
- ); + overlays.openModal(
Hello world!
); } } ``` @@ -186,16 +160,11 @@ You can access `overlays` service through React context. const Demo = () => { const { overlays } = useKibana(); useEffect(() => { - overlays.openModal( -
- Oooops! {errorMessage} -
- ); + overlays.openModal(
Oooops! {errorMessage}
); }, [errorMessage]); }; ``` - ## `notifications` service Wrapper around Core's `notifications` service, allows you to render React elements @@ -206,11 +175,13 @@ import { createKibanaReactContext } from 'kibana-react'; class MyPlugin { start(core) { - const { value: { notifications } } = createKibanaReactContext(core); + const { + value: { notifications }, + } = createKibanaReactContext(core); notifications.toasts.show({ title:
Hello
, - body:
world!
+ body:
world!
, }); } } @@ -234,3 +205,15 @@ const Demo = () => { }, [errorMessage]); }; ``` + +## RedirectAppLinks + +Utility component that will intercept click events on children anchor (`
`) elements to call +`application.navigateToUrl` with the link's href. This will trigger SPA friendly navigation +when the link points to a valid Kibana app. + +```tsx + + Go to another-app + +``` diff --git a/src/plugins/kibana_react/public/util/index.ts b/src/plugins/kibana_react/public/util/index.ts index a6f3f87535f46..030dd4aec0f12 100644 --- a/src/plugins/kibana_react/public/util/index.ts +++ b/src/plugins/kibana_react/public/util/index.ts @@ -19,3 +19,4 @@ export { toMountPoint } from './to_mount_point'; export { MountPointPortal } from './mount_point_portal'; +export { useIfMounted } from './utils'; diff --git a/src/plugins/kibana_react/public/util/mount_point_portal.tsx b/src/plugins/kibana_react/public/util/mount_point_portal.tsx index b762fba88791e..0249302763772 100644 --- a/src/plugins/kibana_react/public/util/mount_point_portal.tsx +++ b/src/plugins/kibana_react/public/util/mount_point_portal.tsx @@ -21,6 +21,7 @@ import { i18n } from '@kbn/i18n'; import React, { useRef, useEffect, useState, Component } from 'react'; import ReactDOM from 'react-dom'; import { MountPoint } from 'kibana/public'; +import { useIfMounted } from './utils'; interface MountPointPortalProps { setMountPoint: (mountPoint: MountPoint) => void; @@ -33,20 +34,30 @@ export const MountPointPortal: React.FC = ({ children, se // state used to force re-renders when the element changes const [shouldRender, setShouldRender] = useState(false); const el = useRef(); + const ifMounted = useIfMounted(); useEffect(() => { setMountPoint((element) => { - el.current = element; - setShouldRender(true); + ifMounted(() => { + el.current = element; + setShouldRender(true); + }); return () => { - setShouldRender(false); - el.current = undefined; + // the component can be unmounted from the dom before the portal target actually + // calls the `unmount` function. This is a no-op but show a scary warning in the console + // so we use a ifMounted effect to avoid it. + ifMounted(() => { + setShouldRender(false); + el.current = undefined; + }); }; }); return () => { - setShouldRender(false); - el.current = undefined; + ifMounted(() => { + setShouldRender(false); + el.current = undefined; + }); }; }, [setMountPoint]); diff --git a/src/plugins/kibana_react/public/util/utils.ts b/src/plugins/kibana_react/public/util/utils.ts new file mode 100644 index 0000000000000..23d4ac2ec4851 --- /dev/null +++ b/src/plugins/kibana_react/public/util/utils.ts @@ -0,0 +1,38 @@ +/* + * 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 { useCallback, useEffect, useRef } from 'react'; + +export const useIfMounted = () => { + const isMounted = useRef(true); + useEffect( + () => () => { + isMounted.current = false; + }, + [] + ); + + const ifMounted = useCallback((func) => { + if (isMounted.current && func) { + func(); + } + }, []); + + return ifMounted; +}; diff --git a/src/plugins/kibana_utils/common/state_containers/README.md b/src/plugins/kibana_utils/common/state_containers/README.md new file mode 100644 index 0000000000000..c623e8b306438 --- /dev/null +++ b/src/plugins/kibana_utils/common/state_containers/README.md @@ -0,0 +1,2 @@ +* [docs](../../docs/state_containers) +* [api reference](https://github.com/elastic/kibana/tree/master/src/plugins/kibana_utils/docs/state_containers) \ No newline at end of file diff --git a/src/plugins/kibana_utils/docs/state_sync/README.md b/src/plugins/kibana_utils/docs/state_sync/README.md index c84bf7f236330..6b4eb0cb1749b 100644 --- a/src/plugins/kibana_utils/docs/state_sync/README.md +++ b/src/plugins/kibana_utils/docs/state_sync/README.md @@ -3,6 +3,18 @@ State syncing utilities are a set of helpers for syncing your application state with URL or browser storage. +**When you should consider using state syncing utils:** + +- You want to sync your application state with URL in similar manner analyze applications do that. +- You want to follow platform's <> out of the box. +- You want to support `state:storeInSessionStore` escape hatch for URL overflowing out of the box. +- You should also consider using them if you'd like to serialize state to different (not `rison`) format. Utils are composable, and you can implement your own `storage`. +- In case you want to sync part of your state with URL, but other part of it with browser storage. + +**When you shouldn't look into using state syncing utils:** + +- Adding a query param flag or simple key/value to URL + They are designed to work together with [state containers](../state_containers). But state containers are not required. State syncing utilities include: @@ -42,9 +54,9 @@ stateContainer.set({ count: 2 }); stop(); ``` -## Demos Plugins +## Demo Plugins -See demos plugins [here](../../../../../examples/state_containers_examples). +See demo plugins [here](../../../../../examples/state_containers_examples). To run them, start kibana with `--run-examples` flag. diff --git a/src/plugins/kibana_utils/public/state_sync/README.md b/src/plugins/kibana_utils/public/state_sync/README.md new file mode 100644 index 0000000000000..eb5f6e60958fc --- /dev/null +++ b/src/plugins/kibana_utils/public/state_sync/README.md @@ -0,0 +1,3 @@ +- [docs](../../docs/state_sync) +- [demo plugins](../../../../../examples/state_containers_examples): run Kibana with `--run-examples` flag. +- [api reference](https://github.com/elastic/kibana/tree/master/src/plugins/kibana_utils/docs/state_sync) diff --git a/src/plugins/management/public/plugin.ts b/src/plugins/management/public/plugin.ts index 794bbc0d0613b..fafedf46c2bda 100644 --- a/src/plugins/management/public/plugin.ts +++ b/src/plugins/management/public/plugin.ts @@ -74,7 +74,7 @@ export class ManagementPlugin implements Plugin * > * { + // TEMP fix to adjust spacing between EuiHeaderList__list items + margin: 0 $euiSizeXS; } diff --git a/src/plugins/navigation/public/top_nav_menu/top_nav_menu.test.tsx b/src/plugins/navigation/public/top_nav_menu/top_nav_menu.test.tsx index f21e5680e8f61..212bc19208ca8 100644 --- a/src/plugins/navigation/public/top_nav_menu/top_nav_menu.test.tsx +++ b/src/plugins/navigation/public/top_nav_menu/top_nav_menu.test.tsx @@ -164,7 +164,10 @@ describe('TopNavMenu', () => { // menu is rendered outside of the component expect(component.find(TOP_NAV_ITEM_SELECTOR).length).toBe(0); - expect(portalTarget.getElementsByTagName('BUTTON').length).toBe(menuItems.length); + + const buttons = portalTarget.querySelectorAll('button'); + expect(buttons.length).toBe(menuItems.length + 1); // should be n+1 buttons in mobile for popover button + expect(buttons[buttons.length - 1].getAttribute('aria-label')).toBe('Open navigation menu'); // last button should be mobile button }); }); }); diff --git a/src/plugins/navigation/public/top_nav_menu/top_nav_menu.tsx b/src/plugins/navigation/public/top_nav_menu/top_nav_menu.tsx index b284c60bac5de..a27addeb14393 100644 --- a/src/plugins/navigation/public/top_nav_menu/top_nav_menu.tsx +++ b/src/plugins/navigation/public/top_nav_menu/top_nav_menu.tsx @@ -18,7 +18,7 @@ */ import React, { ReactElement } from 'react'; -import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { EuiHeaderLinks } from '@elastic/eui'; import classNames from 'classnames'; import { MountPoint } from '../../../../core/public'; @@ -81,31 +81,16 @@ export function TopNavMenu(props: TopNavMenuProps): ReactElement | null { function renderItems(): ReactElement[] | null { if (!config || config.length === 0) return null; return config.map((menuItem: TopNavMenuData, i: number) => { - return ( - - - - ); + return ; }); } function renderMenu(className: string): ReactElement | null { if (!config || config.length === 0) return null; return ( - + {renderItems()} - + ); } diff --git a/src/plugins/navigation/public/top_nav_menu/top_nav_menu_item.tsx b/src/plugins/navigation/public/top_nav_menu/top_nav_menu_item.tsx index b058ef0de448b..96a205b737273 100644 --- a/src/plugins/navigation/public/top_nav_menu/top_nav_menu_item.tsx +++ b/src/plugins/navigation/public/top_nav_menu/top_nav_menu_item.tsx @@ -19,9 +19,7 @@ import { upperFirst, isFunction } from 'lodash'; import React, { MouseEvent } from 'react'; -import { EuiButtonEmpty, EuiToolTip } from '@elastic/eui'; - -import { EuiButton } from '@elastic/eui'; +import { EuiToolTip, EuiButton, EuiHeaderLink } from '@elastic/eui'; import { TopNavMenuData } from './top_nav_menu_data'; export function TopNavMenuItem(props: TopNavMenuData) { @@ -50,13 +48,13 @@ export function TopNavMenuItem(props: TopNavMenuData) { }; const btn = props.emphasize ? ( - + {upperFirst(props.label || props.id!)} ) : ( - + {upperFirst(props.label || props.id!)} - + ); const tooltip = getTooltip(); diff --git a/src/plugins/newsfeed/public/components/newsfeed_header_nav_button.tsx b/src/plugins/newsfeed/public/components/newsfeed_header_nav_button.tsx index 888b807b5296f..628cfde18b0d5 100644 --- a/src/plugins/newsfeed/public/components/newsfeed_header_nav_button.tsx +++ b/src/plugins/newsfeed/public/components/newsfeed_header_nav_button.tsx @@ -68,7 +68,7 @@ export const NewsfeedNavButton = ({ apiFetchResult }: Props) => { aria-label="Newsfeed menu" onClick={showFlyout} > - + {showBadge ? ( ▪ diff --git a/src/plugins/share/server/routes/goto.ts b/src/plugins/share/server/routes/goto.ts index 193f2acb87c95..0cd6d26adac26 100644 --- a/src/plugins/share/server/routes/goto.ts +++ b/src/plugins/share/server/routes/goto.ts @@ -19,11 +19,11 @@ import { CoreSetup, IRouter } from 'kibana/server'; import { schema } from '@kbn/config-schema'; +import { modifyUrl } from '@kbn/std'; import { shortUrlAssertValid } from './lib/short_url_assert_valid'; import { ShortUrlLookupService } from './lib/short_url_lookup'; import { getGotoPath } from '../../common/short_url_routes'; -import { modifyUrl } from '../../../../core/server'; export const createGotoRoute = ({ router, diff --git a/src/plugins/timelion/public/_app.scss b/src/plugins/timelion/public/_app.scss index 3142e1d23cf10..8b9078caba5a8 100644 --- a/src/plugins/timelion/public/_app.scss +++ b/src/plugins/timelion/public/_app.scss @@ -1,8 +1,5 @@ -@import '@elastic/eui/src/global_styling/variables/header'; - .timApp { position: relative; - min-height: calc(100vh - #{$euiHeaderChildSize}); background: $euiColorEmptyShade; [ng-click] { diff --git a/src/plugins/timelion/public/plugin.ts b/src/plugins/timelion/public/plugin.ts index a92ced20cb6d1..24d1b9eb3fb65 100644 --- a/src/plugins/timelion/public/plugin.ts +++ b/src/plugins/timelion/public/plugin.ts @@ -91,7 +91,7 @@ export class TimelionPlugin implements Plugin { title: 'Timelion', order: 8000, defaultPath: '#/', - euiIconType: 'timelionApp', + euiIconType: 'logoKibana', category: DEFAULT_APP_CATEGORIES.kibana, updater$: this.appStateUpdater.asObservable(), mount: async (params: AppMountParameters) => { diff --git a/src/plugins/vis_type_timelion/server/plugin.ts b/src/plugins/vis_type_timelion/server/plugin.ts index 52c50b0646299..c518b73bb3cde 100644 --- a/src/plugins/vis_type_timelion/server/plugin.ts +++ b/src/plugins/vis_type_timelion/server/plugin.ts @@ -21,10 +21,10 @@ import { i18n } from '@kbn/i18n'; import { first } from 'rxjs/operators'; import { TypeOf, schema } from '@kbn/config-schema'; import { RecursiveReadonly } from '@kbn/utility-types'; +import { deepFreeze } from '@kbn/std'; import { PluginStart } from '../../../../src/plugins/data/server'; import { CoreSetup, PluginInitializerContext } from '../../../../src/core/server'; -import { deepFreeze } from '../../../../src/core/server'; import { configSchema } from '../config'; import loadFunctions from './lib/load_functions'; import { functionsRoute } from './routes/functions'; diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/filter_ratios.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/filter_ratios.js index 707ccd1e55bb0..13874166bc558 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/filter_ratios.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/filter_ratios.js @@ -18,22 +18,27 @@ */ const filter = (metric) => metric.type === 'filter_ratio'; +import { esQuery } from '../../../../../../data/server'; import { bucketTransform } from '../../helpers/bucket_transform'; import { overwrite } from '../../helpers'; import { calculateAggRoot } from './calculate_agg_root'; -export function ratios(req, panel) { +export function ratios(req, panel, esQueryConfig, indexPatternObject) { return (next) => (doc) => { panel.series.forEach((column) => { const aggRoot = calculateAggRoot(doc, column); if (column.metrics.some(filter)) { column.metrics.filter(filter).forEach((metric) => { - overwrite(doc, `${aggRoot}.timeseries.aggs.${metric.id}-numerator.filter`, { - query_string: { query: metric.numerator || '*', analyze_wildcard: true }, - }); - overwrite(doc, `${aggRoot}.timeseries.aggs.${metric.id}-denominator.filter`, { - query_string: { query: metric.denominator || '*', analyze_wildcard: true }, - }); + overwrite( + doc, + `${aggRoot}.timeseries.aggs.${metric.id}-numerator.filter`, + esQuery.buildEsQuery(indexPatternObject, metric.numerator, [], esQueryConfig) + ); + overwrite( + doc, + `${aggRoot}.timeseries.aggs.${metric.id}-denominator.filter`, + esQuery.buildEsQuery(indexPatternObject, metric.denominator, [], esQueryConfig) + ); let numeratorPath = `${metric.id}-numerator>_count`; let denominatorPath = `${metric.id}-denominator>_count`; @@ -46,7 +51,7 @@ export function ratios(req, panel) { }), }; overwrite(doc, `${aggRoot}.timeseries.aggs.${metric.id}-numerator.aggs`, aggBody); - overwrite(doc, `${aggBody}.timeseries.aggs.${metric.id}-denominator.aggs`, aggBody); + overwrite(doc, `${aggRoot}.timeseries.aggs.${metric.id}-denominator.aggs`, aggBody); numeratorPath = `${metric.id}-numerator>metric`; denominatorPath = `${metric.id}-denominator>metric`; } diff --git a/src/plugins/visualizations/kibana.json b/src/plugins/visualizations/kibana.json index da3edfbdd3bf5..0bd5de1d9ee15 100644 --- a/src/plugins/visualizations/kibana.json +++ b/src/plugins/visualizations/kibana.json @@ -3,6 +3,6 @@ "version": "kibana", "server": true, "ui": true, - "requiredPlugins": ["data", "expressions", "uiActions", "embeddable", "usageCollection", "inspector"], + "requiredPlugins": ["data", "expressions", "uiActions", "embeddable", "usageCollection", "inspector", "dashboard"], "requiredBundles": ["kibanaUtils", "discover", "savedObjects"] } diff --git a/src/plugins/visualizations/public/embeddable/create_vis_embeddable_from_object.ts b/src/plugins/visualizations/public/embeddable/create_vis_embeddable_from_object.ts index 194deef82a5f0..b27d24d980e8d 100644 --- a/src/plugins/visualizations/public/embeddable/create_vis_embeddable_from_object.ts +++ b/src/plugins/visualizations/public/embeddable/create_vis_embeddable_from_object.ts @@ -18,7 +18,13 @@ */ import { Vis } from '../types'; -import { VisualizeInput, VisualizeEmbeddable } from './visualize_embeddable'; +import { + VisualizeInput, + VisualizeEmbeddable, + VisualizeByValueInput, + VisualizeByReferenceInput, + VisualizeSavedObjectAttributes, +} from './visualize_embeddable'; import { IContainer, ErrorEmbeddable } from '../../../../plugins/embeddable/public'; import { DisabledLabEmbeddable } from './disabled_lab_embeddable'; import { @@ -30,10 +36,18 @@ import { } from '../services'; import { VisualizeEmbeddableFactoryDeps } from './visualize_embeddable_factory'; import { VISUALIZE_ENABLE_LABS_SETTING } from '../../common/constants'; +import { SavedVisualizationsLoader } from '../saved_visualizations'; +import { AttributeService } from '../../../dashboard/public'; export const createVisEmbeddableFromObject = (deps: VisualizeEmbeddableFactoryDeps) => async ( vis: Vis, input: Partial & { id: string }, + savedVisualizationsLoader?: SavedVisualizationsLoader, + attributeService?: AttributeService< + VisualizeSavedObjectAttributes, + VisualizeByValueInput, + VisualizeByReferenceInput + >, parent?: IContainer ): Promise => { const savedVisualizations = getSavedVisualizationsLoader(); @@ -55,6 +69,7 @@ export const createVisEmbeddableFromObject = (deps: VisualizeEmbeddableFactoryDe const indexPattern = vis.data.indexPattern; const indexPatterns = indexPattern ? [indexPattern] : []; const editable = getCapabilities().visualize.save as boolean; + return new VisualizeEmbeddable( getTimeFilter(), { @@ -66,6 +81,8 @@ export const createVisEmbeddableFromObject = (deps: VisualizeEmbeddableFactoryDe deps, }, input, + attributeService, + savedVisualizationsLoader, parent ); } catch (e) { diff --git a/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts b/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts index cc278a6ee9b3d..18ae68ec40fe5 100644 --- a/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts +++ b/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts @@ -20,6 +20,7 @@ import _, { get } from 'lodash'; import { Subscription } from 'rxjs'; import * as Rx from 'rxjs'; +import { i18n } from '@kbn/i18n'; import { VISUALIZE_EMBEDDABLE_TYPE } from './constants'; import { IIndexPattern, @@ -35,6 +36,8 @@ import { Embeddable, IContainer, Adapters, + SavedObjectEmbeddableInput, + ReferenceOrValueEmbeddable, } from '../../../../plugins/embeddable/public'; import { IExpressionLoaderParams, @@ -47,6 +50,10 @@ import { getExpressions, getUiActions } from '../services'; import { VIS_EVENT_TO_TRIGGER } from './events'; import { VisualizeEmbeddableFactoryDeps } from './visualize_embeddable_factory'; import { TriggerId } from '../../../ui_actions/public'; +import { SavedObjectAttributes } from '../../../../core/types'; +import { AttributeService } from '../../../dashboard/public'; +import { SavedVisualizationsLoader } from '../saved_visualizations'; +import { VisSavedObject } from '../types'; const getKeys = (o: T): Array => Object.keys(o) as Array; @@ -75,9 +82,19 @@ export interface VisualizeOutput extends EmbeddableOutput { visTypeName: string; } +export type VisualizeSavedObjectAttributes = SavedObjectAttributes & { + title: string; + vis?: Vis; + savedVis?: VisSavedObject; +}; +export type VisualizeByValueInput = { attributes: VisualizeSavedObjectAttributes } & VisualizeInput; +export type VisualizeByReferenceInput = SavedObjectEmbeddableInput & VisualizeInput; + type ExpressionLoader = InstanceType; -export class VisualizeEmbeddable extends Embeddable { +export class VisualizeEmbeddable + extends Embeddable + implements ReferenceOrValueEmbeddable { private handler?: ExpressionLoader; private timefilter: TimefilterContract; private timeRange?: TimeRange; @@ -93,11 +110,23 @@ export class VisualizeEmbeddable extends Embeddable; + private savedVisualizationsLoader?: SavedVisualizationsLoader; constructor( timefilter: TimefilterContract, { vis, editPath, editUrl, indexPatterns, editable, deps }: VisualizeEmbeddableConfiguration, initialInput: VisualizeInput, + attributeService?: AttributeService< + VisualizeSavedObjectAttributes, + VisualizeByValueInput, + VisualizeByReferenceInput + >, + savedVisualizationsLoader?: SavedVisualizationsLoader, parent?: IContainer ) { super( @@ -118,6 +147,8 @@ export class VisualizeEmbeddable extends Embeddable { + if (!this.attributeService) { + throw new Error('AttributeService must be defined for getInputAsRefType'); + } + return this.attributeService.inputIsRefType(input as VisualizeByReferenceInput); + }; + + getInputAsValueType = async (): Promise => { + const input = { + savedVis: this.vis.serialize(), + }; + if (this.getTitle()) { + input.savedVis.title = this.getTitle(); + } + delete input.savedVis.id; + return new Promise((resolve) => { + resolve({ ...(input as VisualizeByValueInput) }); + }); + }; + + getInputAsRefType = async (): Promise => { + const savedVis = await this.savedVisualizationsLoader?.get({}); + if (!savedVis) { + throw new Error('Error creating a saved vis object'); + } + if (!this.attributeService) { + throw new Error('AttributeService must be defined for getInputAsRefType'); + } + const saveModalTitle = this.getTitle() + ? this.getTitle() + : i18n.translate('visualizations.embeddable.placeholderTitle', { + defaultMessage: 'Placeholder Title', + }); + // @ts-ignore + const attributes: VisualizeSavedObjectAttributes = { + savedVis, + vis: this.vis, + title: this.vis.title, + }; + return this.attributeService.getInputAsRefType( + { + id: this.id, + attributes, + }, + { showSaveModal: true, saveModalTitle } + ); + }; } diff --git a/src/plugins/visualizations/public/embeddable/visualize_embeddable_factory.tsx b/src/plugins/visualizations/public/embeddable/visualize_embeddable_factory.tsx index b81ff5c166183..75e53e8e92dbe 100644 --- a/src/plugins/visualizations/public/embeddable/visualize_embeddable_factory.tsx +++ b/src/plugins/visualizations/public/embeddable/visualize_embeddable_factory.tsx @@ -28,7 +28,14 @@ import { IContainer, } from '../../../embeddable/public'; import { DisabledLabEmbeddable } from './disabled_lab_embeddable'; -import { VisualizeEmbeddable, VisualizeInput, VisualizeOutput } from './visualize_embeddable'; +import { + VisualizeByReferenceInput, + VisualizeByValueInput, + VisualizeEmbeddable, + VisualizeInput, + VisualizeOutput, + VisualizeSavedObjectAttributes, +} from './visualize_embeddable'; import { VISUALIZE_EMBEDDABLE_TYPE } from './constants'; import { SerializedVis, Vis } from '../vis'; import { @@ -43,13 +50,16 @@ import { createVisEmbeddableFromObject } from './create_vis_embeddable_from_obje import { StartServicesGetter } from '../../../kibana_utils/public'; import { VisualizationsStartDeps } from '../plugin'; import { VISUALIZE_ENABLE_LABS_SETTING } from '../../common/constants'; +import { AttributeService } from '../../../dashboard/public'; interface VisualizationAttributes extends SavedObjectAttributes { visState: string; } export interface VisualizeEmbeddableFactoryDeps { - start: StartServicesGetter>; + start: StartServicesGetter< + Pick + >; } export class VisualizeEmbeddableFactory @@ -62,6 +72,12 @@ export class VisualizeEmbeddableFactory > { public readonly type = VISUALIZE_EMBEDDABLE_TYPE; + private attributeService?: AttributeService< + VisualizeSavedObjectAttributes, + VisualizeByValueInput, + VisualizeByReferenceInput + >; + public readonly savedObjectMetaData: SavedObjectMetaData = { name: i18n.translate('visualizations.savedObjectName', { defaultMessage: 'Visualization' }), includeFields: ['visState'], @@ -105,6 +121,19 @@ export class VisualizeEmbeddableFactory return await this.deps.start().core.application.currentAppId$.pipe(first()).toPromise(); } + private async getAttributeService() { + if (!this.attributeService) { + this.attributeService = await this.deps + .start() + .plugins.dashboard.getAttributeService< + VisualizeSavedObjectAttributes, + VisualizeByValueInput, + VisualizeByReferenceInput + >(this.type, { customSaveMethod: this.onSave }); + } + return this.attributeService!; + } + public async createFromSavedObject( savedObjectId: string, input: Partial & { id: string }, @@ -117,7 +146,13 @@ export class VisualizeEmbeddableFactory const visState = convertToSerializedVis(savedObject); const vis = new Vis(savedObject.visState.type, visState); await vis.setState(visState); - return createVisEmbeddableFromObject(this.deps)(vis, input, parent); + return createVisEmbeddableFromObject(this.deps)( + vis, + input, + savedVisualizations, + await this.getAttributeService(), + parent + ); } catch (e) { console.error(e); // eslint-disable-line no-console return new ErrorEmbeddable(e, input, parent); @@ -131,7 +166,14 @@ export class VisualizeEmbeddableFactory const visState = input.savedVis; const vis = new Vis(visState.type, visState); await vis.setState(visState); - return createVisEmbeddableFromObject(this.deps)(vis, input, parent); + const savedVisualizations = getSavedVisualizationsLoader(); + return createVisEmbeddableFromObject(this.deps)( + vis, + input, + savedVisualizations, + await this.getAttributeService(), + parent + ); } else { showNewVisModal({ originatingApp: await this.getCurrentAppId(), @@ -140,4 +182,47 @@ export class VisualizeEmbeddableFactory return undefined; } } + + private async onSave( + type: string, + attributes: VisualizeSavedObjectAttributes + ): Promise<{ id: string }> { + try { + const { title, savedVis } = attributes; + const visObj = attributes.vis; + if (!savedVis) { + throw new Error('No Saved Vis'); + } + const saveOptions = { + confirmOverwrite: false, + returnToOrigin: true, + }; + savedVis.title = title; + savedVis.copyOnSave = false; + savedVis.description = ''; + savedVis.searchSourceFields = visObj?.data.searchSource?.getSerializedFields(); + const serializedVis = ((visObj as unknown) as Vis).serialize(); + const { params, data } = serializedVis; + savedVis.visState = { + title, + type: serializedVis.type, + params, + aggs: data.aggs, + }; + if (visObj) { + savedVis.uiStateJSON = visObj?.uiState.toString(); + } + const id = await savedVis.save(saveOptions); + if (!id || id === '') { + throw new Error( + i18n.translate('visualizations.savingVisualizationFailed.errorMsg', { + defaultMessage: 'Saving a visualization failed', + }) + ); + } + return { id }; + } catch (error) { + throw error; + } + } } diff --git a/src/plugins/visualizations/public/mocks.ts b/src/plugins/visualizations/public/mocks.ts index e0ec4801b3caf..646acc49a6a83 100644 --- a/src/plugins/visualizations/public/mocks.ts +++ b/src/plugins/visualizations/public/mocks.ts @@ -27,6 +27,7 @@ import { dataPluginMock } from '../../../plugins/data/public/mocks'; import { usageCollectionPluginMock } from '../../../plugins/usage_collection/public/mocks'; import { uiActionsPluginMock } from '../../../plugins/ui_actions/public/mocks'; import { inspectorPluginMock } from '../../../plugins/inspector/public/mocks'; +import { dashboardPluginMock } from '../../../plugins/dashboard/public/mocks'; const createSetupContract = (): VisualizationsSetup => ({ createBaseVisualization: jest.fn(), @@ -69,6 +70,8 @@ const createInstance = async () => { uiActions: uiActionsPluginMock.createStartContract(), application: applicationServiceMock.createStartContract(), embeddable: embeddablePluginMock.createStartContract(), + dashboard: dashboardPluginMock.createStartContract(), + getAttributeService: jest.fn(), }); return { diff --git a/src/plugins/visualizations/public/plugin.ts b/src/plugins/visualizations/public/plugin.ts index 3546fa4056491..0ba80887b513f 100644 --- a/src/plugins/visualizations/public/plugin.ts +++ b/src/plugins/visualizations/public/plugin.ts @@ -76,6 +76,7 @@ import { convertToSerializedVis, } from './saved_visualizations/_saved_vis'; import { createSavedSearchesLoader } from '../../discover/public'; +import { DashboardStart } from '../../dashboard/public'; /** * Interface for this plugin's returned setup/start contracts. @@ -109,6 +110,8 @@ export interface VisualizationsStartDeps { inspector: InspectorStart; uiActions: UiActionsStart; application: ApplicationStart; + dashboard: DashboardStart; + getAttributeService: DashboardStart['getAttributeService']; } /** @@ -155,7 +158,7 @@ export class VisualizationsPlugin public start( core: CoreStart, - { data, expressions, uiActions, embeddable }: VisualizationsStartDeps + { data, expressions, uiActions, embeddable, dashboard }: VisualizationsStartDeps ): VisualizationsStart { const types = this.types.start(); setI18n(core.i18n); diff --git a/src/plugins/visualize/public/application/components/visualize_top_nav.tsx b/src/plugins/visualize/public/application/components/visualize_top_nav.tsx index 130561b6245ae..dfd3c09f51ed5 100644 --- a/src/plugins/visualize/public/application/components/visualize_top_nav.tsx +++ b/src/plugins/visualize/public/application/components/visualize_top_nav.tsx @@ -61,6 +61,7 @@ const TopNav = ({ }: VisualizeTopNavProps) => { const { services } = useKibana(); const { TopNavMenu } = services.navigation.ui; + const { setHeaderActionMenu } = services; const { embeddableHandler, vis } = visInstance; const [inspectorSession, setInspectorSession] = useState(); const openInspector = useCallback(() => { @@ -151,6 +152,7 @@ const TopNav = ({ void; scopedHistory: ScopedHistory; dashboard: DashboardStart; + setHeaderActionMenu: AppMountParameters['setHeaderActionMenu']; } export interface SavedVisInstance { diff --git a/src/plugins/visualize/public/plugin.ts b/src/plugins/visualize/public/plugin.ts index 95d5343d5d695..86159a13379a1 100644 --- a/src/plugins/visualize/public/plugin.ts +++ b/src/plugins/visualize/public/plugin.ts @@ -140,7 +140,7 @@ export class VisualizePlugin id: 'visualize', title: 'Visualize', order: 8000, - euiIconType: 'visualizeApp', + euiIconType: 'logoKibana', defaultPath: '#/', category: DEFAULT_APP_CATEGORIES.kibana, updater$: this.appStateUpdater.asObservable(), @@ -196,12 +196,14 @@ export class VisualizePlugin scopedHistory: params.history, restorePreviousUrl, dashboard: pluginsStart.dashboard, + setHeaderActionMenu: params.setHeaderActionMenu, }; params.element.classList.add('visAppWrapper'); const { renderApp } = await import('./application'); const unmount = renderApp(params, services); return () => { + params.element.classList.remove('visAppWrapper'); unlistenParentHistory(); unmount(); appUnMounted(); diff --git a/test/accessibility/apps/discover.ts b/test/accessibility/apps/discover.ts index 7e905fbe89fbd..44639af9da9f8 100644 --- a/test/accessibility/apps/discover.ts +++ b/test/accessibility/apps/discover.ts @@ -34,7 +34,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { ['geo.src', 'IN'], ]; - describe('Discover', () => { + // Failing: See https://github.com/elastic/kibana/issues/59975 + describe.skip('Discover', () => { before(async () => { await esArchiver.load('discover'); await esArchiver.loadIfNeeded('logstash_functional'); diff --git a/test/accessibility/apps/management.ts b/test/accessibility/apps/management.ts index b37500f5b15dd..08177f54ee881 100644 --- a/test/accessibility/apps/management.ts +++ b/test/accessibility/apps/management.ts @@ -64,8 +64,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await a11y.testAppSnapshot(); }); - // Will be enabling this and field formatters after this issue is addressed: https://github.com/elastic/kibana/issues/60030 - it.skip('Edit field type', async () => { + it('Edit field type', async () => { await PageObjects.settings.clickEditFieldFormat(); await a11y.testAppSnapshot(); }); diff --git a/test/functional/page_objects/time_picker.ts b/test/functional/page_objects/time_picker.ts index 8a726cee444c1..237dc8946ae0e 100644 --- a/test/functional/page_objects/time_picker.ts +++ b/test/functional/page_objects/time_picker.ts @@ -39,7 +39,7 @@ export function TimePickerProvider({ getService, getPageObjects }: FtrProviderCo const find = getService('find'); const browser = getService('browser'); const testSubjects = getService('testSubjects'); - const { header, common } = getPageObjects(['header', 'common']); + const { header } = getPageObjects(['header']); const kibanaServer = getService('kibanaServer'); class TimePicker { @@ -127,7 +127,7 @@ export function TimePickerProvider({ getService, getPageObjects }: FtrProviderCo await testSubjects.click('superDatePickerAbsoluteTab'); await testSubjects.click('superDatePickerAbsoluteDateInput'); await this.inputValue('superDatePickerAbsoluteDateInput', toTime); - await common.sleep(500); + await browser.pressKeys(browser.keys.ESCAPE); // close popover because sometimes browser can't find start input // set from time await testSubjects.click('superDatePickerstartDatePopoverButton'); diff --git a/test/functional/page_objects/vega_chart_page.ts b/test/functional/page_objects/vega_chart_page.ts index 557c6bfada01e..044b6b61790ac 100644 --- a/test/functional/page_objects/vega_chart_page.ts +++ b/test/functional/page_objects/vega_chart_page.ts @@ -17,7 +17,6 @@ * under the License. */ -import { Key } from 'selenium-webdriver'; import expect from '@kbn/expect'; import { FtrProviderContext } from '../ftr_provider_context'; @@ -94,9 +93,9 @@ export function VegaChartPageProvider({ const aceGutter = await this.getAceGutterContainer(); await aceGutter.doubleClick(); - await browser.pressKeys(Key.RIGHT); - await browser.pressKeys(Key.LEFT); - await browser.pressKeys(Key.LEFT); + await browser.pressKeys(browser.keys.RIGHT); + await browser.pressKeys(browser.keys.LEFT); + await browser.pressKeys(browser.keys.LEFT); await browser.pressKeys(text); } @@ -105,7 +104,7 @@ export function VegaChartPageProvider({ await retry.try(async () => { await aceGutter.doubleClick(); - await browser.pressKeys(Key.BACK_SPACE); + await browser.pressKeys(browser.keys.BACK_SPACE); expect(await this.getSpec()).to.be(''); }); diff --git a/test/functional/page_objects/visual_builder_page.ts b/test/functional/page_objects/visual_builder_page.ts index a95d535281fa3..6e49fd3b03494 100644 --- a/test/functional/page_objects/visual_builder_page.ts +++ b/test/functional/page_objects/visual_builder_page.ts @@ -58,7 +58,10 @@ export function VisualBuilderPageProvider({ getService, getPageObjects }: FtrPro } public async checkTabIsLoaded(testSubj: string, name: string) { - const isPresent = await testSubjects.exists(testSubj, { timeout: 10000 }); + let isPresent = false; + await retry.try(async () => { + isPresent = await testSubjects.exists(testSubj, { timeout: 20000 }); + }); if (!isPresent) { throw new Error(`TSVB ${name} tab is not loaded`); } @@ -130,8 +133,8 @@ export function VisualBuilderPageProvider({ getService, getPageObjects }: FtrPro public async enterMarkdown(markdown: string) { const input = await find.byCssSelector('.tvbMarkdownEditor__editor textarea'); await this.clearMarkdown(); - await input.type(markdown, { charByChar: true }); - await PageObjects.visChart.waitForVisualizationRenderingStabilized(); + await input.type(markdown); + await PageObjects.common.sleep(3000); } public async clearMarkdown() { diff --git a/test/functional/services/common/browser.ts b/test/functional/services/common/browser.ts index e81845023a8fa..2f8e87c1d58d6 100644 --- a/test/functional/services/common/browser.ts +++ b/test/functional/services/common/browser.ts @@ -22,9 +22,9 @@ import { Key, Origin } from 'selenium-webdriver'; // @ts-ignore internal modules are not typed import { LegacyActionSequence } from 'selenium-webdriver/lib/actions'; import { ProvidedType } from '@kbn/test/types/ftr'; +import { modifyUrl } from '@kbn/std'; import Jimp from 'jimp'; -import { modifyUrl } from '../../../../src/core/utils'; import { WebElementWrapper } from '../lib/web_element_wrapper'; import { FtrProviderContext } from '../../ftr_provider_context'; import { Browsers } from '../remote/browsers'; diff --git a/test/functional/services/listing_table.ts b/test/functional/services/listing_table.ts index 3f9775d7a75f3..aebdc734cfb39 100644 --- a/test/functional/services/listing_table.ts +++ b/test/functional/services/listing_table.ts @@ -33,7 +33,7 @@ export function ListingTableProvider({ getService, getPageObjects }: FtrProvider */ class ListingTable { private async getSearchFilter() { - const searchFilter = await find.allByCssSelector('.euiFieldSearch'); + const searchFilter = await find.allByCssSelector('main .euiFieldSearch'); return searchFilter[0]; } diff --git a/test/interpreter_functional/plugins/kbn_tp_run_pipeline/tsconfig.json b/test/interpreter_functional/plugins/kbn_tp_run_pipeline/tsconfig.json index 4d979fbf7f15f..f77a5eaffc301 100644 --- a/test/interpreter_functional/plugins/kbn_tp_run_pipeline/tsconfig.json +++ b/test/interpreter_functional/plugins/kbn_tp_run_pipeline/tsconfig.json @@ -10,5 +10,8 @@ "public/**/*.tsx", "../../../../typings/**/*", ], - "exclude": [] + "exclude": [], + "references": [ + { "path": "../../../../src/core/tsconfig.json" } + ] } diff --git a/test/plugin_functional/plugins/app_link_test/tsconfig.json b/test/plugin_functional/plugins/app_link_test/tsconfig.json index 4d979fbf7f15f..f77a5eaffc301 100644 --- a/test/plugin_functional/plugins/app_link_test/tsconfig.json +++ b/test/plugin_functional/plugins/app_link_test/tsconfig.json @@ -10,5 +10,8 @@ "public/**/*.tsx", "../../../../typings/**/*", ], - "exclude": [] + "exclude": [], + "references": [ + { "path": "../../../../src/core/tsconfig.json" } + ] } diff --git a/test/plugin_functional/plugins/core_plugin_a/tsconfig.json b/test/plugin_functional/plugins/core_plugin_a/tsconfig.json index ccbffc34bce4a..3d9d8ca9451d4 100644 --- a/test/plugin_functional/plugins/core_plugin_a/tsconfig.json +++ b/test/plugin_functional/plugins/core_plugin_a/tsconfig.json @@ -11,5 +11,8 @@ "server/**/*.ts", "../../../../typings/**/*", ], - "exclude": [] + "exclude": [], + "references": [ + { "path": "../../../../src/core/tsconfig.json" } + ] } diff --git a/test/plugin_functional/plugins/core_plugin_appleave/tsconfig.json b/test/plugin_functional/plugins/core_plugin_appleave/tsconfig.json index 4d979fbf7f15f..f77a5eaffc301 100644 --- a/test/plugin_functional/plugins/core_plugin_appleave/tsconfig.json +++ b/test/plugin_functional/plugins/core_plugin_appleave/tsconfig.json @@ -10,5 +10,8 @@ "public/**/*.tsx", "../../../../typings/**/*", ], - "exclude": [] + "exclude": [], + "references": [ + { "path": "../../../../src/core/tsconfig.json" } + ] } diff --git a/test/plugin_functional/plugins/core_plugin_b/tsconfig.json b/test/plugin_functional/plugins/core_plugin_b/tsconfig.json index ccbffc34bce4a..3d9d8ca9451d4 100644 --- a/test/plugin_functional/plugins/core_plugin_b/tsconfig.json +++ b/test/plugin_functional/plugins/core_plugin_b/tsconfig.json @@ -11,5 +11,8 @@ "server/**/*.ts", "../../../../typings/**/*", ], - "exclude": [] + "exclude": [], + "references": [ + { "path": "../../../../src/core/tsconfig.json" } + ] } diff --git a/test/plugin_functional/plugins/core_plugin_chromeless/tsconfig.json b/test/plugin_functional/plugins/core_plugin_chromeless/tsconfig.json index 4d979fbf7f15f..f77a5eaffc301 100644 --- a/test/plugin_functional/plugins/core_plugin_chromeless/tsconfig.json +++ b/test/plugin_functional/plugins/core_plugin_chromeless/tsconfig.json @@ -10,5 +10,8 @@ "public/**/*.tsx", "../../../../typings/**/*", ], - "exclude": [] + "exclude": [], + "references": [ + { "path": "../../../../src/core/tsconfig.json" } + ] } diff --git a/test/plugin_functional/plugins/core_plugin_route_timeouts/tsconfig.json b/test/plugin_functional/plugins/core_plugin_route_timeouts/tsconfig.json index 0e27246a49980..72793a327d97e 100644 --- a/test/plugin_functional/plugins/core_plugin_route_timeouts/tsconfig.json +++ b/test/plugin_functional/plugins/core_plugin_route_timeouts/tsconfig.json @@ -8,5 +8,8 @@ "server/**/*.ts", "../../../../typings/**/*" ], - "exclude": [] + "exclude": [], + "references": [ + { "path": "../../../../src/core/tsconfig.json" } + ] } diff --git a/test/plugin_functional/plugins/core_plugin_static_assets/tsconfig.json b/test/plugin_functional/plugins/core_plugin_static_assets/tsconfig.json index 433b041b9af3f..f9b0443e0a8bf 100644 --- a/test/plugin_functional/plugins/core_plugin_static_assets/tsconfig.json +++ b/test/plugin_functional/plugins/core_plugin_static_assets/tsconfig.json @@ -10,5 +10,8 @@ "public/**/*.tsx", "../../../../typings/**/*" ], - "exclude": [] + "exclude": [], + "references": [ + { "path": "../../../../src/core/tsconfig.json" } + ] } diff --git a/test/plugin_functional/plugins/core_provider_plugin/tsconfig.json b/test/plugin_functional/plugins/core_provider_plugin/tsconfig.json index 0aac2eb570987..eacd2f5e9aee3 100644 --- a/test/plugin_functional/plugins/core_provider_plugin/tsconfig.json +++ b/test/plugin_functional/plugins/core_provider_plugin/tsconfig.json @@ -10,5 +10,8 @@ "public/**/*.ts", "../../../../typings/**/*" ], - "exclude": [] + "exclude": [], + "references": [ + { "path": "../../../../src/core/tsconfig.json" } + ] } diff --git a/test/plugin_functional/plugins/doc_views_plugin/tsconfig.json b/test/plugin_functional/plugins/doc_views_plugin/tsconfig.json index 433b041b9af3f..f9b0443e0a8bf 100644 --- a/test/plugin_functional/plugins/doc_views_plugin/tsconfig.json +++ b/test/plugin_functional/plugins/doc_views_plugin/tsconfig.json @@ -10,5 +10,8 @@ "public/**/*.tsx", "../../../../typings/**/*" ], - "exclude": [] + "exclude": [], + "references": [ + { "path": "../../../../src/core/tsconfig.json" } + ] } diff --git a/test/plugin_functional/plugins/elasticsearch_client_plugin/tsconfig.json b/test/plugin_functional/plugins/elasticsearch_client_plugin/tsconfig.json index 0e27246a49980..72793a327d97e 100644 --- a/test/plugin_functional/plugins/elasticsearch_client_plugin/tsconfig.json +++ b/test/plugin_functional/plugins/elasticsearch_client_plugin/tsconfig.json @@ -8,5 +8,8 @@ "server/**/*.ts", "../../../../typings/**/*" ], - "exclude": [] + "exclude": [], + "references": [ + { "path": "../../../../src/core/tsconfig.json" } + ] } diff --git a/test/plugin_functional/plugins/index_patterns/tsconfig.json b/test/plugin_functional/plugins/index_patterns/tsconfig.json index d5ccf7adddb2d..9d11a9850f151 100644 --- a/test/plugin_functional/plugins/index_patterns/tsconfig.json +++ b/test/plugin_functional/plugins/index_patterns/tsconfig.json @@ -10,5 +10,8 @@ "server/**/*.tsx", "../../../../typings/**/*", ], - "exclude": [] + "exclude": [], + "references": [ + { "path": "../../../../src/core/tsconfig.json" } + ] } diff --git a/test/plugin_functional/plugins/kbn_sample_panel_action/tsconfig.json b/test/plugin_functional/plugins/kbn_sample_panel_action/tsconfig.json index 4d979fbf7f15f..f77a5eaffc301 100644 --- a/test/plugin_functional/plugins/kbn_sample_panel_action/tsconfig.json +++ b/test/plugin_functional/plugins/kbn_sample_panel_action/tsconfig.json @@ -10,5 +10,8 @@ "public/**/*.tsx", "../../../../typings/**/*", ], - "exclude": [] + "exclude": [], + "references": [ + { "path": "../../../../src/core/tsconfig.json" } + ] } diff --git a/test/plugin_functional/plugins/kbn_top_nav/tsconfig.json b/test/plugin_functional/plugins/kbn_top_nav/tsconfig.json index ccbffc34bce4a..3d9d8ca9451d4 100644 --- a/test/plugin_functional/plugins/kbn_top_nav/tsconfig.json +++ b/test/plugin_functional/plugins/kbn_top_nav/tsconfig.json @@ -11,5 +11,8 @@ "server/**/*.ts", "../../../../typings/**/*", ], - "exclude": [] + "exclude": [], + "references": [ + { "path": "../../../../src/core/tsconfig.json" } + ] } diff --git a/test/plugin_functional/plugins/kbn_tp_custom_visualizations/tsconfig.json b/test/plugin_functional/plugins/kbn_tp_custom_visualizations/tsconfig.json index 7e2f7c97eb058..b704274a58aa4 100644 --- a/test/plugin_functional/plugins/kbn_tp_custom_visualizations/tsconfig.json +++ b/test/plugin_functional/plugins/kbn_tp_custom_visualizations/tsconfig.json @@ -15,5 +15,8 @@ "public/**/*.tsx", "../../../../typings/**/*" ], - "exclude": [] + "exclude": [], + "references": [ + { "path": "../../../../src/core/tsconfig.json" } + ] } diff --git a/test/plugin_functional/plugins/management_test_plugin/tsconfig.json b/test/plugin_functional/plugins/management_test_plugin/tsconfig.json index 4d979fbf7f15f..f77a5eaffc301 100644 --- a/test/plugin_functional/plugins/management_test_plugin/tsconfig.json +++ b/test/plugin_functional/plugins/management_test_plugin/tsconfig.json @@ -10,5 +10,8 @@ "public/**/*.tsx", "../../../../typings/**/*", ], - "exclude": [] + "exclude": [], + "references": [ + { "path": "../../../../src/core/tsconfig.json" } + ] } diff --git a/test/plugin_functional/plugins/rendering_plugin/tsconfig.json b/test/plugin_functional/plugins/rendering_plugin/tsconfig.json index ccbffc34bce4a..3d9d8ca9451d4 100644 --- a/test/plugin_functional/plugins/rendering_plugin/tsconfig.json +++ b/test/plugin_functional/plugins/rendering_plugin/tsconfig.json @@ -11,5 +11,8 @@ "server/**/*.ts", "../../../../typings/**/*", ], - "exclude": [] + "exclude": [], + "references": [ + { "path": "../../../../src/core/tsconfig.json" } + ] } diff --git a/test/plugin_functional/plugins/ui_settings_plugin/tsconfig.json b/test/plugin_functional/plugins/ui_settings_plugin/tsconfig.json index 33b2d6c0f64ad..05b1c2c11da03 100644 --- a/test/plugin_functional/plugins/ui_settings_plugin/tsconfig.json +++ b/test/plugin_functional/plugins/ui_settings_plugin/tsconfig.json @@ -8,5 +8,8 @@ "server/**/*.ts", "../../../../typings/**/*", ], - "exclude": [] + "exclude": [], + "references": [ + { "path": "../../../../src/core/tsconfig.json" } + ] } diff --git a/test/tsconfig.json b/test/tsconfig.json index 325db48978022..6e846690d54d9 100644 --- a/test/tsconfig.json +++ b/test/tsconfig.json @@ -16,5 +16,8 @@ "exclude": [ "plugin_functional/plugins/**/*", "interpreter_functional/plugins/**/*" + ], + "references": [ + { "path": "../src/core/tsconfig.json" } ] } diff --git a/tsconfig.json b/tsconfig.json index 21a817213c62f..11838627c5319 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,7 +1,7 @@ { "extends": "./tsconfig.base.json", "compilerOptions": { - "tsBuildInfoFile": "./build/tsbuildinfo/kibana", + "tsBuildInfoFile": "./build/tsbuildinfo/kibana" }, "include": [ "kibana.d.ts", @@ -11,7 +11,8 @@ ], "exclude": [ "src/**/__fixtures__/**/*", - "src/test_utils/**/*" + "src/test_utils/**/*", + "src/core/**/*" // In the build we actually exclude **/public/**/* from this config so that // we can run the TSC on both this and the .browser version of this config // file, but if we did it during development IDEs would not be able to find @@ -19,6 +20,7 @@ // "src/**/public/**/*" ], "references": [ - { "path": "./src/test_utils/tsconfig.json" } + { "path": "./src/test_utils/tsconfig.json" }, + { "path": "./src/core/tsconfig.json" } ] } diff --git a/tsconfig.refs.json b/tsconfig.refs.json index 0e4cc1bd32da3..66282687f633b 100644 --- a/tsconfig.refs.json +++ b/tsconfig.refs.json @@ -1,6 +1,7 @@ { "include": [], "references": [ - { "path": "./src/test_utils" } + { "path": "./src/test_utils" }, + { "path": "./src/core" }, ] } diff --git a/x-pack/.i18nrc.json b/x-pack/.i18nrc.json index 7d21f958bb80b..bdd0fbea35fa8 100644 --- a/x-pack/.i18nrc.json +++ b/x-pack/.i18nrc.json @@ -2,7 +2,10 @@ "prefix": "xpack", "paths": { "xpack.actions": "plugins/actions", - "xpack.uiActionsEnhanced": ["plugins/ui_actions_enhanced", "examples/ui_actions_enhanced_examples"], + "xpack.uiActionsEnhanced": [ + "plugins/ui_actions_enhanced", + "examples/ui_actions_enhanced_examples" + ], "xpack.alerts": "plugins/alerts", "xpack.eventLog": "plugins/event_log", "xpack.alertingBuiltins": "plugins/alerting_builtins", @@ -21,6 +24,7 @@ "xpack.features": "plugins/features", "xpack.fileUpload": "plugins/file_upload", "xpack.globalSearch": ["plugins/global_search"], + "xpack.globalSearchBar": ["plugins/global_search_bar"], "xpack.graph": ["plugins/graph"], "xpack.grokDebugger": "plugins/grokdebugger", "xpack.idxMgmt": "plugins/index_management", diff --git a/x-pack/examples/ui_actions_enhanced_examples/tsconfig.json b/x-pack/examples/ui_actions_enhanced_examples/tsconfig.json index cae75bb81c723..9f4f010c20fec 100644 --- a/x-pack/examples/ui_actions_enhanced_examples/tsconfig.json +++ b/x-pack/examples/ui_actions_enhanced_examples/tsconfig.json @@ -11,5 +11,8 @@ "server/**/*.ts", "../../typings/**/*" ], - "exclude": [] + "exclude": [], + "references": [ + { "path": "../../../src/core/tsconfig.json" } + ] } diff --git a/x-pack/package.json b/x-pack/package.json index 1e2fa4d7ee550..3e1e1173be2d7 100644 --- a/x-pack/package.json +++ b/x-pack/package.json @@ -95,7 +95,6 @@ "@types/jsdom": "^16.2.3", "@types/json-stable-stringify": "^1.0.32", "@types/jsonwebtoken": "^7.2.8", - "@types/lodash": "^4.14.159", "@types/mapbox-gl": "^1.9.1", "@types/memoize-one": "^4.1.0", "@types/mime": "^2.0.1", @@ -338,7 +337,7 @@ "js-yaml": "3.13.1", "json-stable-stringify": "^1.0.1", "jsonwebtoken": "^8.5.1", - "lodash": "^4.17.15", + "lodash": "^4.17.20", "markdown-it": "^10.0.0", "mime": "^2.4.4", "moment": "^2.24.0", diff --git a/x-pack/plugins/actions/server/lib/license_state.ts b/x-pack/plugins/actions/server/lib/license_state.ts index 914aada08bb2c..1686d0201e96c 100644 --- a/x-pack/plugins/actions/server/lib/license_state.ts +++ b/x-pack/plugins/actions/server/lib/license_state.ts @@ -6,7 +6,7 @@ import { i18n } from '@kbn/i18n'; import { Observable, Subscription } from 'rxjs'; -import { assertNever } from '../../../../../src/core/server'; +import { assertNever } from '@kbn/std'; import { ILicense } from '../../../licensing/common/types'; import { PLUGIN } from '../constants/plugin'; import { ActionType } from '../types'; diff --git a/x-pack/plugins/alerts/server/alerts_client.test.ts b/x-pack/plugins/alerts/server/alerts_client.test.ts index 801c2c8775361..b20e6c6be2ebf 100644 --- a/x-pack/plugins/alerts/server/alerts_client.test.ts +++ b/x-pack/plugins/alerts/server/alerts_client.test.ts @@ -7,6 +7,8 @@ import uuid from 'uuid'; import { schema } from '@kbn/config-schema'; import { AlertsClient, CreateOptions, ConstructorOptions } from './alerts_client'; import { savedObjectsClientMock, loggingSystemMock } from '../../../../src/core/server/mocks'; +import { nodeTypes } from '../../../../src/plugins/data/common'; +import { esKuery } from '../../../../src/plugins/data/server'; import { taskManagerMock } from '../../task_manager/server/task_manager.mock'; import { alertTypeRegistryMock } from './alert_type_registry.mock'; import { alertsAuthorizationMock } from './authorization/alerts_authorization.mock'; @@ -2722,6 +2724,7 @@ describe('find()', () => { Array [ Object { "fields": undefined, + "filter": undefined, "type": "alert", }, ] @@ -2730,9 +2733,11 @@ describe('find()', () => { describe('authorization', () => { test('ensures user is query filter types down to those the user is authorized to find', async () => { + const filter = esKuery.fromKueryExpression( + '((alert.attributes.alertTypeId:myType and alert.attributes.consumer:myApp) or (alert.attributes.alertTypeId:myOtherType and alert.attributes.consumer:myApp) or (alert.attributes.alertTypeId:myOtherType and alert.attributes.consumer:myOtherApp))' + ); authorization.getFindAuthorizationFilter.mockResolvedValue({ - filter: - '((alert.attributes.alertTypeId:myType and alert.attributes.consumer:myApp) or (alert.attributes.alertTypeId:myOtherType and alert.attributes.consumer:myApp) or (alert.attributes.alertTypeId:myOtherType and alert.attributes.consumer:myOtherApp))', + filter, ensureAlertTypeIsAuthorized() {}, logSuccessfulAuthorization() {}, }); @@ -2741,8 +2746,8 @@ describe('find()', () => { await alertsClient.find({ options: { filter: 'someTerm' } }); const [options] = unsecuredSavedObjectsClient.find.mock.calls[0]; - expect(options.filter).toMatchInlineSnapshot( - `"someTerm and ((alert.attributes.alertTypeId:myType and alert.attributes.consumer:myApp) or (alert.attributes.alertTypeId:myOtherType and alert.attributes.consumer:myApp) or (alert.attributes.alertTypeId:myOtherType and alert.attributes.consumer:myOtherApp))"` + expect(options.filter).toEqual( + nodeTypes.function.buildNode('and', [esKuery.fromKueryExpression('someTerm'), filter]) ); expect(authorization.getFindAuthorizationFilter).toHaveBeenCalledTimes(1); }); @@ -2759,7 +2764,6 @@ describe('find()', () => { const ensureAlertTypeIsAuthorized = jest.fn(); const logSuccessfulAuthorization = jest.fn(); authorization.getFindAuthorizationFilter.mockResolvedValue({ - filter: '', ensureAlertTypeIsAuthorized, logSuccessfulAuthorization, }); diff --git a/x-pack/plugins/alerts/server/alerts_client.ts b/x-pack/plugins/alerts/server/alerts_client.ts index 0703a1e13937c..f8da17f4bf089 100644 --- a/x-pack/plugins/alerts/server/alerts_client.ts +++ b/x-pack/plugins/alerts/server/alerts_client.ts @@ -13,6 +13,7 @@ import { SavedObjectReference, SavedObject, } from 'src/core/server'; +import { esKuery } from '../../../../src/plugins/data/server'; import { ActionsClient, ActionsAuthorization } from '../../actions/server'; import { Alert, @@ -37,11 +38,7 @@ import { TaskManagerStartContract } from '../../task_manager/server'; import { taskInstanceToAlertTaskInstance } from './task_runner/alert_task_instance'; import { deleteTaskIfItExists } from './lib/delete_task_if_it_exists'; import { RegistryAlertType } from './alert_type_registry'; -import { - AlertsAuthorization, - WriteOperations, - ReadOperations, -} from './authorization/alerts_authorization'; +import { AlertsAuthorization, WriteOperations, ReadOperations, and } from './authorization'; import { IEventLogClient } from '../../../plugins/event_log/server'; import { parseIsoOrRelativeDate } from './lib/iso_or_relative_date'; import { alertInstanceSummaryFromEventLog } from './lib/alert_instance_summary_from_event_log'; @@ -339,11 +336,6 @@ export class AlertsClient { logSuccessfulAuthorization, } = await this.authorization.getFindAuthorizationFilter(); - if (authorizationFilter) { - options.filter = options.filter - ? `${options.filter} and ${authorizationFilter}` - : authorizationFilter; - } const { page, per_page: perPage, @@ -351,6 +343,10 @@ export class AlertsClient { saved_objects: data, } = await this.unsecuredSavedObjectsClient.find({ ...options, + filter: + (authorizationFilter && options.filter + ? and([esKuery.fromKueryExpression(options.filter), authorizationFilter]) + : authorizationFilter) ?? options.filter, fields: fields ? this.includeFieldsRequiredForAuthentication(fields) : fields, type: 'alert', }); diff --git a/x-pack/plugins/alerts/server/authorization/alerts_authorization.test.ts b/x-pack/plugins/alerts/server/authorization/alerts_authorization.test.ts index c2506381b9df9..9515987af8dd9 100644 --- a/x-pack/plugins/alerts/server/authorization/alerts_authorization.test.ts +++ b/x-pack/plugins/alerts/server/authorization/alerts_authorization.test.ts @@ -6,17 +6,13 @@ import { KibanaRequest } from 'kibana/server'; import { alertTypeRegistryMock } from '../alert_type_registry.mock'; import { securityMock } from '../../../../plugins/security/server/mocks'; +import { esKuery } from '../../../../../src/plugins/data/server'; import { PluginStartContract as FeaturesStartContract, KibanaFeature, } from '../../../features/server'; import { featuresPluginMock } from '../../../features/server/mocks'; -import { - AlertsAuthorization, - ensureFieldIsSafeForQuery, - WriteOperations, - ReadOperations, -} from './alerts_authorization'; +import { AlertsAuthorization, WriteOperations, ReadOperations } from './alerts_authorization'; import { alertsAuthorizationAuditLoggerMock } from './audit_logger.mock'; import { AlertsAuthorizationAuditLogger, AuthorizationResult } from './audit_logger'; import uuid from 'uuid'; @@ -616,8 +612,10 @@ describe('AlertsAuthorization', () => { }); alertTypeRegistry.list.mockReturnValue(setOfAlertTypes); - expect((await alertAuthorization.getFindAuthorizationFilter()).filter).toMatchInlineSnapshot( - `"((alert.attributes.alertTypeId:myAppAlertType and alert.attributes.consumer:(alerts or myApp or myOtherApp or myAppWithSubFeature)) or (alert.attributes.alertTypeId:myOtherAppAlertType and alert.attributes.consumer:(alerts or myApp or myOtherApp or myAppWithSubFeature)) or (alert.attributes.alertTypeId:mySecondAppAlertType and alert.attributes.consumer:(alerts or myApp or myOtherApp or myAppWithSubFeature)))"` + expect((await alertAuthorization.getFindAuthorizationFilter()).filter).toEqual( + esKuery.fromKueryExpression( + `((alert.attributes.alertTypeId:myAppAlertType and alert.attributes.consumer:(alerts or myApp or myOtherApp or myAppWithSubFeature)) or (alert.attributes.alertTypeId:myOtherAppAlertType and alert.attributes.consumer:(alerts or myApp or myOtherApp or myAppWithSubFeature)) or (alert.attributes.alertTypeId:mySecondAppAlertType and alert.attributes.consumer:(alerts or myApp or myOtherApp or myAppWithSubFeature)))` + ) ); expect(auditLogger.alertsAuthorizationSuccess).not.toHaveBeenCalled(); @@ -1246,36 +1244,4 @@ describe('AlertsAuthorization', () => { `); }); }); - - describe('ensureFieldIsSafeForQuery', () => { - test('throws if field contains character that isnt safe in a KQL query', () => { - expect(() => ensureFieldIsSafeForQuery('id', 'alert-*')).toThrowError( - `expected id not to include invalid character: *` - ); - - expect(() => ensureFieldIsSafeForQuery('id', '<=""')).toThrowError( - `expected id not to include invalid character: <=` - ); - - expect(() => ensureFieldIsSafeForQuery('id', '>=""')).toThrowError( - `expected id not to include invalid character: >=` - ); - - expect(() => ensureFieldIsSafeForQuery('id', '1 or alertid:123')).toThrowError( - `expected id not to include whitespace and invalid character: :` - ); - - expect(() => ensureFieldIsSafeForQuery('id', ') or alertid:123')).toThrowError( - `expected id not to include whitespace and invalid characters: ), :` - ); - - expect(() => ensureFieldIsSafeForQuery('id', 'some space')).toThrowError( - `expected id not to include whitespace` - ); - }); - - test('doesnt throws if field is safe as part of a KQL query', () => { - expect(() => ensureFieldIsSafeForQuery('id', '123-0456-678')).not.toThrow(); - }); - }); }); diff --git a/x-pack/plugins/alerts/server/authorization/alerts_authorization.ts b/x-pack/plugins/alerts/server/authorization/alerts_authorization.ts index 9dda006c1eb8e..20b9fecd601e6 100644 --- a/x-pack/plugins/alerts/server/authorization/alerts_authorization.ts +++ b/x-pack/plugins/alerts/server/authorization/alerts_authorization.ts @@ -5,7 +5,7 @@ */ import Boom from 'boom'; -import { map, mapValues, remove, fromPairs, has } from 'lodash'; +import { map, mapValues, fromPairs, has } from 'lodash'; import { KibanaRequest } from 'src/core/server'; import { ALERTS_FEATURE_ID } from '../../common'; import { AlertTypeRegistry } from '../types'; @@ -14,6 +14,8 @@ import { RegistryAlertType } from '../alert_type_registry'; import { PluginStartContract as FeaturesPluginStart } from '../../../features/server'; import { AlertsAuthorizationAuditLogger, ScopeType } from './audit_logger'; import { Space } from '../../../spaces/server'; +import { asFiltersByAlertTypeAndConsumer } from './alerts_authorization_kuery'; +import { KueryNode } from '../../../../../src/plugins/data/server'; export enum ReadOperations { Get = 'get', @@ -215,7 +217,7 @@ export class AlertsAuthorization { } public async getFindAuthorizationFilter(): Promise<{ - filter?: string; + filter?: KueryNode; ensureAlertTypeIsAuthorized: (alertTypeId: string, consumer: string) => void; logSuccessfulAuthorization: () => void; }> { @@ -244,7 +246,7 @@ export class AlertsAuthorization { const authorizedEntries: Map> = new Map(); return { - filter: `(${this.asFiltersByAlertTypeAndConsumer(authorizedAlertTypes).join(' or ')})`, + filter: asFiltersByAlertTypeAndConsumer(authorizedAlertTypes), ensureAlertTypeIsAuthorized: (alertTypeId: string, consumer: string) => { if (!authorizedAlertTypeIdsToConsumers.has(`${alertTypeId}/${consumer}`)) { throw Boom.forbidden( @@ -400,39 +402,6 @@ export class AlertsAuthorization { })) ); } - - private asFiltersByAlertTypeAndConsumer(alertTypes: Set): string[] { - return Array.from(alertTypes).reduce((filters, { id, authorizedConsumers }) => { - ensureFieldIsSafeForQuery('alertTypeId', id); - filters.push( - `(alert.attributes.alertTypeId:${id} and alert.attributes.consumer:(${Object.keys( - authorizedConsumers - ) - .map((consumer) => { - ensureFieldIsSafeForQuery('alertTypeId', id); - return consumer; - }) - .join(' or ')}))` - ); - return filters; - }, []); - } -} - -export function ensureFieldIsSafeForQuery(field: string, value: string): boolean { - const invalid = value.match(/([>=<\*:()]+|\s+)/g); - if (invalid) { - const whitespace = remove(invalid, (chars) => chars.trim().length === 0); - const errors = []; - if (whitespace.length) { - errors.push(`whitespace`); - } - if (invalid.length) { - errors.push(`invalid character${invalid.length > 1 ? `s` : ``}: ${invalid?.join(`, `)}`); - } - throw new Error(`expected ${field} not to include ${errors.join(' and ')}`); - } - return true; } function mergeHasPrivileges(left: HasPrivileges, right?: HasPrivileges): HasPrivileges { diff --git a/x-pack/plugins/alerts/server/authorization/alerts_authorization_kuery.test.ts b/x-pack/plugins/alerts/server/authorization/alerts_authorization_kuery.test.ts new file mode 100644 index 0000000000000..e4b9f8c54c38d --- /dev/null +++ b/x-pack/plugins/alerts/server/authorization/alerts_authorization_kuery.test.ts @@ -0,0 +1,144 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { esKuery } from '../../../../../src/plugins/data/server'; +import { + asFiltersByAlertTypeAndConsumer, + ensureFieldIsSafeForQuery, +} from './alerts_authorization_kuery'; + +describe('asFiltersByAlertTypeAndConsumer', () => { + test('constructs filter for single alert type with single authorized consumer', async () => { + expect( + asFiltersByAlertTypeAndConsumer( + new Set([ + { + actionGroups: [], + defaultActionGroupId: 'default', + id: 'myAppAlertType', + name: 'myAppAlertType', + producer: 'myApp', + authorizedConsumers: { + myApp: { read: true, all: true }, + }, + }, + ]) + ) + ).toEqual( + esKuery.fromKueryExpression( + `((alert.attributes.alertTypeId:myAppAlertType and alert.attributes.consumer:(myApp)))` + ) + ); + }); + + test('constructs filter for single alert type with multiple authorized consumer', async () => { + expect( + asFiltersByAlertTypeAndConsumer( + new Set([ + { + actionGroups: [], + defaultActionGroupId: 'default', + id: 'myAppAlertType', + name: 'myAppAlertType', + producer: 'myApp', + authorizedConsumers: { + alerts: { read: true, all: true }, + myApp: { read: true, all: true }, + myOtherApp: { read: true, all: true }, + }, + }, + ]) + ) + ).toEqual( + esKuery.fromKueryExpression( + `((alert.attributes.alertTypeId:myAppAlertType and alert.attributes.consumer:(alerts or myApp or myOtherApp)))` + ) + ); + }); + + test('constructs filter for multiple alert types across authorized consumer', async () => { + expect( + asFiltersByAlertTypeAndConsumer( + new Set([ + { + actionGroups: [], + defaultActionGroupId: 'default', + id: 'myAppAlertType', + name: 'myAppAlertType', + producer: 'myApp', + authorizedConsumers: { + alerts: { read: true, all: true }, + myApp: { read: true, all: true }, + myOtherApp: { read: true, all: true }, + myAppWithSubFeature: { read: true, all: true }, + }, + }, + { + actionGroups: [], + defaultActionGroupId: 'default', + id: 'myOtherAppAlertType', + name: 'myOtherAppAlertType', + producer: 'alerts', + authorizedConsumers: { + alerts: { read: true, all: true }, + myApp: { read: true, all: true }, + myOtherApp: { read: true, all: true }, + myAppWithSubFeature: { read: true, all: true }, + }, + }, + { + actionGroups: [], + defaultActionGroupId: 'default', + id: 'mySecondAppAlertType', + name: 'mySecondAppAlertType', + producer: 'myApp', + authorizedConsumers: { + alerts: { read: true, all: true }, + myApp: { read: true, all: true }, + myOtherApp: { read: true, all: true }, + myAppWithSubFeature: { read: true, all: true }, + }, + }, + ]) + ) + ).toEqual( + esKuery.fromKueryExpression( + `((alert.attributes.alertTypeId:myAppAlertType and alert.attributes.consumer:(alerts or myApp or myOtherApp or myAppWithSubFeature)) or (alert.attributes.alertTypeId:myOtherAppAlertType and alert.attributes.consumer:(alerts or myApp or myOtherApp or myAppWithSubFeature)) or (alert.attributes.alertTypeId:mySecondAppAlertType and alert.attributes.consumer:(alerts or myApp or myOtherApp or myAppWithSubFeature)))` + ) + ); + }); +}); + +describe('ensureFieldIsSafeForQuery', () => { + test('throws if field contains character that isnt safe in a KQL query', () => { + expect(() => ensureFieldIsSafeForQuery('id', 'alert-*')).toThrowError( + `expected id not to include invalid character: *` + ); + + expect(() => ensureFieldIsSafeForQuery('id', '<=""')).toThrowError( + `expected id not to include invalid character: <=` + ); + + expect(() => ensureFieldIsSafeForQuery('id', '>=""')).toThrowError( + `expected id not to include invalid character: >=` + ); + + expect(() => ensureFieldIsSafeForQuery('id', '1 or alertid:123')).toThrowError( + `expected id not to include whitespace and invalid character: :` + ); + + expect(() => ensureFieldIsSafeForQuery('id', ') or alertid:123')).toThrowError( + `expected id not to include whitespace and invalid characters: ), :` + ); + + expect(() => ensureFieldIsSafeForQuery('id', 'some space')).toThrowError( + `expected id not to include whitespace` + ); + }); + + test('doesnt throws if field is safe as part of a KQL query', () => { + expect(() => ensureFieldIsSafeForQuery('id', '123-0456-678')).not.toThrow(); + }); +}); diff --git a/x-pack/plugins/alerts/server/authorization/alerts_authorization_kuery.ts b/x-pack/plugins/alerts/server/authorization/alerts_authorization_kuery.ts new file mode 100644 index 0000000000000..f236ee7f3c258 --- /dev/null +++ b/x-pack/plugins/alerts/server/authorization/alerts_authorization_kuery.ts @@ -0,0 +1,61 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { remove } from 'lodash'; +import { nodeTypes } from '../../../../../src/plugins/data/common'; +import { KueryNode } from '../../../../../src/plugins/data/server'; +import { RegistryAlertTypeWithAuth } from './alerts_authorization'; + +export const is = (fieldName: string, value: string | KueryNode) => + nodeTypes.function.buildNodeWithArgumentNodes('is', [ + nodeTypes.literal.buildNode(fieldName), + typeof value === 'string' ? nodeTypes.literal.buildNode(value) : value, + nodeTypes.literal.buildNode(false), + ]); + +export const or = ([first, ...args]: KueryNode[]): KueryNode => + args.length ? nodeTypes.function.buildNode('or', [first, or(args)]) : first; + +export const and = ([first, ...args]: KueryNode[]): KueryNode => + args.length ? nodeTypes.function.buildNode('and', [first, and(args)]) : first; + +export function asFiltersByAlertTypeAndConsumer( + alertTypes: Set +): KueryNode { + return or( + Array.from(alertTypes).reduce((filters, { id, authorizedConsumers }) => { + ensureFieldIsSafeForQuery('alertTypeId', id); + filters.push( + and([ + is(`alert.attributes.alertTypeId`, id), + or( + Object.keys(authorizedConsumers).map((consumer) => { + ensureFieldIsSafeForQuery('consumer', consumer); + return is(`alert.attributes.consumer`, consumer); + }) + ), + ]) + ); + return filters; + }, []) + ); +} + +export function ensureFieldIsSafeForQuery(field: string, value: string): boolean { + const invalid = value.match(/([>=<\*:()]+|\s+)/g); + if (invalid) { + const whitespace = remove(invalid, (chars) => chars.trim().length === 0); + const errors = []; + if (whitespace.length) { + errors.push(`whitespace`); + } + if (invalid.length) { + errors.push(`invalid character${invalid.length > 1 ? `s` : ``}: ${invalid?.join(`, `)}`); + } + throw new Error(`expected ${field} not to include ${errors.join(' and ')}`); + } + return true; +} diff --git a/x-pack/plugins/alerts/server/authorization/index.ts b/x-pack/plugins/alerts/server/authorization/index.ts new file mode 100644 index 0000000000000..66c7f0afd0122 --- /dev/null +++ b/x-pack/plugins/alerts/server/authorization/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +export * from './alerts_authorization'; +export * from './alerts_authorization_kuery'; diff --git a/x-pack/plugins/alerts/server/lib/license_state.ts b/x-pack/plugins/alerts/server/lib/license_state.ts index ea0106f717b02..b3204d886960f 100644 --- a/x-pack/plugins/alerts/server/lib/license_state.ts +++ b/x-pack/plugins/alerts/server/lib/license_state.ts @@ -6,9 +6,9 @@ import Boom from 'boom'; import { i18n } from '@kbn/i18n'; +import { assertNever } from '@kbn/std'; import { Observable, Subscription } from 'rxjs'; import { ILicense } from '../../../licensing/common/types'; -import { assertNever } from '../../../../../src/core/server'; import { PLUGIN } from '../constants/plugin'; export interface AlertingLicenseInformation { diff --git a/x-pack/plugins/apm/e2e/cypress/integration/apm.feature b/x-pack/plugins/apm/e2e/cypress/integration/apm.feature index 82d896c5ba17e..285615108266b 100644 --- a/x-pack/plugins/apm/e2e/cypress/integration/apm.feature +++ b/x-pack/plugins/apm/e2e/cypress/integration/apm.feature @@ -1,4 +1,4 @@ -KibanaFeature: APM +Feature: APM Scenario: Transaction duration charts Given a user browses the APM UI application diff --git a/x-pack/plugins/apm/e2e/cypress/support/step_definitions/apm.ts b/x-pack/plugins/apm/e2e/cypress/support/step_definitions/apm.ts index 66d604a663fbf..ab2bf20b36ed4 100644 --- a/x-pack/plugins/apm/e2e/cypress/support/step_definitions/apm.ts +++ b/x-pack/plugins/apm/e2e/cypress/support/step_definitions/apm.ts @@ -12,7 +12,7 @@ export const DEFAULT_TIMEOUT = 60 * 1000; Given(`a user browses the APM UI application`, () => { // open service overview page - loginAndWaitForPage(`/app/apm#/services`, { + loginAndWaitForPage(`/app/apm/services`, { from: '2020-06-01T14:59:32.686Z', to: '2020-06-16T16:59:36.219Z', }); diff --git a/x-pack/plugins/apm/e2e/cypress/support/step_definitions/rum/rum_dashboard.ts b/x-pack/plugins/apm/e2e/cypress/support/step_definitions/rum/rum_dashboard.ts index 804974d8d437d..31aef30c4e23f 100644 --- a/x-pack/plugins/apm/e2e/cypress/support/step_definitions/rum/rum_dashboard.ts +++ b/x-pack/plugins/apm/e2e/cypress/support/step_definitions/rum/rum_dashboard.ts @@ -15,7 +15,7 @@ Given(`a user browses the APM UI application for RUM Data`, () => { // open service overview page const RANGE_FROM = 'now-24h'; const RANGE_TO = 'now'; - loginAndWaitForPage(`/app/apm#/rum-preview`, { + loginAndWaitForPage(`/app/apm/rum-preview`, { from: RANGE_FROM, to: RANGE_TO, }); diff --git a/x-pack/plugins/apm/kibana.json b/x-pack/plugins/apm/kibana.json index 6cc3bb2a2c7e1..8aa4417580337 100644 --- a/x-pack/plugins/apm/kibana.json +++ b/x-pack/plugins/apm/kibana.json @@ -7,7 +7,8 @@ "apmOss", "data", "licensing", - "triggers_actions_ui" + "triggers_actions_ui", + "embeddable" ], "optionalPlugins": [ "cloud", @@ -22,17 +23,13 @@ ], "server": true, "ui": true, - "configPath": [ - "xpack", - "apm" - ], - "extraPublicDirs": [ - "public/style/variables" - ], + "configPath": ["xpack", "apm"], + "extraPublicDirs": ["public/style/variables"], "requiredBundles": [ "kibanaReact", "kibanaUtils", "observability", - "home" + "home", + "maps" ] } diff --git a/x-pack/plugins/apm/public/application/application.test.tsx b/x-pack/plugins/apm/public/application/application.test.tsx index fc369b9cf672a..0566ff19017f4 100644 --- a/x-pack/plugins/apm/public/application/application.test.tsx +++ b/x-pack/plugins/apm/public/application/application.test.tsx @@ -14,6 +14,10 @@ import { createCallApmApi } from '../services/rest/createCallApmApi'; import { renderApp } from './'; import { disableConsoleWarning } from '../utils/testHelpers'; +jest.mock('../services/rest/index_pattern', () => ({ + createStaticIndexPattern: () => Promise.resolve(undefined), +})); + describe('renderApp', () => { let mockConsole: jest.SpyInstance; diff --git a/x-pack/plugins/apm/public/application/csmApp.tsx b/x-pack/plugins/apm/public/application/csmApp.tsx index cdfe42bd628cc..c63ec3700c877 100644 --- a/x-pack/plugins/apm/public/application/csmApp.tsx +++ b/x-pack/plugins/apm/public/application/csmApp.tsx @@ -26,7 +26,7 @@ import { LoadingIndicatorProvider } from '../context/LoadingIndicatorContext'; import { UrlParamsProvider } from '../context/UrlParamsContext'; import { useBreadcrumbs } from '../hooks/use_breadcrumbs'; import { ConfigSchema } from '../index'; -import { ApmPluginSetupDeps } from '../plugin'; +import { ApmPluginSetupDeps, ApmPluginStartDeps } from '../plugin'; import { createCallApmApi } from '../services/rest/createCallApmApi'; import { px, units } from '../style/variables'; @@ -70,11 +70,13 @@ export function CsmAppRoot({ deps, history, config, + corePlugins: { embeddable }, }: { core: CoreStart; deps: ApmPluginSetupDeps; history: AppMountParameters['history']; config: ConfigSchema; + corePlugins: ApmPluginStartDeps; }) { const i18nCore = core.i18n; const plugins = deps; @@ -86,7 +88,7 @@ export function CsmAppRoot({ return ( - + @@ -110,12 +112,19 @@ export const renderApp = ( core: CoreStart, deps: ApmPluginSetupDeps, { element, history }: AppMountParameters, - config: ConfigSchema + config: ConfigSchema, + corePlugins: ApmPluginStartDeps ) => { createCallApmApi(core.http); ReactDOM.render( - , + , element ); return () => { diff --git a/x-pack/plugins/apm/public/application/index.tsx b/x-pack/plugins/apm/public/application/index.tsx index 536d70b053f76..9843f73dafdbb 100644 --- a/x-pack/plugins/apm/public/application/index.tsx +++ b/x-pack/plugins/apm/public/application/index.tsx @@ -25,7 +25,6 @@ import { ScrollToTopOnPathChange } from '../components/app/Main/ScrollToTopOnPat import { ApmPluginContext } from '../context/ApmPluginContext'; import { LicenseProvider } from '../context/LicenseContext'; import { LoadingIndicatorProvider } from '../context/LoadingIndicatorContext'; -import { LocationProvider } from '../context/LocationContext'; import { UrlParamsProvider } from '../context/UrlParamsContext'; import { useBreadcrumbs } from '../hooks/use_breadcrumbs'; import { ApmPluginSetupDeps } from '../plugin'; @@ -99,15 +98,13 @@ export function ApmAppRoot({ - - - - - - - - - + + + + + + + diff --git a/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/List/__test__/List.test.tsx b/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/List/__test__/List.test.tsx index 1acfc5c49245d..84b72b62248b0 100644 --- a/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/List/__test__/List.test.tsx +++ b/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/List/__test__/List.test.tsx @@ -11,6 +11,7 @@ import { MockUrlParamsContextProvider } from '../../../../../context/UrlParamsCo import { mockMoment, toJson } from '../../../../../utils/testHelpers'; import { ErrorGroupList } from '../index'; import props from './props.json'; +import { MemoryRouter } from 'react-router-dom'; jest.mock('@elastic/eui/lib/services/accessibility/html_id_generator', () => { return { @@ -26,9 +27,11 @@ describe('ErrorGroupOverview -> List', () => { it('should render empty state', () => { const storeState = {}; const wrapper = mount( - - - , + + + + + , storeState ); @@ -37,11 +40,13 @@ describe('ErrorGroupOverview -> List', () => { it('should render with data', () => { const wrapper = mount( - - - - - + + + + + + + ); expect(toJson(wrapper)).toMatchSnapshot(); diff --git a/x-pack/plugins/apm/public/components/app/Main/route_config/index.tsx b/x-pack/plugins/apm/public/components/app/Main/route_config/index.tsx index 0cefcbdc54228..4aa2d841e8deb 100644 --- a/x-pack/plugins/apm/public/components/app/Main/route_config/index.tsx +++ b/x-pack/plugins/apm/public/components/app/Main/route_config/index.tsx @@ -113,33 +113,33 @@ function ServiceDetailsTransactions( return ; } -function SettingsAgentConfiguration() { +function SettingsAgentConfiguration(props: RouteComponentProps<{}>) { return ( - + ); } -function SettingsAnomalyDetection() { +function SettingsAnomalyDetection(props: RouteComponentProps<{}>) { return ( - + ); } -function SettingsApmIndices() { +function SettingsApmIndices(props: RouteComponentProps<{}>) { return ( - + ); } -function SettingsCustomizeUI() { +function SettingsCustomizeUI(props: RouteComponentProps<{}>) { return ( - + ); diff --git a/x-pack/plugins/apm/public/components/app/Main/route_config/route_handlers/agent_configuration.tsx b/x-pack/plugins/apm/public/components/app/Main/route_config/route_handlers/agent_configuration.tsx index cc07286457908..f2ae0c2ff99e8 100644 --- a/x-pack/plugins/apm/public/components/app/Main/route_config/route_handlers/agent_configuration.tsx +++ b/x-pack/plugins/apm/public/components/app/Main/route_config/route_handlers/agent_configuration.tsx @@ -13,10 +13,10 @@ import { AgentConfigurationCreateEdit } from '../../../Settings/AgentConfigurati type EditAgentConfigurationRouteHandler = RouteComponentProps<{}>; -export function EditAgentConfigurationRouteHandler({ - history, -}: EditAgentConfigurationRouteHandler) { - const { search } = history.location; +export function EditAgentConfigurationRouteHandler( + props: EditAgentConfigurationRouteHandler +) { + const { search } = props.history.location; // typescript complains because `pageStop` does not exist in `APMQueryParams` // Going forward we should move away from globally declared query params and this is a first step @@ -34,7 +34,7 @@ export function EditAgentConfigurationRouteHandler({ ); return ( - + ; -export function CreateAgentConfigurationRouteHandler({ - history, -}: CreateAgentConfigurationRouteHandlerProps) { - const { search } = history.location; +export function CreateAgentConfigurationRouteHandler( + props: CreateAgentConfigurationRouteHandlerProps +) { + const { search } = props.history.location; // Ignoring here because we specifically DO NOT want to add the query params to the global route handler // @ts-expect-error const { pageStep } = toQuery(search); return ( - + diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/Charts/VisitorBreakdownChart.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/Charts/VisitorBreakdownChart.tsx index 213126ba4bf81..34fcf62178711 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/Charts/VisitorBreakdownChart.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/Charts/VisitorBreakdownChart.tsx @@ -31,9 +31,10 @@ interface Props { count: number; name: string; }>; + loading: boolean; } -export function VisitorBreakdownChart({ options }: Props) { +export function VisitorBreakdownChart({ loading, options }: Props) { const [darkMode] = useUiSetting$('theme:darkMode'); const euiChartTheme = darkMode @@ -41,7 +42,7 @@ export function VisitorBreakdownChart({ options }: Props) { : EUI_CHARTS_THEME_LIGHT; return ( - + - ); } diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/CoreVitals/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/CoreVitals/index.tsx index e8305a6aef0d4..d0bd674ef5c79 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/CoreVitals/index.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/CoreVitals/index.tsx @@ -20,10 +20,11 @@ const CoreVitalsThresholds = { export function CoreVitals() { const { urlParams, uiFilters } = useUrlParams(); - const { start, end, serviceName } = urlParams; + const { start, end } = urlParams; const { data, status } = useFetcher( (callApmApi) => { + const { serviceName } = uiFilters; if (start && end && serviceName) { return callApmApi({ pathname: '/api/apm/rum-client/web-core-vitals', @@ -34,7 +35,7 @@ export function CoreVitals() { } return Promise.resolve(null); }, - [start, end, serviceName, uiFilters] + [start, end, uiFilters] ); const { lcp, lcpRanks, fid, fidRanks, cls, clsRanks } = data || {}; diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/RumDashboard.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/RumDashboard.tsx index f05c07e8512ac..48c0f6cc60d84 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/RumDashboard.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/RumDashboard.tsx @@ -18,6 +18,7 @@ import { PageLoadDistribution } from './PageLoadDistribution'; import { I18LABELS } from './translations'; import { VisitorBreakdown } from './VisitorBreakdown'; import { CoreVitals } from './CoreVitals'; +import { VisitorBreakdownMap } from './VisitorBreakdownMap'; export function RumDashboard() { return ( @@ -67,6 +68,9 @@ export function RumDashboard() { + + +
diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdown/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdown/index.tsx index e18875f32ff72..245f58370d3d7 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdown/index.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdown/index.tsx @@ -16,7 +16,7 @@ export function VisitorBreakdown() { const { start, end } = urlParams; - const { data } = useFetcher( + const { data, status } = useFetcher( (callApmApi) => { if (start && end) { return callApmApi({ @@ -47,14 +47,20 @@ export function VisitorBreakdown() {

{I18LABELS.browser}

- +

{I18LABELS.operatingSystem}

- +
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 new file mode 100644 index 0000000000000..93608a0ccd826 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/EmbeddedMap.tsx @@ -0,0 +1,183 @@ +/* + * 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, useState, useRef } from 'react'; +import uuid from 'uuid'; +import styled from 'styled-components'; + +import { + MapEmbeddable, + MapEmbeddableInput, + // eslint-disable-next-line @kbn/eslint/no-restricted-paths +} from '../../../../../../maps/public/embeddable'; +import { MAP_SAVED_OBJECT_TYPE } from '../../../../../../maps/common/constants'; +import { useKibana } from '../../../../../../../../src/plugins/kibana_react/public'; +import { + ErrorEmbeddable, + ViewMode, + isErrorEmbeddable, +} from '../../../../../../../../src/plugins/embeddable/public'; +import { getLayerList } from './LayerList'; +import { useUrlParams } from '../../../../hooks/useUrlParams'; +import { RenderTooltipContentParams } from '../../../../../../maps/public'; +import { MapToolTip } from './MapToolTip'; +import { useMapFilters } from './useMapFilters'; +import { EmbeddableStart } from '../../../../../../../../src/plugins/embeddable/public'; + +const EmbeddedPanel = styled.div` + z-index: auto; + flex: 1; + display: flex; + flex-direction: column; + height: 100%; + position: relative; + .embPanel__content { + display: flex; + flex: 1 1 100%; + z-index: 1; + min-height: 0; // Absolute must for Firefox to scroll contents + } + &&& .mapboxgl-canvas { + animation: none !important; + } +`; + +interface KibanaDeps { + embeddable: EmbeddableStart; +} +export function EmbeddedMapComponent() { + const { urlParams } = useUrlParams(); + + const { start, end, serviceName } = urlParams; + + const mapFilters = useMapFilters(); + + const [embeddable, setEmbeddable] = useState< + MapEmbeddable | ErrorEmbeddable | undefined + >(); + + const embeddableRoot: React.RefObject = useRef< + HTMLDivElement + >(null); + + const { + services: { embeddable: embeddablePlugin }, + } = useKibana(); + + if (!embeddablePlugin) { + throw new Error('Embeddable start plugin not found'); + } + const factory: any = embeddablePlugin.getEmbeddableFactory( + MAP_SAVED_OBJECT_TYPE + ); + + const input: MapEmbeddableInput = { + id: uuid.v4(), + filters: mapFilters, + refreshConfig: { + value: 0, + pause: false, + }, + viewMode: ViewMode.VIEW, + isLayerTOCOpen: false, + query: { + query: 'transaction.type : "page-load"', + language: 'kuery', + }, + ...(start && { + timeRange: { + from: new Date(start!).toISOString(), + to: new Date(end!).toISOString(), + }, + }), + hideFilterActions: true, + }; + + function renderTooltipContent({ + addFilters, + closeTooltip, + features, + isLocked, + getLayerName, + loadFeatureProperties, + loadFeatureGeometry, + }: RenderTooltipContentParams) { + const props = { + addFilters, + closeTooltip, + isLocked, + getLayerName, + loadFeatureProperties, + loadFeatureGeometry, + }; + + return ; + } + + useEffect(() => { + if (embeddable != null && serviceName) { + embeddable.updateInput({ filters: mapFilters }); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [mapFilters]); + + // DateRange updated useEffect + useEffect(() => { + if (embeddable != null && start != null && end != null) { + const timeRange = { + from: new Date(start).toISOString(), + to: new Date(end).toISOString(), + }; + embeddable.updateInput({ timeRange }); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [start, end]); + + useEffect(() => { + async function setupEmbeddable() { + if (!factory) { + throw new Error('Map embeddable not found.'); + } + const embeddableObject: any = await factory.create({ + ...input, + title: 'Visitors by region', + }); + + if (embeddableObject && !isErrorEmbeddable(embeddableObject)) { + embeddableObject.setRenderTooltipContent(renderTooltipContent); + await embeddableObject.setLayerList(getLayerList()); + } + + setEmbeddable(embeddableObject); + } + + setupEmbeddable(); + + // we want this effect to execute exactly once after the component mounts + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + // We can only render after embeddable has already initialized + useEffect(() => { + if (embeddableRoot.current && embeddable) { + embeddable.render(embeddableRoot.current); + } + }, [embeddable, embeddableRoot]); + + return ( + +
+ + ); +} + +EmbeddedMapComponent.displayName = 'EmbeddedMap'; + +export const EmbeddedMap = React.memo(EmbeddedMapComponent); diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/LayerList.ts b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/LayerList.ts new file mode 100644 index 0000000000000..138a3f4018c65 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/LayerList.ts @@ -0,0 +1,174 @@ +/* + * 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 { + EMSFileSourceDescriptor, + EMSTMSSourceDescriptor, + ESTermSourceDescriptor, + LayerDescriptor as BaseLayerDescriptor, + VectorLayerDescriptor as BaseVectorLayerDescriptor, + VectorStyleDescriptor, +} from '../../../../../../maps/common/descriptor_types'; +import { + AGG_TYPE, + COLOR_MAP_TYPE, + FIELD_ORIGIN, + LABEL_BORDER_SIZES, + STYLE_TYPE, + SYMBOLIZE_AS_TYPES, +} from '../../../../../../maps/common/constants'; + +import { APM_STATIC_INDEX_PATTERN_ID } from '../../../../../../../../src/plugins/apm_oss/public'; + +const ES_TERM_SOURCE: ESTermSourceDescriptor = { + type: 'ES_TERM_SOURCE', + id: '3657625d-17b0-41ef-99ba-3a2b2938655c', + indexPatternTitle: 'apm-*', + term: 'client.geo.country_iso_code', + metrics: [ + { + type: AGG_TYPE.AVG, + field: 'transaction.duration.us', + label: 'Page load duration', + }, + ], + indexPatternId: APM_STATIC_INDEX_PATTERN_ID, + applyGlobalQuery: true, +}; + +export const REGION_NAME = 'region_name'; +export const COUNTRY_NAME = 'name'; + +export const TRANSACTION_DURATION_REGION = + '__kbnjoin__avg_of_transaction.duration.us__e62a1b9c-d7ff-4fd4-a0f6-0fdc44bb9e41'; + +export const TRANSACTION_DURATION_COUNTRY = + '__kbnjoin__avg_of_transaction.duration.us__3657625d-17b0-41ef-99ba-3a2b2938655c'; + +interface LayerDescriptor extends BaseLayerDescriptor { + sourceDescriptor: EMSTMSSourceDescriptor; +} + +interface VectorLayerDescriptor extends BaseVectorLayerDescriptor { + sourceDescriptor: EMSFileSourceDescriptor; +} + +export function getLayerList() { + const baseLayer: LayerDescriptor = { + sourceDescriptor: { type: 'EMS_TMS', isAutoSelect: true }, + id: 'b7af286d-2580-4f47-be93-9653d594ce7e', + label: null, + minZoom: 0, + maxZoom: 24, + alpha: 1, + visible: true, + style: { type: 'TILE' }, + type: 'VECTOR_TILE', + }; + + const getLayerStyle = (fieldName: string): VectorStyleDescriptor => { + return { + type: 'VECTOR', + properties: { + icon: { type: STYLE_TYPE.STATIC, options: { value: 'marker' } }, + fillColor: { + type: STYLE_TYPE.DYNAMIC, + options: { + color: 'Blue to Red', + colorCategory: 'palette_0', + fieldMetaOptions: { isEnabled: true, sigma: 3 }, + type: COLOR_MAP_TYPE.ORDINAL, + field: { + name: fieldName, + origin: FIELD_ORIGIN.JOIN, + }, + useCustomColorRamp: false, + }, + }, + lineColor: { + type: STYLE_TYPE.DYNAMIC, + options: { color: '#3d3d3d', fieldMetaOptions: { isEnabled: true } }, + }, + lineWidth: { type: STYLE_TYPE.STATIC, options: { size: 1 } }, + iconSize: { type: STYLE_TYPE.STATIC, options: { size: 6 } }, + iconOrientation: { + type: STYLE_TYPE.STATIC, + options: { orientation: 0 }, + }, + labelText: { type: STYLE_TYPE.STATIC, options: { value: '' } }, + labelColor: { + type: STYLE_TYPE.STATIC, + options: { color: '#000000' }, + }, + labelSize: { type: STYLE_TYPE.STATIC, options: { size: 14 } }, + labelBorderColor: { + type: STYLE_TYPE.STATIC, + options: { color: '#FFFFFF' }, + }, + symbolizeAs: { options: { value: SYMBOLIZE_AS_TYPES.CIRCLE } }, + labelBorderSize: { options: { size: LABEL_BORDER_SIZES.SMALL } }, + }, + isTimeAware: true, + }; + }; + + const pageLoadDurationByCountryLayer: VectorLayerDescriptor = { + joins: [ + { + leftField: 'iso2', + right: ES_TERM_SOURCE, + }, + ], + sourceDescriptor: { + type: 'EMS_FILE', + id: 'world_countries', + tooltipProperties: [COUNTRY_NAME], + applyGlobalQuery: true, + }, + style: getLayerStyle(TRANSACTION_DURATION_COUNTRY), + id: 'e8d1d974-eed8-462f-be2c-f0004b7619b2', + label: null, + minZoom: 0, + maxZoom: 24, + alpha: 0.75, + visible: true, + type: 'VECTOR', + }; + + const pageLoadDurationByAdminRegionLayer: VectorLayerDescriptor = { + joins: [ + { + leftField: 'region_iso_code', + right: { + type: 'ES_TERM_SOURCE', + id: 'e62a1b9c-d7ff-4fd4-a0f6-0fdc44bb9e41', + indexPatternTitle: 'apm-*', + term: 'client.geo.region_iso_code', + metrics: [{ type: AGG_TYPE.AVG, field: 'transaction.duration.us' }], + indexPatternId: APM_STATIC_INDEX_PATTERN_ID, + }, + }, + ], + sourceDescriptor: { + type: 'EMS_FILE', + id: 'administrative_regions_lvl2', + tooltipProperties: ['region_iso_code', REGION_NAME], + }, + style: getLayerStyle(TRANSACTION_DURATION_REGION), + id: '0e936d41-8765-41c9-97f0-05e166391366', + label: null, + minZoom: 3, + maxZoom: 24, + alpha: 0.75, + visible: true, + type: 'VECTOR', + }; + return [ + baseLayer, + pageLoadDurationByCountryLayer, + pageLoadDurationByAdminRegionLayer, + ]; +} diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/MapToolTip.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/MapToolTip.tsx new file mode 100644 index 0000000000000..07b40addedec3 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/MapToolTip.tsx @@ -0,0 +1,109 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useEffect, useState } from 'react'; +import { + EuiDescriptionList, + EuiDescriptionListDescription, + EuiDescriptionListTitle, + EuiOutsideClickDetector, + EuiPopoverTitle, +} from '@elastic/eui'; +import styled from 'styled-components'; +import { + COUNTRY_NAME, + REGION_NAME, + TRANSACTION_DURATION_COUNTRY, + TRANSACTION_DURATION_REGION, +} from './LayerList'; +import { RenderTooltipContentParams } from '../../../../../../maps/public'; +import { I18LABELS } from '../translations'; + +type MapToolTipProps = Partial; + +const DescriptionItem = styled(EuiDescriptionListDescription)` + &&& { + width: 25%; + } +`; + +const TitleItem = styled(EuiDescriptionListTitle)` + &&& { + width: 75%; + } +`; + +function MapToolTipComponent({ + closeTooltip, + features = [], + loadFeatureProperties, +}: MapToolTipProps) { + const { id: featureId, layerId } = features[0] ?? {}; + + const [regionName, setRegionName] = useState(featureId as string); + const [pageLoadDuration, setPageLoadDuration] = useState(''); + + const formatPageLoadValue = (val: number) => { + const valInMs = val / 1000; + if (valInMs > 1000) { + return (valInMs / 1000).toFixed(2) + ' sec'; + } + + return (valInMs / 1000).toFixed(0) + ' ms'; + }; + + useEffect(() => { + const loadRegionInfo = async () => { + if (loadFeatureProperties) { + const items = await loadFeatureProperties({ layerId, featureId }); + items.forEach((item) => { + if ( + item.getPropertyKey() === COUNTRY_NAME || + item.getPropertyKey() === REGION_NAME + ) { + setRegionName(item.getRawValue() as string); + } + if ( + item.getPropertyKey() === TRANSACTION_DURATION_REGION || + item.getPropertyKey() === TRANSACTION_DURATION_COUNTRY + ) { + setPageLoadDuration( + formatPageLoadValue(+(item.getRawValue() as string)) + ); + } + }); + } + }; + loadRegionInfo(); + }); + + return ( + { + if (closeTooltip != null) { + closeTooltip(); + } + }} + > + <> + {regionName} + + + {I18LABELS.avgPageLoadDuration} + + {pageLoadDuration} + + + + ); +} + +export const MapToolTip = React.memo(MapToolTipComponent); diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/__stories__/MapTooltip.stories.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/__stories__/MapTooltip.stories.tsx new file mode 100644 index 0000000000000..023f5d61a964e --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/__stories__/MapTooltip.stories.tsx @@ -0,0 +1,57 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { storiesOf } from '@storybook/react'; +import React from 'react'; +import { EuiThemeProvider } from '../../../../../../../observability/public'; +import { MapToolTip } from '../MapToolTip'; +import { COUNTRY_NAME, TRANSACTION_DURATION_COUNTRY } from '../LayerList'; + +storiesOf('app/RumDashboard/VisitorsRegionMap', module) + .addDecorator((storyFn) => {storyFn()}) + .add( + 'Tooltip', + () => { + const loadFeatureProps = async () => { + return [ + { + getPropertyKey: () => COUNTRY_NAME, + getRawValue: () => 'United States', + }, + { + getPropertyKey: () => TRANSACTION_DURATION_COUNTRY, + getRawValue: () => 2434353, + }, + ]; + }; + return ( + + ); + }, + { + info: { + propTables: false, + source: false, + }, + } + ); diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/__tests__/EmbeddedMap.test.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/__tests__/EmbeddedMap.test.tsx new file mode 100644 index 0000000000000..790be81bb65c0 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/__tests__/EmbeddedMap.test.tsx @@ -0,0 +1,44 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { render } from 'enzyme'; +import React from 'react'; + +import { EmbeddedMap } from '../EmbeddedMap'; +import { KibanaContextProvider } from '../../../../../../../security_solution/public/common/lib/kibana'; +import { embeddablePluginMock } from '../../../../../../../../../src/plugins/embeddable/public/mocks'; + +describe('Embedded Map', () => { + test('it renders', () => { + const [core] = mockCore(); + + const wrapper = render( + + + + ); + + expect(wrapper).toMatchSnapshot(); + }); +}); + +const mockEmbeddable = embeddablePluginMock.createStartContract(); + +mockEmbeddable.getEmbeddableFactory = jest.fn().mockImplementation(() => ({ + create: () => ({ + reload: jest.fn(), + setRenderTooltipContent: jest.fn(), + setLayerList: jest.fn(), + }), +})); + +const mockCore: () => [any] = () => { + const core = { + embeddable: mockEmbeddable, + }; + + return [core]; +}; diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/__tests__/LayerList.test.ts b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/__tests__/LayerList.test.ts new file mode 100644 index 0000000000000..eb149ee2a132d --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/__tests__/LayerList.test.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { mockLayerList } from './__mocks__/regions_layer.mock'; +import { getLayerList } from '../LayerList'; + +describe('LayerList', () => { + describe('getLayerList', () => { + test('it returns the region layer', () => { + const layerList = getLayerList(); + expect(layerList).toStrictEqual(mockLayerList); + }); + }); +}); diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/__tests__/MapToolTip.test.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/__tests__/MapToolTip.test.tsx new file mode 100644 index 0000000000000..cbaae40b04361 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/__tests__/MapToolTip.test.tsx @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { render, shallow } from 'enzyme'; +import React from 'react'; + +import { MapToolTip } from '../MapToolTip'; + +describe('Map Tooltip', () => { + test('it shallow renders', () => { + const wrapper = shallow(); + + expect(wrapper).toMatchSnapshot(); + }); + + test('it renders', () => { + const wrapper = render(); + + expect(wrapper).toMatchSnapshot(); + }); +}); 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 new file mode 100644 index 0000000000000..c45f8b27d7d3e --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/__tests__/__mocks__/regions_layer.mock.ts @@ -0,0 +1,151 @@ +/* + * 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 const mockLayerList = [ + { + sourceDescriptor: { type: 'EMS_TMS', isAutoSelect: true }, + id: 'b7af286d-2580-4f47-be93-9653d594ce7e', + label: null, + minZoom: 0, + maxZoom: 24, + alpha: 1, + visible: true, + style: { type: 'TILE' }, + type: 'VECTOR_TILE', + }, + { + joins: [ + { + leftField: 'iso2', + right: { + type: 'ES_TERM_SOURCE', + id: '3657625d-17b0-41ef-99ba-3a2b2938655c', + indexPatternTitle: 'apm-*', + term: 'client.geo.country_iso_code', + metrics: [ + { + type: 'avg', + field: 'transaction.duration.us', + label: 'Page load duration', + }, + ], + indexPatternId: 'apm_static_index_pattern_id', + applyGlobalQuery: true, + }, + }, + ], + sourceDescriptor: { + type: 'EMS_FILE', + id: 'world_countries', + tooltipProperties: ['name'], + applyGlobalQuery: true, + }, + style: { + type: 'VECTOR', + properties: { + icon: { type: 'STATIC', options: { value: 'marker' } }, + fillColor: { + type: 'DYNAMIC', + options: { + color: 'Blue to Red', + colorCategory: 'palette_0', + fieldMetaOptions: { isEnabled: true, sigma: 3 }, + type: 'ORDINAL', + field: { + name: + '__kbnjoin__avg_of_transaction.duration.us__3657625d-17b0-41ef-99ba-3a2b2938655c', + origin: 'join', + }, + useCustomColorRamp: false, + }, + }, + lineColor: { + type: 'DYNAMIC', + options: { color: '#3d3d3d', fieldMetaOptions: { isEnabled: true } }, + }, + lineWidth: { type: 'STATIC', options: { size: 1 } }, + iconSize: { type: 'STATIC', options: { size: 6 } }, + iconOrientation: { type: 'STATIC', options: { orientation: 0 } }, + labelText: { type: 'STATIC', options: { value: '' } }, + labelColor: { type: 'STATIC', options: { color: '#000000' } }, + labelSize: { type: 'STATIC', options: { size: 14 } }, + labelBorderColor: { type: 'STATIC', options: { color: '#FFFFFF' } }, + symbolizeAs: { options: { value: 'circle' } }, + labelBorderSize: { options: { size: 'SMALL' } }, + }, + isTimeAware: true, + }, + id: 'e8d1d974-eed8-462f-be2c-f0004b7619b2', + label: null, + minZoom: 0, + maxZoom: 24, + alpha: 0.75, + visible: true, + type: 'VECTOR', + }, + { + joins: [ + { + leftField: 'region_iso_code', + right: { + type: 'ES_TERM_SOURCE', + id: 'e62a1b9c-d7ff-4fd4-a0f6-0fdc44bb9e41', + indexPatternTitle: 'apm-*', + term: 'client.geo.region_iso_code', + metrics: [{ type: 'avg', field: 'transaction.duration.us' }], + indexPatternId: 'apm_static_index_pattern_id', + }, + }, + ], + sourceDescriptor: { + type: 'EMS_FILE', + id: 'administrative_regions_lvl2', + tooltipProperties: ['region_iso_code', 'region_name'], + }, + style: { + type: 'VECTOR', + properties: { + icon: { type: 'STATIC', options: { value: 'marker' } }, + fillColor: { + type: 'DYNAMIC', + options: { + color: 'Blue to Red', + colorCategory: 'palette_0', + fieldMetaOptions: { isEnabled: true, sigma: 3 }, + type: 'ORDINAL', + field: { + name: + '__kbnjoin__avg_of_transaction.duration.us__e62a1b9c-d7ff-4fd4-a0f6-0fdc44bb9e41', + origin: 'join', + }, + useCustomColorRamp: false, + }, + }, + lineColor: { + type: 'DYNAMIC', + options: { color: '#3d3d3d', fieldMetaOptions: { isEnabled: true } }, + }, + lineWidth: { type: 'STATIC', options: { size: 1 } }, + iconSize: { type: 'STATIC', options: { size: 6 } }, + iconOrientation: { type: 'STATIC', options: { orientation: 0 } }, + labelText: { type: 'STATIC', options: { value: '' } }, + labelColor: { type: 'STATIC', options: { color: '#000000' } }, + labelSize: { type: 'STATIC', options: { size: 14 } }, + labelBorderColor: { type: 'STATIC', options: { color: '#FFFFFF' } }, + symbolizeAs: { options: { value: 'circle' } }, + labelBorderSize: { options: { size: 'SMALL' } }, + }, + isTimeAware: true, + }, + id: '0e936d41-8765-41c9-97f0-05e166391366', + label: null, + minZoom: 3, + maxZoom: 24, + alpha: 0.75, + visible: true, + type: 'VECTOR', + }, +]; diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/__tests__/__snapshots__/EmbeddedMap.test.tsx.snap b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/__tests__/__snapshots__/EmbeddedMap.test.tsx.snap new file mode 100644 index 0000000000000..67f79c9fc747e --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/__tests__/__snapshots__/EmbeddedMap.test.tsx.snap @@ -0,0 +1,45 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Embedded Map it renders 1`] = ` +.c0 { + z-index: auto; + -webkit-flex: 1; + -ms-flex: 1; + flex: 1; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + height: 100%; + position: relative; +} + +.c0 .embPanel__content { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex: 1 1 100%; + -ms-flex: 1 1 100%; + flex: 1 1 100%; + z-index: 1; + min-height: 0; +} + +.c0.c0.c0 .mapboxgl-canvas { + -webkit-animation: none !important; + animation: none !important; +} + +
+
+
+`; diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/__tests__/__snapshots__/MapToolTip.test.tsx.snap b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/__tests__/__snapshots__/MapToolTip.test.tsx.snap new file mode 100644 index 0000000000000..860727a7a0f86 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/__tests__/__snapshots__/MapToolTip.test.tsx.snap @@ -0,0 +1,55 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Map Tooltip it renders 1`] = ` +Array [ +
, + .c1.c1.c1 { + width: 25%; +} + +.c0.c0.c0 { + width: 75%; +} + +
+
+ Average page load duration +
+
+
, +] +`; + +exports[`Map Tooltip it shallow renders 1`] = ` + + + + + Average page load duration + + + + +`; diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/index.tsx new file mode 100644 index 0000000000000..44bfe5abbaca2 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/index.tsx @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { EuiTitle, EuiSpacer } from '@elastic/eui'; +import { EmbeddedMap } from './EmbeddedMap'; +import { I18LABELS } from '../translations'; + +export function VisitorBreakdownMap() { + return ( + <> + +

{I18LABELS.pageLoadDurationByRegion}

+
+ +
+ +
+ + ); +} diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/useMapFilters.ts b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/useMapFilters.ts new file mode 100644 index 0000000000000..357e04c538e68 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/useMapFilters.ts @@ -0,0 +1,102 @@ +/* + * 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 { useEffect, useState } from 'react'; +import { useUrlParams } from '../../../../hooks/useUrlParams'; +import { FieldFilter as Filter } from '../../../../../../../../src/plugins/data/common'; +import { + CLIENT_GEO_COUNTRY_ISO_CODE, + SERVICE_NAME, + USER_AGENT_DEVICE, + USER_AGENT_NAME, + USER_AGENT_OS, +} from '../../../../../common/elasticsearch_fieldnames'; + +import { APM_STATIC_INDEX_PATTERN_ID } from '../../../../../../../../src/plugins/apm_oss/public'; + +const getMatchFilter = (field: string, value: string): Filter => { + return { + meta: { + index: APM_STATIC_INDEX_PATTERN_ID, + alias: null, + negate: false, + disabled: false, + type: 'phrase', + key: field, + params: { query: value }, + }, + query: { match_phrase: { [field]: value } }, + }; +}; + +const getMultiMatchFilter = (field: string, values: string[]): Filter => { + return { + meta: { + index: APM_STATIC_INDEX_PATTERN_ID, + type: 'phrases', + key: field, + value: values.join(', '), + params: values, + alias: null, + negate: false, + disabled: false, + }, + query: { + bool: { + should: values.map((value) => ({ match_phrase: { [field]: value } })), + minimum_should_match: 1, + }, + }, + }; +}; +export const useMapFilters = (): Filter[] => { + const { urlParams, uiFilters } = useUrlParams(); + + const { serviceName } = urlParams; + + const { browser, device, os, location } = uiFilters; + + const [mapFilters, setMapFilters] = useState([]); + + const existFilter: Filter = { + meta: { + index: APM_STATIC_INDEX_PATTERN_ID, + alias: null, + negate: false, + disabled: false, + type: 'exists', + key: 'transaction.marks.navigationTiming.fetchStart', + value: 'exists', + }, + exists: { + field: 'transaction.marks.navigationTiming.fetchStart', + }, + }; + + useEffect(() => { + const filters = [existFilter]; + if (serviceName) { + filters.push(getMatchFilter(SERVICE_NAME, serviceName)); + } + if (browser) { + filters.push(getMultiMatchFilter(USER_AGENT_NAME, browser)); + } + if (device) { + filters.push(getMultiMatchFilter(USER_AGENT_DEVICE, device)); + } + if (os) { + filters.push(getMultiMatchFilter(USER_AGENT_OS, os)); + } + if (location) { + filters.push(getMultiMatchFilter(CLIENT_GEO_COUNTRY_ISO_CODE, location)); + } + + setMapFilters(filters); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [serviceName, browser, device, os, location]); + + return mapFilters; +}; diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/index.tsx index 8d1959ec14d15..fa0551252b6a1 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/index.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/index.tsx @@ -58,7 +58,7 @@ export function RumOverview() { return ( <> - + diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/translations.ts b/x-pack/plugins/apm/public/components/app/RumDashboard/translations.ts index 660ed5a92a0e6..ec135168729b4 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/translations.ts +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/translations.ts @@ -64,6 +64,18 @@ export const I18LABELS = { defaultMessage: 'Operating system', } ), + avgPageLoadDuration: i18n.translate( + 'xpack.apm.rum.visitorBreakdownMap.avgPageLoadDuration', + { + defaultMessage: 'Average page load duration', + } + ), + pageLoadDurationByRegion: i18n.translate( + 'xpack.apm.rum.visitorBreakdownMap.pageLoadDurationByRegion', + { + defaultMessage: 'Page load duration by region', + } + ), }; export const VisitorBreakdownLabel = i18n.translate( diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/Buttons.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/Buttons.tsx index 55d068cba5935..8670cf623c253 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/Buttons.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/Buttons.tsx @@ -25,11 +25,7 @@ export function Buttons({ }: ButtonsProps) { const { core } = useApmPluginContext(); const { basePath } = core.http; - // The params may contain the service name. We want to use the selected node's - // service name in the button URLs, so make a copy and set the - // `serviceName` property. - const urlParams = { ...useUrlParams().urlParams } as APMQueryParams; - urlParams.serviceName = selectedNodeServiceName; + const urlParams = useUrlParams().urlParams as APMQueryParams; const detailsUrl = getAPMHref({ basePath, diff --git a/x-pack/plugins/apm/public/components/app/ServiceOverview/__test__/ServiceOverview.test.tsx b/x-pack/plugins/apm/public/components/app/ServiceOverview/__test__/ServiceOverview.test.tsx index 8eeff018ad03f..e4ba1e36378d9 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceOverview/__test__/ServiceOverview.test.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceOverview/__test__/ServiceOverview.test.tsx @@ -6,21 +6,22 @@ import { render, wait, waitForElement } from '@testing-library/react'; import { CoreStart } from 'kibana/public'; +import { merge } from 'lodash'; import React, { FunctionComponent, ReactChild } from 'react'; +import { MemoryRouter } from 'react-router-dom'; import { createKibanaReactContext } from 'src/plugins/kibana_react/public'; -import { merge } from 'lodash'; import { ServiceOverview } from '..'; +import { EuiThemeProvider } from '../../../../../../observability/public'; import { ApmPluginContextValue } from '../../../../context/ApmPluginContext'; import { mockApmPluginContextValue, MockApmPluginContextWrapper, } from '../../../../context/ApmPluginContext/MockApmPluginContext'; +import * as useAnomalyDetectionJobs from '../../../../hooks/useAnomalyDetectionJobs'; import { FETCH_STATUS } from '../../../../hooks/useFetcher'; import * as useLocalUIFilters from '../../../../hooks/useLocalUIFilters'; import * as urlParamsHooks from '../../../../hooks/useUrlParams'; -import * as useAnomalyDetectionJobs from '../../../../hooks/useAnomalyDetectionJobs'; import { SessionStorageMock } from '../../../../services/__test__/SessionStorageMock'; -import { EuiThemeProvider } from '../../../../../../../legacy/common/eui_styled_components'; const KibanaReactContext = createKibanaReactContext({ usageCollection: { reportUiStats: () => {} }, @@ -44,13 +45,15 @@ function wrapper({ children }: { children: ReactChild }) { }) as unknown) as ApmPluginContextValue; return ( - + - - {children} - + + + {children} + + - + ); } diff --git a/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/List/index.tsx b/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/List/index.tsx index 210b3c3da8d0b..a67df86b21b1e 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/List/index.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/List/index.tsx @@ -15,12 +15,12 @@ import { import { i18n } from '@kbn/i18n'; import { isEmpty } from 'lodash'; import React, { useState } from 'react'; +import { useLocation } from 'react-router-dom'; import { getOptionLabel } from '../../../../../../common/agent_configuration/all_option'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { AgentConfigurationListAPIResponse } from '../../../../../../server/lib/settings/agent_configuration/list_configurations'; import { useApmPluginContext } from '../../../../../hooks/useApmPluginContext'; import { FETCH_STATUS } from '../../../../../hooks/useFetcher'; -import { useLocation } from '../../../../../hooks/useLocation'; import { useTheme } from '../../../../../hooks/useTheme'; import { px, units } from '../../../../../style/variables'; import { diff --git a/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/index.tsx b/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/index.tsx index 06d3f883e211a..8e32c55da9161 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/index.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/index.tsx @@ -15,10 +15,10 @@ import { import { i18n } from '@kbn/i18n'; import { isEmpty } from 'lodash'; import React from 'react'; +import { useLocation } from 'react-router-dom'; import { useTrackPageview } from '../../../../../../observability/public'; import { useApmPluginContext } from '../../../../hooks/useApmPluginContext'; import { useFetcher } from '../../../../hooks/useFetcher'; -import { useLocation } from '../../../../hooks/useLocation'; import { createAgentConfigurationHref } from '../../../shared/Links/apm/agentConfigurationLinks'; import { AgentConfigurationList } from './List'; diff --git a/x-pack/plugins/apm/public/components/app/Settings/Settings.test.tsx b/x-pack/plugins/apm/public/components/app/Settings/Settings.test.tsx index 0e0194cac848c..21da12477b024 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/Settings.test.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/Settings.test.tsx @@ -8,23 +8,27 @@ import { render } from '@testing-library/react'; import { MockApmPluginContextWrapper } from '../../../context/ApmPluginContext/MockApmPluginContext'; import React, { ReactNode } from 'react'; import { Settings } from './'; -import { LocationContext } from '../../../context/LocationContext'; import { createMemoryHistory } from 'history'; +import { MemoryRouter, RouteComponentProps } from 'react-router-dom'; + +const { location } = createMemoryHistory(); function Wrapper({ children }: { children?: ReactNode }) { - const { location } = createMemoryHistory(); return ( - + {children} - + ); } describe('Settings', () => { it('renders', async () => { + const routerProps = ({ + location, + } as unknown) as RouteComponentProps<{}>; expect(() => render( - +
, { wrapper: Wrapper } 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 9ed3a85fe68b5..aa71050802215 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/index.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/index.tsx @@ -4,25 +4,29 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { ReactNode } from 'react'; -import { i18n } from '@kbn/i18n'; import { EuiButtonEmpty, EuiPage, - EuiSideNav, - EuiPageSideBar, EuiPageBody, + EuiPageSideBar, + EuiSideNav, } from '@elastic/eui'; -import { HomeLink } from '../../shared/Links/apm/HomeLink'; -import { useLocation } from '../../../hooks/useLocation'; -import { getAPMHref } from '../../shared/Links/apm/APMLink'; +import { i18n } from '@kbn/i18n'; +import React, { ReactNode } from 'react'; +import { RouteComponentProps } from 'react-router-dom'; import { useApmPluginContext } from '../../../hooks/useApmPluginContext'; +import { getAPMHref } from '../../shared/Links/apm/APMLink'; +import { HomeLink } from '../../shared/Links/apm/HomeLink'; + +interface SettingsProps extends RouteComponentProps<{}> { + children: ReactNode; +} -export function Settings(props: { children: ReactNode }) { +export function Settings({ children, location }: SettingsProps) { const { core } = useApmPluginContext(); const { basePath } = core.http; const canAccessML = !!core.application.capabilities.ml?.canAccessML; - const { search, pathname } = useLocation(); + const { search, pathname } = location; function getSettingsHref(path: string) { return getAPMHref({ basePath, path: `/settings${path}`, search }); @@ -94,7 +98,7 @@ export function Settings(props: { children: ReactNode }) { ]} /> - {props.children} + {children} ); diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/index.tsx b/x-pack/plugins/apm/public/components/app/TransactionDetails/index.tsx index bab31c9a460d0..614089c0457ac 100644 --- a/x-pack/plugins/apm/public/components/app/TransactionDetails/index.tsx +++ b/x-pack/plugins/apm/public/components/app/TransactionDetails/index.tsx @@ -18,7 +18,6 @@ import { useTrackPageview } from '../../../../../observability/public'; import { Projection } from '../../../../common/projections'; import { ChartsSyncContextProvider } from '../../../context/ChartsSyncContext'; import { FETCH_STATUS } from '../../../hooks/useFetcher'; -import { useLocation } from '../../../hooks/useLocation'; import { useTransactionCharts } from '../../../hooks/useTransactionCharts'; import { useTransactionDistribution } from '../../../hooks/useTransactionDistribution'; import { useUrlParams } from '../../../hooks/useUrlParams'; @@ -32,9 +31,11 @@ import { WaterfallWithSummmary } from './WaterfallWithSummmary'; type TransactionDetailsProps = RouteComponentProps<{ serviceName: string }>; -export function TransactionDetails({ match }: TransactionDetailsProps) { +export function TransactionDetails({ + location, + match, +}: TransactionDetailsProps) { const { serviceName } = match.params; - const location = useLocation(); const { urlParams } = useUrlParams(); const { data: distributionData, diff --git a/x-pack/plugins/apm/public/components/app/TransactionOverview/index.tsx b/x-pack/plugins/apm/public/components/app/TransactionOverview/index.tsx index 544e2450fe5d9..3e32b0ec23b13 100644 --- a/x-pack/plugins/apm/public/components/app/TransactionOverview/index.tsx +++ b/x-pack/plugins/apm/public/components/app/TransactionOverview/index.tsx @@ -19,11 +19,11 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { Location } from 'history'; import { first } from 'lodash'; import React, { useMemo } from 'react'; +import { useLocation } from 'react-router-dom'; import { useTrackPageview } from '../../../../../observability/public'; import { Projection } from '../../../../common/projections'; import { ChartsSyncContextProvider } from '../../../context/ChartsSyncContext'; import { IUrlParams } from '../../../context/UrlParamsContext/types'; -import { useLocation } from '../../../hooks/useLocation'; import { useServiceTransactionTypes } from '../../../hooks/useServiceTransactionTypes'; import { useTransactionCharts } from '../../../hooks/useTransactionCharts'; import { useTransactionList } from '../../../hooks/useTransactionList'; diff --git a/x-pack/plugins/apm/public/components/shared/DatePicker/__test__/DatePicker.test.tsx b/x-pack/plugins/apm/public/components/shared/DatePicker/__test__/DatePicker.test.tsx index 01b1628504b3b..9de70d50b25e1 100644 --- a/x-pack/plugins/apm/public/components/shared/DatePicker/__test__/DatePicker.test.tsx +++ b/x-pack/plugins/apm/public/components/shared/DatePicker/__test__/DatePicker.test.tsx @@ -11,7 +11,6 @@ import { createMemoryHistory } from 'history'; import React, { ReactNode } from 'react'; import { Router } from 'react-router-dom'; import { MockApmPluginContextWrapper } from '../../../../context/ApmPluginContext/MockApmPluginContext'; -import { LocationProvider } from '../../../../context/LocationContext'; import { UrlParamsContext, useUiFilters, @@ -46,11 +45,9 @@ function mountDatePicker(params?: IUrlParams) { return mount( - - - - - + + + ); diff --git a/x-pack/plugins/apm/public/components/shared/DatePicker/index.tsx b/x-pack/plugins/apm/public/components/shared/DatePicker/index.tsx index 549ee228b7afe..b4d716f89169e 100644 --- a/x-pack/plugins/apm/public/components/shared/DatePicker/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/DatePicker/index.tsx @@ -7,10 +7,9 @@ import { EuiSuperDatePicker } from '@elastic/eui'; import { isEmpty, isEqual, pickBy } from 'lodash'; import React from 'react'; -import { useHistory } from 'react-router-dom'; +import { useHistory, useLocation } from 'react-router-dom'; import { UI_SETTINGS } from '../../../../../../../src/plugins/data/common'; import { useApmPluginContext } from '../../../hooks/useApmPluginContext'; -import { useLocation } from '../../../hooks/useLocation'; import { useUrlParams } from '../../../hooks/useUrlParams'; import { clearCache } from '../../../services/rest/callApi'; import { fromQuery, toQuery } from '../Links/url_helpers'; diff --git a/x-pack/plugins/apm/public/components/shared/EnvironmentFilter/index.tsx b/x-pack/plugins/apm/public/components/shared/EnvironmentFilter/index.tsx index 7e5c789507e07..c3ecaa1b053b6 100644 --- a/x-pack/plugins/apm/public/components/shared/EnvironmentFilter/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/EnvironmentFilter/index.tsx @@ -8,13 +8,12 @@ import { EuiSelect } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { History } from 'history'; import React from 'react'; -import { useHistory, useParams } from 'react-router-dom'; +import { useHistory, useLocation, useParams } from 'react-router-dom'; import { ENVIRONMENT_ALL, ENVIRONMENT_NOT_DEFINED, } from '../../../../common/environment_filter_values'; import { useEnvironments } from '../../../hooks/useEnvironments'; -import { useLocation } from '../../../hooks/useLocation'; import { useUrlParams } from '../../../hooks/useUrlParams'; import { fromQuery, toQuery } from '../Links/url_helpers'; 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 efd1446f21b21..157e014bee424 100644 --- a/x-pack/plugins/apm/public/components/shared/KueryBar/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/KueryBar/index.tsx @@ -7,7 +7,7 @@ import { i18n } from '@kbn/i18n'; import { startsWith, uniqueId } from 'lodash'; import React, { useState } from 'react'; -import { useHistory, useParams } from 'react-router-dom'; +import { useHistory, useLocation, useParams } from 'react-router-dom'; import styled from 'styled-components'; import { esKuery, @@ -16,7 +16,6 @@ import { } from '../../../../../../../src/plugins/data/public'; import { useApmPluginContext } from '../../../hooks/useApmPluginContext'; import { useDynamicIndexPattern } from '../../../hooks/useDynamicIndexPattern'; -import { useLocation } from '../../../hooks/useLocation'; import { useUrlParams } from '../../../hooks/useUrlParams'; import { fromQuery, toQuery } from '../Links/url_helpers'; import { getBoolFilter } from './get_bool_filter'; diff --git a/x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/DiscoverLink.tsx b/x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/DiscoverLink.tsx index d4b20782a5527..93b5672aa54f9 100644 --- a/x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/DiscoverLink.tsx +++ b/x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/DiscoverLink.tsx @@ -8,11 +8,11 @@ import { EuiLink } from '@elastic/eui'; import { Location } from 'history'; import { IBasePath } from 'kibana/public'; import React from 'react'; +import { useLocation } from 'react-router-dom'; import rison, { RisonValue } from 'rison-node'; import url from 'url'; import { APM_STATIC_INDEX_PATTERN_ID } from '../../../../../../../../src/plugins/apm_oss/public'; import { useApmPluginContext } from '../../../../hooks/useApmPluginContext'; -import { useLocation } from '../../../../hooks/useLocation'; import { getTimepickerRisonData } from '../rison_helpers'; interface Props { diff --git a/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLLink.tsx b/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLLink.tsx index 5e2d728716704..d8ec212515c6f 100644 --- a/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLLink.tsx +++ b/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLLink.tsx @@ -6,11 +6,11 @@ import { EuiLink } from '@elastic/eui'; import React from 'react'; -import url from 'url'; +import { useLocation } from 'react-router-dom'; import rison, { RisonValue } from 'rison-node'; -import { useLocation } from '../../../../hooks/useLocation'; -import { getTimepickerRisonData, TimepickerRisonData } from '../rison_helpers'; +import url from 'url'; import { useApmPluginContext } from '../../../../hooks/useApmPluginContext'; +import { getTimepickerRisonData, TimepickerRisonData } from '../rison_helpers'; interface MlRisonData { ml?: { diff --git a/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/useTimeSeriesExplorerHref.test.ts b/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/useTimeSeriesExplorerHref.test.ts index 28daae7fd830e..66f3903ba849b 100644 --- a/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/useTimeSeriesExplorerHref.test.ts +++ b/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/useTimeSeriesExplorerHref.test.ts @@ -12,7 +12,7 @@ jest.mock('../../../../hooks/useApmPluginContext', () => ({ }), })); -jest.mock('../../../../hooks/useLocation', () => ({ +jest.mock('react-router-dom', () => ({ useLocation: () => ({ search: '?rangeFrom=2020-07-29T17:27:29.000Z&rangeTo=2020-07-29T18:45:00.000Z&refreshInterval=10000&refreshPaused=true', diff --git a/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/useTimeSeriesExplorerHref.ts b/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/useTimeSeriesExplorerHref.ts index 459ee8f0282ff..3b60962d797ed 100644 --- a/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/useTimeSeriesExplorerHref.ts +++ b/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/useTimeSeriesExplorerHref.ts @@ -4,10 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import url from 'url'; import querystring from 'querystring'; +import { useLocation } from 'react-router-dom'; import rison from 'rison-node'; -import { useLocation } from '../../../../hooks/useLocation'; +import url from 'url'; import { useApmPluginContext } from '../../../../hooks/useApmPluginContext'; import { getTimepickerRisonData } from '../rison_helpers'; diff --git a/x-pack/plugins/apm/public/components/shared/Links/apm/APMLink.tsx b/x-pack/plugins/apm/public/components/shared/Links/apm/APMLink.tsx index cde2471cc4ce3..bd43f2455c545 100644 --- a/x-pack/plugins/apm/public/components/shared/Links/apm/APMLink.tsx +++ b/x-pack/plugins/apm/public/components/shared/Links/apm/APMLink.tsx @@ -8,9 +8,9 @@ import { EuiLink, EuiLinkAnchorProps } from '@elastic/eui'; import { IBasePath } from 'kibana/public'; import { pick } from 'lodash'; import React from 'react'; +import { useLocation } from 'react-router-dom'; import url from 'url'; import { useApmPluginContext } from '../../../../hooks/useApmPluginContext'; -import { useLocation } from '../../../../hooks/useLocation'; import { APMQueryParams, fromQuery, toQuery } from '../url_helpers'; interface Props extends EuiLinkAnchorProps { diff --git a/x-pack/plugins/apm/public/components/shared/MetadataTable/ErrorMetadata/__test__/ErrorMetadata.test.tsx b/x-pack/plugins/apm/public/components/shared/MetadataTable/ErrorMetadata/__test__/ErrorMetadata.test.tsx index de77115ce90ee..e95122f54aff1 100644 --- a/x-pack/plugins/apm/public/components/shared/MetadataTable/ErrorMetadata/__test__/ErrorMetadata.test.tsx +++ b/x-pack/plugins/apm/public/components/shared/MetadataTable/ErrorMetadata/__test__/ErrorMetadata.test.tsx @@ -4,18 +4,27 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; -import { ErrorMetadata } from '..'; import { render } from '@testing-library/react'; +import React, { ReactNode } from 'react'; +import { MemoryRouter } from 'react-router-dom'; +import { ErrorMetadata } from '..'; import { APMError } from '../../../../../../typings/es_schemas/ui/apm_error'; +import { MockApmPluginContextWrapper } from '../../../../../context/ApmPluginContext/MockApmPluginContext'; import { expectTextsInDocument, expectTextsNotInDocument, } from '../../../../../utils/testHelpers'; -import { MockApmPluginContextWrapper } from '../../../../../context/ApmPluginContext/MockApmPluginContext'; + +function Wrapper({ children }: { children?: ReactNode }) { + return ( + + {children} + + ); +} const renderOptions = { - wrapper: MockApmPluginContextWrapper, + wrapper: Wrapper, }; function getError() { diff --git a/x-pack/plugins/apm/public/components/shared/MetadataTable/SpanMetadata/__test__/SpanMetadata.test.tsx b/x-pack/plugins/apm/public/components/shared/MetadataTable/SpanMetadata/__test__/SpanMetadata.test.tsx index f27a7ebb780e0..1f10d923e351e 100644 --- a/x-pack/plugins/apm/public/components/shared/MetadataTable/SpanMetadata/__test__/SpanMetadata.test.tsx +++ b/x-pack/plugins/apm/public/components/shared/MetadataTable/SpanMetadata/__test__/SpanMetadata.test.tsx @@ -4,18 +4,27 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; import { render } from '@testing-library/react'; +import React, { ReactNode } from 'react'; +import { MemoryRouter } from 'react-router-dom'; import { SpanMetadata } from '..'; import { Span } from '../../../../../../typings/es_schemas/ui/span'; +import { MockApmPluginContextWrapper } from '../../../../../context/ApmPluginContext/MockApmPluginContext'; import { expectTextsInDocument, expectTextsNotInDocument, } from '../../../../../utils/testHelpers'; -import { MockApmPluginContextWrapper } from '../../../../../context/ApmPluginContext/MockApmPluginContext'; + +function Wrapper({ children }: { children?: ReactNode }) { + return ( + + {children} + + ); +} const renderOptions = { - wrapper: MockApmPluginContextWrapper, + wrapper: Wrapper, }; describe('SpanMetadata', () => { diff --git a/x-pack/plugins/apm/public/components/shared/MetadataTable/TransactionMetadata/__test__/TransactionMetadata.test.tsx b/x-pack/plugins/apm/public/components/shared/MetadataTable/TransactionMetadata/__test__/TransactionMetadata.test.tsx index 0577dca852bcf..8359716fc6966 100644 --- a/x-pack/plugins/apm/public/components/shared/MetadataTable/TransactionMetadata/__test__/TransactionMetadata.test.tsx +++ b/x-pack/plugins/apm/public/components/shared/MetadataTable/TransactionMetadata/__test__/TransactionMetadata.test.tsx @@ -4,18 +4,27 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; -import { TransactionMetadata } from '..'; import { render } from '@testing-library/react'; +import React, { ReactNode } from 'react'; +import { MemoryRouter } from 'react-router-dom'; +import { TransactionMetadata } from '..'; import { Transaction } from '../../../../../../typings/es_schemas/ui/transaction'; +import { MockApmPluginContextWrapper } from '../../../../../context/ApmPluginContext/MockApmPluginContext'; import { expectTextsInDocument, expectTextsNotInDocument, } from '../../../../../utils/testHelpers'; -import { MockApmPluginContextWrapper } from '../../../../../context/ApmPluginContext/MockApmPluginContext'; + +function Wrapper({ children }: { children?: ReactNode }) { + return ( + + {children} + + ); +} const renderOptions = { - wrapper: MockApmPluginContextWrapper, + wrapper: Wrapper, }; function getTransaction() { diff --git a/x-pack/plugins/apm/public/components/shared/MetadataTable/__test__/MetadataTable.test.tsx b/x-pack/plugins/apm/public/components/shared/MetadataTable/__test__/MetadataTable.test.tsx index 0b40c2a727fa3..8e53aa4aa1089 100644 --- a/x-pack/plugins/apm/public/components/shared/MetadataTable/__test__/MetadataTable.test.tsx +++ b/x-pack/plugins/apm/public/components/shared/MetadataTable/__test__/MetadataTable.test.tsx @@ -4,15 +4,24 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; import { render } from '@testing-library/react'; +import React, { ReactNode } from 'react'; +import { MemoryRouter } from 'react-router-dom'; import { MetadataTable } from '..'; +import { MockApmPluginContextWrapper } from '../../../../context/ApmPluginContext/MockApmPluginContext'; import { expectTextsInDocument } from '../../../../utils/testHelpers'; import { SectionsWithRows } from '../helper'; -import { MockApmPluginContextWrapper } from '../../../../context/ApmPluginContext/MockApmPluginContext'; + +function Wrapper({ children }: { children?: ReactNode }) { + return ( + + {children} + + ); +} const renderOptions = { - wrapper: MockApmPluginContextWrapper, + wrapper: Wrapper, }; describe('MetadataTable', () => { diff --git a/x-pack/plugins/apm/public/components/shared/MetadataTable/index.tsx b/x-pack/plugins/apm/public/components/shared/MetadataTable/index.tsx index a8990e70150d1..1d2ac4d18a2a7 100644 --- a/x-pack/plugins/apm/public/components/shared/MetadataTable/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/MetadataTable/index.tsx @@ -16,8 +16,7 @@ import { import { i18n } from '@kbn/i18n'; import { isEmpty } from 'lodash'; import React, { useCallback } from 'react'; -import { useHistory } from 'react-router-dom'; -import { useLocation } from '../../../hooks/useLocation'; +import { useHistory, useLocation } from 'react-router-dom'; import { useUrlParams } from '../../../hooks/useUrlParams'; import { ElasticDocsLink } from '../../shared/Links/ElasticDocsLink'; import { HeightRetainer } from '../HeightRetainer'; diff --git a/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLink/CustomLinkPopover.test.tsx b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLink/CustomLinkPopover.test.tsx index 0f42d8f9beb46..62952d1fb501b 100644 --- a/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLink/CustomLinkPopover.test.tsx +++ b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLink/CustomLinkPopover.test.tsx @@ -5,6 +5,7 @@ */ import { act, fireEvent, render } from '@testing-library/react'; import React, { ReactNode } from 'react'; +import { MemoryRouter } from 'react-router-dom'; import { CustomLink } from '../../../../../common/custom_link/custom_link_types'; import { Transaction } from '../../../../../typings/es_schemas/ui/transaction'; import { MockApmPluginContextWrapper } from '../../../../context/ApmPluginContext/MockApmPluginContext'; @@ -12,7 +13,11 @@ import { expectTextsInDocument } from '../../../../utils/testHelpers'; import { CustomLinkPopover } from './CustomLinkPopover'; function Wrapper({ children }: { children?: ReactNode }) { - return {children}; + return ( + + {children} + + ); } describe('CustomLinkPopover', () => { diff --git a/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLink/ManageCustomLink.test.tsx b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLink/ManageCustomLink.test.tsx index a2a2a211a48b1..29e93a47629b3 100644 --- a/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLink/ManageCustomLink.test.tsx +++ b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLink/ManageCustomLink.test.tsx @@ -4,17 +4,22 @@ * you may not use this file except in compliance with the Elastic License. */ +import { act, fireEvent, render } from '@testing-library/react'; import React, { ReactNode } from 'react'; -import { render, act, fireEvent } from '@testing-library/react'; -import { ManageCustomLink } from './ManageCustomLink'; +import { MemoryRouter } from 'react-router-dom'; +import { MockApmPluginContextWrapper } from '../../../../context/ApmPluginContext/MockApmPluginContext'; import { expectTextsInDocument, expectTextsNotInDocument, } from '../../../../utils/testHelpers'; -import { MockApmPluginContextWrapper } from '../../../../context/ApmPluginContext/MockApmPluginContext'; +import { ManageCustomLink } from './ManageCustomLink'; function Wrapper({ children }: { children?: ReactNode }) { - return {children}; + return ( + + {children} + + ); } describe('ManageCustomLink', () => { diff --git a/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLink/index.test.tsx b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLink/index.test.tsx index 055fcc0955c68..5abeae265dfa6 100644 --- a/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLink/index.test.tsx +++ b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLink/index.test.tsx @@ -6,6 +6,7 @@ import { act, fireEvent, render } from '@testing-library/react'; import React, { ReactNode } from 'react'; +import { MemoryRouter } from 'react-router-dom'; import { CustomLink } from '.'; import { CustomLink as CustomLinkType } from '../../../../../common/custom_link/custom_link_types'; import { Transaction } from '../../../../../typings/es_schemas/ui/transaction'; @@ -17,7 +18,11 @@ import { } from '../../../../utils/testHelpers'; function Wrapper({ children }: { children?: ReactNode }) { - return {children}; + return ( + + {children} + + ); } describe('Custom links', () => { diff --git a/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/TransactionActionMenu.tsx b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/TransactionActionMenu.tsx index c7ce83053db5c..4a548b44cf361 100644 --- a/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/TransactionActionMenu.tsx +++ b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/TransactionActionMenu.tsx @@ -7,6 +7,7 @@ import { EuiButtonEmpty } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React, { useMemo, useState } from 'react'; +import { useLocation } from 'react-router-dom'; import { ActionMenu, ActionMenuDivider, @@ -21,7 +22,6 @@ import { Transaction } from '../../../../typings/es_schemas/ui/transaction'; import { useApmPluginContext } from '../../../hooks/useApmPluginContext'; import { useFetcher } from '../../../hooks/useFetcher'; import { useLicense } from '../../../hooks/useLicense'; -import { useLocation } from '../../../hooks/useLocation'; import { useUrlParams } from '../../../hooks/useUrlParams'; import { CustomLinkFlyout } from '../../app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout'; import { convertFiltersToQuery } from '../../app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/helper'; diff --git a/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/__test__/TransactionActionMenu.test.tsx b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/__test__/TransactionActionMenu.test.tsx index e45df2feff771..ec0b473c3ade8 100644 --- a/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/__test__/TransactionActionMenu.test.tsx +++ b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/__test__/TransactionActionMenu.test.tsx @@ -6,6 +6,7 @@ import { act, fireEvent, render } from '@testing-library/react'; import React from 'react'; +import { MemoryRouter } from 'react-router-dom'; import { License } from '../../../../../../licensing/common/license'; import { Transaction } from '../../../../../typings/es_schemas/ui/transaction'; import { MockApmPluginContextWrapper } from '../../../../context/ApmPluginContext/MockApmPluginContext'; @@ -19,13 +20,19 @@ import { import { TransactionActionMenu } from '../TransactionActionMenu'; import * as Transactions from './mockData'; +function Wrapper({ children }: { children?: React.ReactNode }) { + return ( + + {children} + + ); +} + const renderTransaction = async (transaction: Record) => { const rendered = render( , { - wrapper: ({ children }: { children?: React.ReactNode }) => ( - {children} - ), + wrapper: Wrapper, } ); @@ -246,14 +253,11 @@ describe('TransactionActionMenu component', () => { function renderTransactionActionMenuWithLicense(license: License) { return render( - - - - + + , + { wrapper: Wrapper } ); } it('doesnt show custom links when license is not valid', () => { @@ -286,14 +290,11 @@ describe('TransactionActionMenu component', () => { }); const component = render( - - - - + + , + { wrapper: Wrapper } ); act(() => { fireEvent.click(component.getByText('Actions')); diff --git a/x-pack/plugins/apm/public/components/shared/charts/ErroneousTransactionsRateChart/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/ErroneousTransactionsRateChart/index.tsx index 1a91e398cec76..85d975870d9bc 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/ErroneousTransactionsRateChart/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/ErroneousTransactionsRateChart/index.tsx @@ -3,12 +3,11 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { EuiTitle } from '@elastic/eui'; +import { EuiPanel, EuiSpacer, EuiTitle } from '@elastic/eui'; import theme from '@elastic/eui/dist/eui_theme_light.json'; import { i18n } from '@kbn/i18n'; import React, { useCallback } from 'react'; -import { EuiPanel } from '@elastic/eui'; -import { EuiSpacer } from '@elastic/eui'; +import { useParams } from 'react-router-dom'; import { asPercent } from '../../../../../common/utils/formatters'; import { useChartsSync } from '../../../../hooks/useChartsSync'; import { useFetcher } from '../../../../hooks/useFetcher'; @@ -22,16 +21,11 @@ const tickFormatY = (y?: number) => { }; export function ErroneousTransactionsRateChart() { + const { serviceName } = useParams<{ serviceName?: string }>(); const { urlParams, uiFilters } = useUrlParams(); const syncedChartsProps = useChartsSync(); - const { - serviceName, - start, - end, - transactionType, - transactionName, - } = urlParams; + const { start, end, transactionType, transactionName } = urlParams; const { data } = useFetcher(() => { if (serviceName && start && end) { diff --git a/x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/ErrorMarker.test.tsx b/x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/ErrorMarker.test.tsx index 6a5ab344d2ba0..abe81185635b5 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/ErrorMarker.test.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/ErrorMarker.test.tsx @@ -7,6 +7,7 @@ import { fireEvent } from '@testing-library/react'; import { act } from '@testing-library/react-hooks'; import React, { ReactNode } from 'react'; +import { MemoryRouter } from 'react-router-dom'; import { MockApmPluginContextWrapper } from '../../../../../context/ApmPluginContext/MockApmPluginContext'; import { expectTextsInDocument, @@ -16,7 +17,11 @@ import { ErrorMark } from '../../../../app/TransactionDetails/WaterfallWithSummm import { ErrorMarker } from './ErrorMarker'; function Wrapper({ children }: { children?: ReactNode }) { - return {children}; + return ( + + {children} + + ); } describe('ErrorMarker', () => { diff --git a/x-pack/plugins/apm/public/context/LocationContext.tsx b/x-pack/plugins/apm/public/context/LocationContext.tsx deleted file mode 100644 index 1e9c20494b42e..0000000000000 --- a/x-pack/plugins/apm/public/context/LocationContext.tsx +++ /dev/null @@ -1,18 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { Location } from 'history'; -import React, { createContext } from 'react'; -import { withRouter } from 'react-router-dom'; - -const initialLocation = {} as Location; - -const LocationContext = createContext(initialLocation); -const LocationProvider = withRouter(({ location, children }) => { - return ; -}); - -export { LocationContext, LocationProvider }; diff --git a/x-pack/plugins/apm/public/hooks/useAvgDurationByBrowser.test.ts b/x-pack/plugins/apm/public/hooks/useAvgDurationByBrowser.test.tsx similarity index 82% rename from x-pack/plugins/apm/public/hooks/useAvgDurationByBrowser.test.ts rename to x-pack/plugins/apm/public/hooks/useAvgDurationByBrowser.test.tsx index b34cc95a26399..bb947e307437e 100644 --- a/x-pack/plugins/apm/public/hooks/useAvgDurationByBrowser.test.ts +++ b/x-pack/plugins/apm/public/hooks/useAvgDurationByBrowser.test.tsx @@ -8,6 +8,12 @@ import { renderHook } from '@testing-library/react-hooks'; import theme from '@elastic/eui/dist/eui_theme_light.json'; import * as useFetcherModule from './useFetcher'; import { useAvgDurationByBrowser } from './useAvgDurationByBrowser'; +import React, { ReactNode } from 'react'; +import { MemoryRouter } from 'react-router-dom'; + +function Wrapper({ children }: { children?: ReactNode }) { + return {children}; +} describe('useAvgDurationByBrowser', () => { it('returns data', () => { @@ -19,7 +25,9 @@ describe('useAvgDurationByBrowser', () => { refetch: () => {}, status: 'success' as useFetcherModule.FETCH_STATUS, }); - const { result } = renderHook(() => useAvgDurationByBrowser()); + const { result } = renderHook(() => useAvgDurationByBrowser(), { + wrapper: Wrapper, + }); expect(result.current.data).toEqual([ { diff --git a/x-pack/plugins/apm/public/hooks/useAvgDurationByBrowser.ts b/x-pack/plugins/apm/public/hooks/useAvgDurationByBrowser.ts index a8312dd0448e6..78dc4210711ef 100644 --- a/x-pack/plugins/apm/public/hooks/useAvgDurationByBrowser.ts +++ b/x-pack/plugins/apm/public/hooks/useAvgDurationByBrowser.ts @@ -5,12 +5,13 @@ */ import theme from '@elastic/eui/dist/eui_theme_light.json'; -import { useFetcher } from './useFetcher'; -import { useUrlParams } from './useUrlParams'; +import { useParams } from 'react-router-dom'; +import { getVizColorForIndex } from '../../common/viz_colors'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { AvgDurationByBrowserAPIResponse } from '../../server/lib/transactions/avg_duration_by_browser'; import { TimeSeries } from '../../typings/timeseries'; -import { getVizColorForIndex } from '../../common/viz_colors'; +import { useFetcher } from './useFetcher'; +import { useUrlParams } from './useUrlParams'; function toTimeSeries(data?: AvgDurationByBrowserAPIResponse): TimeSeries[] { if (!data) { @@ -27,8 +28,9 @@ function toTimeSeries(data?: AvgDurationByBrowserAPIResponse): TimeSeries[] { } export function useAvgDurationByBrowser() { + const { serviceName } = useParams<{ serviceName?: string }>(); const { - urlParams: { serviceName, start, end, transactionName }, + urlParams: { start, end, transactionName }, uiFilters, } = useUrlParams(); diff --git a/x-pack/plugins/apm/public/hooks/useAvgDurationByCountry.ts b/x-pack/plugins/apm/public/hooks/useAvgDurationByCountry.ts index 818657226cb14..983f949b72961 100644 --- a/x-pack/plugins/apm/public/hooks/useAvgDurationByCountry.ts +++ b/x-pack/plugins/apm/public/hooks/useAvgDurationByCountry.ts @@ -3,13 +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 { useParams } from 'react-router-dom'; import { useFetcher } from './useFetcher'; import { useUrlParams } from './useUrlParams'; export function useAvgDurationByCountry() { + const { serviceName } = useParams<{ serviceName?: string }>(); const { - urlParams: { serviceName, start, end, transactionName }, + urlParams: { start, end, transactionName }, uiFilters, } = useUrlParams(); diff --git a/x-pack/plugins/apm/public/hooks/useTransactionBreakdown.ts b/x-pack/plugins/apm/public/hooks/useTransactionBreakdown.ts index 9db78fde2d8c8..08d2300c3254a 100644 --- a/x-pack/plugins/apm/public/hooks/useTransactionBreakdown.ts +++ b/x-pack/plugins/apm/public/hooks/useTransactionBreakdown.ts @@ -4,12 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ +import { useParams } from 'react-router-dom'; import { useFetcher } from './useFetcher'; import { useUrlParams } from './useUrlParams'; export function useTransactionBreakdown() { + const { serviceName } = useParams<{ serviceName?: string }>(); const { - urlParams: { serviceName, start, end, transactionName, transactionType }, + urlParams: { start, end, transactionName, transactionType }, uiFilters, } = useUrlParams(); diff --git a/x-pack/plugins/apm/public/hooks/useTransactionCharts.ts b/x-pack/plugins/apm/public/hooks/useTransactionCharts.ts index 2ecd900386496..e66d70a53afa6 100644 --- a/x-pack/plugins/apm/public/hooks/useTransactionCharts.ts +++ b/x-pack/plugins/apm/public/hooks/useTransactionCharts.ts @@ -5,13 +5,15 @@ */ import { useMemo } from 'react'; +import { useParams } from 'react-router-dom'; import { getTransactionCharts } from '../selectors/chartSelectors'; import { useFetcher } from './useFetcher'; import { useUrlParams } from './useUrlParams'; export function useTransactionCharts() { + const { serviceName } = useParams<{ serviceName?: string }>(); const { - urlParams: { serviceName, transactionType, start, end, transactionName }, + urlParams: { transactionType, start, end, transactionName }, uiFilters, } = useUrlParams(); diff --git a/x-pack/plugins/apm/public/hooks/useTransactionDistribution.ts b/x-pack/plugins/apm/public/hooks/useTransactionDistribution.ts index 539918c432783..d93a27df1c861 100644 --- a/x-pack/plugins/apm/public/hooks/useTransactionDistribution.ts +++ b/x-pack/plugins/apm/public/hooks/useTransactionDistribution.ts @@ -4,11 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import { IUrlParams } from '../context/UrlParamsContext/types'; -import { useFetcher } from './useFetcher'; -import { useUiFilters } from '../context/UrlParamsContext'; +import { useParams } from 'react-router-dom'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { TransactionDistributionAPIResponse } from '../../server/lib/transactions/distribution'; +import { useUiFilters } from '../context/UrlParamsContext'; +import { IUrlParams } from '../context/UrlParamsContext/types'; +import { useFetcher } from './useFetcher'; const INITIAL_DATA = { buckets: [] as TransactionDistributionAPIResponse['buckets'], @@ -17,8 +18,8 @@ const INITIAL_DATA = { }; export function useTransactionDistribution(urlParams: IUrlParams) { + const { serviceName } = useParams<{ serviceName?: string }>(); const { - serviceName, start, end, transactionType, diff --git a/x-pack/plugins/apm/public/plugin.ts b/x-pack/plugins/apm/public/plugin.ts index b950b493c0f19..51ac6673251fb 100644 --- a/x-pack/plugins/apm/public/plugin.ts +++ b/x-pack/plugins/apm/public/plugin.ts @@ -37,6 +37,7 @@ import { import { AlertType } from '../common/alert_types'; import { featureCatalogueEntry } from './featureCatalogueEntry'; import { toggleAppLinkInNav } from './toggleAppLinkInNav'; +import { EmbeddableStart } from '../../../../src/plugins/embeddable/public'; export type ApmPluginSetup = void; export type ApmPluginStart = void; @@ -57,6 +58,7 @@ export interface ApmPluginStartDeps { home: void; licensing: void; triggers_actions_ui: TriggersAndActionsUIPublicPluginStart; + embeddable: EmbeddableStart; } export class ApmPlugin implements Plugin { @@ -103,7 +105,7 @@ export class ApmPlugin implements Plugin { id: 'apm', title: 'APM', order: 8300, - euiIconType: 'apmApp', + euiIconType: 'logoObservability', appRoute: '/app/apm', icon: 'plugins/apm/public/icon.svg', category: DEFAULT_APP_CATEGORIES.observability, @@ -123,16 +125,23 @@ export class ApmPlugin implements Plugin { id: 'csm', title: 'Client Side Monitoring', order: 8500, + euiIconType: 'logoObservability', category: DEFAULT_APP_CATEGORIES.observability, async mount(params: AppMountParameters) { // Load application bundle and Get start service - const [{ renderApp }, [coreStart]] = await Promise.all([ + const [{ renderApp }, [coreStart, corePlugins]] = await Promise.all([ import('./application/csmApp'), core.getStartServices(), ]); - return renderApp(coreStart, pluginSetupDeps, params, config); + return renderApp( + coreStart, + pluginSetupDeps, + params, + config, + corePlugins as ApmPluginStartDeps + ); }, }); } diff --git a/x-pack/plugins/apm/public/services/rest/apm_overview_fetchers.test.ts b/x-pack/plugins/apm/public/services/rest/apm_overview_fetchers.test.ts index 8b3ed38e25319..4e306c93805d0 100644 --- a/x-pack/plugins/apm/public/services/rest/apm_overview_fetchers.test.ts +++ b/x-pack/plugins/apm/public/services/rest/apm_overview_fetchers.test.ts @@ -51,7 +51,7 @@ describe('Observability dashboard data', () => { ); const response = await fetchOverviewPageData(params); expect(response).toEqual({ - appLink: '/app/apm#/services?rangeFrom=now-15m&rangeTo=now', + appLink: '/app/apm/services?rangeFrom=now-15m&rangeTo=now', stats: { services: { type: 'number', @@ -82,7 +82,7 @@ describe('Observability dashboard data', () => { ); const response = await fetchOverviewPageData(params); expect(response).toEqual({ - appLink: '/app/apm#/services?rangeFrom=now-15m&rangeTo=now', + appLink: '/app/apm/services?rangeFrom=now-15m&rangeTo=now', stats: { services: { type: 'number', @@ -109,7 +109,7 @@ describe('Observability dashboard data', () => { ); const response = await fetchOverviewPageData(params); expect(response).toEqual({ - appLink: '/app/apm#/services?rangeFrom=now-15m&rangeTo=now', + appLink: '/app/apm/services?rangeFrom=now-15m&rangeTo=now', stats: { services: { type: 'number', diff --git a/x-pack/plugins/apm/public/services/rest/apm_overview_fetchers.ts b/x-pack/plugins/apm/public/services/rest/apm_overview_fetchers.ts index a20f89fac2d60..422c7b882e5dc 100644 --- a/x-pack/plugins/apm/public/services/rest/apm_overview_fetchers.ts +++ b/x-pack/plugins/apm/public/services/rest/apm_overview_fetchers.ts @@ -32,7 +32,7 @@ export const fetchOverviewPageData = async ({ const { serviceCount, transactionCoordinates } = data; return { - appLink: `/app/apm#/services?rangeFrom=${relativeTime.start}&rangeTo=${relativeTime.end}`, + appLink: `/app/apm/services?rangeFrom=${relativeTime.start}&rangeTo=${relativeTime.end}`, stats: { services: { type: 'number', diff --git a/x-pack/plugins/apm/public/utils/testHelpers.tsx b/x-pack/plugins/apm/public/utils/testHelpers.tsx index 037da01c74464..a69288f7bd4f9 100644 --- a/x-pack/plugins/apm/public/utils/testHelpers.tsx +++ b/x-pack/plugins/apm/public/utils/testHelpers.tsx @@ -16,7 +16,6 @@ import { render, waitForElement } from '@testing-library/react'; import { MemoryRouter } from 'react-router-dom'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { APMConfig } from '../../server'; -import { LocationProvider } from '../context/LocationContext'; import { PromiseReturnType } from '../../typings/common'; import { EuiThemeProvider } from '../../../observability/public'; import { @@ -68,9 +67,7 @@ export async function getRenderedHref(Component: React.FC, location: Location) { const el = render( - - - + ); diff --git a/x-pack/plugins/canvas/public/components/asset_manager/__stories__/__snapshots__/asset.stories.storyshot b/x-pack/plugins/canvas/public/components/asset_manager/__stories__/__snapshots__/asset.stories.storyshot index 87205b363e697..14791cd3d8b25 100644 --- a/x-pack/plugins/canvas/public/components/asset_manager/__stories__/__snapshots__/asset.stories.storyshot +++ b/x-pack/plugins/canvas/public/components/asset_manager/__stories__/__snapshots__/asset.stories.storyshot @@ -355,181 +355,3 @@ exports[`Storyshots components/Assets/Asset marker 1`] = `
`; - -exports[`Storyshots components/Assets/Asset redux 1`] = ` -
-
-
-
-
- Asset thumbnail -
-
-
-
-

- - airplane - -
- - - ( - 1 - kb) - - -

-
-
-
-
- - - -
-
- -
- -
-
-
-
- -
- -
-
-
-
- - - -
-
-
-
-
-`; diff --git a/x-pack/plugins/canvas/public/components/fullscreen/fullscreen.scss b/x-pack/plugins/canvas/public/components/fullscreen/fullscreen.scss index 7110a22408fe2..edd681a2c33e8 100644 --- a/x-pack/plugins/canvas/public/components/fullscreen/fullscreen.scss +++ b/x-pack/plugins/canvas/public/components/fullscreen/fullscreen.scss @@ -1,5 +1,5 @@ body.canvas-isFullscreen { // sass-lint:disable-line no-qualifying-elements - // following two rules are for overriding the header bar padding + // following two rules are for overriding the header bar padding &.euiBody--headerIsFixed { padding-top: 0; } @@ -13,28 +13,17 @@ body.canvas-isFullscreen { // sass-lint:disable-line no-qualifying-elements padding-left: 0 !important; // sass-lint:disable-line no-important } - // hide global loading indicator .kbnLoadingIndicator { display: none; } - // remove space for global nav elements - // TODO #64541 - // Can delete this block - .chrHeaderWrapper ~ .app-wrapper { - // Override locked nav at all breakpoints - left: 0 !important; // sass-lint:disable-line no-important - top: 0; - } - // set the background color .canvasLayout { background: $euiColorInk; } // hide all the interface parts - .chrHeaderWrapper, // K7 global top nav .canvasLayout__stageHeader, .canvasLayout__sidebar, .canvasLayout__footer, diff --git a/x-pack/plugins/canvas/public/lib/fullscreen.js b/x-pack/plugins/canvas/public/lib/fullscreen.js index bb9990d4f5457..6ad52d591c9c4 100644 --- a/x-pack/plugins/canvas/public/lib/fullscreen.js +++ b/x-pack/plugins/canvas/public/lib/fullscreen.js @@ -4,6 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +import { platformService } from '../services'; + export const fullscreenClass = 'canvas-isFullscreen'; export function setFullscreen(fullscreen, doc = document) { @@ -13,8 +15,10 @@ export function setFullscreen(fullscreen, doc = document) { const isFullscreen = bodyClassList.contains(fullscreenClass); if (enabled && !isFullscreen) { + platformService.getService().setFullscreen(false); bodyClassList.add(fullscreenClass); } else if (!enabled && isFullscreen) { bodyClassList.remove(fullscreenClass); + platformService.getService().setFullscreen(true); } } diff --git a/x-pack/plugins/canvas/public/plugin.tsx b/x-pack/plugins/canvas/public/plugin.tsx index b02fb9db28612..fbca1e51bd5c6 100644 --- a/x-pack/plugins/canvas/public/plugin.tsx +++ b/x-pack/plugins/canvas/public/plugin.tsx @@ -88,7 +88,7 @@ export class CanvasPlugin category: DEFAULT_APP_CATEGORIES.kibana, id: 'canvas', title: 'Canvas', - euiIconType: 'canvasApp', + euiIconType: 'logoKibana', order: 3000, updater$: this.appUpdater, mount: async (params: AppMountParameters) => { diff --git a/x-pack/plugins/canvas/public/services/platform.ts b/x-pack/plugins/canvas/public/services/platform.ts index 92c378e9aa597..a27c57f32cf9f 100644 --- a/x-pack/plugins/canvas/public/services/platform.ts +++ b/x-pack/plugins/canvas/public/services/platform.ts @@ -10,6 +10,7 @@ import { IUiSettingsClient, ChromeBreadcrumb, IBasePath, + ChromeStart, } from '../../../../../src/core/public'; import { CanvasServiceFactory } from '.'; @@ -22,6 +23,7 @@ export interface PlatformService { getUISetting: (key: string, defaultValue?: any) => any; setBreadcrumbs: (newBreadcrumbs: ChromeBreadcrumb[]) => void; setRecentlyAccessed: (link: string, label: string, id: string) => void; + setFullscreen: ChromeStart['setIsVisible']; // TODO: these should go away. We want thin accessors, not entire objects. // Entire objects are hard to mock, and hide our dependency on the external service. @@ -45,6 +47,7 @@ export const platformServiceFactory: CanvasServiceFactory = ( getUISetting: coreStart.uiSettings.get.bind(coreStart.uiSettings), setBreadcrumbs: coreStart.chrome.setBreadcrumbs, setRecentlyAccessed: coreStart.chrome.recentlyAccessed.add, + setFullscreen: coreStart.chrome.setIsVisible, // TODO: these should go away. We want thin accessors, not entire objects. // Entire objects are hard to mock, and hide our dependency on the external service. diff --git a/x-pack/plugins/canvas/public/services/stubs/platform.ts b/x-pack/plugins/canvas/public/services/stubs/platform.ts index 9ada579573502..bef3b2609537c 100644 --- a/x-pack/plugins/canvas/public/services/stubs/platform.ts +++ b/x-pack/plugins/canvas/public/services/stubs/platform.ts @@ -20,4 +20,5 @@ export const platformService: PlatformService = { getSavedObjects: noop, getSavedObjectsClient: noop, getUISettings: noop, + setFullscreen: noop, }; diff --git a/x-pack/plugins/enterprise_search/common/constants.ts b/x-pack/plugins/enterprise_search/common/constants.ts index c6ca0d532ce07..d6a51e8b482d0 100644 --- a/x-pack/plugins/enterprise_search/common/constants.ts +++ b/x-pack/plugins/enterprise_search/common/constants.ts @@ -29,6 +29,7 @@ export const ENTERPRISE_SEARCH_PLUGIN = { }), ], URL: '/app/enterprise_search/overview', + LOGO: 'logoEnterpriseSearch', }; export const APP_SEARCH_PLUGIN = { diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/handle_api_errors.test.ts b/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/handle_api_errors.test.ts new file mode 100644 index 0000000000000..6f2aec757eb37 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/handle_api_errors.test.ts @@ -0,0 +1,65 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +jest.mock('./', () => ({ + FlashMessagesLogic: { + actions: { + setFlashMessages: jest.fn(), + setQueuedMessages: jest.fn(), + }, + }, +})); +import { FlashMessagesLogic } from './'; + +import { flashAPIErrors } from './handle_api_errors'; + +describe('flashAPIErrors', () => { + const mockHttpError = { + body: { + statusCode: 404, + error: 'Not Found', + message: 'Could not find X,Could not find Y,Something else bad happened', + attributes: { + errors: ['Could not find X', 'Could not find Y', 'Something else bad happened'], + }, + }, + } as any; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('converts API errors into flash messages', () => { + flashAPIErrors(mockHttpError); + + expect(FlashMessagesLogic.actions.setFlashMessages).toHaveBeenCalledWith([ + { type: 'error', message: 'Could not find X' }, + { type: 'error', message: 'Could not find Y' }, + { type: 'error', message: 'Something else bad happened' }, + ]); + }); + + it('queues messages when isQueued is passed', () => { + flashAPIErrors(mockHttpError, { isQueued: true }); + + expect(FlashMessagesLogic.actions.setQueuedMessages).toHaveBeenCalledWith([ + { type: 'error', message: 'Could not find X' }, + { type: 'error', message: 'Could not find Y' }, + { type: 'error', message: 'Something else bad happened' }, + ]); + }); + + it('displays a generic error message and re-throws non-API errors', () => { + try { + flashAPIErrors(Error('whatever') as any); + } catch (e) { + expect(e.message).toEqual('whatever'); + expect(FlashMessagesLogic.actions.setFlashMessages).toHaveBeenCalledWith([ + { type: 'error', message: 'An unexpected error occurred' }, + ]); + } + }); +}); 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 new file mode 100644 index 0000000000000..5e56c4fb0bd22 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/handle_api_errors.ts @@ -0,0 +1,56 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { HttpResponse } from 'src/core/public'; + +import { FlashMessagesLogic, IFlashMessage } from './'; + +/** + * The API errors we are handling can come from one of two ways: + * - When our http calls recieve a response containing an error code, such as a 404 or 500 + * - Our own JS while handling a successful response + * + * In the first case, if it is a purposeful error (like a 404) we will receive an + * `errors` property in the response's data, which will contain messages we can + * display to the user. + */ +interface IErrorResponse { + statusCode: number; + error: string; + message: string; + attributes: { + errors: string[]; + }; +} +interface IOptions { + isQueued?: boolean; +} + +/** + * Converts API/HTTP errors into user-facing Flash Messages + */ +export const flashAPIErrors = ( + error: HttpResponse, + { isQueued }: IOptions = {} +) => { + const defaultErrorMessage = 'An unexpected error occurred'; + + const errorFlashMessages: IFlashMessage[] = Array.isArray(error?.body?.attributes?.errors) + ? error.body!.attributes.errors.map((message) => ({ type: 'error', message })) + : [{ type: 'error', message: defaultErrorMessage }]; + + if (isQueued) { + FlashMessagesLogic.actions.setQueuedMessages(errorFlashMessages); + } else { + FlashMessagesLogic.actions.setFlashMessages(errorFlashMessages); + } + + // If this was a programming error or a failed request (such as a CORS) error, + // we rethrow the error so it shows up in the developer console + if (!error?.body?.message) { + throw error; + } +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/http/http_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/shared/http/http_logic.test.ts index a6957340d33d3..c032e3b04ebe6 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/http/http_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/http/http_logic.test.ts @@ -74,21 +74,24 @@ describe('HttpLogic', () => { describe('errorConnectingInterceptor', () => { it('handles errors connecting to Enterprise Search', async () => { const { responseError } = mockHttp.intercept.mock.calls[0][0] as any; - await responseError({ response: { url: '/api/app_search/engines', status: 502 } }); + const httpResponse = { response: { url: '/api/app_search/engines', status: 502 } }; + await expect(responseError(httpResponse)).rejects.toEqual(httpResponse); expect(HttpLogic.actions.setErrorConnecting).toHaveBeenCalled(); }); it('does not handle non-502 Enterprise Search errors', async () => { const { responseError } = mockHttp.intercept.mock.calls[0][0] as any; - await responseError({ response: { url: '/api/workplace_search/overview', status: 404 } }); + const httpResponse = { response: { url: '/api/workplace_search/overview', status: 404 } }; + await expect(responseError(httpResponse)).rejects.toEqual(httpResponse); expect(HttpLogic.actions.setErrorConnecting).not.toHaveBeenCalled(); }); it('does not handle errors for unrelated calls', async () => { const { responseError } = mockHttp.intercept.mock.calls[0][0] as any; - await responseError({ response: { url: '/api/some_other_plugin/', status: 502 } }); + const httpResponse = { response: { url: '/api/some_other_plugin/', status: 502 } }; + await expect(responseError(httpResponse)).rejects.toEqual(httpResponse); expect(HttpLogic.actions.setErrorConnecting).not.toHaveBeenCalled(); }); 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 fb2d9b1061723..ec9db30ddef3b 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 @@ -6,7 +6,7 @@ import { kea, MakeLogicType } from 'kea'; -import { HttpSetup } from 'src/core/public'; +import { HttpSetup, HttpInterceptorResponseError } from 'src/core/public'; export interface IHttpValues { http: HttpSetup; @@ -68,7 +68,9 @@ export const HttpLogic = kea>({ if (isApiResponse && hasErrorConnecting) { actions.setErrorConnecting(true); } - return httpResponse; + + // Re-throw error so that downstream catches work as expected + return Promise.reject(httpResponse) as Promise; }, }); httpInterceptors.push(errorConnectingInterceptor); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/user_icon/user_icon.scss b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/user_icon/user_icon.scss new file mode 100644 index 0000000000000..a47abba723fc8 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/user_icon/user_icon.scss @@ -0,0 +1,137 @@ +.user-icon { + border-radius: 50%; + overflow: hidden; + width: 70px; + height: 70px; + display: inline-flex; + justify-content: center; + align-items: center; + flex-shrink: 0; + position: relative; + font-weight: 500; + background: $euiColorPrimary; + color: $euiColorEmptyShade; + + &:not(.user-icon--small) { + font-size: 1.75rem; + } + + &--small { + width: 30px; + height: 30px; + font-size: .875rem; + margin-right: .5rem; + } + + &__text { + text-shadow: 0 1px 2px rgba(black, 0.08); + justify-content: center; + align-items: center; + font-size: 1.125rem; + color: $euiColorEmptyShade; + font-weight: 500; + + .user-icon--small & { + font-size: .87rem; + } + } + + &__image { + max-width: 100%; + width: 100%; + height: auto; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + } +} + +.group-avatars { + display: flex; + flex-direction: row; + justify-content: flex-start; + padding-top: 4px; + + &__additional { + line-height: 2; + vertical-align: middle; + padding-left: 4px; + } + + .group-user-icon { + display: flex; + justify-content: center; + align-items: center; + position: relative; + overflow: hidden; + width: 32px; + height: 32px; + + & > * { + pointer-events: none; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + } + + &__text { + padding-bottom: 2px; + z-index: 2; + line-height: 1; + font-weight: 500; + color: $euiColorEmptyShade; + font-size: .875rem; + text-shadow: 0 1px 2px rgba(black, 0.08); + } + + &__image { + width: 24px; + height: 24px; + } + } +} + +.groups-tooltip { + position: relative; + display: inline-block; + + &:hover &__container { + visibility: visible; + } + + &__container { + visibility: hidden; + position: absolute; + display: flex; + flex-direction: column; + align-items: flex-start; + box-shadow: 0px 0px 12px rgba(black, 0.15); + background: white; + text-align: left; + text-overflow: ellipsis; + min-width: 125px; + padding: 6px 10px 6px 10px; + border-radius: 3px; + z-index: 5; + top: 100%; + margin-left: -10px; + } + + &__text { + line-height: 1.5; + white-space: nowrap; + } + + &__text:before { + content:''; + display:block; + width:0; + height:0; + position:absolute; + border-left: 6px solid transparent; + border-right: 6px solid transparent; + border-bottom: 6px solid white; + top: -6px; + left: 16px; + } +} diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/user_icon/user_icon.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/user_icon/user_icon.test.tsx index f8c1d00047825..c8d8540bfbd01 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/user_icon/user_icon.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/user_icon/user_icon.test.tsx @@ -15,7 +15,7 @@ describe('SourcesTable', () => { it('renders with picture', () => { const wrapper = shallow(); - expect(wrapper.find('.avatar')).toHaveLength(1); + expect(wrapper.find('.user-icon')).toHaveLength(1); expect(wrapper.find('.avatar__image')).toHaveLength(1); }); @@ -26,7 +26,7 @@ describe('SourcesTable', () => { }; const wrapper = shallow(); - expect(wrapper.find('.avatar')).toHaveLength(1); + expect(wrapper.find('.user-icon')).toHaveLength(1); expect(wrapper.find('.avatar__text')).toHaveLength(1); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/user_icon/user_icon.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/user_icon/user_icon.tsx index 28ea303d14eaf..7abec84173f33 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/user_icon/user_icon.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/user_icon/user_icon.tsx @@ -8,8 +8,10 @@ import React from 'react'; import { IUser } from '../../../types'; +import './user_icon.scss'; + export const UserIcon: React.FC = ({ name, pictureUrl, color, initials, email }) => ( -
+
{pictureUrl ? ( {name ) : ( diff --git a/x-pack/plugins/enterprise_search/public/plugin.ts b/x-pack/plugins/enterprise_search/public/plugin.ts index b735db7c49520..63f334811ce31 100644 --- a/x-pack/plugins/enterprise_search/public/plugin.ts +++ b/x-pack/plugins/enterprise_search/public/plugin.ts @@ -5,26 +5,25 @@ */ import { - Plugin, - PluginInitializerContext, + AppMountParameters, CoreSetup, CoreStart, - AppMountParameters, HttpSetup, + Plugin, + PluginInitializerContext, } from 'src/core/public'; +import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/public'; import { FeatureCatalogueCategory, HomePublicPluginSetup, } from '../../../../src/plugins/home/public'; -import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/public'; import { LicensingPluginSetup } from '../../licensing/public'; - -import { IInitialAppData } from '../common/types'; import { - ENTERPRISE_SEARCH_PLUGIN, APP_SEARCH_PLUGIN, + ENTERPRISE_SEARCH_PLUGIN, WORKPLACE_SEARCH_PLUGIN, } from '../common/constants'; +import { IInitialAppData } from '../common/types'; import { ExternalUrl, IExternalUrl } from './applications/shared/enterprise_search_url'; export interface ClientConfigType { @@ -73,6 +72,7 @@ export class EnterpriseSearchPlugin implements Plugin { core.application.register({ id: APP_SEARCH_PLUGIN.ID, title: APP_SEARCH_PLUGIN.NAME, + euiIconType: ENTERPRISE_SEARCH_PLUGIN.LOGO, appRoute: APP_SEARCH_PLUGIN.URL, category: DEFAULT_APP_CATEGORIES.enterpriseSearch, mount: async (params: AppMountParameters) => { @@ -92,6 +92,7 @@ export class EnterpriseSearchPlugin implements Plugin { core.application.register({ id: WORKPLACE_SEARCH_PLUGIN.ID, title: WORKPLACE_SEARCH_PLUGIN.NAME, + euiIconType: ENTERPRISE_SEARCH_PLUGIN.LOGO, appRoute: WORKPLACE_SEARCH_PLUGIN.URL, category: DEFAULT_APP_CATEGORIES.enterpriseSearch, mount: async (params: AppMountParameters) => { diff --git a/x-pack/plugins/enterprise_search/server/lib/enterprise_search_request_handler.test.ts b/x-pack/plugins/enterprise_search/server/lib/enterprise_search_request_handler.test.ts index 34f83ef3a3fd2..0c1e81e3aba46 100644 --- a/x-pack/plugins/enterprise_search/server/lib/enterprise_search_request_handler.test.ts +++ b/x-pack/plugins/enterprise_search/server/lib/enterprise_search_request_handler.test.ts @@ -30,109 +30,198 @@ describe('EnterpriseSearchRequestHandler', () => { fetchMock.mockReset(); }); - it('makes an API call and returns the response', async () => { - const responseBody = { - results: [{ name: 'engine1' }], - meta: { page: { total_results: 1 } }, - }; + describe('createRequest()', () => { + it('makes an API call and returns the response', async () => { + const responseBody = { + results: [{ name: 'engine1' }], + meta: { page: { total_results: 1 } }, + }; - EnterpriseSearchAPI.mockReturn(responseBody); + EnterpriseSearchAPI.mockReturn(responseBody); - const requestHandler = enterpriseSearchRequestHandler.createRequest({ - path: '/as/credentials/collection', - }); + const requestHandler = enterpriseSearchRequestHandler.createRequest({ + path: '/as/credentials/collection', + }); - await makeAPICall(requestHandler, { - query: { - type: 'indexed', - pageIndex: 1, - }, - }); + await makeAPICall(requestHandler, { + query: { + type: 'indexed', + pageIndex: 1, + }, + }); - EnterpriseSearchAPI.shouldHaveBeenCalledWith( - 'http://localhost:3002/as/credentials/collection?type=indexed&pageIndex=1', - { method: 'GET' } - ); + EnterpriseSearchAPI.shouldHaveBeenCalledWith( + 'http://localhost:3002/as/credentials/collection?type=indexed&pageIndex=1', + { method: 'GET' } + ); - expect(responseMock.custom).toHaveBeenCalledWith({ - body: responseBody, - statusCode: 200, + expect(responseMock.custom).toHaveBeenCalledWith({ + body: responseBody, + statusCode: 200, + }); }); - }); - describe('request passing', () => { - it('passes route method', async () => { - const requestHandler = enterpriseSearchRequestHandler.createRequest({ path: '/api/example' }); + describe('request passing', () => { + it('passes route method', async () => { + const requestHandler = enterpriseSearchRequestHandler.createRequest({ + path: '/api/example', + }); + + await makeAPICall(requestHandler, { route: { method: 'POST' } }); + EnterpriseSearchAPI.shouldHaveBeenCalledWith('http://localhost:3002/api/example', { + method: 'POST', + }); - await makeAPICall(requestHandler, { route: { method: 'POST' } }); - EnterpriseSearchAPI.shouldHaveBeenCalledWith('http://localhost:3002/api/example', { - method: 'POST', + await makeAPICall(requestHandler, { route: { method: 'DELETE' } }); + EnterpriseSearchAPI.shouldHaveBeenCalledWith('http://localhost:3002/api/example', { + method: 'DELETE', + }); }); - await makeAPICall(requestHandler, { route: { method: 'DELETE' } }); - EnterpriseSearchAPI.shouldHaveBeenCalledWith('http://localhost:3002/api/example', { - method: 'DELETE', + it('passes request body', async () => { + const requestHandler = enterpriseSearchRequestHandler.createRequest({ + path: '/api/example', + }); + await makeAPICall(requestHandler, { body: { bodacious: true } }); + + EnterpriseSearchAPI.shouldHaveBeenCalledWith('http://localhost:3002/api/example', { + body: '{"bodacious":true}', + }); }); - }); - it('passes request body', async () => { - const requestHandler = enterpriseSearchRequestHandler.createRequest({ path: '/api/example' }); - await makeAPICall(requestHandler, { body: { bodacious: true } }); + it('passes custom params set by the handler, which override request params', async () => { + const requestHandler = enterpriseSearchRequestHandler.createRequest({ + path: '/api/example', + params: { someQuery: true }, + }); + await makeAPICall(requestHandler, { query: { someQuery: false } }); - EnterpriseSearchAPI.shouldHaveBeenCalledWith('http://localhost:3002/api/example', { - body: '{"bodacious":true}', + EnterpriseSearchAPI.shouldHaveBeenCalledWith( + 'http://localhost:3002/api/example?someQuery=true' + ); }); }); - it('passes custom params set by the handler, which override request params', async () => { - const requestHandler = enterpriseSearchRequestHandler.createRequest({ - path: '/api/example', - params: { someQuery: true }, + describe('response passing', () => { + it('returns the response status code from Enterprise Search', async () => { + EnterpriseSearchAPI.mockReturn({}, { status: 201 }); + + const requestHandler = enterpriseSearchRequestHandler.createRequest({ + path: '/api/example', + }); + await makeAPICall(requestHandler); + + EnterpriseSearchAPI.shouldHaveBeenCalledWith('http://localhost:3002/api/example'); + expect(responseMock.custom).toHaveBeenCalledWith({ body: {}, statusCode: 201 }); }); - await makeAPICall(requestHandler, { query: { someQuery: false } }); - EnterpriseSearchAPI.shouldHaveBeenCalledWith( - 'http://localhost:3002/api/example?someQuery=true' - ); + // TODO: It's possible we may also pass back headers at some point + // from Enterprise Search, e.g. the x-read-only mode header }); }); - describe('response passing', () => { - it('returns the response status code from Enterprise Search', async () => { - EnterpriseSearchAPI.mockReturn({}, { status: 404 }); + describe('error responses', () => { + describe('handleClientError()', () => { + afterEach(() => { + EnterpriseSearchAPI.shouldHaveBeenCalledWith('http://localhost:3002/api/4xx'); + expect(mockLogger.error).not.toHaveBeenCalled(); + }); - const requestHandler = enterpriseSearchRequestHandler.createRequest({ path: '/api/example' }); - await makeAPICall(requestHandler); + it('passes back json.error', async () => { + const error = 'some error message'; + EnterpriseSearchAPI.mockReturn({ error }, { status: 404, headers: JSON_HEADER }); - EnterpriseSearchAPI.shouldHaveBeenCalledWith('http://localhost:3002/api/example'); - expect(responseMock.custom).toHaveBeenCalledWith({ body: {}, statusCode: 404 }); - }); + const requestHandler = enterpriseSearchRequestHandler.createRequest({ path: '/api/4xx' }); + await makeAPICall(requestHandler); - // TODO: It's possible we may also pass back headers at some point - // from Enterprise Search, e.g. the x-read-only mode header - }); + expect(responseMock.customError).toHaveBeenCalledWith({ + statusCode: 404, + body: { + message: 'some error message', + attributes: { errors: ['some error message'] }, + }, + }); + }); - describe('error handling', () => { - afterEach(() => { - expect(mockLogger.error).toHaveBeenCalledWith( - expect.stringContaining('Error connecting to Enterprise Search') - ); + it('passes back json.errors', async () => { + const errors = ['one', 'two', 'three']; + EnterpriseSearchAPI.mockReturn({ errors }, { status: 400, headers: JSON_HEADER }); + + const requestHandler = enterpriseSearchRequestHandler.createRequest({ path: '/api/4xx' }); + await makeAPICall(requestHandler); + + expect(responseMock.customError).toHaveBeenCalledWith({ + statusCode: 400, + body: { + message: 'one,two,three', + attributes: { errors: ['one', 'two', 'three'] }, + }, + }); + }); + + it('handles empty json', async () => { + EnterpriseSearchAPI.mockReturn({}, { status: 400, headers: JSON_HEADER }); + + const requestHandler = enterpriseSearchRequestHandler.createRequest({ path: '/api/4xx' }); + await makeAPICall(requestHandler); + + expect(responseMock.customError).toHaveBeenCalledWith({ + statusCode: 400, + body: { + message: 'Bad Request', + attributes: { errors: ['Bad Request'] }, + }, + }); + }); + + it('handles invalid json', async () => { + EnterpriseSearchAPI.mockReturn('invalid' as any, { status: 400, headers: JSON_HEADER }); + + const requestHandler = enterpriseSearchRequestHandler.createRequest({ path: '/api/4xx' }); + await makeAPICall(requestHandler); + + expect(responseMock.customError).toHaveBeenCalledWith({ + statusCode: 400, + body: { + message: 'Bad Request', + attributes: { errors: ['Bad Request'] }, + }, + }); + }); + + it('handles blank bodies', async () => { + EnterpriseSearchAPI.mockReturn(undefined as any, { status: 404 }); + + const requestHandler = enterpriseSearchRequestHandler.createRequest({ path: '/api/4xx' }); + await makeAPICall(requestHandler); + + expect(responseMock.customError).toHaveBeenCalledWith({ + statusCode: 404, + body: { + message: 'Not Found', + attributes: { errors: ['Not Found'] }, + }, + }); + }); }); - it('returns an error when an API request fails', async () => { - EnterpriseSearchAPI.mockReturnError(); - const requestHandler = enterpriseSearchRequestHandler.createRequest({ path: '/api/failed' }); + it('handleServerError()', async () => { + EnterpriseSearchAPI.mockReturn('something crashed!' as any, { status: 500 }); + const requestHandler = enterpriseSearchRequestHandler.createRequest({ path: '/api/5xx' }); await makeAPICall(requestHandler); - EnterpriseSearchAPI.shouldHaveBeenCalledWith('http://localhost:3002/api/failed'); + EnterpriseSearchAPI.shouldHaveBeenCalledWith('http://localhost:3002/api/5xx'); expect(responseMock.customError).toHaveBeenCalledWith({ - body: 'Error connecting to Enterprise Search: Failed', statusCode: 502, + body: expect.stringContaining('Enterprise Search encountered an internal server error'), }); + expect(mockLogger.error).toHaveBeenCalledWith( + 'Enterprise Search Server Error 500 at : "something crashed!"' + ); }); - it('returns an error when `hasValidData` fails', async () => { + it('handleInvalidDataError()', async () => { EnterpriseSearchAPI.mockReturn({ results: false }); const requestHandler = enterpriseSearchRequestHandler.createRequest({ path: '/api/invalid', @@ -143,15 +232,29 @@ describe('EnterpriseSearchRequestHandler', () => { EnterpriseSearchAPI.shouldHaveBeenCalledWith('http://localhost:3002/api/invalid'); expect(responseMock.customError).toHaveBeenCalledWith({ - body: 'Error connecting to Enterprise Search: Invalid data received', statusCode: 502, + body: 'Invalid data received from Enterprise Search', }); - expect(mockLogger.debug).toHaveBeenCalledWith( + expect(mockLogger.error).toHaveBeenCalledWith( 'Invalid data received from : {"results":false}' ); }); - describe('user authentication errors', () => { + it('handleConnectionError()', async () => { + EnterpriseSearchAPI.mockReturnError(); + const requestHandler = enterpriseSearchRequestHandler.createRequest({ path: '/api/failed' }); + + await makeAPICall(requestHandler); + EnterpriseSearchAPI.shouldHaveBeenCalledWith('http://localhost:3002/api/failed'); + + expect(responseMock.customError).toHaveBeenCalledWith({ + statusCode: 502, + body: 'Error connecting to Enterprise Search: Failed', + }); + expect(mockLogger.error).toHaveBeenCalled(); + }); + + describe('handleAuthenticationError()', () => { afterEach(async () => { const requestHandler = enterpriseSearchRequestHandler.createRequest({ path: '/api/unauthenticated', @@ -160,9 +263,10 @@ describe('EnterpriseSearchRequestHandler', () => { EnterpriseSearchAPI.shouldHaveBeenCalledWith('http://localhost:3002/api/unauthenticated'); expect(responseMock.customError).toHaveBeenCalledWith({ - body: 'Error connecting to Enterprise Search: Cannot authenticate Enterprise Search user', statusCode: 502, + body: 'Cannot authenticate Enterprise Search user', }); + expect(mockLogger.error).toHaveBeenCalled(); }); it('errors when redirected to /login', async () => { @@ -175,7 +279,7 @@ describe('EnterpriseSearchRequestHandler', () => { }); }); - it('has a helper for checking empty objects', async () => { + it('isEmptyObj', async () => { expect(enterpriseSearchRequestHandler.isEmptyObj({})).toEqual(true); expect(enterpriseSearchRequestHandler.isEmptyObj({ empty: false })).toEqual(false); }); diff --git a/x-pack/plugins/enterprise_search/server/lib/enterprise_search_request_handler.ts b/x-pack/plugins/enterprise_search/server/lib/enterprise_search_request_handler.ts index 18f10c590847c..00d5eaf5d6a83 100644 --- a/x-pack/plugins/enterprise_search/server/lib/enterprise_search_request_handler.ts +++ b/x-pack/plugins/enterprise_search/server/lib/enterprise_search_request_handler.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import fetch from 'node-fetch'; +import fetch, { Response } from 'node-fetch'; import querystring from 'querystring'; import { RequestHandler, @@ -25,6 +25,12 @@ interface IRequestParams { params?: object; hasValidData?: (body?: ResponseBody) => boolean; } +interface IErrorResponse { + message: string; + attributes: { + errors: string[]; + }; +} export interface IEnterpriseSearchRequestHandler { createRequest(requestParams?: object): RequestHandler; } @@ -71,34 +77,131 @@ export class EnterpriseSearchRequestHandler { ? JSON.stringify(request.body) : undefined; - // Call the Enterprise Search API and pass back response to the front-end + // Call the Enterprise Search API const apiResponse = await fetch(url, { method, headers, body }); + // Handle authentication redirects if (apiResponse.url.endsWith('/login') || apiResponse.url.endsWith('/ent/select')) { - throw new Error('Cannot authenticate Enterprise Search user'); + return this.handleAuthenticationError(response); } + // Handle 400-500+ responses from the Enterprise Search server const { status } = apiResponse; - const json = await apiResponse.json(); + if (status >= 500) { + return this.handleServerError(response, apiResponse, url); + } else if (status >= 400) { + return this.handleClientError(response, apiResponse); + } - if (hasValidData(json)) { - return response.custom({ statusCode: status, body: json }); - } else { - this.log.debug(`Invalid data received from <${url}>: ${JSON.stringify(json)}`); - throw new Error('Invalid data received'); + // Check returned data + const json = await apiResponse.json(); + if (!hasValidData(json)) { + return this.handleInvalidDataError(response, url, json); } + + // Pass successful responses back to the front-end + return response.custom({ statusCode: status, body: json }); } catch (e) { - const errorMessage = `Error connecting to Enterprise Search: ${e?.message || e.toString()}`; + // Catch connection/auth errors + return this.handleConnectionError(response, e); + } + }; + } - this.log.error(errorMessage); - if (e instanceof Error) this.log.debug(e.stack as string); + /** + * Attempt to grab a usable error body from Enterprise Search - this isn't + * always possible because some of our internal endpoints send back blank + * bodies, and sometimes the server sends back Ruby on Rails error pages + */ + async getErrorResponseBody(apiResponse: Response) { + const { statusText } = apiResponse; + const contentType = apiResponse.headers.get('content-type') || ''; - return response.customError({ statusCode: 502, body: errorMessage }); - } + // Default response + let body: IErrorResponse = { + message: statusText, + attributes: { errors: [statusText] }, }; + + try { + if (contentType.includes('application/json')) { + // Try parsing body as JSON + const json = await apiResponse.json(); + + // Some of our internal endpoints return either an `error` or `errors` key, + // which can both return either a string or array of strings ¯\_(ツ)_/¯ + const errors = json.error || json.errors || [statusText]; + body = { + message: errors.toString(), + attributes: { errors: Array.isArray(errors) ? errors : [errors] }, + }; + } else { + // Try parsing body as text/html + const text = await apiResponse.text(); + if (text) { + body = { + message: text, + attributes: { errors: [text] }, + }; + } + } + } catch { + // Fail silently + } + + return body; + } + + /** + * Error response helpers + */ + + async handleClientError(response: KibanaResponseFactory, apiResponse: Response) { + const { status } = apiResponse; + const body = await this.getErrorResponseBody(apiResponse); + + return response.customError({ statusCode: status, body }); + } + + async handleServerError(response: KibanaResponseFactory, apiResponse: Response, url: string) { + const { status } = apiResponse; + const { message } = await this.getErrorResponseBody(apiResponse); + + // Don't expose server errors to the front-end, as they may contain sensitive stack traces + const errorMessage = + 'Enterprise Search encountered an internal server error. Please contact your system administrator if the problem persists.'; + + this.log.error(`Enterprise Search Server Error ${status} at <${url}>: ${message}`); + return response.customError({ statusCode: 502, body: errorMessage }); + } + + handleInvalidDataError(response: KibanaResponseFactory, url: string, json: object) { + const errorMessage = 'Invalid data received from Enterprise Search'; + + this.log.error(`Invalid data received from <${url}>: ${JSON.stringify(json)}`); + return response.customError({ statusCode: 502, body: errorMessage }); } - // Small helper + handleConnectionError(response: KibanaResponseFactory, e: Error) { + const errorMessage = `Error connecting to Enterprise Search: ${e?.message || e.toString()}`; + + this.log.error(errorMessage); + if (e instanceof Error) this.log.debug(e.stack as string); + + return response.customError({ statusCode: 502, body: errorMessage }); + } + + handleAuthenticationError(response: KibanaResponseFactory) { + const errorMessage = 'Cannot authenticate Enterprise Search user'; + + this.log.error(errorMessage); + return response.customError({ statusCode: 502, body: errorMessage }); + } + + /** + * Misc helpers + */ + isEmptyObj(obj: object) { return Object.keys(obj).length === 0; } diff --git a/x-pack/plugins/features/server/plugin.ts b/x-pack/plugins/features/server/plugin.ts index 8a799887bba09..857bba4c606d4 100644 --- a/x-pack/plugins/features/server/plugin.ts +++ b/x-pack/plugins/features/server/plugin.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import { RecursiveReadonly } from '@kbn/utility-types'; +import { deepFreeze } from '@kbn/std'; import { CoreSetup, CoreStart, @@ -12,7 +13,6 @@ import { PluginInitializerContext, } from '../../../../src/core/server'; import { Capabilities as UICapabilities } from '../../../../src/core/server'; -import { deepFreeze } from '../../../../src/core/server'; import { PluginSetupContract as TimelionSetupContract } from '../../../../src/plugins/vis_type_timelion/server'; import { FeatureRegistry } from './feature_registry'; import { uiCapabilitiesForFeatures } from './ui_capabilities_for_features'; diff --git a/x-pack/plugins/global_search_bar/README.md b/x-pack/plugins/global_search_bar/README.md new file mode 100644 index 0000000000000..e16aac39e3f4e --- /dev/null +++ b/x-pack/plugins/global_search_bar/README.md @@ -0,0 +1,3 @@ +# Kibana GlobalSearchBar plugin + +The GlobalSearchBar plugin provides a search interface for navigating Kibana. (It is the UI to the GlobalSearch plugin.) diff --git a/x-pack/plugins/global_search_bar/kibana.json b/x-pack/plugins/global_search_bar/kibana.json new file mode 100644 index 0000000000000..2d4ffa34d346f --- /dev/null +++ b/x-pack/plugins/global_search_bar/kibana.json @@ -0,0 +1,10 @@ +{ + "id": "globalSearchBar", + "version": "8.0.0", + "kibanaVersion": "kibana", + "server": false, + "ui": true, + "requiredPlugins": ["globalSearch"], + "optionalPlugins": [], + "configPath": ["xpack", "global_search_bar"] +} diff --git a/x-pack/plugins/global_search_bar/public/components/__snapshots__/search_bar.test.tsx.snap b/x-pack/plugins/global_search_bar/public/components/__snapshots__/search_bar.test.tsx.snap new file mode 100644 index 0000000000000..f7e4bfd1c961c --- /dev/null +++ b/x-pack/plugins/global_search_bar/public/components/__snapshots__/search_bar.test.tsx.snap @@ -0,0 +1,75 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`SearchBar correctly filters and sorts results 1`] = ` +Array [ + Object { + "append": undefined, + "className": "euiSelectableTemplateSitewide__listItem", + "key": "Canvas", + "label": "Canvas", + "prepend": undefined, + "title": "Canvasundefinedundefined", + "url": "/app/test/Canvas", + }, + Object { + "append": undefined, + "className": "euiSelectableTemplateSitewide__listItem", + "key": "Discover", + "label": "Discover", + "prepend": undefined, + "title": "Discoverundefinedundefined", + "url": "/app/test/Discover", + }, + Object { + "append": undefined, + "className": "euiSelectableTemplateSitewide__listItem", + "key": "Graph", + "label": "Graph", + "prepend": undefined, + "title": "Graphundefinedundefined", + "url": "/app/test/Graph", + }, +] +`; + +exports[`SearchBar correctly filters and sorts results 2`] = ` +Array [ + Object { + "append": undefined, + "className": "euiSelectableTemplateSitewide__listItem", + "key": "Discover", + "label": "Discover", + "prepend": undefined, + "title": "Discoverundefinedundefined", + "url": "/app/test/Discover", + }, + Object { + "append": undefined, + "className": "euiSelectableTemplateSitewide__listItem", + "key": "My Dashboard", + "label": "My Dashboard", + "meta": Array [ + Object { + "text": "Test", + }, + ], + "prepend": undefined, + "title": "My Dashboard • Test", + "url": "/app/test/My Dashboard", + }, +] +`; + +exports[`SearchBar supports keyboard shortcuts 1`] = ` + +`; diff --git a/x-pack/plugins/global_search_bar/public/components/search_bar.test.tsx b/x-pack/plugins/global_search_bar/public/components/search_bar.test.tsx new file mode 100644 index 0000000000000..0d1e8725b4911 --- /dev/null +++ b/x-pack/plugins/global_search_bar/public/components/search_bar.test.tsx @@ -0,0 +1,97 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { wait } from '@testing-library/react'; +import { of } from 'rxjs'; +import { mountWithIntl } from 'test_utils/enzyme_helpers'; +import { + GlobalSearchBatchedResults, + GlobalSearchPluginStart, + GlobalSearchResult, +} from '../../../global_search/public'; +import { globalSearchPluginMock } from '../../../global_search/public/mocks'; +import { SearchBar } from '../components/search_bar'; + +type Result = { id: string; type: string } | string; + +const createResult = (result: Result): GlobalSearchResult => { + const id = typeof result === 'string' ? result : result.id; + const type = typeof result === 'string' ? 'application' : result.type; + + return { + id, + type, + title: id, + url: `/app/test/${id}`, + score: 42, + }; +}; + +const createBatch = (...results: Result[]): GlobalSearchBatchedResults => ({ + results: results.map(createResult), +}); + +jest.mock('@elastic/eui/lib/services/accessibility/html_id_generator', () => ({ + htmlIdGenerator: () => () => 'mockId', +})); + +const getSelectableProps: any = (component: any) => component.find('EuiSelectable').props(); +const getSearchProps: any = (component: any) => component.find('EuiFieldSearch').props(); + +describe('SearchBar', () => { + let searchService: GlobalSearchPluginStart; + let findSpy: jest.SpyInstance; + + beforeEach(() => { + searchService = globalSearchPluginMock.createStartContract(); + findSpy = jest.spyOn(searchService, 'find'); + jest.useFakeTimers(); + }); + + it('correctly filters and sorts results', async () => { + const navigate = jest.fn(); + findSpy + .mockReturnValueOnce( + of( + createBatch('Discover', 'Canvas'), + createBatch({ id: 'Visualize', type: 'test' }, 'Graph') + ) + ) + .mockReturnValueOnce(of(createBatch('Discover', { id: 'My Dashboard', type: 'test' }))); + + const component = mountWithIntl( + + ); + + expect(findSpy).toHaveBeenCalledTimes(0); + component.find('input[data-test-subj="header-search"]').simulate('focus'); + jest.runAllTimers(); + component.update(); + expect(findSpy).toHaveBeenCalledTimes(1); + expect(findSpy).toHaveBeenCalledWith('', {}); + expect(getSelectableProps(component).options).toMatchSnapshot(); + await wait(() => getSearchProps(component).onSearch('d')); + jest.runAllTimers(); + component.update(); + expect(getSelectableProps(component).options).toMatchSnapshot(); + expect(findSpy).toHaveBeenCalledTimes(2); + expect(findSpy).toHaveBeenCalledWith('d', {}); + }); + + it('supports keyboard shortcuts', () => { + mountWithIntl(); + + const searchEvent = new KeyboardEvent('keydown', { + key: '/', + ctrlKey: true, + metaKey: true, + } as any); + window.dispatchEvent(searchEvent); + + expect(document.activeElement).toMatchSnapshot(); + }); +}); diff --git a/x-pack/plugins/global_search_bar/public/components/search_bar.tsx b/x-pack/plugins/global_search_bar/public/components/search_bar.tsx new file mode 100644 index 0000000000000..d00349e21a7e4 --- /dev/null +++ b/x-pack/plugins/global_search_bar/public/components/search_bar.tsx @@ -0,0 +1,205 @@ +/* + * 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 { + EuiBadge, + EuiFlexGroup, + EuiFlexItem, + EuiSelectableTemplateSitewide, + EuiSelectableTemplateSitewideOption, + EuiText, + EuiSelectableMessage, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { ApplicationStart } from 'kibana/public'; +import React, { useCallback, useState } from 'react'; +import useDebounce from 'react-use/lib/useDebounce'; +import useEvent from 'react-use/lib/useEvent'; +import useMountedState from 'react-use/lib/useMountedState'; +import { GlobalSearchPluginStart, GlobalSearchResult } from '../../../global_search/public'; + +interface Props { + globalSearch: GlobalSearchPluginStart['find']; + navigateToUrl: ApplicationStart['navigateToUrl']; +} + +const clearField = (field: HTMLInputElement) => { + const nativeInputValue = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value'); + const nativeInputValueSetter = nativeInputValue ? nativeInputValue.set : undefined; + if (nativeInputValueSetter) { + nativeInputValueSetter.call(field, ''); + } + + field.dispatchEvent(new Event('change')); +}; + +const cleanMeta = (str: string) => (str.charAt(0).toUpperCase() + str.slice(1)).replace(/-/g, ' '); +const blurEvent = new FocusEvent('blur'); + +export function SearchBar({ globalSearch, navigateToUrl }: Props) { + const isMounted = useMountedState(); + const [searchValue, setSearchValue] = useState(''); + const [searchRef, setSearchRef] = useState(null); + const [options, _setOptions] = useState([] as EuiSelectableTemplateSitewideOption[]); + const isMac = navigator.platform.toLowerCase().indexOf('mac') >= 0; + + const setOptions = useCallback( + (_options: GlobalSearchResult[]) => { + if (!isMounted()) return; + + _setOptions([ + ..._options.map((option) => ({ + key: option.id, + label: option.title, + url: option.url, + ...(option.icon && { icon: { type: option.icon } }), + ...(option.type && + option.type !== 'application' && { meta: [{ text: cleanMeta(option.type) }] }), + })), + ]); + }, + [isMounted, _setOptions] + ); + + useDebounce( + () => { + let arr: GlobalSearchResult[] = []; + globalSearch(searchValue, {}).subscribe({ + next: ({ results }) => { + if (searchValue.length > 0) { + arr = [...results, ...arr].sort((a, b) => { + if (a.score < b.score) return 1; + if (a.score > b.score) return -1; + return 0; + }); + setOptions(arr); + return; + } + + // if searchbar is empty, filter to only applications and sort alphabetically + results = results.filter(({ type }: GlobalSearchResult) => type === 'application'); + + arr = [...results, ...arr].sort((a, b) => { + const titleA = a.title.toUpperCase(); // ignore upper and lowercase + const titleB = b.title.toUpperCase(); // ignore upper and lowercase + if (titleA < titleB) return -1; + if (titleA > titleB) return 1; + return 0; + }); + + setOptions(arr); + }, + error: () => { + // TODO #74430 - add telemetry to see if errors are happening + // Not doing anything on error right now because it'll either just show the previous + // results or empty results which is basically what we want anyways + }, + complete: () => {}, + }); + }, + 250, + [searchValue] + ); + + const onKeyDown = (event: KeyboardEvent) => { + if (event.key === '/' && (isMac ? event.metaKey : event.ctrlKey)) { + if (searchRef) { + event.preventDefault(); + searchRef.focus(); + } + } + }; + + const onChange = (selected: EuiSelectableTemplateSitewideOption[]) => { + // @ts-ignore - ts error is "union type is too complex to express" + const { url } = selected.find(({ checked }) => checked === 'on'); + + navigateToUrl(url); + (document.activeElement as HTMLElement).blur(); + if (searchRef) { + clearField(searchRef); + searchRef.dispatchEvent(blurEvent); + } + }; + + useEvent('keydown', onKeyDown); + + return ( + +

+ +

+

+ +

+ + } + popoverFooter={ + + + + + + ), + commandDescription: ( + + + {isMac ? ( + + ) : ( + + )} + + + ), + }} + /> + + + } + /> + ); +} diff --git a/x-pack/plugins/global_search_bar/public/index.ts b/x-pack/plugins/global_search_bar/public/index.ts new file mode 100644 index 0000000000000..9e11c43ce872c --- /dev/null +++ b/x-pack/plugins/global_search_bar/public/index.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { PluginInitializer } from 'src/core/public'; +import { GlobalSearchBarPlugin } from './plugin'; + +export const plugin: PluginInitializer<{}, {}, {}, {}> = () => new GlobalSearchBarPlugin(); diff --git a/x-pack/plugins/global_search_bar/public/plugin.tsx b/x-pack/plugins/global_search_bar/public/plugin.tsx new file mode 100644 index 0000000000000..0c8cc2e64726a --- /dev/null +++ b/x-pack/plugins/global_search_bar/public/plugin.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 { CoreStart, Plugin } from 'src/core/public'; +import React from 'react'; +import { I18nProvider } from '@kbn/i18n/react'; +import ReactDOM from 'react-dom'; +import { ApplicationStart } from 'kibana/public'; +import { SearchBar } from '../public/components/search_bar'; +import { GlobalSearchPluginStart } from '../../global_search/public'; + +export interface GlobalSearchBarPluginStartDeps { + globalSearch: GlobalSearchPluginStart; +} + +export class GlobalSearchBarPlugin implements Plugin<{}, {}> { + public async setup() { + return {}; + } + + public start(core: CoreStart, { globalSearch }: GlobalSearchBarPluginStartDeps) { + core.chrome.navControls.registerCenter({ + order: 1000, + mount: (target) => this.mount(target, globalSearch, core.application.navigateToUrl), + }); + return {}; + } + + private mount( + targetDomElement: HTMLElement, + globalSearch: GlobalSearchPluginStart, + navigateToUrl: ApplicationStart['navigateToUrl'] + ) { + ReactDOM.render( + + + , + targetDomElement + ); + + return () => ReactDOM.unmountComponentAtNode(targetDomElement); + } +} diff --git a/x-pack/plugins/graph/public/angular/templates/index.html b/x-pack/plugins/graph/public/angular/templates/index.html index 50385008d7b2b..10bbb2e8ec6c7 100644 --- a/x-pack/plugins/graph/public/angular/templates/index.html +++ b/x-pack/plugins/graph/public/angular/templates/index.html @@ -1,6 +1,6 @@
- +
diff --git a/x-pack/plugins/graph/public/app.js b/x-pack/plugins/graph/public/app.js index fd2b96e0570f6..183f8bddead11 100644 --- a/x-pack/plugins/graph/public/app.js +++ b/x-pack/plugins/graph/public/app.js @@ -53,6 +53,7 @@ export function initGraphApp(angularModule, deps) { graphSavePolicy, overlays, savedObjects, + setHeaderActionMenu, } = deps; const app = angularModule; @@ -465,6 +466,7 @@ export function initGraphApp(angularModule, deps) { }; // ===== Menubar configuration ========= + $scope.setHeaderActionMenu = setHeaderActionMenu; $scope.topNavMenu = []; $scope.topNavMenu.push({ key: 'new', diff --git a/x-pack/plugins/graph/public/application.ts b/x-pack/plugins/graph/public/application.ts index a9ba464016157..90e87ff4ec85e 100644 --- a/x-pack/plugins/graph/public/application.ts +++ b/x-pack/plugins/graph/public/application.ts @@ -27,6 +27,7 @@ import { SavedObjectsClientContract, ToastsStart, OverlayStart, + AppMountParameters, } from 'kibana/public'; // @ts-ignore import { initGraphApp } from './app'; @@ -73,6 +74,7 @@ export interface GraphDependencies { overlays: OverlayStart; savedObjects: SavedObjectsStart; kibanaLegacy: KibanaLegacyStart; + setHeaderActionMenu: AppMountParameters['setHeaderActionMenu']; } export const renderApp = ({ appBasePath, element, kibanaLegacy, ...deps }: GraphDependencies) => { diff --git a/x-pack/plugins/graph/public/components/_app.scss b/x-pack/plugins/graph/public/components/_app.scss index f6984f5369a30..b9b23e596a05e 100644 --- a/x-pack/plugins/graph/public/components/_app.scss +++ b/x-pack/plugins/graph/public/components/_app.scss @@ -1,3 +1,3 @@ .gphGraph__bar { - margin: 0px $euiSizeS $euiSizeS $euiSizeS; + margin: $euiSizeS; } diff --git a/x-pack/plugins/graph/public/plugin.ts b/x-pack/plugins/graph/public/plugin.ts index e452b74ab35e8..6cf9d8be07bc9 100644 --- a/x-pack/plugins/graph/public/plugin.ts +++ b/x-pack/plugins/graph/public/plugin.ts @@ -74,7 +74,7 @@ export class GraphPlugin title: 'Graph', order: 6000, appRoute: '/app/graph', - euiIconType: 'graphApp', + euiIconType: 'logoKibana', category: DEFAULT_APP_CATEGORIES.kibana, mount: async (params: AppMountParameters) => { const [coreStart, pluginsStart] = await core.getStartServices(); diff --git a/x-pack/plugins/graph/public/state_management/url_templates.ts b/x-pack/plugins/graph/public/state_management/url_templates.ts index 19de52d444209..9149a27a4f986 100644 --- a/x-pack/plugins/graph/public/state_management/url_templates.ts +++ b/x-pack/plugins/graph/public/state_management/url_templates.ts @@ -7,6 +7,7 @@ import actionCreatorFactory from 'typescript-fsa'; import { reducerWithInitialState } from 'typescript-fsa-reducers/dist'; import { i18n } from '@kbn/i18n'; +import { modifyUrl } from '@kbn/std'; import rison from 'rison-node'; import { takeEvery, select } from 'redux-saga/effects'; import { format, parse } from 'url'; @@ -17,7 +18,6 @@ import { setDatasource, IndexpatternDatasource, requestDatasource } from './data import { outlinkEncoders } from '../helpers/outlink_encoders'; import { urlTemplatePlaceholder } from '../helpers/url_template'; import { matchesOne } from './helpers'; -import { modifyUrl } from '../../../../../src/core/public'; const actionCreator = actionCreatorFactory('x-pack/graph/urlTemplates'); diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/components/edit_policy.test.tsx b/x-pack/plugins/index_lifecycle_management/__jest__/components/edit_policy.test.tsx index 28b25c3eb4530..3867c30655379 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/components/edit_policy.test.tsx +++ b/x-pack/plugins/index_lifecycle_management/__jest__/components/edit_policy.test.tsx @@ -275,6 +275,40 @@ describe('edit policy', () => { save(rendered); expectedErrorMessages(rendered, [positiveNumbersAboveZeroErrorMessage]); }); + test('should show forcemerge input when rollover enabled', () => { + const rendered = mountWithIntl(component); + setPolicyName(rendered, 'mypolicy'); + expect(findTestSubject(rendered, 'hot-forceMergeSwitch').exists()).toBeTruthy(); + }); + test('should hide forcemerge input when rollover is disabled', () => { + const rendered = mountWithIntl(component); + setPolicyName(rendered, 'mypolicy'); + noRollover(rendered); + rendered.update(); + expect(findTestSubject(rendered, 'hot-forceMergeSwitch').exists()).toBeFalsy(); + }); + test('should show positive number required above zero error when trying to save hot phase with 0 for force merge', async () => { + const rendered = mountWithIntl(component); + setPolicyName(rendered, 'mypolicy'); + findTestSubject(rendered, 'hot-forceMergeSwitch').simulate('click'); + rendered.update(); + const forcemergeInput = findTestSubject(rendered, 'hot-selectedForceMergeSegments'); + forcemergeInput.simulate('change', { target: { value: '0' } }); + rendered.update(); + save(rendered); + expectedErrorMessages(rendered, [positiveNumbersAboveZeroErrorMessage]); + }); + test('should show positive number above 0 required error when trying to save hot phase with -1 for force merge', async () => { + const rendered = mountWithIntl(component); + setPolicyName(rendered, 'mypolicy'); + findTestSubject(rendered, 'hot-forceMergeSwitch').simulate('click'); + rendered.update(); + const forcemergeInput = findTestSubject(rendered, 'hot-selectedForceMergeSegments'); + forcemergeInput.simulate('change', { target: { value: '-1' } }); + rendered.update(); + save(rendered); + expectedErrorMessages(rendered, [positiveNumbersAboveZeroErrorMessage]); + }); test('should show positive number required error when trying to save with -1 for index priority', () => { const rendered = mountWithIntl(component); noRollover(rendered); @@ -364,10 +398,10 @@ describe('edit policy', () => { setPolicyName(rendered, 'mypolicy'); await activatePhase(rendered, 'warm'); setPhaseAfter(rendered, 'warm', '1'); - findTestSubject(rendered, 'forceMergeSwitch').simulate('click'); + findTestSubject(rendered, 'warm-forceMergeSwitch').simulate('click'); rendered.update(); - const shrinkInput = rendered.find('input#warm-selectedForceMergeSegments'); - shrinkInput.simulate('change', { target: { value: '0' } }); + const forcemergeInput = findTestSubject(rendered, 'warm-selectedForceMergeSegments'); + forcemergeInput.simulate('change', { target: { value: '0' } }); rendered.update(); save(rendered); expectedErrorMessages(rendered, [positiveNumbersAboveZeroErrorMessage]); @@ -378,10 +412,10 @@ describe('edit policy', () => { setPolicyName(rendered, 'mypolicy'); await activatePhase(rendered, 'warm'); setPhaseAfter(rendered, 'warm', '1'); - findTestSubject(rendered, 'forceMergeSwitch').simulate('click'); + findTestSubject(rendered, 'warm-forceMergeSwitch').simulate('click'); rendered.update(); - const shrinkInput = rendered.find('input#warm-selectedForceMergeSegments'); - shrinkInput.simulate('change', { target: { value: '-1' } }); + const forcemergeInput = findTestSubject(rendered, 'warm-selectedForceMergeSegments'); + forcemergeInput.simulate('change', { target: { value: '-1' } }); rendered.update(); save(rendered); expectedErrorMessages(rendered, [positiveNumbersAboveZeroErrorMessage]); diff --git a/x-pack/plugins/index_lifecycle_management/common/types/policies.ts b/x-pack/plugins/index_lifecycle_management/common/types/policies.ts index d88d5b5021a25..97effee44533a 100644 --- a/x-pack/plugins/index_lifecycle_management/common/types/policies.ts +++ b/x-pack/plugins/index_lifecycle_management/common/types/policies.ts @@ -41,6 +41,9 @@ export interface SerializedHotPhase extends SerializedPhase { max_age?: string; max_docs?: number; }; + forcemerge?: { + max_num_segments: number; + }; set_priority?: { priority: number | null; }; @@ -131,7 +134,15 @@ export interface PhaseWithIndexPriority { phaseIndexPriority: string; } -export interface HotPhase extends CommonPhaseSettings, PhaseWithIndexPriority { +export interface PhaseWithForcemergeAction { + forceMergeEnabled: boolean; + selectedForceMergeSegments: string; +} + +export interface HotPhase + extends CommonPhaseSettings, + PhaseWithIndexPriority, + PhaseWithForcemergeAction { rolloverEnabled: boolean; selectedMaxSizeStored: string; selectedMaxSizeStoredUnits: string; @@ -144,12 +155,11 @@ export interface WarmPhase extends CommonPhaseSettings, PhaseWithMinAge, PhaseWithAllocationAction, - PhaseWithIndexPriority { + PhaseWithIndexPriority, + PhaseWithForcemergeAction { warmPhaseOnRollover: boolean; shrinkEnabled: boolean; selectedPrimaryShardCount: string; - forceMergeEnabled: boolean; - selectedForceMergeSegments: string; } export interface ColdPhase diff --git a/x-pack/plugins/index_lifecycle_management/public/application/constants/policy.ts b/x-pack/plugins/index_lifecycle_management/public/application/constants/policy.ts index 4fd74da06f1b3..f11860d36faf8 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/constants/policy.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/constants/policy.ts @@ -20,6 +20,8 @@ export const defaultNewHotPhase: HotPhase = { selectedMaxAgeUnits: 'd', selectedMaxSizeStored: '50', selectedMaxSizeStoredUnits: 'gb', + forceMergeEnabled: false, + selectedForceMergeSegments: '', phaseIndexPriority: '100', selectedMaxDocuments: '', }; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/forcemerge.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/forcemerge.tsx new file mode 100644 index 0000000000000..50cc7fd8b3274 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/forcemerge.tsx @@ -0,0 +1,97 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { FormattedMessage } from '@kbn/i18n/react'; +import { + EuiDescribedFormGroup, + EuiFieldNumber, + EuiSpacer, + EuiSwitch, + EuiTextColor, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import React from 'react'; +import { LearnMoreLink } from './learn_more_link'; +import { ErrableFormRow } from './form_errors'; +import { Phases, PhaseWithForcemergeAction } from '../../../../../common/types'; +import { PhaseValidationErrors } from '../../../services/policies/policy_validation'; + +const forcemergeLabel = i18n.translate('xpack.indexLifecycleMgmt.warmPhase.forceMergeDataLabel', { + defaultMessage: 'Force merge data', +}); + +interface Props { + errors?: PhaseValidationErrors; + phase: keyof Phases & string; + phaseData: PhaseWithForcemergeAction; + setPhaseData: (dataKey: keyof PhaseWithForcemergeAction, value: boolean | string) => void; + isShowingErrors: boolean; +} +export const Forcemerge: React.FunctionComponent = ({ + errors, + phaseData, + phase, + setPhaseData, + isShowingErrors, +}) => { + return ( + + + + } + description={ + + {' '} + + + } + titleSize="xs" + fullWidth + > + { + setPhaseData('forceMergeEnabled', e.target.checked); + }} + aria-controls="forcemergeContent" + /> + + +
+ {phaseData.forceMergeEnabled ? ( + + { + setPhaseData('selectedForceMergeSegments', e.target.value); + }} + min={1} + /> + + ) : null} +
+
+ ); +}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/index.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/index.ts index e933c46e98491..4410c4bb38397 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/index.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/index.ts @@ -15,3 +15,4 @@ export { PhaseErrorMessage } from './phase_error_message'; export { PolicyJsonFlyout } from './policy_json_flyout'; export { SetPriorityInput } from './set_priority_input'; export { SnapshotPolicies } from './snapshot_policies'; +export { Forcemerge } from './forcemerge'; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/hot_phase.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/hot_phase.tsx index 59949ad93fa5d..7682b92488086 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/hot_phase.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/hot_phase.tsx @@ -27,6 +27,7 @@ import { PhaseErrorMessage, ErrableFormRow, SetPriorityInput, + Forcemerge, } from '../components'; const maxSizeStoredUnits = [ @@ -313,6 +314,15 @@ export class HotPhase extends PureComponent { ) : null} + {phaseData.rolloverEnabled ? ( + + ) : null} errors={errors} phaseData={phaseData} diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/warm_phase.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/warm_phase.tsx index 71286475bcfe9..c806056899cac 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/warm_phase.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/warm_phase.tsx @@ -29,6 +29,7 @@ import { SetPriorityInput, NodeAllocation, MinAgeInput, + Forcemerge, } from '../components'; const shrinkLabel = i18n.translate('xpack.indexLifecycleMgmt.warmPhase.shrinkIndexLabel', { @@ -42,10 +43,6 @@ const moveToWarmPhaseOnRolloverLabel = i18n.translate( } ); -const forcemergeLabel = i18n.translate('xpack.indexLifecycleMgmt.warmPhase.forceMergeDataLabel', { - defaultMessage: 'Force merge data', -}); - const warmProperty: keyof Phases = 'warm'; const phaseProperty = (propertyName: keyof WarmPhaseInterface) => propertyName; @@ -262,64 +259,13 @@ export class WarmPhase extends PureComponent {
- - - - } - description={ - - {' '} - - - } - titleSize="xs" - fullWidth - > - { - setPhaseData(phaseProperty('forceMergeEnabled'), e.target.checked); - }} - aria-controls="forcemergeContent" - /> - - -
- {phaseData.forceMergeEnabled ? ( - - { - setPhaseData(phaseProperty('selectedForceMergeSegments'), e.target.value); - }} - min={1} - /> - - ) : null} -
-
+ errors={errors} phaseData={phaseData} diff --git a/x-pack/plugins/index_lifecycle_management/public/application/services/policies/hot_phase.ts b/x-pack/plugins/index_lifecycle_management/public/application/services/policies/hot_phase.ts index fb7f74efeb66e..4ef4890b5ffe3 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/services/policies/hot_phase.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/services/policies/hot_phase.ts @@ -24,6 +24,8 @@ const hotPhaseInitialization: HotPhase = { selectedMaxAgeUnits: 'd', selectedMaxSizeStored: '', selectedMaxSizeStoredUnits: 'gb', + forceMergeEnabled: false, + selectedForceMergeSegments: '', phaseIndexPriority: '', selectedMaxDocuments: '', }; @@ -58,6 +60,12 @@ export const hotPhaseFromES = (phaseSerialized?: SerializedHotPhase): HotPhase = } } + if (actions.forcemerge) { + const forcemerge = actions.forcemerge; + phase.forceMergeEnabled = true; + phase.selectedForceMergeSegments = forcemerge.max_num_segments.toString(); + } + if (actions.set_priority) { phase.phaseIndexPriority = actions.set_priority.priority ? actions.set_priority.priority.toString() @@ -93,8 +101,19 @@ export const hotPhaseToES = ( if (isNumber(phase.selectedMaxDocuments)) { esPhase.actions.rollover.max_docs = parseInt(phase.selectedMaxDocuments, 10); } + if (phase.forceMergeEnabled && isNumber(phase.selectedForceMergeSegments)) { + esPhase.actions.forcemerge = { + max_num_segments: parseInt(phase.selectedForceMergeSegments, 10), + }; + } else { + delete esPhase.actions.forcemerge; + } } else { delete esPhase.actions.rollover; + // forcemerge is only allowed if rollover is enabled + if (esPhase.actions.forcemerge) { + delete esPhase.actions.forcemerge; + } } if (isNumber(phase.phaseIndexPriority)) { @@ -147,6 +166,15 @@ export const validateHotPhase = (phase: HotPhase): PhaseValidationErrors { ]); }; + const setLoadComponentTemplatesResponse = (response?: HttpResponse, error?: any) => { + const status = error ? error.status || 400 : 200; + const body = error ? error.body : response; + + server.respondWith('GET', `${API_BASE_PATH}/component_templates`, [ + status, + { 'Content-Type': 'application/json' }, + JSON.stringify(body), + ]); + }; + return { setLoadTemplatesResponse, setLoadIndicesResponse, @@ -114,6 +125,7 @@ const registerHttpRequestMockHelpers = (server: SinonFakeServer) => { setCreateTemplateResponse, setUpdateTemplateResponse, setSimulateTemplateResponse, + setLoadComponentTemplatesResponse, }; }; diff --git a/x-pack/plugins/index_management/__jest__/client_integration/helpers/setup_environment.tsx b/x-pack/plugins/index_management/__jest__/client_integration/helpers/setup_environment.tsx index 910d9be842da8..e221c3d421e02 100644 --- a/x-pack/plugins/index_management/__jest__/client_integration/helpers/setup_environment.tsx +++ b/x-pack/plugins/index_management/__jest__/client_integration/helpers/setup_environment.tsx @@ -24,7 +24,11 @@ import { ExtensionsService } from '../../../public/services'; import { UiMetricService } from '../../../public/application/services/ui_metric'; import { setUiMetricService } from '../../../public/application/services/api'; import { setExtensionsService } from '../../../public/application/store/selectors'; -import { MappingsEditorProvider } from '../../../public/application/components'; +import { + MappingsEditorProvider, + ComponentTemplatesProvider, +} from '../../../public/application/components'; +import { componentTemplatesMockDependencies } from '../../../public/application/components/component_templates/__jest__'; import { init as initHttpRequests } from './http_requests'; const mockHttpClient = axios.create({ adapter: axiosXhrAdapter }); @@ -34,9 +38,11 @@ export const services = { extensionsService: new ExtensionsService(), uiMetricService: new UiMetricService('index_management'), }; + services.uiMetricService.setup({ reportUiStats() {} } as any); setExtensionsService(services.extensionsService); setUiMetricService(services.uiMetricService); + const appDependencies = { services, core: { getUrlForApp: () => {} }, @@ -66,9 +72,11 @@ export const WithAppDependencies = (Comp: any, overridingDependencies: any = {}) return ( - - - + + + + + ); diff --git a/x-pack/plugins/index_management/__jest__/client_integration/index_template_wizard/constants.ts b/x-pack/plugins/index_management/__jest__/client_integration/index_template_wizard/constants.ts index 3f6e5d7d4dab2..ef5cffc05d8d7 100644 --- a/x-pack/plugins/index_management/__jest__/client_integration/index_template_wizard/constants.ts +++ b/x-pack/plugins/index_management/__jest__/client_integration/index_template_wizard/constants.ts @@ -26,7 +26,5 @@ export const ALIASES = { }; export const MAPPINGS = { - _source: {}, - _meta: {}, properties: {}, }; diff --git a/x-pack/plugins/index_management/__jest__/client_integration/index_template_wizard/template_clone.test.tsx b/x-pack/plugins/index_management/__jest__/client_integration/index_template_wizard/template_clone.test.tsx index ccb729db44f9b..8b74e9fb0cdf8 100644 --- a/x-pack/plugins/index_management/__jest__/client_integration/index_template_wizard/template_clone.test.tsx +++ b/x-pack/plugins/index_management/__jest__/client_integration/index_template_wizard/template_clone.test.tsx @@ -7,8 +7,8 @@ import React from 'react'; import { act } from 'react-dom/test-utils'; -import { getTemplate } from '../../../test/fixtures'; -import { setupEnvironment, nextTick } from '../helpers'; +import { getComposableTemplate } from '../../../test/fixtures'; +import { setupEnvironment } from '../helpers'; import { TEMPLATE_NAME, INDEX_PATTERNS as DEFAULT_INDEX_PATTERNS, MAPPINGS } from './constants'; import { setup } from './template_clone.helpers'; @@ -41,32 +41,35 @@ jest.mock('@elastic/eui', () => { }; }); -// FLAKY: https://github.com/elastic/kibana/issues/59849 -describe.skip('', () => { +const templateToClone = getComposableTemplate({ + name: TEMPLATE_NAME, + indexPatterns: ['indexPattern1'], + template: { + mappings: MAPPINGS, + }, +}); + +describe('', () => { let testBed: TemplateFormTestBed; const { server, httpRequestsMockHelpers } = setupEnvironment(); - afterAll(() => { - server.restore(); + beforeAll(() => { + jest.useFakeTimers(); + httpRequestsMockHelpers.setLoadComponentTemplatesResponse([]); + httpRequestsMockHelpers.setLoadTemplateResponse(templateToClone); }); - const templateToClone = getTemplate({ - name: TEMPLATE_NAME, - indexPatterns: ['indexPattern1'], - template: { - mappings: MAPPINGS, - }, - isLegacy: true, + afterAll(() => { + server.restore(); + jest.useRealTimers(); }); beforeEach(async () => { - httpRequestsMockHelpers.setLoadTemplateResponse(templateToClone); - await act(async () => { testBed = await setup(); - await testBed.waitFor('templateForm'); }); + testBed.component.update(); }); test('should set the correct page title', () => { @@ -80,30 +83,26 @@ describe.skip('', () => { beforeEach(async () => { const { actions } = testBed; - await act(async () => { - // Complete step 1 (logistics) - // Specify index patterns, but do not change name (keep default) - await actions.completeStepOne({ - indexPatterns: DEFAULT_INDEX_PATTERNS, - }); - - // Bypass step 2 (index settings) - await actions.completeStepTwo(); - - // Bypass step 3 (mappings) - await actions.completeStepThree(); - - // Bypass step 4 (aliases) - await actions.completeStepFour(); + // Logistics + // Specify index patterns, but do not change name (keep default) + await actions.completeStepOne({ + indexPatterns: DEFAULT_INDEX_PATTERNS, }); + // Component templates + await actions.completeStepTwo(); + // Index settings + await actions.completeStepThree(); + // Mappings + await actions.completeStepFour(); + // Aliases + await actions.completeStepFive(); }); it('should send the correct payload', async () => { const { actions } = testBed; await act(async () => { - actions.clickSubmitButton(); - await nextTick(); + actions.clickNextButton(); }); const latestRequest = server.requests[server.requests.length - 1]; @@ -113,6 +112,8 @@ describe.skip('', () => { name: `${templateToClone.name}-copy`, indexPatterns: DEFAULT_INDEX_PATTERNS, }; + // @ts-expect-error + delete expected.template; // As no settings, mappings or aliases have been defined, no "template" param is sent expect(JSON.parse(JSON.parse(latestRequest.requestBody).body)).toEqual(expected); }); diff --git a/x-pack/plugins/index_management/__jest__/client_integration/index_template_wizard/template_create.test.tsx b/x-pack/plugins/index_management/__jest__/client_integration/index_template_wizard/template_create.test.tsx index 76b6c34f999d5..b67e503f8d3e2 100644 --- a/x-pack/plugins/index_management/__jest__/client_integration/index_template_wizard/template_create.test.tsx +++ b/x-pack/plugins/index_management/__jest__/client_integration/index_template_wizard/template_create.test.tsx @@ -7,12 +7,11 @@ import React from 'react'; import { act } from 'react-dom/test-utils'; -import { setupEnvironment, nextTick } from '../helpers'; +import { setupEnvironment } from '../helpers'; import { TEMPLATE_NAME, SETTINGS, - MAPPINGS, ALIASES, INDEX_PATTERNS as DEFAULT_INDEX_PATTERNS, } from './constants'; @@ -61,21 +60,50 @@ const KEYWORD_MAPPING_FIELD = { type: 'keyword', }; -// FLAKY: https://github.com/elastic/kibana/issues/67833 -describe.skip('', () => { +const componentTemplate1 = { + name: 'test_component_template_1', + hasMappings: true, + hasAliases: false, + hasSettings: false, + usedBy: [], + isManaged: false, +}; + +const componentTemplate2 = { + name: 'test_component_template_2', + hasMappings: false, + hasAliases: false, + hasSettings: true, + usedBy: ['test_index_template_1'], + isManaged: false, +}; + +const componentTemplates = [componentTemplate1, componentTemplate2]; + +describe('', () => { let testBed: TemplateFormTestBed; const { server, httpRequestsMockHelpers } = setupEnvironment(); + beforeAll(() => { + jest.useFakeTimers(); + + httpRequestsMockHelpers.setLoadComponentTemplatesResponse(componentTemplates); + + // disable all react-beautiful-dnd development warnings + (window as any)['__react-beautiful-dnd-disable-dev-warnings'] = true; + }); + afterAll(() => { server.restore(); + jest.useRealTimers(); + (window as any)['__react-beautiful-dnd-disable-dev-warnings'] = false; }); describe('on component mount', () => { beforeEach(async () => { await act(async () => { testBed = await setup(); - await testBed.waitFor('templateForm'); }); }); @@ -93,9 +121,8 @@ describe.skip('', () => { await act(async () => { actions.clickNextButton(); - await nextTick(); - component.update(); }); + component.update(); expect(find('nextButton').props().disabled).toEqual(true); }); @@ -105,21 +132,131 @@ describe.skip('', () => { beforeEach(async () => { await act(async () => { testBed = await setup(); - await testBed.waitFor('templateForm'); }); + testBed.component.update(); }); - describe('index settings (step 2)', () => { + describe('component templates (step 2)', () => { beforeEach(async () => { const { actions } = testBed; + await actions.completeStepOne({ name: TEMPLATE_NAME, indexPatterns: ['index1'] }); + }); + + it('should set the correct page title', async () => { + const { exists, find } = testBed; + + expect(exists('stepComponents')).toBe(true); + expect(find('stepTitle').text()).toEqual('Component templates (optional)'); + }); + + it('should list the available component templates', () => { + const { + actions: { + componentTemplates: { getComponentTemplatesInList }, + }, + } = testBed; + const componentsFound = getComponentTemplatesInList(); + expect(componentsFound).toEqual(componentTemplates.map((c) => c.name)); + }); + + it('should allow to search for a component', async () => { + const { + component, + form: { setInputValue }, + actions: { + componentTemplates: { getComponentTemplatesInList }, + }, + } = testBed; await act(async () => { - // Complete step 1 (logistics) - await actions.completeStepOne({ name: TEMPLATE_NAME, indexPatterns: ['index1'] }); + setInputValue('componentTemplateSearchBox', 'template_2'); }); + component.update(); + + const componentsFound = getComponentTemplatesInList(); + expect(componentsFound).toEqual(['test_component_template_2']); }); - it('should set the correct page title', () => { + it('should allow to filter component by Index settings, mappings and aliases', async () => { + const { + find, + exists, + actions: { + componentTemplates: { showFilters, selectFilter, getComponentTemplatesInList }, + }, + } = testBed; + + showFilters(); + + expect(find('filterList.filterItem').map((wrapper) => wrapper.text())).toEqual([ + 'Index settings', + 'Mappings', + 'Aliases', + ]); + + await selectFilter('settings'); + expect(getComponentTemplatesInList()).toEqual(['test_component_template_2']); // only this one has settings + + await selectFilter('mappings'); + expect(exists('componentTemplatesList')).toBe(false); // no component has **both** settings and mappings + expect(exists('componentTemplates.emptySearchResult')).toBe(true); + expect(find('componentTemplates.emptySearchResult').text()).toContain( + 'No components match your search' + ); + + await selectFilter('settings'); // unselect settings + expect(getComponentTemplatesInList()).toEqual(['test_component_template_1']); // only this one has mappings + + await selectFilter('mappings'); // unselect mappings (back to start) + expect(getComponentTemplatesInList()).toEqual([ + 'test_component_template_1', + 'test_component_template_2', + ]); + + await selectFilter('aliases'); + expect(exists('componentTemplatesList')).toBe(false); // no component has aliases defined. + }); + + it('should allow to select and unselect a component template', async () => { + const { + find, + exists, + actions: { + componentTemplates: { + selectComponentAt, + unSelectComponentAt, + getComponentTemplatesSelected, + }, + }, + } = testBed; + + // Start with empty selection + expect(exists('componentTemplatesSelection.emptyPrompt')).toBe(true); + expect(find('componentTemplatesSelection.emptyPrompt').text()).toContain( + 'Add component template building blocks to this template.' + ); + + // Select first component in the list + await selectComponentAt(0); + expect(exists('componentTemplatesSelection.emptyPrompt')).toBe(false); + expect(getComponentTemplatesSelected()).toEqual(['test_component_template_1']); + + // Unselect the component + await unSelectComponentAt(0); + expect(exists('componentTemplatesSelection.emptyPrompt')).toBe(true); + }); + }); + + describe('index settings (step 3)', () => { + beforeEach(async () => { + const { actions } = testBed; + // Logistics + await actions.completeStepOne({ name: TEMPLATE_NAME, indexPatterns: ['index1'] }); + // Component templates + await actions.completeStepTwo(); + }); + + it('should set the correct page title', async () => { const { exists, find } = testBed; expect(exists('stepSettings')).toBe(true); @@ -130,24 +267,22 @@ describe.skip('', () => { const { form, actions } = testBed; await act(async () => { - actions.completeStepTwo('{ invalidJsonString '); + actions.completeStepThree('{ invalidJsonString '); }); expect(form.getErrorsMessages()).toContain('Invalid JSON format.'); }); }); - describe('mappings (step 3)', () => { + describe('mappings (step 4)', () => { beforeEach(async () => { const { actions } = testBed; - - await act(async () => { - // Complete step 1 (logistics) - await actions.completeStepOne({ name: TEMPLATE_NAME, indexPatterns: ['index1'] }); - - // Complete step 2 (index settings) - await actions.completeStepTwo('{}'); - }); + // Logistics + await actions.completeStepOne({ name: TEMPLATE_NAME, indexPatterns: ['index1'] }); + // Component templates + await actions.completeStepTwo(); + // Index settings + await actions.completeStepThree('{}'); }); it('should set the correct page title', () => { @@ -160,11 +295,9 @@ describe.skip('', () => { it('should allow the user to define document fields for a mapping', async () => { const { actions, find } = testBed; - await act(async () => { - await actions.addMappingField('field_1', 'text'); - await actions.addMappingField('field_2', 'text'); - await actions.addMappingField('field_3', 'text'); - }); + await actions.mappings.addField('field_1', 'text'); + await actions.mappings.addField('field_2', 'text'); + await actions.mappings.addField('field_3', 'text'); expect(find('fieldsListItem').length).toBe(3); }); @@ -172,10 +305,8 @@ describe.skip('', () => { it('should allow the user to remove a document field from a mapping', async () => { const { actions, find } = testBed; - await act(async () => { - await actions.addMappingField('field_1', 'text'); - await actions.addMappingField('field_2', 'text'); - }); + await actions.mappings.addField('field_1', 'text'); + await actions.mappings.addField('field_2', 'text'); expect(find('fieldsListItem').length).toBe(2); @@ -187,20 +318,17 @@ describe.skip('', () => { }); }); - describe('aliases (step 4)', () => { + describe('aliases (step 5)', () => { beforeEach(async () => { const { actions } = testBed; - - await act(async () => { - // Complete step 1 (logistics) - await actions.completeStepOne({ name: TEMPLATE_NAME, indexPatterns: ['index1'] }); - - // Complete step 2 (index settings) - await actions.completeStepTwo('{}'); - - // Complete step 3 (mappings) - await actions.completeStepThree(); - }); + // Logistics + await actions.completeStepOne({ name: TEMPLATE_NAME, indexPatterns: ['index1'] }); + // Component templates + await actions.completeStepTwo(); + // Index settings + await actions.completeStepThree('{}'); + // Mappings + await actions.completeStepFour(); }); it('should set the correct page title', () => { @@ -213,39 +341,35 @@ describe.skip('', () => { it('should not allow invalid json', async () => { const { actions, form } = testBed; - await act(async () => { - // Complete step 4 (aliases) with invalid json - await actions.completeStepFour('{ invalidJsonString ', false); - }); + // Complete step 5 (aliases) with invalid json + await actions.completeStepFive('{ invalidJsonString '); expect(form.getErrorsMessages()).toContain('Invalid JSON format.'); }); }); }); - describe('review (step 5)', () => { + describe('review (step 6)', () => { beforeEach(async () => { await act(async () => { testBed = await setup(); - await testBed.waitFor('templateForm'); - - const { actions } = testBed; - - // Complete step 1 (logistics) - await actions.completeStepOne({ - name: TEMPLATE_NAME, - indexPatterns: DEFAULT_INDEX_PATTERNS, - }); - - // Complete step 2 (index settings) - await actions.completeStepTwo(JSON.stringify(SETTINGS)); - - // Complete step 3 (mappings) - await actions.completeStepThree(); + }); + testBed.component.update(); - // Complete step 4 (aliases) - await actions.completeStepFour(JSON.stringify(ALIASES)); + const { actions } = testBed; + // Logistics + await actions.completeStepOne({ + name: TEMPLATE_NAME, + indexPatterns: DEFAULT_INDEX_PATTERNS, }); + // Component templates + await actions.completeStepTwo('test_component_template_1'); + // Index settings + await actions.completeStepThree(JSON.stringify(SETTINGS)); + // Mappings + await actions.completeStepFour(); + // Aliases + await actions.completeStepFive(JSON.stringify(ALIASES)); }); it('should set the correct step title', () => { @@ -255,26 +379,30 @@ describe.skip('', () => { }); describe('tabs', () => { - test('should have 2 tabs', () => { + test('should have 3 tabs', () => { const { find } = testBed; - expect(find('summaryTabContent').find('.euiTab').length).toBe(2); + expect(find('summaryTabContent').find('.euiTab').length).toBe(3); expect( find('summaryTabContent') .find('.euiTab') .map((t) => t.text()) - ).toEqual(['Summary', 'Request']); + ).toEqual(['Summary', 'Preview', 'Request']); }); - test('should navigate to the Request tab', async () => { + test('should navigate to the preview and request tab', async () => { const { exists, actions } = testBed; expect(exists('summaryTab')).toBe(true); expect(exists('requestTab')).toBe(false); + expect(exists('previewTab')).toBe(false); - actions.selectSummaryTab('request'); - + await actions.review.selectTab('preview'); expect(exists('summaryTab')).toBe(false); + expect(exists('previewTab')).toBe(true); + + await actions.review.selectTab('request'); + expect(exists('previewTab')).toBe(false); expect(exists('requestTab')).toBe(true); }); }); @@ -282,24 +410,23 @@ describe.skip('', () => { it('should render a warning message if a wildcard is used as an index pattern', async () => { await act(async () => { testBed = await setup(); - await testBed.waitFor('templateForm'); - - const { actions } = testBed; - // Complete step 1 (logistics) - await actions.completeStepOne({ - name: TEMPLATE_NAME, - indexPatterns: ['*'], // Set wildcard index pattern - }); - - // Complete step 2 (index settings) - await actions.completeStepTwo(JSON.stringify({})); - - // Complete step 3 (mappings) - await actions.completeStepThree(); + }); + testBed.component.update(); - // Complete step 4 (aliases) - await actions.completeStepFour(JSON.stringify({})); + const { actions } = testBed; + // Logistics + await actions.completeStepOne({ + name: TEMPLATE_NAME, + indexPatterns: ['*'], // Set wildcard index pattern }); + // Component templates + await actions.completeStepTwo(); + // Index settings + await actions.completeStepThree(JSON.stringify({})); + // Mappings + await actions.completeStepFour(); + // Aliases + await actions.completeStepFive(JSON.stringify({})); const { exists, find } = testBed; @@ -316,32 +443,32 @@ describe.skip('', () => { await act(async () => { testBed = await setup(); - await testBed.waitFor('templateForm'); - - const { actions } = testBed; - // Complete step 1 (logistics) - await actions.completeStepOne({ - name: TEMPLATE_NAME, - indexPatterns: DEFAULT_INDEX_PATTERNS, - }); - - // Complete step 2 (index settings) - await actions.completeStepTwo(JSON.stringify(SETTINGS)); - - // Complete step 3 (mappings) - await actions.completeStepThree(MAPPING_FIELDS); + }); + testBed.component.update(); - // Complete step 4 (aliases) - await actions.completeStepFour(JSON.stringify(ALIASES)); + const { actions } = testBed; + // Logistics + await actions.completeStepOne({ + name: TEMPLATE_NAME, + indexPatterns: DEFAULT_INDEX_PATTERNS, }); + // Component templates + await actions.completeStepTwo('test_component_template_1'); + // Index settings + await actions.completeStepThree(JSON.stringify(SETTINGS)); + // Mappings + await actions.completeStepFour(MAPPING_FIELDS); + // Aliases + await actions.completeStepFive(JSON.stringify(ALIASES)); }); it('should send the correct payload', async () => { - const { actions } = testBed; + const { actions, find } = testBed; + + expect(find('stepTitle').text()).toEqual(`Review details for '${TEMPLATE_NAME}'`); await act(async () => { - actions.clickSubmitButton(); - await nextTick(); + actions.clickNextButton(); }); const latestRequest = server.requests[server.requests.length - 1]; @@ -349,10 +476,10 @@ describe.skip('', () => { const expected = { name: TEMPLATE_NAME, indexPatterns: DEFAULT_INDEX_PATTERNS, + composedOf: ['test_component_template_1'], template: { settings: SETTINGS, mappings: { - ...MAPPINGS, properties: { [BOOLEAN_MAPPING_FIELD.name]: { type: BOOLEAN_MAPPING_FIELD.type, @@ -370,6 +497,7 @@ describe.skip('', () => { _kbnMeta: { type: 'default', isLegacy: false, + hasDatastream: false, }, }; @@ -388,10 +516,9 @@ describe.skip('', () => { httpRequestsMockHelpers.setCreateTemplateResponse(undefined, { body: error }); await act(async () => { - actions.clickSubmitButton(); - await nextTick(); - component.update(); + actions.clickNextButton(); }); + component.update(); expect(exists('saveTemplateError')).toBe(true); expect(find('saveTemplateError').text()).toContain(error.message); diff --git a/x-pack/plugins/index_management/__jest__/client_integration/index_template_wizard/template_edit.test.tsx b/x-pack/plugins/index_management/__jest__/client_integration/index_template_wizard/template_edit.test.tsx index de66013241236..37d489b6afe72 100644 --- a/x-pack/plugins/index_management/__jest__/client_integration/index_template_wizard/template_edit.test.tsx +++ b/x-pack/plugins/index_management/__jest__/client_integration/index_template_wizard/template_edit.test.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { act } from 'react-dom/test-utils'; import * as fixtures from '../../../test/fixtures'; -import { setupEnvironment, nextTick } from '../helpers'; +import { setupEnvironment } from '../helpers'; import { TEMPLATE_NAME, SETTINGS, ALIASES, MAPPINGS as DEFAULT_MAPPING } from './constants'; import { setup } from './template_edit.helpers'; @@ -52,51 +52,51 @@ jest.mock('@elastic/eui', () => { }; }); -// FLAKY: https://github.com/elastic/kibana/issues/65567 -describe.skip('', () => { +describe('', () => { let testBed: TemplateFormTestBed; const { server, httpRequestsMockHelpers } = setupEnvironment(); + beforeAll(() => { + jest.useFakeTimers(); + httpRequestsMockHelpers.setLoadComponentTemplatesResponse([]); + }); + afterAll(() => { server.restore(); + jest.useRealTimers(); }); describe('without mappings', () => { const templateToEdit = fixtures.getTemplate({ name: 'index_template_without_mappings', indexPatterns: ['indexPattern1'], - isLegacy: true, }); - beforeEach(async () => { + beforeAll(() => { httpRequestsMockHelpers.setLoadTemplateResponse(templateToEdit); + }); - testBed = await setup(); - + beforeEach(async () => { await act(async () => { - await nextTick(); - testBed.component.update(); + testBed = await setup(); }); + + testBed.component.update(); }); it('allows you to add mappings', async () => { const { actions, find } = testBed; - - await act(async () => { - // Complete step 1 (logistics) - await actions.completeStepOne(); - - // Step 2 (index settings) - await actions.completeStepTwo(); - - // Step 3 (mappings) - await act(async () => { - await actions.addMappingField('field_1', 'text'); - }); - - expect(find('fieldsListItem').length).toBe(1); - }); + // Logistics + await actions.completeStepOne(); + // Component templates + await actions.completeStepTwo(); + // Index settings + await actions.completeStepThree(); + // Mappings + await actions.mappings.addField('field_1', 'text'); + + expect(find('fieldsListItem').length).toBe(1); }); }); @@ -107,16 +107,17 @@ describe.skip('', () => { template: { mappings: MAPPING, }, - isLegacy: true, }); - beforeEach(async () => { + beforeAll(() => { httpRequestsMockHelpers.setLoadTemplateResponse(templateToEdit); + }); + beforeEach(async () => { await act(async () => { testBed = await setup(); - await testBed.waitFor('templateForm'); }); + testBed.component.update(); }); test('should set the correct page title', () => { @@ -138,64 +139,64 @@ describe.skip('', () => { beforeEach(async () => { const { actions } = testBed; - await act(async () => { - // Complete step 1 (logistics) - await actions.completeStepOne({ - indexPatterns: UPDATED_INDEX_PATTERN, - }); - - // Step 2 (index settings) - await actions.completeStepTwo(JSON.stringify(SETTINGS)); + // Logistics + await actions.completeStepOne({ + indexPatterns: UPDATED_INDEX_PATTERN, + priority: 3, }); + // Component templates + await actions.completeStepTwo(); + // Index settings + await actions.completeStepThree(JSON.stringify(SETTINGS)); }); it('should send the correct payload with changed values', async () => { - const { actions, component, find, form } = testBed; + const { actions, component, exists, form } = testBed; + // Make some changes to the mappings await act(async () => { - // Make some changes to the mappings (step 3) - actions.clickEditButtonAtField(0); // Select the first field to edit - await nextTick(); - component.update(); }); + component.update(); - // verify edit field flyout - expect(find('mappingsEditorFieldEdit').length).toEqual(1); + // Verify that the edit field flyout is opened + expect(exists('mappingsEditorFieldEdit')).toBe(true); + // Change the field name await act(async () => { - // change the field name form.setInputValue('nameParameterInput', UPDATED_MAPPING_TEXT_FIELD_NAME); + }); - // Save changes + // Save changes on the field + await act(async () => { actions.clickEditFieldUpdateButton(); - await nextTick(); - component.update(); + }); + component.update(); - // Proceed to the next step + // Proceed to the next step + await act(async () => { actions.clickNextButton(); - await nextTick(50); - component.update(); + }); + component.update(); - // Step 4 (aliases) - await actions.completeStepFour(JSON.stringify(ALIASES)); + // Aliases + await actions.completeStepFive(JSON.stringify(ALIASES)); - // Submit the form - actions.clickSubmitButton(); - await nextTick(); + // Submit the form + await act(async () => { + actions.clickNextButton(); }); const latestRequest = server.requests[server.requests.length - 1]; - const { version, order } = templateToEdit; + const { version } = templateToEdit; const expected = { name: TEMPLATE_NAME, version, - order, + priority: 3, indexPatterns: UPDATED_INDEX_PATTERN, template: { mappings: { - ...MAPPING, properties: { [UPDATED_MAPPING_TEXT_FIELD_NAME]: { type: 'text', @@ -215,6 +216,7 @@ describe.skip('', () => { _kbnMeta: { type: 'default', isLegacy: templateToEdit._kbnMeta.isLegacy, + hasDatastream: false, }, }; diff --git a/x-pack/plugins/index_management/__jest__/client_integration/index_template_wizard/template_form.helpers.ts b/x-pack/plugins/index_management/__jest__/client_integration/index_template_wizard/template_form.helpers.ts index fdf837a914cf1..025410129a002 100644 --- a/x-pack/plugins/index_management/__jest__/client_integration/index_template_wizard/template_form.helpers.ts +++ b/x-pack/plugins/index_management/__jest__/client_integration/index_template_wizard/template_form.helpers.ts @@ -3,10 +3,10 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ +import { act } from 'react-dom/test-utils'; import { TestBed, SetupFunc, UnwrapPromise } from '../../../../../test_utils'; import { TemplateDeserialized } from '../../../common'; -import { nextTick } from '../helpers'; interface MappingField { name: string; @@ -30,10 +30,6 @@ export const formSetup = async (initTestBed: SetupFunc) => { testBed.find('backButton').simulate('click'); }; - const clickSubmitButton = () => { - testBed.find('submitButton').simulate('click'); - }; - const clickEditButtonAtField = (index: number) => { testBed.find('editFieldButton').at(index).simulate('click'); }; @@ -52,16 +48,100 @@ export const formSetup = async (initTestBed: SetupFunc) => { testBed.find('createFieldForm.cancelButton').simulate('click'); }; + // Step component templates actions + const componentTemplates = { + getComponentTemplatesInList() { + const { find } = testBed; + return find('componentTemplatesList.item.name').map((wrapper) => wrapper.text()); + }, + getComponentTemplatesSelected() { + const { find } = testBed; + return find('componentTemplatesSelection.item.name').map((wrapper) => wrapper.text()); + }, + showFilters() { + const { find, component } = testBed; + act(() => { + find('componentTemplates.filterButton').simulate('click'); + }); + component.update(); + }, + async selectFilter(filter: 'settings' | 'mappings' | 'aliases') { + const { find, component } = testBed; + const filters = ['settings', 'mappings', 'aliases']; + const index = filters.indexOf(filter); + + await act(async () => { + find('filterList.filterItem').at(index).simulate('click'); + }); + component.update(); + }, + async selectComponentAt(index: number) { + const { find, component } = testBed; + + await act(async () => { + find('componentTemplatesList.item.action-plusInCircle').at(index).simulate('click'); + }); + component.update(); + }, + async unSelectComponentAt(index: number) { + const { find, component } = testBed; + + await act(async () => { + find('componentTemplatesSelection.item.action-minusInCircle').at(index).simulate('click'); + }); + component.update(); + }, + }; + + // Step Mappings actions + const mappings = { + async addField(name: string, type: string) { + const { find, form, component } = testBed; + + await act(async () => { + form.setInputValue('nameParameterInput', name); + find('createFieldForm.mockComboBox').simulate('change', [ + { + label: type, + value: type, + }, + ]); + }); + + await act(async () => { + find('createFieldForm.addButton').simulate('click'); + }); + + component.update(); + }, + }; + + // Step Review actions + const review = { + async selectTab(tab: 'summary' | 'preview' | 'request') { + const tabs = ['summary', 'preview', 'request']; + + await act(async () => { + testBed.find('summaryTabContent').find('.euiTab').at(tabs.indexOf(tab)).simulate('click'); + }); + + testBed.component.update(); + }, + }; + const completeStepOne = async ({ name, indexPatterns, order, + priority, version, }: Partial = {}) => { - const { form, find, waitFor } = testBed; + const { component, form, find } = testBed; if (name) { - form.setInputValue('nameField.input', name); + act(() => { + form.setInputValue('nameField.input', name); + }); } if (indexPatterns) { @@ -70,94 +150,104 @@ export const formSetup = async (initTestBed: SetupFunc) => { value: pattern, })); - find('mockComboBox').simulate('change', indexPatternsFormatted); // Using mocked EuiComboBox - await nextTick(); + act(() => { + find('mockComboBox').simulate('change', indexPatternsFormatted); // Using mocked EuiComboBox + }); } - if (order) { - form.setInputValue('orderField.input', JSON.stringify(order)); - } + await act(async () => { + if (order) { + form.setInputValue('orderField.input', JSON.stringify(order)); + } - if (version) { - form.setInputValue('versionField.input', JSON.stringify(version)); - } + if (priority) { + form.setInputValue('priorityField.input', JSON.stringify(priority)); + } - clickNextButton(); - await waitFor('stepSettings'); + if (version) { + form.setInputValue('versionField.input', JSON.stringify(version)); + } + + clickNextButton(); + }); + + component.update(); }; - const completeStepTwo = async (settings?: string) => { - const { find, component, waitFor } = testBed; + const completeStepTwo = async (componentName?: string) => { + const { find, component } = testBed; + + if (componentName) { + // Find the index of the template in the list + const allComponents = find('componentTemplatesList.item.name').map((wrapper) => + wrapper.text() + ); + const index = allComponents.indexOf(componentName); + if (index < 0) { + throw new Error( + `Could not find component "${componentName}" in the list ${JSON.stringify(allComponents)}` + ); + } - if (settings) { - find('mockCodeEditor').simulate('change', { - jsonString: settings, - }); // Using mocked EuiCodeEditor - await nextTick(); - component.update(); + await componentTemplates.selectComponentAt(index); } - clickNextButton(); - await waitFor('stepMappings'); + await act(async () => { + clickNextButton(); + }); + + component.update(); + }; + + const completeStepThree = async (settings?: string) => { + const { find, component } = testBed; + + await act(async () => { + if (settings) { + find('mockCodeEditor').simulate('change', { + jsonString: settings, + }); // Using mocked EuiCodeEditor + } + + clickNextButton(); + }); + + component.update(); }; - const completeStepThree = async (mappingFields?: MappingField[]) => { - const { waitFor } = testBed; + const completeStepFour = async (mappingFields?: MappingField[]) => { + const { component } = testBed; if (mappingFields) { for (const field of mappingFields) { const { name, type } = field; - await addMappingField(name, type); + await mappings.addField(name, type); } } - clickNextButton(); - await waitFor('stepAliases'); + await act(async () => { + clickNextButton(); + }); + + component.update(); }; - const completeStepFour = async (aliases?: string, waitForNextStep = true) => { - const { find, component, waitFor } = testBed; + const completeStepFive = async (aliases?: string) => { + const { find, component } = testBed; if (aliases) { - find('mockCodeEditor').simulate('change', { - jsonString: aliases, - }); // Using mocked EuiCodeEditor - await nextTick(); + await act(async () => { + find('mockCodeEditor').simulate('change', { + jsonString: aliases, + }); // Using mocked EuiCodeEditor + }); component.update(); } - clickNextButton(); - - if (waitForNextStep) { - await waitFor('summaryTab'); - } else { - component.update(); - } - }; - - const selectSummaryTab = (tab: 'summary' | 'request') => { - const tabs = ['summary', 'request']; - - testBed.find('summaryTabContent').find('.euiTab').at(tabs.indexOf(tab)).simulate('click'); - }; - - const addMappingField = async (name: string, type: string) => { - const { find, form, component } = testBed; - - form.setInputValue('nameParameterInput', name); - find('createFieldForm.mockComboBox').simulate('change', [ - { - label: type, - value: type, - }, - ]); - - await nextTick(50); - component.update(); - - find('createFieldForm.addButton').simulate('click'); + await act(async () => { + clickNextButton(); + }); - await nextTick(); component.update(); }; @@ -166,7 +256,6 @@ export const formSetup = async (initTestBed: SetupFunc) => { actions: { clickNextButton, clickBackButton, - clickSubmitButton, clickEditButtonAtField, clickEditFieldUpdateButton, deleteMappingsFieldAt, @@ -175,8 +264,10 @@ export const formSetup = async (initTestBed: SetupFunc) => { completeStepTwo, completeStepThree, completeStepFour, - selectSummaryTab, - addMappingField, + completeStepFive, + componentTemplates, + mappings, + review, }, }; }; @@ -187,6 +278,17 @@ export type TestSubjects = | 'backButton' | 'codeEditorContainer' | 'confirmModalConfirmButton' + | 'componentTemplates.filterButton' + | 'componentTemplates.emptySearchResult' + | 'filterList.filterItem' + | 'componentTemplatesList' + | 'componentTemplatesList.item.name' + | 'componentTemplatesList.item.action-plusInCircle' + | 'componentTemplatesSelection' + | 'componentTemplatesSelection.item.name' + | 'componentTemplatesSelection.item.action-minusInCircle' + | 'componentTemplatesSelection.emptyPrompt' + | 'componentTemplateSearchBox' | 'createFieldForm.addPropertyButton' | 'createFieldForm.addButton' | 'createFieldForm.addFieldButton' @@ -209,12 +311,15 @@ export type TestSubjects = | 'nextButton' | 'orderField' | 'orderField.input' + | 'priorityField.input' | 'pageTitle' + | 'previewTab' | 'removeFieldButton' | 'requestTab' | 'saveTemplateError' | 'settingsEditor' | 'systemTemplateEditCallout' + | 'stepComponents' | 'stepAliases' | 'stepMappings' | 'stepSettings' diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/helpers/index.ts b/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/helpers/index.ts index 93eb65aac0761..4b92235993e26 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/helpers/index.ts +++ b/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/helpers/index.ts @@ -9,7 +9,7 @@ import { setup as componentTemplateDetailsSetup } from './component_template_det export { nextTick, getRandomString, findTestSubject } from '../../../../../../../../../test_utils'; -export { setupEnvironment } from './setup_environment'; +export { setupEnvironment, appDependencies } from './setup_environment'; export const pageHelpers = { componentTemplateList: { setup: componentTemplatesListSetup }, diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/helpers/setup_environment.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/helpers/setup_environment.tsx index 79e213229fc51..ac748e1b7dc2c 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/helpers/setup_environment.tsx +++ b/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/helpers/setup_environment.tsx @@ -24,7 +24,7 @@ import { API_BASE_PATH } from './constants'; const mockHttpClient = axios.create({ adapter: axiosXhrAdapter }); const { GlobalFlyoutProvider } = GlobalFlyout; -const appDependencies = { +export const appDependencies = { httpClient: (mockHttpClient as unknown) as HttpSetup, apiBasePath: API_BASE_PATH, trackMetric: () => {}, diff --git a/x-pack/plugins/apm/public/hooks/useLocation.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/index.ts similarity index 58% rename from x-pack/plugins/apm/public/hooks/useLocation.tsx rename to x-pack/plugins/index_management/public/application/components/component_templates/__jest__/index.ts index e2b4e3e479629..ebd2cd9392568 100644 --- a/x-pack/plugins/apm/public/hooks/useLocation.tsx +++ b/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/index.ts @@ -4,9 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -import { useContext } from 'react'; -import { LocationContext } from '../context/LocationContext'; - -export function useLocation() { - return useContext(LocationContext); -} +export { appDependencies as componentTemplatesMockDependencies } from './client_integration/helpers'; diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_selector/component_templates.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_selector/component_templates.tsx index b07279c57d2be..3c10ef2cc7edc 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_selector/component_templates.tsx +++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_selector/component_templates.tsx @@ -145,12 +145,13 @@ export const ComponentTemplates = ({ isLoading, components, listItemProps }: Pro /> } + data-test-subj="emptySearchResult" /> ); }; return ( -
+
@@ -162,6 +163,7 @@ export const ComponentTemplates = ({ isLoading, components, listItemProps }: Pro }} aria-label={i18nTexts.searchBoxPlaceholder} className="componentTemplates__searchBox" + data-test-subj="componentTemplateSearchBox" /> diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_selector/component_templates_list.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_selector/component_templates_list.tsx index 0c64c38c8963f..519f2482c9323 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_selector/component_templates_list.tsx +++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_selector/component_templates_list.tsx @@ -19,10 +19,10 @@ interface Props { export const ComponentTemplatesList = ({ components, listItemProps }: Props) => { return ( - <> +
{components.map((component) => ( ))} - +
); }; diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_selector/component_templates_list_item.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_selector/component_templates_list_item.tsx index ad75c8dcbcc54..40bb062f5ddf3 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_selector/component_templates_list_item.tsx +++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_selector/component_templates_list_item.tsx @@ -48,6 +48,7 @@ export const ComponentTemplatesListItem = ({ className={classNames('componentTemplatesListItem', { 'componentTemplatesListItem--selected': isSelectedValue, })} + data-test-subj="item" > @@ -59,7 +60,7 @@ export const ComponentTemplatesListItem = ({
)} - + {/* {component.name} */} onViewDetail(component)}>{component.name} @@ -83,7 +84,7 @@ export const ComponentTemplatesListItem = ({ action.handler(component)} - data-test-subj="addPropertyButton" + data-test-subj={`action-${action.icon}`} aria-label={action.label} /> diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_selector/component_templates_selector.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_selector/component_templates_selector.tsx index ff871b8b79247..f2fd5048931de 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_selector/component_templates_selector.tsx +++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_selector/component_templates_selector.tsx @@ -160,6 +160,7 @@ export const ComponentTemplatesSelector = ({ // eslint-disable-next-line @typescript-eslint/naming-convention 'componentTemplatesSelector__selection--is-empty': !hasSelection, })} + data-test-subj="componentTemplatesSelection" > {hasSelection ? ( <> @@ -200,7 +201,7 @@ export const ComponentTemplatesSelector = ({
) : ( - +

0} numActiveFilters={activeFilters.length} - data-test-subj="viewButton" + data-test-subj="filterButton" > { - let testBed: MappingsEditorTestBed; - /** * Variable to store the mappings data forwarded to the consumer component */ let data: any; + let onChangeHandler: jest.Mock = jest.fn(); + let getMappingsEditorData = getMappingsEditorDataFactory(onChangeHandler); + let testBed: MappingsEditorTestBed; beforeAll(() => { jest.useFakeTimers(); @@ -34,6 +34,11 @@ describe('Mappings editor: shape datatype', () => { jest.useRealTimers(); }); + beforeEach(() => { + onChangeHandler = jest.fn(); + getMappingsEditorData = getMappingsEditorDataFactory(onChangeHandler); + }); + test('initial view and default parameters values', async () => { const defaultMappings = { properties: { @@ -45,7 +50,10 @@ describe('Mappings editor: shape datatype', () => { const updatedMappings = { ...defaultMappings }; - testBed = setup({ value: defaultMappings, onChange: onChangeHandler }); + await act(async () => { + testBed = setup({ value: defaultMappings, onChange: onChangeHandler }); + }); + testBed.component.update(); const { component, @@ -53,7 +61,7 @@ describe('Mappings editor: shape datatype', () => { } = testBed; // Open the flyout to edit the field - startEditField('myField'); + await startEditField('myField'); // Save the field and close the flyout await updateFieldAndCloseFlyout(); diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/datatypes/text_datatype.test.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/datatypes/text_datatype.test.tsx index 66989baa2dc67..1832bedee0143 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/datatypes/text_datatype.test.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/datatypes/text_datatype.test.tsx @@ -9,8 +9,6 @@ import { componentHelpers, MappingsEditorTestBed } from '../helpers'; import { getFieldConfig } from '../../../lib'; const { setup, getMappingsEditorDataFactory } = componentHelpers.mappingsEditor; -const onChangeHandler = jest.fn(); -const getMappingsEditorData = getMappingsEditorDataFactory(onChangeHandler); // Parameters automatically added to the text datatype when saved (with the default values) export const defaultTextParameters = { @@ -24,14 +22,14 @@ export const defaultTextParameters = { store: false, }; -// FLAKY: https://github.com/elastic/kibana/issues/66669 -describe.skip('Mappings editor: text datatype', () => { - let testBed: MappingsEditorTestBed; - +describe('Mappings editor: text datatype', () => { /** * Variable to store the mappings data forwarded to the consumer component */ let data: any; + let onChangeHandler: jest.Mock = jest.fn(); + let getMappingsEditorData = getMappingsEditorDataFactory(onChangeHandler); + let testBed: MappingsEditorTestBed; beforeAll(() => { jest.useFakeTimers(); @@ -41,8 +39,9 @@ describe.skip('Mappings editor: text datatype', () => { jest.useRealTimers(); }); - afterEach(() => { - onChangeHandler.mockReset(); + beforeEach(() => { + onChangeHandler = jest.fn(); + getMappingsEditorData = getMappingsEditorDataFactory(onChangeHandler); }); test('initial view and default parameters values', async () => { @@ -56,7 +55,10 @@ describe.skip('Mappings editor: text datatype', () => { const updatedMappings = { ...defaultMappings }; - testBed = setup({ value: defaultMappings, onChange: onChangeHandler }); + await act(async () => { + testBed = setup({ value: defaultMappings, onChange: onChangeHandler }); + }); + testBed.component.update(); const { component, @@ -64,7 +66,7 @@ describe.skip('Mappings editor: text datatype', () => { } = testBed; // Open the flyout to edit the field - startEditField('myField'); + await startEditField('myField'); // It should have searchable ("index" param) active by default const indexFieldConfig = getFieldConfig('index'); @@ -80,7 +82,7 @@ describe.skip('Mappings editor: text datatype', () => { ({ data } = await getMappingsEditorData(component)); expect(data).toEqual(updatedMappings); - }, 10000); + }); test('analyzer parameter: default values', async () => { const defaultMappings = { @@ -96,7 +98,10 @@ describe.skip('Mappings editor: text datatype', () => { }, }; - testBed = setup({ value: defaultMappings, onChange: onChangeHandler }); + await act(async () => { + testBed = setup({ value: defaultMappings, onChange: onChangeHandler }); + }); + testBed.component.update(); const { component, @@ -113,8 +118,8 @@ describe.skip('Mappings editor: text datatype', () => { const fieldToEdit = 'myField'; // Start edit and immediately save to have all the default values - startEditField(fieldToEdit); - showAdvancedSettings(); + await startEditField(fieldToEdit); + await showAdvancedSettings(); await updateFieldAndCloseFlyout(); expect(exists('mappingsEditorFieldEdit')).toBe(false); @@ -133,8 +138,8 @@ describe.skip('Mappings editor: text datatype', () => { expect(data).toEqual(updatedMappings); // Re-open the edit panel - startEditField(fieldToEdit); - showAdvancedSettings(); + await startEditField(fieldToEdit); + await showAdvancedSettings(); // When no analyzer is defined, defaults to "Index default" let indexAnalyzerValue = find('indexAnalyzer.select').props().value; @@ -158,9 +163,8 @@ describe.skip('Mappings editor: text datatype', () => { expect(exists('searchAnalyzer')).toBe(false); // Uncheck the "Use same analyzer for search" checkbox and make sure the dedicated select appears - selectCheckBox('useSameAnalyzerForSearchCheckBox.input', false); - act(() => { - jest.advanceTimersByTime(1000); + await act(async () => { + selectCheckBox('useSameAnalyzerForSearchCheckBox.input', false); }); component.update(); @@ -169,7 +173,6 @@ describe.skip('Mappings editor: text datatype', () => { let searchAnalyzerValue = find('searchAnalyzer.select').props().value; expect(searchAnalyzerValue).toEqual('index_default'); - // Change the value of the 3 analyzers await act(async () => { // Change the value of the 3 analyzers setSelectValue('indexAnalyzer.select', 'standard', false); @@ -195,8 +198,8 @@ describe.skip('Mappings editor: text datatype', () => { expect(data).toEqual(updatedMappings); // Re-open the flyout and make sure the select have the correct updated value - startEditField('myField'); - showAdvancedSettings(); + await startEditField('myField'); + await showAdvancedSettings(); isUseSameAnalyzerForSearchChecked = getCheckboxValue('useSameAnalyzerForSearchCheckBox.input'); expect(isUseSameAnalyzerForSearchChecked).toBe(false); @@ -208,7 +211,7 @@ describe.skip('Mappings editor: text datatype', () => { expect(indexAnalyzerValue).toBe('standard'); expect(searchAnalyzerValue).toBe('simple'); expect(searchQuoteAnalyzerValue).toBe('whitespace'); - }, 50000); + }); test('analyzer parameter: custom analyzer (external plugin)', async () => { const defaultMappings = { @@ -234,7 +237,10 @@ describe.skip('Mappings editor: text datatype', () => { }, }; - testBed = setup({ value: defaultMappings, onChange: onChangeHandler }); + await act(async () => { + testBed = setup({ value: defaultMappings, onChange: onChangeHandler }); + }); + testBed.component.update(); const { find, @@ -245,8 +251,8 @@ describe.skip('Mappings editor: text datatype', () => { } = testBed; const fieldToEdit = 'myField'; - startEditField(fieldToEdit); - showAdvancedSettings(); + await startEditField(fieldToEdit); + await showAdvancedSettings(); expect(exists('indexAnalyzer-custom')).toBe(true); expect(exists('searchAnalyzer-custom')).toBe(true); @@ -301,7 +307,7 @@ describe.skip('Mappings editor: text datatype', () => { }; expect(data).toEqual(updatedMappings); - }, 100000); + }); test('analyzer parameter: custom analyzer (from index settings)', async () => { const indexSettings = { @@ -320,8 +326,6 @@ describe.skip('Mappings editor: text datatype', () => { const customAnalyzers = Object.keys(indexSettings.analysis.analyzer); const defaultMappings = { - _meta: {}, - _source: {}, properties: { myField: { type: 'text', @@ -340,11 +344,14 @@ describe.skip('Mappings editor: text datatype', () => { }, }; - testBed = setup({ - value: defaultMappings, - onChange: onChangeHandler, - indexSettings, + await act(async () => { + testBed = setup({ + value: defaultMappings, + onChange: onChangeHandler, + indexSettings, + }); }); + testBed.component.update(); const { component, @@ -354,8 +361,8 @@ describe.skip('Mappings editor: text datatype', () => { } = testBed; const fieldToEdit = 'myField'; - startEditField(fieldToEdit); - showAdvancedSettings(); + await startEditField(fieldToEdit); + await showAdvancedSettings(); // It should have 2 selects const indexAnalyzerSelects = find('indexAnalyzer.select'); @@ -395,5 +402,5 @@ describe.skip('Mappings editor: text datatype', () => { }; expect(data).toEqual(updatedMappings); - }, 50000); + }); }); diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/edit_field.test.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/edit_field.test.tsx index c146c7704911f..9634817835101 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/edit_field.test.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/edit_field.test.tsx @@ -8,10 +8,14 @@ import { act } from 'react-dom/test-utils'; import { componentHelpers, MappingsEditorTestBed } from './helpers'; import { defaultTextParameters, defaultShapeParameters } from './datatypes'; const { setup, getMappingsEditorDataFactory } = componentHelpers.mappingsEditor; -const onChangeHandler = jest.fn(); -const getMappingsEditorData = getMappingsEditorDataFactory(onChangeHandler); describe('Mappings editor: edit field', () => { + /** + * Variable to store the mappings data forwarded to the consumer component + */ + let data: any; + let onChangeHandler: jest.Mock = jest.fn(); + let getMappingsEditorData = getMappingsEditorDataFactory(onChangeHandler); let testBed: MappingsEditorTestBed; beforeAll(() => { @@ -22,8 +26,9 @@ describe('Mappings editor: edit field', () => { jest.useRealTimers(); }); - afterEach(() => { - onChangeHandler.mockReset(); + beforeEach(() => { + onChangeHandler = jest.fn(); + getMappingsEditorData = getMappingsEditorDataFactory(onChangeHandler); }); test('should open a flyout with the correct field to edit', async () => { @@ -43,7 +48,11 @@ describe('Mappings editor: edit field', () => { }, }; - testBed = setup({ value: defaultMappings, onChange: onChangeHandler }); + await act(async () => { + testBed = setup({ value: defaultMappings, onChange: onChangeHandler }); + }); + testBed.component.update(); + await testBed.actions.expandAllFieldsAndReturnMetadata(); const { @@ -51,7 +60,7 @@ describe('Mappings editor: edit field', () => { actions: { startEditField }, } = testBed; // Open the flyout to edit the field - startEditField('user.address.street'); + await startEditField('user.address.street'); // It should have the correct title expect(find('mappingsEditorFieldEdit.flyoutTitle').text()).toEqual(`Edit field 'street'`); @@ -72,7 +81,10 @@ describe('Mappings editor: edit field', () => { }, }; - testBed = setup({ value: defaultMappings, onChange: onChangeHandler }); + await act(async () => { + testBed = setup({ value: defaultMappings, onChange: onChangeHandler }); + }); + testBed.component.update(); const { find, @@ -83,19 +95,18 @@ describe('Mappings editor: edit field', () => { expect(exists('userNameField' as any)).toBe(true); // Open the flyout, change the field type and save it - startEditField('userName'); + await startEditField('userName'); // Change the field type - find('mappingsEditorFieldEdit.fieldType').simulate('change', [ - { label: 'Shape', value: defaultShapeParameters.type }, - ]); - act(() => { - jest.advanceTimersByTime(1000); + await act(async () => { + find('mappingsEditorFieldEdit.fieldType').simulate('change', [ + { label: 'Shape', value: defaultShapeParameters.type }, + ]); }); await updateFieldAndCloseFlyout(); - const { data } = await getMappingsEditorData(component); + ({ data } = await getMappingsEditorData(component)); const updatedMappings = { ...defaultMappings, @@ -107,5 +118,5 @@ describe('Mappings editor: edit field', () => { }; expect(data).toEqual(updatedMappings); - }, 50000); + }); }); diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/helpers/mappings_editor.helpers.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/helpers/mappings_editor.helpers.tsx index a6558b28a1273..2a4af89c46559 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/helpers/mappings_editor.helpers.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/helpers/mappings_editor.helpers.tsx @@ -149,22 +149,28 @@ const createActions = (testBed: TestBed) => { return { field: find(testSubject as TestSubjects), testSubject }; }; - const addField = (name: string, type: string) => { - form.setInputValue('nameParameterInput', name); - find('createFieldForm.fieldType').simulate('change', [ - { - label: type, - value: type, - }, - ]); - find('createFieldForm.addButton').simulate('click'); + const addField = async (name: string, type: string) => { + await act(async () => { + form.setInputValue('nameParameterInput', name); + find('createFieldForm.fieldType').simulate('change', [ + { + label: type, + value: type, + }, + ]); + }); + + await act(async () => { + find('createFieldForm.addButton').simulate('click'); + }); + + component.update(); }; - const startEditField = (path: string) => { + const startEditField = async (path: string) => { const { testSubject } = getFieldAt(path); - find(`${testSubject}.editFieldButton` as TestSubjects).simulate('click'); - act(() => { - jest.advanceTimersByTime(1000); + await act(async () => { + find(`${testSubject}.editFieldButton` as TestSubjects).simulate('click'); }); component.update(); }; @@ -174,34 +180,33 @@ const createActions = (testBed: TestBed) => { find('mappingsEditorFieldEdit.editFieldUpdateButton').simulate('click'); }); component.update(); - - act(() => { - jest.advanceTimersByTime(1000); - }); }; - const showAdvancedSettings = () => { + const showAdvancedSettings = async () => { if (find('mappingsEditorFieldEdit.advancedSettings').props().style.display === 'block') { // Already opened, nothing else to do return; } - find('mappingsEditorFieldEdit.toggleAdvancedSetting').simulate('click'); - - act(() => { - jest.advanceTimersByTime(1000); + await act(async () => { + find('mappingsEditorFieldEdit.toggleAdvancedSetting').simulate('click'); }); + component.update(); }; - const selectTab = (tab: 'fields' | 'templates' | 'advanced') => { + const selectTab = async (tab: 'fields' | 'templates' | 'advanced') => { const index = ['fields', 'templates', 'advanced'].indexOf(tab); const tabElement = find('formTab').at(index); if (tabElement.length === 0) { throw new Error(`Tab not found: "${tab}"`); } - tabElement.simulate('click'); + + await act(async () => { + tabElement.simulate('click'); + }); + component.update(); }; const updateJsonEditor = (testSubject: TestSubjects, value: object) => { diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/mappings_editor.test.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/mappings_editor.test.tsx index 0743211a2b7bf..68933ddc9a935 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/mappings_editor.test.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/mappings_editor.test.tsx @@ -8,15 +8,15 @@ import { act } from 'react-dom/test-utils'; import { componentHelpers, MappingsEditorTestBed } from './helpers'; const { setup, getMappingsEditorDataFactory } = componentHelpers.mappingsEditor; -const onChangeHandler = jest.fn(); -const getMappingsEditorData = getMappingsEditorDataFactory(onChangeHandler); -// FLAKY: https://github.com/elastic/kibana/issues/66457 -describe.skip('Mappings editor: core', () => { +describe('Mappings editor: core', () => { /** * Variable to store the mappings data forwarded to the consumer component */ let data: any; + let onChangeHandler: jest.Mock = jest.fn(); + let getMappingsEditorData = getMappingsEditorDataFactory(onChangeHandler); + let testBed: MappingsEditorTestBed; beforeAll(() => { jest.useFakeTimers(); @@ -26,8 +26,9 @@ describe.skip('Mappings editor: core', () => { jest.useRealTimers(); }); - afterEach(() => { - onChangeHandler.mockReset(); + beforeEach(() => { + onChangeHandler = jest.fn(); + getMappingsEditorData = getMappingsEditorDataFactory(onChangeHandler); }); test('default behaviour', async () => { @@ -42,11 +43,14 @@ describe.skip('Mappings editor: core', () => { }, }; - const { component } = setup({ value: defaultMappings, onChange: onChangeHandler }); + await act(async () => { + testBed = setup({ value: defaultMappings, onChange: onChangeHandler }); + }); + + const { component } = testBed; + component.update(); const expectedMappings = { - _meta: {}, // Was not defined so an empty object is returned - _source: {}, // Was not defined so an empty object is returned ...defaultMappings, properties: { user: { @@ -78,8 +82,13 @@ describe.skip('Mappings editor: core', () => { }, }, }; - const testBed = setup({ onChange: onChangeHandler, value }); - const { exists } = testBed; + + await act(async () => { + testBed = setup({ onChange: onChangeHandler, value }); + }); + + const { component, exists } = testBed; + component.update(); expect(exists('mappingsEditor')).toBe(true); expect(exists('mappingTypesDetectedCallout')).toBe(true); @@ -94,8 +103,12 @@ describe.skip('Mappings editor: core', () => { }, }, }; - const testBed = setup({ onChange: onChangeHandler, value }); - const { exists } = testBed; + await act(async () => { + testBed = setup({ onChange: onChangeHandler, value }); + }); + + const { component, exists } = testBed; + component.update(); expect(exists('mappingsEditor')).toBe(true); expect(exists('mappingTypesDetectedCallout')).toBe(false); @@ -108,10 +121,12 @@ describe.skip('Mappings editor: core', () => { properties: {}, dynamic_templates: [{ before: 'foo' }], }; - let testBed: MappingsEditorTestBed; beforeEach(async () => { - testBed = setup({ value: defaultMappings, onChange: onChangeHandler }); + await act(async () => { + testBed = setup({ value: defaultMappings, onChange: onChangeHandler }); + }); + testBed.component.update(); }); test('should keep the changes when switching tabs', async () => { @@ -129,10 +144,7 @@ describe.skip('Mappings editor: core', () => { expect(find('fieldsListItem').length).toEqual(0); // Check that we start with an empty list const newField = { name: 'John', type: 'text' }; - await act(async () => { - addField(newField.name, newField.type); - }); - component.update(); + await addField(newField.name, newField.type); expect(find('fieldsListItem').length).toEqual(1); @@ -142,10 +154,7 @@ describe.skip('Mappings editor: core', () => { // ------------------------------------- // Navigate to dynamic templates tab // ------------------------------------- - await act(async () => { - selectTab('templates'); - }); - component.update(); + await selectTab('templates'); let templatesValue = getJsonEditorValue('dynamicTemplatesEditor'); expect(templatesValue).toEqual(defaultMappings.dynamic_templates); @@ -163,10 +172,7 @@ describe.skip('Mappings editor: core', () => { // ------------------------------------------------------ // Switch to advanced settings tab and make some changes // ------------------------------------------------------ - await act(async () => { - selectTab('advanced'); - }); - component.update(); + await selectTab('advanced'); let isDynamicMappingsEnabled = getToggleValue( 'advancedConfiguration.dynamicMappingsToggle.input' @@ -194,10 +200,7 @@ describe.skip('Mappings editor: core', () => { // ---------------------------------------------------------------------------- // Go back to dynamic templates tab and make sure our changes are still there // ---------------------------------------------------------------------------- - await act(async () => { - selectTab('templates'); - }); - component.update(); + await selectTab('templates'); templatesValue = getJsonEditorValue('dynamicTemplatesEditor'); expect(templatesValue).toEqual(updatedValueTemplates); @@ -205,18 +208,13 @@ describe.skip('Mappings editor: core', () => { // ----------------------------------------------------------- // Go back to fields and make sure our created field is there // ----------------------------------------------------------- - await act(async () => { - selectTab('fields'); - }); - component.update(); + await selectTab('fields'); + field = find('fieldsListItem').at(0); expect(find('fieldName', field).text()).toEqual(newField.name); // Go back to advanced settings tab make sure dynamic mappings is disabled - await act(async () => { - selectTab('advanced'); - }); - component.update(); + await selectTab('advanced'); isDynamicMappingsEnabled = getToggleValue( 'advancedConfiguration.dynamicMappingsToggle.input' @@ -231,46 +229,47 @@ describe.skip('Mappings editor: core', () => { /** * Note: the "indexSettings" prop will be tested along with the "analyzer" parameter on a text datatype field, * as it is the only place where it is consumed by the mappings editor. - * - * The test that covers it is text_datatype.test.tsx: "analyzer parameter: custom analyzer (from index settings)" + * The test that covers it is in the "text_datatype.test.tsx": "analyzer parameter: custom analyzer (from index settings)" */ - const defaultMappings: any = { - dynamic: true, - numeric_detection: false, - date_detection: true, - properties: { - title: { type: 'text' }, - address: { - type: 'object', - properties: { - street: { type: 'text' }, - city: { type: 'text' }, + let defaultMappings: any; + + beforeEach(async () => { + defaultMappings = { + dynamic: true, + numeric_detection: false, + date_detection: true, + properties: { + title: { type: 'text' }, + address: { + type: 'object', + properties: { + street: { type: 'text' }, + city: { type: 'text' }, + }, }, }, - }, - dynamic_templates: [{ initial: 'value' }], - _source: { - enabled: true, - includes: ['field1', 'field2'], - excludes: ['field3'], - }, - _meta: { - some: 'metaData', - }, - _routing: { - required: false, - }, - }; - - let testBed: MappingsEditorTestBed; + dynamic_templates: [{ initial: 'value' }], + _source: { + enabled: true, + includes: ['field1', 'field2'], + excludes: ['field3'], + }, + _meta: { + some: 'metaData', + }, + _routing: { + required: false, + }, + }; - beforeEach(async () => { - testBed = setup({ value: defaultMappings, onChange: onChangeHandler }); + await act(async () => { + testBed = setup({ value: defaultMappings, onChange: onChangeHandler }); + }); + testBed.component.update(); }); test('props.value => should prepopulate the editor data', async () => { const { - component, actions: { selectTab, getJsonEditorValue, getComboBoxValue, getToggleValue }, find, } = testBed; @@ -285,10 +284,7 @@ describe.skip('Mappings editor: core', () => { /** * Dynamic templates */ - await act(async () => { - selectTab('templates'); - }); - component.update(); + await selectTab('templates'); // Test that dynamic templates JSON is rendered in the templates editor const templatesValue = getJsonEditorValue('dynamicTemplatesEditor'); @@ -297,10 +293,7 @@ describe.skip('Mappings editor: core', () => { /** * Advanced settings */ - await act(async () => { - selectTab('advanced'); - }); - component.update(); + await selectTab('advanced'); const isDynamicMappingsEnabled = getToggleValue( 'advancedConfiguration.dynamicMappingsToggle.input' @@ -339,7 +332,14 @@ describe.skip('Mappings editor: core', () => { /** * Mapped fields */ + await act(async () => { + find('addFieldButton').simulate('click'); + }); + component.update(); + const newField = { name: 'someNewField', type: 'text' }; + await addField(newField.name, newField.type); + updatedMappings = { ...updatedMappings, properties: { @@ -348,26 +348,14 @@ describe.skip('Mappings editor: core', () => { }, }; - await act(async () => { - find('addFieldButton').simulate('click'); - }); - component.update(); - - await act(async () => { - addField(newField.name, newField.type); - }); - component.update(); - ({ data } = await getMappingsEditorData(component)); + expect(data).toEqual(updatedMappings); /** * Dynamic templates */ - await act(async () => { - await selectTab('templates'); - }); - component.update(); + await selectTab('templates'); const updatedTemplatesValue = [{ someTemplateProp: 'updated' }]; updatedMappings = { @@ -385,10 +373,7 @@ describe.skip('Mappings editor: core', () => { /** * Advanced settings */ - await act(async () => { - selectTab('advanced'); - }); - component.update(); + await selectTab('advanced'); // Disbable dynamic mappings await act(async () => { diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/analyzer_parameter.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/analyzer_parameter.tsx index dc52a362008c6..1457c4583aa0e 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/analyzer_parameter.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/analyzer_parameter.tsx @@ -71,9 +71,8 @@ export const AnalyzerParameter = ({ allowsIndexDefaultOption = true, 'data-test-subj': dataTestSubj, }: Props) => { - const indexSettings = useIndexSettings(); + const { value: indexSettings } = useIndexSettings(); const customAnalyzers = getCustomAnalyzers(indexSettings); - const analyzerOptions = allowsIndexDefaultOption ? ANALYZER_OPTIONS : ANALYZER_OPTIONS_WITHOUT_DEFAULT; diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/analyzer_parameter_selects.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/analyzer_parameter_selects.tsx index 17d3ea0909bfb..c966df82fb507 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/analyzer_parameter_selects.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/analyzer_parameter_selects.tsx @@ -49,16 +49,17 @@ export const AnalyzerParameterSelects = ({ 'data-test-subj': dataTestSubj, }: Props) => { const { form } = useForm({ defaultValue: { main: mainDefaultValue, sub: subDefaultValue } }); + const { subscribe } = form; useEffect(() => { - const subscription = form.subscribe((updateData) => { + const subscription = subscribe((updateData) => { const formData = updateData.data.raw; const value = formData.sub ? formData.sub : formData.main; onChange(value); }); return subscription.unsubscribe; - }, [form, onChange]); + }, [subscribe, onChange]); const getSubOptionsMeta = useCallback( (mainValue: string) => diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/index_settings_context.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/index_settings_context.tsx index 411193f10b24a..bd84c3a905ec8 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/index_settings_context.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/index_settings_context.tsx @@ -3,23 +3,32 @@ * 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, { createContext, useContext } from 'react'; +import React, { createContext, useContext, useState } from 'react'; import { IndexSettings } from './types'; -const IndexSettingsContext = createContext(undefined); +const IndexSettingsContext = createContext< + { value: IndexSettings; update: (value: IndexSettings) => void } | undefined +>(undefined); interface Props { - indexSettings: IndexSettings | undefined; children: React.ReactNode; } -export const IndexSettingsProvider = ({ indexSettings = {}, children }: Props) => ( - {children} -); +export const IndexSettingsProvider = ({ children }: Props) => { + const [state, setState] = useState({}); + + return ( + + {children} + + ); +}; export const useIndexSettings = () => { const ctx = useContext(IndexSettingsContext); - - return ctx === undefined ? {} : ctx; + if (ctx === undefined) { + throw new Error('useIndexSettings must be used within a '); + } + return ctx; }; diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/mappings_editor.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/mappings_editor.tsx index 39451639bfb86..39c4a2885efa5 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/mappings_editor.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/mappings_editor.tsx @@ -25,7 +25,7 @@ import { import { extractMappingsDefinition } from './lib'; import { useMappingsState } from './mappings_state_context'; import { useMappingsStateListener } from './use_state_listener'; -import { IndexSettingsProvider } from './index_settings_context'; +import { useIndexSettings } from './index_settings_context'; type TabName = 'fields' | 'advanced' | 'templates'; @@ -94,6 +94,12 @@ export const MappingsEditor = React.memo(({ onChange, value, indexSettings }: Pr */ useMappingsStateListener({ onChange, value: parsedDefaultValue }); + // Update the Index settings context so it is available in the Global flyout + const { update: updateIndexSettings } = useIndexSettings(); + if (indexSettings !== undefined) { + updateIndexSettings(indexSettings); + } + const state = useMappingsState(); const [selectedTab, selectTab] = useState('fields'); @@ -141,43 +147,41 @@ export const MappingsEditor = React.memo(({ onChange, value, indexSettings }: Pr {multipleMappingsDeclared ? ( ) : ( - -

- - changeTab('fields')} - isSelected={selectedTab === 'fields'} - data-test-subj="formTab" - > - {i18n.translate('xpack.idxMgmt.mappingsEditor.fieldsTabLabel', { - defaultMessage: 'Mapped fields', - })} - - changeTab('templates')} - isSelected={selectedTab === 'templates'} - data-test-subj="formTab" - > - {i18n.translate('xpack.idxMgmt.mappingsEditor.templatesTabLabel', { - defaultMessage: 'Dynamic templates', - })} - - changeTab('advanced')} - isSelected={selectedTab === 'advanced'} - data-test-subj="formTab" - > - {i18n.translate('xpack.idxMgmt.mappingsEditor.advancedTabLabel', { - defaultMessage: 'Advanced options', - })} - - - - - - {tabToContentMap[selectedTab]} -
- +
+ + changeTab('fields')} + isSelected={selectedTab === 'fields'} + data-test-subj="formTab" + > + {i18n.translate('xpack.idxMgmt.mappingsEditor.fieldsTabLabel', { + defaultMessage: 'Mapped fields', + })} + + changeTab('templates')} + isSelected={selectedTab === 'templates'} + data-test-subj="formTab" + > + {i18n.translate('xpack.idxMgmt.mappingsEditor.templatesTabLabel', { + defaultMessage: 'Dynamic templates', + })} + + changeTab('advanced')} + isSelected={selectedTab === 'advanced'} + data-test-subj="formTab" + > + {i18n.translate('xpack.idxMgmt.mappingsEditor.advancedTabLabel', { + defaultMessage: 'Advanced options', + })} + + + + + + {tabToContentMap[selectedTab]} +
)}
); diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/mappings_editor_context.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/mappings_editor_context.tsx index 596b49cc89ee8..8e30d07c2262f 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/mappings_editor_context.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/mappings_editor_context.tsx @@ -6,7 +6,12 @@ import React from 'react'; import { StateProvider } from './mappings_state_context'; +import { IndexSettingsProvider } from './index_settings_context'; export const MappingsEditorProvider: React.FC = ({ children }) => { - return {children}; + return ( + + {children} + + ); }; diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/reducer.ts b/x-pack/plugins/index_management/public/application/components/mappings_editor/reducer.ts index 2a8368c666859..e7efd6f28343b 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/reducer.ts +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/reducer.ts @@ -175,10 +175,18 @@ export const reducer = (state: State, action: Action): State => { fields: action.value.fields, configuration: { ...state.configuration, + data: { + raw: action.value.configuration, + format: () => action.value.configuration, + }, defaultValue: action.value.configuration, }, templates: { ...state.templates, + data: { + raw: action.value.templates, + format: () => action.value.templates, + }, defaultValue: action.value.templates, }, documentFields: { diff --git a/x-pack/plugins/index_management/test/fixtures/template.ts b/x-pack/plugins/index_management/test/fixtures/template.ts index 3b9de2b3409b6..ac6e8b7879a26 100644 --- a/x-pack/plugins/index_management/test/fixtures/template.ts +++ b/x-pack/plugins/index_management/test/fixtures/template.ts @@ -11,6 +11,42 @@ const objHasProperties = (obj?: Record): boolean => { return obj === undefined || Object.keys(obj).length === 0 ? false : true; }; +export const getComposableTemplate = ({ + name = getRandomString(), + version = getRandomNumber(), + priority = getRandomNumber(), + indexPatterns = [], + template: { settings, aliases, mappings } = {}, + hasDatastream = false, + isLegacy = false, + type = 'default', +}: Partial< + TemplateDeserialized & { + isLegacy?: boolean; + type?: TemplateType; + hasDatastream: boolean; + } +> = {}): TemplateDeserialized => { + const indexTemplate = { + name, + version, + priority, + indexPatterns, + template: { + aliases, + mappings, + settings, + }, + _kbnMeta: { + type, + hasDatastream, + isLegacy, + }, + }; + + return indexTemplate; +}; + export const getTemplate = ({ name = getRandomString(), version = getRandomNumber(), diff --git a/x-pack/plugins/infra/public/plugin.ts b/x-pack/plugins/infra/public/plugin.ts index 8a1264d254e40..66715b3fee28b 100644 --- a/x-pack/plugins/infra/public/plugin.ts +++ b/x-pack/plugins/infra/public/plugin.ts @@ -52,7 +52,7 @@ export class Plugin implements InfraClientPluginClass { title: i18n.translate('xpack.infra.logs.pluginTitle', { defaultMessage: 'Logs', }), - euiIconType: 'logsApp', + euiIconType: 'logoObservability', order: 8100, appRoute: '/app/logs', category: DEFAULT_APP_CATEGORIES.observability, @@ -70,7 +70,7 @@ export class Plugin implements InfraClientPluginClass { title: i18n.translate('xpack.infra.metrics.pluginTitle', { defaultMessage: 'Metrics', }), - euiIconType: 'metricsApp', + euiIconType: 'logoObservability', order: 8200, appRoute: '/app/metrics', category: DEFAULT_APP_CATEGORIES.observability, diff --git a/x-pack/plugins/ingest_manager/common/constants/routes.ts b/x-pack/plugins/ingest_manager/common/constants/routes.ts index 551a0f7d60164..3e065142ea101 100644 --- a/x-pack/plugins/ingest_manager/common/constants/routes.ts +++ b/x-pack/plugins/ingest_manager/common/constants/routes.ts @@ -21,7 +21,8 @@ export const EPM_API_ROUTES = { LIST_PATTERN: EPM_PACKAGES_MANY, LIMITED_LIST_PATTERN: `${EPM_PACKAGES_MANY}/limited`, INFO_PATTERN: EPM_PACKAGES_ONE, - INSTALL_PATTERN: EPM_PACKAGES_ONE, + INSTALL_FROM_REGISTRY_PATTERN: EPM_PACKAGES_ONE, + INSTALL_BY_UPLOAD_PATTERN: EPM_PACKAGES_MANY, DELETE_PATTERN: EPM_PACKAGES_ONE, FILEPATH_PATTERN: `${EPM_PACKAGES_FILE}/{filePath*}`, CATEGORIES_PATTERN: `${EPM_API_ROOT}/categories`, diff --git a/x-pack/plugins/ingest_manager/common/services/routes.ts b/x-pack/plugins/ingest_manager/common/services/routes.ts index 1d802739a1b86..b7521f95b4f83 100644 --- a/x-pack/plugins/ingest_manager/common/services/routes.ts +++ b/x-pack/plugins/ingest_manager/common/services/routes.ts @@ -40,7 +40,10 @@ export const epmRouteService = { }, getInstallPath: (pkgkey: string) => { - return EPM_API_ROUTES.INSTALL_PATTERN.replace('{pkgkey}', pkgkey).replace(/\/$/, ''); // trim trailing slash + return EPM_API_ROUTES.INSTALL_FROM_REGISTRY_PATTERN.replace('{pkgkey}', pkgkey).replace( + /\/$/, + '' + ); // trim trailing slash }, getRemovePath: (pkgkey: string) => { diff --git a/x-pack/plugins/ingest_manager/common/types/rest_spec/epm.ts b/x-pack/plugins/ingest_manager/common/types/rest_spec/epm.ts index 5fb718f91b876..54e767fee4b22 100644 --- a/x-pack/plugins/ingest_manager/common/types/rest_spec/epm.ts +++ b/x-pack/plugins/ingest_manager/common/types/rest_spec/epm.ts @@ -71,6 +71,10 @@ export interface InstallPackageResponse { response: AssetReference[]; } +export interface MessageResponse { + response: string; +} + export interface DeletePackageRequest { params: { pkgkey: string; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/layouts/default.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/layouts/default.tsx index 30294779d1a3d..7da8330740532 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/layouts/default.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/layouts/default.tsx @@ -5,11 +5,11 @@ */ import React from 'react'; import styled from 'styled-components'; -import { EuiTabs, EuiTab, EuiFlexGroup, EuiFlexItem, EuiIcon, EuiButtonEmpty } from '@elastic/eui'; +import { EuiTabs, EuiTab, EuiFlexGroup, EuiFlexItem, EuiButtonEmpty } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { Section } from '../sections'; import { AlphaMessaging, SettingFlyout } from '../components'; -import { useLink, useConfig, useCore } from '../hooks'; +import { useLink, useConfig } from '../hooks'; interface Props { showSettings?: boolean; @@ -42,7 +42,6 @@ export const DefaultLayout: React.FunctionComponent = ({ }) => { const { getHref } = useLink(); const { fleet } = useConfig(); - const { uiSettings } = useCore(); const [isSettingsFlyoutOpen, setIsSettingsFlyoutOpen] = React.useState(false); return ( @@ -58,11 +57,6 @@ export const DefaultLayout: React.FunctionComponent = ({