diff --git a/.buildkite/scripts/steps/storybooks/build_and_upload.js b/.buildkite/scripts/steps/storybooks/build_and_upload.js index c1032575ae4a6..49e36d2126cd4 100644 --- a/.buildkite/scripts/steps/storybooks/build_and_upload.js +++ b/.buildkite/scripts/steps/storybooks/build_and_upload.js @@ -6,13 +6,20 @@ const path = require('path'); const STORYBOOKS = [ 'apm', 'canvas', + 'codeeditor', 'ci_composite', 'url_template_editor', - 'codeeditor', 'dashboard', 'dashboard_enhanced', 'data_enhanced', 'embeddable', + 'expression_error', + 'expression_image', + 'expression_metric', + 'expression_repeat_image', + 'expression_reveal_image', + 'expression_shape', + 'expression_tagcloud', 'fleet', 'infra', 'security_solution', diff --git a/docs/CHANGELOG.asciidoc b/docs/CHANGELOG.asciidoc index 3b88cc26b3207..12c9751d73a85 100644 --- a/docs/CHANGELOG.asciidoc +++ b/docs/CHANGELOG.asciidoc @@ -13,6 +13,7 @@ Review important information about the {kib} 7.x releases. // Best practices: // * When there are changes to kibana.yml settings, include the links to the new settings. +* <> * <> * <> * <> @@ -62,11 +63,442 @@ Review important information about the {kib} 7.x releases. * <> -- +[[release-notes-7.15.0]] +== {kib} 7.15.0 + +For information about the {kib} 7.15.0 release, review the following information. + +[float] +[[known-issue-7.15.0]] +=== Known issues + +.Upgrade Elastic Agents to use Osquery Manager integration +[%collapsible] +==== +*Details* + +You must upgrade your Elastic Agents to the latest version to use the Osquery Manager integration. + +*Impact* + +To upgrade, refer to {fleet-guide}/upgrade-elastic-agent.html[Upgrade Elastic Agent]. +==== + +.APM: Metrics-powered throughput is incorrect +[%collapsible] +==== +*Details* + +In some cases, users who have enabled APM aggregated transactions (metrics-powered UI), will see throughput incorrectly stuck on 1tpm. + +*Impact* + +Disable metrics powered transactions with `xpack.apm.searchAggregatedTransactions: 'never'` until 7.15.1. See {kibana-pull}112240[#112240] for more information. +==== + +[float] +[[breaking-changes-7.15.0]] +=== Breaking changes + +Breaking changes can prevent your application from optimal operation and performance. +Before you upgrade to 7.15.0, review the breaking changes, then mitigate the impact to your application. + +// tag::notable-breaking-changes[] + +[discrete] +[[breaking-osquery]] +.New scheduled query action ID format for Osquery Manager +[%collapsible] +==== +*Details* + +Action IDs for scheduled queries have been changed, which may break dashboards, alerts, or other features that depend on Osquery action IDs. + +*Impact* + +Previously, scheduled query action IDs used the query ID. For example, if you scheduled a query with the name `rpm_packages`, the action ID would be `rpm_packages`. Now, scheduled query action IDs use the `pack__` format. For example, if you have a scheduled query with the name `rpm_packages` in a group named `system_monitoring`, the action ID would be `pack_system_monitoring_rpm_packages`. +==== + +[discrete] +[[deprecation-105055]] +.Removes support for legacy charts library +[%collapsible] +==== +*Details* + +The legacy area, line, and charts library are deprecated in 7.15.0. In 7.16.0, support for the legacy charts library will be removed. For more information, refer to {kibana-pull}105055[#105055]. + +*Impact* + +When you upgrade, {kib} uses the new charts library by default, which includes improved performance, color palettes, fill capacity, and more. + +If you have changed the default {kibana-ref}/advanced-options.html#kibana-visualization-settings[Advanced Settings], you must disable the legacy charts library to use the new charts library: + +. Open the main menu, then click *Stack Management > Advanced Settings*. + +. Deselect *Legacy charts library*. +==== + +// end::notable-breaking-changes[] + +[float] +[[deprecations-7.15.0]] +=== Deprecations + +The following functionality is deprecated in 7.15.0, and will be removed in 8.0.0. +Deprecated functionality does not have an immediate impact on your application, but we strongly recommend +you make the necessary updates after you upgrade to 7.15.0. + +[discrete] +[[deprecation-108826]] +.Deprecates the dashboard APIs +[%collapsible] +==== +*Details* + +The import and export dashboard APIs are deprecated. For more information, refer to {kibana-pull}108826[#108826]. + +*Impact* + +Use the <> and <> APIs. +==== + +[discrete] +[[deprecation-108281]] +.Deprecates alerting and action settings +[%collapsible] +==== +*Details* + +The ability to disable alerts, actions, task manager, stack alerts, and event log plugins is deprecated. For more information, refer to {kibana-pull}108281[#108281]. + +*Impact* + +Use the supported <>. +==== + +[discrete] +[[deprecation-106566]] +.Moves filter utils to package and cleanup API +[%collapsible] +==== +*Details* + +The following filter-related utilities are deprecated when imported from the data plugin. + +On the client side, `esFilters` is now deprecated: +* `FilterLabel`, `FilterItem`, `getDisplayValueFromFilter`, `generateFilters`, `extractTimeRange` can be imported from `data\public` (not using `esFilters`). + +* The following utility functions are available for import from `@kbn/es-query`: +** `COMPARE_ALL_OPTIONS` +** `compareFilters` +** `enableFilter` +** `pinFilter` +** `toggleFilterDisabled` +** `dedupFilters` +** `onlyDisabledFiltersChanged` +** `uniqFilters` + +* The following functions are deprecated and won't be part of the 8.0.0 public API: +** `convertRangeFilterToTimeRangeString` +** `extractTimeFilter` +** `changeTimeFilter` +** `mapAndFlattenFilters` + +On the server side, `esFilters` is now deprecated. All utility functions are available for import from `@kbn/es-query`. + +For more information, refer to {kibana-pull}106566[#106566]. + +*Impact* + +Import all utility functions from `@kbn/es-query`. +==== + +[discrete] +[[deprecation-106232]] +.Refactors `textarea` UI argument +[%collapsible] +==== +*Details* + +Based on the `x-pack/plugins/canvas/CONTRIBUTING.md`, `recompose` has been removed in favor of React hooks at `textarea`. For more information, refer to {kibana-pull}106232[#106232]. + +*Impact* + +There is no user-facing impact. +==== + +[discrete] +[[deprecation-105981]] +.Deprecates `xpack.maps.showMapVisualizationTypes` +[%collapsible] +==== +*Details* + +Deprecates the `xpack.maps.showMapVisualizationTypes` *Maps* setting. For more information, refer to {kibana-pull}105981[#105981]. + +*Impact* + +When you upgrade, remove `xpack.maps.showMapVisualizationTypes` from your kibana.yml file. +==== + +[discrete] +[[deprecation-105742]] +.Refactors `string` UI argument +[%collapsible] +==== +*Details* + +Based on the `x-pack/plugins/canvas/CONTRIBUTING.md`, `recompose` has been removed in favor of React hooks at `string`. For more information, refer to {kibana-pull}105742[#105742]. + +*Impact* + +There is no user-facing impact. +==== + +[discrete] +[[deprecation-104685]] +.Deprecates legacy audit logger +[%collapsible] +==== +*Details* + +The legacy audit logger is deprecated. For more information, refer to {kibana-pull}104685[#104685]. + +*Impact* + +Use the ECS-comliant audit logger. For more information, refer to <>. +==== + +[discrete] +[[deprecation-100781]] +.Deprecates scripted fields +[%collapsible] +==== +*Details* + +Adding scripted fields to index patterns is deprecated. For more information, refer to {kibana-pull}100781[#100781]. + +*Impact* + +Use runtime fields. For more information, refer to <>. +==== + +[float] +[[features-7.15.0]] +=== Features +{kib} 7.15.0 adds the following new and notable features. + +APM:: +* Adds uninstrumented dependencies view {kibana-pull}106223[#106223] +* Replaces error rate table with failed transactions correlations {kibana-pull}108441[#108441] +* Moves latency correlations from flyout to transactions page {kibana-pull}107266[#107266] +Data ingest:: +* Adds copy_from to set processor {kibana-pull}104070[#104070] +* Adds community id processor {kibana-pull}103863[#103863] +* Adds network direction processor {kibana-pull}103436[#103436] +Elastic Security:: +For the Elastic Security 7.15.0 release information, refer to {security-guide}/release-notes.html[_Elastic Security Solution Release Notes_]. +Lens & Visualizations:: +* Synchronizes cursor position for x-axis across all *Lens* visualizations on a dashboard {kibana-pull}106845[#106845] +* Client Side caching in *Visualize* {kibana-pull}105589[#105589] +Machine Learning:: +* Checks for error messages in the anomaly detection jobs health rule type {kibana-pull}108701[#108701] +* Adds reset anomaly detection jobs link to jobs list {kibana-pull}108039[#108039] +* Delayed data test for anomaly detection jobs health rule type {kibana-pull}107183[#107183] +* Model memory state test for anomaly detection jobs health rule type {kibana-pull}106763[#106763] +* Alerting rule for anomaly detection jobs monitoring {kibana-pull}106084[#106084] +* Jobs import and export {kibana-pull}101037[#101037] +Osquery Manager:: +* Adds RBAC for the Osquery plugin {kibana-pull}106669[#106669] +* Adds an ECS mapping editor for scheduled queries {kibana-pull}107706[#107706] +Querying & Filtering:: +* Adds scoring support to KQL {kibana-pull}103727[#103727] +Sharing:: +* Redirect endpoint {kibana-pull}103899[#103899] + +For more information about the features introduced in 7.15.0, refer to <>. + +[[enhancements-and-bug-fixes-v7.15.0]] +=== Enhancements and bug fixes + +For detailed information about the 7.15.0 release, review the enhancements and bug fixes. + +[float] +[[enhancement-v7.15.0]] +=== Enhancements +Alerting:: +* Changed rules table to support visual indication for disabled and muted alerts {kibana-pull}104190[#104190] +APM:: +* Enables JVM metrics for the Ruby Agent running on a JVM (jRuby) {kibana-pull}108933[#108933] +* Adds APM agent instrumentation instructions in Fleet {kibana-pull}108242[#108242] +* Renames "Error rate" to "Failed transaction rate" {kibana-pull}107895[#107895] +* Fixes service inventory responsive design {kibana-pull}107690[#107690] +* Adds a logs tab for services {kibana-pull}107664[#107664] +* Adds time comparison to the Transactions page {kibana-pull}107299[#107299] +* Adds time comparison to the services inventory {kibana-pull}107094[#107094] +* Adds backend info and icons to flyouts {kibana-pull}107089[#107089] +* Supports visualizing composite spans {kibana-pull}106862[#106862] +* Allows editing of APM rules {kibana-pull}106598[#106598] +* Redesigns the APM Integration settings {kibana-pull}106535[#106535] +* Adds bulk update route to rule registry and bulk update function to alerts client {kibana-pull}106297[#106297] +* Replaces error rate table with failed transactions correlations {kibana-pull}108441[#108441] +* Moves latency correlations from flyout to transactions page {kibana-pull}107266[#107266] +* Latency correlations: Field/value candidates prioritization {kibana-pull}107370[#107370] +Canvas:: +* Expression progress {kibana-pull}104457[#104457] +* Expression metric {kibana-pull}104390[#104390] +* Expression image {kibana-pull}104318[#104318] +* Expression repeat image {kibana-pull}104255[#104255] +* Expression reveal image. Async libs and images loading {kibana-pull}103399[#103399] +* Expression shape {kibana-pull}103219[#103219] +* Expression error {kibana-pull}103048[#103048] +* Expression reveal image {kibana-pull}101987[#101987] +Data ingest:: +* Adds copy_from to set processor {kibana-pull}104070[#104070] +* Adds community id processor {kibana-pull}103863[#103863] +* Support output_format in date processor {kibana-pull}103729[#103729] +* Adds network direction processor {kibana-pull}103436[#103436] +Discover:: +* Split single query into 2 queries for faster results {kibana-pull}104818[#104818] +* Improves empty state page {kibana-pull}103602[#103602] +Elastic Security:: +For the Elastic Security 7.15.0 release information, refer to {security-guide}/release-notes.html[_Elastic Security Solution Release Notes_]. +Enterprise Search:: +For the Elastic Enterprise Search 7.15.0 release information, refer to {enterprise-search-ref}/release-notes-7.15.0.html[7.15.0 release notes]. +Fleet:: +* Updates Package Policy UI to support upgrading package policies {kibana-pull}107171[#107171] +* Removes subseconds from `event.ingested` {kibana-pull}104044[#104044] +* Adds package policy upgrade API {kibana-pull}103017[#103017] +Kibana Home & Add Data:: +* Use *Lens* in ecommerce data {kibana-pull}106039[#106039] +* Use *Lens* in flights dashboard {kibana-pull}104780[#104780] +Lens & Visualizations:: +* Long legend values support in *Aggregation based* visualizations {kibana-pull}108365[#108365] +* Long legend values support in *TSVB* {kibana-pull}108023[#108023] +* Supports long legend values in *Lens* {kibana-pull}107894[#107894] +* Adds a color picker in percentiles and percentiles rank aggs in *TSVB* {kibana-pull}107390[#107390] +* Synchronize cursor position for x-axis across all *Lens* visualizations on a dashboard {kibana-pull}106845[#106845] +* Allows the users to change the axis orientation in *Lens* {kibana-pull}106369[#106369] +* Improve network error message in *Lens* {kibana-pull}106246[#106246] +* Cleanup bar value labels configuration in *Lens* {kibana-pull}106231[#106231] +* Cleanup bar value labels configuration in *Aggregation based* visualizations {kibana-pull}106198[#106198] +* Adds client-side caching in *Visualize Library* to eliminate unnecessary data fetching when editing visualizations {kibana-pull}105589[#105589] +* Display legend inside chart in *Lens* {kibana-pull}105571[#105571] +* Adds a deprecation notice in the UI and a docs section {kibana-pull}105055[#105055] +* Replace flot with elastic-chart in *Timelion* {kibana-pull}81565[#81565] +Logs:: +* Refactor breadcrumbs {kibana-pull}103249[#103249] +Machine Learning:: +* Adds support for model_prune_window in job wizard {kibana-pull}108734[#108734] +* Checks for error messages in the anomaly detection jobs health rule type {kibana-pull}108701[#108701] +* Adds initial record score to the anomalies table expanded row content {kibana-pull}108216[#108216] +* Adds reset anomaly detection jobs link to jobs list {kibana-pull}108039[#108039] +* Adds evaluation quality metrics to classification exploration view {kibana-pull}107862[#107862] +* Adds a 30 day model prune window to non-rare security jobs {kibana-pull}107752[#107752] +* Delayed data test for anomaly detection jobs health rule type {kibana-pull}107183[#107183] +* Model memory state test for anomaly detection jobs health rule type {kibana-pull}106763[#106763] +* Enables index data visualizer document count chart to update time range query {kibana-pull}106438[#106438] +* Alerting rule for anomaly detection jobs monitoring {kibana-pull}106084[#106084] +* Edits rare anomaly detection job summaries {kibana-pull}105694[#105694] +* Jobs import and export {kibana-pull}101037[#101037] +Management:: +* Adds index templates flyout to the edit policy form {kibana-pull}108362[#108362] +* Refactored policies list to use EuiInMemoryTable {kibana-pull}107510[#107510] +* Transforms: Adds a type column to the transforms management table {kibana-pull}106990[#106990] +* Adds a flyout with linked index templates {kibana-pull}106734[#106734] +* Adds es version field support {kibana-pull}104870[#104870] +* Adds preview for runtime fields {kibana-pull}100198[#100198] +Maps:: +* 'show this layer only' layer action {kibana-pull}107947[#107947] +* Adds indication in layer TOC when layer is filtered by map bounds {kibana-pull}107662[#107662] +* Show actionable message when term joins have no matches {kibana-pull}105161[#105161] +* Adds edit tools defaults for user and timestamp {kibana-pull}103588[#103588] +* Auto generate legends and styles from mvt data {kibana-pull}94811[#94811] +Metrics:: +* Moves saved views button to page header {kibana-pull}107951[#107951] +* Adds manage rules link to alerts dropdown {kibana-pull}107950[#107950] +* Adds checkbox to optionally drop partial buckets from threshold alerts {kibana-pull}107676[#107676] +* Adds system.cpu.total.norm.pct to default metrics {kibana-pull}102428[#102428] +Monitoring:: +* Enables OOTB alerts in RAC page and multiple rules of a rule type {kibana-pull}106457[#106457] +Osquery Manager:: +* Adds status info for scheduled queries {kibana-pull}106600[#106600] +Platform:: +* Improves not found response handling in the saved objects repository {kibana-pull}108749[#108749] +* Updates `esaggs` expressions function to return partial results {kibana-pull}105620[#105620] +* Updates expressions public API to expose partial results support {kibana-pull}102403[#102403] +* Changes execution of alerts from async to sync {kibana-pull}97311[#97311] +Querying & Filtering:: +* Filter FilterBar suggestions by time (according to flag) {kibana-pull}107192[#107192] +Reporting:: +* Adds support of chunked export {kibana-pull}108485[#108485] +* Consolidate report job warnings and add warning for deprecated types {kibana-pull}106184[#106184] +* Adds warning logs about CSV export type being deprecated {kibana-pull}104025[#104025] +* New UI for migrating reporting indices ILM policy {kibana-pull}103853[#103853] +Security:: +* Support authenticating to Elasticsearch via service account tokens {kibana-pull}102121[#102121] +* Space management page UX improvements {kibana-pull}100448[#100448] +Sharing:: +* Redirect endpoint {kibana-pull}103899[#103899] +* Adds *Lens* markdown plugin {kibana-pull}96703[#96703] +Uptime:: +* Improve dedupe client performance {kibana-pull}103979[#103979] +* Multi Series View {kibana-pull}103855[#103855] +* Adds browser monitors configuration options {kibana-pull}102928[#102928] +Other:: +* Adds Thumbnails to Search UI {kibana-pull}104199[#104199] + +[float] +[[fixes-v7.15.0]] +=== Bug Fixes +APM:: +* Custom links creation don't work {kibana-pull}110676[#110676] +* Show relevant nodes in focused service map {kibana-pull}108028[#108028] +* Display throughput as tps (instead of tpm) when bucket size < 60 seconds {kibana-pull}107850[#107850] +Canvas:: +* `Flyout` refactor {kibana-pull}106728[#106728] +* Register `expression_functions` in `{expression}/public/plugin.ts` {kibana-pull}106636[#106636] +Discover:: +* Hide "Manage Searches" if user has insufficient permissions {kibana-pull}109099[#109099] +* Don't give write permissions to index patterns via Discover write permissions {kibana-pull}108376[#108376] +* Fixes multi-field display when parent field is not indexed {kibana-pull}102938[#102938] +Elastic Security:: +For the Elastic Security 7.15.0 release information, refer to {security-guide}/release-notes.html[_Elastic Security Solution Release Notes_]. +Fleet:: +* Fixes Fleet settings and HostInput error handling {kibana-pull}109418[#109418] +* Fixes Agent policy search to support simple text filters {kibana-pull}107306[#107306] +Kibana Home & Add Data:: +* Fixes `tabindex` and collapsible functionality {kibana-pull}107462[#107462] +Lens & Visualizations:: +* Fixes filters reappearing in the saved object when saving in *Lens* {kibana-pull}110460[#110460] +* Fixes small multiple title in dark mode {kibana-pull}110008[#110008] +* Markdown variables should be clickable in *TSVB* {kibana-pull}108844[#108844] +* Fixes Markdown variables are not available on the first rendering in *TSVB* {kibana-pull}108836[#108836] +* Switching between some aggregations in bucket section for sibling aggregations breaks the visualization {kibana-pull}108693[#108693] +* Fixes when clicking "Save and Return" on a Lens visualization the visualization's description gets erased in *Lens* {kibana-pull}108669[#108669] +* [Accessibility] Take into account background color for non opaque colors in *Lens* {kibana-pull}107877[#107877] +* Adds the ability to override runtime_mappings in *Vega* {kibana-pull}107875[#107875] +* Fixes behavior for points and bars in *timelion* {kibana-pull}107398[#107398] +* Formats correctly the falsy values on the x axis in *Lens* {kibana-pull}107134[#107134] +* Fixes send data request twice when opening visualizations {kibana-pull}106398[#106398] +* Filter button in legend keyword accessible {kibana-pull}106374[#106374] +* Annotations support runtime fields in *TSVB* {kibana-pull}104287[#104287] +* Top_hit supports runtime fields in *TSVB* {kibana-pull}103401[#103401] +* Saved visualization with search string confuse altering of search string {kibana-pull}103396[#103396] +Machine Learning:: +* Fixes "Show charts" control state {kibana-pull}110602[#110602] +* Fixes alignment of sorting arrow when histogram charts are enabled in data grid {kibana-pull}110053[#110053] +* Removes hardcoded datafeed indices for security auth and network modules {kibana-pull}109692[#109692] +* Fixes missing final new line character issue {kibana-pull}109274[#109274] +* Ensures cloning retains hyperparameters and results field is correct in data frame analytics wizard {kibana-pull}107811[#107811] +* Disables query delay editing for non-admin users {kibana-pull}107517[#107517] +* Ensures results view loads correctly for data frame analytics job created in Dev Tools {kibana-pull}107024[#107024] +Management:: +* Removes index pattern placeholder advanced setting {kibana-pull}110334[#110334] +* Fixes suffix field format leaks to index pattern management {kibana-pull}107139[#107139] +* Enable inspector to display multiple requests for multiple layers {kibana-pull}105224[#105224] +* Unified check for CSV cells for known formula characters (and value escaping more in general) {kibana-pull}105221[#105221] +Maps:: +* Abort full screen in dashboard and maps when user clicks back button {kibana-pull}108747[#108747] +* Include caused_by field for import failures {kibana-pull}107907[#107907] +Metrics:: +* Fixes refresh button for node details page {kibana-pull}108666[#108666] +* Removes alert previews {kibana-pull}107978[#107978] +* Ensures alert dropdown closes properly {kibana-pull}106343[#106343] +* Increase number of saved views fetched to 1000 {kibana-pull}106310[#106310] +* Drop partial buckets from ALL Metrics UI queries {kibana-pull}104784[#104784] +Platform:: +* Migrations: limit batch sizes to migrations.batchSizeBytes (= 100mb by default) {kibana-pull}109540[#109540] +Reporting:: +* Fixes ability to export CSV on searched data with frozen indices {kibana-pull}109976[#109976] + [[release-notes-7.14.2]] == {kib} 7.14.2 Review the following information about the 7.14.2 release. +[float] +[[known-issue-v7.14.2]] +=== Known issue +{kib} is unable to restore *Discover* search sessions with a relative time range. When you restore the *Discover* search session, then run a new search, {kib} displays a `Your search session is still running` message. For more information, refer to {kibana-issue}101430[#101430]. + [float] [[breaking-changes-v7.14.2]] === Breaking changes @@ -252,8 +684,6 @@ For information about the {kib} 7.14.0 release, review the following information Breaking changes can prevent your application from optimal operation and performance. Before you upgrade to 7.14.0, review the breaking changes, then mitigate the impact to your application. -// tag::notable-breaking-changes[] - [discrete] [[breaking-102263]] .Changes audit event terminology @@ -310,8 +740,6 @@ For more information, refer to {kibana-pull}99078[#99078]. When you upgrade to 7.14.0, {kib} automatically reflects the changes. No action is needed. ==== -// end::notable-breaking-changes[] - [float] [[deprecations-7.14.0]] === Deprecations @@ -1014,8 +1442,6 @@ You should export the used index patterns separately. Breaking changes can prevent your application from optimal operation and performance. Before you upgrade to 7.13.0, review the breaking changes, then mitigate the impact to your application. -// tag::notable-breaking-changes[] - [discrete] [[breaking-97206]] .Remove Elastic Agent routes and related services @@ -1052,9 +1478,6 @@ The *Explore underlying data* context menu on dashboards is now disabled by defa To enable the *Explore underlying data* context menu, set `xpack.discoverEnhanced.actions.exploreDataInContextMenu.enabled` to `true` in kibana.yml. ==== -// end::notable-breaking-changes[] - - [float] [[deprecations-7.13.0]] === Deprecations @@ -1584,8 +2007,6 @@ For information about the {kib} 7.12.0 release, review the following information Breaking changes can prevent your application from optimal operation and performance. Before you upgrade to 7.12.0, review the breaking changes, then mitigate the impact to your application. -// tag::notable-breaking-changes[] - [discrete] [[breaking-89632]] .Removes geo threshold alert type @@ -1629,8 +2050,6 @@ To display the cluster data in *Discover*, load documents directly from `_source . Go to `discover:searchFieldsFromSource`, then select *On*. ==== -// end::notable-breaking-changes[] - [float] [[known-issues-v7.12.0]] === Known issues @@ -2180,9 +2599,6 @@ Breaking changes can prevent your application from optimal operation and perform // The following section is re-used in the Installation and Upgrade Guide -// tag::notable-breaking-changes[] - - [discrete] [[ingestManager_renamed_fleet]] ==== Ingest Manager plugin renamed Fleet @@ -2220,8 +2636,6 @@ which drops support for glibc `2.12`-based operating systems. *via https://github.com/elastic/kibana/pull/83425[#83425]* -// end::notable-breaking-changes[] - [float] [[deprecation-v7.11.0]] === Deprecations @@ -2728,9 +3142,6 @@ Breaking changes can prevent your application from optimal operation and perform // The following section is re-used in the Installation and Upgrade Guide -// tag::notable-breaking-changes[] - - [discrete] [[breaking_kibana_legacy_plugins]] ===== Legacy plugins support removed @@ -2765,8 +3176,6 @@ more information about this property. *via https://github.com/elastic/kibana/pull/73805[#73805]* -// end::notable-breaking-changes[] - [discrete] [[general-plugin-API-changes-7-10]] ==== Breaking changes for plugin developers @@ -4100,8 +4509,6 @@ Breaking changes can prevent your application from optimal operation and perform // The following section is re-used in the Installation and Upgrade Guide -// tag::notable-breaking-changes[] - [float] [[breaking_kibana_keystore]] ===== `kibana.keystore` moved from the data folder to the config folder @@ -4113,8 +4520,6 @@ that path will continue to be used. *via https://github.com/elastic/kibana/pull/57856[#57856]* -// end::notable-breaking-changes[] - [float] [[general-plugin-API-changes-79]] ==== Breaking changes for plugin developers @@ -5200,9 +5605,6 @@ Breaking changes can prevent your application from optimal operation and perform // The following section is re-used in the Installation and Upgrade Guide - -// tag::notable-breaking-changes[] - [float] [[user-facing-changes-78]] ==== Breaking changes for users @@ -5240,10 +5642,6 @@ This fixes the Back button when navigating between dashboards using drilldowns. *via https://github.com/elastic/kibana/pull/62415[#62415]* ==== - -// end::notable-breaking-changes[] - - [float] [[general-plugin-API-changes-78]] ==== Breaking changes for plugin developers @@ -6063,9 +6461,6 @@ For information about the Kibana 7.7.0 release, review the following information Breaking changes can prevent your application from optimal operation and performance. Before you upgrade to 7.7.0, review the breaking changes, then mitigate the impact to your application. // The following section is re-used in the Installation and Upgrade Guide -// tag::notable-breaking-changes[] - -// end::notable-breaking-changes[] [float] ==== Breaking changes for users @@ -7501,9 +7896,6 @@ Breaking changes can prevent your application from optimal operation and perform * <> // The following section is re-used in the Installation and Upgrade Guide -//tag::notable-breaking-changes[] - -// end::notable-breaking-changes[] [float] [[user-facing-changes]] @@ -9899,7 +10291,6 @@ The following section is re-used in the Installation and Upgrade Guide [[breaking_70_notable]] === Notable breaking changes //// -// tag::notable-breaking-changes[] [float] [[breaking_75_search_instead_of-msearch]] @@ -9927,7 +10318,6 @@ Any installs that previously enabled the Code app will now log a warning when Kibana starts up. It's safe to remove all configurations starting with `xpack.code.`. Starting in 8.0, these warnings will become errors that prevent Kibana from starting up. -// end::notable-breaking-changes[] [float] [[enhancement-7.5.0]] @@ -10303,7 +10693,6 @@ The following section is re-used in the Installation and Upgrade Guide [[breaking_70_notable]] === Notable breaking changes //// -// tag::notable-breaking-changes[] [float] [[breaking_74_search_instead_of-msearch]] @@ -10319,9 +10708,6 @@ When the advanced setting `courier:batchSearches` is disabled, requests from *Discover*, *Visualize*, and *Dashboard* will now query {es} using the `_search` endpoint rather than the `_msearch` endpoint. - -// end::notable-breaking-changes[] - [float] [[enhancement-7.4.0]] === Enhancements @@ -10691,7 +11077,6 @@ The following section is re-used in the Installation and Upgrade Guide [[breaking_70_notable]] === Notable breaking changes //// -// tag::notable-breaking-changes[] [float] ==== Visibility of features after configuring a term join in Maps @@ -10712,8 +11097,6 @@ histograms might no longer work in 7.3. If you run into issues starting a advanced editor of the {transforms} wizard. The advanced editor will remove the unsupported attribute once the configuration gets applied. -// end::notable-breaking-changes[] - [float] [[breaking_73_dashboard_import_export]] ==== Dashboard import and export APIs @@ -11050,10 +11433,6 @@ and <>. //NOTE: The notable-breaking-changes tagged regions are re-used in the //Installation and Upgrade Guide -//tag::notable-breaking-changes[] - -// end::notable-breaking-changes[] - [float] [[breaking_72_index_pattern_changes]] @@ -11074,10 +11453,6 @@ on the root wildcard term. For example, a query on an index pattern such as //NOTE: The notable-breaking-changes tagged regions are re-used in the //Installation and Upgrade Guide -//tag::notable-breaking-changes[] - -// end::notable-breaking-changes[] - [float] [[enhancement-7.2.0]] === Enhancements @@ -11553,9 +11928,6 @@ The following section is re-used in the Installation and Upgrade Guide [[breaking_70_notable]] === Notable breaking changes //// -// tag::notable-breaking-changes[] - -// end::notable-breaking-changes[] [float] [[breaking_70_api_changes]] diff --git a/docs/apm/dependencies.asciidoc b/docs/apm/dependencies.asciidoc new file mode 100644 index 0000000000000..b3afdc4880df5 --- /dev/null +++ b/docs/apm/dependencies.asciidoc @@ -0,0 +1,32 @@ +[role="xpack"] +[[dependencies]] +=== Dependencies + +APM agents collect details about external calls made from instrumented services. +Sometimes, these external calls resolve into a downstream service that's instrumented -- in these cases, +you can utilize <> to drill down into problematic downstream services. +Other times, though, it's not possible to instrument a downstream dependency -- +like with a database or third-party service. +**Dependencies** gives you a window into these uninstrumented, downstream dependencies. + +[role="screenshot"] +image::apm/images/dependencies.png[Dependencies view in the APM app in Kibana] + +Many application issues are caused by slow or unresponsive downstream dependencies. +And because a single, slow dependency can significantly impact the end-user experience, +it's important to be able to quickly identify these problems and determine the root cause. + +Select a dependency to see detailed latency, throughput, and failed transaction rate metrics. + +[role="screenshot"] +image::apm/images/dependencies-drilldown.png[Dependencies drilldown view in the APM app in Kibana] + +When viewing a dependency, consider your pattern of usage with that dependency. +If your usage pattern _hasn't_ increased or decreased, +but the experience has been negatively effected -- either with an increase in latency or errors, +there's likely a problem with the dependency that needs to be addressed. + +If your usage pattern _has_ changed, the dependency view can quickly show you whether +that pattern change exists in all upstream services, or just a subset of your services. +You might then start digging into traces coming from +impacted services to determine why that pattern change has occurred. diff --git a/docs/apm/errors.asciidoc b/docs/apm/errors.asciidoc index c468d7f0235b2..d8fc75bf50340 100644 --- a/docs/apm/errors.asciidoc +++ b/docs/apm/errors.asciidoc @@ -4,19 +4,21 @@ TIP: {apm-overview-ref-v}/errors.html[Errors] are groups of exceptions with a similar exception or log message. -The *Errors* overview provides a high-level view of the error message and culprit, -the number of occurrences, and the most recent occurrence. -Just like the transaction overview, you'll notice we group together like errors. -This makes it very easy to quickly see which errors are affecting your services, +The *Errors* overview provides a high-level view of the exceptions that APM agents catch, +or that users manually report with APM agent APIs. +Like errors are grouped together to make it easy to quickly see which errors are affecting your services, and to take actions to rectify them. +A service returning a 5xx code from a request handler, controller, etc., will not create +an exception that an APM agent can catch, and will therefore not show up in this view. + [role="screenshot"] -image::apm/images/apm-errors-overview.png[Example view of the errors overview in the APM app in Kibana] +image::apm/images/apm-errors-overview.png[APM Errors overview] Selecting an error group ID or error message brings you to the *Error group*. [role="screenshot"] -image::apm/images/apm-error-group.png[Example view of the error group page in the APM app in Kibana] +image::apm/images/apm-error-group.png[APM Error group] Here, you'll see the error message, culprit, and the number of occurrences over time. diff --git a/docs/apm/getting-started.asciidoc b/docs/apm/getting-started.asciidoc index 357f694453f4b..c0cb89b51fcc1 100644 --- a/docs/apm/getting-started.asciidoc +++ b/docs/apm/getting-started.asciidoc @@ -29,6 +29,7 @@ start with: * <> * <> +* <> * <> Notice something awry? Select a service or trace and dive deeper with: @@ -46,6 +47,8 @@ include::services.asciidoc[] include::traces.asciidoc[] +include::dependencies.asciidoc[] + include::service-maps.asciidoc[] include::service-overview.asciidoc[] diff --git a/docs/apm/images/all-instances.png b/docs/apm/images/all-instances.png index e77c8af2c46f6..70028b5a9b58b 100644 Binary files a/docs/apm/images/all-instances.png and b/docs/apm/images/all-instances.png differ diff --git a/docs/apm/images/apm-distributed-tracing.png b/docs/apm/images/apm-distributed-tracing.png index 0dbffa591d43a..4d1b8cde20e95 100644 Binary files a/docs/apm/images/apm-distributed-tracing.png and b/docs/apm/images/apm-distributed-tracing.png differ diff --git a/docs/apm/images/apm-geo-ui.png b/docs/apm/images/apm-geo-ui.png index 5bbe713c908a4..69c1390a27989 100644 Binary files a/docs/apm/images/apm-geo-ui.png and b/docs/apm/images/apm-geo-ui.png differ diff --git a/docs/apm/images/apm-logs-tab.png b/docs/apm/images/apm-logs-tab.png index 891d2b7a1dd69..c79be8b5eb0b7 100644 Binary files a/docs/apm/images/apm-logs-tab.png and b/docs/apm/images/apm-logs-tab.png differ diff --git a/docs/apm/images/apm-services-overview.png b/docs/apm/images/apm-services-overview.png index 7aeb5f1ac379f..271c0347aa53e 100644 Binary files a/docs/apm/images/apm-services-overview.png and b/docs/apm/images/apm-services-overview.png differ diff --git a/docs/apm/images/apm-span-detail.png b/docs/apm/images/apm-span-detail.png index c9f55575b2232..d0b6a4de3d3df 100644 Binary files a/docs/apm/images/apm-span-detail.png and b/docs/apm/images/apm-span-detail.png differ diff --git a/docs/apm/images/apm-transaction-duration-dist.png b/docs/apm/images/apm-transaction-duration-dist.png index 91ae6c3a59ad2..9c7ab5dd67dc0 100644 Binary files a/docs/apm/images/apm-transaction-duration-dist.png and b/docs/apm/images/apm-transaction-duration-dist.png differ diff --git a/docs/apm/images/apm-transaction-sample.png b/docs/apm/images/apm-transaction-sample.png index 54eea902f0311..a9490fc20d853 100644 Binary files a/docs/apm/images/apm-transaction-sample.png and b/docs/apm/images/apm-transaction-sample.png differ diff --git a/docs/apm/images/apm-transactions-overview.png b/docs/apm/images/apm-transactions-overview.png index 66cf739a861b7..34cd0219b895d 100644 Binary files a/docs/apm/images/apm-transactions-overview.png and b/docs/apm/images/apm-transactions-overview.png differ diff --git a/docs/apm/images/apm-transactions-table.png b/docs/apm/images/apm-transactions-table.png index b573adfb0c450..8a3415bc9a9f1 100644 Binary files a/docs/apm/images/apm-transactions-table.png and b/docs/apm/images/apm-transactions-table.png differ diff --git a/docs/apm/images/dependencies-drilldown.png b/docs/apm/images/dependencies-drilldown.png new file mode 100644 index 0000000000000..4c491c1ffa254 Binary files /dev/null and b/docs/apm/images/dependencies-drilldown.png differ diff --git a/docs/apm/images/dependencies.png b/docs/apm/images/dependencies.png new file mode 100644 index 0000000000000..260025d31654b Binary files /dev/null and b/docs/apm/images/dependencies.png differ diff --git a/docs/apm/images/error-rate.png b/docs/apm/images/error-rate.png index 036c7a08302bd..845fa2af07de1 100644 Binary files a/docs/apm/images/error-rate.png and b/docs/apm/images/error-rate.png differ diff --git a/docs/apm/images/spans-dependencies.png b/docs/apm/images/spans-dependencies.png index d6e26a5061a6e..558099dd450c1 100644 Binary files a/docs/apm/images/spans-dependencies.png and b/docs/apm/images/spans-dependencies.png differ diff --git a/docs/apm/index.asciidoc b/docs/apm/index.asciidoc index 1041755208efc..e6eb72a5fe805 100644 --- a/docs/apm/index.asciidoc +++ b/docs/apm/index.asciidoc @@ -11,8 +11,7 @@ endif::[] [partintro] -- -The APM app in {kib} is provided with the basic license. -It allows you to monitor your software services and applications in real-time; +The APM app in {kib} allows you to monitor your software services and applications in real-time; visualize detailed performance information on your services, identify and analyze errors, and monitor host-level and agent-specific metrics like JVM and Go runtime metrics. diff --git a/docs/apm/service-overview.asciidoc b/docs/apm/service-overview.asciidoc index f1096a4e43bbc..05537cef58c98 100644 --- a/docs/apm/service-overview.asciidoc +++ b/docs/apm/service-overview.asciidoc @@ -69,34 +69,43 @@ image::apm/images/traffic-transactions.png[Traffic and transactions] [discrete] [[service-error-rates]] -=== Error rate and errors +=== Failed transaction rate and errors -The *Error rate* chart displays the average error rates relating to the service, within a specific time range. -An HTTP response code greater than 400 does not necessarily indicate a failed transaction. -<>. +The failed transaction rate represents the percentage of failed transactions from the perspective of the selected service. +It's useful for visualizing unexpected increases, decreases, or irregular patterns in a service's transactions. ++ +[TIP] +==== +HTTP **transactions** from the HTTP server perspective do not consider a `4xx` status code (client error) as a failure +because the failure was caused by the caller, not the HTTP server. Thus, `event.outcome=success` and there will be no increase in failed transaction rate. + +HTTP **spans** from the client perspective however, are considered failures if the HTTP status code is ≥ 400. +These spans will set `event.outcome=failure` and increase the failed transaction rate. + +If there is no HTTP status, both transactions and spans are considered successful unless an error is reported. +==== The *Errors* table provides a high-level view of each error message when it first and last occurred, along with the total number of occurrences. This makes it very easy to quickly see which errors affect your services and take actions to rectify them. To do so, click *View errors*. [role="screenshot"] -image::apm/images/error-rate.png[Error rate and errors] +image::apm/images/error-rate.png[failed transaction rate and errors] [discrete] [[service-span-duration]] === Span types average duration and dependencies -The *Average duration by span type* chart visualizes each span type's average duration and helps you determine +The *Time spent by span type* chart visualizes each span type's average duration and helps you determine which spans could be slowing down transactions. The "app" label displayed under the chart indicates that something was happening within the application. This could signal that the agent does not have auto-instrumentation for whatever was happening during that time or that the time was spent in the application code and not in database or external requests. The *Dependencies* table displays a list of downstream services or external connections relevant -to the service at the selected time range. The table displays latency, traffic, error rate, and the impact of +to the service at the selected time range. The table displays latency, throughput, failed transaction rate, and the impact of each dependency. By default, dependencies are sorted by _Impact_ to show the most used and the slowest dependency. -If there is a particular dependency you are interested in, click *View service map* to view the related -<>. +If there is a particular dependency you are interested in, click *<>* to learn more about it. NOTE: Displaying dependencies for services instrumented with the Real User Monitoring (RUM) agent requires an agent version ≥ v5.6.3. @@ -106,11 +115,11 @@ image::apm/images/spans-dependencies.png[Span type duration and dependencies] [discrete] [[service-instances]] -=== All instances +=== Instances -The *All instances* table displays a list of all the available service instances within the selected time range. -Depending on how the service runs, the instance could be a host or a container. The table displays latency, traffic, -errors, CPU usage, and memory usage for each instance. By default, instances are sorted by _Throughput_. +The *Instances* table displays a list of all the available service instances within the selected time range. +Depending on how the service runs, the instance could be a host or a container. The table displays latency, throughput, +failed transaction, CPU usage, and memory usage for each instance. By default, instances are sorted by _Throughput_. [role="screenshot"] image::apm/images/all-instances.png[All instances] diff --git a/docs/apm/set-up.asciidoc b/docs/apm/set-up.asciidoc index b2e78bd08bc93..3cbe45ec913b7 100644 --- a/docs/apm/set-up.asciidoc +++ b/docs/apm/set-up.asciidoc @@ -8,7 +8,7 @@ APM is available via the navigation sidebar in {Kib}. If you have not already installed and configured Elastic APM, -the *Setup Instructions* in Kibana will get you started. +the *Add data* page will get you started. [role="screenshot"] image::apm/images/apm-setup.png[Installation instructions on the APM page in Kibana] @@ -17,10 +17,9 @@ image::apm/images/apm-setup.png[Installation instructions on the APM page in Kib [[apm-configure-index-pattern]] === Load the index pattern -Index patterns tell Kibana which Elasticsearch indices you want to explore. +Index patterns tell {kib} which {es} indices you want to explore. An APM index pattern is necessary for certain features in the APM app, like the query bar. -To set up the correct index pattern, -simply click *Load Kibana objects* at the bottom of the Setup Instructions. +To set up the correct index pattern, on the *Add data* page, click *Load Kibana objects*. [role="screenshot"] image::apm/images/apm-index-pattern.png[Setup index pattern for APM in Kibana] diff --git a/docs/apm/transactions.asciidoc b/docs/apm/transactions.asciidoc index 76006d375d075..c0850e4e9d507 100644 --- a/docs/apm/transactions.asciidoc +++ b/docs/apm/transactions.asciidoc @@ -8,7 +8,7 @@ APM agents automatically collect performance metrics on HTTP requests, database [role="screenshot"] image::apm/images/apm-transactions-overview.png[Example view of transactions table in the APM app in Kibana] -The *Latency*, *transactions per minute*, *Error rate*, and *Average duration by span type* +The *Latency*, *transactions per minute*, *Failed transaction rate*, and *Average duration by span type* charts display information on all transactions associated with the selected service: *Latency*:: @@ -23,17 +23,17 @@ Useful for determining if more responses than usual are being served with a part Like in the latency graph, you can zoom in on anomalies to further investigate them. [[transaction-error-rate]] -*Error rate*:: -The error rate represents the percentage of failed transactions from the perspective of the selected service. +*Failed transaction rate*:: +The failed transaction rate represents the percentage of failed transactions from the perspective of the selected service. It's useful for visualizing unexpected increases, decreases, or irregular patterns in a service's transactions. + [TIP] ==== HTTP **transactions** from the HTTP server perspective do not consider a `4xx` status code (client error) as a failure -because the failure was caused by the caller, not the HTTP server. Thus, there will be no increase in error rate. +because the failure was caused by the caller, not the HTTP server. Thus, `event.outcome=success` and there will be no increase in failed transaction rate. HTTP **spans** from the client perspective however, are considered failures if the HTTP status code is ≥ 400. -These spans will increase the error rate. +These spans will set `event.outcome=failure` and increase the failed transaction rate. If there is no HTTP status, both transactions and spans are considered successful unless an error is reported. ==== @@ -97,7 +97,7 @@ This page is visually similar to the transaction overview, but it shows data fro the selected transaction group. [role="screenshot"] -image::apm/images/apm-transaction-response-dist.png[Example view of response time distribution] +image::apm/images/apm-transactions-overview.png[Example view of response time distribution] [[transaction-duration-distribution]] ==== Latency distribution @@ -110,10 +110,10 @@ It's the requests on the right, the ones taking longer than average, that we pro [role="screenshot"] image::apm/images/apm-transaction-duration-dist.png[Example view of latency distribution graph] -Select a latency duration _bucket_ to display up to ten trace samples. +Click and drag to select a latency duration _bucket_ to display up to 500 trace samples. [[transaction-trace-sample]] -==== Trace sample +==== Trace samples Trace samples are based on the _bucket_ selection in the *Latency distribution* chart; update the samples by selecting a new _bucket_. @@ -167,4 +167,11 @@ and solve problems. [role="screenshot"] image::apm/images/apm-logs-tab.png[APM logs tab] -// To do: link to log correlation +[[transaction-latency-correlations]] +==== Correlations + +Correlations surface attributes of your data that are potentially correlated with high-latency or erroneous transactions. +To learn more, see <>. + +[role="screenshot"] +image::apm/images/correlations-hover.png[APM lattency correlations] diff --git a/docs/user/dashboard/lens.asciidoc b/docs/user/dashboard/lens.asciidoc index 0f130f15c8a77..c3e0a5523a78d 100644 --- a/docs/user/dashboard/lens.asciidoc +++ b/docs/user/dashboard/lens.asciidoc @@ -441,4 +441,20 @@ Pagination in a data table is unsupported. To use pagination in data tables, cre [%collapsible] ==== Specifying the color for a single data point, such as a single bar or line, is unsupported. -==== \ No newline at end of file +==== + +[discrete] +[[is-it-possible-to-inspect-the-elasticsearch-queries-in-Lens]] +.*How do I inspect {es} queries in visualizations?* +[%collapsible] +==== +You can inspect the requests sent by the visualization to {es} using the Inspector. It can be accessed within the editor or in the dashboard. +==== + +[discrete] +[[how-to-isolate-a-single-series-in-a-chart]] +.*How do I isolate a single series in a chart?* +[%collapsible] +==== +For area, line, and bar charts, press Shift, then click the series in the legend. All other series are automatically unselected. +==== diff --git a/package.json b/package.json index 92b6fd16f5853..d1d02f8502b42 100644 --- a/package.json +++ b/package.json @@ -107,14 +107,14 @@ "@elastic/search-ui-app-search-connector": "^1.6.0", "@emotion/react": "^11.4.0", "@hapi/accept": "^5.0.2", - "@hapi/boom": "^9.1.1", + "@hapi/boom": "^9.1.4", "@hapi/cookie": "^11.0.2", "@hapi/good-squeeze": "6.0.0", "@hapi/h2o2": "^9.1.0", - "@hapi/hapi": "^20.0.3", - "@hapi/hoek": "^9.1.1", - "@hapi/inert": "^6.0.3", - "@hapi/podium": "^4.1.1", + "@hapi/hapi": "^20.2.0", + "@hapi/hoek": "^9.2.0", + "@hapi/inert": "^6.0.4", + "@hapi/podium": "^4.1.3", "@hapi/wreck": "^17.1.0", "@kbn/ace": "link:bazel-bin/packages/kbn-ace", "@kbn/alerts": "link:bazel-bin/packages/kbn-alerts", @@ -530,11 +530,10 @@ "@types/glob": "^7.1.2", "@types/gulp": "^4.0.6", "@types/gulp-zip": "^4.0.1", - "@types/hapi__cookie": "^10.1.1", - "@types/hapi__h2o2": "^8.3.2", - "@types/hapi__hapi": "^20.0.2", - "@types/hapi__inert": "^5.2.2", - "@types/hapi__podium": "^3.4.1", + "@types/hapi__cookie": "^10.1.3", + "@types/hapi__h2o2": "^8.3.3", + "@types/hapi__hapi": "^20.0.9", + "@types/hapi__inert": "^5.2.3", "@types/has-ansi": "^3.0.0", "@types/he": "^1.1.1", "@types/history": "^4.7.3", @@ -666,7 +665,7 @@ "callsites": "^3.1.0", "chai": "3.5.0", "chance": "1.0.18", - "chromedriver": "^92.0.1", + "chromedriver": "^93.0.1", "clean-webpack-plugin": "^3.0.0", "cmd-shim": "^2.1.0", "compression-webpack-plugin": "^4.0.0", diff --git a/packages/kbn-cli-dev-mode/src/base_path_proxy_server.ts b/packages/kbn-cli-dev-mode/src/base_path_proxy_server.ts index 6d12d5d05f07c..b198e6139d5d7 100644 --- a/packages/kbn-cli-dev-mode/src/base_path_proxy_server.ts +++ b/packages/kbn-cli-dev-mode/src/base_path_proxy_server.ts @@ -131,7 +131,7 @@ export class BasePathProxyServer { agent: this.httpsAgent, passThrough: true, xforward: true, - mapUri: async (request) => { + mapUri: async (request: Request) => { return { // Passing in this header to merge it is a workaround until this is fixed: // https://github.com/hapijs/h2o2/issues/124 diff --git a/packages/kbn-legacy-logging/BUILD.bazel b/packages/kbn-legacy-logging/BUILD.bazel index 1148cf1d38b65..c4927fe076e15 100644 --- a/packages/kbn-legacy-logging/BUILD.bazel +++ b/packages/kbn-legacy-logging/BUILD.bazel @@ -29,6 +29,7 @@ RUNTIME_DEPS = [ "//packages/kbn-utils", "@npm//@elastic/numeral", "@npm//@hapi/hapi", + "@npm//@hapi/podium", "@npm//chokidar", "@npm//lodash", "@npm//moment-timezone", @@ -41,12 +42,12 @@ TYPES_DEPS = [ "//packages/kbn-config-schema", "//packages/kbn-utils", "@npm//@elastic/numeral", + "@npm//@hapi/podium", "@npm//chokidar", "@npm//query-string", "@npm//rxjs", "@npm//tslib", "@npm//@types/hapi__hapi", - "@npm//@types/hapi__podium", "@npm//@types/jest", "@npm//@types/lodash", "@npm//@types/moment-timezone", diff --git a/packages/kbn-legacy-logging/src/legacy_logging_server.ts b/packages/kbn-legacy-logging/src/legacy_logging_server.ts index c02eb2803515a..f6c42dd1b161f 100644 --- a/packages/kbn-legacy-logging/src/legacy_logging_server.ts +++ b/packages/kbn-legacy-logging/src/legacy_logging_server.ts @@ -112,7 +112,6 @@ export class LegacyLoggingServer { tags: [getLegacyLogLevel(level), ...context.split('.'), ...tags], timestamp: timestamp.getTime(), }) - // @ts-expect-error @hapi/podium emit is actually an async function .catch((err) => { // eslint-disable-next-line no-console console.error('An unexpected error occurred while writing to the log:', err.stack); diff --git a/packages/kbn-typed-react-router-config/src/create_router.test.tsx b/packages/kbn-typed-react-router-config/src/create_router.test.tsx index 61ba8eb157ee3..9837d45ddd869 100644 --- a/packages/kbn-typed-react-router-config/src/create_router.test.tsx +++ b/packages/kbn-typed-react-router-config/src/create_router.test.tsx @@ -33,6 +33,28 @@ describe('createRouter', () => { }, }, children: [ + { + path: '/services/{serviceName}/errors', + element: <>, + params: t.type({ + path: t.type({ + serviceName: t.string, + }), + }), + children: [ + { + path: '/services/{serviceName}/errors/{groupId}', + element: <>, + params: t.type({ + path: t.type({ groupId: t.string }), + }), + }, + { + path: '/services/{serviceName}/errors', + element: <>, + }, + ], + }, { path: '/services', element: <>, @@ -43,7 +65,7 @@ describe('createRouter', () => { }), }, { - path: '/services', + path: '/services/{serviceName}', element: <>, children: [ { @@ -252,6 +274,28 @@ describe('createRouter', () => { }, }); }); + + it('matches deep routes', () => { + history.push('/services/opbeans-java/errors/foo?rangeFrom=now-15m&rangeTo=now'); + + const matchedRoutes = router.matchRoutes( + '/services/{serviceName}/errors/{groupId}', + history.location + ); + + expect(matchedRoutes.length).toEqual(4); + + expect(matchedRoutes[matchedRoutes.length - 1].match).toEqual({ + isExact: true, + params: { + path: { + groupId: 'foo', + }, + }, + path: '/services/:serviceName/errors/:groupId', + url: '/services/opbeans-java/errors/foo', + }); + }); }); describe('link', () => { diff --git a/packages/kbn-typed-react-router-config/src/create_router.ts b/packages/kbn-typed-react-router-config/src/create_router.ts index 7f2ac818fc9b9..13f09e7546de5 100644 --- a/packages/kbn-typed-react-router-config/src/create_router.ts +++ b/packages/kbn-typed-react-router-config/src/create_router.ts @@ -26,7 +26,7 @@ const deepExactRt: typeof deepExactRtTyped = deepExactRtNonTyped; const mergeRt: typeof mergeRtTyped = mergeRtNonTyped; function toReactRouterPath(path: string) { - return path.replace(/(?:{([^\/]+)})/, ':$1'); + return path.replace(/(?:{([^\/]+)})/g, ':$1'); } export function createRouter(routes: TRoutes): Router { diff --git a/src/core/public/application/application_service.test.ts b/src/core/public/application/application_service.test.ts index de9e4d4496f3b..f348936d26795 100644 --- a/src/core/public/application/application_service.test.ts +++ b/src/core/public/application/application_service.test.ts @@ -804,6 +804,21 @@ describe('#start()', () => { `); }); + it("when openInNewTab is true it doesn't update currentApp$ after mounting", async () => { + service.setup(setupDeps); + + const { currentAppId$, navigateToApp } = await service.start(startDeps); + const stop$ = new Subject(); + const promise = currentAppId$.pipe(bufferCount(4), takeUntil(stop$)).toPromise(); + + await navigateToApp('delta', { openInNewTab: true }); + stop$.next(); + + const appIds = await promise; + + expect(appIds).toBeUndefined(); + }); + it('updates httpLoadingCount$ while mounting', async () => { // Use a memory history so that mounting the component will work const { createMemoryHistory } = jest.requireActual('history'); diff --git a/src/core/public/application/application_service.tsx b/src/core/public/application/application_service.tsx index 2e804bf2f5413..3ba0d78cf15fd 100644 --- a/src/core/public/application/application_service.tsx +++ b/src/core/public/application/application_service.tsx @@ -250,16 +250,15 @@ export class ApplicationService { if (path === undefined) { path = applications$.value.get(appId)?.defaultPath; } - if (!navigatingToSameApp) { - this.appInternalStates.delete(this.currentAppId$.value!); - } if (openInNewTab) { this.openInNewTab!(getAppUrl(availableMounters, appId, path)); } else { + if (!navigatingToSameApp) { + this.appInternalStates.delete(this.currentAppId$.value!); + } this.navigate!(getAppUrl(availableMounters, appId, path), state, replace); + this.currentAppId$.next(appId); } - - this.currentAppId$.next(appId); } }; diff --git a/src/core/public/doc_links/doc_links_service.ts b/src/core/public/doc_links/doc_links_service.ts index 107b51413a41c..771b53994ece9 100644 --- a/src/core/public/doc_links/doc_links_service.ts +++ b/src/core/public/doc_links/doc_links_service.ts @@ -157,8 +157,20 @@ export class DocLinksService { rollupJobs: `${KIBANA_DOCS}data-rollups.html`, elasticsearch: { docsBase: `${ELASTICSEARCH_DOCS}`, + apiCompatibilityHeader: `${ELASTICSEARCH_DOCS}api-conventions.html#api-compatibility`, asyncSearch: `${ELASTICSEARCH_DOCS}async-search-intro.html`, dataStreams: `${ELASTICSEARCH_DOCS}data-streams.html`, + deprecationLogging: `${ELASTICSEARCH_DOCS}logging.html#deprecation-logging`, + ilm: `${ELASTICSEARCH_DOCS}index-lifecycle-management.html`, + ilmForceMerge: `${ELASTICSEARCH_DOCS}ilm-forcemerge.html`, + ilmFreeze: `${ELASTICSEARCH_DOCS}ilm-freeze.html`, + ilmPhaseTransitions: `${ELASTICSEARCH_DOCS}ilm-index-lifecycle.html#ilm-phase-transitions`, + ilmReadOnly: `${ELASTICSEARCH_DOCS}ilm-readonly.html`, + ilmRollover: `${ELASTICSEARCH_DOCS}ilm-rollover.html`, + ilmSearchableSnapshot: `${ELASTICSEARCH_DOCS}ilm-searchable-snapshot.html`, + ilmSetPriority: `${ELASTICSEARCH_DOCS}ilm-set-priority.html`, + ilmShrink: `${ELASTICSEARCH_DOCS}ilm-shrink.html`, + ilmWaitForSnapshot: `${ELASTICSEARCH_DOCS}ilm-wait-for-snapshot.html`, indexModules: `${ELASTICSEARCH_DOCS}index-modules.html`, indexSettings: `${ELASTICSEARCH_DOCS}index-modules.html#index-modules-settings`, indexTemplates: `${ELASTICSEARCH_DOCS}index-templates.html`, @@ -200,17 +212,17 @@ export class DocLinksService { mappingStore: `${ELASTICSEARCH_DOCS}mapping-store.html`, mappingTermVector: `${ELASTICSEARCH_DOCS}term-vector.html`, mappingTypesRemoval: `${ELASTICSEARCH_DOCS}removal-of-types.html`, + migrateIndexAllocationFilters: `${ELASTICSEARCH_DOCS}migrate-index-allocation-filters.html`, nodeRoles: `${ELASTICSEARCH_DOCS}modules-node.html#node-roles`, - remoteClusters: `${ELASTICSEARCH_DOCS}modules-remote-clusters.html`, - remoteClustersProxy: `${ELASTICSEARCH_DOCS}modules-remote-clusters.html#proxy-mode`, - remoteClusersProxySettings: `${ELASTICSEARCH_DOCS}modules-remote-clusters.html#remote-cluster-proxy-settings`, + releaseHighlights: `${ELASTICSEARCH_DOCS}release-highlights.html`, + remoteClusters: `${ELASTICSEARCH_DOCS}remote-clusters.html`, + remoteClustersProxy: `${ELASTICSEARCH_DOCS}remote-clusters.html#proxy-mode`, + remoteClusersProxySettings: `${ELASTICSEARCH_DOCS}remote-clusters-settings.html#remote-cluster-proxy-settings`, scriptParameters: `${ELASTICSEARCH_DOCS}modules-scripting-using.html#prefer-params`, + setupUpgrade: `${ELASTICSEARCH_DOCS}setup-upgrade.html`, + shardAllocationSettings: `${ELASTICSEARCH_DOCS}modules-cluster.html#cluster-shard-allocation-settings`, transportSettings: `${ELASTICSEARCH_DOCS}modules-network.html#common-network-settings`, typesRemoval: `${ELASTICSEARCH_DOCS}removal-of-types.html`, - deprecationLogging: `${ELASTICSEARCH_DOCS}logging.html#deprecation-logging`, - setupUpgrade: `${ELASTICSEARCH_DOCS}setup-upgrade.html`, - releaseHighlights: `${ELASTICSEARCH_DOCS}release-highlights.html`, - apiCompatibilityHeader: `${ELASTICSEARCH_DOCS}api-conventions.html#api-compatibility`, }, siem: { guide: `${ELASTIC_WEBSITE_URL}guide/en/security/${DOC_LINK_VERSION}/index.html`, @@ -270,6 +282,7 @@ export class DocLinksService { outlierDetectionRoc: `${ELASTIC_WEBSITE_URL}guide/en/machine-learning/${DOC_LINK_VERSION}/ml-dfanalytics-evaluate.html#ml-dfanalytics-roc`, regressionEvaluation: `${ELASTIC_WEBSITE_URL}guide/en/machine-learning/${DOC_LINK_VERSION}/ml-dfanalytics-evaluate.html#ml-dfanalytics-regression-evaluation`, classificationAucRoc: `${ELASTIC_WEBSITE_URL}guide/en/machine-learning/${DOC_LINK_VERSION}/ml-dfanalytics-evaluate.html#ml-dfanalytics-class-aucroc`, + setUpgradeMode: `${ELASTICSEARCH_DOCS}ml-set-upgrade-mode.html`, }, transforms: { guide: `${ELASTICSEARCH_DOCS}transforms.html`, @@ -336,7 +349,7 @@ export class DocLinksService { elasticsearchSettings: `${ELASTICSEARCH_DOCS}security-settings.html`, elasticsearchEnableSecurity: `${ELASTICSEARCH_DOCS}configuring-stack-security.html`, indicesPrivileges: `${ELASTICSEARCH_DOCS}security-privileges.html#privileges-list-indices`, - kibanaTLS: `${KIBANA_DOCS}configuring-tls.html`, + kibanaTLS: `${ELASTICSEARCH_DOCS}security-basic-setup.html#encrypt-internode-communication`, kibanaPrivileges: `${KIBANA_DOCS}kibana-privileges.html`, mappingRoles: `${ELASTICSEARCH_DOCS}mapping-roles.html`, mappingRolesFieldRules: `${ELASTICSEARCH_DOCS}role-mapping-resources.html#mapping-roles-rule-field`, @@ -398,6 +411,7 @@ export class DocLinksService { registerUrl: `${ELASTICSEARCH_DOCS}snapshots-register-repository.html#snapshots-read-only-repository`, restoreSnapshot: `${ELASTICSEARCH_DOCS}snapshots-restore-snapshot.html`, restoreSnapshotApi: `${ELASTICSEARCH_DOCS}restore-snapshot-api.html#restore-snapshot-api-request-body`, + searchableSnapshotSharedCache: `${ELASTICSEARCH_DOCS}searchable-snapshots.html#searchable-snapshots-shared-cache`, }, ingest: { append: `${ELASTICSEARCH_DOCS}append-processor.html`, @@ -415,6 +429,7 @@ export class DocLinksService { fail: `${ELASTICSEARCH_DOCS}fail-processor.html`, foreach: `${ELASTICSEARCH_DOCS}foreach-processor.html`, geoIp: `${ELASTICSEARCH_DOCS}geoip-processor.html`, + geoMatch: `${ELASTICSEARCH_DOCS}geo-match-enrich-policy-type.html`, grok: `${ELASTICSEARCH_DOCS}grok-processor.html`, gsub: `${ELASTICSEARCH_DOCS}gsub-processor.html`, htmlString: `${ELASTICSEARCH_DOCS}htmlstrip-processor.html`, diff --git a/src/core/server/http/router/socket.test.ts b/src/core/server/http/router/socket.test.ts index 60c91786767a6..389c08825d51b 100644 --- a/src/core/server/http/router/socket.test.ts +++ b/src/core/server/http/router/socket.test.ts @@ -92,7 +92,7 @@ describe('KibanaSocket', () => { }); const socket = new KibanaSocket(tlsSocket); - expect(socket.renegotiate({})).resolves.toBe(result); + await expect(socket.renegotiate({})).rejects.toBe(result); expect(spy).toBeCalledTimes(1); }); diff --git a/src/core/server/saved_objects/migrationsv2/actions/integration_tests/actions.test.ts b/src/core/server/saved_objects/migrationsv2/actions/integration_tests/actions.test.ts index 936746ddc6930..32d12e13434aa 100644 --- a/src/core/server/saved_objects/migrationsv2/actions/integration_tests/actions.test.ts +++ b/src/core/server/saved_objects/migrationsv2/actions/integration_tests/actions.test.ts @@ -802,7 +802,9 @@ describe('migration actions', () => { } `); }); - it('resolves left wait_for_task_completion_timeout when the task does not finish within the timeout', async () => { + + // FLAKY https://github.com/elastic/kibana/issues/113012 + it.skip('resolves left wait_for_task_completion_timeout when the task does not finish within the timeout', async () => { const res = (await reindex({ client, sourceIndex: 'existing_index_with_docs', diff --git a/src/core/server/saved_objects/migrationsv2/integration_tests/migration_from_v1.test.ts b/src/core/server/saved_objects/migrationsv2/integration_tests/migration_from_older_v1.test.ts similarity index 64% rename from src/core/server/saved_objects/migrationsv2/integration_tests/migration_from_v1.test.ts rename to src/core/server/saved_objects/migrationsv2/integration_tests/migration_from_older_v1.test.ts index 0f336d7fba43a..8e01e11eaccfb 100644 --- a/src/core/server/saved_objects/migrationsv2/integration_tests/migration_from_v1.test.ts +++ b/src/core/server/saved_objects/migrationsv2/integration_tests/migration_from_older_v1.test.ts @@ -20,7 +20,7 @@ import { InternalCoreStart } from '../../../internal_types'; import { Root } from '../../../root'; const kibanaVersion = Env.createDefault(REPO_ROOT, getEnvOptions()).packageInfo.version; -const logFilePath = Path.join(__dirname, 'migration_from_v1.log'); +const logFilePath = Path.join(__dirname, 'migration_from_older_v1.log'); const asyncUnlink = Util.promisify(Fs.unlink); async function removeLogFile() { @@ -28,7 +28,35 @@ async function removeLogFile() { await asyncUnlink(logFilePath).catch(() => void 0); } -describe('migration v2', () => { +const assertMigratedDocuments = (arr: any[], target: any[]) => target.every((v) => arr.includes(v)); + +function sortByTypeAndId(a: { type: string; id: string }, b: { type: string; id: string }) { + return a.type.localeCompare(b.type) || a.id.localeCompare(b.id); +} + +async function fetchDocuments(esClient: ElasticsearchClient, index: string) { + const { body } = await esClient.search({ + index, + body: { + query: { + match_all: {}, + }, + _source: ['type', 'id'], + }, + }); + + return body.hits.hits + .map((h) => ({ + ...h._source, + id: h._id, + })) + .sort(sortByTypeAndId); +} + +describe('migrating from 7.3.0-xpack which used v1 migrations', () => { + const migratedIndex = `.kibana_${kibanaVersion}_001`; + const originalIndex = `.kibana_1`; // v1 migrations index + let esServer: kbnTestServer.TestElasticsearchUtils; let root: Root; let coreStart: InternalCoreStart; @@ -130,65 +158,50 @@ describe('migration v2', () => { await new Promise((resolve) => setTimeout(resolve, 10000)); }; - // FLAKY: https://github.com/elastic/kibana/issues/87968 - describe.skip('migrating from 7.3.0-xpack version', () => { - const migratedIndex = `.kibana_${kibanaVersion}_001`; - - beforeAll(async () => { - await removeLogFile(); - await startServers({ - oss: false, - dataArchive: Path.join(__dirname, 'archives', '7.3.0_xpack_sample_saved_objects.zip'), - }); + beforeAll(async () => { + await removeLogFile(); + await startServers({ + oss: false, + dataArchive: Path.join(__dirname, 'archives', '7.3.0_xpack_sample_saved_objects.zip'), }); + }); - afterAll(async () => { - await stopServers(); - }); + afterAll(async () => { + await stopServers(); + }); - it('creates the new index and the correct aliases', async () => { - const { body } = await esClient.indices.get( - { - index: migratedIndex, - }, - { ignore: [404] } - ); + it('creates the new index and the correct aliases', async () => { + const { body } = await esClient.indices.get( + { + index: migratedIndex, + }, + { ignore: [404] } + ); - const response = body[migratedIndex]; + const response = body[migratedIndex]; - expect(response).toBeDefined(); - expect(Object.keys(response.aliases!).sort()).toEqual([ - '.kibana', - `.kibana_${kibanaVersion}`, - ]); - }); + expect(response).toBeDefined(); + expect(Object.keys(response.aliases!).sort()).toEqual(['.kibana', `.kibana_${kibanaVersion}`]); + }); - it('copies all the document of the previous index to the new one', async () => { - const migratedIndexResponse = await esClient.count({ - index: migratedIndex, - }); - const oldIndexResponse = await esClient.count({ - index: '.kibana_1', - }); + it('copies all the document of the previous index to the new one', async () => { + const originalDocs = await fetchDocuments(esClient, originalIndex); + const migratedDocs = await fetchDocuments(esClient, migratedIndex); + expect(assertMigratedDocuments(migratedDocs, originalDocs)); + }); - // Use a >= comparison since once Kibana has started it might create new - // documents like telemetry tasks - expect(migratedIndexResponse.body.count).toBeGreaterThanOrEqual(oldIndexResponse.body.count); + it('migrates the documents to the highest version', async () => { + const expectedVersions = getExpectedVersionPerType(); + const res = await esClient.search({ + index: migratedIndex, + body: { + sort: ['_doc'], + }, + size: 10000, }); - - it('migrates the documents to the highest version', async () => { - const expectedVersions = getExpectedVersionPerType(); - const res = await esClient.search({ - index: migratedIndex, - body: { - sort: ['_doc'], - }, - size: 10000, - }); - const allDocuments = res.body.hits.hits as SavedObjectsRawDoc[]; - allDocuments.forEach((doc) => { - assertMigrationVersion(doc, expectedVersions); - }); + const allDocuments = res.body.hits.hits as SavedObjectsRawDoc[]; + allDocuments.forEach((doc) => { + assertMigrationVersion(doc, expectedVersions); }); }); }); diff --git a/src/dev/build/tasks/os_packages/docker_generator/run.ts b/src/dev/build/tasks/os_packages/docker_generator/run.ts index c5a4ff64d2188..21f223a09f60d 100644 --- a/src/dev/build/tasks/os_packages/docker_generator/run.ts +++ b/src/dev/build/tasks/os_packages/docker_generator/run.ts @@ -47,7 +47,7 @@ export async function runDockerGenerator( // General docker var config const license = 'Elastic License'; - const imageTag = 'docker.elastic.co/kibana/kibana'; + const imageTag = `docker.elastic.co/kibana${flags.cloud ? '-ci' : ''}/kibana`; const version = config.getBuildVersion(); const artifactArchitecture = flags.architecture === 'aarch64' ? 'aarch64' : 'x86_64'; const artifactPrefix = `kibana-${version}-linux`; diff --git a/src/plugins/data/common/data_views/data_views/data_view.ts b/src/plugins/data/common/data_views/data_views/data_view.ts index 4e8411233dffa..c4d6f173ef423 100644 --- a/src/plugins/data/common/data_views/data_views/data_view.ts +++ b/src/plugins/data/common/data_views/data_views/data_view.ts @@ -10,6 +10,7 @@ import _, { each, reject } from 'lodash'; import { castEsToKbnFieldTypeName } from '@kbn/field-types'; +import type { estypes } from '@elastic/elasticsearch'; import { FieldAttrs, FieldAttrSet, DataViewAttributes } from '../..'; import type { RuntimeField } from '../types'; import { DuplicateField } from '../../../../kibana_utils/common'; @@ -158,7 +159,7 @@ export class DataView implements IIndexPattern { }; getComputedFields() { - const scriptFields: Record = {}; + const scriptFields: Record = {}; if (!this.fields) { return { storedFields: ['*'], @@ -185,7 +186,7 @@ export class DataView implements IIndexPattern { scriptFields[field.name] = { script: { source: field.script as string, - lang: field.lang as string, + lang: field.lang, }, }; }); diff --git a/src/plugins/data/common/data_views/errors/data_view_saved_object_conflict.ts b/src/plugins/data/common/data_views/errors/data_view_saved_object_conflict.ts new file mode 100644 index 0000000000000..3fcb281655727 --- /dev/null +++ b/src/plugins/data/common/data_views/errors/data_view_saved_object_conflict.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export class DataViewSavedObjectConflictError extends Error { + constructor(savedObjectId: string) { + super(`Conflict loading DataView saved object, id: ${savedObjectId}`); + this.name = 'DataViewSavedObjectConflictError'; + } +} diff --git a/src/plugins/data/common/data_views/errors/index.ts b/src/plugins/data/common/data_views/errors/index.ts index 63bd1ac5f5848..20ff90d3fd6cf 100644 --- a/src/plugins/data/common/data_views/errors/index.ts +++ b/src/plugins/data/common/data_views/errors/index.ts @@ -7,3 +7,4 @@ */ export * from './duplicate_index_pattern'; +export * from './data_view_saved_object_conflict'; diff --git a/src/plugins/data/common/data_views/types.ts b/src/plugins/data/common/data_views/types.ts index d7f65e43dd2df..f0be1a88302a2 100644 --- a/src/plugins/data/common/data_views/types.ts +++ b/src/plugins/data/common/data_views/types.ts @@ -100,8 +100,8 @@ export type OnUnsupportedTimePattern = ({ }) => void; export interface UiSettingsCommon { - get: (key: string) => Promise; - getAll: () => Promise>; + get: (key: string) => Promise; + getAll: () => Promise>; set: (key: string, value: any) => Promise; remove: (key: string) => Promise; } diff --git a/src/plugins/data/common/search/aggs/metrics/top_hit.test.ts b/src/plugins/data/common/search/aggs/metrics/top_hit.test.ts index 04d382f1aa6d1..37ce9c4edb8d1 100644 --- a/src/plugins/data/common/search/aggs/metrics/top_hit.test.ts +++ b/src/plugins/data/common/search/aggs/metrics/top_hit.test.ts @@ -133,28 +133,28 @@ describe('Top hit metric', () => { }); it('should request the _source field', () => { - init({ field: '_source' }); - expect(aggDsl.top_hits._source).toBeTruthy(); - expect(aggDsl.top_hits.docvalue_fields).toBeUndefined(); + init({ fieldName: '_source' }); + expect(aggDsl.top_hits._source).toBe(true); + expect(aggDsl.top_hits.fields).toBeUndefined(); }); - it('requests both source and docvalues_fields for non-text aggregatable fields', () => { + it('requests fields for non-text aggregatable fields', () => { init({ fieldName: 'bytes', readFromDocValues: true }); - expect(aggDsl.top_hits._source).toBe('bytes'); - expect(aggDsl.top_hits.docvalue_fields).toEqual([{ field: 'bytes' }]); + expect(aggDsl.top_hits._source).toBe(false); + expect(aggDsl.top_hits.fields).toEqual([{ field: 'bytes' }]); }); - it('requests both source and docvalues_fields for date aggregatable fields', () => { + it('requests fields for date aggregatable fields', () => { init({ fieldName: '@timestamp', readFromDocValues: true, fieldType: KBN_FIELD_TYPES.DATE }); - expect(aggDsl.top_hits._source).toBe('@timestamp'); - expect(aggDsl.top_hits.docvalue_fields).toEqual([{ field: '@timestamp', format: 'date_time' }]); + expect(aggDsl.top_hits._source).toBe(false); + expect(aggDsl.top_hits.fields).toEqual([{ field: '@timestamp', format: 'date_time' }]); }); - it('requests just source for aggregatable text fields', () => { + it('requests fields for aggregatable text fields', () => { init({ fieldName: 'machine.os' }); - expect(aggDsl.top_hits._source).toBe('machine.os'); - expect(aggDsl.top_hits.docvalue_fields).toBeUndefined(); + expect(aggDsl.top_hits._source).toBe(false); + expect(aggDsl.top_hits.fields).toEqual([{ field: 'machine.os' }]); }); describe('try to get the value from the top hit', () => { diff --git a/src/plugins/data/common/search/aggs/metrics/top_hit.ts b/src/plugins/data/common/search/aggs/metrics/top_hit.ts index 094b5cda9a46d..a4bd99d6b210d 100644 --- a/src/plugins/data/common/search/aggs/metrics/top_hit.ts +++ b/src/plugins/data/common/search/aggs/metrics/top_hit.ts @@ -78,8 +78,8 @@ export const getTopHitMetricAgg = () => { }, }; } else { - if (field.readFromDocValues) { - output.params.docvalue_fields = [ + if (field.name !== '_source') { + output.params.fields = [ { field: field.name, // always format date fields as date_time to avoid @@ -89,7 +89,7 @@ export const getTopHitMetricAgg = () => { }, ]; } - output.params._source = field.name === '_source' ? true : field.name; + output.params._source = field.name === '_source'; } }, }, diff --git a/src/plugins/data/public/data_views/saved_objects_client_wrapper.test.ts b/src/plugins/data/public/data_views/saved_objects_client_wrapper.test.ts new file mode 100644 index 0000000000000..221a18ac7fab7 --- /dev/null +++ b/src/plugins/data/public/data_views/saved_objects_client_wrapper.test.ts @@ -0,0 +1,55 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { SavedObjectsClientPublicToCommon } from './saved_objects_client_wrapper'; +import { savedObjectsServiceMock } from 'src/core/public/mocks'; + +import { DataViewSavedObjectConflictError } from '../../common/data_views'; + +describe('SavedObjectsClientPublicToCommon', () => { + const soClient = savedObjectsServiceMock.createStartContract().client; + + test('get saved object - exactMatch', async () => { + const mockedSavedObject = { + version: 'abc', + }; + soClient.resolve = jest + .fn() + .mockResolvedValue({ outcome: 'exactMatch', saved_object: mockedSavedObject }); + const service = new SavedObjectsClientPublicToCommon(soClient); + const result = await service.get('index-pattern', '1'); + expect(result).toStrictEqual(mockedSavedObject); + }); + + test('get saved object - aliasMatch', async () => { + const mockedSavedObject = { + version: 'def', + }; + soClient.resolve = jest + .fn() + .mockResolvedValue({ outcome: 'aliasMatch', saved_object: mockedSavedObject }); + const service = new SavedObjectsClientPublicToCommon(soClient); + const result = await service.get('index-pattern', '1'); + expect(result).toStrictEqual(mockedSavedObject); + }); + + test('get saved object - conflict', async () => { + const mockedSavedObject = { + version: 'ghi', + }; + + soClient.resolve = jest + .fn() + .mockResolvedValue({ outcome: 'conflict', saved_object: mockedSavedObject }); + const service = new SavedObjectsClientPublicToCommon(soClient); + + await expect(service.get('index-pattern', '1')).rejects.toThrow( + DataViewSavedObjectConflictError + ); + }); +}); diff --git a/src/plugins/data/public/data_views/saved_objects_client_wrapper.ts b/src/plugins/data/public/data_views/saved_objects_client_wrapper.ts index c8e633c6ec878..0d497d1203e2f 100644 --- a/src/plugins/data/public/data_views/saved_objects_client_wrapper.ts +++ b/src/plugins/data/public/data_views/saved_objects_client_wrapper.ts @@ -12,9 +12,10 @@ import { SavedObjectsClientCommon, SavedObjectsClientCommonFindArgs, SavedObject, + DataViewSavedObjectConflictError, } from '../../common/data_views'; -type SOClient = Pick; +type SOClient = Pick; const simpleSavedObjectToSavedObject = (simpleSavedObject: SimpleSavedObject): SavedObject => ({ @@ -33,8 +34,11 @@ export class SavedObjectsClientPublicToCommon implements SavedObjectsClientCommo } async get(type: string, id: string) { - const response = await this.savedObjectClient.get(type, id); - return simpleSavedObjectToSavedObject(response); + const response = await this.savedObjectClient.resolve(type, id); + if (response.outcome === 'conflict') { + throw new DataViewSavedObjectConflictError(id); + } + return simpleSavedObjectToSavedObject(response.saved_object); } async update( type: string, diff --git a/src/plugins/data/public/data_views/ui_settings_wrapper.ts b/src/plugins/data/public/data_views/ui_settings_wrapper.ts index e0998ed72b2e6..f8ae317391fa3 100644 --- a/src/plugins/data/public/data_views/ui_settings_wrapper.ts +++ b/src/plugins/data/public/data_views/ui_settings_wrapper.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { IUiSettingsClient } from 'src/core/public'; +import { IUiSettingsClient, PublicUiSettingsParams, UserProvidedValues } from 'src/core/public'; import { UiSettingsCommon } from '../../common'; export class UiSettingsPublicToCommon implements UiSettingsCommon { @@ -14,11 +14,11 @@ export class UiSettingsPublicToCommon implements UiSettingsCommon { constructor(uiSettings: IUiSettingsClient) { this.uiSettings = uiSettings; } - get(key: string) { + get(key: string): Promise { return Promise.resolve(this.uiSettings.get(key)); } - getAll() { + getAll(): Promise>> { return Promise.resolve(this.uiSettings.getAll()); } diff --git a/src/plugins/data/public/search/errors/es_error.test.tsx b/src/plugins/data/public/search/errors/es_error.test.tsx index fd1100ba34afc..4d1bc8b03b8f2 100644 --- a/src/plugins/data/public/search/errors/es_error.test.tsx +++ b/src/plugins/data/public/search/errors/es_error.test.tsx @@ -25,4 +25,31 @@ describe('EsError', () => { expect(typeof esError.attributes).toEqual('object'); expect(esError.attributes).toEqual(error.attributes); }); + + it('contains some explanation of the error in the message', () => { + // error taken from Vega's issue + const error = { + message: + 'x_content_parse_exception: [x_content_parse_exception] Reason: [1:78] [date_histogram] failed to parse field [calendar_interval]', + statusCode: 400, + attributes: { + root_cause: [ + { + type: 'x_content_parse_exception', + reason: '[1:78] [date_histogram] failed to parse field [calendar_interval]', + }, + ], + type: 'x_content_parse_exception', + reason: '[1:78] [date_histogram] failed to parse field [calendar_interval]', + caused_by: { + type: 'illegal_argument_exception', + reason: 'The supplied interval [2q] could not be parsed as a calendar interval.', + }, + }, + } as any; + const esError = new EsError(error); + expect(esError.message).toEqual( + 'EsError: The supplied interval [2q] could not be parsed as a calendar interval.' + ); + }); }); diff --git a/src/plugins/data/public/search/errors/es_error.tsx b/src/plugins/data/public/search/errors/es_error.tsx index 3303d48bf2adb..71c11af48830f 100644 --- a/src/plugins/data/public/search/errors/es_error.tsx +++ b/src/plugins/data/public/search/errors/es_error.tsx @@ -9,6 +9,7 @@ import React from 'react'; import { EuiCodeBlock, EuiSpacer } from '@elastic/eui'; import { ApplicationStart } from 'kibana/public'; +import { i18n } from '@kbn/i18n'; import { KbnError } from '../../../../kibana_utils/common'; import { IEsError } from './types'; import { getRootCause } from './utils'; @@ -17,7 +18,12 @@ export class EsError extends KbnError { readonly attributes: IEsError['attributes']; constructor(protected readonly err: IEsError) { - super('EsError'); + super( + `EsError: ${ + getRootCause(err)?.reason || + i18n.translate('data.esError.unknownRootCause', { defaultMessage: 'unknown' }) + }` + ); this.attributes = err.attributes; } diff --git a/src/plugins/data/public/search/errors/utils.ts b/src/plugins/data/public/search/errors/utils.ts index aba4e965d64c8..cb3e83dc8001c 100644 --- a/src/plugins/data/public/search/errors/utils.ts +++ b/src/plugins/data/public/search/errors/utils.ts @@ -5,8 +5,8 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ - -import { FailedShard } from './types'; +import type { ErrorCause } from '@elastic/elasticsearch/api/types'; +import type { FailedShard, Reason } from './types'; import { KibanaServerError } from '../../../../kibana_utils/common'; export function getFailedShards(err: KibanaServerError): FailedShard | undefined { @@ -15,6 +15,16 @@ export function getFailedShards(err: KibanaServerError): FailedShard | unde return failedShards ? failedShards[0] : undefined; } +function getNestedCause(err: KibanaServerError | ErrorCause): Reason { + const attr = ((err as KibanaServerError).attributes || err) as ErrorCause; + const { type, reason, caused_by: causedBy } = attr; + if (causedBy) { + return getNestedCause(causedBy); + } + return { type, reason }; +} + export function getRootCause(err: KibanaServerError) { - return getFailedShards(err)?.reason; + // Give shard failures priority, then try to get the error navigating nested objects + return getFailedShards(err)?.reason || getNestedCause(err); } diff --git a/src/plugins/data/public/search/search_interceptor/search_interceptor.test.ts b/src/plugins/data/public/search/search_interceptor/search_interceptor.test.ts index 155638250a2a4..7186938816d5f 100644 --- a/src/plugins/data/public/search/search_interceptor/search_interceptor.test.ts +++ b/src/plugins/data/public/search/search_interceptor/search_interceptor.test.ts @@ -501,12 +501,12 @@ describe('SearchInterceptor', () => { opts: { isRestore?: boolean; isStored?: boolean; - sessionId: string; + sessionId?: string; } | null ) => { const sessionServiceMock = sessionService as jest.Mocked; sessionServiceMock.getSearchOptions.mockImplementation(() => - opts + opts && opts.sessionId ? { sessionId: opts.sessionId, isRestore: opts.isRestore ?? false, @@ -515,6 +515,7 @@ describe('SearchInterceptor', () => { : null ); sessionServiceMock.isRestore.mockReturnValue(!!opts?.isRestore); + sessionServiceMock.getSessionId.mockImplementation(() => opts?.sessionId); fetchMock.mockResolvedValue({ result: 200 }); }; @@ -606,6 +607,41 @@ describe('SearchInterceptor', () => { expect(SearchSessionIncompleteWarning).toBeCalledTimes(0); }); + test('should not show warning if a search outside of session is running', async () => { + setup({ + isRestore: false, + isStored: false, + }); + + const responses = [ + { + time: 10, + value: { + isPartial: false, + isRunning: false, + isRestored: false, + id: 1, + rawResponse: { + took: 1, + }, + }, + }, + ]; + mockFetchImplementation(responses); + + const response = searchInterceptor.search( + {}, + { + sessionId: undefined, + } + ); + response.subscribe({ next, error, complete }); + + await timeTravel(10); + + expect(SearchSessionIncompleteWarning).toBeCalledTimes(0); + }); + test('should show warning once if a search is not available during restore', async () => { setup({ isRestore: true, diff --git a/src/plugins/data/public/search/search_interceptor/search_interceptor.ts b/src/plugins/data/public/search/search_interceptor/search_interceptor.ts index ff3c173fd18cf..180e826b5bc4e 100644 --- a/src/plugins/data/public/search/search_interceptor/search_interceptor.ts +++ b/src/plugins/data/public/search/search_interceptor/search_interceptor.ts @@ -352,8 +352,14 @@ export class SearchInterceptor { ); }), tap((response) => { - if (this.deps.session.isRestore() && response.isRestored === false) { - this.showRestoreWarning(this.deps.session.getSessionId()); + const isSearchInScopeOfSession = + sessionId && sessionId === this.deps.session.getSessionId(); + if ( + isSearchInScopeOfSession && + this.deps.session.isRestore() && + response.isRestored === false + ) { + this.showRestoreWarning(sessionId); } }), finalize(() => { diff --git a/src/plugins/data/server/data_views/routes.ts b/src/plugins/data/server/data_views/routes.ts index 32fa50940bca7..9488285fc7e2c 100644 --- a/src/plugins/data/server/data_views/routes.ts +++ b/src/plugins/data/server/data_views/routes.ts @@ -7,7 +7,7 @@ */ import { schema } from '@kbn/config-schema'; -import { HttpServiceSetup, RequestHandlerContext, StartServicesAccessor } from 'kibana/server'; +import { HttpServiceSetup, StartServicesAccessor } from 'kibana/server'; import { IndexPatternsFetcher } from './fetcher'; import { registerCreateIndexPatternRoute } from './routes/create_index_pattern'; import { registerGetIndexPatternRoute } from './routes/get_index_pattern'; @@ -154,7 +154,7 @@ export function registerRoutes( }), }, }, - async (context: RequestHandlerContext, request: any, response: any) => { + async (context, request, response) => { const { asCurrentUser } = context.core.elasticsearch.client; const indexPatterns = new IndexPatternsFetcher(asCurrentUser); const { pattern, interval, look_back: lookBack, meta_fields: metaFields } = request.query; diff --git a/src/plugins/data/server/data_views/saved_objects_client_wrapper.test.ts b/src/plugins/data/server/data_views/saved_objects_client_wrapper.test.ts new file mode 100644 index 0000000000000..bbe857894b3f0 --- /dev/null +++ b/src/plugins/data/server/data_views/saved_objects_client_wrapper.test.ts @@ -0,0 +1,55 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { SavedObjectsClientServerToCommon } from './saved_objects_client_wrapper'; +import { SavedObjectsClientContract } from 'src/core/server'; + +import { DataViewSavedObjectConflictError } from '../../common/data_views'; + +describe('SavedObjectsClientPublicToCommon', () => { + const soClient = { resolve: jest.fn() } as unknown as SavedObjectsClientContract; + + test('get saved object - exactMatch', async () => { + const mockedSavedObject = { + version: 'abc', + }; + soClient.resolve = jest + .fn() + .mockResolvedValue({ outcome: 'exactMatch', saved_object: mockedSavedObject }); + const service = new SavedObjectsClientServerToCommon(soClient); + const result = await service.get('index-pattern', '1'); + expect(result).toStrictEqual(mockedSavedObject); + }); + + test('get saved object - aliasMatch', async () => { + const mockedSavedObject = { + version: 'def', + }; + soClient.resolve = jest + .fn() + .mockResolvedValue({ outcome: 'aliasMatch', saved_object: mockedSavedObject }); + const service = new SavedObjectsClientServerToCommon(soClient); + const result = await service.get('index-pattern', '1'); + expect(result).toStrictEqual(mockedSavedObject); + }); + + test('get saved object - conflict', async () => { + const mockedSavedObject = { + version: 'ghi', + }; + + soClient.resolve = jest + .fn() + .mockResolvedValue({ outcome: 'conflict', saved_object: mockedSavedObject }); + const service = new SavedObjectsClientServerToCommon(soClient); + + await expect(service.get('index-pattern', '1')).rejects.toThrow( + DataViewSavedObjectConflictError + ); + }); +}); diff --git a/src/plugins/data/server/data_views/saved_objects_client_wrapper.ts b/src/plugins/data/server/data_views/saved_objects_client_wrapper.ts index 034f59aa52568..22024cfad9057 100644 --- a/src/plugins/data/server/data_views/saved_objects_client_wrapper.ts +++ b/src/plugins/data/server/data_views/saved_objects_client_wrapper.ts @@ -10,6 +10,7 @@ import { SavedObjectsClientContract, SavedObject } from 'src/core/server'; import { SavedObjectsClientCommon, SavedObjectsClientCommonFindArgs, + DataViewSavedObjectConflictError, } from '../../common/data_views'; export class SavedObjectsClientServerToCommon implements SavedObjectsClientCommon { @@ -23,7 +24,11 @@ export class SavedObjectsClientServerToCommon implements SavedObjectsClientCommo } async get(type: string, id: string) { - return await this.savedObjectClient.get(type, id); + const response = await this.savedObjectClient.resolve(type, id); + if (response.outcome === 'conflict') { + throw new DataViewSavedObjectConflictError(id); + } + return response.saved_object; } async update( type: string, diff --git a/src/plugins/data/server/data_views/ui_settings_wrapper.ts b/src/plugins/data/server/data_views/ui_settings_wrapper.ts index 3b00aab7d6bdd..dce552205db2e 100644 --- a/src/plugins/data/server/data_views/ui_settings_wrapper.ts +++ b/src/plugins/data/server/data_views/ui_settings_wrapper.ts @@ -14,11 +14,11 @@ export class UiSettingsServerToCommon implements UiSettingsCommon { constructor(uiSettings: IUiSettingsClient) { this.uiSettings = uiSettings; } - get(key: string) { + get(key: string): Promise { return this.uiSettings.get(key); } - getAll() { + getAll(): Promise> { return this.uiSettings.getAll(); } diff --git a/src/plugins/data/server/index.ts b/src/plugins/data/server/index.ts index 9d2e94bcf15c0..a17c66c694b2d 100644 --- a/src/plugins/data/server/index.ts +++ b/src/plugins/data/server/index.ts @@ -54,6 +54,7 @@ export { IndexPattern, IndexPatternsService, IndexPatternsService as IndexPatternsCommonService, + DataView, } from '../common'; /** diff --git a/src/plugins/data/server/search/strategies/eql_search/eql_search_strategy.test.ts b/src/plugins/data/server/search/strategies/eql_search/eql_search_strategy.test.ts index 58a5e875f7c93..d32080928d630 100644 --- a/src/plugins/data/server/search/strategies/eql_search/eql_search_strategy.test.ts +++ b/src/plugins/data/server/search/strategies/eql_search/eql_search_strategy.test.ts @@ -132,7 +132,6 @@ describe('EQL search strategy', () => { expect(request).toEqual( expect.objectContaining({ ignore_unavailable: true, - ignore_throttled: true, }) ); }); diff --git a/src/plugins/data/server/search/strategies/ese_search/request_utils.test.ts b/src/plugins/data/server/search/strategies/ese_search/request_utils.test.ts index 272e41e8bf82d..91b323de7c07b 100644 --- a/src/plugins/data/server/search/strategies/ese_search/request_utils.test.ts +++ b/src/plugins/data/server/search/strategies/ese_search/request_utils.test.ts @@ -31,12 +31,12 @@ const getMockSearchSessionsConfig = ({ describe('request utils', () => { describe('getIgnoreThrottled', () => { - test('returns `ignore_throttled` as `true` when `includeFrozen` is `false`', async () => { + test('does not return `ignore_throttled` when `includeFrozen` is `false`', async () => { const mockUiSettingsClient = getMockUiSettingsClient({ [UI_SETTINGS.SEARCH_INCLUDE_FROZEN]: false, }); const result = await getIgnoreThrottled(mockUiSettingsClient); - expect(result.ignore_throttled).toBe(true); + expect(result).not.toHaveProperty('ignore_throttled'); }); test('returns `ignore_throttled` as `false` when `includeFrozen` is `true`', async () => { diff --git a/src/plugins/data/server/search/strategies/ese_search/request_utils.ts b/src/plugins/data/server/search/strategies/ese_search/request_utils.ts index 8bf4473355ccf..e224215571ca9 100644 --- a/src/plugins/data/server/search/strategies/ese_search/request_utils.ts +++ b/src/plugins/data/server/search/strategies/ese_search/request_utils.ts @@ -23,7 +23,7 @@ export async function getIgnoreThrottled( uiSettingsClient: IUiSettingsClient ): Promise> { const includeFrozen = await uiSettingsClient.get(UI_SETTINGS.SEARCH_INCLUDE_FROZEN); - return { ignore_throttled: !includeFrozen }; + return includeFrozen ? { ignore_throttled: false } : {}; } /** diff --git a/src/plugins/discover/public/application/services/use_es_doc_search.ts b/src/plugins/discover/public/application/services/use_es_doc_search.ts index c5216c483fd10..a2f0cd6f8442b 100644 --- a/src/plugins/discover/public/application/services/use_es_doc_search.ts +++ b/src/plugins/discover/public/application/services/use_es_doc_search.ts @@ -37,7 +37,7 @@ export function buildSearchBody( }, }, stored_fields: computedFields.storedFields, - script_fields: computedFields.scriptFields as Record, + script_fields: computedFields.scriptFields, version: true, }, }; diff --git a/src/plugins/es_ui_shared/__packages_do_not_import__/authorization/components/authorization_provider.tsx b/src/plugins/es_ui_shared/__packages_do_not_import__/authorization/components/authorization_provider.tsx index be49a7697afca..7d8d00156e541 100644 --- a/src/plugins/es_ui_shared/__packages_do_not_import__/authorization/components/authorization_provider.tsx +++ b/src/plugins/es_ui_shared/__packages_do_not_import__/authorization/components/authorization_provider.tsx @@ -13,7 +13,7 @@ import { useRequest } from '../../../public/request'; import { Privileges, Error as CustomError } from '../types'; -interface Authorization { +export interface Authorization { isLoading: boolean; apiError: CustomError | null; privileges: Privileges; diff --git a/src/plugins/es_ui_shared/__packages_do_not_import__/authorization/components/index.ts b/src/plugins/es_ui_shared/__packages_do_not_import__/authorization/components/index.ts index f8eb7e3c7c0c8..75d79a204f141 100644 --- a/src/plugins/es_ui_shared/__packages_do_not_import__/authorization/components/index.ts +++ b/src/plugins/es_ui_shared/__packages_do_not_import__/authorization/components/index.ts @@ -10,6 +10,7 @@ export { AuthorizationProvider, AuthorizationContext, useAuthorizationContext, + Authorization, } from './authorization_provider'; export { WithPrivileges } from './with_privileges'; diff --git a/src/plugins/es_ui_shared/__packages_do_not_import__/authorization/components/with_privileges.test.ts b/src/plugins/es_ui_shared/__packages_do_not_import__/authorization/components/with_privileges.test.ts new file mode 100644 index 0000000000000..243bfdb995f5d --- /dev/null +++ b/src/plugins/es_ui_shared/__packages_do_not_import__/authorization/components/with_privileges.test.ts @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { convertPrivilegesToArray } from './with_privileges'; + +describe('convertPrivilegesToArray', () => { + test('extracts section and privilege', () => { + expect(convertPrivilegesToArray('index.index_name')).toEqual([['index', 'index_name']]); + expect(convertPrivilegesToArray(['index.index_name', 'cluster.management'])).toEqual([ + ['index', 'index_name'], + ['cluster', 'management'], + ]); + expect(convertPrivilegesToArray('index.index_name.with-many.dots')).toEqual([ + ['index', 'index_name.with-many.dots'], + ]); + }); + + test('throws when it cannot extract section and privilege', () => { + expect(() => { + convertPrivilegesToArray('bad_privilege_string'); + }).toThrow('Required privilege must have the format "section.privilege"'); + }); +}); diff --git a/src/plugins/es_ui_shared/__packages_do_not_import__/authorization/components/with_privileges.tsx b/src/plugins/es_ui_shared/__packages_do_not_import__/authorization/components/with_privileges.tsx index c0e675877c562..6485bd7f45e55 100644 --- a/src/plugins/es_ui_shared/__packages_do_not_import__/authorization/components/with_privileges.tsx +++ b/src/plugins/es_ui_shared/__packages_do_not_import__/authorization/components/with_privileges.tsx @@ -10,13 +10,14 @@ import { MissingPrivileges } from '../types'; import { useAuthorizationContext } from './authorization_provider'; +type Privileges = string | string[]; interface Props { /** * Each required privilege must have the format "section.privilege". * To indicate that *all* privileges from a section are required, we can use the asterix * e.g. "index.*" */ - privileges: string | string[]; + privileges: Privileges; children: (childrenProps: { isLoading: boolean; hasPrivileges: boolean; @@ -26,24 +27,30 @@ interface Props { type Privilege = [string, string]; -const toArray = (value: string | string[]): string[] => +const toArray = (value: Privileges): string[] => Array.isArray(value) ? (value as string[]) : ([value] as string[]); -export const WithPrivileges = ({ privileges: requiredPrivileges, children }: Props) => { - const { isLoading, privileges } = useAuthorizationContext(); - - const privilegesToArray: Privilege[] = toArray(requiredPrivileges).map((p) => { - const [section, privilege] = p.split('.'); - if (!privilege) { - // Oh! we forgot to use the dot "." notation. +export const convertPrivilegesToArray = (privileges: Privileges): Privilege[] => { + return toArray(privileges).map((p) => { + // Since an privilege can contain a dot in its name: + // * `section` needs to be extracted from the beginning of the string until the first dot + // * `privilege` should be everything after the dot + const indexOfFirstPeriod = p.indexOf('.'); + if (indexOfFirstPeriod === -1) { throw new Error('Required privilege must have the format "section.privilege"'); } - return [section, privilege]; + + return [p.slice(0, indexOfFirstPeriod), p.slice(indexOfFirstPeriod + 1)]; }); +}; + +export const WithPrivileges = ({ privileges: requiredPrivileges, children }: Props) => { + const { isLoading, privileges } = useAuthorizationContext(); + const privilegesArray = convertPrivilegesToArray(requiredPrivileges); const hasPrivileges = isLoading ? false - : privilegesToArray.every((privilege) => { + : privilegesArray.every((privilege) => { const [section, requiredPrivilege] = privilege; if (!privileges.missingPrivileges[section]) { // if the section does not exist in our missingPriviledges, everything is OK @@ -61,7 +68,7 @@ export const WithPrivileges = ({ privileges: requiredPrivileges, children }: Pro return !privileges.missingPrivileges[section]!.includes(requiredPrivilege); }); - const privilegesMissing = privilegesToArray.reduce((acc, [section, privilege]) => { + const privilegesMissing = privilegesArray.reduce((acc, [section, privilege]) => { if (privilege === '*') { acc[section] = privileges.missingPrivileges[section] || []; } else if ( diff --git a/src/plugins/es_ui_shared/__packages_do_not_import__/authorization/index.ts b/src/plugins/es_ui_shared/__packages_do_not_import__/authorization/index.ts index e63d98512a2cd..9ccbc5a5cd3df 100644 --- a/src/plugins/es_ui_shared/__packages_do_not_import__/authorization/index.ts +++ b/src/plugins/es_ui_shared/__packages_do_not_import__/authorization/index.ts @@ -14,6 +14,7 @@ export { SectionError, PageError, useAuthorizationContext, + Authorization, } from './components'; export { Privileges, MissingPrivileges, Error } from './types'; diff --git a/src/plugins/es_ui_shared/common/index.ts b/src/plugins/es_ui_shared/common/index.ts index b8cfe0ae48585..1c2955b8e5e28 100644 --- a/src/plugins/es_ui_shared/common/index.ts +++ b/src/plugins/es_ui_shared/common/index.ts @@ -6,4 +6,4 @@ * Side Public License, v 1. */ -export { Privileges, MissingPrivileges } from '../__packages_do_not_import__/authorization'; +export { Privileges, MissingPrivileges } from '../__packages_do_not_import__/authorization/types'; diff --git a/src/plugins/es_ui_shared/public/authorization/index.ts b/src/plugins/es_ui_shared/public/authorization/index.ts index f68ad3da2a4b5..b8fb2f45794ee 100644 --- a/src/plugins/es_ui_shared/public/authorization/index.ts +++ b/src/plugins/es_ui_shared/public/authorization/index.ts @@ -17,4 +17,5 @@ export { PageError, useAuthorizationContext, WithPrivileges, + Authorization, } from '../../__packages_do_not_import__/authorization'; diff --git a/src/plugins/es_ui_shared/public/index.ts b/src/plugins/es_ui_shared/public/index.ts index 9db00bc4be8df..2dc50536ca631 100644 --- a/src/plugins/es_ui_shared/public/index.ts +++ b/src/plugins/es_ui_shared/public/index.ts @@ -45,6 +45,7 @@ export { PageError, Error, useAuthorizationContext, + Authorization, } from './authorization'; export { Forms, ace, GlobalFlyout, XJson }; diff --git a/src/plugins/expressions/common/execution/execution.ts b/src/plugins/expressions/common/execution/execution.ts index 0c4185c82dc3c..0bb12951202a5 100644 --- a/src/plugins/expressions/common/execution/execution.ts +++ b/src/plugins/expressions/common/execution/execution.ts @@ -180,7 +180,7 @@ export class Execution< const ast = execution.ast || parseExpression(this.expression); this.state = createExecutionContainer({ - ...executor.state.get(), + ...executor.state, state: 'not-started', ast, }); diff --git a/src/plugins/expressions/common/executor/executor.ts b/src/plugins/expressions/common/executor/executor.ts index 55d3a7b897864..ce411ea94eafe 100644 --- a/src/plugins/expressions/common/executor/executor.ts +++ b/src/plugins/expressions/common/executor/executor.ts @@ -49,7 +49,7 @@ export class TypesRegistry implements IRegistry { } public get(id: string): ExpressionType | null { - return this.executor.state.selectors.getType(id); + return this.executor.getType(id) ?? null; } public toJS(): Record { @@ -71,7 +71,7 @@ export class FunctionsRegistry implements IRegistry { } public get(id: string): ExpressionFunction | null { - return this.executor.state.selectors.getFunction(id); + return this.executor.getFunction(id) ?? null; } public toJS(): Record { @@ -95,22 +95,44 @@ export class Executor = Record; + public readonly container: ExecutorContainer; /** * @deprecated */ - public readonly functions: FunctionsRegistry; + public readonly functions = new FunctionsRegistry(this); /** * @deprecated */ - public readonly types: TypesRegistry; + public readonly types = new TypesRegistry(this); + + protected parent?: Executor; constructor(state?: ExecutorState) { - this.state = createExecutorContainer(state); - this.functions = new FunctionsRegistry(this); - this.types = new TypesRegistry(this); + this.container = createExecutorContainer(state); + } + + public get state(): ExecutorState { + const parent = this.parent?.state; + const state = this.container.get(); + + return { + ...(parent ?? {}), + ...state, + types: { + ...(parent?.types ?? {}), + ...state.types, + }, + functions: { + ...(parent?.functions ?? {}), + ...state.functions, + }, + context: { + ...(parent?.context ?? {}), + ...state.context, + }, + }; } public registerFunction( @@ -119,15 +141,18 @@ export class Executor = Record { - return { ...this.state.get().functions }; + return { + ...(this.parent?.getFunctions() ?? {}), + ...this.container.get().functions, + }; } public registerType( @@ -136,23 +161,30 @@ export class Executor = Record { - return { ...this.state.get().types }; + return { + ...(this.parent?.getTypes() ?? {}), + ...this.container.get().types, + }; } public extendContext(extraContext: Record) { - this.state.transitions.extendContext(extraContext); + this.container.transitions.extendContext(extraContext); } public get context(): Record { - return this.state.selectors.getContext(); + return { + ...(this.parent?.context ?? {}), + ...this.container.selectors.getContext(), + }; } /** @@ -199,18 +231,15 @@ export class Executor = Record { - return asts.map((arg) => { - if (arg && typeof arg === 'object') { - return this.walkAst(arg, action); - } - return arg; - }); - }); + link.arguments = mapValues(fnArgs, (asts) => + asts.map((arg) => + arg != null && typeof arg === 'object' ? this.walkAst(arg, action) : arg + ) + ); action(fn, link); } @@ -275,39 +304,19 @@ export class Executor = Record { - if (!fn.migrations[version]) return link; - const updatedAst = fn.migrations[version](link) as ExpressionAstFunction; - link.arguments = updatedAst.arguments; - link.type = updatedAst.type; + if (!fn.migrations[version]) { + return; + } + + ({ arguments: link.arguments, type: link.type } = fn.migrations[version]( + link + ) as ExpressionAstFunction); }); } public fork(): Executor { - const initialState = this.state.get(); - const fork = new Executor(initialState); - - /** - * Synchronize registry state - make any new types, functions and context - * also available in the forked instance of `Executor`. - */ - this.state.state$.subscribe(({ types, functions, context }) => { - const state = fork.state.get(); - fork.state.set({ - ...state, - types: { - ...types, - ...state.types, - }, - functions: { - ...functions, - ...state.functions, - }, - context: { - ...context, - ...state.context, - }, - }); - }); + const fork = new Executor(); + fork.parent = this; return fork; } diff --git a/src/plugins/expressions/common/service/expressions_services.test.ts b/src/plugins/expressions/common/service/expressions_services.test.ts index db73d300e1273..620917dc64d4d 100644 --- a/src/plugins/expressions/common/service/expressions_services.test.ts +++ b/src/plugins/expressions/common/service/expressions_services.test.ts @@ -17,11 +17,16 @@ describe('ExpressionsService', () => { const expressions = new ExpressionsService(); expect(expressions.setup()).toMatchObject({ + getFunction: expect.any(Function), getFunctions: expect.any(Function), + getRenderer: expect.any(Function), + getRenderers: expect.any(Function), + getType: expect.any(Function), + getTypes: expect.any(Function), registerFunction: expect.any(Function), registerType: expect.any(Function), registerRenderer: expect.any(Function), - run: expect.any(Function), + fork: expect.any(Function), }); }); @@ -30,7 +35,16 @@ describe('ExpressionsService', () => { expressions.setup(); expect(expressions.start()).toMatchObject({ + getFunction: expect.any(Function), getFunctions: expect.any(Function), + getRenderer: expect.any(Function), + getRenderers: expect.any(Function), + getType: expect.any(Function), + getTypes: expect.any(Function), + registerFunction: expect.any(Function), + registerType: expect.any(Function), + registerRenderer: expect.any(Function), + execute: expect.any(Function), run: expect.any(Function), }); }); @@ -54,21 +68,21 @@ describe('ExpressionsService', () => { const service = new ExpressionsService(); const fork = service.fork(); - expect(fork.executor.state.get().types).toEqual(service.executor.state.get().types); + expect(fork.getTypes()).toEqual(service.getTypes()); }); test('fork keeps all functions of the origin service', () => { const service = new ExpressionsService(); const fork = service.fork(); - expect(fork.executor.state.get().functions).toEqual(service.executor.state.get().functions); + expect(fork.getFunctions()).toEqual(service.getFunctions()); }); test('fork keeps context of the origin service', () => { const service = new ExpressionsService(); const fork = service.fork(); - expect(fork.executor.state.get().context).toEqual(service.executor.state.get().context); + expect(fork.executor.state.context).toEqual(service.executor.state.context); }); test('newly registered functions in origin are also available in fork', () => { @@ -82,7 +96,7 @@ describe('ExpressionsService', () => { fn: () => {}, }); - expect(fork.executor.state.get().functions).toEqual(service.executor.state.get().functions); + expect(fork.getFunctions()).toEqual(service.getFunctions()); }); test('newly registered functions in fork are NOT available in origin', () => { @@ -96,14 +110,15 @@ describe('ExpressionsService', () => { fn: () => {}, }); - expect(Object.values(fork.executor.state.get().functions)).toHaveLength( - Object.values(service.executor.state.get().functions).length + 1 + expect(Object.values(fork.getFunctions())).toHaveLength( + Object.values(service.getFunctions()).length + 1 ); }); test('fork can execute an expression with newly registered function', async () => { const service = new ExpressionsService(); const fork = service.fork(); + fork.start(); service.registerFunction({ name: '__test__', @@ -118,5 +133,28 @@ describe('ExpressionsService', () => { expect(result).toBe('123'); }); + + test('throw on fork if the service is already started', async () => { + const service = new ExpressionsService(); + service.start(); + + expect(() => service.fork()).toThrow(); + }); + }); + + describe('.execute()', () => { + test('throw if the service is not started', () => { + const expressions = new ExpressionsService(); + + expect(() => expressions.execute('foo', null)).toThrow(); + }); + }); + + describe('.run()', () => { + test('throw if the service is not started', () => { + const expressions = new ExpressionsService(); + + expect(() => expressions.run('foo', null)).toThrow(); + }); }); }); diff --git a/src/plugins/expressions/common/service/expressions_services.ts b/src/plugins/expressions/common/service/expressions_services.ts index 2be4f5207bb82..f21eaa34d7868 100644 --- a/src/plugins/expressions/common/service/expressions_services.ts +++ b/src/plugins/expressions/common/service/expressions_services.ts @@ -41,22 +41,86 @@ import { * The public contract that `ExpressionsService` provides to other plugins * in Kibana Platform in *setup* life-cycle. */ -export type ExpressionsServiceSetup = Pick< - ExpressionsService, - | 'getFunction' - | 'getFunctions' - | 'getRenderer' - | 'getRenderers' - | 'getType' - | 'getTypes' - | 'registerFunction' - | 'registerRenderer' - | 'registerType' - | 'run' - | 'fork' - | 'extract' - | 'inject' ->; +export interface ExpressionsServiceSetup { + /** + * Get a registered `ExpressionFunction` by its name, which was registered + * using the `registerFunction` method. The returned `ExpressionFunction` + * instance is an internal representation of the function in Expressions + * service - do not mutate that object. + * @deprecated Use start contract instead. + */ + getFunction(name: string): ReturnType; + + /** + * Returns POJO map of all registered expression functions, where keys are + * names of the functions and values are `ExpressionFunction` instances. + * @deprecated Use start contract instead. + */ + getFunctions(): ReturnType; + + /** + * Returns POJO map of all registered expression types, where keys are + * names of the types and values are `ExpressionType` instances. + * @deprecated Use start contract instead. + */ + getTypes(): ReturnType; + + /** + * Create a new instance of `ExpressionsService`. The new instance inherits + * all state of the original `ExpressionsService`, including all expression + * types, expression functions and context. Also, all new types and functions + * registered in the original services AFTER the forking event will be + * available in the forked instance. However, all new types and functions + * registered in the forked instances will NOT be available to the original + * service. + * @param name A fork name that can be used to get fork instance later. + */ + fork(name?: string): ExpressionsService; + + /** + * Register an expression function, which will be possible to execute as + * part of the expression pipeline. + * + * Below we register a function which simply sleeps for given number of + * milliseconds to delay the execution and outputs its input as-is. + * + * ```ts + * expressions.registerFunction({ + * name: 'sleep', + * args: { + * time: { + * aliases: ['_'], + * help: 'Time in milliseconds for how long to sleep', + * types: ['number'], + * }, + * }, + * help: '', + * fn: async (input, args, context) => { + * await new Promise(r => setTimeout(r, args.time)); + * return input; + * }, + * } + * ``` + * + * The actual function is defined in the `fn` key. The function can be *async*. + * It receives three arguments: (1) `input` is the output of the previous function + * or the initial input of the expression if the function is first in chain; + * (2) `args` are function arguments as defined in expression string, that can + * be edited by user (e.g in case of Canvas); (3) `context` is a shared object + * passed to all functions that can be used for side-effects. + */ + registerFunction( + functionDefinition: AnyExpressionFunctionDefinition | (() => AnyExpressionFunctionDefinition) + ): void; + + registerType( + typeDefinition: AnyExpressionTypeDefinition | (() => AnyExpressionTypeDefinition) + ): void; + + registerRenderer( + definition: AnyExpressionRenderDefinition | (() => AnyExpressionRenderDefinition) + ): void; +} export interface ExpressionExecutionParams { searchContext?: SerializableRecord; @@ -97,7 +161,13 @@ export interface ExpressionsServiceStart { * instance is an internal representation of the function in Expressions * service - do not mutate that object. */ - getFunction: (name: string) => ReturnType; + getFunction(name: string): ReturnType; + + /** + * Returns POJO map of all registered expression functions, where keys are + * names of the functions and values are `ExpressionFunction` instances. + */ + getFunctions(): ReturnType; /** * Get a registered `ExpressionRenderer` by its name, which was registered @@ -105,7 +175,13 @@ export interface ExpressionsServiceStart { * instance is an internal representation of the renderer in Expressions * service - do not mutate that object. */ - getRenderer: (name: string) => ReturnType; + getRenderer(name: string): ReturnType; + + /** + * Returns POJO map of all registered expression renderers, where keys are + * names of the renderers and values are `ExpressionRenderer` instances. + */ + getRenderers(): ReturnType; /** * Get a registered `ExpressionType` by its name, which was registered @@ -113,7 +189,13 @@ export interface ExpressionsServiceStart { * instance is an internal representation of the type in Expressions * service - do not mutate that object. */ - getType: (name: string) => ReturnType; + getType(name: string): ReturnType; + + /** + * Returns POJO map of all registered expression types, where keys are + * names of the types and values are `ExpressionType` instances. + */ + getTypes(): ReturnType; /** * Executes expression string or a parsed expression AST and immediately @@ -139,34 +221,23 @@ export interface ExpressionsServiceStart { * expressions.run('...', null, { elasticsearchClient }); * ``` */ - run: ( + run( ast: string | ExpressionAstExpression, input: Input, params?: ExpressionExecutionParams - ) => Observable>; + ): Observable>; /** * Starts expression execution and immediately returns `ExecutionContract` * instance that tracks the progress of the execution and can be used to * interact with the execution. */ - execute: ( + execute( ast: string | ExpressionAstExpression, // This any is for legacy reasons. input: Input, params?: ExpressionExecutionParams - ) => ExecutionContract; - - /** - * Create a new instance of `ExpressionsService`. The new instance inherits - * all state of the original `ExpressionsService`, including all expression - * types, expression functions and context. Also, all new types and functions - * registered in the original services AFTER the forking event will be - * available in the forked instance. However, all new types and functions - * registered in the forked instances will NOT be available to the original - * service. - */ - fork: () => ExpressionsService; + ): ExecutionContract; } export interface ExpressionServiceParams { @@ -193,7 +264,19 @@ export interface ExpressionServiceParams { * * so that JSDoc appears in developers IDE when they use those `plugins.expressions.registerFunction(`. */ -export class ExpressionsService implements PersistableStateService { +export class ExpressionsService + implements + PersistableStateService, + ExpressionsServiceSetup, + ExpressionsServiceStart +{ + /** + * @note Workaround since the expressions service is frozen. + */ + private static started = new WeakSet(); + private children = new Map(); + private parent?: ExpressionsService; + public readonly executor: Executor; public readonly renderers: ExpressionRendererRegistry; @@ -205,94 +288,85 @@ export class ExpressionsService implements PersistableStateService { - * await new Promise(r => setTimeout(r, args.time)); - * return input; - * }, - * } - * ``` - * - * The actual function is defined in the `fn` key. The function can be *async*. - * It receives three arguments: (1) `input` is the output of the previous function - * or the initial input of the expression if the function is first in chain; - * (2) `args` are function arguments as defined in expression string, that can - * be edited by user (e.g in case of Canvas); (3) `context` is a shared object - * passed to all functions that can be used for side-effects. - */ - public readonly registerFunction = ( - functionDefinition: AnyExpressionFunctionDefinition | (() => AnyExpressionFunctionDefinition) - ): void => this.executor.registerFunction(functionDefinition); - - public readonly registerType = ( - typeDefinition: AnyExpressionTypeDefinition | (() => AnyExpressionTypeDefinition) - ): void => this.executor.registerType(typeDefinition); + private isStarted(): boolean { + return !!(ExpressionsService.started.has(this) || this.parent?.isStarted()); + } - public readonly registerRenderer = ( - definition: AnyExpressionRenderDefinition | (() => AnyExpressionRenderDefinition) - ): void => this.renderers.register(definition); + private assertSetup() { + if (this.isStarted()) { + throw new Error('The expression service is already started and can no longer be configured.'); + } + } - public readonly run: ExpressionsServiceStart['run'] = (ast, input, params) => - this.executor.run(ast, input, params); + private assertStart() { + if (!this.isStarted()) { + throw new Error('The expressions service has not started yet.'); + } + } public readonly getFunction: ExpressionsServiceStart['getFunction'] = (name) => this.executor.getFunction(name); - /** - * Returns POJO map of all registered expression functions, where keys are - * names of the functions and values are `ExpressionFunction` instances. - */ - public readonly getFunctions = (): ReturnType => + public readonly getFunctions: ExpressionsServiceStart['getFunctions'] = () => this.executor.getFunctions(); - public readonly getRenderer: ExpressionsServiceStart['getRenderer'] = (name) => - this.renderers.get(name); + public readonly getRenderer: ExpressionsServiceStart['getRenderer'] = (name) => { + this.assertStart(); - /** - * Returns POJO map of all registered expression renderers, where keys are - * names of the renderers and values are `ExpressionRenderer` instances. - */ - public readonly getRenderers = (): ReturnType => - this.renderers.toJS(); + return this.renderers.get(name); + }; - public readonly getType: ExpressionsServiceStart['getType'] = (name) => - this.executor.getType(name); + public readonly getRenderers: ExpressionsServiceStart['getRenderers'] = () => { + this.assertStart(); - /** - * Returns POJO map of all registered expression types, where keys are - * names of the types and values are `ExpressionType` instances. - */ - public readonly getTypes = (): ReturnType => this.executor.getTypes(); + return this.renderers.toJS(); + }; + + public readonly getType: ExpressionsServiceStart['getType'] = (name) => { + this.assertStart(); + + return this.executor.getType(name); + }; + + public readonly getTypes: ExpressionsServiceStart['getTypes'] = () => this.executor.getTypes(); + + public readonly registerFunction: ExpressionsServiceSetup['registerFunction'] = ( + functionDefinition + ) => this.executor.registerFunction(functionDefinition); + + public readonly registerType: ExpressionsServiceSetup['registerType'] = (typeDefinition) => + this.executor.registerType(typeDefinition); + + public readonly registerRenderer: ExpressionsServiceSetup['registerRenderer'] = (definition) => + this.renderers.register(definition); + + public readonly fork: ExpressionsServiceSetup['fork'] = (name) => { + this.assertSetup(); + + const executor = this.executor.fork(); + const renderers = this.renderers; + const fork = new (this.constructor as typeof ExpressionsService)({ executor, renderers }); + fork.parent = this; + + if (name) { + this.children.set(name, fork); + } + + return fork; + }; public readonly execute: ExpressionsServiceStart['execute'] = ((ast, input, params) => { + this.assertStart(); const execution = this.executor.createExecution(ast, params); execution.start(input); + return execution.contract; }) as ExpressionsServiceStart['execute']; - public readonly fork = () => { - const executor = this.executor.fork(); - const renderers = this.renderers; - const fork = new (this.constructor as typeof ExpressionsService)({ executor, renderers }); + public readonly run: ExpressionsServiceStart['run'] = (ast, input, params) => { + this.assertStart(); - return fork; + return this.executor.run(ast, input, params); }; /** @@ -371,8 +445,12 @@ export class ExpressionsService implements PersistableStateService { service.registerFunction(func); } + service.start(); + const moduleMock = { __execution: undefined, __getLastExecution: () => moduleMock.__execution, diff --git a/src/plugins/expressions/public/mocks.tsx b/src/plugins/expressions/public/mocks.tsx index 3a5450fc02837..f2f6a6807f339 100644 --- a/src/plugins/expressions/public/mocks.tsx +++ b/src/plugins/expressions/public/mocks.tsx @@ -16,19 +16,13 @@ export type Start = jest.Mocked; const createSetupContract = (): Setup => { const setupContract: Setup = { - extract: jest.fn(), fork: jest.fn(), getFunction: jest.fn(), getFunctions: jest.fn(), - getRenderer: jest.fn(), - getRenderers: jest.fn(), - getType: jest.fn(), getTypes: jest.fn(), - inject: jest.fn(), registerFunction: jest.fn(), registerRenderer: jest.fn(), registerType: jest.fn(), - run: jest.fn(), }; return setupContract; }; @@ -38,10 +32,12 @@ const createStartContract = (): Start => { execute: jest.fn(), ExpressionLoader: jest.fn(), ExpressionRenderHandler: jest.fn(), - fork: jest.fn(), getFunction: jest.fn(), + getFunctions: jest.fn(), getRenderer: jest.fn(), + getRenderers: jest.fn(), getType: jest.fn(), + getTypes: jest.fn(), loader: jest.fn(), ReactExpressionRenderer: jest.fn((props) => <>), render: jest.fn(), diff --git a/src/plugins/expressions/public/plugin.test.ts b/src/plugins/expressions/public/plugin.test.ts index 1963eb1f1b3f7..61ff0d8b54033 100644 --- a/src/plugins/expressions/public/plugin.test.ts +++ b/src/plugins/expressions/public/plugin.test.ts @@ -32,16 +32,6 @@ describe('ExpressionsPublicPlugin', () => { expect(setup.getFunctions().add.name).toBe('add'); }); }); - - describe('.run()', () => { - test('can execute simple expression', async () => { - const { setup } = await expressionsPluginMock.createPlugin(); - const { result } = await setup - .run('var_set name="foo" value="bar" | var name="foo"', null) - .toPromise(); - expect(result).toBe('bar'); - }); - }); }); describe('start contract', () => { diff --git a/src/plugins/expressions/server/mocks.ts b/src/plugins/expressions/server/mocks.ts index f4379145f6a6c..bf36ab3c5daa9 100644 --- a/src/plugins/expressions/server/mocks.ts +++ b/src/plugins/expressions/server/mocks.ts @@ -13,37 +13,24 @@ import { coreMock } from '../../../core/server/mocks'; export type Setup = jest.Mocked; export type Start = jest.Mocked; -const createSetupContract = (): Setup => { - const setupContract: Setup = { - extract: jest.fn(), - fork: jest.fn(), - getFunction: jest.fn(), - getFunctions: jest.fn(), - getRenderer: jest.fn(), - getRenderers: jest.fn(), - getType: jest.fn(), - getTypes: jest.fn(), - inject: jest.fn(), - registerFunction: jest.fn(), - registerRenderer: jest.fn(), - registerType: jest.fn(), - run: jest.fn(), - }; - return setupContract; -}; - -const createStartContract = (): Start => { - const startContract: Start = { +const createSetupContract = (): Setup => ({ + fork: jest.fn(), + getFunction: jest.fn(), + getFunctions: jest.fn(), + getTypes: jest.fn(), + registerFunction: jest.fn(), + registerRenderer: jest.fn(), + registerType: jest.fn(), +}); + +const createStartContract = (): Start => + ({ execute: jest.fn(), - fork: jest.fn(), getFunction: jest.fn(), getRenderer: jest.fn(), getType: jest.fn(), run: jest.fn(), - }; - - return startContract; -}; + } as unknown as Start); const createPlugin = async () => { const pluginInitializerContext = coreMock.createPluginInitializerContext(); diff --git a/src/plugins/expressions/server/plugin.test.ts b/src/plugins/expressions/server/plugin.test.ts index c41cda36e7623..52ecf1ff9979e 100644 --- a/src/plugins/expressions/server/plugin.test.ts +++ b/src/plugins/expressions/server/plugin.test.ts @@ -24,15 +24,5 @@ describe('ExpressionsServerPlugin', () => { expect(setup.getFunctions().add.name).toBe('add'); }); }); - - describe('.run()', () => { - test('can execute simple expression', async () => { - const { setup } = await expressionsPluginMock.createPlugin(); - const { result } = await setup - .run('var_set name="foo" value="bar" | var name="foo"', null) - .toPromise(); - expect(result).toBe('bar'); - }); - }); }); }); diff --git a/src/plugins/kibana_utils/public/index.ts b/src/plugins/kibana_utils/public/index.ts index b0e0b8b2298ab..0ac4c61f4a711 100644 --- a/src/plugins/kibana_utils/public/index.ts +++ b/src/plugins/kibana_utils/public/index.ts @@ -6,6 +6,9 @@ * Side Public License, v 1. */ +import { PluginInitializerContext } from 'src/core/public'; +import { KibanaUtilsPublicPlugin } from './plugin'; + // TODO: https://github.com/elastic/kibana/issues/109893 /* eslint-disable @kbn/eslint/no_export_all */ @@ -78,10 +81,8 @@ export { export { applyDiff } from './state_management/utils/diff_object'; export { createStartServicesGetter, StartServicesGetter } from './core/create_start_service_getter'; -/** dummy plugin, we just want kibanaUtils to have its own bundle */ -export function plugin() { - return new (class KibanaUtilsPlugin { - setup() {} - start() {} - })(); +export { KibanaUtilsSetup } from './plugin'; + +export function plugin(initializerContext: PluginInitializerContext) { + return new KibanaUtilsPublicPlugin(initializerContext); } diff --git a/src/plugins/kibana_utils/public/mocks.ts b/src/plugins/kibana_utils/public/mocks.ts new file mode 100644 index 0000000000000..a537c2fc74e90 --- /dev/null +++ b/src/plugins/kibana_utils/public/mocks.ts @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { KibanaUtilsSetup, KibanaUtilsStart } from './plugin'; + +export type Setup = jest.Mocked; +export type Start = jest.Mocked; + +const createSetupContract = (): Setup => { + return { + setVersion: jest.fn(), + }; +}; + +const createStartContract = (): Start => { + return undefined; +}; + +export const kibanaUtilsPluginMock = { + createSetupContract, + createStartContract, +}; diff --git a/src/plugins/kibana_utils/public/plugin.ts b/src/plugins/kibana_utils/public/plugin.ts new file mode 100644 index 0000000000000..b255aa34ccfdb --- /dev/null +++ b/src/plugins/kibana_utils/public/plugin.ts @@ -0,0 +1,41 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { PluginInitializerContext, CoreSetup, CoreStart, Plugin } from 'src/core/public'; +import { History } from 'history'; +import { setVersion } from './set_version'; + +export interface KibanaUtilsSetup { + setVersion: (history: Pick) => void; +} + +export type KibanaUtilsStart = undefined; + +export class KibanaUtilsPublicPlugin implements Plugin { + private readonly version: string; + + constructor(initializerContext: PluginInitializerContext) { + this.version = initializerContext.env.packageInfo.version; + } + + public setup(core: CoreSetup): KibanaUtilsSetup { + return { + setVersion: this.setVersion, + }; + } + + public start(core: CoreStart): KibanaUtilsStart { + return undefined; + } + + public stop() {} + + private setVersion = (history: Pick) => { + setVersion(history, this.version); + }; +} diff --git a/src/plugins/kibana_utils/public/set_version.test.ts b/src/plugins/kibana_utils/public/set_version.test.ts new file mode 100644 index 0000000000000..eb70d889d0f03 --- /dev/null +++ b/src/plugins/kibana_utils/public/set_version.test.ts @@ -0,0 +1,59 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { History } from 'history'; +import { setVersion } from './set_version'; + +describe('setVersion', () => { + test('sets version, if one is not set', () => { + const history: Pick = { + location: { + hash: '', + search: '', + pathname: '/', + state: {}, + }, + replace: jest.fn(), + }; + setVersion(history, '1.2.3'); + + expect(history.replace).toHaveBeenCalledTimes(1); + expect(history.replace).toHaveBeenCalledWith('/?_v=1.2.3'); + }); + + test('overwrites, if version already set to a different value', () => { + const history: Pick = { + location: { + hash: '/view/dashboards', + search: 'a=b&_v=7.16.6', + pathname: '/foo/bar', + state: {}, + }, + replace: jest.fn(), + }; + setVersion(history, '8.0.0'); + + expect(history.replace).toHaveBeenCalledTimes(1); + expect(history.replace).toHaveBeenCalledWith('/foo/bar?a=b&_v=8.0.0#/view/dashboards'); + }); + + test('does nothing, if version already set to correct value', () => { + const history: Pick = { + location: { + hash: '/view/dashboards', + search: 'a=b&_v=8.0.0', + pathname: '/foo/bar', + state: {}, + }, + replace: jest.fn(), + }; + setVersion(history, '8.0.0'); + + expect(history.replace).toHaveBeenCalledTimes(0); + }); +}); diff --git a/src/plugins/kibana_utils/public/set_version.ts b/src/plugins/kibana_utils/public/set_version.ts new file mode 100644 index 0000000000000..b3acb39ed5134 --- /dev/null +++ b/src/plugins/kibana_utils/public/set_version.ts @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { History } from 'history'; + +export const setVersion = (history: Pick, version: string) => { + const search = new URLSearchParams(history.location.search); + if (search.get('_v') === version) return; + search.set('_v', version); + const path = + history.location.pathname + + '?' + + search.toString() + + (history.location.hash ? '#' + history.location.hash : ''); + history.replace(path); +}; diff --git a/src/plugins/vis_default_editor/public/components/controls/date_ranges.test.tsx b/src/plugins/vis_default_editor/public/components/controls/date_ranges.test.tsx index f10c695662546..02cc0aadfff61 100644 --- a/src/plugins/vis_default_editor/public/components/controls/date_ranges.test.tsx +++ b/src/plugins/vis_default_editor/public/components/controls/date_ranges.test.tsx @@ -55,6 +55,9 @@ describe('DateRangesParamEditor component', () => { }); it('should validate range values with date math', function () { + const mockedConsoleWarn = jest.spyOn(console, 'warn'); // mocked console.warn to avoid console messages when running tests + mockedConsoleWarn.mockImplementation(() => {}); + const component = mountWithIntl(); // should allow empty values @@ -86,5 +89,7 @@ describe('DateRangesParamEditor component', () => { component.setProps({ value: [{ from: '5/5/2005+3d' }] }); expect(setValidity).toHaveBeenNthCalledWith(10, false); + + mockedConsoleWarn.mockRestore(); }); }); diff --git a/src/plugins/vis_default_editor/public/components/controls/percentiles.test.tsx b/src/plugins/vis_default_editor/public/components/controls/percentiles.test.tsx index 849253840f18c..c009196c20d8c 100644 --- a/src/plugins/vis_default_editor/public/components/controls/percentiles.test.tsx +++ b/src/plugins/vis_default_editor/public/components/controls/percentiles.test.tsx @@ -9,10 +9,22 @@ import React from 'react'; import { AggParamEditorProps } from '../agg_param_props'; import { IAggConfig } from 'src/plugins/data/public'; -import { mount } from 'enzyme'; +import { mountWithIntl as mount } from '@kbn/test/jest'; import { PercentilesEditor } from './percentiles'; import { EditorVisState } from '../sidebar/state/reducers'; +// mocking random id generator function +jest.mock('@elastic/eui', () => { + const original = jest.requireActual('@elastic/eui'); + + return { + ...original, + htmlIdGenerator: (fn: unknown) => { + let counter = 0; + return () => counter++; + }, + }; +}); describe('PercentilesEditor component', () => { let setValue: jest.Mock; let setValidity: jest.Mock; diff --git a/src/plugins/vis_types/vega/public/data_model/search_api.ts b/src/plugins/vis_types/vega/public/data_model/search_api.ts index e00cf647930a8..11302ad65d56b 100644 --- a/src/plugins/vis_types/vega/public/data_model/search_api.ts +++ b/src/plugins/vis_types/vega/public/data_model/search_api.ts @@ -95,7 +95,16 @@ export class SearchAPI { } ) .pipe( - tap((data) => this.inspectSearchResult(data, requestResponders[requestId])), + tap( + (data) => this.inspectSearchResult(data, requestResponders[requestId]), + (err) => + this.inspectSearchResult( + { + rawResponse: err?.err, + }, + requestResponders[requestId] + ) + ), map((data) => ({ name: requestId, rawResponse: data.rawResponse, diff --git a/src/plugins/vis_types/vega/public/data_model/vega_parser.test.js b/src/plugins/vis_types/vega/public/data_model/vega_parser.test.js index be356ea4e05ce..cfeed174307ac 100644 --- a/src/plugins/vis_types/vega/public/data_model/vega_parser.test.js +++ b/src/plugins/vis_types/vega/public/data_model/vega_parser.test.js @@ -5,8 +5,8 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ - import { cloneDeep } from 'lodash'; +import 'jest-canvas-mock'; import { euiThemeVars } from '@kbn/ui-shared-deps-src/theme'; import { VegaParser } from './vega_parser'; import { bypassExternalUrlCheck } from '../vega_view/vega_base_view'; diff --git a/src/plugins/vis_types/vega/public/vega_view/vega_map_view/map_service_settings.ts b/src/plugins/vis_types/vega/public/vega_view/vega_map_view/map_service_settings.ts index 3399d0628ad65..9772e693358b6 100644 --- a/src/plugins/vis_types/vega/public/vega_view/vega_map_view/map_service_settings.ts +++ b/src/plugins/vis_types/vega/public/vega_view/vega_map_view/map_service_settings.ts @@ -66,6 +66,11 @@ export class MapServiceSettings { tileApiUrl: this.config.emsTileApiUrl, landingPageUrl: this.config.emsLandingPageUrl, }); + + // Allow zooms > 10 for Vega Maps + // any kibana user, regardless of distribution, should get all zoom levels + // use `sspl` license to indicate this + this.emsClient.addQueryParams({ license: 'sspl' }); } public async getTmsService(tmsTileLayer: string) { diff --git a/src/plugins/vis_types/vega/public/vega_view/vega_map_view/view.test.ts b/src/plugins/vis_types/vega/public/vega_view/vega_map_view/view.test.ts index d3d0b6cb0411e..8ca2b2bd26eed 100644 --- a/src/plugins/vis_types/vega/public/vega_view/vega_map_view/view.test.ts +++ b/src/plugins/vis_types/vega/public/vega_view/vega_map_view/view.test.ts @@ -124,6 +124,8 @@ describe('vega_map_view/view', () => { } as unknown as VegaViewParams); } + let mockedConsoleLog: jest.SpyInstance; + beforeEach(() => { vegaParser = new VegaParser( JSON.stringify(vegaMap), @@ -137,10 +139,13 @@ describe('vega_map_view/view', () => { {}, mockGetServiceSettings ); + mockedConsoleLog = jest.spyOn(console, 'log'); // mocked console.log to avoid messages in the console when running tests + mockedConsoleLog.mockImplementation(() => {}); // comment this line when console logging for debugging }); afterEach(() => { jest.clearAllMocks(); + mockedConsoleLog.mockRestore(); }); test('should be added TmsRasterLayer and do not use tmsService if mapStyle is "user_configured"', async () => { diff --git a/src/plugins/vis_types/vega/public/vega_visualization.test.js b/src/plugins/vis_types/vega/public/vega_visualization.test.js index 05a88880822ca..dd76e2d470004 100644 --- a/src/plugins/vis_types/vega/public/vega_visualization.test.js +++ b/src/plugins/vis_types/vega/public/vega_visualization.test.js @@ -81,7 +81,11 @@ describe('VegaVisualizations', () => { mockWidth.mockRestore(); mockHeight.mockRestore(); }); + test('should show vegalite graph and update on resize (may fail in dev env)', async () => { + const mockedConsoleLog = jest.spyOn(console, 'log'); // mocked console.log to avoid messages in the console when running tests + mockedConsoleLog.mockImplementation(() => {}); // comment this line when console logging for debugging comment this line + let vegaVis; try { vegaVis = new VegaVisualization(domNode, jest.fn()); @@ -111,6 +115,8 @@ describe('VegaVisualizations', () => { } finally { vegaVis.destroy(); } + expect(console.log).toBeCalledTimes(2); + mockedConsoleLog.mockRestore(); }); test('should show vega graph (may fail in dev env)', async () => { @@ -130,7 +136,6 @@ describe('VegaVisualizations', () => { mockGetServiceSettings ); await vegaParser.parseAsync(); - await vegaVis.render(vegaParser); expect(domNode.innerHTML).toMatchSnapshot(); } finally { diff --git a/test/functional/apps/dashboard/dashboard_unsaved_state.ts b/test/functional/apps/dashboard/dashboard_unsaved_state.ts index 8043c8bf8cc37..c2da82a96cd0c 100644 --- a/test/functional/apps/dashboard/dashboard_unsaved_state.ts +++ b/test/functional/apps/dashboard/dashboard_unsaved_state.ts @@ -24,7 +24,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { let unsavedPanelCount = 0; const testQuery = 'Test Query'; - describe('dashboard unsaved state', () => { + // FLAKY https://github.com/elastic/kibana/issues/112812 + describe.skip('dashboard unsaved state', () => { before(async () => { await esArchiver.load('test/functional/fixtures/es_archiver/dashboard/current/kibana'); await kibanaServer.uiSettings.replace({ diff --git a/test/functional/apps/visualize/_timelion.ts b/test/functional/apps/visualize/_timelion.ts index 85dbf7cc5ca96..bb85b6821df31 100644 --- a/test/functional/apps/visualize/_timelion.ts +++ b/test/functional/apps/visualize/_timelion.ts @@ -18,6 +18,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { 'timelion', 'common', ]); + const security = getService('security'); const monacoEditor = getService('monacoEditor'); const kibanaServer = getService('kibanaServer'); const elasticChart = getService('elasticChart'); @@ -26,6 +27,11 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { describe('Timelion visualization', () => { before(async () => { + await security.testUser.setRoles([ + 'kibana_admin', + 'long_window_logstash', + 'test_logstash_reader', + ]); await kibanaServer.uiSettings.update({ 'timelion:legacyChartsLibrary': false, }); diff --git a/test/functional/apps/visualize/_tsvb_time_series.ts b/test/functional/apps/visualize/_tsvb_time_series.ts index 21bee2d16442f..968c979dfa89d 100644 --- a/test/functional/apps/visualize/_tsvb_time_series.ts +++ b/test/functional/apps/visualize/_tsvb_time_series.ts @@ -17,6 +17,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { 'timeToVisualize', 'dashboard', ]); + const security = getService('security'); const testSubjects = getService('testSubjects'); const retry = getService('retry'); const filterBar = getService('filterBar'); @@ -27,6 +28,11 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { describe('visual builder', function describeIndexTests() { before(async () => { + await security.testUser.setRoles([ + 'kibana_admin', + 'long_window_logstash', + 'test_logstash_reader', + ]); await visualize.initTests(); }); beforeEach(async () => { diff --git a/test/functional/page_objects/visual_builder_page.ts b/test/functional/page_objects/visual_builder_page.ts index c324de1231b7d..f5628f60e5a9c 100644 --- a/test/functional/page_objects/visual_builder_page.ts +++ b/test/functional/page_objects/visual_builder_page.ts @@ -284,7 +284,8 @@ export class VisualBuilderPageObject extends FtrService { const drilldownEl = await this.testSubjects.find('drilldownUrl'); await drilldownEl.clearValue(); - await drilldownEl.type(value); + await drilldownEl.type(value, { charByChar: true }); + await this.header.waitUntilLoadingHasFinished(); } /** diff --git a/test/functional/services/saved_query_management_component.ts b/test/functional/services/saved_query_management_component.ts index 37c6a45557f2f..a216f8cb0469e 100644 --- a/test/functional/services/saved_query_management_component.ts +++ b/test/functional/services/saved_query_management_component.ts @@ -166,8 +166,9 @@ export class SavedQueryManagementComponentService extends FtrService { const isOpenAlready = await this.testSubjects.exists('saved-query-management-popover'); if (isOpenAlready) return; + await this.testSubjects.click('saved-query-management-popover-button'); + await this.retry.waitFor('saved query management popover to have any text', async () => { - await this.testSubjects.click('saved-query-management-popover-button'); const queryText = await this.testSubjects.getVisibleText('saved-query-management-popover'); return queryText.length > 0; }); diff --git a/test/interpreter_functional/test_suites/run_pipeline/esaggs_timeshift.ts b/test/interpreter_functional/test_suites/run_pipeline/esaggs_timeshift.ts index 0bc32672d41b9..244d07d2cfc82 100644 --- a/test/interpreter_functional/test_suites/run_pipeline/esaggs_timeshift.ts +++ b/test/interpreter_functional/test_suites/run_pipeline/esaggs_timeshift.ts @@ -37,7 +37,8 @@ export default function ({ }: FtrProviderContext & { updateBaselines: boolean }) { let expectExpression: ExpectExpression; - describe('esaggs timeshift tests', () => { + // FLAKY https://github.com/elastic/kibana/issues/107028 + describe.skip('esaggs timeshift tests', () => { before(() => { expectExpression = expectExpressionProvider({ getService, updateBaselines }); }); diff --git a/test/scripts/jenkins_storybook.sh b/test/scripts/jenkins_storybook.sh index 00cc0d78599dd..17ca46b0097b1 100755 --- a/test/scripts/jenkins_storybook.sh +++ b/test/scripts/jenkins_storybook.sh @@ -20,6 +20,7 @@ yarn storybook --site expression_repeat_image yarn storybook --site expression_reveal_image yarn storybook --site expression_shape yarn storybook --site expression_tagcloud +yarn storybook --site fleet yarn storybook --site infra yarn storybook --site security_solution yarn storybook --site ui_actions_enhanced diff --git a/x-pack/plugins/actions/common/index.ts b/x-pack/plugins/actions/common/index.ts index cff876b5995a1..1e51adf3e9d09 100644 --- a/x-pack/plugins/actions/common/index.ts +++ b/x-pack/plugins/actions/common/index.ts @@ -15,3 +15,10 @@ export * from './rewrite_request_case'; export const BASE_ACTION_API_PATH = '/api/actions'; export const INTERNAL_BASE_ACTION_API_PATH = '/internal/actions'; export const ACTIONS_FEATURE_ID = 'actions'; + +// supported values for `service` in addition to nodemailer's list of well-known services +export enum AdditionalEmailServices { + ELASTIC_CLOUD = 'elastic_cloud', + EXCHANGE = 'exchange_server', + OTHER = 'other', +} diff --git a/x-pack/plugins/actions/server/actions_client.test.ts b/x-pack/plugins/actions/server/actions_client.test.ts index a341cdf58b9e2..7549d2ecaab77 100644 --- a/x-pack/plugins/actions/server/actions_client.test.ts +++ b/x-pack/plugins/actions/server/actions_client.test.ts @@ -20,6 +20,7 @@ import { licenseStateMock } from './lib/license_state.mock'; import { licensingMock } from '../../licensing/server/mocks'; import { httpServerMock } from '../../../../src/core/server/mocks'; import { auditServiceMock } from '../../security/server/audit/index.mock'; +import { usageCountersServiceMock } from 'src/plugins/usage_collection/server/usage_counters/usage_counters_service.mock'; import { elasticsearchServiceMock, @@ -28,7 +29,12 @@ import { import { actionExecutorMock } from './lib/action_executor.mock'; import uuid from 'uuid'; import { ActionsAuthorization } from './authorization/actions_authorization'; +import { + getAuthorizationModeBySource, + AuthorizationMode, +} from './authorization/get_authorization_mode_by_source'; import { actionsAuthorizationMock } from './authorization/actions_authorization.mock'; +import { trackLegacyRBACExemption } from './lib/track_legacy_rbac_exemption'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { elasticsearchClientMock } from '../../../../src/core/server/elasticsearch/client/mocks'; @@ -38,6 +44,22 @@ jest.mock('../../../../src/core/server/saved_objects/service/lib/utils', () => ( }, })); +jest.mock('./lib/track_legacy_rbac_exemption', () => ({ + trackLegacyRBACExemption: jest.fn(), +})); + +jest.mock('./authorization/get_authorization_mode_by_source', () => { + return { + getAuthorizationModeBySource: jest.fn(() => { + return 1; + }), + AuthorizationMode: { + Legacy: 0, + RBAC: 1, + }, + }; +}); + const defaultKibanaIndex = '.kibana'; const unsecuredSavedObjectsClient = savedObjectsClientMock.create(); const scopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); @@ -47,6 +69,8 @@ const executionEnqueuer = jest.fn(); const ephemeralExecutionEnqueuer = jest.fn(); const request = httpServerMock.createKibanaRequest(); const auditLogger = auditServiceMock.create().asScoped(request); +const mockUsageCountersSetup = usageCountersServiceMock.createSetupContract(); +const mockUsageCounter = mockUsageCountersSetup.createUsageCounter('test'); const mockTaskManager = taskManagerMock.createSetup(); @@ -82,6 +106,7 @@ beforeEach(() => { request, authorization: authorization as unknown as ActionsAuthorization, auditLogger, + usageCounter: mockUsageCounter, }); }); @@ -1640,6 +1665,9 @@ describe('update()', () => { describe('execute()', () => { describe('authorization', () => { test('ensures user is authorised to excecute actions', async () => { + (getAuthorizationModeBySource as jest.Mock).mockImplementationOnce(() => { + return AuthorizationMode.RBAC; + }); await actionsClient.execute({ actionId: 'action-id', params: { @@ -1650,6 +1678,9 @@ describe('execute()', () => { }); test('throws when user is not authorised to create the type of action', async () => { + (getAuthorizationModeBySource as jest.Mock).mockImplementationOnce(() => { + return AuthorizationMode.RBAC; + }); authorization.ensureAuthorized.mockRejectedValue( new Error(`Unauthorized to execute all actions`) ); @@ -1665,6 +1696,21 @@ describe('execute()', () => { expect(authorization.ensureAuthorized).toHaveBeenCalledWith('execute'); }); + + test('tracks legacy RBAC', async () => { + (getAuthorizationModeBySource as jest.Mock).mockImplementationOnce(() => { + return AuthorizationMode.Legacy; + }); + + await actionsClient.execute({ + actionId: 'action-id', + params: { + name: 'my name', + }, + }); + + expect(trackLegacyRBACExemption as jest.Mock).toBeCalledWith('execute', mockUsageCounter); + }); }); test('calls the actionExecutor with the appropriate parameters', async () => { @@ -1756,6 +1802,9 @@ describe('execute()', () => { describe('enqueueExecution()', () => { describe('authorization', () => { test('ensures user is authorised to excecute actions', async () => { + (getAuthorizationModeBySource as jest.Mock).mockImplementationOnce(() => { + return AuthorizationMode.RBAC; + }); await actionsClient.enqueueExecution({ id: uuid.v4(), params: {}, @@ -1766,6 +1815,9 @@ describe('enqueueExecution()', () => { }); test('throws when user is not authorised to create the type of action', async () => { + (getAuthorizationModeBySource as jest.Mock).mockImplementationOnce(() => { + return AuthorizationMode.RBAC; + }); authorization.ensureAuthorized.mockRejectedValue( new Error(`Unauthorized to execute all actions`) ); @@ -1781,6 +1833,24 @@ describe('enqueueExecution()', () => { expect(authorization.ensureAuthorized).toHaveBeenCalledWith('execute'); }); + + test('tracks legacy RBAC', async () => { + (getAuthorizationModeBySource as jest.Mock).mockImplementationOnce(() => { + return AuthorizationMode.Legacy; + }); + + await actionsClient.enqueueExecution({ + id: uuid.v4(), + params: {}, + spaceId: 'default', + apiKey: null, + }); + + expect(trackLegacyRBACExemption as jest.Mock).toBeCalledWith( + 'enqueueExecution', + mockUsageCounter + ); + }); }); test('calls the executionEnqueuer with the appropriate parameters', async () => { diff --git a/x-pack/plugins/actions/server/actions_client.ts b/x-pack/plugins/actions/server/actions_client.ts index d6f6037ecd8b8..b391e50283ad1 100644 --- a/x-pack/plugins/actions/server/actions_client.ts +++ b/x-pack/plugins/actions/server/actions_client.ts @@ -7,6 +7,7 @@ import Boom from '@hapi/boom'; import type { estypes } from '@elastic/elasticsearch'; +import { UsageCounter } from 'src/plugins/usage_collection/server'; import { i18n } from '@kbn/i18n'; import { omitBy, isUndefined } from 'lodash'; @@ -42,6 +43,7 @@ import { } from './authorization/get_authorization_mode_by_source'; import { connectorAuditEvent, ConnectorAuditAction } from './lib/audit_events'; import { RunNowResult } from '../../task_manager/server'; +import { trackLegacyRBACExemption } from './lib/track_legacy_rbac_exemption'; // We are assuming there won't be many actions. This is why we will load // all the actions in advance and assume the total count to not go over 10000. @@ -74,6 +76,7 @@ interface ConstructorOptions { request: KibanaRequest; authorization: ActionsAuthorization; auditLogger?: AuditLogger; + usageCounter?: UsageCounter; } export interface UpdateOptions { @@ -93,6 +96,7 @@ export class ActionsClient { private readonly executionEnqueuer: ExecutionEnqueuer; private readonly ephemeralExecutionEnqueuer: ExecutionEnqueuer; private readonly auditLogger?: AuditLogger; + private readonly usageCounter?: UsageCounter; constructor({ actionTypeRegistry, @@ -106,6 +110,7 @@ export class ActionsClient { request, authorization, auditLogger, + usageCounter, }: ConstructorOptions) { this.actionTypeRegistry = actionTypeRegistry; this.unsecuredSavedObjectsClient = unsecuredSavedObjectsClient; @@ -118,6 +123,7 @@ export class ActionsClient { this.request = request; this.authorization = authorization; this.auditLogger = auditLogger; + this.usageCounter = usageCounter; } /** @@ -478,6 +484,8 @@ export class ActionsClient { AuthorizationMode.RBAC ) { await this.authorization.ensureAuthorized('execute'); + } else { + trackLegacyRBACExemption('execute', this.usageCounter); } return this.actionExecutor.execute({ actionId, @@ -495,6 +503,8 @@ export class ActionsClient { AuthorizationMode.RBAC ) { await this.authorization.ensureAuthorized('execute'); + } else { + trackLegacyRBACExemption('enqueueExecution', this.usageCounter); } return this.executionEnqueuer(this.unsecuredSavedObjectsClient, options); } @@ -506,6 +516,8 @@ export class ActionsClient { AuthorizationMode.RBAC ) { await this.authorization.ensureAuthorized('execute'); + } else { + trackLegacyRBACExemption('ephemeralEnqueuedExecution', this.usageCounter); } return this.ephemeralExecutionEnqueuer(this.unsecuredSavedObjectsClient, options); } diff --git a/x-pack/plugins/actions/server/builtin_action_types/email.ts b/x-pack/plugins/actions/server/builtin_action_types/email.ts index d10046341b268..fcd003286d5bb 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/email.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/email.ts @@ -17,6 +17,7 @@ import { Logger } from '../../../../../src/core/server'; import { ActionType, ActionTypeExecutorOptions, ActionTypeExecutorResult } from '../types'; import { ActionsConfigurationUtilities } from '../actions_config'; import { renderMustacheString, renderMustacheObject } from '../lib/mustache_renderer'; +import { AdditionalEmailServices } from '../../common'; export type EmailActionType = ActionType< ActionTypeConfigType, @@ -33,13 +34,6 @@ export type EmailActionTypeExecutorOptions = ActionTypeExecutorOptions< // config definition export type ActionTypeConfigType = TypeOf; -// supported values for `service` in addition to nodemailer's list of well-known services -export enum AdditionalEmailServices { - ELASTIC_CLOUD = 'elastic_cloud', - EXCHANGE = 'exchange_server', - OTHER = 'other', -} - // these values for `service` require users to fill in host/port/secure export const CUSTOM_HOST_PORT_SERVICES: string[] = [AdditionalEmailServices.OTHER]; diff --git a/x-pack/plugins/actions/server/builtin_action_types/lib/request_oauth_client_credentials_token.ts b/x-pack/plugins/actions/server/builtin_action_types/lib/request_oauth_client_credentials_token.ts index 09080ee0c0063..b632cdf5f5219 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/lib/request_oauth_client_credentials_token.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/lib/request_oauth_client_credentials_token.ts @@ -7,6 +7,7 @@ import qs from 'query-string'; import axios from 'axios'; +import stringify from 'json-stable-stringify'; import { Logger } from '../../../../../../src/core/server'; import { request } from './axios_utils'; import { ActionsConfigurationUtilities } from '../../actions_config'; @@ -59,7 +60,7 @@ export async function requestOAuthClientCredentialsToken( expiresIn: res.data.expires_in, }; } else { - const errString = JSON.stringify(res.data); + const errString = stringify(res.data); logger.warn( `error thrown getting the access token from ${tokenUrl} for clientID: ${clientId}: ${errString}` ); diff --git a/x-pack/plugins/actions/server/builtin_action_types/lib/send_email.ts b/x-pack/plugins/actions/server/builtin_action_types/lib/send_email.ts index ea3c0f91b6a5c..53c70fddc5a09 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/lib/send_email.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/lib/send_email.ts @@ -13,10 +13,10 @@ import { Logger } from '../../../../../../src/core/server'; import { ActionsConfigurationUtilities } from '../../actions_config'; import { CustomHostSettings } from '../../config'; import { getNodeSSLOptions, getSSLSettingsFromConfig } from './get_node_ssl_options'; -import { AdditionalEmailServices } from '../email'; import { sendEmailGraphApi } from './send_email_graph_api'; import { requestOAuthClientCredentialsToken } from './request_oauth_client_credentials_token'; import { ProxySettings } from '../../types'; +import { AdditionalEmailServices } from '../../../common'; // an email "service" which doesn't actually send, just returns what it would send export const JSON_TRANSPORT_SERVICE = '__json'; diff --git a/x-pack/plugins/actions/server/builtin_action_types/lib/send_email_graph_api.ts b/x-pack/plugins/actions/server/builtin_action_types/lib/send_email_graph_api.ts index 10e9a3bc8d27c..ea1579095bb97 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/lib/send_email_graph_api.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/lib/send_email_graph_api.ts @@ -5,6 +5,8 @@ * 2.0. */ +// @ts-expect-error missing type def +import stringify from 'json-stringify-safe'; import axios, { AxiosResponse } from 'axios'; import { Logger } from '../../../../../../src/core/server'; import { request } from './axios_utils'; @@ -41,9 +43,9 @@ export async function sendEmailGraphApi( validateStatus: () => true, }); if (res.status === 202) { - return res; + return res.data; } - const errString = JSON.stringify(res.data); + const errString = stringify(res.data); logger.warn( `error thrown sending Microsoft Exchange email for clientID: ${sendEmailOptions.options.transport.clientId}: ${errString}` ); diff --git a/x-pack/plugins/actions/server/lib/track_legacy_rbac_exemption.test.ts b/x-pack/plugins/actions/server/lib/track_legacy_rbac_exemption.test.ts new file mode 100644 index 0000000000000..ffd8e7f17c11f --- /dev/null +++ b/x-pack/plugins/actions/server/lib/track_legacy_rbac_exemption.test.ts @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { usageCountersServiceMock } from 'src/plugins/usage_collection/server/usage_counters/usage_counters_service.mock'; +import { trackLegacyRBACExemption } from './track_legacy_rbac_exemption'; + +describe('trackLegacyRBACExemption', () => { + it('should call `usageCounter.incrementCounter`', () => { + const mockUsageCountersSetup = usageCountersServiceMock.createSetupContract(); + const mockUsageCounter = mockUsageCountersSetup.createUsageCounter('test'); + + trackLegacyRBACExemption('test', mockUsageCounter); + expect(mockUsageCounter.incrementCounter).toHaveBeenCalledWith({ + counterName: `source_test`, + counterType: 'legacyRBACExemption', + incrementBy: 1, + }); + }); + + it('should do nothing if no usage counter is provided', () => { + let err; + try { + trackLegacyRBACExemption('test', undefined); + } catch (e) { + err = e; + } + expect(err).toBeUndefined(); + }); +}); diff --git a/x-pack/plugins/actions/server/lib/track_legacy_rbac_exemption.ts b/x-pack/plugins/actions/server/lib/track_legacy_rbac_exemption.ts new file mode 100644 index 0000000000000..73c859c4cd21e --- /dev/null +++ b/x-pack/plugins/actions/server/lib/track_legacy_rbac_exemption.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { UsageCounter } from 'src/plugins/usage_collection/server'; + +export function trackLegacyRBACExemption(source: string, usageCounter?: UsageCounter) { + if (usageCounter) { + usageCounter.incrementCounter({ + counterName: `source_${source}`, + counterType: 'legacyRBACExemption', + incrementBy: 1, + }); + } +} diff --git a/x-pack/plugins/actions/server/plugin.ts b/x-pack/plugins/actions/server/plugin.ts index fe133ddb6f0ac..78808b669d9e9 100644 --- a/x-pack/plugins/actions/server/plugin.ts +++ b/x-pack/plugins/actions/server/plugin.ts @@ -6,7 +6,7 @@ */ import type { PublicMethodsOf } from '@kbn/utility-types'; -import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; +import { UsageCollectionSetup, UsageCounter } from 'src/plugins/usage_collection/server'; import { PluginInitializerContext, Plugin, @@ -151,6 +151,7 @@ export class ActionsPlugin implements Plugin(), this.licenseState, - usageCounter + this.usageCounter ); // Cleanup failed execution task definition @@ -367,6 +368,7 @@ export class ActionsPlugin implements Plugin void; - setDurationPercentile?: (value: PercentileOption) => void; - showThreshold?: boolean; - durationPercentile?: PercentileOption; -} - -export type PercentileOption = 50 | 75 | 99; -const percentilOptions: PercentileOption[] = [50, 75, 99]; - -export function CustomFields({ - fieldNames, - setFieldNames, - setDurationPercentile = () => {}, - showThreshold = false, - durationPercentile = 75, -}: Props) { - const trackApmEvent = useUiTracker({ app: 'apm' }); - const { defaultFieldNames, getSuggestions } = useFieldNames(); - const [suggestedFieldNames, setSuggestedFieldNames] = useState( - getSuggestions('') - ); - - useEffect(() => { - if (suggestedFieldNames.length) { - return; - } - setSuggestedFieldNames(getSuggestions('')); - }, [getSuggestions, suggestedFieldNames]); - - return ( - - - - {showThreshold && ( - - - ({ - value: percentile, - text: i18n.translate( - 'xpack.apm.correlations.customize.thresholdPercentile', - { - defaultMessage: '{percentile}th percentile', - values: { percentile }, - } - ), - }))} - onChange={(e) => { - setDurationPercentile( - parseInt(e.target.value, 10) as PercentileOption - ); - }} - /> - - - )} - - { - setFieldNames(defaultFieldNames); - }} - > - {i18n.translate( - 'xpack.apm.correlations.customize.fieldHelpTextReset', - { defaultMessage: 'reset' } - )} - - ), - docsLink: ( - - {i18n.translate( - 'xpack.apm.correlations.customize.fieldHelpTextDocsLink', - { - defaultMessage: - 'Learn more about the default fields.', - } - )} - - ), - }} - /> - } - > - ({ label }))} - onChange={(options) => { - const nextFieldNames = options.map((option) => option.label); - setFieldNames(nextFieldNames); - trackApmEvent({ metric: 'customize_correlations_fields' }); - }} - onCreateOption={(term) => { - const nextFieldNames = [...fieldNames, term]; - setFieldNames(nextFieldNames); - }} - onSearchChange={(searchValue) => { - setSuggestedFieldNames(getSuggestions(searchValue)); - }} - options={suggestedFieldNames.map((label) => ({ label }))} - /> - - - - - ); -} diff --git a/x-pack/plugins/apm/public/components/app/correlations/use_field_names.ts b/x-pack/plugins/apm/public/components/app/correlations/use_field_names.ts deleted file mode 100644 index ff88808c51d15..0000000000000 --- a/x-pack/plugins/apm/public/components/app/correlations/use_field_names.ts +++ /dev/null @@ -1,74 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { memoize } from 'lodash'; -import { useEffect, useMemo, useState } from 'react'; -import { isRumAgentName } from '../../../../common/agent_name'; -import { useApmServiceContext } from '../../../context/apm_service/use_apm_service_context'; -import { useDynamicIndexPatternFetcher } from '../../../hooks/use_dynamic_index_pattern'; - -interface IndexPattern { - fields: Array<{ name: string; esTypes: string[] }>; -} - -export function useFieldNames() { - const { agentName } = useApmServiceContext(); - const isRumAgent = isRumAgentName(agentName); - const { indexPattern } = useDynamicIndexPatternFetcher(); - - const [defaultFieldNames, setDefaultFieldNames] = useState( - getDefaultFieldNames(indexPattern, isRumAgent) - ); - - const getSuggestions = useMemo( - () => - memoize((searchValue: string) => - getMatchingFieldNames(indexPattern, searchValue) - ), - [indexPattern] - ); - - useEffect(() => { - setDefaultFieldNames(getDefaultFieldNames(indexPattern, isRumAgent)); - }, [indexPattern, isRumAgent]); - - return { defaultFieldNames, getSuggestions }; -} - -function getMatchingFieldNames( - indexPattern: IndexPattern | undefined, - inputValue: string -) { - if (!indexPattern) { - return []; - } - return indexPattern.fields - .filter( - ({ name, esTypes }) => - name.startsWith(inputValue) && esTypes[0] === 'keyword' // only show fields of type 'keyword' - ) - .map(({ name }) => name); -} - -function getDefaultFieldNames( - indexPattern: IndexPattern | undefined, - isRumAgent: boolean -) { - const labelFields = getMatchingFieldNames(indexPattern, 'labels.').slice( - 0, - 6 - ); - return isRumAgent - ? [ - ...labelFields, - 'user_agent.name', - 'user_agent.os.name', - 'url.original', - ...getMatchingFieldNames(indexPattern, 'user.').slice(0, 6), - ] - : [...labelFields, 'service.version', 'service.node.name', 'host.ip']; -} diff --git a/x-pack/plugins/apm/public/components/shared/charts/transaction_distribution_chart/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/transaction_distribution_chart/index.tsx index e8a159f23ee3d..535fb777166bb 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/transaction_distribution_chart/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/transaction_distribution_chart/index.tsx @@ -256,6 +256,7 @@ export function TransactionDistributionChart({ /> {data.map((d, i) => ( { - describe('getHistogramIntervalRequest', () => { - it('returns the request body for the transaction duration ranges aggregation', () => { - const req = getHistogramIntervalRequest(params); - - expect(req).toEqual({ - body: { - aggs: { - transaction_duration_max: { - max: { - field: 'transaction.duration.us', - }, - }, - transaction_duration_min: { - min: { - field: 'transaction.duration.us', - }, - }, - }, - query: { - bool: { - filter: [ - { - term: { - 'processor.event': 'transaction', - }, - }, - { - range: { - '@timestamp': { - format: 'epoch_millis', - gte: 1577836800000, - lte: 1609459200000, - }, - }, - }, - ], - }, - }, - size: 0, - }, - index: params.index, - ignore_throttled: !params.includeFrozen, - ignore_unavailable: true, - }); - }); - }); - - describe('fetchTransactionDurationHistogramInterval', () => { - it('fetches the interval duration for histograms', async () => { - const esClientSearchMock = jest.fn( - ( - req: estypes.SearchRequest - ): { - body: estypes.SearchResponse; - } => { - return { - body: { - aggregations: { - transaction_duration_max: { - value: 10000, - }, - transaction_duration_min: { - value: 10, - }, - }, - } as unknown as estypes.SearchResponse, - }; - } - ); - - const esClientMock = { - search: esClientSearchMock, - } as unknown as ElasticsearchClient; - - const resp = await fetchTransactionDurationHistogramInterval( - esClientMock, - params - ); - - expect(resp).toEqual(10); - expect(esClientSearchMock).toHaveBeenCalledTimes(1); - }); - }); -}); diff --git a/x-pack/plugins/apm/server/lib/search_strategies/queries/query_histogram_interval.ts b/x-pack/plugins/apm/server/lib/search_strategies/queries/query_histogram_interval.ts deleted file mode 100644 index 906105003b716..0000000000000 --- a/x-pack/plugins/apm/server/lib/search_strategies/queries/query_histogram_interval.ts +++ /dev/null @@ -1,57 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type { estypes } from '@elastic/elasticsearch'; - -import type { ElasticsearchClient } from 'src/core/server'; - -import { TRANSACTION_DURATION } from '../../../../common/elasticsearch_fieldnames'; -import type { SearchStrategyParams } from '../../../../common/search_strategies/types'; - -import { getQueryWithParams } from './get_query_with_params'; -import { getRequestBase } from './get_request_base'; - -const HISTOGRAM_INTERVALS = 1000; - -export const getHistogramIntervalRequest = ( - params: SearchStrategyParams -): estypes.SearchRequest => ({ - ...getRequestBase(params), - body: { - query: getQueryWithParams({ params }), - size: 0, - aggs: { - transaction_duration_min: { min: { field: TRANSACTION_DURATION } }, - transaction_duration_max: { max: { field: TRANSACTION_DURATION } }, - }, - }, -}); - -export const fetchTransactionDurationHistogramInterval = async ( - esClient: ElasticsearchClient, - params: SearchStrategyParams -): Promise => { - const resp = await esClient.search(getHistogramIntervalRequest(params)); - - if (resp.body.aggregations === undefined) { - throw new Error( - 'fetchTransactionDurationHistogramInterval failed, did not return aggregations.' - ); - } - - const transactionDurationDelta = - ( - resp.body.aggregations - .transaction_duration_max as estypes.AggregationsValueAggregate - ).value - - ( - resp.body.aggregations - .transaction_duration_min as estypes.AggregationsValueAggregate - ).value; - - return transactionDurationDelta / (HISTOGRAM_INTERVALS - 1); -}; diff --git a/x-pack/plugins/apm/server/lib/search_strategies/search_strategy_provider.test.ts b/x-pack/plugins/apm/server/lib/search_strategies/search_strategy_provider.test.ts index b56ab83f547ff..6e03c879f9b97 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/search_strategy_provider.test.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/search_strategy_provider.test.ts @@ -57,17 +57,6 @@ const clientSearchMock = ( aggregations = { transaction_duration_percentiles: { values: {} } }; } - // fetchTransactionDurationHistogramInterval - if ( - aggs.transaction_duration_min !== undefined && - aggs.transaction_duration_max !== undefined - ) { - aggregations = { - transaction_duration_min: { value: 0 }, - transaction_duration_max: { value: 1234 }, - }; - } - // fetchTransactionDurationCorrelation if (aggs.logspace_ranges !== undefined) { aggregations = { logspace_ranges: { buckets: [] } }; diff --git a/x-pack/plugins/apm/server/routes/correlations.ts b/x-pack/plugins/apm/server/routes/correlations.ts deleted file mode 100644 index 4728aa2e8d3f6..0000000000000 --- a/x-pack/plugins/apm/server/routes/correlations.ts +++ /dev/null @@ -1,214 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import Boom from '@hapi/boom'; -import { i18n } from '@kbn/i18n'; -import * as t from 'io-ts'; -import { isActivePlatinumLicense } from '../../common/license_check'; -import { getCorrelationsForFailedTransactions } from '../lib/correlations/errors/get_correlations_for_failed_transactions'; -import { getOverallErrorTimeseries } from '../lib/correlations/errors/get_overall_error_timeseries'; -import { getCorrelationsForSlowTransactions } from '../lib/correlations/latency/get_correlations_for_slow_transactions'; -import { getOverallLatencyDistribution } from '../lib/correlations/latency/get_overall_latency_distribution'; -import { setupRequest } from '../lib/helpers/setup_request'; -import { createApmServerRoute } from './create_apm_server_route'; -import { createApmServerRouteRepository } from './create_apm_server_route_repository'; -import { environmentRt, kueryRt, rangeRt } from './default_api_types'; - -const INVALID_LICENSE = i18n.translate( - 'xpack.apm.significanTerms.license.text', - { - defaultMessage: - 'To use the correlations API, you must be subscribed to an Elastic Platinum license.', - } -); - -const correlationsLatencyDistributionRoute = createApmServerRoute({ - endpoint: 'GET /api/apm/correlations/latency/overall_distribution', - params: t.type({ - query: t.intersection([ - t.partial({ - serviceName: t.string, - transactionName: t.string, - transactionType: t.string, - }), - environmentRt, - kueryRt, - rangeRt, - ]), - }), - options: { tags: ['access:apm'] }, - handler: async (resources) => { - const { context, params } = resources; - if (!isActivePlatinumLicense(context.licensing.license)) { - throw Boom.forbidden(INVALID_LICENSE); - } - const setup = await setupRequest(resources); - const { - environment, - kuery, - serviceName, - transactionType, - transactionName, - } = params.query; - - return getOverallLatencyDistribution({ - environment, - kuery, - serviceName, - transactionType, - transactionName, - setup, - }); - }, -}); - -const correlationsForSlowTransactionsRoute = createApmServerRoute({ - endpoint: 'GET /api/apm/correlations/latency/slow_transactions', - params: t.type({ - query: t.intersection([ - t.partial({ - serviceName: t.string, - transactionName: t.string, - transactionType: t.string, - }), - t.type({ - durationPercentile: t.string, - fieldNames: t.string, - maxLatency: t.string, - distributionInterval: t.string, - }), - environmentRt, - kueryRt, - rangeRt, - ]), - }), - options: { tags: ['access:apm'] }, - handler: async (resources) => { - const { context, params } = resources; - - if (!isActivePlatinumLicense(context.licensing.license)) { - throw Boom.forbidden(INVALID_LICENSE); - } - const setup = await setupRequest(resources); - const { - environment, - kuery, - serviceName, - transactionType, - transactionName, - durationPercentile, - fieldNames, - maxLatency, - distributionInterval, - } = params.query; - - return getCorrelationsForSlowTransactions({ - environment, - kuery, - serviceName, - transactionType, - transactionName, - durationPercentile: parseInt(durationPercentile, 10), - fieldNames: fieldNames.split(','), - setup, - maxLatency: parseInt(maxLatency, 10), - distributionInterval: parseInt(distributionInterval, 10), - }); - }, -}); - -const correlationsErrorDistributionRoute = createApmServerRoute({ - endpoint: 'GET /api/apm/correlations/errors/overall_timeseries', - params: t.type({ - query: t.intersection([ - t.partial({ - serviceName: t.string, - transactionName: t.string, - transactionType: t.string, - }), - environmentRt, - kueryRt, - rangeRt, - ]), - }), - options: { tags: ['access:apm'] }, - handler: async (resources) => { - const { params, context } = resources; - - if (!isActivePlatinumLicense(context.licensing.license)) { - throw Boom.forbidden(INVALID_LICENSE); - } - const setup = await setupRequest(resources); - const { - environment, - kuery, - serviceName, - transactionType, - transactionName, - } = params.query; - - return getOverallErrorTimeseries({ - environment, - kuery, - serviceName, - transactionType, - transactionName, - setup, - }); - }, -}); - -const correlationsForFailedTransactionsRoute = createApmServerRoute({ - endpoint: 'GET /api/apm/correlations/errors/failed_transactions', - params: t.type({ - query: t.intersection([ - t.partial({ - serviceName: t.string, - transactionName: t.string, - transactionType: t.string, - }), - t.type({ - fieldNames: t.string, - }), - environmentRt, - kueryRt, - rangeRt, - ]), - }), - options: { tags: ['access:apm'] }, - handler: async (resources) => { - const { context, params } = resources; - if (!isActivePlatinumLicense(context.licensing.license)) { - throw Boom.forbidden(INVALID_LICENSE); - } - const setup = await setupRequest(resources); - const { - environment, - kuery, - serviceName, - transactionType, - transactionName, - fieldNames, - } = params.query; - - return getCorrelationsForFailedTransactions({ - environment, - kuery, - serviceName, - transactionType, - transactionName, - fieldNames: fieldNames.split(','), - setup, - }); - }, -}); - -export const correlationsRouteRepository = createApmServerRouteRepository() - .add(correlationsLatencyDistributionRoute) - .add(correlationsForSlowTransactionsRoute) - .add(correlationsErrorDistributionRoute) - .add(correlationsForFailedTransactionsRoute); diff --git a/x-pack/plugins/apm/server/routes/get_global_apm_server_route_repository.ts b/x-pack/plugins/apm/server/routes/get_global_apm_server_route_repository.ts index 9bc9108da9055..09756e30d9682 100644 --- a/x-pack/plugins/apm/server/routes/get_global_apm_server_route_repository.ts +++ b/x-pack/plugins/apm/server/routes/get_global_apm_server_route_repository.ts @@ -12,7 +12,6 @@ import type { import { PickByValue } from 'utility-types'; import { alertsChartPreviewRouteRepository } from './alerts/chart_preview'; import { backendsRouteRepository } from './backends'; -import { correlationsRouteRepository } from './correlations'; import { createApmServerRouteRepository } from './create_apm_server_route_repository'; import { environmentsRouteRepository } from './environments'; import { errorsRouteRepository } from './errors'; @@ -49,7 +48,6 @@ const getTypedGlobalApmServerRouteRepository = () => { .merge(traceRouteRepository) .merge(transactionRouteRepository) .merge(alertsChartPreviewRouteRepository) - .merge(correlationsRouteRepository) .merge(agentConfigurationRouteRepository) .merge(anomalyDetectionRouteRepository) .merge(apmIndicesRouteRepository) diff --git a/x-pack/plugins/canvas/public/application.tsx b/x-pack/plugins/canvas/public/application.tsx index 30b2d78a6b1fe..f2fe944bfd45d 100644 --- a/x-pack/plugins/canvas/public/application.tsx +++ b/x-pack/plugins/canvas/public/application.tsx @@ -98,11 +98,10 @@ export const initializeCanvas = async ( setupPlugins: CanvasSetupDeps, startPlugins: CanvasStartDeps, registries: SetupRegistries, - appUpdater: BehaviorSubject, - pluginServices: PluginServices + appUpdater: BehaviorSubject ) => { await startLegacyServices(coreSetup, coreStart, setupPlugins, startPlugins, appUpdater); - const { expressions } = pluginServices.getServices(); + const { expressions } = setupPlugins; // Adding these functions here instead of in plugin.ts. // Some of these functions have deep dependencies into Canvas, which was bulking up the size diff --git a/x-pack/plugins/canvas/public/plugin.tsx b/x-pack/plugins/canvas/public/plugin.tsx index 555cedb6b16a1..bd5d884f1485c 100644 --- a/x-pack/plugins/canvas/public/plugin.tsx +++ b/x-pack/plugins/canvas/public/plugin.tsx @@ -132,8 +132,7 @@ export class CanvasPlugin setupPlugins, startPlugins, registries, - this.appUpdater, - pluginServices + this.appUpdater ); const unmount = renderApp({ coreStart, startPlugins, params, canvasStore, pluginServices }); diff --git a/x-pack/plugins/canvas/public/services/expressions.ts b/x-pack/plugins/canvas/public/services/expressions.ts index a1af0fba50a5c..01bb0adb17711 100644 --- a/x-pack/plugins/canvas/public/services/expressions.ts +++ b/x-pack/plugins/canvas/public/services/expressions.ts @@ -5,6 +5,6 @@ * 2.0. */ -import { ExpressionsService } from '../../../../../src/plugins/expressions/public'; +import { ExpressionsServiceStart } from '../../../../../src/plugins/expressions/public'; -export type CanvasExpressionsService = ExpressionsService; +export type CanvasExpressionsService = ExpressionsServiceStart; diff --git a/x-pack/plugins/canvas/public/services/kibana/expressions.ts b/x-pack/plugins/canvas/public/services/kibana/expressions.ts index 4e3bb52a5d449..780de5309d97e 100644 --- a/x-pack/plugins/canvas/public/services/kibana/expressions.ts +++ b/x-pack/plugins/canvas/public/services/kibana/expressions.ts @@ -16,4 +16,4 @@ export type CanvasExpressionsServiceFactory = KibanaPluginServiceFactory< >; export const expressionsServiceFactory: CanvasExpressionsServiceFactory = ({ startPlugins }) => - startPlugins.expressions.fork(); + startPlugins.expressions; diff --git a/x-pack/plugins/canvas/server/saved_objects/workpad_references.ts b/x-pack/plugins/canvas/server/saved_objects/workpad_references.ts index b0d20add2f79a..e9eefa1bdb3f4 100644 --- a/x-pack/plugins/canvas/server/saved_objects/workpad_references.ts +++ b/x-pack/plugins/canvas/server/saved_objects/workpad_references.ts @@ -6,14 +6,15 @@ */ import { fromExpression, toExpression } from '@kbn/interpreter/common'; +import { PersistableStateService } from '../../../../../src/plugins/kibana_utils/common'; import { SavedObjectReference } from '../../../../../src/core/server'; import { WorkpadAttributes } from '../routes/workpad/workpad_attributes'; -import { ExpressionsServerSetup } from '../../../../../src/plugins/expressions/server'; +import type { ExpressionAstExpression } from '../../../../../src/plugins/expressions'; export const extractReferences = ( workpad: WorkpadAttributes, - expressions: ExpressionsServerSetup + expressions: PersistableStateService ): { workpad: WorkpadAttributes; references: SavedObjectReference[] } => { // We need to find every element in the workpad and extract references const references: SavedObjectReference[] = []; @@ -42,7 +43,7 @@ export const extractReferences = ( export const injectReferences = ( workpad: WorkpadAttributes, references: SavedObjectReference[], - expressions: ExpressionsServerSetup + expressions: PersistableStateService ) => { const pages = workpad.pages.map((page) => { const elements = page.elements.map((element) => { diff --git a/x-pack/plugins/cases/common/api/cases/case.ts b/x-pack/plugins/cases/common/api/cases/case.ts index 05a053307b29c..cd26ca0bab977 100644 --- a/x-pack/plugins/cases/common/api/cases/case.ts +++ b/x-pack/plugins/cases/common/api/cases/case.ts @@ -254,6 +254,16 @@ export const CaseResponseRt = rt.intersection([ }), ]); +export const CaseResolveResponseRt = rt.intersection([ + rt.type({ + case: CaseResponseRt, + outcome: rt.union([rt.literal('exactMatch'), rt.literal('aliasMatch'), rt.literal('conflict')]), + }), + rt.partial({ + alias_target_id: rt.string, + }), +]); + export const CasesFindResponseRt = rt.intersection([ rt.type({ cases: rt.array(CaseResponseRt), @@ -319,6 +329,7 @@ export type CaseAttributes = rt.TypeOf; export type CasesClientPostRequest = rt.TypeOf; export type CasePostRequest = rt.TypeOf; export type CaseResponse = rt.TypeOf; +export type CaseResolveResponse = rt.TypeOf; export type CasesResponse = rt.TypeOf; export type CasesFindRequest = rt.TypeOf; export type CasesByAlertIDRequest = rt.TypeOf; diff --git a/x-pack/plugins/cases/common/ui/types.ts b/x-pack/plugins/cases/common/ui/types.ts index c89c3eb08263b..948b203af14a8 100644 --- a/x-pack/plugins/cases/common/ui/types.ts +++ b/x-pack/plugins/cases/common/ui/types.ts @@ -114,6 +114,12 @@ export interface Case extends BasicCase { type: CaseType; } +export interface ResolvedCase { + case: Case; + outcome: 'exactMatch' | 'aliasMatch' | 'conflict'; + aliasTargetId?: string; +} + export interface QueryParams { page: number; perPage: number; diff --git a/x-pack/plugins/cases/public/common/lib/kibana/kibana_react.mock.ts b/x-pack/plugins/cases/public/common/lib/kibana/kibana_react.mock.ts index 18370be61bdf1..09f0215f5629f 100644 --- a/x-pack/plugins/cases/public/common/lib/kibana/kibana_react.mock.ts +++ b/x-pack/plugins/cases/public/common/lib/kibana/kibana_react.mock.ts @@ -13,6 +13,7 @@ import { KibanaContextProvider } from '../../../../../../../src/plugins/kibana_r import { StartServices } from '../../../types'; import { EuiTheme } from '../../../../../../../src/plugins/kibana_react/common'; import { securityMock } from '../../../../../security/public/mocks'; +import { spacesPluginMock } from '../../../../../spaces/public/mocks'; import { triggersActionsUiMock } from '../../../../../triggers_actions_ui/public/mocks'; export const createStartServicesMock = (): StartServices => @@ -25,6 +26,7 @@ export const createStartServicesMock = (): StartServices => }, security: securityMock.createStart(), triggersActionsUi: triggersActionsUiMock.createStart(), + spaces: spacesPluginMock.createStartContract(), } as unknown as StartServices); export const createWithKibanaMock = () => { diff --git a/x-pack/plugins/cases/public/components/case_view/index.test.tsx b/x-pack/plugins/cases/public/components/case_view/index.test.tsx index f12c8ba098d43..6fc9e1719e1cf 100644 --- a/x-pack/plugins/cases/public/components/case_view/index.test.tsx +++ b/x-pack/plugins/cases/public/components/case_view/index.test.tsx @@ -18,6 +18,7 @@ import { getAlertUserAction, } from '../../containers/mock'; import { TestProviders } from '../../common/mock'; +import { SpacesApi } from '../../../../spaces/public'; import { useUpdateCase } from '../../containers/use_update_case'; import { useGetCase } from '../../containers/use_get_case'; import { useGetCaseUserActions } from '../../containers/use_get_case_user_actions'; @@ -47,6 +48,13 @@ const useConnectorsMock = useConnectors as jest.Mock; const usePostPushToServiceMock = usePostPushToService as jest.Mock; const useKibanaMock = useKibana as jest.Mocked; +const spacesUiApiMock = { + redirectLegacyUrl: jest.fn().mockResolvedValue(undefined), + components: { + getLegacyUrlConflict: jest.fn().mockReturnValue(
), + }, +}; + const alertsHit = [ { _id: 'alert-id-1', @@ -138,6 +146,7 @@ describe('CaseView ', () => { isLoading: false, isError: false, data, + resolveOutcome: 'exactMatch', updateCase, fetchCase, }; @@ -174,6 +183,7 @@ describe('CaseView ', () => { actionTypeTitle: '.servicenow', iconClass: 'logoSecurity', }); + useKibanaMock().services.spaces = { ui: spacesUiApiMock } as unknown as SpacesApi; }); it('should render CaseComponent', async () => { @@ -395,36 +405,7 @@ describe('CaseView ', () => { })); const wrapper = mount( - + ); await waitFor(() => { @@ -439,36 +420,7 @@ describe('CaseView ', () => { })); const wrapper = mount( - + ); await waitFor(() => { @@ -477,43 +429,66 @@ describe('CaseView ', () => { }); it('should return case view when data is there', async () => { - (useGetCase as jest.Mock).mockImplementation(() => defaultGetCase); + (useGetCase as jest.Mock).mockImplementation(() => ({ + ...defaultGetCase, + resolveOutcome: 'exactMatch', + })); const wrapper = mount( - + ); await waitFor(() => { expect(wrapper.find('[data-test-subj="case-view-title"]').exists()).toBeTruthy(); + expect(spacesUiApiMock.components.getLegacyUrlConflict).not.toHaveBeenCalled(); + expect(spacesUiApiMock.redirectLegacyUrl).not.toHaveBeenCalled(); + }); + }); + + it('should redirect case view when resolves to alias match', async () => { + const resolveAliasId = `${defaultGetCase.data.id}_2`; + (useGetCase as jest.Mock).mockImplementation(() => ({ + ...defaultGetCase, + resolveOutcome: 'aliasMatch', + resolveAliasId, + })); + const wrapper = mount( + + + + ); + await waitFor(() => { + expect(wrapper.find('[data-test-subj="case-view-title"]').exists()).toBeTruthy(); + expect(spacesUiApiMock.components.getLegacyUrlConflict).not.toHaveBeenCalled(); + expect(spacesUiApiMock.redirectLegacyUrl).toHaveBeenCalledWith( + `cases/${resolveAliasId}`, + 'case' + ); + }); + }); + + it('should redirect case view when resolves to conflict', async () => { + const resolveAliasId = `${defaultGetCase.data.id}_2`; + (useGetCase as jest.Mock).mockImplementation(() => ({ + ...defaultGetCase, + resolveOutcome: 'conflict', + resolveAliasId, + })); + const wrapper = mount( + + + + ); + await waitFor(() => { + expect(wrapper.find('[data-test-subj="case-view-title"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="conflict-component"]').exists()).toBeTruthy(); + expect(spacesUiApiMock.redirectLegacyUrl).not.toHaveBeenCalled(); + expect(spacesUiApiMock.components.getLegacyUrlConflict).toHaveBeenCalledWith({ + objectNoun: 'case', + currentObjectId: defaultGetCase.data.id, + otherObjectId: resolveAliasId, + otherObjectPath: `cases/${resolveAliasId}`, + }); }); }); @@ -521,41 +496,12 @@ describe('CaseView ', () => { (useGetCase as jest.Mock).mockImplementation(() => defaultGetCase); const wrapper = mount( - + ); wrapper.find('[data-test-subj="case-refresh"]').first().simulate('click'); await waitFor(() => { - expect(fetchCaseUserActions).toBeCalledWith('1234', 'resilient-2', undefined); + expect(fetchCaseUserActions).toBeCalledWith(caseProps.caseData.id, 'resilient-2', undefined); expect(fetchCase).toBeCalled(); }); }); diff --git a/x-pack/plugins/cases/public/components/case_view/index.tsx b/x-pack/plugins/cases/public/components/case_view/index.tsx index bb0b894238b9d..81e7607c9011f 100644 --- a/x-pack/plugins/cases/public/components/case_view/index.tsx +++ b/x-pack/plugins/cases/public/components/case_view/index.tsx @@ -40,6 +40,7 @@ import { CasesNavigation } from '../links'; import { OwnerProvider } from '../owner_context'; import { getConnectorById } from '../utils'; import { DoesNotExist } from './does_not_exist'; +import { useKibana } from '../../common/lib/kibana'; export interface CaseViewComponentProps { allCasesNavigation: CasesNavigation; @@ -499,6 +500,14 @@ export const CaseComponent = React.memo( } ); +export const CaseViewLoading = () => ( + + + + + +); + export const CaseView = React.memo( ({ allCasesNavigation, @@ -518,27 +527,59 @@ export const CaseView = React.memo( refreshRef, hideSyncAlerts, }: CaseViewProps) => { - const { data, isLoading, isError, fetchCase, updateCase } = useGetCase(caseId, subCaseId); - if (isError) { - return ; - } - if (isLoading) { - return ( - - - - - - ); - } - if (onCaseDataSuccess && data) { - onCaseDataSuccess(data); - } + const { data, resolveOutcome, resolveAliasId, isLoading, isError, fetchCase, updateCase } = + useGetCase(caseId, subCaseId); + const { spaces: spacesApi, http } = useKibana().services; - return ( + useEffect(() => { + if (onCaseDataSuccess && data) { + onCaseDataSuccess(data); + } + }, [data, onCaseDataSuccess]); + + useEffect(() => { + if (spacesApi && resolveOutcome === 'aliasMatch' && resolveAliasId != null) { + // CAUTION: the path /cases/:detailName is working in both Observability (/app/observability/cases/:detailName) and + // Security Solutions (/app/security/cases/:detailName) plugins. This will need to be changed if this component is loaded + // under any another path, passing a path builder function by props from every parent plugin. + const newPath = http.basePath.prepend( + `cases/${resolveAliasId}${window.location.search}${window.location.hash}` + ); + spacesApi.ui.redirectLegacyUrl(newPath, i18n.CASE); + } + }, [resolveOutcome, resolveAliasId, spacesApi, http]); + + const getLegacyUrlConflictCallout = useCallback(() => { + // This function returns a callout component *if* we have encountered a "legacy URL conflict" scenario + if (data && spacesApi && resolveOutcome === 'conflict' && resolveAliasId != null) { + // We have resolved to one object, but another object has a legacy URL alias associated with this ID/page. We should display a + // callout with a warning for the user, and provide a way for them to navigate to the other object. + const otherObjectId = resolveAliasId; // This is always defined if outcome === 'conflict' + // CAUTION: the path /cases/:detailName is working in both Observability (/app/observability/cases/:detailName) and + // Security Solutions (/app/security/cases/:detailName) plugins. This will need to be changed if this component is loaded + // under any another path, passing a path builder function by props from every parent plugin. + const otherObjectPath = http.basePath.prepend( + `cases/${otherObjectId}${window.location.search}${window.location.hash}` + ); + return spacesApi.ui.components.getLegacyUrlConflict({ + objectNoun: i18n.CASE, + currentObjectId: data.id, + otherObjectId, + otherObjectPath, + }); + } + return null; + }, [data, resolveAliasId, resolveOutcome, spacesApi, http.basePath]); + + return isError ? ( + + ) : isLoading ? ( + + ) : ( data && ( + {getLegacyUrlConflictCallout()} => Promise.resolve(basicCase); +export const resolveCase = async ( + caseId: string, + includeComments: boolean = true, + signal: AbortSignal +): Promise => Promise.resolve(basicResolvedCase); + export const getCasesStatus = async (signal: AbortSignal): Promise => Promise.resolve(casesStatus); diff --git a/x-pack/plugins/cases/public/containers/api.test.tsx b/x-pack/plugins/cases/public/containers/api.test.tsx index e47930e81fe6b..654ade308ed44 100644 --- a/x-pack/plugins/cases/public/containers/api.test.tsx +++ b/x-pack/plugins/cases/public/containers/api.test.tsx @@ -30,6 +30,7 @@ import { postCase, postComment, pushCase, + resolveCase, } from './api'; import { @@ -68,7 +69,7 @@ describe('Case Configuration API', () => { }); const data = ['1', '2']; - test('check url, method, signal', async () => { + test('should be called with correct check url, method, signal', async () => { await deleteCases(data, abortCtrl.signal); expect(fetchMock).toHaveBeenCalledWith(`${CASES_URL}`, { method: 'DELETE', @@ -77,7 +78,7 @@ describe('Case Configuration API', () => { }); }); - test('happy path', async () => { + test('should return correct response', async () => { const resp = await deleteCases(data, abortCtrl.signal); expect(resp).toEqual(''); }); @@ -89,7 +90,7 @@ describe('Case Configuration API', () => { fetchMock.mockResolvedValue(actionLicenses); }); - test('check url, method, signal', async () => { + test('should be called with correct check url, method, signal', async () => { await getActionLicense(abortCtrl.signal); expect(fetchMock).toHaveBeenCalledWith(`/api/actions/connector_types`, { method: 'GET', @@ -97,7 +98,7 @@ describe('Case Configuration API', () => { }); }); - test('happy path', async () => { + test('should return correct response', async () => { const resp = await getActionLicense(abortCtrl.signal); expect(resp).toEqual(actionLicenses); }); @@ -110,7 +111,7 @@ describe('Case Configuration API', () => { }); const data = basicCase.id; - test('check url, method, signal', async () => { + test('should be called with correct check url, method, signal', async () => { await getCase(data, true, abortCtrl.signal); expect(fetchMock).toHaveBeenCalledWith(`${CASES_URL}/${basicCase.id}`, { method: 'GET', @@ -119,18 +120,46 @@ describe('Case Configuration API', () => { }); }); - test('happy path', async () => { + test('should return correct response', async () => { const resp = await getCase(data, true, abortCtrl.signal); expect(resp).toEqual(basicCase); }); }); + describe('resolveCase', () => { + const targetAliasId = '12345'; + const basicResolveCase = { + outcome: 'aliasMatch', + case: basicCaseSnake, + }; + const caseId = basicCase.id; + + beforeEach(() => { + fetchMock.mockClear(); + fetchMock.mockResolvedValue({ ...basicResolveCase, target_alias_id: targetAliasId }); + }); + + test('should be called with correct check url, method, signal', async () => { + await resolveCase(caseId, true, abortCtrl.signal); + expect(fetchMock).toHaveBeenCalledWith(`${CASES_URL}/${caseId}/resolve`, { + method: 'GET', + query: { includeComments: true }, + signal: abortCtrl.signal, + }); + }); + + test('should return correct response', async () => { + const resp = await resolveCase(caseId, true, abortCtrl.signal); + expect(resp).toEqual({ ...basicResolveCase, case: basicCase, targetAliasId }); + }); + }); + describe('getCases', () => { beforeEach(() => { fetchMock.mockClear(); fetchMock.mockResolvedValue(allCasesSnake); }); - test('check url, method, signal', async () => { + test('should be called with correct check url, method, signal', async () => { await getCases({ filterOptions: { ...DEFAULT_FILTER_OPTIONS, owner: [SECURITY_SOLUTION_OWNER] }, queryParams: DEFAULT_QUERY_PARAMS, @@ -148,7 +177,7 @@ describe('Case Configuration API', () => { }); }); - test('correctly applies filters', async () => { + test('should applies correct filters', async () => { await getCases({ filterOptions: { ...DEFAULT_FILTER_OPTIONS, @@ -175,7 +204,7 @@ describe('Case Configuration API', () => { }); }); - test('tags with weird chars get handled gracefully', async () => { + test('should handle tags with weird chars', async () => { const weirdTags: string[] = ['(', '"double"']; await getCases({ @@ -204,7 +233,7 @@ describe('Case Configuration API', () => { }); }); - test('happy path', async () => { + test('should return correct response', async () => { const resp = await getCases({ filterOptions: { ...DEFAULT_FILTER_OPTIONS, owner: [SECURITY_SOLUTION_OWNER] }, queryParams: DEFAULT_QUERY_PARAMS, @@ -219,7 +248,7 @@ describe('Case Configuration API', () => { fetchMock.mockClear(); fetchMock.mockResolvedValue(casesStatusSnake); }); - test('check url, method, signal', async () => { + test('should be called with correct check url, method, signal', async () => { await getCasesStatus(abortCtrl.signal, [SECURITY_SOLUTION_OWNER]); expect(fetchMock).toHaveBeenCalledWith(`${CASES_URL}/status`, { method: 'GET', @@ -228,7 +257,7 @@ describe('Case Configuration API', () => { }); }); - test('happy path', async () => { + test('should return correct response', async () => { const resp = await getCasesStatus(abortCtrl.signal, [SECURITY_SOLUTION_OWNER]); expect(resp).toEqual(casesStatus); }); @@ -240,7 +269,7 @@ describe('Case Configuration API', () => { fetchMock.mockResolvedValue(caseUserActionsSnake); }); - test('check url, method, signal', async () => { + test('should be called with correct check url, method, signal', async () => { await getCaseUserActions(basicCase.id, abortCtrl.signal); expect(fetchMock).toHaveBeenCalledWith(`${CASES_URL}/${basicCase.id}/user_actions`, { method: 'GET', @@ -248,7 +277,7 @@ describe('Case Configuration API', () => { }); }); - test('happy path', async () => { + test('should return correct response', async () => { const resp = await getCaseUserActions(basicCase.id, abortCtrl.signal); expect(resp).toEqual(caseUserActions); }); @@ -260,7 +289,7 @@ describe('Case Configuration API', () => { fetchMock.mockResolvedValue(respReporters); }); - test('check url, method, signal', async () => { + test('should be called with correct check url, method, signal', async () => { await getReporters(abortCtrl.signal, [SECURITY_SOLUTION_OWNER]); expect(fetchMock).toHaveBeenCalledWith(`${CASES_URL}/reporters`, { method: 'GET', @@ -271,7 +300,7 @@ describe('Case Configuration API', () => { }); }); - test('happy path', async () => { + test('should return correct response', async () => { const resp = await getReporters(abortCtrl.signal, [SECURITY_SOLUTION_OWNER]); expect(resp).toEqual(respReporters); }); @@ -283,7 +312,7 @@ describe('Case Configuration API', () => { fetchMock.mockResolvedValue(tags); }); - test('check url, method, signal', async () => { + test('should be called with correct check url, method, signal', async () => { await getTags(abortCtrl.signal, [SECURITY_SOLUTION_OWNER]); expect(fetchMock).toHaveBeenCalledWith(`${CASES_URL}/tags`, { method: 'GET', @@ -294,7 +323,7 @@ describe('Case Configuration API', () => { }); }); - test('happy path', async () => { + test('should return correct response', async () => { const resp = await getTags(abortCtrl.signal, [SECURITY_SOLUTION_OWNER]); expect(resp).toEqual(tags); }); @@ -306,7 +335,7 @@ describe('Case Configuration API', () => { fetchMock.mockResolvedValue([basicCaseSnake]); }); const data = { description: 'updated description' }; - test('check url, method, signal', async () => { + test('should be called with correct check url, method, signal', async () => { await patchCase(basicCase.id, data, basicCase.version, abortCtrl.signal); expect(fetchMock).toHaveBeenCalledWith(`${CASES_URL}`, { method: 'PATCH', @@ -317,7 +346,7 @@ describe('Case Configuration API', () => { }); }); - test('happy path', async () => { + test('should return correct response', async () => { const resp = await patchCase( basicCase.id, { description: 'updated description' }, @@ -341,7 +370,7 @@ describe('Case Configuration API', () => { }, ]; - test('check url, method, signal', async () => { + test('should be called with correct check url, method, signal', async () => { await patchCasesStatus(data, abortCtrl.signal); expect(fetchMock).toHaveBeenCalledWith(`${CASES_URL}`, { method: 'PATCH', @@ -350,7 +379,7 @@ describe('Case Configuration API', () => { }); }); - test('happy path', async () => { + test('should return correct response', async () => { const resp = await patchCasesStatus(data, abortCtrl.signal); expect(resp).toEqual({ ...cases }); }); @@ -362,7 +391,7 @@ describe('Case Configuration API', () => { fetchMock.mockResolvedValue(basicCaseSnake); }); - test('check url, method, signal', async () => { + test('should be called with correct check url, method, signal', async () => { await patchComment({ caseId: basicCase.id, commentId: basicCase.comments[0].id, @@ -384,7 +413,7 @@ describe('Case Configuration API', () => { }); }); - test('happy path', async () => { + test('should return correct response', async () => { const resp = await patchComment({ caseId: basicCase.id, commentId: basicCase.comments[0].id, @@ -418,7 +447,7 @@ describe('Case Configuration API', () => { owner: SECURITY_SOLUTION_OWNER, }; - test('check url, method, signal', async () => { + test('should be called with correct check url, method, signal', async () => { await postCase(data, abortCtrl.signal); expect(fetchMock).toHaveBeenCalledWith(`${CASES_URL}`, { method: 'POST', @@ -427,7 +456,7 @@ describe('Case Configuration API', () => { }); }); - test('happy path', async () => { + test('should return correct response', async () => { const resp = await postCase(data, abortCtrl.signal); expect(resp).toEqual(basicCase); }); @@ -444,7 +473,7 @@ describe('Case Configuration API', () => { type: CommentType.user as const, }; - test('check url, method, signal', async () => { + test('should be called with correct check url, method, signal', async () => { await postComment(data, basicCase.id, abortCtrl.signal); expect(fetchMock).toHaveBeenCalledWith(`${CASES_URL}/${basicCase.id}/comments`, { method: 'POST', @@ -453,7 +482,7 @@ describe('Case Configuration API', () => { }); }); - test('happy path', async () => { + test('should return correct response', async () => { const resp = await postComment(data, basicCase.id, abortCtrl.signal); expect(resp).toEqual(basicCase); }); @@ -467,7 +496,7 @@ describe('Case Configuration API', () => { fetchMock.mockResolvedValue(pushedCaseSnake); }); - test('check url, method, signal', async () => { + test('should be called with correct check url, method, signal', async () => { await pushCase(basicCase.id, connectorId, abortCtrl.signal); expect(fetchMock).toHaveBeenCalledWith( `${CASES_URL}/${basicCase.id}/connector/${connectorId}/_push`, @@ -479,7 +508,7 @@ describe('Case Configuration API', () => { ); }); - test('happy path', async () => { + test('should return correct response', async () => { const resp = await pushCase(basicCase.id, connectorId, abortCtrl.signal); expect(resp).toEqual(pushedCase); }); diff --git a/x-pack/plugins/cases/public/containers/api.ts b/x-pack/plugins/cases/public/containers/api.ts index 51a68376936af..75e8c8f58705d 100644 --- a/x-pack/plugins/cases/public/containers/api.ts +++ b/x-pack/plugins/cases/public/containers/api.ts @@ -14,6 +14,7 @@ import { CasePatchRequest, CasePostRequest, CaseResponse, + CaseResolveResponse, CASES_URL, CasesFindResponse, CasesResponse, @@ -35,6 +36,7 @@ import { SubCaseResponse, SubCasesResponse, User, + ResolvedCase, } from '../../common'; import { getAllConnectorTypesUrl } from '../../common/utils/connectors_api'; @@ -61,6 +63,7 @@ import { decodeCasesFindResponse, decodeCasesStatusResponse, decodeCaseUserActionsResponse, + decodeCaseResolveResponse, } from './utils'; export const getCase = async ( @@ -78,6 +81,24 @@ export const getCase = async ( return convertToCamelCase(decodeCaseResponse(response)); }; +export const resolveCase = async ( + caseId: string, + includeComments: boolean = true, + signal: AbortSignal +): Promise => { + const response = await KibanaServices.get().http.fetch( + getCaseDetailsUrl(caseId) + '/resolve', + { + method: 'GET', + query: { + includeComments, + }, + signal, + } + ); + return convertToCamelCase(decodeCaseResolveResponse(response)); +}; + export const getSubCase = async ( caseId: string, subCaseId: string, diff --git a/x-pack/plugins/cases/public/containers/mock.ts b/x-pack/plugins/cases/public/containers/mock.ts index fcd564969d486..f7d1daabd60ea 100644 --- a/x-pack/plugins/cases/public/containers/mock.ts +++ b/x-pack/plugins/cases/public/containers/mock.ts @@ -20,6 +20,7 @@ import { CommentResponse, CommentType, ConnectorTypes, + ResolvedCase, isCreateConnector, isPush, isUpdateConnector, @@ -163,6 +164,12 @@ export const basicCase: Case = { subCaseIds: [], }; +export const basicResolvedCase: ResolvedCase = { + case: basicCase, + outcome: 'aliasMatch', + aliasTargetId: `${basicCase.id}_2`, +}; + export const collectionCase: Case = { type: CaseType.collection, owner: SECURITY_SOLUTION_OWNER, diff --git a/x-pack/plugins/cases/public/containers/use_get_case.test.tsx b/x-pack/plugins/cases/public/containers/use_get_case.test.tsx index c88f530709c8a..e825e232aebdc 100644 --- a/x-pack/plugins/cases/public/containers/use_get_case.test.tsx +++ b/x-pack/plugins/cases/public/containers/use_get_case.test.tsx @@ -7,7 +7,7 @@ import { renderHook, act } from '@testing-library/react-hooks'; import { useGetCase, UseGetCase } from './use_get_case'; -import { basicCase } from './mock'; +import { basicCase, basicResolvedCase } from './mock'; import * as api from './api'; jest.mock('./api'); @@ -28,6 +28,7 @@ describe('useGetCase', () => { await waitForNextUpdate(); expect(result.current).toEqual({ data: null, + resolveOutcome: null, isLoading: false, isError: false, fetchCase: result.current.fetchCase, @@ -36,13 +37,13 @@ describe('useGetCase', () => { }); }); - it('calls getCase with correct arguments', async () => { - const spyOnGetCase = jest.spyOn(api, 'getCase'); + it('calls resolveCase with correct arguments', async () => { + const spyOnResolveCase = jest.spyOn(api, 'resolveCase'); await act(async () => { const { waitForNextUpdate } = renderHook(() => useGetCase(basicCase.id)); await waitForNextUpdate(); await waitForNextUpdate(); - expect(spyOnGetCase).toBeCalledWith(basicCase.id, true, abortCtrl.signal); + expect(spyOnResolveCase).toBeCalledWith(basicCase.id, true, abortCtrl.signal); }); }); @@ -55,6 +56,8 @@ describe('useGetCase', () => { await waitForNextUpdate(); expect(result.current).toEqual({ data: basicCase, + resolveOutcome: basicResolvedCase.outcome, + resolveAliasId: basicResolvedCase.aliasTargetId, isLoading: false, isError: false, fetchCase: result.current.fetchCase, @@ -64,7 +67,7 @@ describe('useGetCase', () => { }); it('refetch case', async () => { - const spyOnGetCase = jest.spyOn(api, 'getCase'); + const spyOnResolveCase = jest.spyOn(api, 'resolveCase'); await act(async () => { const { result, waitForNextUpdate } = renderHook(() => useGetCase(basicCase.id) @@ -72,7 +75,7 @@ describe('useGetCase', () => { await waitForNextUpdate(); await waitForNextUpdate(); result.current.fetchCase(); - expect(spyOnGetCase).toHaveBeenCalledTimes(2); + expect(spyOnResolveCase).toHaveBeenCalledTimes(2); }); }); @@ -103,8 +106,8 @@ describe('useGetCase', () => { }); it('unhappy path', async () => { - const spyOnGetCase = jest.spyOn(api, 'getCase'); - spyOnGetCase.mockImplementation(() => { + const spyOnResolveCase = jest.spyOn(api, 'resolveCase'); + spyOnResolveCase.mockImplementation(() => { throw new Error('Something went wrong'); }); @@ -117,6 +120,7 @@ describe('useGetCase', () => { expect(result.current).toEqual({ data: null, + resolveOutcome: null, isLoading: false, isError: true, fetchCase: result.current.fetchCase, diff --git a/x-pack/plugins/cases/public/containers/use_get_case.tsx b/x-pack/plugins/cases/public/containers/use_get_case.tsx index b9326ad057c9e..52610981a227c 100644 --- a/x-pack/plugins/cases/public/containers/use_get_case.tsx +++ b/x-pack/plugins/cases/public/containers/use_get_case.tsx @@ -7,20 +7,22 @@ import { useEffect, useReducer, useCallback, useRef } from 'react'; -import { Case } from './types'; +import { Case, ResolvedCase } from './types'; import * as i18n from './translations'; import { useToasts } from '../common/lib/kibana'; -import { getCase, getSubCase } from './api'; +import { resolveCase, getSubCase } from './api'; interface CaseState { data: Case | null; + resolveOutcome: ResolvedCase['outcome'] | null; + resolveAliasId?: string; isLoading: boolean; isError: boolean; } type Action = | { type: 'FETCH_INIT'; payload: { silent: boolean } } - | { type: 'FETCH_SUCCESS'; payload: Case } + | { type: 'FETCH_SUCCESS'; payload: ResolvedCase } | { type: 'FETCH_FAILURE' } | { type: 'UPDATE_CASE'; payload: Case }; @@ -40,7 +42,9 @@ const dataFetchReducer = (state: CaseState, action: Action): CaseState => { ...state, isLoading: false, isError: false, - data: action.payload, + data: action.payload.case, + resolveOutcome: action.payload.outcome, + resolveAliasId: action.payload.aliasTargetId, }; case 'FETCH_FAILURE': return { @@ -72,6 +76,7 @@ export const useGetCase = (caseId: string, subCaseId?: string): UseGetCase => { isLoading: false, isError: false, data: null, + resolveOutcome: null, }); const toasts = useToasts(); const isCancelledRef = useRef(false); @@ -89,9 +94,12 @@ export const useGetCase = (caseId: string, subCaseId?: string): UseGetCase => { abortCtrlRef.current = new AbortController(); dispatch({ type: 'FETCH_INIT', payload: { silent } }); - const response = await (subCaseId - ? getSubCase(caseId, subCaseId, true, abortCtrlRef.current.signal) - : getCase(caseId, true, abortCtrlRef.current.signal)); + const response: ResolvedCase = subCaseId + ? { + case: await getSubCase(caseId, subCaseId, true, abortCtrlRef.current.signal), + outcome: 'exactMatch', // sub-cases are not resolved, forced to exactMatch always + } + : await resolveCase(caseId, true, abortCtrlRef.current.signal); if (!isCancelledRef.current) { dispatch({ type: 'FETCH_SUCCESS', payload: response }); diff --git a/x-pack/plugins/cases/public/containers/utils.ts b/x-pack/plugins/cases/public/containers/utils.ts index b0cc0c72fee78..458899e5f53c9 100644 --- a/x-pack/plugins/cases/public/containers/utils.ts +++ b/x-pack/plugins/cases/public/containers/utils.ts @@ -30,6 +30,8 @@ import { CaseUserActionsResponseRt, CommentType, CasePatchRequest, + CaseResolveResponse, + CaseResolveResponseRt, } from '../../common'; import { AllCases, Case, UpdateByKey } from './types'; import * as i18n from './translations'; @@ -80,6 +82,12 @@ export const createToasterPlainError = (message: string) => new ToasterError([me export const decodeCaseResponse = (respCase?: CaseResponse) => pipe(CaseResponseRt.decode(respCase), fold(throwErrors(createToasterPlainError), identity)); +export const decodeCaseResolveResponse = (respCase?: CaseResolveResponse) => + pipe( + CaseResolveResponseRt.decode(respCase), + fold(throwErrors(createToasterPlainError), identity) + ); + export const decodeCasesResponse = (respCase?: CasesResponse) => pipe(CasesResponseRt.decode(respCase), fold(throwErrors(createToasterPlainError), identity)); diff --git a/x-pack/plugins/cases/public/types.ts b/x-pack/plugins/cases/public/types.ts index db2e5d6ab6bff..5b19bcfa8ac46 100644 --- a/x-pack/plugins/cases/public/types.ts +++ b/x-pack/plugins/cases/public/types.ts @@ -16,6 +16,7 @@ import type { } from '../../triggers_actions_ui/public'; import type { DataPublicPluginStart } from '../../../../src/plugins/data/public'; import type { EmbeddableStart } from '../../../../src/plugins/embeddable/public'; +import type { SpacesPluginStart } from '../../spaces/public'; import type { Storage } from '../../../../src/plugins/kibana_utils/public'; import { AllCasesProps } from './components/all_cases'; @@ -36,6 +37,7 @@ export interface StartPlugins { lens: LensPublicStart; storage: Storage; triggersActionsUi: TriggersActionsStart; + spaces?: SpacesPluginStart; } /** diff --git a/x-pack/plugins/cases/server/authorization/__snapshots__/audit_logger.test.ts.snap b/x-pack/plugins/cases/server/authorization/__snapshots__/audit_logger.test.ts.snap index 3ca77944776b3..50c085b7f22a8 100644 --- a/x-pack/plugins/cases/server/authorization/__snapshots__/audit_logger.test.ts.snap +++ b/x-pack/plugins/cases/server/authorization/__snapshots__/audit_logger.test.ts.snap @@ -1596,6 +1596,90 @@ Object { } `; +exports[`audit_logger log function event structure creates the correct audit event for operation: "resolveCase" with an error and entity 1`] = ` +Object { + "error": Object { + "code": "Error", + "message": "an error", + }, + "event": Object { + "action": "case_resolve", + "category": Array [ + "database", + ], + "outcome": "failure", + "type": Array [ + "access", + ], + }, + "kibana": Object { + "saved_object": Object { + "id": "1", + "type": "cases", + }, + }, + "message": "Failed attempt to access cases [id=1] as owner \\"awesome\\"", +} +`; + +exports[`audit_logger log function event structure creates the correct audit event for operation: "resolveCase" with an error but no entity 1`] = ` +Object { + "error": Object { + "code": "Error", + "message": "an error", + }, + "event": Object { + "action": "case_resolve", + "category": Array [ + "database", + ], + "outcome": "failure", + "type": Array [ + "access", + ], + }, + "message": "Failed attempt to access a case as any owners", +} +`; + +exports[`audit_logger log function event structure creates the correct audit event for operation: "resolveCase" without an error but with an entity 1`] = ` +Object { + "event": Object { + "action": "case_resolve", + "category": Array [ + "database", + ], + "outcome": "success", + "type": Array [ + "access", + ], + }, + "kibana": Object { + "saved_object": Object { + "id": "5", + "type": "cases", + }, + }, + "message": "User has accessed cases [id=5] as owner \\"super\\"", +} +`; + +exports[`audit_logger log function event structure creates the correct audit event for operation: "resolveCase" without an error or entity 1`] = ` +Object { + "event": Object { + "action": "case_resolve", + "category": Array [ + "database", + ], + "outcome": "success", + "type": Array [ + "access", + ], + }, + "message": "User has accessed a case as any owners", +} +`; + exports[`audit_logger log function event structure creates the correct audit event for operation: "updateCase" with an error and entity 1`] = ` Object { "error": Object { diff --git a/x-pack/plugins/cases/server/authorization/index.ts b/x-pack/plugins/cases/server/authorization/index.ts index 90b89c7f75766..1a74640515173 100644 --- a/x-pack/plugins/cases/server/authorization/index.ts +++ b/x-pack/plugins/cases/server/authorization/index.ts @@ -152,6 +152,14 @@ export const Operations: Record Promise; */ export enum ReadOperations { GetCase = 'getCase', + ResolveCase = 'resolveCase', FindCases = 'findCases', GetCaseIDsByAlertID = 'getCaseIDsByAlertID', GetCaseStatuses = 'getCaseStatuses', diff --git a/x-pack/plugins/cases/server/client/cases/client.ts b/x-pack/plugins/cases/server/client/cases/client.ts index 0932308c2e269..fd9bd489f31b2 100644 --- a/x-pack/plugins/cases/server/client/cases/client.ts +++ b/x-pack/plugins/cases/server/client/cases/client.ts @@ -18,6 +18,7 @@ import { CasesClient } from '../client'; import { CasesClientInternal } from '../client_internal'; import { ICasePostRequest, + ICaseResolveResponse, ICaseResponse, ICasesFindRequest, ICasesFindResponse, @@ -31,6 +32,7 @@ import { find } from './find'; import { CasesByAlertIDParams, get, + resolve, getCasesByAlertID, GetParams, getReporters, @@ -57,6 +59,11 @@ export interface CasesSubClient { * Retrieves a single case with the specified ID. */ get(params: GetParams): Promise; + /** + * @experimental + * Retrieves a single case resolving the specified ID. + */ + resolve(params: GetParams): Promise; /** * Pushes a specific case to an external system. */ @@ -99,6 +106,7 @@ export const createCasesSubClient = ( create: (data: CasePostRequest) => create(data, clientArgs), find: (params: CasesFindRequest) => find(params, clientArgs), get: (params: GetParams) => get(params, clientArgs), + resolve: (params: GetParams) => resolve(params, clientArgs), push: (params: PushParams) => push(params, clientArgs, casesClient, casesClientInternal), update: (cases: CasesPatchRequest) => update(cases, clientArgs, casesClientInternal), delete: (ids: string[]) => deleteCases(ids, clientArgs), diff --git a/x-pack/plugins/cases/server/client/cases/get.ts b/x-pack/plugins/cases/server/client/cases/get.ts index 6b0015d4ffb14..c6ab033c2a848 100644 --- a/x-pack/plugins/cases/server/client/cases/get.ts +++ b/x-pack/plugins/cases/server/client/cases/get.ts @@ -9,10 +9,12 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { fold } from 'fp-ts/lib/Either'; import { identity } from 'fp-ts/lib/function'; -import { SavedObject } from 'kibana/server'; +import { SavedObject, SavedObjectsResolveResponse } from 'kibana/server'; import { CaseResponseRt, CaseResponse, + CaseResolveResponseRt, + CaseResolveResponse, User, UsersRt, AllTagsFindRequest, @@ -230,6 +232,86 @@ export const get = async ( } }; +/** + * Retrieves a case resolving its ID and optionally loading its comments and sub case comments. + * + * @experimental + */ +export const resolve = async ( + { id, includeComments, includeSubCaseComments }: GetParams, + clientArgs: CasesClientArgs +): Promise => { + const { unsecuredSavedObjectsClient, caseService, logger, authorization } = clientArgs; + + try { + if (!ENABLE_CASE_CONNECTOR && includeSubCaseComments) { + throw Boom.badRequest( + 'The `includeSubCaseComments` is not supported when the case connector feature is disabled' + ); + } + + const { + saved_object: savedObject, + ...resolveData + }: SavedObjectsResolveResponse = await caseService.getResolveCase({ + unsecuredSavedObjectsClient, + id, + }); + + await authorization.ensureAuthorized({ + operation: Operations.resolveCase, + entities: [ + { + id: savedObject.id, + owner: savedObject.attributes.owner, + }, + ], + }); + + let subCaseIds: string[] = []; + if (ENABLE_CASE_CONNECTOR) { + const subCasesForCaseId = await caseService.findSubCasesByCaseId({ + unsecuredSavedObjectsClient, + ids: [id], + }); + subCaseIds = subCasesForCaseId.saved_objects.map((so) => so.id); + } + + if (!includeComments) { + return CaseResolveResponseRt.encode({ + ...resolveData, + case: flattenCaseSavedObject({ + savedObject, + subCaseIds, + }), + }); + } + + const theComments = await caseService.getAllCaseComments({ + unsecuredSavedObjectsClient, + id, + options: { + sortField: 'created_at', + sortOrder: 'asc', + }, + includeSubCaseComments: ENABLE_CASE_CONNECTOR && includeSubCaseComments, + }); + + return CaseResolveResponseRt.encode({ + ...resolveData, + case: flattenCaseSavedObject({ + savedObject, + subCaseIds, + comments: theComments.saved_objects, + totalComment: theComments.total, + totalAlerts: countAlertsForID({ comments: theComments, id }), + }), + }); + } catch (error) { + throw createCaseError({ message: `Failed to resolve case id: ${id}: ${error}`, error, logger }); + } +}; + /** * Retrieves the tags from all the cases. */ diff --git a/x-pack/plugins/cases/server/client/mocks.ts b/x-pack/plugins/cases/server/client/mocks.ts index 05b7d055656b1..f0ca7ae9eaf71 100644 --- a/x-pack/plugins/cases/server/client/mocks.ts +++ b/x-pack/plugins/cases/server/client/mocks.ts @@ -22,6 +22,7 @@ const createCasesSubClientMock = (): CasesSubClientMock => { return { create: jest.fn(), find: jest.fn(), + resolve: jest.fn(), get: jest.fn(), push: jest.fn(), update: jest.fn(), diff --git a/x-pack/plugins/cases/server/client/typedoc_interfaces.ts b/x-pack/plugins/cases/server/client/typedoc_interfaces.ts index bf444ee9420ed..feeaa6b6dcb58 100644 --- a/x-pack/plugins/cases/server/client/typedoc_interfaces.ts +++ b/x-pack/plugins/cases/server/client/typedoc_interfaces.ts @@ -16,6 +16,7 @@ import { AllCommentsResponse, CasePostRequest, + CaseResolveResponse, CaseResponse, CasesConfigurePatch, CasesConfigureRequest, @@ -40,6 +41,7 @@ export interface ICasePostRequest extends CasePostRequest {} export interface ICasesFindRequest extends CasesFindRequest {} export interface ICasesPatchRequest extends CasesPatchRequest {} export interface ICaseResponse extends CaseResponse {} +export interface ICaseResolveResponse extends CaseResolveResponse {} export interface ICasesResponse extends CasesResponse {} export interface ICasesFindResponse extends CasesFindResponse {} diff --git a/x-pack/plugins/cases/server/routes/api/cases/get_case.ts b/x-pack/plugins/cases/server/routes/api/cases/get_case.ts index 2313c3cad9007..4d81b6d5e11b3 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/get_case.ts +++ b/x-pack/plugins/cases/server/routes/api/cases/get_case.ts @@ -45,4 +45,38 @@ export function initGetCaseApi({ router, logger }: RouteDeps) { } } ); + + router.get( + { + path: `${CASE_DETAILS_URL}/resolve`, + validate: { + params: schema.object({ + case_id: schema.string(), + }), + query: schema.object({ + includeComments: schema.boolean({ defaultValue: true }), + includeSubCaseComments: schema.maybe(schema.boolean({ defaultValue: false })), + }), + }, + }, + async (context, request, response) => { + try { + const casesClient = await context.cases.getCasesClient(); + const id = request.params.case_id; + + return response.ok({ + body: await casesClient.cases.resolve({ + id, + includeComments: request.query.includeComments, + includeSubCaseComments: request.query.includeSubCaseComments, + }), + }); + } catch (error) { + logger.error( + `Failed to retrieve case in resolve route case id: ${request.params.case_id} \ninclude comments: ${request.query.includeComments} \ninclude sub comments: ${request.query.includeSubCaseComments}: ${error}` + ); + return response.customError(wrapError(error)); + } + } + ); } diff --git a/x-pack/plugins/cases/server/services/cases/index.ts b/x-pack/plugins/cases/server/services/cases/index.ts index 72c2033f83535..3c76be6d6dd93 100644 --- a/x-pack/plugins/cases/server/services/cases/index.ts +++ b/x-pack/plugins/cases/server/services/cases/index.ts @@ -16,6 +16,7 @@ import { SavedObjectsFindResult, SavedObjectsBulkUpdateResponse, SavedObjectsUpdateResponse, + SavedObjectsResolveResponse, } from 'kibana/server'; import type { estypes } from '@elastic/elasticsearch'; @@ -738,6 +739,27 @@ export class CasesService { throw error; } } + + public async getResolveCase({ + unsecuredSavedObjectsClient, + id: caseId, + }: GetCaseArgs): Promise> { + try { + this.log.debug(`Attempting to resolve case ${caseId}`); + const resolveCaseResult = await unsecuredSavedObjectsClient.resolve( + CASE_SAVED_OBJECT, + caseId + ); + return { + ...resolveCaseResult, + saved_object: transformSavedObjectToExternalModel(resolveCaseResult.saved_object), + }; + } catch (error) { + this.log.error(`Error on resolve case ${caseId}: ${error}`); + throw error; + } + } + public async getSubCase({ unsecuredSavedObjectsClient, id, diff --git a/x-pack/plugins/cases/server/services/mocks.ts b/x-pack/plugins/cases/server/services/mocks.ts index a29d227cfbb0f..1ea9f481d302f 100644 --- a/x-pack/plugins/cases/server/services/mocks.ts +++ b/x-pack/plugins/cases/server/services/mocks.ts @@ -36,6 +36,7 @@ export const createCaseServiceMock = (): CaseServiceMock => { getCases: jest.fn(), getCaseIdsByAlertId: jest.fn(), getMostRecentSubCase: jest.fn(), + getResolveCase: jest.fn(), getSubCase: jest.fn(), getSubCases: jest.fn(), getTags: jest.fn(), diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/error_state/error_state_prompt.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/error_state/error_state_prompt.tsx index 3fbae0a564c17..2ecb6bffc8212 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/error_state/error_state_prompt.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/error_state/error_state_prompt.tsx @@ -69,13 +69,13 @@ export const ErrorStatePrompt: React.FC = () => {
  • diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/__mocks__/content_sources.mock.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/__mocks__/content_sources.mock.ts index 5f515fc99769c..28e796c256396 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/__mocks__/content_sources.mock.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/__mocks__/content_sources.mock.ts @@ -25,6 +25,7 @@ export const contentSources = [ allowsReauth: true, boost: 1, activities: [], + isOauth1: false, }, { id: '124', @@ -40,6 +41,7 @@ export const contentSources = [ allowsReauth: true, boost: 0.5, activities: [], + isOauth1: true, }, ]; @@ -303,6 +305,7 @@ export const sourceConfigData = { privateSourcesEnabled: false, categories: ['wiki', 'atlassian', 'intranet'], configuredFields: { + isOauth1: false, clientId: 'CyztADsSECRETCSAUCEh1a', clientSecret: 'GSjJxqSECRETCSAUCEksHk', baseUrl: 'https://mine.atlassian.net', diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/source_config_fields/source_config_fields.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/source_config_fields/source_config_fields.test.tsx index 9af91107d7304..9aa0286b2bef0 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/source_config_fields/source_config_fields.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/source_config_fields/source_config_fields.test.tsx @@ -25,6 +25,7 @@ describe('SourceConfigFields', () => { it('renders with all items, hiding API Keys', () => { const wrapper = shallow( { it('shows API keys', () => { const wrapper = shallow( - + ); expect(wrapper.find(ApiKey)).toHaveLength(2); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/source_config_fields/source_config_fields.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/source_config_fields/source_config_fields.tsx index 236d475b8f687..e33e7817b5209 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/source_config_fields/source_config_fields.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/source_config_fields/source_config_fields.tsx @@ -20,6 +20,7 @@ import { ApiKey } from '../api_key'; import { CredentialItem } from '../credential_item'; interface SourceConfigFieldsProps { + isOauth1?: boolean; clientId?: string; clientSecret?: string; publicKey?: string; @@ -28,14 +29,13 @@ interface SourceConfigFieldsProps { } export const SourceConfigFields: React.FC = ({ + isOauth1, clientId, clientSecret, publicKey, consumerKey, baseUrl, }) => { - const showApiKey = (publicKey || consumerKey) && !clientId; - const credentialItem = (label: string, item?: string) => item && ; @@ -58,10 +58,10 @@ export const SourceConfigFields: React.FC = ({ return ( <> - {showApiKey && keyElement} - {credentialItem(CLIENT_ID_LABEL, clientId)} + {isOauth1 && keyElement} + {!isOauth1 && credentialItem(CLIENT_ID_LABEL, clientId)} - {credentialItem(CLIENT_SECRET_LABEL, clientSecret)} + {!isOauth1 && credentialItem(CLIENT_SECRET_LABEL, clientSecret)} {credentialItem(BASE_URL_LABEL, baseUrl)} diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/types.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/types.ts index d8fcb414cff75..c524bd4f7617a 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/types.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/types.ts @@ -108,6 +108,7 @@ export interface ContentSourceDetails extends ContentSource { allowsReauth: boolean; boost: number; activities: SourceActivity[]; + isOauth1: boolean; } interface DescriptionList { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_settings.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_settings.tsx index afcb2fc741909..36a6374dddcd6 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_settings.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_settings.tsx @@ -77,6 +77,7 @@ export const SourceSettings: React.FC = () => { custom: isCustom, isIndexedSource, areThumbnailsConfigEnabled, + isOauth1, indexing: { enabled, features: { @@ -98,10 +99,9 @@ export const SourceSettings: React.FC = () => { getSourceConfigData(serviceType); }, []); - const { - configuration: { isPublicKey }, - editPath, - } = staticSourceData.find((source) => source.serviceType === serviceType) as SourceDataItem; + const { editPath } = staticSourceData.find( + (source) => source.serviceType === serviceType + ) as SourceDataItem; const [inputValue, setValue] = useState(name); const [confirmModalVisible, setModalVisibility] = useState(false); @@ -207,10 +207,11 @@ export const SourceSettings: React.FC = () => { {showConfig && ( diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/details_page/components/package_policies/package_policies_table.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/details_page/components/package_policies/package_policies_table.tsx index d3a6bb7561d39..9f125533f36c2 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/details_page/components/package_policies/package_policies_table.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/details_page/components/package_policies/package_policies_table.tsx @@ -102,6 +102,7 @@ export const PackagePoliciesTable: React.FunctionComponent = ({ { field: 'name', sortable: true, + truncateText: true, name: i18n.translate('xpack.fleet.policyDetails.packagePoliciesTable.nameColumnTitle', { defaultMessage: 'Name', }), diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/filter_dataset.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/filter_dataset.tsx index 18f6a8b565ab9..865c360a47bd9 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/filter_dataset.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/filter_dataset.tsx @@ -37,7 +37,7 @@ export const DatasetFilter: React.FunctionComponent<{ field: DATASET_FIELD, query: '', }); - setDatasetValues(values.sort()); + if (values.length > 0) setDatasetValues(values.sort()); } catch (e) { setDatasetValues([AGENT_DATASET]); } diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/filter_log_level.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/filter_log_level.tsx index b423f3a8a57b3..805d2fab45240 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/filter_log_level.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/filter_log_level.tsx @@ -6,7 +6,7 @@ */ import React, { memo, useState, useEffect, useCallback } from 'react'; -import { EuiPopover, EuiFilterButton, EuiFilterSelectItem } from '@elastic/eui'; +import { EuiPopover, EuiFilterButton, EuiFilterSelectItem, EuiIcon, EuiSpacer } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { useStartServices } from '../../../../../hooks'; @@ -57,6 +57,29 @@ export const LogLevelFilter: React.FunctionComponent<{ fetchValues(); }, [data.autocomplete]); + const noLogsFound = ( +
    +
    + + +

    + {i18n.translate('xpack.fleet.agentLogs.logLevelEmpty', { + defaultMessage: 'No Logs Found', + })} +

    +
    +
    + ); + const filterSelect = levelValues.map((level) => ( + onToggleLevel(level)} + > + {level} + + )); + return ( - {levelValues.map((level) => ( - onToggleLevel(level)} - > - {level} - - ))} + {levelValues.length === 0 ? noLogsFound : filterSelect} ); }); diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/index.test.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/index.test.tsx index 31a3e2164a247..d70b6c68016be 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/index.test.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/index.test.tsx @@ -241,38 +241,6 @@ describe('when on integration detail', () => { 'http://localhost/mock/app/integrations/edit-integration/e8a37031-2907-44f6-89d2-98bd493f60dc' ); }); - - it('should NOT show link for agent count if it is zero', async () => { - await mockedApi.waitForApi(); - const firstRowAgentCount = renderResult.getAllByTestId('rowAgentCount')[0]; - expect(firstRowAgentCount.textContent).toEqual('0'); - expect(firstRowAgentCount.tagName).not.toEqual('A'); - }); - - it('should show add agent button if agent count is zero', async () => { - await mockedApi.waitForApi(); - const firstRowAgentCount = renderResult.getAllByTestId('rowAgentCount')[0]; - expect(firstRowAgentCount.textContent).toEqual('0'); - - const addAgentButton = renderResult.getAllByTestId('addAgentButton')[0]; - expect(addAgentButton).not.toBeNull(); - }); - - it('should show link for agent count if greater than zero', async () => { - await mockedApi.waitForApi(); - const secondRowAgentCount = renderResult.getAllByTestId('rowAgentCount')[1]; - expect(secondRowAgentCount.textContent).toEqual('100'); - expect(secondRowAgentCount.tagName).toEqual('A'); - }); - - it('should NOT show add agent button if agent count is greater than zero', async () => { - await mockedApi.waitForApi(); - const secondRowAgentCount = renderResult.getAllByTestId('rowAgentCount')[1]; - expect(secondRowAgentCount.textContent).toEqual('100'); - - const addAgentButton = renderResult.getAllByTestId('addAgentButton')[1]; - expect(addAgentButton).toBeUndefined(); - }); }); }); diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/policies/components/package_policy_agents_cell.test.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/policies/components/package_policy_agents_cell.test.tsx new file mode 100644 index 0000000000000..8872c61299093 --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/policies/components/package_policy_agents_cell.test.tsx @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { act } from '@testing-library/react'; + +import { createIntegrationsTestRendererMock } from '../../../../../../../../mock'; + +import { PackagePolicyAgentsCell } from './package_policy_agents_cell'; + +function renderCell({ agentCount = 0, agentPolicyId = '123', onAddAgent = () => {} }) { + const renderer = createIntegrationsTestRendererMock(); + + return renderer.render( + + ); +} + +describe('PackagePolicyAgentsCell', () => { + test('it should display add agent if count is 0', async () => { + const utils = renderCell({ agentCount: 0 }); + await act(async () => { + expect(utils.queryByText('Add agent')).toBeInTheDocument(); + }); + }); + + test('it should display only count if count > 0', async () => { + const utils = renderCell({ agentCount: 9999 }); + await act(async () => { + expect(utils.queryByText('Add agent')).not.toBeInTheDocument(); + expect(utils.queryByText('9999')).toBeInTheDocument(); + }); + }); +}); diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/policies/components/package_policy_agents_cell.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/policies/components/package_policy_agents_cell.tsx new file mode 100644 index 0000000000000..37543e7e5ae1b --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/policies/components/package_policy_agents_cell.tsx @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { EuiButton } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; + +import { LinkedAgentCount } from '../../../../../../components'; + +export const PackagePolicyAgentsCell = ({ + agentPolicyId, + agentCount = 0, + onAddAgent, +}: { + agentPolicyId: string; + agentCount?: number; + onAddAgent: () => void; +}) => { + if (agentCount > 0) { + return ( + + ); + } + + return ( + + + + ); +}; diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/policies/package_policies.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/policies/package_policies.tsx index 92b4012011fc8..42eb68099970a 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/policies/package_policies.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/policies/package_policies.tsx @@ -13,19 +13,16 @@ import type { EuiTableFieldDataColumnType, } from '@elastic/eui'; import { - EuiButtonIcon, EuiBasicTable, EuiLink, EuiFlexGroup, EuiFlexItem, - EuiToolTip, EuiText, EuiButton, EuiSpacer, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedRelative, FormattedMessage } from '@kbn/i18n/react'; -import styled from 'styled-components'; import { InstallStatus } from '../../../../../types'; import type { GetAgentPoliciesResponseItem, InMemoryPackagePolicy } from '../../../../../types'; @@ -41,10 +38,10 @@ import { PACKAGE_POLICY_SAVED_OBJECT_TYPE } from '../../../../../constants'; import { AgentEnrollmentFlyout, AgentPolicySummaryLine, - LinkedAgentCount, PackagePolicyActionsMenu, } from '../../../../../components'; +import { PackagePolicyAgentsCell } from './components/package_policy_agents_cell'; import { usePackagePoliciesWithAgentPolicy } from './use_package_policies_with_agent_policy'; import { Persona } from './persona'; @@ -58,10 +55,6 @@ interface InMemoryPackagePolicyAndAgentPolicy { agentPolicy: GetAgentPoliciesResponseItem; } -const AddAgentButton = styled(EuiButtonIcon)` - margin-left: ${(props) => props.theme.eui.euiSizeS}; -`; - const IntegrationDetailsLink = memo<{ packagePolicy: InMemoryPackagePolicyAndAgentPolicy['packagePolicy']; }>(({ packagePolicy }) => { @@ -266,51 +259,6 @@ export const PackagePoliciesPage = ({ name, version }: PackagePoliciesPanelProps return ; }, }, - { - field: '', - name: i18n.translate('xpack.fleet.epm.packageDetails.integrationList.agentCount', { - defaultMessage: 'Agents', - }), - truncateText: true, - align: 'left', - width: '8ch', - render({ packagePolicy, agentPolicy }: InMemoryPackagePolicyAndAgentPolicy) { - const count = agentPolicy?.agents ?? 0; - - return ( - <> - - {count === 0 && ( - - setFlyoutOpenForPolicyId(agentPolicy.id)} - data-test-subj="addAgentButton" - aria-label={i18n.translate( - 'xpack.fleet.epm.packageDetails.integrationList.addAgent', - { - defaultMessage: 'Add Agent', - } - )} - /> - - )} - - ); - }, - }, { field: 'packagePolicy.updated_by', name: i18n.translate('xpack.fleet.epm.packageDetails.integrationList.updatedBy', { @@ -335,6 +283,21 @@ export const PackagePoliciesPage = ({ name, version }: PackagePoliciesPanelProps ); }, }, + { + field: '', + name: i18n.translate('xpack.fleet.epm.packageDetails.integrationList.agentCount', { + defaultMessage: 'Agents', + }), + render({ agentPolicy }: InMemoryPackagePolicyAndAgentPolicy) { + return ( + setFlyoutOpenForPolicyId(agentPolicy.id)} + /> + ); + }, + }, { field: '', name: i18n.translate('xpack.fleet.epm.packageDetails.integrationList.actions', { diff --git a/x-pack/plugins/fleet/public/components/package_policy_actions_menu.tsx b/x-pack/plugins/fleet/public/components/package_policy_actions_menu.tsx index 7dc313970ef20..ee7d5f97fcbac 100644 --- a/x-pack/plugins/fleet/public/components/package_policy_actions_menu.tsx +++ b/x-pack/plugins/fleet/public/components/package_policy_actions_menu.tsx @@ -69,7 +69,7 @@ export const PackagePolicyActionsMenu: React.FunctionComponent<{ > , ] diff --git a/x-pack/plugins/fleet/server/routes/agent_policy/handlers.ts b/x-pack/plugins/fleet/server/routes/agent_policy/handlers.ts index a7cf606e92c0b..b3197d918d231 100644 --- a/x-pack/plugins/fleet/server/routes/agent_policy/handlers.ts +++ b/x-pack/plugins/fleet/server/routes/agent_policy/handlers.ts @@ -136,7 +136,7 @@ export const createAgentPolicyHandler: RequestHandler< }); } - await agentPolicyService.createFleetPolicyChangeAction(soClient, agentPolicy.id); + await agentPolicyService.createFleetServerPolicy(soClient, agentPolicy.id); const body: CreateAgentPolicyResponse = { item: agentPolicy, diff --git a/x-pack/plugins/fleet/server/routes/setup/handlers.test.ts b/x-pack/plugins/fleet/server/routes/setup/handlers.test.ts index 0e22f544ddfa3..bd82989a9e828 100644 --- a/x-pack/plugins/fleet/server/routes/setup/handlers.test.ts +++ b/x-pack/plugins/fleet/server/routes/setup/handlers.test.ts @@ -11,17 +11,17 @@ import type { PostFleetSetupResponse } from '../../../common'; import { RegistryError } from '../../errors'; import { createAppContextStartContractMock, xpackMocks } from '../../mocks'; import { appContextService } from '../../services/app_context'; -import { setupIngestManager } from '../../services/setup'; +import { setupFleet } from '../../services/setup'; import { fleetSetupHandler } from './handlers'; jest.mock('../../services/setup', () => { return { - setupIngestManager: jest.fn(), + setupFleet: jest.fn(), }; }); -const mockSetupIngestManager = setupIngestManager as jest.MockedFunction; +const mockSetupFleet = setupFleet as jest.MockedFunction; describe('FleetSetupHandler', () => { let context: ReturnType; @@ -45,7 +45,7 @@ describe('FleetSetupHandler', () => { }); it('POST /setup succeeds w/200 and body of resolved value', async () => { - mockSetupIngestManager.mockImplementation(() => + mockSetupFleet.mockImplementation(() => Promise.resolve({ isInitialized: true, nonFatalErrors: [], @@ -59,9 +59,7 @@ describe('FleetSetupHandler', () => { }); it('POST /setup fails w/500 on custom error', async () => { - mockSetupIngestManager.mockImplementation(() => - Promise.reject(new Error('SO method mocked to throw')) - ); + mockSetupFleet.mockImplementation(() => Promise.reject(new Error('SO method mocked to throw'))); await fleetSetupHandler(context, request, response); expect(response.customError).toHaveBeenCalledTimes(1); @@ -74,7 +72,7 @@ describe('FleetSetupHandler', () => { }); it('POST /setup fails w/502 on RegistryError', async () => { - mockSetupIngestManager.mockImplementation(() => + mockSetupFleet.mockImplementation(() => Promise.reject(new RegistryError('Registry method mocked to throw')) ); diff --git a/x-pack/plugins/fleet/server/routes/setup/handlers.ts b/x-pack/plugins/fleet/server/routes/setup/handlers.ts index fe1e30f9f05d6..6311b9d970d35 100644 --- a/x-pack/plugins/fleet/server/routes/setup/handlers.ts +++ b/x-pack/plugins/fleet/server/routes/setup/handlers.ts @@ -9,7 +9,7 @@ import type { RequestHandler } from 'src/core/server'; import { appContextService } from '../../services'; import type { GetFleetStatusResponse, PostFleetSetupResponse } from '../../../common'; -import { setupIngestManager } from '../../services/setup'; +import { setupFleet } from '../../services/setup'; import { hasFleetServers } from '../../services/fleet_server'; import { defaultIngestErrorHandler } from '../../errors'; @@ -46,7 +46,7 @@ export const fleetSetupHandler: RequestHandler = async (context, request, respon try { const soClient = context.core.savedObjects.client; const esClient = context.core.elasticsearch.client.asCurrentUser; - const setupStatus = await setupIngestManager(soClient, esClient); + const setupStatus = await setupFleet(soClient, esClient); const body: PostFleetSetupResponse = { ...setupStatus, nonFatalErrors: setupStatus.nonFatalErrors.map((e) => { diff --git a/x-pack/plugins/fleet/server/services/agent_policy.test.ts b/x-pack/plugins/fleet/server/services/agent_policy.test.ts index 6a5cb28dbaa0a..5617f8ef7bd7c 100644 --- a/x-pack/plugins/fleet/server/services/agent_policy.test.ts +++ b/x-pack/plugins/fleet/server/services/agent_policy.test.ts @@ -7,13 +7,16 @@ import { elasticsearchServiceMock, savedObjectsClientMock } from 'src/core/server/mocks'; -import type { AgentPolicy, NewAgentPolicy } from '../types'; +import type { AgentPolicy, FullAgentPolicy, NewAgentPolicy } from '../types'; import { agentPolicyService } from './agent_policy'; import { agentPolicyUpdateEventHandler } from './agent_policy_update'; import { getAgentsByKuery } from './agents'; import { packagePolicyService } from './package_policy'; +import { appContextService } from './app_context'; +import { outputService } from './output'; +import { getFullAgentPolicy } from './agent_policies'; function getSavedObjectMock(agentPolicyAttributes: any) { const mock = savedObjectsClientMock.create(); @@ -47,9 +50,18 @@ function getSavedObjectMock(agentPolicyAttributes: any) { return mock; } +jest.mock('./output'); jest.mock('./agent_policy_update'); jest.mock('./agents'); jest.mock('./package_policy'); +jest.mock('./app_context'); +jest.mock('./agent_policies/full_agent_policy'); + +const mockedAppContextService = appContextService as jest.Mocked; +const mockedOutputService = outputService as jest.Mocked; +const mockedGetFullAgentPolicy = getFullAgentPolicy as jest.Mock< + ReturnType +>; function getAgentPolicyUpdateMock() { return agentPolicyUpdateEventHandler as unknown as jest.Mock< @@ -214,4 +226,64 @@ describe('agent policy', () => { expect(calledWith[2]).toHaveProperty('is_managed', true); }); }); + + describe('createFleetServerPolicy', () => { + beforeEach(() => { + mockedGetFullAgentPolicy.mockReset(); + }); + it('should not create a .fleet-policy document if we cannot get the full policy', async () => { + const soClient = savedObjectsClientMock.create(); + const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; + mockedAppContextService.getInternalUserESClient.mockReturnValue(esClient); + mockedOutputService.getDefaultOutputId.mockResolvedValue('default-output'); + mockedGetFullAgentPolicy.mockResolvedValue(null); + + soClient.get.mockResolvedValue({ + attributes: {}, + id: 'policy123', + type: 'mocked', + references: [], + }); + await agentPolicyService.createFleetServerPolicy(soClient, 'policy123'); + + expect(esClient.create).not.toBeCalled(); + }); + + it('should create a .fleet-policy document if we can get the full policy', async () => { + const soClient = savedObjectsClientMock.create(); + const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; + mockedAppContextService.getInternalUserESClient.mockReturnValue(esClient); + mockedOutputService.getDefaultOutputId.mockResolvedValue('default-output'); + mockedGetFullAgentPolicy.mockResolvedValue({ + id: 'policy123', + revision: 1, + inputs: [ + { + id: 'input-123', + }, + ], + } as FullAgentPolicy); + + soClient.get.mockResolvedValue({ + attributes: {}, + id: 'policy123', + type: 'mocked', + references: [], + }); + await agentPolicyService.createFleetServerPolicy(soClient, 'policy123'); + + expect(esClient.create).toBeCalledWith( + expect.objectContaining({ + index: '.fleet-policies', + body: expect.objectContaining({ + '@timestamp': expect.anything(), + data: { id: 'policy123', inputs: [{ id: 'input-123' }], revision: 1 }, + default_fleet_server: false, + policy_id: 'policy123', + revision_idx: 1, + }), + }) + ); + }); + }); }); diff --git a/x-pack/plugins/fleet/server/services/agent_policy.ts b/x-pack/plugins/fleet/server/services/agent_policy.ts index 751e981cb8085..6ebe890aeaef2 100644 --- a/x-pack/plugins/fleet/server/services/agent_policy.ts +++ b/x-pack/plugins/fleet/server/services/agent_policy.ts @@ -429,7 +429,7 @@ class AgentPolicyService { throw new Error('Copied agent policy not found'); } - await this.createFleetPolicyChangeAction(soClient, newAgentPolicy.id); + await this.createFleetServerPolicy(soClient, newAgentPolicy.id); return updatedAgentPolicy; } @@ -655,10 +655,11 @@ class AgentPolicyService { }; } - public async createFleetPolicyChangeAction( + public async createFleetServerPolicy( soClient: SavedObjectsClientContract, agentPolicyId: string ) { + // Use internal ES client so we have permissions to write to .fleet* indices const esClient = appContextService.getInternalUserESClient(); const defaultOutputId = await outputService.getDefaultOutputId(soClient); @@ -666,14 +667,6 @@ class AgentPolicyService { return; } - await this.createFleetPolicyChangeFleetServer(soClient, esClient, agentPolicyId); - } - - public async createFleetPolicyChangeFleetServer( - soClient: SavedObjectsClientContract, - esClient: ElasticsearchClient, - agentPolicyId: string - ) { const policy = await agentPolicyService.get(soClient, agentPolicyId); const fullPolicy = await agentPolicyService.getFullAgentPolicy(soClient, agentPolicyId); if (!policy || !fullPolicy || !fullPolicy.revision) { diff --git a/x-pack/plugins/fleet/server/services/agent_policy_update.ts b/x-pack/plugins/fleet/server/services/agent_policy_update.ts index 9703467d84c18..51bf068b8b111 100644 --- a/x-pack/plugins/fleet/server/services/agent_policy_update.ts +++ b/x-pack/plugins/fleet/server/services/agent_policy_update.ts @@ -43,11 +43,11 @@ export async function agentPolicyUpdateEventHandler( name: 'Default', agentPolicyId, }); - await agentPolicyService.createFleetPolicyChangeAction(internalSoClient, agentPolicyId); + await agentPolicyService.createFleetServerPolicy(internalSoClient, agentPolicyId); } if (action === 'updated') { - await agentPolicyService.createFleetPolicyChangeAction(internalSoClient, agentPolicyId); + await agentPolicyService.createFleetServerPolicy(internalSoClient, agentPolicyId); } if (action === 'deleted') { diff --git a/x-pack/plugins/fleet/server/services/agents/setup.ts b/x-pack/plugins/fleet/server/services/agents/setup.ts index 81ae6b177783d..2b680dee1146e 100644 --- a/x-pack/plugins/fleet/server/services/agents/setup.ts +++ b/x-pack/plugins/fleet/server/services/agents/setup.ts @@ -11,12 +11,9 @@ import { SO_SEARCH_LIMIT } from '../../constants'; import { agentPolicyService } from '../agent_policy'; /** - * During the migration from 7.9 to 7.10 we introduce a new agent action POLICY_CHANGE per policy - * this function ensure that action exist for each policy - * - * @param soClient + * Ensure a .fleet-policy document exist for each agent policy so Fleet server can retrieve it */ -export async function ensureAgentActionPolicyChangeExists( +export async function ensureFleetServerAgentPoliciesExists( soClient: SavedObjectsClientContract, esClient: ElasticsearchClient ) { @@ -32,7 +29,7 @@ export async function ensureAgentActionPolicyChangeExists( )); if (!policyChangeActionExist) { - return agentPolicyService.createFleetPolicyChangeAction(soClient, agentPolicy.id); + return agentPolicyService.createFleetServerPolicy(soClient, agentPolicy.id); } }) ); diff --git a/x-pack/plugins/fleet/server/services/epm/packages/cleanup.test.ts b/x-pack/plugins/fleet/server/services/epm/packages/cleanup.test.ts new file mode 100644 index 0000000000000..482e42a46060e --- /dev/null +++ b/x-pack/plugins/fleet/server/services/epm/packages/cleanup.test.ts @@ -0,0 +1,110 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { SavedObjectsClientContract } from 'kibana/server'; +import { savedObjectsClientMock } from 'src/core/server/mocks'; + +import type { PackagePolicyServiceInterface } from '../../package_policy'; +import * as storage from '../archive/storage'; +import { packagePolicyService } from '../../package_policy'; + +import { removeOldAssets } from './cleanup'; + +jest.mock('../..', () => ({ + appContextService: { + getLogger: () => ({ + info: jest.fn(), + }), + }, +})); + +jest.mock('../../package_policy'); + +describe(' Cleanup old assets', () => { + let soClient: jest.Mocked; + const packagePolicyServiceMock = + packagePolicyService as jest.Mocked; + let removeArchiveEntriesMock: jest.MockedFunction; + + function mockFindVersions(versions: string[]) { + soClient.find.mockResolvedValue({ + page: 0, + per_page: 0, + total: 0, + saved_objects: [], + aggregations: { + versions: { + buckets: versions.map((v) => ({ key: '0.3.3' })), + }, + }, + }); + } + + beforeEach(() => { + soClient = savedObjectsClientMock.create(); + packagePolicyServiceMock.list.mockClear(); + removeArchiveEntriesMock = jest.spyOn(storage, 'removeArchiveEntries') as any; + removeArchiveEntriesMock.mockClear(); + }); + it('should remove old assets from 2 versions if none of the policies are using it', async () => { + mockFindVersions(['0.3.3', '0.3.4']); + packagePolicyServiceMock.list.mockResolvedValue({ total: 0, items: [], page: 0, perPage: 0 }); + soClient.createPointInTimeFinder = jest.fn().mockResolvedValue({ + close: jest.fn(), + find: function* asyncGenerator() { + yield { saved_objects: [{ id: '1' }, { id: '2' }] }; + }, + }); + + await removeOldAssets({ soClient, pkgName: 'apache', currentVersion: '1.0.0' }); + + expect(removeArchiveEntriesMock).toHaveBeenCalledWith({ + savedObjectsClient: soClient, + refs: [ + { id: '1', type: 'epm-packages-assets' }, + { id: '2', type: 'epm-packages-assets' }, + ], + }); + expect(removeArchiveEntriesMock).toHaveBeenCalledTimes(2); + }); + + it('should not remove old assets if used by policies', async () => { + mockFindVersions(['0.3.3']); + packagePolicyServiceMock.list.mockResolvedValue({ total: 1, items: [], page: 0, perPage: 0 }); + + await removeOldAssets({ soClient, pkgName: 'apache', currentVersion: '1.0.0' }); + + expect(removeArchiveEntriesMock).not.toHaveBeenCalled(); + }); + + it('should remove old assets from all pages', async () => { + mockFindVersions(['0.3.3']); + packagePolicyServiceMock.list.mockResolvedValue({ total: 0, items: [], page: 0, perPage: 0 }); + soClient.createPointInTimeFinder = jest.fn().mockResolvedValue({ + close: jest.fn(), + find: function* asyncGenerator() { + yield { saved_objects: [{ id: '1' }, { id: '2' }] }; + yield { saved_objects: [{ id: '3' }] }; + }, + }); + + await removeOldAssets({ soClient, pkgName: 'apache', currentVersion: '1.0.0' }); + + expect(removeArchiveEntriesMock).toHaveBeenCalledWith({ + savedObjectsClient: soClient, + refs: [ + { id: '1', type: 'epm-packages-assets' }, + { id: '2', type: 'epm-packages-assets' }, + ], + }); + expect(removeArchiveEntriesMock).toHaveBeenCalledWith({ + savedObjectsClient: soClient, + refs: [{ id: '3', type: 'epm-packages-assets' }], + }); + expect(removeArchiveEntriesMock).toHaveBeenCalledTimes(2); + }); +}); diff --git a/x-pack/plugins/fleet/server/services/epm/packages/cleanup.ts b/x-pack/plugins/fleet/server/services/epm/packages/cleanup.ts new file mode 100644 index 0000000000000..d70beb53eddab --- /dev/null +++ b/x-pack/plugins/fleet/server/services/epm/packages/cleanup.ts @@ -0,0 +1,80 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { SavedObjectsClientContract } from 'src/core/server'; + +import { removeArchiveEntries } from '../archive/storage'; + +import { ASSETS_SAVED_OBJECT_TYPE, PACKAGE_POLICY_SAVED_OBJECT_TYPE } from '../../../../common'; +import type { PackageAssetReference } from '../../../../common'; +import { packagePolicyService } from '../../package_policy'; +import { appContextService } from '../..'; + +export async function removeOldAssets(options: { + soClient: SavedObjectsClientContract; + pkgName: string; + currentVersion: string; +}) { + const { soClient, pkgName, currentVersion } = options; + + // find all assets of older versions + const aggs = { + versions: { terms: { field: `${ASSETS_SAVED_OBJECT_TYPE}.attributes.package_version` } }, + }; + const oldVersionsAgg = await soClient.find({ + type: ASSETS_SAVED_OBJECT_TYPE, + filter: `${ASSETS_SAVED_OBJECT_TYPE}.attributes.package_name:${pkgName} AND ${ASSETS_SAVED_OBJECT_TYPE}.attributes.package_version<${currentVersion}`, + aggs, + page: 0, + perPage: 0, + }); + + const oldVersions = oldVersionsAgg.aggregations.versions.buckets.map( + (obj: { key: string }) => obj.key + ); + + for (const oldVersion of oldVersions) { + await removeAssetsFromVersion(soClient, pkgName, oldVersion); + } +} + +async function removeAssetsFromVersion( + soClient: SavedObjectsClientContract, + pkgName: string, + oldVersion: string +) { + // check if any policies are using this package version + const { total } = await packagePolicyService.list(soClient, { + kuery: `${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.package.name:${pkgName} AND ${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.package.version:${oldVersion}`, + page: 0, + perPage: 0, + }); + // don't delete if still being used + if (total > 0) { + appContextService + .getLogger() + .info(`Package "${pkgName}-${oldVersion}" still being used by policies`); + return; + } + + // check if old version has assets + const finder = await soClient.createPointInTimeFinder({ + type: ASSETS_SAVED_OBJECT_TYPE, + filter: `${ASSETS_SAVED_OBJECT_TYPE}.attributes.package_name:${pkgName} AND ${ASSETS_SAVED_OBJECT_TYPE}.attributes.package_version:${oldVersion}`, + perPage: 1000, + fields: ['id'], + }); + + for await (const assets of finder.find()) { + const refs = assets.saved_objects.map( + (obj) => ({ id: obj.id, type: ASSETS_SAVED_OBJECT_TYPE } as PackageAssetReference) + ); + + await removeArchiveEntries({ savedObjectsClient: soClient, refs }); + } + await finder.close(); +} diff --git a/x-pack/plugins/fleet/server/services/epm/packages/install.ts b/x-pack/plugins/fleet/server/services/epm/packages/install.ts index 2568f40594f10..bd1968f03c263 100644 --- a/x-pack/plugins/fleet/server/services/epm/packages/install.ts +++ b/x-pack/plugins/fleet/server/services/epm/packages/install.ts @@ -38,6 +38,7 @@ import { isUnremovablePackage, getInstallation, getInstallationObject } from './ import { removeInstallation } from './remove'; import { getPackageSavedObjects } from './get'; import { _installPackage } from './_install_package'; +import { removeOldAssets } from './cleanup'; export async function isPackageInstalled(options: { savedObjectsClient: SavedObjectsClientContract; @@ -267,7 +268,12 @@ async function installPackageFromRegistry({ installType, installSource: 'registry', }) - .then((assets) => { + .then(async (assets) => { + await removeOldAssets({ + soClient: savedObjectsClient, + pkgName: packageInfo.name, + currentVersion: packageInfo.version, + }); return { assets, status: 'installed', installType }; }) .catch(async (err: Error) => { diff --git a/x-pack/plugins/fleet/server/services/epm/registry/requests.test.ts b/x-pack/plugins/fleet/server/services/epm/registry/requests.test.ts index 01fd4ad143d18..61f6cc164eb30 100644 --- a/x-pack/plugins/fleet/server/services/epm/registry/requests.test.ts +++ b/x-pack/plugins/fleet/server/services/epm/registry/requests.test.ts @@ -15,7 +15,7 @@ const { Response, FetchError } = jest.requireActual('node-fetch'); const fetchMock = require('node-fetch') as jest.Mock; jest.setTimeout(120 * 1000); -describe('setupIngestManager', () => { +describe('Registry request', () => { beforeEach(async () => {}); afterEach(async () => { diff --git a/x-pack/plugins/fleet/server/services/fleet_server/saved_object_migrations.ts b/x-pack/plugins/fleet/server/services/fleet_server/saved_object_migrations.ts index 379bc8fa39bff..bbaf9c9479eb4 100644 --- a/x-pack/plugins/fleet/server/services/fleet_server/saved_object_migrations.ts +++ b/x-pack/plugins/fleet/server/services/fleet_server/saved_object_migrations.ts @@ -190,11 +190,7 @@ async function migrateAgentPolicies() { // @ts-expect-error value is number | TotalHits if (res.body.hits.total.value === 0) { - return agentPolicyService.createFleetPolicyChangeFleetServer( - soClient, - esClient, - agentPolicy.id - ); + return agentPolicyService.createFleetServerPolicy(soClient, agentPolicy.id); } }) ); diff --git a/x-pack/plugins/fleet/server/services/package_policy.ts b/x-pack/plugins/fleet/server/services/package_policy.ts index c806b37f88153..d84d50e00b8a9 100644 --- a/x-pack/plugins/fleet/server/services/package_policy.ts +++ b/x-pack/plugins/fleet/server/services/package_policy.ts @@ -65,6 +65,7 @@ import { getAssetsData } from './epm/packages/assets'; import { compileTemplate } from './epm/agent/agent'; import { normalizeKuery } from './saved_object'; import { appContextService } from '.'; +import { removeOldAssets } from './epm/packages/cleanup'; export type InputsOverride = Partial & { vars?: Array; @@ -575,6 +576,11 @@ class PackagePolicyService { name: packagePolicy.name, success: true, }); + await removeOldAssets({ + soClient, + pkgName: packageInfo.name, + currentVersion: packageInfo.version, + }); } catch (error) { result.push({ id, diff --git a/x-pack/plugins/fleet/server/services/setup.test.ts b/x-pack/plugins/fleet/server/services/setup.test.ts index 212b0fabd26fb..e6b76694a9fca 100644 --- a/x-pack/plugins/fleet/server/services/setup.test.ts +++ b/x-pack/plugins/fleet/server/services/setup.test.ts @@ -8,7 +8,7 @@ import { createAppContextStartContractMock, xpackMocks } from '../mocks'; import { appContextService } from './app_context'; -import { setupIngestManager } from './setup'; +import { setupFleet } from './setup'; const mockedMethodThrowsError = () => jest.fn().mockImplementation(() => { @@ -21,7 +21,7 @@ const mockedMethodThrowsCustom = () => throw new CustomTestError('method mocked to throw'); }); -describe('setupIngestManager', () => { +describe('setupFleet', () => { let context: ReturnType; beforeEach(async () => { @@ -44,7 +44,7 @@ describe('setupIngestManager', () => { soClient.update = mockedMethodThrowsError(); const esClient = context.core.elasticsearch.client.asCurrentUser; - const setupPromise = setupIngestManager(soClient, esClient); + const setupPromise = setupFleet(soClient, esClient); await expect(setupPromise).rejects.toThrow('SO method mocked to throw'); await expect(setupPromise).rejects.toThrow(Error); }); @@ -57,7 +57,7 @@ describe('setupIngestManager', () => { soClient.update = mockedMethodThrowsCustom(); const esClient = context.core.elasticsearch.client.asCurrentUser; - const setupPromise = setupIngestManager(soClient, esClient); + const setupPromise = setupFleet(soClient, esClient); await expect(setupPromise).rejects.toThrow('method mocked to throw'); await expect(setupPromise).rejects.toThrow(CustomTestError); }); diff --git a/x-pack/plugins/fleet/server/services/setup.ts b/x-pack/plugins/fleet/server/services/setup.ts index 8c49bffdbf25c..08c580d80c804 100644 --- a/x-pack/plugins/fleet/server/services/setup.ts +++ b/x-pack/plugins/fleet/server/services/setup.ts @@ -25,7 +25,7 @@ import { outputService } from './output'; import { generateEnrollmentAPIKey, hasEnrollementAPIKeysForPolicy } from './api_keys'; import { settingsService } from '.'; import { awaitIfPending } from './setup_utils'; -import { ensureAgentActionPolicyChangeExists } from './agents'; +import { ensureFleetServerAgentPoliciesExists } from './agents'; import { awaitIfFleetServerSetupPending } from './fleet_server'; import { ensureFleetFinalPipelineIsInstalled } from './epm/elasticsearch/ingest_pipeline/install'; import { ensureDefaultComponentTemplate } from './epm/elasticsearch/template/install'; @@ -38,7 +38,7 @@ export interface SetupStatus { nonFatalErrors: Array; } -export async function setupIngestManager( +export async function setupFleet( soClient: SavedObjectsClientContract, esClient: ElasticsearchClient ): Promise { @@ -101,7 +101,7 @@ async function createSetupSideEffects( await cleanPreconfiguredOutputs(soClient, outputsOrUndefined ?? []); await ensureDefaultEnrollmentAPIKeysExists(soClient, esClient); - await ensureAgentActionPolicyChangeExists(soClient, esClient); + await ensureFleetServerAgentPoliciesExists(soClient, esClient); return { isInitialized: true, diff --git a/x-pack/plugins/graph/public/components/settings/settings.test.tsx b/x-pack/plugins/graph/public/components/settings/settings.test.tsx index 060b1e93fbdc0..1c3fbb9f54f67 100644 --- a/x-pack/plugins/graph/public/components/settings/settings.test.tsx +++ b/x-pack/plugins/graph/public/components/settings/settings.test.tsx @@ -24,6 +24,18 @@ import { createMockGraphStore } from '../../state_management/mocks'; import { Provider } from 'react-redux'; import { UrlTemplate } from '../../types'; +jest.mock('@elastic/eui', () => { + const original = jest.requireActual('@elastic/eui'); + + return { + ...original, + htmlIdGenerator: (fn: unknown) => { + let counter = 0; + return () => String(counter++); + }, + }; +}); + describe('settings', () => { let store: GraphStore; let dispatchSpy: jest.Mock; diff --git a/x-pack/plugins/graph/public/helpers/use_workspace_loader.test.tsx b/x-pack/plugins/graph/public/helpers/use_workspace_loader.test.tsx index cd0857f82ab6b..c4c40ada490f1 100644 --- a/x-pack/plugins/graph/public/helpers/use_workspace_loader.test.tsx +++ b/x-pack/plugins/graph/public/helpers/use_workspace_loader.test.tsx @@ -4,15 +4,13 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import React from 'react'; -import { mount } from 'enzyme'; import { useWorkspaceLoader, UseWorkspaceLoaderProps } from './use_workspace_loader'; import { coreMock } from 'src/core/public/mocks'; import { spacesPluginMock } from '../../../spaces/public/mocks'; import { createMockGraphStore } from '../state_management/mocks'; import { Workspace } from '../types'; import { SavedObjectsClientCommon } from 'src/plugins/data/common'; -import { act } from 'react-dom/test-utils'; +import { renderHook, act, RenderHookOptions } from '@testing-library/react-hooks'; jest.mock('react-router-dom', () => { const useLocation = () => ({ @@ -41,20 +39,6 @@ const mockSavedObjectsClient = { find: jest.fn().mockResolvedValue({ title: 'test' }), } as unknown as SavedObjectsClientCommon; -async function setup(props: UseWorkspaceLoaderProps) { - const returnVal = {}; - function TestComponent() { - Object.assign(returnVal, useWorkspaceLoader(props)); - return null; - } - await act(async () => { - const promise = Promise.resolve(); - mount(); - await act(() => promise); - }); - return returnVal; -} - describe('use_workspace_loader', () => { const defaultProps = { workspaceRef: { current: {} as Workspace }, @@ -62,13 +46,16 @@ describe('use_workspace_loader', () => { savedObjectsClient: mockSavedObjectsClient, coreStart: coreMock.createStart(), spaces: spacesPluginMock.createStartContract(), - }; + } as unknown as UseWorkspaceLoaderProps; it('should not redirect if outcome is exactMatch', async () => { await act(async () => { - await setup(defaultProps as unknown as UseWorkspaceLoaderProps); + renderHook( + () => useWorkspaceLoader(defaultProps), + defaultProps as RenderHookOptions + ); }); - expect(defaultProps.spaces.ui.redirectLegacyUrl).not.toHaveBeenCalled(); + expect(defaultProps.spaces?.ui.redirectLegacyUrl).not.toHaveBeenCalled(); expect(defaultProps.store.dispatch).toHaveBeenCalled(); }); it('should redirect if outcome is aliasMatch', async () => { @@ -83,11 +70,15 @@ describe('use_workspace_loader', () => { alias_target_id: 'aliasTargetId', }), }, - }; + } as unknown as UseWorkspaceLoaderProps; + await act(async () => { - await setup(props as unknown as UseWorkspaceLoaderProps); + renderHook( + () => useWorkspaceLoader(props), + props as RenderHookOptions + ); }); - expect(props.spaces.ui.redirectLegacyUrl).toHaveBeenCalledWith( + expect(props.spaces?.ui.redirectLegacyUrl).toHaveBeenCalledWith( '#/workspace/aliasTargetId?query={}', 'Graph' ); diff --git a/x-pack/plugins/graph/public/state_management/mocks.ts b/x-pack/plugins/graph/public/state_management/mocks.ts index 6f7b5dba2e4a1..5003e550f87a1 100644 --- a/x-pack/plugins/graph/public/state_management/mocks.ts +++ b/x-pack/plugins/graph/public/state_management/mocks.ts @@ -42,6 +42,7 @@ export function createMockGraphStore({ }): MockedGraphEnvironment { const workspaceMock = { runLayout: jest.fn(), + simpleSearch: jest.fn(), nodes: [], edges: [], options: {}, @@ -55,7 +56,7 @@ export function createMockGraphStore({ chrome: { setBreadcrumbs: jest.fn(), } as unknown as ChromeStart, - createWorkspace: jest.fn(), + createWorkspace: jest.fn((index, advancedSettings) => workspaceMock), getWorkspace: jest.fn(() => workspaceMock), indexPatternProvider: { get: jest.fn(() => diff --git a/x-pack/plugins/graph/public/state_management/persistence.test.ts b/x-pack/plugins/graph/public/state_management/persistence.test.ts index dc59869fafd4c..2ef68f2198070 100644 --- a/x-pack/plugins/graph/public/state_management/persistence.test.ts +++ b/x-pack/plugins/graph/public/state_management/persistence.test.ts @@ -23,6 +23,18 @@ import { settingsSelector } from './advanced_settings'; import { openSaveModal } from '../services/save_modal'; const waitForPromise = () => new Promise((r) => setTimeout(r)); +// mocking random id generator function +jest.mock('@elastic/eui', () => { + const original = jest.requireActual('@elastic/eui'); + + return { + ...original, + htmlIdGenerator: (fn: unknown) => { + let counter = 0; + return () => counter++; + }, + }; +}); jest.mock('../services/persistence', () => ({ lookupIndexPatternId: jest.fn(() => ({ id: '123', attributes: { title: 'test-pattern' } })), diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/hot_phase_validation.test.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/hot_phase_validation.test.ts index 6cbc28ec161f2..6dca2b7b23fb7 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/hot_phase_validation.test.ts +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/hot_phase_validation.test.ts @@ -158,13 +158,23 @@ describe(' hot phase validation', () => { }); describe('shrink', () => { - test(`doesn't allow 0 for shrink`, async () => { - await actions.hot.setShrink('0'); + test(`doesn't allow 0 for shrink size`, async () => { + await actions.hot.setShrinkSize('0'); actions.errors.waitForValidation(); actions.errors.expectMessages([i18nTexts.editPolicy.errors.numberGreatThan0Required]); }); - test(`doesn't allow -1 for shrink`, async () => { - await actions.hot.setShrink('-1'); + test(`doesn't allow -1 for shrink size`, async () => { + await actions.hot.setShrinkSize('-1'); + actions.errors.waitForValidation(); + actions.errors.expectMessages([i18nTexts.editPolicy.errors.numberGreatThan0Required]); + }); + test(`doesn't allow 0 for shrink count`, async () => { + await actions.hot.setShrinkCount('0'); + actions.errors.waitForValidation(); + actions.errors.expectMessages([i18nTexts.editPolicy.errors.numberGreatThan0Required]); + }); + test(`doesn't allow -1 for shrink count`, async () => { + await actions.hot.setShrinkCount('-1'); actions.errors.waitForValidation(); actions.errors.expectMessages([i18nTexts.editPolicy.errors.numberGreatThan0Required]); }); diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/warm_phase_validation.test.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/warm_phase_validation.test.ts index 0b8bfceebfaf4..741611f2d2c3b 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/warm_phase_validation.test.ts +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/warm_phase_validation.test.ts @@ -58,17 +58,33 @@ describe(' warm phase validation', () => { }); describe('shrink', () => { - test(`doesn't allow 0 for shrink`, async () => { + test(`doesn't allow 0 for shrink size`, async () => { const { actions } = testBed; - await actions.warm.setShrink('0'); + await actions.warm.setShrinkSize('0'); actions.errors.waitForValidation(); actions.errors.expectMessages([i18nTexts.editPolicy.errors.numberGreatThan0Required]); }); - test(`doesn't allow -1 for shrink`, async () => { + test(`doesn't allow -1 for shrink size`, async () => { const { actions } = testBed; - await actions.warm.setShrink('-1'); + await actions.warm.setShrinkSize('-1'); + + actions.errors.waitForValidation(); + + actions.errors.expectMessages([i18nTexts.editPolicy.errors.numberGreatThan0Required]); + }); + test(`doesn't allow 0 for shrink count`, async () => { + const { actions } = testBed; + await actions.warm.setShrinkCount('0'); + + actions.errors.waitForValidation(); + + actions.errors.expectMessages([i18nTexts.editPolicy.errors.numberGreatThan0Required]); + }); + test(`doesn't allow -1 for shrink count`, async () => { + const { actions } = testBed; + await actions.warm.setShrinkCount('-1'); actions.errors.waitForValidation(); diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/serialization/policy_serialization.test.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/serialization/policy_serialization.test.ts index c315bde7e37d8..7a4d1f7efca63 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/serialization/policy_serialization.test.ts +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/serialization/policy_serialization.test.ts @@ -171,7 +171,7 @@ describe(' serialization', () => { await actions.hot.toggleForceMerge(); await actions.hot.setForcemergeSegmentsCount('123'); await actions.hot.setBestCompression(true); - await actions.hot.setShrink('2'); + await actions.hot.setShrinkCount('2'); await actions.hot.toggleReadonly(); await actions.hot.setIndexPriority('123'); @@ -274,7 +274,7 @@ describe(' serialization', () => { await actions.warm.setDataAllocation('node_attrs'); await actions.warm.setSelectedNodeAttribute('test:123'); await actions.warm.setReplicas('123'); - await actions.warm.setShrink('123'); + await actions.warm.setShrinkCount('123'); await actions.warm.toggleForceMerge(); await actions.warm.setForcemergeSegmentsCount('123'); await actions.warm.setBestCompression(true); @@ -546,4 +546,49 @@ describe(' serialization', () => { }); }); }); + + describe('shrink', () => { + test('shrink shard size', async () => { + const { actions } = testBed; + await actions.hot.setShrinkSize('50'); + + await actions.togglePhase('warm'); + await actions.warm.setMinAgeValue('11'); + await actions.warm.setShrinkSize('100'); + + await actions.savePolicy(); + const latestRequest = server.requests[server.requests.length - 1]; + const entirePolicy = JSON.parse(JSON.parse(latestRequest.requestBody).body); + expect(entirePolicy).toMatchInlineSnapshot(` + Object { + "name": "my_policy", + "phases": Object { + "hot": Object { + "actions": Object { + "rollover": Object { + "max_age": "30d", + "max_primary_shard_size": "50gb", + }, + "shrink": Object { + "max_primary_shard_size": "50gb", + }, + }, + "min_age": "0ms", + }, + "warm": Object { + "actions": Object { + "set_priority": Object { + "priority": 50, + }, + "shrink": Object { + "max_primary_shard_size": "100gb", + }, + }, + "min_age": "11d", + }, + }, + } + `); + }); + }); }); diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/shrink_actions.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/shrink_actions.ts index 29c3e4a04a9a1..394a64696d5eb 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/shrink_actions.ts +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/shrink_actions.ts @@ -6,18 +6,41 @@ */ import { TestBed } from '@kbn/test/jest'; +import { act } from 'react-dom/test-utils'; import { Phase } from '../../../../common/types'; -import { createFormToggleAndSetValueAction } from './form_toggle_and_set_value_action'; +import { createFormSetValueAction } from './form_set_value_action'; export const createShrinkActions = (testBed: TestBed, phase: Phase) => { - const { exists } = testBed; - const toggleSelector = `${phase}-shrinkSwitch`; + const { exists, form, component, find } = testBed; + const toggleShrinkSelector = `${phase}-shrinkSwitch`; + const shrinkSizeSelector = `${phase}-primaryShardSize`; + const shrinkCountSelector = `${phase}-primaryShardCount`; + + const changeShrinkRadioButton = async (selector: string) => { + await act(async () => { + await find(selector).find('input').simulate('change'); + }); + component.update(); + }; return { - shrinkExists: () => exists(toggleSelector), - setShrink: createFormToggleAndSetValueAction( - testBed, - toggleSelector, - `${phase}-primaryShardCount` - ), + shrinkExists: () => exists(toggleShrinkSelector), + setShrinkCount: async (value: string) => { + if (!exists(shrinkCountSelector) && !exists(shrinkSizeSelector)) { + await form.toggleEuiSwitch(toggleShrinkSelector); + } + if (!exists(shrinkCountSelector)) { + await changeShrinkRadioButton(`${phase}-configureShardCount`); + } + await createFormSetValueAction(testBed, shrinkCountSelector)(value); + }, + setShrinkSize: async (value: string) => { + if (!exists(shrinkCountSelector) && !exists(shrinkSizeSelector)) { + await form.toggleEuiSwitch(toggleShrinkSelector); + } + if (!exists(shrinkSizeSelector)) { + await changeShrinkRadioButton(`${phase}-configureShardSize`); + } + await createFormSetValueAction(testBed, shrinkSizeSelector)(value); + }, }; }; 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 3a338c80fa56c..b9922a0d59459 100644 --- a/x-pack/plugins/index_lifecycle_management/common/types/policies.ts +++ b/x-pack/plugins/index_lifecycle_management/common/types/policies.ts @@ -168,7 +168,8 @@ export interface AllocateAction { } export interface ShrinkAction { - number_of_shards: number; + number_of_shards?: number; + max_primary_shard_size?: string; } export interface ForcemergeAction { diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/described_form_row/described_form_row.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/described_form_row/described_form_row.tsx index cc05ac2c47872..98fe3d52bc6ae 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/described_form_row/described_form_row.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/described_form_row/described_form_row.tsx @@ -29,7 +29,7 @@ export interface SwitchProps } export type Props = EuiDescribedFormGroupProps & { - children: (() => JSX.Element) | JSX.Element | JSX.Element[] | undefined; + children: (() => JSX.Element) | JSX.Element | JSX.Element[] | undefined | null; switchProps?: SwitchProps; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/hot_phase/components/max_age_field.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/hot_phase/components/max_age_field.tsx index 7fbdaf344b8fa..7ee861ce8071e 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/hot_phase/components/max_age_field.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/hot_phase/components/max_age_field.tsx @@ -11,10 +11,8 @@ import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { NumericField } from '../../../../../../../shared_imports'; import { UseField } from '../../../../form'; -import { ROLLOVER_FORM_PATHS } from '../../../../constants'; -import { UnitField } from './unit_field'; - -import { maxAgeUnits } from '../constants'; +import { ROLLOVER_FORM_PATHS, timeUnits } from '../../../../constants'; +import { UnitField } from '../../shared_fields/unit_field'; export const MaxAgeField: FunctionComponent = () => { return ( @@ -30,7 +28,7 @@ export const MaxAgeField: FunctionComponent = () => { append: ( { append: ( { return ( @@ -30,15 +28,10 @@ export const MaxPrimaryShardSizeField: FunctionComponent = () => { append: ( ), diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/hot_phase/constants.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/hot_phase/constants.ts deleted file mode 100644 index 3b03c7c7ec0c8..0000000000000 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/hot_phase/constants.ts +++ /dev/null @@ -1,92 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { i18n } from '@kbn/i18n'; - -export const maxSizeStoredUnits = [ - { - value: 'gb', - text: i18n.translate('xpack.indexLifecycleMgmt.hotPhase.gigabytesLabel', { - defaultMessage: 'gigabytes', - }), - }, - { - value: 'mb', - text: i18n.translate('xpack.indexLifecycleMgmt.hotPhase.megabytesLabel', { - defaultMessage: 'megabytes', - }), - }, - { - value: 'b', - text: i18n.translate('xpack.indexLifecycleMgmt.hotPhase.bytesLabel', { - defaultMessage: 'bytes', - }), - }, - { - value: 'kb', - text: i18n.translate('xpack.indexLifecycleMgmt.hotPhase.kilobytesLabel', { - defaultMessage: 'kilobytes', - }), - }, - { - value: 'tb', - text: i18n.translate('xpack.indexLifecycleMgmt.hotPhase.terabytesLabel', { - defaultMessage: 'terabytes', - }), - }, - { - value: 'pb', - text: i18n.translate('xpack.indexLifecycleMgmt.hotPhase.petabytesLabel', { - defaultMessage: 'petabytes', - }), - }, -]; - -export const maxAgeUnits = [ - { - value: 'd', - text: i18n.translate('xpack.indexLifecycleMgmt.hotPhase.daysLabel', { - defaultMessage: 'days', - }), - }, - { - value: 'h', - text: i18n.translate('xpack.indexLifecycleMgmt.hotPhase.hoursLabel', { - defaultMessage: 'hours', - }), - }, - { - value: 'm', - text: i18n.translate('xpack.indexLifecycleMgmt.hotPhase.minutesLabel', { - defaultMessage: 'minutes', - }), - }, - { - value: 's', - text: i18n.translate('xpack.indexLifecycleMgmt.hotPhase.secondsLabel', { - defaultMessage: 'seconds', - }), - }, - { - value: 'ms', - text: i18n.translate('xpack.indexLifecycleMgmt.hotPhase.millisecondsLabel', { - defaultMessage: 'milliseconds', - }), - }, - { - value: 'micros', - text: i18n.translate('xpack.indexLifecycleMgmt.hotPhase.microsecondsLabel', { - defaultMessage: 'microseconds', - }), - }, - { - value: 'nanos', - text: i18n.translate('xpack.indexLifecycleMgmt.hotPhase.nanosecondsLabel', { - defaultMessage: 'nanoseconds', - }), - }, -]; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/min_age_field/min_age_field.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/min_age_field/min_age_field.tsx index 2c42d76415dbc..b6c6102425d12 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/min_age_field/min_age_field.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/min_age_field/min_age_field.tsx @@ -25,43 +25,10 @@ import { PhaseWithTiming } from '../../../../../../../../common/types'; import { getFieldValidityAndErrorMessage, useFormData } from '../../../../../../../shared_imports'; import { UseField, useConfiguration, useGlobalFields } from '../../../../form'; import { getPhaseMinAgeInMilliseconds } from '../../../../lib'; +import { timeUnits } from '../../../../constants'; import { getUnitsAriaLabelForPhase, getTimingLabelForPhase } from './util'; const i18nTexts = { - daysOptionLabel: i18n.translate('xpack.indexLifecycleMgmt.editPolicy.daysOptionLabel', { - defaultMessage: 'days', - }), - - hoursOptionLabel: i18n.translate('xpack.indexLifecycleMgmt.editPolicy.hoursOptionLabel', { - defaultMessage: 'hours', - }), - minutesOptionLabel: i18n.translate('xpack.indexLifecycleMgmt.editPolicy.minutesOptionLabel', { - defaultMessage: 'minutes', - }), - - secondsOptionLabel: i18n.translate('xpack.indexLifecycleMgmt.editPolicy.secondsOptionLabel', { - defaultMessage: 'seconds', - }), - millisecondsOptionLabel: i18n.translate( - 'xpack.indexLifecycleMgmt.editPolicy.milliSecondsOptionLabel', - { - defaultMessage: 'milliseconds', - } - ), - - microsecondsOptionLabel: i18n.translate( - 'xpack.indexLifecycleMgmt.editPolicy.microSecondsOptionLabel', - { - defaultMessage: 'microseconds', - } - ), - - nanosecondsOptionLabel: i18n.translate( - 'xpack.indexLifecycleMgmt.editPolicy.nanoSecondsOptionLabel', - { - defaultMessage: 'nanoseconds', - } - ), rolloverToolTipDescription: i18n.translate( 'xpack.indexLifecycleMgmt.editPolicy.minimumAge.rolloverToolTipDescription', { @@ -180,36 +147,7 @@ export const MinAgeField: FunctionComponent = ({ phase }): React.ReactEle append={selectAppendValue} data-test-subj={`${phase}-selectedMinimumAgeUnits`} aria-label={getUnitsAriaLabelForPhase(phase)} - options={[ - { - value: 'd', - text: i18nTexts.daysOptionLabel, - }, - { - value: 'h', - text: i18nTexts.hoursOptionLabel, - }, - { - value: 'm', - text: i18nTexts.minutesOptionLabel, - }, - { - value: 's', - text: i18nTexts.secondsOptionLabel, - }, - { - value: 'ms', - text: i18nTexts.millisecondsOptionLabel, - }, - { - value: 'micros', - text: i18nTexts.microsecondsOptionLabel, - }, - { - value: 'nanos', - text: i18nTexts.nanosecondsOptionLabel, - }, - ]} + options={timeUnits} /> ); }} diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/shrink_field.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/shrink_field.tsx index 8ac387ba106b7..1becf90de4d46 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/shrink_field.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/shrink_field.tsx @@ -6,24 +6,35 @@ */ import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiTextColor } from '@elastic/eui'; +import { EuiTextColor, EuiRadioGroup, EuiSpacer } from '@elastic/eui'; import React, { FunctionComponent } from 'react'; -import { NumericField } from '../../../../../../shared_imports'; +import { get } from 'lodash'; +import { NumericField, useFormData } from '../../../../../../shared_imports'; import { useEditPolicyContext } from '../../../edit_policy_context'; -import { UseField } from '../../../form'; +import { UseField, useGlobalFields } from '../../../form'; import { i18nTexts } from '../../../i18n_texts'; import { LearnMoreLink, DescribedFormRow } from '../../'; +import { byteSizeUnits } from '../../../constants'; +import { UnitField } from './unit_field'; interface Props { phase: 'hot' | 'warm'; } export const ShrinkField: FunctionComponent = ({ phase }) => { - const path = `phases.${phase}.actions.shrink.number_of_shards`; + const globalFields = useGlobalFields(); + const { setValue: setIsUsingShardSize } = + globalFields[`${phase}IsUsingShardSize` as 'hotIsUsingShardSize']; const { policy } = useEditPolicyContext(); + const isUsingShardSizePath = `_meta.${phase}.shrink.isUsingShardSize`; + const [formData] = useFormData({ watch: [isUsingShardSizePath] }); + const isUsingShardSize: boolean | undefined = get(formData, isUsingShardSizePath); + const path = `phases.${phase}.actions.shrink.${ + isUsingShardSize ? 'max_primary_shard_size' : 'number_of_shards' + }`; return ( = ({ phase }) => { }} fullWidth > - - + {isUsingShardSize === undefined ? null : ( + <> + setIsUsingShardSize(id === `${phase}-configureShardSize`)} + /> + + ) : null, }, }} /> - - - + + )} ); }; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/hot_phase/components/unit_field.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/unit_field.tsx similarity index 97% rename from x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/hot_phase/components/unit_field.tsx rename to x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/unit_field.tsx index 2ef8917d53989..6b76b0357b8ba 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/hot_phase/components/unit_field.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/unit_field.tsx @@ -7,8 +7,7 @@ import React, { FunctionComponent, useState } from 'react'; import { EuiFilterSelectItem, EuiPopover, EuiButtonEmpty } from '@elastic/eui'; - -import { UseField } from '../../../../form'; +import { UseField } from '../../../form'; interface Props { path: string; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/constants.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/constants.ts index 88d5f6e138882..bd8c06de7e402 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/constants.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/constants.ts @@ -5,6 +5,8 @@ * 2.0. */ +import { i18n } from '@kbn/i18n'; + export const isUsingCustomRolloverPath = '_meta.hot.customRollover.enabled'; export const isUsingDefaultRolloverPath = '_meta.hot.isUsingDefaultRollover'; @@ -25,3 +27,93 @@ export const ROLLOVER_FORM_PATHS = { * exist as a "managed" repository. */ export const CLOUD_DEFAULT_REPO = 'found-snapshots'; + +/* + * Labels for byte size units + */ +export const byteSizeUnits = [ + { + value: 'gb', + text: i18n.translate('xpack.indexLifecycleMgmt.editPolicy.byteSizeUnits.gigabytesLabel', { + defaultMessage: 'gigabytes', + }), + }, + { + value: 'mb', + text: i18n.translate('xpack.indexLifecycleMgmt.editPolicy.byteSizeUnits.megabytesLabel', { + defaultMessage: 'megabytes', + }), + }, + { + value: 'b', + text: i18n.translate('xpack.indexLifecycleMgmt.editPolicy.byteSizeUnits.bytesLabel', { + defaultMessage: 'bytes', + }), + }, + { + value: 'kb', + text: i18n.translate('xpack.indexLifecycleMgmt.editPolicy.byteSizeUnits.kilobytesLabel', { + defaultMessage: 'kilobytes', + }), + }, + { + value: 'tb', + text: i18n.translate('xpack.indexLifecycleMgmt.editPolicy.byteSizeUnits.terabytesLabel', { + defaultMessage: 'terabytes', + }), + }, + { + value: 'pb', + text: i18n.translate('xpack.indexLifecycleMgmt.editPolicy.byteSizeUnits.petabytesLabel', { + defaultMessage: 'petabytes', + }), + }, +]; + +/* + * Labels for time units + */ +export const timeUnits = [ + { + value: 'd', + text: i18n.translate('xpack.indexLifecycleMgmt.editPolicy.timeUnits.daysLabel', { + defaultMessage: 'days', + }), + }, + { + value: 'h', + text: i18n.translate('xpack.indexLifecycleMgmt.editPolicy.timeUnits.hoursLabel', { + defaultMessage: 'hours', + }), + }, + { + value: 'm', + text: i18n.translate('xpack.indexLifecycleMgmt.editPolicy.timeUnits.minutesLabel', { + defaultMessage: 'minutes', + }), + }, + { + value: 's', + text: i18n.translate('xpack.indexLifecycleMgmt.editPolicy.timeUnits.secondsLabel', { + defaultMessage: 'seconds', + }), + }, + { + value: 'ms', + text: i18n.translate('xpack.indexLifecycleMgmt.editPolicy.timeUnits.millisecondsLabel', { + defaultMessage: 'milliseconds', + }), + }, + { + value: 'micros', + text: i18n.translate('xpack.indexLifecycleMgmt.editPolicy.timeUnits.microsecondsLabel', { + defaultMessage: 'microseconds', + }), + }, + { + value: 'nanos', + text: i18n.translate('xpack.indexLifecycleMgmt.editPolicy.timeUnits.nanosecondsLabel', { + defaultMessage: 'nanoseconds', + }), + }, +]; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/deserializer.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/deserializer.ts index dc3714bdaf8da..1ce5b8aa7a717 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/deserializer.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/deserializer.ts @@ -39,6 +39,7 @@ export const createDeserializer = }, bestCompression: hot?.actions?.forcemerge?.index_codec === 'best_compression', readonlyEnabled: Boolean(hot?.actions?.readonly), + shrink: { isUsingShardSize: Boolean(hot?.actions.shrink?.max_primary_shard_size) }, }, warm: { enabled: Boolean(warm), @@ -47,6 +48,7 @@ export const createDeserializer = dataTierAllocationType: determineDataTierAllocationType(warm?.actions), readonlyEnabled: Boolean(warm?.actions?.readonly), minAgeToMilliSeconds: -1, + shrink: { isUsingShardSize: Boolean(warm?.actions.shrink?.max_primary_shard_size) }, }, cold: { enabled: Boolean(cold), @@ -97,6 +99,13 @@ export const createDeserializer = draft._meta.hot.customRollover.maxAgeUnit = maxAge.units; } } + if (draft.phases.hot?.actions.shrink?.max_primary_shard_size) { + const primaryShardSize = splitSizeAndUnits( + draft.phases.hot.actions.shrink.max_primary_shard_size! + ); + draft.phases.hot.actions.shrink.max_primary_shard_size = primaryShardSize.size; + draft._meta.hot.shrink.maxPrimaryShardSizeUnits = primaryShardSize.units; + } if (draft.phases.warm) { if (draft.phases.warm.actions?.allocate?.require) { @@ -110,6 +119,14 @@ export const createDeserializer = draft.phases.warm.min_age = minAge.size; draft._meta.warm.minAgeUnit = minAge.units; } + + if (draft.phases.warm.actions.shrink?.max_primary_shard_size) { + const primaryShardSize = splitSizeAndUnits( + draft.phases.warm.actions.shrink.max_primary_shard_size! + ); + draft.phases.warm.actions.shrink.max_primary_shard_size = primaryShardSize.size; + draft._meta.warm.shrink.maxPrimaryShardSizeUnits = primaryShardSize.units; + } } if (draft.phases.cold) { diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/global_fields_context.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/global_fields_context.tsx index 5834fe5b9ea77..bb02e5a50a48a 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/global_fields_context.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/global_fields_context.tsx @@ -11,13 +11,15 @@ import { UseMultiFields, FieldHook, FieldConfig } from '../../../../shared_impor /** * Those are the fields that we always want present in our form. */ -interface GlobalFieldsTypes { +export interface GlobalFieldsTypes { deleteEnabled: boolean; searchableSnapshotRepo: string; warmMinAgeMilliSeconds: number; coldMinAgeMilliSeconds: number; frozenMinAgeMilliSeconds: number; deleteMinAgeMilliSeconds: number; + hotIsUsingShardSize: boolean; + warmIsUsingShardSize: boolean; } type GlobalFields = { @@ -46,6 +48,12 @@ export const globalFields: Record { diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/schema.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/schema.ts index c26f54cbb6f5a..24112cf4725d2 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/schema.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/schema.ts @@ -93,6 +93,22 @@ const numberOfShardsField = { label: i18n.translate('xpack.indexLifecycleMgmt.shrink.numberOfPrimaryShardsLabel', { defaultMessage: 'Number of primary shards', }), + defaultValue: 1, + validations: [ + { + validator: emptyField(i18nTexts.editPolicy.errors.numberRequired), + }, + { + validator: numberGreaterThanField({ + message: i18nTexts.editPolicy.errors.numberGreatThan0Required, + than: 0, + }), + }, + ], + serializer: serializers.stringToNumber, +}; +const shardSizeField = { + label: i18nTexts.editPolicy.maxPrimaryShardSizeLabel, validations: [ { validator: emptyField(i18nTexts.editPolicy.errors.numberRequired), @@ -173,6 +189,14 @@ export const getSchema = (isCloudEnabled: boolean): FormSchema => ({ defaultValue: false, label: i18nTexts.editPolicy.readonlyEnabledFieldLabel, }, + shrink: { + isUsingShardSize: { + defaultValue: false, + }, + maxPrimaryShardSizeUnits: { + defaultValue: 'gb', + }, + }, }, warm: { enabled: { @@ -207,6 +231,14 @@ export const getSchema = (isCloudEnabled: boolean): FormSchema => ({ defaultValue: false, label: i18nTexts.editPolicy.readonlyEnabledFieldLabel, }, + shrink: { + isUsingShardSize: { + defaultValue: false, + }, + maxPrimaryShardSizeUnits: { + defaultValue: 'gb', + }, + }, }, cold: { enabled: { @@ -334,12 +366,7 @@ export const getSchema = (isCloudEnabled: boolean): FormSchema => ({ fieldsToValidateOnChange: rolloverFormPaths, }, max_primary_shard_size: { - label: i18n.translate( - 'xpack.indexLifecycleMgmt.hotPhase.maximumPrimaryShardSizeLabel', - { - defaultMessage: 'Maximum primary shard size', - } - ), + label: i18nTexts.editPolicy.maxPrimaryShardSizeLabel, validations: [ { validator: rolloverThresholdsValidator, @@ -370,6 +397,7 @@ export const getSchema = (isCloudEnabled: boolean): FormSchema => ({ }, shrink: { number_of_shards: numberOfShardsField, + max_primary_shard_size: shardSizeField, }, set_priority: { priority: getPriorityField('hot'), @@ -385,6 +413,7 @@ export const getSchema = (isCloudEnabled: boolean): FormSchema => ({ }, shrink: { number_of_shards: numberOfShardsField, + max_primary_shard_size: shardSizeField, }, forcemerge: { max_num_segments: maxNumSegmentsField, diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/serializer/serializer.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/serializer/serializer.ts index cf81468dd2b48..652f045922d4d 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/serializer/serializer.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/serializer/serializer.ts @@ -43,100 +43,104 @@ export const createSerializer = */ if (draft.phases.hot) { draft.phases.hot.min_age = draft.phases.hot.min_age ?? '0ms'; - } - if (draft.phases.hot?.actions) { - const hotPhaseActions = draft.phases.hot.actions; + if (draft.phases.hot?.actions) { + const hotPhaseActions = draft.phases.hot.actions; - /** - * HOT PHASE ROLLOVER - */ - if (isUsingRollover) { - if (_meta.hot?.isUsingDefaultRollover) { - hotPhaseActions.rollover = cloneDeep(defaultRolloverAction); - } else { - // Rollover may not exist if editing an existing policy with initially no rollover configured - if (!hotPhaseActions.rollover) { - hotPhaseActions.rollover = {}; + /** + * HOT PHASE ROLLOVER + */ + if (isUsingRollover) { + if (_meta.hot?.isUsingDefaultRollover) { + hotPhaseActions.rollover = cloneDeep(defaultRolloverAction); + } else { + // Rollover may not exist if editing an existing policy with initially no rollover configured + if (!hotPhaseActions.rollover) { + hotPhaseActions.rollover = {}; + } + + // We are using user-defined, custom rollover settings. + if (updatedPolicy.phases.hot!.actions.rollover?.max_age) { + hotPhaseActions.rollover.max_age = `${hotPhaseActions.rollover.max_age}${_meta.hot?.customRollover.maxAgeUnit}`; + } else { + delete hotPhaseActions.rollover.max_age; + } + + if (typeof updatedPolicy.phases.hot!.actions.rollover?.max_docs !== 'number') { + delete hotPhaseActions.rollover.max_docs; + } + + if (updatedPolicy.phases.hot!.actions.rollover?.max_primary_shard_size) { + hotPhaseActions.rollover.max_primary_shard_size = `${hotPhaseActions.rollover.max_primary_shard_size}${_meta.hot?.customRollover.maxPrimaryShardSizeUnit}`; + } else { + delete hotPhaseActions.rollover.max_primary_shard_size; + } + + if (updatedPolicy.phases.hot!.actions.rollover?.max_size) { + hotPhaseActions.rollover.max_size = `${hotPhaseActions.rollover.max_size}${_meta.hot?.customRollover.maxStorageSizeUnit}`; + } else { + delete hotPhaseActions.rollover.max_size; + } } - // We are using user-defined, custom rollover settings. - if (updatedPolicy.phases.hot!.actions.rollover?.max_age) { - hotPhaseActions.rollover.max_age = `${hotPhaseActions.rollover.max_age}${_meta.hot?.customRollover.maxAgeUnit}`; + /** + * HOT PHASE FORCEMERGE + */ + if (!updatedPolicy.phases.hot!.actions?.forcemerge) { + delete hotPhaseActions.forcemerge; + } else if (_meta.hot?.bestCompression) { + hotPhaseActions.forcemerge!.index_codec = 'best_compression'; } else { - delete hotPhaseActions.rollover.max_age; + delete hotPhaseActions.forcemerge!.index_codec; } - if (typeof updatedPolicy.phases.hot!.actions.rollover?.max_docs !== 'number') { - delete hotPhaseActions.rollover.max_docs; + if (_meta.hot?.bestCompression && hotPhaseActions.forcemerge) { + hotPhaseActions.forcemerge.index_codec = 'best_compression'; } - if (updatedPolicy.phases.hot!.actions.rollover?.max_primary_shard_size) { - hotPhaseActions.rollover.max_primary_shard_size = `${hotPhaseActions.rollover.max_primary_shard_size}${_meta.hot?.customRollover.maxPrimaryShardSizeUnit}`; + /** + * HOT PHASE READ-ONLY + */ + if (_meta.hot?.readonlyEnabled) { + hotPhaseActions.readonly = hotPhaseActions.readonly ?? {}; } else { - delete hotPhaseActions.rollover.max_primary_shard_size; + delete hotPhaseActions.readonly; } - - if (updatedPolicy.phases.hot!.actions.rollover?.max_size) { - hotPhaseActions.rollover.max_size = `${hotPhaseActions.rollover.max_size}${_meta.hot?.customRollover.maxStorageSizeUnit}`; + /** + * HOT PHASE SHRINK + */ + if (!updatedPolicy.phases.hot?.actions?.shrink) { + delete hotPhaseActions.shrink; + } else if (_meta.hot.shrink.isUsingShardSize) { + delete hotPhaseActions.shrink!.number_of_shards; + hotPhaseActions.shrink!.max_primary_shard_size = `${hotPhaseActions.shrink?.max_primary_shard_size}${_meta.hot?.shrink.maxPrimaryShardSizeUnits}`; } else { - delete hotPhaseActions.rollover.max_size; + delete hotPhaseActions.shrink!.max_primary_shard_size; } + } else { + delete hotPhaseActions.rollover; + delete hotPhaseActions.forcemerge; + delete hotPhaseActions.readonly; + delete hotPhaseActions.shrink; } - /** - * HOT PHASE FORCEMERGE + * HOT PHASE SET PRIORITY */ - if (!updatedPolicy.phases.hot!.actions?.forcemerge) { - delete hotPhaseActions.forcemerge; - } else if (_meta.hot?.bestCompression) { - hotPhaseActions.forcemerge!.index_codec = 'best_compression'; - } else { - delete hotPhaseActions.forcemerge!.index_codec; - } - - if (_meta.hot?.bestCompression && hotPhaseActions.forcemerge) { - hotPhaseActions.forcemerge.index_codec = 'best_compression'; + if (!updatedPolicy.phases.hot!.actions?.set_priority) { + delete hotPhaseActions.set_priority; } /** - * HOT PHASE READ-ONLY + * HOT PHASE SEARCHABLE SNAPSHOT */ - if (_meta.hot?.readonlyEnabled) { - hotPhaseActions.readonly = hotPhaseActions.readonly ?? {}; + if (updatedPolicy.phases.hot!.actions?.searchable_snapshot) { + hotPhaseActions.searchable_snapshot = { + ...hotPhaseActions.searchable_snapshot, + snapshot_repository: _meta.searchableSnapshot.repository, + }; } else { - delete hotPhaseActions.readonly; + delete hotPhaseActions.searchable_snapshot; } - } else { - delete hotPhaseActions.rollover; - delete hotPhaseActions.forcemerge; - delete hotPhaseActions.readonly; - } - - /** - * HOT PHASE SET PRIORITY - */ - if (!updatedPolicy.phases.hot!.actions?.set_priority) { - delete hotPhaseActions.set_priority; - } - - /** - * HOT PHASE SHRINK - */ - if (!updatedPolicy.phases.hot?.actions?.shrink) { - delete hotPhaseActions.shrink; - } - - /** - * HOT PHASE SEARCHABLE SNAPSHOT - */ - if (updatedPolicy.phases.hot!.actions?.searchable_snapshot) { - hotPhaseActions.searchable_snapshot = { - ...hotPhaseActions.searchable_snapshot, - snapshot_repository: _meta.searchableSnapshot.repository, - }; - } else { - delete hotPhaseActions.searchable_snapshot; } } @@ -197,6 +201,11 @@ export const createSerializer = */ if (!updatedPolicy.phases.warm?.actions?.shrink) { delete warmPhase.actions.shrink; + } else if (_meta.warm.shrink.isUsingShardSize) { + delete warmPhase.actions.shrink!.number_of_shards; + warmPhase.actions.shrink!.max_primary_shard_size = `${warmPhase.actions.shrink?.max_primary_shard_size}${_meta.warm?.shrink.maxPrimaryShardSizeUnits}`; + } else { + delete warmPhase.actions.shrink!.max_primary_shard_size; } } else { delete draft.phases.warm; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/i18n_texts.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/i18n_texts.ts index bfc31c220825a..dca07fa6b1ead 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/i18n_texts.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/i18n_texts.ts @@ -9,9 +9,21 @@ import { i18n } from '@kbn/i18n'; export const i18nTexts = { editPolicy: { - shrinkLabel: i18n.translate('xpack.indexLifecycleMgmt.shrink.indexFieldLabel', { + shrinkLabel: i18n.translate('xpack.indexLifecycleMgmt.shrink.enableShrinkLabel', { defaultMessage: 'Shrink index', }), + shrinkCountLabel: i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.shrink.configureShardCountLabel', + { + defaultMessage: 'Configure shard count', + } + ), + shrinkSizeLabel: i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.shrink.configureShardSizeLabel', + { + defaultMessage: 'Configure shard size', + } + ), rolloverOffsetsHotPhaseTiming: i18n.translate( 'xpack.indexLifecycleMgmt.rollover.rolloverOffsetsPhaseTimingDescription', { @@ -89,6 +101,18 @@ export const i18nTexts = { defaultMessage: 'Searchable snapshot storage', } ), + maxPrimaryShardSizeLabel: i18n.translate( + 'xpack.indexLifecycleMgmt.hotPhase.maximumPrimaryShardSizeLabel', + { + defaultMessage: 'Maximum primary shard size', + } + ), + maxPrimaryShardSizeUnitsLabel: i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.maximumPrimaryShardSizeAriaLabel', + { + defaultMessage: 'Maximum shard size units', + } + ), errors: { numberRequired: i18n.translate( 'xpack.indexLifecycleMgmt.editPolicy.errors.numberRequiredErrorMessage', diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/types.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/types.ts index ba7d31cf6da49..6c4d311d6177c 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/types.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/types.ts @@ -22,7 +22,14 @@ export interface ForcemergeFields { bestCompression: boolean; } -interface HotPhaseMetaFields extends ForcemergeFields { +interface ShrinkFields { + shrink: { + isUsingShardSize: boolean; + maxPrimaryShardSizeUnits?: string; + }; +} + +interface HotPhaseMetaFields extends ForcemergeFields, ShrinkFields { /** * By default rollover is enabled with set values for max age, max size and max docs. In this policy form * opting in to default rollover overrides custom rollover values. @@ -47,7 +54,11 @@ interface HotPhaseMetaFields extends ForcemergeFields { }; } -interface WarmPhaseMetaFields extends DataAllocationMetaFields, MinAgeField, ForcemergeFields { +interface WarmPhaseMetaFields + extends DataAllocationMetaFields, + MinAgeField, + ForcemergeFields, + ShrinkFields { enabled: boolean; warmPhaseOnRollover: boolean; readonlyEnabled: boolean; diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/evaluate_alert.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/evaluate_alert.ts index c7c1eb5454d1d..45eef3cc85a57 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/evaluate_alert.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/evaluate_alert.ts @@ -64,6 +64,7 @@ export const evaluateAlert = { const { criteria, groupBy, filterQuery, shouldDropPartialBuckets } = params; @@ -91,21 +92,53 @@ export const evaluateAlert = { - if (isTooManyBucketsPreviewException(points)) throw points; - return { - ...criterion, - metric: criterion.metric ?? DOCUMENT_COUNT_I18N, - currentValue: Array.isArray(points) ? last(points)?.value : NaN, - timestamp: Array.isArray(points) ? last(points)?.key : NaN, - shouldFire: pointsEvaluator(points, threshold, comparator), - shouldWarn: pointsEvaluator(points, warningThreshold, warningComparator), - isNoData: Array.isArray(points) - ? points.map((point) => point?.value === null || point === null) - : [points === null], - isError: isNaN(Array.isArray(points) ? last(points)?.value : points), - }; - }); + // If any previous groups are no longer being reported, backfill them with null values + const currentGroups = Object.keys(currentValues); + + const missingGroups = prevGroups.filter((g) => !currentGroups.includes(g)); + if (currentGroups.length === 0 && missingGroups.length === 0) { + missingGroups.push(UNGROUPED_FACTORY_KEY); + } + const backfillTimestamp = + last(last(Object.values(currentValues)))?.key ?? new Date().toISOString(); + const backfilledPrevGroups: Record< + string, + Array<{ key: string; value: number }> + > = missingGroups.reduce( + (result, group) => ({ + ...result, + [group]: [ + { + key: backfillTimestamp, + value: criterion.aggType === Aggregators.COUNT ? 0 : null, + }, + ], + }), + {} + ); + const currentValuesWithBackfilledPrevGroups = { + ...currentValues, + ...backfilledPrevGroups, + }; + + return mapValues( + currentValuesWithBackfilledPrevGroups, + (points: any[] | typeof NaN | null) => { + if (isTooManyBucketsPreviewException(points)) throw points; + return { + ...criterion, + metric: criterion.metric ?? DOCUMENT_COUNT_I18N, + currentValue: Array.isArray(points) ? last(points)?.value : NaN, + timestamp: Array.isArray(points) ? last(points)?.key : NaN, + shouldFire: pointsEvaluator(points, threshold, comparator), + shouldWarn: pointsEvaluator(points, warningThreshold, warningComparator), + isNoData: Array.isArray(points) + ? points.map((point) => point?.value === null || point === null) + : [points === null], + isError: isNaN(Array.isArray(points) ? last(points)?.value : points), + }; + } + ); }) ); }; @@ -119,7 +152,7 @@ const getMetric: ( filterQuery: string | undefined, timeframe?: { start?: number; end: number }, shouldDropPartialBuckets?: boolean -) => Promise> = async function ( +) => Promise>> = async function ( esClient, params, index, diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts index 8eb19ad582057..869d0afd52367 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts @@ -37,10 +37,13 @@ let persistAlertInstances = false; // eslint-disable-line prefer-const type TestRuleState = Record & { aRuleStateKey: string; + groups: string[]; + groupBy?: string | string[]; }; const initialRuleState: TestRuleState = { aRuleStateKey: 'INITIAL_RULE_STATE_VALUE', + groups: [], }; const mockOptions = { @@ -90,6 +93,7 @@ const mockOptions = { describe('The metric threshold alert type', () => { describe('querying the entire infrastructure', () => { + afterAll(() => clearInstances()); const instanceID = '*'; const execute = (comparator: Comparator, threshold: number[], sourceId: string = 'default') => executor({ @@ -157,20 +161,29 @@ describe('The metric threshold alert type', () => { }); describe('querying with a groupBy parameter', () => { - const execute = (comparator: Comparator, threshold: number[], sourceId: string = 'default') => + afterAll(() => clearInstances()); + const execute = ( + comparator: Comparator, + threshold: number[], + groupBy: string[] = ['something'], + metric?: string, + state?: any + ) => executor({ ...mockOptions, services, params: { - groupBy: 'something', + groupBy, criteria: [ { ...baseNonCountCriterion, comparator, threshold, + metric: metric ?? baseNonCountCriterion.metric, }, ], }, + state: state ?? mockOptions.state.wrapped, }); const instanceIdA = 'a'; const instanceIdB = 'b'; @@ -194,9 +207,35 @@ describe('The metric threshold alert type', () => { expect(mostRecentAction(instanceIdA).action.group).toBe('a'); expect(mostRecentAction(instanceIdB).action.group).toBe('b'); }); + test('reports previous groups and the groupBy parameter in its state', async () => { + const stateResult = await execute(Comparator.GT, [0.75]); + expect(stateResult.groups).toEqual(expect.arrayContaining(['a', 'b'])); + expect(stateResult.groupBy).toEqual(['something']); + }); + test('persists previous groups that go missing, until the groupBy param changes', async () => { + const stateResult1 = await execute(Comparator.GT, [0.75], ['something'], 'test.metric.2'); + expect(stateResult1.groups).toEqual(expect.arrayContaining(['a', 'b', 'c'])); + const stateResult2 = await execute( + Comparator.GT, + [0.75], + ['something'], + 'test.metric.1', + stateResult1 + ); + expect(stateResult2.groups).toEqual(expect.arrayContaining(['a', 'b', 'c'])); + const stateResult3 = await execute( + Comparator.GT, + [0.75], + ['something', 'something-else'], + 'test.metric.1', + stateResult2 + ); + expect(stateResult3.groups).toEqual(expect.arrayContaining(['a', 'b'])); + }); }); describe('querying with multiple criteria', () => { + afterAll(() => clearInstances()); const execute = ( comparator: Comparator, thresholdA: number[], @@ -257,6 +296,7 @@ describe('The metric threshold alert type', () => { }); }); describe('querying with the count aggregator', () => { + afterAll(() => clearInstances()); const instanceID = '*'; const execute = (comparator: Comparator, threshold: number[], sourceId: string = 'default') => executor({ @@ -279,8 +319,47 @@ describe('The metric threshold alert type', () => { await execute(Comparator.LT, [0.5]); expect(mostRecentAction(instanceID)).toBe(undefined); }); + describe('with a groupBy parameter', () => { + const executeGroupBy = ( + comparator: Comparator, + threshold: number[], + sourceId: string = 'default', + state?: any + ) => + executor({ + ...mockOptions, + services, + params: { + sourceId, + groupBy: 'something', + criteria: [ + { + ...baseCountCriterion, + comparator, + threshold, + }, + ], + }, + state: state ?? mockOptions.state.wrapped, + }); + const instanceIdA = 'a'; + const instanceIdB = 'b'; + + test('successfully detects and alerts on a document count of 0', async () => { + const resultState = await executeGroupBy(Comparator.LT_OR_EQ, [0]); + expect(mostRecentAction(instanceIdA)).toBe(undefined); + expect(mostRecentAction(instanceIdB)).toBe(undefined); + await executeGroupBy(Comparator.LT_OR_EQ, [0], 'empty-response', resultState); + expect(mostRecentAction(instanceIdA).id).toBe(FIRED_ACTIONS.id); + expect(mostRecentAction(instanceIdB).id).toBe(FIRED_ACTIONS.id); + await executeGroupBy(Comparator.LT_OR_EQ, [0]); + expect(mostRecentAction(instanceIdA)).toBe(undefined); + expect(mostRecentAction(instanceIdB)).toBe(undefined); + }); + }); }); describe('querying with the p99 aggregator', () => { + afterAll(() => clearInstances()); const instanceID = '*'; const execute = (comparator: Comparator, threshold: number[], sourceId: string = 'default') => executor({ @@ -306,6 +385,7 @@ describe('The metric threshold alert type', () => { }); }); describe('querying with the p95 aggregator', () => { + afterAll(() => clearInstances()); const instanceID = '*'; const execute = (comparator: Comparator, threshold: number[], sourceId: string = 'default') => executor({ @@ -332,6 +412,7 @@ describe('The metric threshold alert type', () => { }); }); describe("querying a metric that hasn't reported data", () => { + afterAll(() => clearInstances()); const instanceID = '*'; const execute = (alertOnNoData: boolean, sourceId: string = 'default') => executor({ @@ -360,7 +441,51 @@ describe('The metric threshold alert type', () => { }); }); + describe('querying a groupBy alert that starts reporting no data, and then later reports data', () => { + afterAll(() => clearInstances()); + const instanceID = '*'; + const instanceIdA = 'a'; + const instanceIdB = 'b'; + const execute = (metric: string, state?: any) => + executor({ + ...mockOptions, + services, + params: { + groupBy: 'something', + sourceId: 'default', + criteria: [ + { + ...baseNonCountCriterion, + comparator: Comparator.GT, + threshold: [0], + metric, + }, + ], + alertOnNoData: true, + }, + state: state ?? mockOptions.state.wrapped, + }); + const resultState: any[] = []; + test('first sends a No Data alert with the * group, but then reports groups when data is available', async () => { + resultState.push(await execute('test.metric.3')); + expect(mostRecentAction(instanceID).id).toBe(FIRED_ACTIONS.id); + resultState.push(await execute('test.metric.3', resultState.pop())); + expect(mostRecentAction(instanceID).id).toBe(FIRED_ACTIONS.id); + resultState.push(await execute('test.metric.1', resultState.pop())); + expect(mostRecentAction(instanceID)).toBe(undefined); + expect(mostRecentAction(instanceIdA).id).toBe(FIRED_ACTIONS.id); + expect(mostRecentAction(instanceIdB).id).toBe(FIRED_ACTIONS.id); + }); + test('sends No Data alerts for the previously detected groups when they stop reporting data, but not the * group', async () => { + await execute('test.metric.3', resultState.pop()); + expect(mostRecentAction(instanceID)).toBe(undefined); + expect(mostRecentAction(instanceIdA).id).toBe(FIRED_ACTIONS.id); + expect(mostRecentAction(instanceIdB).id).toBe(FIRED_ACTIONS.id); + }); + }); + describe("querying a rate-aggregated metric that hasn't reported data", () => { + afterAll(() => clearInstances()); const instanceID = '*'; const execute = (sourceId: string = 'default') => executor({ @@ -439,6 +564,7 @@ describe('The metric threshold alert type', () => { */ describe('querying a metric with a percentage metric', () => { + afterAll(() => clearInstances()); const instanceID = '*'; const execute = () => executor({ @@ -497,7 +623,15 @@ const services: AlertServicesMock & services.scopedClusterClient.asCurrentUser.search.mockImplementation((params?: any): any => { const from = params?.body.query.bool.filter[0]?.range['@timestamp'].gte; if (params.index === 'alternatebeat-*') return mocks.changedSourceIdResponse(from); + if (params.index === 'empty-response') return mocks.emptyMetricResponse; const metric = params?.body.query.bool.filter[1]?.exists.field; + if (metric === 'test.metric.3') { + return elasticsearchClientMock.createSuccessTransportRequestPromise( + params?.body.aggs.aggregatedIntervals?.aggregations.aggregatedValueMax + ? mocks.emptyRateResponse + : mocks.emptyMetricResponse + ); + } if (params?.body.aggs.groupings) { if (params?.body.aggs.groupings.composite.after) { return elasticsearchClientMock.createSuccessTransportRequestPromise( @@ -517,12 +651,6 @@ services.scopedClusterClient.asCurrentUser.search.mockImplementation((params?: a return elasticsearchClientMock.createSuccessTransportRequestPromise( mocks.alternateMetricResponse() ); - } else if (metric === 'test.metric.3') { - return elasticsearchClientMock.createSuccessTransportRequestPromise( - params?.body.aggs.aggregatedIntervals.aggregations.aggregatedValueMax - ? mocks.emptyRateResponse - : mocks.emptyMetricResponse - ); } return elasticsearchClientMock.createSuccessTransportRequestPromise(mocks.basicMetricResponse()); }); @@ -534,6 +662,13 @@ services.savedObjectsClient.get.mockImplementation(async (type: string, sourceId type, references: [], }; + if (sourceId === 'empty-response') + return { + id: 'empty', + attributes: { metricAlias: 'empty-response' }, + type, + references: [], + }; return { id: 'default', attributes: { metricAlias: 'metricbeat-*' }, type, references: [] }; }); @@ -561,7 +696,13 @@ services.alertInstanceFactory.mockImplementation((instanceID: string) => { }); function mostRecentAction(id: string) { - return alertInstances.get(id)!.actionQueue.pop(); + const instance = alertInstances.get(id); + if (!instance) return undefined; + return instance.actionQueue.pop(); +} + +function clearInstances() { + alertInstances.clear(); } const baseNonCountCriterion: Pick< diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts index 9c99ad6bf49e2..f49b281909f4b 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { first, last } from 'lodash'; +import { first, last, isEqual } from 'lodash'; import { i18n } from '@kbn/i18n'; import moment from 'moment'; import { ALERT_REASON } from '@kbn/rule-data-utils'; @@ -24,12 +24,16 @@ import { // buildRecoveredAlertReason, stateToAlertMessage, } from '../common/messages'; +import { UNGROUPED_FACTORY_KEY } from '../common/utils'; import { createFormatter } from '../../../../common/formatters'; import { AlertStates, Comparator } from './types'; import { evaluateAlert, EvaluatedAlertParams } from './lib/evaluate_alert'; export type MetricThresholdAlertTypeParams = Record; -export type MetricThresholdAlertTypeState = AlertTypeState; // no specific state used +export type MetricThresholdAlertTypeState = AlertTypeState & { + groups: string[]; + groupBy?: string | string[]; +}; export type MetricThresholdAlertInstanceState = AlertInstanceState; // no specific instace state used export type MetricThresholdAlertInstanceContext = AlertInstanceContext; // no specific instace state used @@ -58,7 +62,7 @@ export const createMetricThresholdExecutor = (libs: InfraBackendLibs) => MetricThresholdAlertInstanceContext, MetricThresholdAllowedActionGroups >(async function (options) { - const { services, params } = options; + const { services, params, state } = options; const { criteria } = params; if (criteria.length === 0) throw new Error('Cannot execute an alert with 0 conditions'); const { alertWithLifecycle, savedObjectsClient } = services; @@ -80,14 +84,28 @@ export const createMetricThresholdExecutor = (libs: InfraBackendLibs) => sourceId || 'default' ); const config = source.configuration; + + const previousGroupBy = state.groupBy; + const prevGroups = isEqual(previousGroupBy, params.groupBy) + ? // Filter out the * key from the previous groups, only include it if it's one of + // the current groups. In case of a groupBy alert that starts out with no data and no + // groups, we don't want to persist the existence of the * alert instance + state.groups?.filter((g) => g !== UNGROUPED_FACTORY_KEY) ?? [] + : []; + const alertResults = await evaluateAlert( services.scopedClusterClient.asCurrentUser, params as EvaluatedAlertParams, - config + config, + prevGroups ); // Because each alert result has the same group definitions, just grab the groups from the first one. - const groups = Object.keys(first(alertResults)!); + const resultGroups = Object.keys(first(alertResults)!); + // Merge the list of currently fetched groups and previous groups, and uniquify them. This is necessary for reporting + // no data results on groups that get removed + const groups = [...new Set([...prevGroups, ...resultGroups])]; + for (const group of groups) { // AND logic; all criteria must be across the threshold const shouldAlertFire = alertResults.every((result) => @@ -169,6 +187,8 @@ export const createMetricThresholdExecutor = (libs: InfraBackendLibs) => }); } } + + return { groups, groupBy: params.groupBy }; }); export const FIRED_ACTIONS = { diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/test_mocks.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/test_mocks.ts index b1173f2d611c8..db6b771e91784 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/test_mocks.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/test_mocks.ts @@ -199,12 +199,20 @@ export const alternateCompositeResponse = (from: number) => ({ buckets: bucketsA(from), }, }, + { + key: { + groupBy0: 'c', + }, + aggregatedIntervals: { + buckets: bucketsC(from), + }, + }, ], }, }, hits: { total: { - value: 2, + value: 3, }, }, }); diff --git a/x-pack/plugins/infra/server/utils/get_all_composite_data.ts b/x-pack/plugins/infra/server/utils/get_all_composite_data.ts index df97c91aacd04..1ab290796e36d 100644 --- a/x-pack/plugins/infra/server/utils/get_all_composite_data.ts +++ b/x-pack/plugins/infra/server/utils/get_all_composite_data.ts @@ -24,7 +24,7 @@ export const getAllCompositeData = async < const { body: response } = await esClientSearch(options); // Nothing available, return the previous buckets. - if (response.hits.total.value === 0) { + if (response.hits?.total.value === 0) { return previousBuckets; } diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/dissect.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/dissect.tsx index 3864581317e38..be55000bf374a 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/dissect.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/dissect.tsx @@ -42,11 +42,7 @@ const getFieldsConfig = (esDocUrl: string): Record => { defaultMessage="Pattern used to dissect the specified field. The pattern is defined by the parts of the string to discard. Use a {keyModifier} to alter the dissection behavior." values={{ keyModifier: ( - + {i18n.translate( 'xpack.ingestPipelines.pipelineEditor.dissectForm.patternFieldHelpText.dissectProcessorLink', { @@ -97,7 +93,7 @@ const getFieldsConfig = (esDocUrl: string): Record => { export const Dissect: FunctionComponent = () => { const { services } = useKibana(); - const fieldsConfig = getFieldsConfig(services.documentation.getEsDocsBasePath()); + const fieldsConfig = getFieldsConfig(services.documentation.getDissectKeyModifiersUrl()); return ( <> diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/enrich.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/enrich.tsx index dfbcfc9566507..1c6292795d587 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/enrich.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/enrich.tsx @@ -139,7 +139,6 @@ const fieldsConfig: FieldsConfig = { export const Enrich: FunctionComponent = () => { const { services } = useKibana(); - const esDocUrl = services.documentation.getEsDocsBasePath(); return ( <> { defaultMessage="Name of the {enrichPolicyLink}." values={{ enrichPolicyLink: ( - + {i18n.translate( 'xpack.ingestPipelines.pipelineEditor.enrichForm.policyNameHelpText.enrichPolicyLink', { defaultMessage: 'enrich policy' } @@ -206,11 +209,7 @@ export const Enrich: FunctionComponent = () => { defaultMessage="Operator used to match the geo-shape of incoming documents to enrich documents. Only used for {geoMatchPolicyLink}." values={{ geoMatchPolicyLink: ( - + {i18n.translate( 'xpack.ingestPipelines.pipelineEditor.enrichForm.shapeRelationFieldHelpText.geoMatchPoliciesLink', { defaultMessage: 'geo-match enrich policies' } diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/inference.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/inference.tsx index 9575e6d690e00..9c3601c368342 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/inference.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/inference.tsx @@ -28,14 +28,12 @@ const { emptyField, isJsonField } = fieldValidators; const INFERENCE_CONFIG_DOCS = { regression: { - path: 'inference-processor.html#inference-processor-regression-opt', linkLabel: i18n.translate( 'xpack.ingestPipelines.pipelineEditor.inferenceForm.inferenceConfigField.regressionLinkLabel', { defaultMessage: 'regression' } ), }, classification: { - path: 'inference-processor.html#inference-processor-classification-opt', linkLabel: i18n.translate( 'xpack.ingestPipelines.pipelineEditor.inferenceForm.inferenceConfigField.classificationLinkLabel', { defaultMessage: 'classification' } @@ -43,27 +41,22 @@ const INFERENCE_CONFIG_DOCS = { }, }; -const getInferenceConfigHelpText = (esDocsBasePath: string): React.ReactNode => { +const getInferenceConfigHelpText = ( + regressionDocsLink: string, + classificationDocsLink: string +): React.ReactNode => { return ( + {INFERENCE_CONFIG_DOCS.regression.linkLabel} ), classification: ( - + {INFERENCE_CONFIG_DOCS.classification.linkLabel} ), @@ -158,7 +151,8 @@ const fieldsConfig: FieldsConfig = { export const Inference: FunctionComponent = () => { const { services } = useKibana(); - const esDocUrl = services.documentation.getEsDocsBasePath(); + const regressionDocsLink = services.documentation.getRegressionUrl(); + const classificationDocsLink = services.documentation.getClassificationUrl(); return ( <> @@ -188,7 +182,7 @@ export const Inference: FunctionComponent = () => { = ({ values={{ learnMoreLink: ( diff --git a/x-pack/plugins/ingest_pipelines/public/application/services/documentation.ts b/x-pack/plugins/ingest_pipelines/public/application/services/documentation.ts index 8aa165cc502a8..801088b868370 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/services/documentation.ts +++ b/x-pack/plugins/ingest_pipelines/public/application/services/documentation.ts @@ -13,16 +13,28 @@ export class DocumentationService { private processorsUrl: string = ''; private handlingFailureUrl: string = ''; private putPipelineApiUrl: string = ''; + private simulatePipelineApiUrl: string = ''; + private enrichDataUrl: string = ''; + private geoMatchUrl: string = ''; + private dissectKeyModifiersUrl: string = ''; + private classificationUrl: string = ''; + private regressionUrl: string = ''; public setup(docLinks: DocLinksStart): void { const { DOC_LINK_VERSION, ELASTIC_WEBSITE_URL, links } = docLinks; const docsBase = `${ELASTIC_WEBSITE_URL}guide/en`; this.esDocBasePath = `${docsBase}/elasticsearch/reference/${DOC_LINK_VERSION}`; - this.ingestNodeUrl = `${links.ingest.pipelines}`; - this.processorsUrl = `${links.ingest.processors}`; - this.handlingFailureUrl = `${links.ingest.pipelineFailure}`; - this.putPipelineApiUrl = `${links.apis.createPipeline}`; + this.ingestNodeUrl = links.ingest.pipelines; + this.processorsUrl = links.ingest.processors; + this.handlingFailureUrl = links.ingest.pipelineFailure; + this.putPipelineApiUrl = links.apis.createPipeline; + this.simulatePipelineApiUrl = links.apis.simulatePipeline; + this.enrichDataUrl = links.ingest.enrich; + this.geoMatchUrl = links.ingest.geoMatch; + this.dissectKeyModifiersUrl = links.ingest.dissectKeyModifiers; + this.classificationUrl = links.ingest.inferenceClassification; + this.regressionUrl = links.ingest.inferenceRegression; } public getEsDocsBasePath() { @@ -44,6 +56,30 @@ export class DocumentationService { public getPutPipelineApiUrl() { return this.putPipelineApiUrl; } + + public getSimulatePipelineApiUrl() { + return this.simulatePipelineApiUrl; + } + + public getEnrichDataUrl() { + return this.enrichDataUrl; + } + + public getGeoMatchUrl() { + return this.geoMatchUrl; + } + + public getDissectKeyModifiersUrl() { + return this.dissectKeyModifiersUrl; + } + + public getClassificationUrl() { + return this.classificationUrl; + } + + public getRegressionUrl() { + return this.regressionUrl; + } } export const documentationService = new DocumentationService(); diff --git a/x-pack/plugins/lens/public/app_plugin/app.test.tsx b/x-pack/plugins/lens/public/app_plugin/app.test.tsx index 25a809cb3c05d..275e6519367c7 100644 --- a/x-pack/plugins/lens/public/app_plugin/app.test.tsx +++ b/x-pack/plugins/lens/public/app_plugin/app.test.tsx @@ -604,6 +604,9 @@ describe('Lens App', () => { }); it('handles save failure by showing a warning, but still allows another save', async () => { + const mockedConsoleDir = jest.spyOn(console, 'dir'); // mocked console.dir to avoid messages in the console when running tests + mockedConsoleDir.mockImplementation(() => {}); + const services = makeDefaultServices(sessionIdSubject); services.attributeService.wrapAttributes = jest .fn() @@ -620,6 +623,9 @@ describe('Lens App', () => { }); expect(props.redirectTo).not.toHaveBeenCalled(); expect(getButton(instance).disableButton).toEqual(false); + // eslint-disable-next-line no-console + expect(console.dir).toHaveBeenCalledTimes(1); + mockedConsoleDir.mockRestore(); }); it('saves new doc and redirects to originating app', async () => { diff --git a/x-pack/plugins/monitoring/server/lib/ccs_utils.test.js b/x-pack/plugins/monitoring/common/ccs_utils.test.js similarity index 100% rename from x-pack/plugins/monitoring/server/lib/ccs_utils.test.js rename to x-pack/plugins/monitoring/common/ccs_utils.test.js diff --git a/x-pack/plugins/monitoring/server/lib/ccs_utils.ts b/x-pack/plugins/monitoring/common/ccs_utils.ts similarity index 96% rename from x-pack/plugins/monitoring/server/lib/ccs_utils.ts rename to x-pack/plugins/monitoring/common/ccs_utils.ts index 1d899456913b9..7efe6e43ddbbd 100644 --- a/x-pack/plugins/monitoring/server/lib/ccs_utils.ts +++ b/x-pack/plugins/monitoring/common/ccs_utils.ts @@ -6,7 +6,8 @@ */ import { isFunction, get } from 'lodash'; -import type { MonitoringConfig } from '../config'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import type { MonitoringConfig } from '../server/config'; type Config = Partial & { get?: (key: string) => any; diff --git a/x-pack/plugins/monitoring/common/types/alerts.ts b/x-pack/plugins/monitoring/common/types/alerts.ts index 17bbffce19a18..1f68b0c55a046 100644 --- a/x-pack/plugins/monitoring/common/types/alerts.ts +++ b/x-pack/plugins/monitoring/common/types/alerts.ts @@ -48,12 +48,16 @@ export interface CommonAlertParams { duration: string; threshold?: number; limit?: string; + filterQuery?: string; + filterQueryText?: string; [key: string]: unknown; } export interface ThreadPoolRejectionsAlertParams { threshold: number; duration: string; + filterQuery?: string; + filterQueryText?: string; } export interface AlertEnableAction { diff --git a/x-pack/plugins/monitoring/public/alerts/ccr_read_exceptions_alert/index.tsx b/x-pack/plugins/monitoring/public/alerts/ccr_read_exceptions_alert/index.tsx index 1de9a175026a6..64eab04cbd5ce 100644 --- a/x-pack/plugins/monitoring/public/alerts/ccr_read_exceptions_alert/index.tsx +++ b/x-pack/plugins/monitoring/public/alerts/ccr_read_exceptions_alert/index.tsx @@ -15,6 +15,7 @@ import { RULE_REQUIRES_APP_CONTEXT, } from '../../../common/constants'; import { AlertTypeParams } from '../../../../alerting/common'; +import { MonitoringConfig } from '../../types'; interface ValidateOptions extends AlertTypeParams { duration: string; @@ -36,7 +37,9 @@ const validate = (inputValues: ValidateOptions): ValidationResult => { return validationResult; }; -export function createCCRReadExceptionsAlertType(): AlertTypeModel { +export function createCCRReadExceptionsAlertType( + config: MonitoringConfig +): AlertTypeModel { return { id: RULE_CCR_READ_EXCEPTIONS, description: RULE_DETAILS[RULE_CCR_READ_EXCEPTIONS].description, @@ -45,7 +48,11 @@ export function createCCRReadExceptionsAlertType(): AlertTypeModel ( - + ), validate, defaultActionMessage: '{{context.internalFullMessage}}', diff --git a/x-pack/plugins/monitoring/public/alerts/components/param_details_form/expression.tsx b/x-pack/plugins/monitoring/public/alerts/components/param_details_form/expression.tsx index df17ce1a911a0..827eed955d535 100644 --- a/x-pack/plugins/monitoring/public/alerts/components/param_details_form/expression.tsx +++ b/x-pack/plugins/monitoring/public/alerts/components/param_details_form/expression.tsx @@ -5,14 +5,23 @@ * 2.0. */ -import React, { Fragment } from 'react'; -import { EuiForm, EuiSpacer } from '@elastic/eui'; +import React, { Fragment, useCallback } from 'react'; +import { EuiForm, EuiFormRow, EuiSpacer } from '@elastic/eui'; +import { DataPublicPluginStart } from 'src/plugins/data/public'; +import { debounce } from 'lodash'; +import { i18n } from '@kbn/i18n'; import { CommonAlertParamDetails } from '../../../../common/types/alerts'; import { AlertParamDuration } from '../../flyout_expressions/alert_param_duration'; import { AlertParamType } from '../../../../common/enums'; import { AlertParamPercentage } from '../../flyout_expressions/alert_param_percentage'; import { AlertParamNumber } from '../../flyout_expressions/alert_param_number'; import { AlertParamTextField } from '../../flyout_expressions/alert_param_textfield'; +import { MonitoringConfig } from '../../../types'; +import { useDerivedIndexPattern } from './use_derived_index_pattern'; +import { KueryBar } from '../../../components/kuery_bar'; +import { convertKueryToElasticSearchQuery } from '../../../lib/kuery'; + +const FILTER_TYPING_DEBOUNCE_MS = 500; export interface Props { alertParams: { [property: string]: any }; @@ -20,10 +29,14 @@ export interface Props { setAlertProperty: (property: string, value: any) => void; errors: { [key: string]: string[] }; paramDetails: CommonAlertParamDetails; + data: DataPublicPluginStart; + config?: MonitoringConfig; } export const Expression: React.FC = (props) => { - const { alertParams, paramDetails, setAlertParams, errors } = props; + const { alertParams, paramDetails, setAlertParams, errors, config, data } = props; + + const { derivedIndexPattern } = useDerivedIndexPattern(data, config); const alertParamsUi = Object.keys(paramDetails).map((alertParamName) => { const details = paramDetails[alertParamName]; @@ -77,10 +90,44 @@ export const Expression: React.FC = (props) => { } }); + const onFilterChange = useCallback( + (filter: string) => { + setAlertParams('filterQueryText', filter); + setAlertParams( + 'filterQuery', + convertKueryToElasticSearchQuery(filter, derivedIndexPattern) || '' + ); + }, + [setAlertParams, derivedIndexPattern] + ); + + /* eslint-disable-next-line react-hooks/exhaustive-deps */ + const debouncedOnFilterChange = useCallback(debounce(onFilterChange, FILTER_TYPING_DEBOUNCE_MS), [ + onFilterChange, + ]); + return ( - {alertParamsUi} - + + {alertParamsUi} + + + + + + ); }; diff --git a/x-pack/plugins/monitoring/public/alerts/components/param_details_form/use_derived_index_pattern.tsx b/x-pack/plugins/monitoring/public/alerts/components/param_details_form/use_derived_index_pattern.tsx new file mode 100644 index 0000000000000..1a4d88d690b84 --- /dev/null +++ b/x-pack/plugins/monitoring/public/alerts/components/param_details_form/use_derived_index_pattern.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useEffect, useState } from 'react'; +import { DataPublicPluginStart, IFieldType, IIndexPattern } from 'src/plugins/data/public'; +import { + INDEX_PATTERN_BEATS, + INDEX_PATTERN_ELASTICSEARCH, + INDEX_PATTERN_KIBANA, + INDEX_PATTERN_LOGSTASH, +} from '../../../../common/constants'; +import { prefixIndexPattern } from '../../../../common/ccs_utils'; +import { MonitoringConfig } from '../../../types'; + +const INDEX_PATTERNS = `${INDEX_PATTERN_ELASTICSEARCH},${INDEX_PATTERN_KIBANA},${INDEX_PATTERN_LOGSTASH},${INDEX_PATTERN_BEATS}`; + +export const useDerivedIndexPattern = ( + data: DataPublicPluginStart, + config?: MonitoringConfig +): { loading: boolean; derivedIndexPattern: IIndexPattern } => { + const indexPattern = prefixIndexPattern(config || ({} as MonitoringConfig), INDEX_PATTERNS, '*'); + const [loading, setLoading] = useState(true); + const [fields, setFields] = useState([]); + useEffect(() => { + (async function fetchData() { + const result = await data.indexPatterns.getFieldsForWildcard({ + pattern: indexPattern, + }); + setFields(result); + setLoading(false); + })(); + }, [indexPattern, data.indexPatterns]); + return { + loading, + derivedIndexPattern: { + title: indexPattern, + fields, + }, + }; +}; diff --git a/x-pack/plugins/monitoring/public/alerts/cpu_usage_alert/cpu_usage_alert.tsx b/x-pack/plugins/monitoring/public/alerts/cpu_usage_alert/cpu_usage_alert.tsx index ec7a583ec2ba1..f0e0a413435f9 100644 --- a/x-pack/plugins/monitoring/public/alerts/cpu_usage_alert/cpu_usage_alert.tsx +++ b/x-pack/plugins/monitoring/public/alerts/cpu_usage_alert/cpu_usage_alert.tsx @@ -11,8 +11,11 @@ import { AlertTypeModel } from '../../../../triggers_actions_ui/public/types'; import { RULE_CPU_USAGE, RULE_DETAILS, RULE_REQUIRES_APP_CONTEXT } from '../../../common/constants'; import { validate, MonitoringAlertTypeParams } from '../components/param_details_form/validation'; import { Expression, Props } from '../components/param_details_form/expression'; +import { MonitoringConfig } from '../../types'; -export function createCpuUsageAlertType(): AlertTypeModel { +export function createCpuUsageAlertType( + config: MonitoringConfig +): AlertTypeModel { return { id: RULE_CPU_USAGE, description: RULE_DETAILS[RULE_CPU_USAGE].description, @@ -21,7 +24,11 @@ export function createCpuUsageAlertType(): AlertTypeModel ( - + ), validate, defaultActionMessage: '{{context.internalFullMessage}}', diff --git a/x-pack/plugins/monitoring/public/alerts/disk_usage_alert/index.tsx b/x-pack/plugins/monitoring/public/alerts/disk_usage_alert/index.tsx index 779945da0c9e0..5f9f9536ae567 100644 --- a/x-pack/plugins/monitoring/public/alerts/disk_usage_alert/index.tsx +++ b/x-pack/plugins/monitoring/public/alerts/disk_usage_alert/index.tsx @@ -16,8 +16,11 @@ import { RULE_DETAILS, RULE_REQUIRES_APP_CONTEXT, } from '../../../common/constants'; +import { MonitoringConfig } from '../../types'; -export function createDiskUsageAlertType(): AlertTypeModel { +export function createDiskUsageAlertType( + config: MonitoringConfig +): AlertTypeModel { return { id: RULE_DISK_USAGE, description: RULE_DETAILS[RULE_DISK_USAGE].description, @@ -26,7 +29,11 @@ export function createDiskUsageAlertType(): AlertTypeModel ( - + ), validate, defaultActionMessage: '{{context.internalFullMessage}}', diff --git a/x-pack/plugins/monitoring/public/alerts/large_shard_size_alert/index.tsx b/x-pack/plugins/monitoring/public/alerts/large_shard_size_alert/index.tsx index e0f595abe7602..afaf20d60d882 100644 --- a/x-pack/plugins/monitoring/public/alerts/large_shard_size_alert/index.tsx +++ b/x-pack/plugins/monitoring/public/alerts/large_shard_size_alert/index.tsx @@ -15,6 +15,7 @@ import { RULE_REQUIRES_APP_CONTEXT, } from '../../../common/constants'; import { AlertTypeParams } from '../../../../alerting/common'; +import { MonitoringConfig } from '../../types'; interface ValidateOptions extends AlertTypeParams { indexPattern: string; @@ -36,7 +37,9 @@ const validate = (inputValues: ValidateOptions): ValidationResult => { return validationResult; }; -export function createLargeShardSizeAlertType(): AlertTypeModel { +export function createLargeShardSizeAlertType( + config: MonitoringConfig +): AlertTypeModel { return { id: RULE_LARGE_SHARD_SIZE, description: RULE_DETAILS[RULE_LARGE_SHARD_SIZE].description, @@ -45,7 +48,11 @@ export function createLargeShardSizeAlertType(): AlertTypeModel return `${docLinks.links.monitoring.alertsKibanaLargeShardSize}`; }, alertParamsExpression: (props: Props) => ( - + ), validate, defaultActionMessage: '{{context.internalFullMessage}}', diff --git a/x-pack/plugins/monitoring/public/alerts/legacy_alert/expression.tsx b/x-pack/plugins/monitoring/public/alerts/legacy_alert/expression.tsx new file mode 100644 index 0000000000000..fe6adf66c1d4f --- /dev/null +++ b/x-pack/plugins/monitoring/public/alerts/legacy_alert/expression.tsx @@ -0,0 +1,56 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React, { useCallback } from 'react'; +import { debounce } from 'lodash'; +import { EuiSpacer, EuiForm, EuiFormRow } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { useDerivedIndexPattern } from '../components/param_details_form/use_derived_index_pattern'; +import { convertKueryToElasticSearchQuery } from '../../lib/kuery'; +import { KueryBar } from '../../components/kuery_bar'; +import { Props } from '../components/param_details_form/expression'; + +const FILTER_TYPING_DEBOUNCE_MS = 500; + +export const Expression = ({ alertParams, config, setAlertParams, data }: Props) => { + const { derivedIndexPattern } = useDerivedIndexPattern(data, config); + const onFilterChange = useCallback( + (filter: string) => { + setAlertParams('filterQueryText', filter); + setAlertParams( + 'filterQuery', + convertKueryToElasticSearchQuery(filter, derivedIndexPattern) || '' + ); + }, + [setAlertParams, derivedIndexPattern] + ); + + /* eslint-disable-next-line react-hooks/exhaustive-deps */ + const debouncedOnFilterChange = useCallback(debounce(onFilterChange, FILTER_TYPING_DEBOUNCE_MS), [ + onFilterChange, + ]); + return ( + + + + + + + ); +}; diff --git a/x-pack/plugins/monitoring/public/alerts/legacy_alert/legacy_alert.tsx b/x-pack/plugins/monitoring/public/alerts/legacy_alert/legacy_alert.tsx index cac4873bc0c79..a6c22035c5a5a 100644 --- a/x-pack/plugins/monitoring/public/alerts/legacy_alert/legacy_alert.tsx +++ b/x-pack/plugins/monitoring/public/alerts/legacy_alert/legacy_alert.tsx @@ -5,9 +5,7 @@ * 2.0. */ -import React, { Fragment } from 'react'; -import { i18n } from '@kbn/i18n'; -import { EuiTextColor, EuiSpacer } from '@elastic/eui'; +import React from 'react'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { AlertTypeModel } from '../../../../triggers_actions_ui/public/types'; import { @@ -15,8 +13,11 @@ import { LEGACY_RULE_DETAILS, RULE_REQUIRES_APP_CONTEXT, } from '../../../common/constants'; +import { MonitoringConfig } from '../../types'; +import { Expression } from './expression'; +import { Props } from '../components/param_details_form/expression'; -export function createLegacyAlertTypes(): AlertTypeModel[] { +export function createLegacyAlertTypes(config: MonitoringConfig): AlertTypeModel[] { return LEGACY_RULES.map((legacyAlert) => { return { id: legacyAlert, @@ -25,17 +26,7 @@ export function createLegacyAlertTypes(): AlertTypeModel[] { documentationUrl(docLinks) { return `${docLinks.links.monitoring.alertsKibanaClusterAlerts}`; }, - alertParamsExpression: () => ( - - - - {i18n.translate('xpack.monitoring.alerts.legacyAlert.expressionText', { - defaultMessage: 'There is nothing to configure.', - })} - - - - ), + alertParamsExpression: (props: Props) => , defaultActionMessage: '{{context.internalFullMessage}}', validate: () => ({ errors: {} }), requiresAppContext: RULE_REQUIRES_APP_CONTEXT, diff --git a/x-pack/plugins/monitoring/public/alerts/memory_usage_alert/index.tsx b/x-pack/plugins/monitoring/public/alerts/memory_usage_alert/index.tsx index 3e55b6d5454ff..2fe0c9b77c0eb 100644 --- a/x-pack/plugins/monitoring/public/alerts/memory_usage_alert/index.tsx +++ b/x-pack/plugins/monitoring/public/alerts/memory_usage_alert/index.tsx @@ -16,8 +16,11 @@ import { RULE_DETAILS, RULE_REQUIRES_APP_CONTEXT, } from '../../../common/constants'; +import { MonitoringConfig } from '../../types'; -export function createMemoryUsageAlertType(): AlertTypeModel { +export function createMemoryUsageAlertType( + config: MonitoringConfig +): AlertTypeModel { return { id: RULE_MEMORY_USAGE, description: RULE_DETAILS[RULE_MEMORY_USAGE].description, @@ -26,7 +29,11 @@ export function createMemoryUsageAlertType(): AlertTypeModel ( - + ), validate, defaultActionMessage: '{{context.internalFullMessage}}', diff --git a/x-pack/plugins/monitoring/public/alerts/thread_pool_rejections_alert/index.tsx b/x-pack/plugins/monitoring/public/alerts/thread_pool_rejections_alert/index.tsx index 7fd9438e1cea3..e8a15ad835581 100644 --- a/x-pack/plugins/monitoring/public/alerts/thread_pool_rejections_alert/index.tsx +++ b/x-pack/plugins/monitoring/public/alerts/thread_pool_rejections_alert/index.tsx @@ -13,6 +13,7 @@ import { Expression, Props } from '../components/param_details_form/expression'; import { AlertTypeModel } from '../../../../triggers_actions_ui/public/types'; import { CommonAlertParamDetails } from '../../../common/types/alerts'; import { RULE_REQUIRES_APP_CONTEXT } from '../../../common/constants'; +import { MonitoringConfig } from '../../types'; interface ThreadPoolTypes { [key: string]: unknown; @@ -26,7 +27,8 @@ interface ThreadPoolRejectionAlertDetails { export function createThreadPoolRejectionsAlertType( alertId: string, - threadPoolAlertDetails: ThreadPoolRejectionAlertDetails + threadPoolAlertDetails: ThreadPoolRejectionAlertDetails, + config: MonitoringConfig ): AlertTypeModel { return { id: alertId, @@ -38,7 +40,7 @@ export function createThreadPoolRejectionsAlertType( alertParamsExpression: (props: Props) => ( <> - + ), validate: (inputValues: ThreadPoolTypes) => { diff --git a/x-pack/plugins/monitoring/public/application/hooks/use_table.ts b/x-pack/plugins/monitoring/public/application/hooks/use_table.ts new file mode 100644 index 0000000000000..60264f3657fe3 --- /dev/null +++ b/x-pack/plugins/monitoring/public/application/hooks/use_table.ts @@ -0,0 +1,162 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useState, useCallback } from 'react'; +import { EUI_SORT_ASCENDING } from '../../../common/constants'; +import { euiTableStorageGetter, euiTableStorageSetter } from '../../components/table'; +import { Storage } from '../../../../../../src/plugins/kibana_utils/public'; + +interface Pagination { + pageSize: number; + initialPageSize: number; + pageIndex: number; + initialPageIndex: number; + pageSizeOptions: number[]; + totalItemCount: number; +} + +interface Page { + size: number; + index: number; +} + +interface Sorting { + sort: { + field: string; + direction: string; + }; +} + +const PAGE_SIZE_OPTIONS = [5, 10, 20, 50]; + +const DEFAULT_PAGINATION = { + pageSize: 20, + initialPageSize: 20, + pageIndex: 0, + initialPageIndex: 0, + pageSizeOptions: PAGE_SIZE_OPTIONS, + totalItemCount: 0, +}; + +const getPaginationInitialState = (page: Page | undefined) => { + const pagination = DEFAULT_PAGINATION; + + if (page) { + pagination.initialPageSize = page.size; + pagination.pageSize = page.size; + pagination.initialPageIndex = page.index; + pagination.pageIndex = page.index; + } + + return { + ...pagination, + pageSizeOptions: PAGE_SIZE_OPTIONS, + }; +}; + +export function useTable(storageKey: string) { + const storage = new Storage(window.localStorage); + const getLocalStorageData = euiTableStorageGetter(storageKey); + const setLocalStorageData = euiTableStorageSetter(storageKey); + + const storageData = getLocalStorageData(storage); + // get initial state from localstorage + const [pagination, setPagination] = useState( + getPaginationInitialState(storageData.page) + ); + + const updateTotalItemCount = useCallback( + (num) => { + // only update pagination state if different + if (num === pagination.totalItemCount) return; + setPagination({ + ...pagination, + totalItemCount: num, + }); + }, + [setPagination, pagination] + ); + + // get initial state from localStorage + const [sorting, setSorting] = useState(storageData.sort || { sort: {} }); + const cleanSortingData = (sortData: Sorting) => { + const sort = sortData || { sort: {} }; + + if (!sort.sort.field) { + sort.sort.field = 'name'; + } + if (!sort.sort.direction) { + sort.sort.direction = EUI_SORT_ASCENDING; + } + + return sort; + }; + + const [query, setQuery] = useState(''); + + const onTableChange = () => { + // we are already updating the state in fetchMoreData. We would need to check in react + // if both methods are needed or we can clean one of them + // For now I just keep it so existing react components don't break + }; + + const getPaginationRouteOptions = useCallback(() => { + if (!pagination || !sorting) { + return {}; + } + + return { + pagination: { + size: pagination.pageSize, + index: pagination.pageIndex, + }, + ...sorting, + queryText: query, + }; + }, [pagination, query, sorting]); + + const getPaginationTableProps = () => { + return { + sorting, + pagination, + onTableChange, + fetchMoreData: ({ + page, + sort, + queryText, + }: { + page: Page; + sort: Sorting; + queryText: string; + }) => { + setPagination({ + ...pagination, + ...{ + initialPageSize: page.size, + pageSize: page.size, + initialPageIndex: page.index, + pageIndex: page.index, + pageSizeOptions: PAGE_SIZE_OPTIONS, + }, + }); + setSorting(cleanSortingData(sort)); + setQuery(queryText); + + setLocalStorageData(storage, { + page, + sort, + }); + }, + }; + }; + + return { + getPaginationRouteOptions, + getPaginationTableProps, + updateTotalItemCount, + }; +} diff --git a/x-pack/plugins/monitoring/public/application/index.tsx b/x-pack/plugins/monitoring/public/application/index.tsx index 8cd5bc3088acc..85dc5286efa42 100644 --- a/x-pack/plugins/monitoring/public/application/index.tsx +++ b/x-pack/plugins/monitoring/public/application/index.tsx @@ -20,7 +20,9 @@ import { createPreserveQueryHistory } from './preserve_query_history'; import { RouteInit } from './route_init'; import { NoDataPage } from './pages/no_data'; import { ElasticsearchOverviewPage } from './pages/elasticsearch/overview'; -import { CODE_PATH_ELASTICSEARCH } from '../../common/constants'; +import { BeatsOverviewPage } from './pages/beats/overview'; +import { CODE_PATH_ELASTICSEARCH, CODE_PATH_BEATS } from '../../common/constants'; +import { ElasticsearchNodesPage } from './pages/elasticsearch/nodes_page'; import { MonitoringTimeContainer } from './hooks/use_monitoring_time'; import { BreadcrumbContainer } from './hooks/use_breadcrumbs'; @@ -77,12 +79,28 @@ const MonitoringApp: React.FC<{ /> {/* ElasticSearch Views */} + + + + {/* Beats Views */} + + = ({ cluster, ...props }) => { + const tabs: TabMenuItem[] = [ + { + id: 'overview', + label: i18n.translate('xpack.monitoring.beatsNavigation.overviewLinkText', { + defaultMessage: 'Overview', + }), + route: '/beats', + }, + { + id: 'instances', + label: i18n.translate('xpack.monitoring.beatsNavigation.instancesLinkText', { + defaultMessage: 'Instances', + }), + route: '/beats/beats', + }, + ]; + + return ; +}; diff --git a/x-pack/plugins/monitoring/public/application/pages/beats/overview.tsx b/x-pack/plugins/monitoring/public/application/pages/beats/overview.tsx new file mode 100644 index 0000000000000..3efad7b82549c --- /dev/null +++ b/x-pack/plugins/monitoring/public/application/pages/beats/overview.tsx @@ -0,0 +1,85 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useContext, useState, useCallback, useEffect } from 'react'; +import { i18n } from '@kbn/i18n'; +import { find } from 'lodash'; +import { ComponentProps } from '../../route_init'; +import { BeatsTemplate } from './beats_template'; +import { GlobalStateContext } from '../../global_state_context'; +import { useCharts } from '../../hooks/use_charts'; +import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; +// @ts-ignore +import { BeatsOverview } from '../../../components/beats/overview'; +import { BreadcrumbContainer } from '../../hooks/use_breadcrumbs'; + +export const BeatsOverviewPage: React.FC = ({ clusters }) => { + const globalState = useContext(GlobalStateContext); + const { zoomInfo, onBrush } = useCharts(); + const { services } = useKibana<{ data: any }>(); + const clusterUuid = globalState.cluster_uuid; + const ccs = globalState.ccs; + const { generate: generateBreadcrumbs } = useContext(BreadcrumbContainer.Context); + const cluster = find(clusters, { + cluster_uuid: clusterUuid, + }) as any; + + const [data, setData] = useState(null); + + const title = i18n.translate('xpack.monitoring.beats.overview.routeTitle', { + defaultMessage: 'Beats - Overview', + }); + + const pageTitle = i18n.translate('xpack.monitoring.beats.overview.pageTitle', { + defaultMessage: 'Beats overview', + }); + + useEffect(() => { + if (cluster) { + generateBreadcrumbs(cluster.cluster_name, { + inBeats: true, + }); + } + }, [cluster, generateBreadcrumbs]); + + const getPageData = useCallback(async () => { + const bounds = services.data?.query.timefilter.timefilter.getBounds(); + const url = `../api/monitoring/v1/clusters/${clusterUuid}/beats`; + + const response = await services.http?.fetch(url, { + method: 'POST', + body: JSON.stringify({ + ccs, + timeRange: { + min: bounds.min.toISOString(), + max: bounds.max.toISOString(), + }, + }), + }); + + setData(response); + }, [ccs, clusterUuid, services.data?.query.timefilter.timefilter, services.http]); + + const renderOverview = (overviewData: any) => { + if (overviewData === null) { + return null; + } + return ; + }; + + return ( + +
    {renderOverview(data)}
    +
    + ); +}; diff --git a/x-pack/plugins/monitoring/public/application/pages/elasticsearch/nodes_page.tsx b/x-pack/plugins/monitoring/public/application/pages/elasticsearch/nodes_page.tsx new file mode 100644 index 0000000000000..652fe83231441 --- /dev/null +++ b/x-pack/plugins/monitoring/public/application/pages/elasticsearch/nodes_page.tsx @@ -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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React, { useContext, useState, useCallback } from 'react'; +import { i18n } from '@kbn/i18n'; +import { find } from 'lodash'; +import { ElasticsearchTemplate } from './elasticsearch_template'; +import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; +import { GlobalStateContext } from '../../global_state_context'; +import { ExternalConfigContext } from '../../external_config_context'; +import { ElasticsearchNodes } from '../../../components/elasticsearch'; +import { ComponentProps } from '../../route_init'; +import { SetupModeRenderer } from '../../setup_mode/setup_mode_renderer'; +import { SetupModeContext } from '../../../components/setup_mode/setup_mode_context'; +import { useTable } from '../../hooks/use_table'; + +interface SetupModeProps { + setupMode: any; + flyoutComponent: any; + bottomBarComponent: any; +} + +export const ElasticsearchNodesPage: React.FC = ({ clusters }) => { + const globalState = useContext(GlobalStateContext); + const { showCgroupMetricsElasticsearch } = useContext(ExternalConfigContext); + const { services } = useKibana<{ data: any }>(); + const { getPaginationRouteOptions, updateTotalItemCount, getPaginationTableProps } = + useTable('elasticsearch.nodes'); + const clusterUuid = globalState.cluster_uuid; + const ccs = globalState.ccs; + const cluster = find(clusters, { + cluster_uuid: clusterUuid, + }); + const [data, setData] = useState({} as any); + + const title = i18n.translate('xpack.monitoring.elasticsearch.nodes.routeTitle', { + defaultMessage: 'Elasticsearch - Nodes', + }); + + const pageTitle = i18n.translate('xpack.monitoring.elasticsearch.nodes.pageTitle', { + defaultMessage: 'Elasticsearch nodes', + }); + + const getPageData = useCallback(async () => { + const bounds = services.data?.query.timefilter.timefilter.getBounds(); + const url = `../api/monitoring/v1/clusters/${clusterUuid}/elasticsearch/nodes`; + const response = await services.http?.fetch(url, { + method: 'POST', + body: JSON.stringify({ + ccs, + timeRange: { + min: bounds.min.toISOString(), + max: bounds.max.toISOString(), + }, + ...getPaginationRouteOptions(), + }), + }); + + setData(response); + updateTotalItemCount(response.totalNodeCount); + }, [ + ccs, + clusterUuid, + services.data?.query.timefilter.timefilter, + services.http, + getPaginationRouteOptions, + updateTotalItemCount, + ]); + + return ( + +
    + ( + + {flyoutComponent} + + {bottomBarComponent} + + )} + /> +
    +
    + ); +}; diff --git a/x-pack/plugins/monitoring/public/components/elasticsearch/index.d.ts b/x-pack/plugins/monitoring/public/components/elasticsearch/index.d.ts index 4460b8432134b..615e79a0bf154 100644 --- a/x-pack/plugins/monitoring/public/components/elasticsearch/index.d.ts +++ b/x-pack/plugins/monitoring/public/components/elasticsearch/index.d.ts @@ -6,3 +6,4 @@ */ export const ElasticsearchOverview: FunctionComponent; +export const ElasticsearchNodes: FunctionComponent; diff --git a/x-pack/plugins/monitoring/public/components/kuery_bar/autocomplete_field.tsx b/x-pack/plugins/monitoring/public/components/kuery_bar/autocomplete_field.tsx new file mode 100644 index 0000000000000..522256ea49b98 --- /dev/null +++ b/x-pack/plugins/monitoring/public/components/kuery_bar/autocomplete_field.tsx @@ -0,0 +1,316 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiFieldSearch, EuiOutsideClickDetector, EuiPanel } from '@elastic/eui'; +import React from 'react'; +import { QuerySuggestion } from '../../../../../../src/plugins/data/public'; +import { euiStyled } from '../../../../../../src/plugins/kibana_react/common'; +import { composeStateUpdaters } from '../../lib/typed_react'; +import { SuggestionItem } from './suggestion_item'; + +interface AutocompleteFieldProps { + isLoadingSuggestions: boolean; + isValid: boolean; + loadSuggestions: (value: string, cursorPosition: number, maxCount?: number) => void; + onSubmit?: (value: string) => void; + onChange?: (value: string) => void; + placeholder?: string; + suggestions: QuerySuggestion[]; + value: string; + disabled?: boolean; + autoFocus?: boolean; + 'aria-label'?: string; +} + +interface AutocompleteFieldState { + areSuggestionsVisible: boolean; + isFocused: boolean; + selectedIndex: number | null; +} + +export class AutocompleteField extends React.Component< + AutocompleteFieldProps, + AutocompleteFieldState +> { + public readonly state: AutocompleteFieldState = { + areSuggestionsVisible: false, + isFocused: false, + selectedIndex: null, + }; + + private inputElement: HTMLInputElement | null = null; + + public render() { + const { + suggestions, + isLoadingSuggestions, + isValid, + placeholder, + value, + disabled, + 'aria-label': ariaLabel, + } = this.props; + const { areSuggestionsVisible, selectedIndex } = this.state; + + return ( + + + + {areSuggestionsVisible && !isLoadingSuggestions && suggestions.length > 0 ? ( + + {suggestions.map((suggestion, suggestionIndex) => ( + + ))} + + ) : null} + + + ); + } + + public componentDidMount() { + if (this.inputElement && this.props.autoFocus) { + this.inputElement.focus(); + } + } + + public componentDidUpdate(prevProps: AutocompleteFieldProps) { + const hasNewValue = prevProps.value !== this.props.value; + const hasNewSuggestions = prevProps.suggestions !== this.props.suggestions; + + if (hasNewValue) { + this.updateSuggestions(); + } + + if (hasNewValue && this.props.value === '') { + this.submit(); + } + + if (hasNewSuggestions && this.state.isFocused) { + this.showSuggestions(); + } + } + + private handleChangeInputRef = (element: HTMLInputElement | null) => { + this.inputElement = element; + }; + + private handleChange = (evt: React.ChangeEvent) => { + this.changeValue(evt.currentTarget.value); + }; + + private handleKeyDown = (evt: React.KeyboardEvent) => { + const { suggestions } = this.props; + + switch (evt.key) { + case 'ArrowUp': + evt.preventDefault(); + if (suggestions.length > 0) { + this.setState( + composeStateUpdaters(withSuggestionsVisible, withPreviousSuggestionSelected) + ); + } + break; + case 'ArrowDown': + evt.preventDefault(); + if (suggestions.length > 0) { + this.setState(composeStateUpdaters(withSuggestionsVisible, withNextSuggestionSelected)); + } else { + this.updateSuggestions(); + } + break; + case 'Enter': + evt.preventDefault(); + if (this.state.selectedIndex !== null) { + this.applySelectedSuggestion(); + } else { + this.submit(); + } + break; + case 'Escape': + evt.preventDefault(); + this.setState(withSuggestionsHidden); + break; + } + }; + + private handleKeyUp = (evt: React.KeyboardEvent) => { + switch (evt.key) { + case 'ArrowLeft': + case 'ArrowRight': + case 'Home': + case 'End': + this.updateSuggestions(); + break; + } + }; + + private handleFocus = () => { + this.setState(composeStateUpdaters(withSuggestionsVisible, withFocused)); + }; + + private handleBlur = () => { + this.setState(composeStateUpdaters(withSuggestionsHidden, withUnfocused)); + }; + + private selectSuggestionAt = (index: number) => () => { + this.setState(withSuggestionAtIndexSelected(index)); + }; + + private applySelectedSuggestion = () => { + if (this.state.selectedIndex !== null) { + this.applySuggestionAt(this.state.selectedIndex)(); + } + }; + + private applySuggestionAt = (index: number) => () => { + const { value, suggestions } = this.props; + const selectedSuggestion = suggestions[index]; + + if (!selectedSuggestion) { + return; + } + + const newValue = + value.substr(0, selectedSuggestion.start) + + selectedSuggestion.text + + value.substr(selectedSuggestion.end); + + this.setState(withSuggestionsHidden); + this.changeValue(newValue); + this.focusInputElement(); + }; + + private changeValue = (value: string) => { + const { onChange } = this.props; + + if (onChange) { + onChange(value); + } + }; + + private focusInputElement = () => { + if (this.inputElement) { + this.inputElement.focus(); + } + }; + + private showSuggestions = () => { + this.setState(withSuggestionsVisible); + }; + + private submit = () => { + const { isValid, onSubmit, value } = this.props; + + if (isValid && onSubmit) { + onSubmit(value); + } + + this.setState(withSuggestionsHidden); + }; + + private updateSuggestions = () => { + const inputCursorPosition = this.inputElement ? this.inputElement.selectionStart || 0 : 0; + this.props.loadSuggestions(this.props.value, inputCursorPosition, 200); + }; +} + +const withPreviousSuggestionSelected = ( + state: AutocompleteFieldState, + props: AutocompleteFieldProps +): AutocompleteFieldState => ({ + ...state, + selectedIndex: + props.suggestions.length === 0 + ? null + : state.selectedIndex !== null + ? (state.selectedIndex + props.suggestions.length - 1) % props.suggestions.length + : Math.max(props.suggestions.length - 1, 0), +}); + +const withNextSuggestionSelected = ( + state: AutocompleteFieldState, + props: AutocompleteFieldProps +): AutocompleteFieldState => ({ + ...state, + selectedIndex: + props.suggestions.length === 0 + ? null + : state.selectedIndex !== null + ? (state.selectedIndex + 1) % props.suggestions.length + : 0, +}); + +const withSuggestionAtIndexSelected = + (suggestionIndex: number) => + (state: AutocompleteFieldState, props: AutocompleteFieldProps): AutocompleteFieldState => ({ + ...state, + selectedIndex: + props.suggestions.length === 0 + ? null + : suggestionIndex >= 0 && suggestionIndex < props.suggestions.length + ? suggestionIndex + : 0, + }); + +const withSuggestionsVisible = (state: AutocompleteFieldState) => ({ + ...state, + areSuggestionsVisible: true, +}); + +const withSuggestionsHidden = (state: AutocompleteFieldState) => ({ + ...state, + areSuggestionsVisible: false, + selectedIndex: null, +}); + +const withFocused = (state: AutocompleteFieldState) => ({ + ...state, + isFocused: true, +}); + +const withUnfocused = (state: AutocompleteFieldState) => ({ + ...state, + isFocused: false, +}); + +const AutocompleteContainer = euiStyled.div` + position: relative; +`; + +const SuggestionsPanel = euiStyled(EuiPanel).attrs(() => ({ + paddingSize: 'none', + hasShadow: true, +}))` + position: absolute; + width: 100%; + margin-top: 2px; + overflow-x: hidden; + overflow-y: scroll; + z-index: ${(props) => props.theme.eui.euiZLevel1}; + max-height: 322px; +`; diff --git a/x-pack/plugins/monitoring/public/components/kuery_bar/index.tsx b/x-pack/plugins/monitoring/public/components/kuery_bar/index.tsx new file mode 100644 index 0000000000000..ca0a8122772f3 --- /dev/null +++ b/x-pack/plugins/monitoring/public/components/kuery_bar/index.tsx @@ -0,0 +1,98 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; + +import React, { useEffect, useState } from 'react'; +import { WithKueryAutocompletion } from './with_kuery_autocompletion'; +import { AutocompleteField } from './autocomplete_field'; +import { esKuery, IIndexPattern, QuerySuggestion } from '../../../../../../src/plugins/data/public'; + +type LoadSuggestionsFn = ( + e: string, + p: number, + m?: number, + transform?: (s: QuerySuggestion[]) => QuerySuggestion[] +) => void; +export type CurryLoadSuggestionsType = (loadSuggestions: LoadSuggestionsFn) => LoadSuggestionsFn; + +interface Props { + derivedIndexPattern: IIndexPattern; + onSubmit: (query: string) => void; + onChange?: (query: string) => void; + value?: string | null; + placeholder?: string; + curryLoadSuggestions?: CurryLoadSuggestionsType; +} + +function validateQuery(query: string) { + try { + esKuery.fromKueryExpression(query); + } catch (err) { + return false; + } + return true; +} + +export const KueryBar = ({ + derivedIndexPattern, + onSubmit, + onChange, + value, + placeholder, + curryLoadSuggestions = defaultCurryLoadSuggestions, +}: Props) => { + const [draftQuery, setDraftQuery] = useState(value || ''); + const [isValid, setValidation] = useState(true); + + // This ensures that if value changes out side this component it will update. + useEffect(() => { + if (value) { + setDraftQuery(value); + } + }, [value]); + + const handleChange = (query: string) => { + setValidation(validateQuery(query)); + setDraftQuery(query); + if (onChange) { + onChange(query); + } + }; + + const filteredDerivedIndexPattern = { + ...derivedIndexPattern, + fields: derivedIndexPattern.fields, + }; + + const defaultPlaceholder = i18n.translate('xpack.monitoring.alerts.kqlSearchFieldPlaceholder', { + defaultMessage: 'Search for monitoring data', + }); + + return ( + + {({ isLoadingSuggestions, loadSuggestions, suggestions }) => ( + + )} + + ); +}; + +const defaultCurryLoadSuggestions: CurryLoadSuggestionsType = + (loadSuggestions) => + (...args) => + loadSuggestions(...args); diff --git a/x-pack/plugins/monitoring/public/components/kuery_bar/suggestion_item.tsx b/x-pack/plugins/monitoring/public/components/kuery_bar/suggestion_item.tsx new file mode 100644 index 0000000000000..3681bf26987cc --- /dev/null +++ b/x-pack/plugins/monitoring/public/components/kuery_bar/suggestion_item.tsx @@ -0,0 +1,119 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiIcon } from '@elastic/eui'; +import { transparentize } from 'polished'; +import React from 'react'; +import { euiStyled } from '../../../../../../src/plugins/kibana_react/common'; +import { QuerySuggestion, QuerySuggestionTypes } from '../../../../../../src/plugins/data/public'; + +interface Props { + isSelected?: boolean; + onClick?: React.MouseEventHandler; + onMouseEnter?: React.MouseEventHandler; + suggestion: QuerySuggestion; +} + +export const SuggestionItem: React.FC = (props) => { + const { isSelected, onClick, onMouseEnter, suggestion } = props; + + return ( + + + + + {suggestion.text} + {suggestion.description} + + ); +}; + +SuggestionItem.defaultProps = { + isSelected: false, +}; + +const SuggestionItemContainer = euiStyled.div<{ + isSelected?: boolean; +}>` + display: flex; + flex-direction: row; + font-size: ${(props) => props.theme.eui.euiFontSizeS}; + height: ${(props) => props.theme.eui.euiSizeXL}; + white-space: nowrap; + background-color: ${(props) => + props.isSelected ? props.theme.eui.euiColorLightestShade : 'transparent'}; +`; + +const SuggestionItemField = euiStyled.div` + align-items: center; + cursor: pointer; + display: flex; + flex-direction: row; + height: ${(props) => props.theme.eui.euiSizeXL}; + padding: ${(props) => props.theme.eui.euiSizeXS}; +`; + +const SuggestionItemIconField = euiStyled(SuggestionItemField)<{ + suggestionType: QuerySuggestionTypes; +}>` + background-color: ${(props) => + transparentize(0.9, getEuiIconColor(props.theme, props.suggestionType))}; + color: ${(props) => getEuiIconColor(props.theme, props.suggestionType)}; + flex: 0 0 auto; + justify-content: center; + width: ${(props) => props.theme.eui.euiSizeXL}; +`; + +const SuggestionItemTextField = euiStyled(SuggestionItemField)` + flex: 2 0 0; + font-family: ${(props) => props.theme.eui.euiCodeFontFamily}; +`; + +const SuggestionItemDescriptionField = euiStyled(SuggestionItemField)` + flex: 3 0 0; + + p { + display: inline; + + span { + font-family: ${(props) => props.theme.eui.euiCodeFontFamily}; + } + } +`; + +const getEuiIconType = (suggestionType: QuerySuggestionTypes) => { + switch (suggestionType) { + case QuerySuggestionTypes.Field: + return 'kqlField'; + case QuerySuggestionTypes.Value: + return 'kqlValue'; + case QuerySuggestionTypes.RecentSearch: + return 'search'; + case QuerySuggestionTypes.Conjunction: + return 'kqlSelector'; + case QuerySuggestionTypes.Operator: + return 'kqlOperand'; + default: + return 'empty'; + } +}; + +const getEuiIconColor = (theme: any, suggestionType: QuerySuggestionTypes): string => { + switch (suggestionType) { + case QuerySuggestionTypes.Field: + return theme?.eui.euiColorVis7; + case QuerySuggestionTypes.Value: + return theme?.eui.euiColorVis0; + case QuerySuggestionTypes.Operator: + return theme?.eui.euiColorVis1; + case QuerySuggestionTypes.Conjunction: + return theme?.eui.euiColorVis2; + case QuerySuggestionTypes.RecentSearch: + default: + return theme?.eui.euiColorMediumShade; + } +}; diff --git a/x-pack/plugins/monitoring/public/components/kuery_bar/with_kuery_autocompletion.tsx b/x-pack/plugins/monitoring/public/components/kuery_bar/with_kuery_autocompletion.tsx new file mode 100644 index 0000000000000..8d79bf4039846 --- /dev/null +++ b/x-pack/plugins/monitoring/public/components/kuery_bar/with_kuery_autocompletion.tsx @@ -0,0 +1,111 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { QuerySuggestion, IIndexPattern, DataPublicPluginStart } from 'src/plugins/data/public'; +import { + withKibana, + KibanaReactContextValue, + KibanaServices, +} from '../../../../../../src/plugins/kibana_react/public'; +import { RendererFunction } from '../../lib/typed_react'; + +interface WithKueryAutocompletionLifecycleProps { + kibana: KibanaReactContextValue<{ data: DataPublicPluginStart } & KibanaServices>; + children: RendererFunction<{ + isLoadingSuggestions: boolean; + loadSuggestions: (expression: string, cursorPosition: number, maxSuggestions?: number) => void; + suggestions: QuerySuggestion[]; + }>; + indexPattern: IIndexPattern; +} + +interface WithKueryAutocompletionLifecycleState { + // lacking cancellation support in the autocompletion api, + // this is used to keep older, slower requests from clobbering newer ones + currentRequest: { + expression: string; + cursorPosition: number; + } | null; + suggestions: QuerySuggestion[]; +} + +class WithKueryAutocompletionComponent extends React.Component< + WithKueryAutocompletionLifecycleProps, + WithKueryAutocompletionLifecycleState +> { + public readonly state: WithKueryAutocompletionLifecycleState = { + currentRequest: null, + suggestions: [], + }; + + public render() { + const { currentRequest, suggestions } = this.state; + + return this.props.children({ + isLoadingSuggestions: currentRequest !== null, + loadSuggestions: this.loadSuggestions, + suggestions, + }); + } + + private loadSuggestions = async ( + expression: string, + cursorPosition: number, + maxSuggestions?: number, + transformSuggestions?: (s: QuerySuggestion[]) => QuerySuggestion[] + ) => { + const { indexPattern } = this.props; + const language = 'kuery'; + const hasQuerySuggestions = + this.props.kibana.services.data?.autocomplete.hasQuerySuggestions(language); + + if (!hasQuerySuggestions) { + return; + } + + this.setState({ + currentRequest: { + expression, + cursorPosition, + }, + suggestions: [], + }); + + const suggestions = + (await this.props.kibana.services.data.autocomplete.getQuerySuggestions({ + language, + query: expression, + selectionStart: cursorPosition, + selectionEnd: cursorPosition, + indexPatterns: [indexPattern], + boolFilter: [], + })) || []; + + const transformedSuggestions = transformSuggestions + ? transformSuggestions(suggestions) + : suggestions; + + this.setState((state) => + state.currentRequest && + state.currentRequest.expression !== expression && + state.currentRequest.cursorPosition !== cursorPosition + ? state // ignore this result, since a newer request is in flight + : { + ...state, + currentRequest: null, + suggestions: maxSuggestions + ? transformedSuggestions.slice(0, maxSuggestions) + : transformedSuggestions, + } + ); + }; +} + +export const WithKueryAutocompletion = withKibana( + WithKueryAutocompletionComponent +); diff --git a/x-pack/plugins/monitoring/public/components/table/index.d.ts b/x-pack/plugins/monitoring/public/components/table/index.d.ts new file mode 100644 index 0000000000000..6b54b3d97e5f1 --- /dev/null +++ b/x-pack/plugins/monitoring/public/components/table/index.d.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export const euiTableStorageGetter: (string) => any; +export const euiTableStorageSetter: (string) => any; diff --git a/x-pack/plugins/monitoring/public/lib/kuery.ts b/x-pack/plugins/monitoring/public/lib/kuery.ts new file mode 100644 index 0000000000000..19706d7664c22 --- /dev/null +++ b/x-pack/plugins/monitoring/public/lib/kuery.ts @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { esKuery, IIndexPattern } from '../../../../../src/plugins/data/public'; + +export const convertKueryToElasticSearchQuery = ( + kueryExpression: string, + indexPattern: IIndexPattern +) => { + try { + return kueryExpression + ? JSON.stringify( + esKuery.toElasticsearchQuery(esKuery.fromKueryExpression(kueryExpression), indexPattern) + ) + : ''; + } catch (err) { + return ''; + } +}; diff --git a/x-pack/plugins/monitoring/public/lib/typed_react.tsx b/x-pack/plugins/monitoring/public/lib/typed_react.tsx new file mode 100644 index 0000000000000..b5b7a440c117c --- /dev/null +++ b/x-pack/plugins/monitoring/public/lib/typed_react.tsx @@ -0,0 +1,82 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { omit } from 'lodash'; +import React from 'react'; +import { InferableComponentEnhancerWithProps, ConnectedComponent } from 'react-redux'; + +export type RendererResult = React.ReactElement | null; +export type RendererFunction = (args: RenderArgs) => Result; + +export type ChildFunctionRendererProps = { + children: RendererFunction; + initializeOnMount?: boolean; + resetOnUnmount?: boolean; +} & RenderArgs; + +interface ChildFunctionRendererOptions { + onInitialize?: (props: RenderArgs) => void; + onCleanup?: (props: RenderArgs) => void; +} + +export const asChildFunctionRenderer = ( + hoc: InferableComponentEnhancerWithProps, + { onInitialize, onCleanup }: ChildFunctionRendererOptions = {} +): ConnectedComponent< + React.ComponentClass<{}>, + { + children: RendererFunction; + initializeOnMount?: boolean; + resetOnUnmount?: boolean; + } & OwnProps +> => + hoc( + class ChildFunctionRenderer extends React.Component> { + public displayName = 'ChildFunctionRenderer'; + + public componentDidMount() { + if (this.props.initializeOnMount && onInitialize) { + onInitialize(this.getRendererArgs()); + } + } + + public componentWillUnmount() { + if (this.props.resetOnUnmount && onCleanup) { + onCleanup(this.getRendererArgs()); + } + } + + public render() { + return (this.props.children as ChildFunctionRendererProps['children'])( + this.getRendererArgs() + ); + } + + private getRendererArgs = () => + omit(this.props, ['children', 'initializeOnMount', 'resetOnUnmount']) as Pick< + ChildFunctionRendererProps, + keyof InjectedProps + >; + } as any + ); + +export type StateUpdater = ( + prevState: Readonly, + prevProps: Readonly +) => State | null; + +export type PropsOfContainer = Container extends InferableComponentEnhancerWithProps< + infer InjectedProps, + any +> + ? InjectedProps + : never; + +export function composeStateUpdaters(...updaters: Array>) { + return (state: State, props: Props) => + updaters.reduce((currentState, updater) => updater(currentState, props) || currentState, state); +} diff --git a/x-pack/plugins/monitoring/public/plugin.ts b/x-pack/plugins/monitoring/public/plugin.ts index ad71cdbeb106c..aee5072947531 100644 --- a/x-pack/plugins/monitoring/public/plugin.ts +++ b/x-pack/plugins/monitoring/public/plugin.ts @@ -28,14 +28,6 @@ import { RULE_THREAD_POOL_WRITE_REJECTIONS, RULE_DETAILS, } from '../common/constants'; -import { createCpuUsageAlertType } from './alerts/cpu_usage_alert'; -import { createMissingMonitoringDataAlertType } from './alerts/missing_monitoring_data_alert'; -import { createLegacyAlertTypes } from './alerts/legacy_alert'; -import { createDiskUsageAlertType } from './alerts/disk_usage_alert'; -import { createThreadPoolRejectionsAlertType } from './alerts/thread_pool_rejections_alert'; -import { createMemoryUsageAlertType } from './alerts/memory_usage_alert'; -import { createCCRReadExceptionsAlertType } from './alerts/ccr_read_exceptions_alert'; -import { createLargeShardSizeAlertType } from './alerts/large_shard_size_alert'; import { setConfig } from './external_config'; interface MonitoringSetupPluginDependencies { @@ -49,11 +41,11 @@ const HASH_CHANGE = 'hashchange'; export class MonitoringPlugin implements - Plugin + Plugin { constructor(private initializerContext: PluginInitializerContext) {} - public setup( + public async setup( core: CoreSetup, plugins: MonitoringSetupPluginDependencies ) { @@ -86,7 +78,7 @@ export class MonitoringPlugin }); } - this.registerAlerts(plugins); + await this.registerAlerts(plugins, monitoring); const app: App = { id, @@ -152,7 +144,6 @@ export class MonitoringPlugin }; core.application.register(app); - return true; } public start(core: CoreStart, plugins: any) {} @@ -192,29 +183,48 @@ export class MonitoringPlugin ]; } - private registerAlerts(plugins: MonitoringSetupPluginDependencies) { + private async registerAlerts( + plugins: MonitoringSetupPluginDependencies, + config: MonitoringConfig + ) { const { triggersActionsUi: { ruleTypeRegistry }, } = plugins; - ruleTypeRegistry.register(createCpuUsageAlertType()); - ruleTypeRegistry.register(createDiskUsageAlertType()); - ruleTypeRegistry.register(createMemoryUsageAlertType()); + + const { createCpuUsageAlertType } = await import('./alerts/cpu_usage_alert'); + const { createMissingMonitoringDataAlertType } = await import( + './alerts/missing_monitoring_data_alert' + ); + const { createLegacyAlertTypes } = await import('./alerts/legacy_alert'); + const { createDiskUsageAlertType } = await import('./alerts/disk_usage_alert'); + const { createThreadPoolRejectionsAlertType } = await import( + './alerts/thread_pool_rejections_alert' + ); + const { createMemoryUsageAlertType } = await import('./alerts/memory_usage_alert'); + const { createCCRReadExceptionsAlertType } = await import('./alerts/ccr_read_exceptions_alert'); + const { createLargeShardSizeAlertType } = await import('./alerts/large_shard_size_alert'); + + ruleTypeRegistry.register(createCpuUsageAlertType(config)); + ruleTypeRegistry.register(createDiskUsageAlertType(config)); + ruleTypeRegistry.register(createMemoryUsageAlertType(config)); ruleTypeRegistry.register(createMissingMonitoringDataAlertType()); ruleTypeRegistry.register( createThreadPoolRejectionsAlertType( RULE_THREAD_POOL_SEARCH_REJECTIONS, - RULE_DETAILS[RULE_THREAD_POOL_SEARCH_REJECTIONS] + RULE_DETAILS[RULE_THREAD_POOL_SEARCH_REJECTIONS], + config ) ); ruleTypeRegistry.register( createThreadPoolRejectionsAlertType( RULE_THREAD_POOL_WRITE_REJECTIONS, - RULE_DETAILS[RULE_THREAD_POOL_WRITE_REJECTIONS] + RULE_DETAILS[RULE_THREAD_POOL_WRITE_REJECTIONS], + config ) ); - ruleTypeRegistry.register(createCCRReadExceptionsAlertType()); - ruleTypeRegistry.register(createLargeShardSizeAlertType()); - const legacyAlertTypes = createLegacyAlertTypes(); + ruleTypeRegistry.register(createCCRReadExceptionsAlertType(config)); + ruleTypeRegistry.register(createLargeShardSizeAlertType(config)); + const legacyAlertTypes = createLegacyAlertTypes(config); for (const legacyAlertType of legacyAlertTypes) { ruleTypeRegistry.register(legacyAlertType); } diff --git a/x-pack/plugins/monitoring/server/alerts/ccr_read_exceptions_rule.ts b/x-pack/plugins/monitoring/server/alerts/ccr_read_exceptions_rule.ts index 3a50aca7d4b84..e3a3537ea2eaf 100644 --- a/x-pack/plugins/monitoring/server/alerts/ccr_read_exceptions_rule.ts +++ b/x-pack/plugins/monitoring/server/alerts/ccr_read_exceptions_rule.ts @@ -88,7 +88,8 @@ export class CCRReadExceptionsRule extends BaseRule { esIndexPattern, startMs, endMs, - Globals.app.config.ui.max_bucket_size + Globals.app.config.ui.max_bucket_size, + params.filterQuery ); return stats.map((stat) => { @@ -278,7 +279,7 @@ export class CCRReadExceptionsRule extends BaseRule { state: AlertingDefaults.ALERT_STATE.firing, remoteCluster, followerIndex, - /* continue to send "remoteClusters" and "followerIndices" values for users still using it though + /* continue to send "remoteClusters" and "followerIndices" values for users still using it though we have replaced it with "remoteCluster" and "followerIndex" in the template due to alerts per index instead of all indices see https://github.com/elastic/kibana/issues/100136#issuecomment-865229431 */ diff --git a/x-pack/plugins/monitoring/server/alerts/cluster_health_rule.ts b/x-pack/plugins/monitoring/server/alerts/cluster_health_rule.ts index 7fac3b74a1b66..b9b9b90845eea 100644 --- a/x-pack/plugins/monitoring/server/alerts/cluster_health_rule.ts +++ b/x-pack/plugins/monitoring/server/alerts/cluster_health_rule.ts @@ -73,7 +73,12 @@ export class ClusterHealthRule extends BaseRule { if (availableCcs) { esIndexPattern = getCcsIndexPattern(esIndexPattern, availableCcs); } - const healths = await fetchClusterHealth(esClient, clusters, esIndexPattern); + const healths = await fetchClusterHealth( + esClient, + clusters, + esIndexPattern, + params.filterQuery + ); return healths.map((clusterHealth) => { const shouldFire = clusterHealth.health !== AlertClusterHealthType.Green; const severity = diff --git a/x-pack/plugins/monitoring/server/alerts/cpu_usage_rule.ts b/x-pack/plugins/monitoring/server/alerts/cpu_usage_rule.ts index 2e57a3c22de1b..7e38efcb819ea 100644 --- a/x-pack/plugins/monitoring/server/alerts/cpu_usage_rule.ts +++ b/x-pack/plugins/monitoring/server/alerts/cpu_usage_rule.ts @@ -76,7 +76,8 @@ export class CpuUsageRule extends BaseRule { esIndexPattern, startMs, endMs, - Globals.app.config.ui.max_bucket_size + Globals.app.config.ui.max_bucket_size, + params.filterQuery ); return stats.map((stat) => { if (Globals.app.config.ui.container.elasticsearch.enabled) { @@ -203,7 +204,7 @@ export class CpuUsageRule extends BaseRule { internalShortMessage, internalFullMessage: Globals.app.isCloud ? internalShortMessage : internalFullMessage, state: AlertingDefaults.ALERT_STATE.firing, - /* continue to send "nodes" and "count" values for users before https://github.com/elastic/kibana/pull/102544 + /* continue to send "nodes" and "count" values for users before https://github.com/elastic/kibana/pull/102544 see https://github.com/elastic/kibana/issues/100136#issuecomment-865229431 */ nodes: `${firingNode.nodeName}:${firingNode.cpuUsage}`, diff --git a/x-pack/plugins/monitoring/server/alerts/disk_usage_rule.ts b/x-pack/plugins/monitoring/server/alerts/disk_usage_rule.ts index ae3025c1db92c..bac70baebb4e2 100644 --- a/x-pack/plugins/monitoring/server/alerts/disk_usage_rule.ts +++ b/x-pack/plugins/monitoring/server/alerts/disk_usage_rule.ts @@ -72,7 +72,8 @@ export class DiskUsageRule extends BaseRule { clusters, esIndexPattern, duration as string, - Globals.app.config.ui.max_bucket_size + Globals.app.config.ui.max_bucket_size, + params.filterQuery ); return stats.map((stat) => { @@ -212,7 +213,7 @@ export class DiskUsageRule extends BaseRule { internalShortMessage, internalFullMessage: Globals.app.isCloud ? internalShortMessage : internalFullMessage, state: AlertingDefaults.ALERT_STATE.firing, - /* continue to send "nodes" and "count" values for users before https://github.com/elastic/kibana/pull/102544 + /* continue to send "nodes" and "count" values for users before https://github.com/elastic/kibana/pull/102544 see https://github.com/elastic/kibana/issues/100136#issuecomment-865229431 */ nodes: `${firingNode.nodeName}:${firingNode.diskUsage}`, diff --git a/x-pack/plugins/monitoring/server/alerts/elasticsearch_version_mismatch_rule.ts b/x-pack/plugins/monitoring/server/alerts/elasticsearch_version_mismatch_rule.ts index 6a5abcb4975f4..352cac531f8e8 100644 --- a/x-pack/plugins/monitoring/server/alerts/elasticsearch_version_mismatch_rule.ts +++ b/x-pack/plugins/monitoring/server/alerts/elasticsearch_version_mismatch_rule.ts @@ -66,7 +66,8 @@ export class ElasticsearchVersionMismatchRule extends BaseRule { esClient, clusters, esIndexPattern, - Globals.app.config.ui.max_bucket_size + Globals.app.config.ui.max_bucket_size, + params.filterQuery ); return elasticsearchVersions.map((elasticsearchVersion) => { diff --git a/x-pack/plugins/monitoring/server/alerts/kibana_version_mismatch_rule.ts b/x-pack/plugins/monitoring/server/alerts/kibana_version_mismatch_rule.ts index 90275ea4d23a8..6d9410ed0e5a0 100644 --- a/x-pack/plugins/monitoring/server/alerts/kibana_version_mismatch_rule.ts +++ b/x-pack/plugins/monitoring/server/alerts/kibana_version_mismatch_rule.ts @@ -79,7 +79,8 @@ export class KibanaVersionMismatchRule extends BaseRule { esClient, clusters, kibanaIndexPattern, - Globals.app.config.ui.max_bucket_size + Globals.app.config.ui.max_bucket_size, + params.filterQuery ); return kibanaVersions.map((kibanaVersion) => { diff --git a/x-pack/plugins/monitoring/server/alerts/large_shard_size_rule.ts b/x-pack/plugins/monitoring/server/alerts/large_shard_size_rule.ts index 86f96daa3b21d..b0370a23159d7 100644 --- a/x-pack/plugins/monitoring/server/alerts/large_shard_size_rule.ts +++ b/x-pack/plugins/monitoring/server/alerts/large_shard_size_rule.ts @@ -75,7 +75,8 @@ export class LargeShardSizeRule extends BaseRule { esIndexPattern, threshold!, shardIndexPatterns, - Globals.app.config.ui.max_bucket_size + Globals.app.config.ui.max_bucket_size, + params.filterQuery ); return stats.map((stat) => { @@ -211,7 +212,7 @@ export class LargeShardSizeRule extends BaseRule { internalShortMessage, internalFullMessage, state: AlertingDefaults.ALERT_STATE.firing, - /* continue to send "shardIndices" values for users still using it though + /* continue to send "shardIndices" values for users still using it though we have replaced it with shardIndex in the template due to alerts per index instead of all indices see https://github.com/elastic/kibana/issues/100136#issuecomment-865229431 */ diff --git a/x-pack/plugins/monitoring/server/alerts/license_expiration_rule.ts b/x-pack/plugins/monitoring/server/alerts/license_expiration_rule.ts index 67ea8bd57b491..c26929b05ab26 100644 --- a/x-pack/plugins/monitoring/server/alerts/license_expiration_rule.ts +++ b/x-pack/plugins/monitoring/server/alerts/license_expiration_rule.ts @@ -87,7 +87,7 @@ export class LicenseExpirationRule extends BaseRule { if (availableCcs) { esIndexPattern = getCcsIndexPattern(esIndexPattern, availableCcs); } - const licenses = await fetchLicenses(esClient, clusters, esIndexPattern); + const licenses = await fetchLicenses(esClient, clusters, esIndexPattern, params.filterQuery); return licenses.map((license) => { const { clusterUuid, type, expiryDateMS, status, ccs } = license; diff --git a/x-pack/plugins/monitoring/server/alerts/logstash_version_mismatch_rule.ts b/x-pack/plugins/monitoring/server/alerts/logstash_version_mismatch_rule.ts index 0f9ad4dd4b117..e59ed9efbefb2 100644 --- a/x-pack/plugins/monitoring/server/alerts/logstash_version_mismatch_rule.ts +++ b/x-pack/plugins/monitoring/server/alerts/logstash_version_mismatch_rule.ts @@ -66,7 +66,8 @@ export class LogstashVersionMismatchRule extends BaseRule { esClient, clusters, logstashIndexPattern, - Globals.app.config.ui.max_bucket_size + Globals.app.config.ui.max_bucket_size, + params.filterQuery ); return logstashVersions.map((logstashVersion) => { diff --git a/x-pack/plugins/monitoring/server/alerts/memory_usage_rule.ts b/x-pack/plugins/monitoring/server/alerts/memory_usage_rule.ts index 384610e659d47..d94e1234ce813 100644 --- a/x-pack/plugins/monitoring/server/alerts/memory_usage_rule.ts +++ b/x-pack/plugins/monitoring/server/alerts/memory_usage_rule.ts @@ -82,7 +82,8 @@ export class MemoryUsageRule extends BaseRule { esIndexPattern, startMs, endMs, - Globals.app.config.ui.max_bucket_size + Globals.app.config.ui.max_bucket_size, + params.filterQuery ); return stats.map((stat) => { @@ -223,7 +224,7 @@ export class MemoryUsageRule extends BaseRule { internalShortMessage, internalFullMessage: Globals.app.isCloud ? internalShortMessage : internalFullMessage, state: AlertingDefaults.ALERT_STATE.firing, - /* continue to send "nodes" and "count" values for users before https://github.com/elastic/kibana/pull/102544 + /* continue to send "nodes" and "count" values for users before https://github.com/elastic/kibana/pull/102544 see https://github.com/elastic/kibana/issues/100136#issuecomment-865229431 */ nodes: `${firingNode.nodeName}:${firingNode.memoryUsage.toFixed(2)}`, diff --git a/x-pack/plugins/monitoring/server/alerts/missing_monitoring_data_rule.ts b/x-pack/plugins/monitoring/server/alerts/missing_monitoring_data_rule.ts index 32e4ff738c71b..1b45b19fe89f8 100644 --- a/x-pack/plugins/monitoring/server/alerts/missing_monitoring_data_rule.ts +++ b/x-pack/plugins/monitoring/server/alerts/missing_monitoring_data_rule.ts @@ -75,7 +75,8 @@ export class MissingMonitoringDataRule extends BaseRule { indexPattern, Globals.app.config.ui.max_bucket_size, now, - now - limit - LIMIT_BUFFER + now - limit - LIMIT_BUFFER, + params.filterQuery ); return missingData.map((missing) => { return { @@ -198,7 +199,7 @@ export class MissingMonitoringDataRule extends BaseRule { internalShortMessage, internalFullMessage: Globals.app.isCloud ? internalShortMessage : internalFullMessage, state: AlertingDefaults.ALERT_STATE.firing, - /* continue to send "nodes" and "count" values for users before https://github.com/elastic/kibana/pull/102544 + /* continue to send "nodes" and "count" values for users before https://github.com/elastic/kibana/pull/102544 see https://github.com/elastic/kibana/issues/100136#issuecomment-865229431 */ nodes: `node: ${firingNode.nodeName}`, diff --git a/x-pack/plugins/monitoring/server/alerts/nodes_changed_rule.ts b/x-pack/plugins/monitoring/server/alerts/nodes_changed_rule.ts index 90bd70f32c8cb..6645466f30c73 100644 --- a/x-pack/plugins/monitoring/server/alerts/nodes_changed_rule.ts +++ b/x-pack/plugins/monitoring/server/alerts/nodes_changed_rule.ts @@ -114,7 +114,8 @@ export class NodesChangedRule extends BaseRule { const nodesFromClusterStats = await fetchNodesFromClusterStats( esClient, clusters, - esIndexPattern + esIndexPattern, + params.filterQuery ); return nodesFromClusterStats.map((nodes) => { const { removed, added, restarted } = getNodeStates(nodes); diff --git a/x-pack/plugins/monitoring/server/alerts/thread_pool_rejections_rule_base.ts b/x-pack/plugins/monitoring/server/alerts/thread_pool_rejections_rule_base.ts index c478b2f687c02..678f8b429167f 100644 --- a/x-pack/plugins/monitoring/server/alerts/thread_pool_rejections_rule_base.ts +++ b/x-pack/plugins/monitoring/server/alerts/thread_pool_rejections_rule_base.ts @@ -86,7 +86,8 @@ export class ThreadPoolRejectionsRuleBase extends BaseRule { esIndexPattern, Globals.app.config.ui.max_bucket_size, this.threadPoolType, - duration + duration, + params.filterQuery ); return stats.map((stat) => { @@ -257,7 +258,7 @@ export class ThreadPoolRejectionsRuleBase extends BaseRule { internalFullMessage: Globals.app.isCloud ? internalShortMessage : internalFullMessage, threadPoolType: type, state: AlertingDefaults.ALERT_STATE.firing, - /* continue to send "count" value for users before https://github.com/elastic/kibana/pull/102544 + /* continue to send "count" value for users before https://github.com/elastic/kibana/pull/102544 see https://github.com/elastic/kibana/issues/100136#issuecomment-865229431 */ count: 1, diff --git a/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/lib/fetch_stack_product_usage.ts b/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/lib/fetch_stack_product_usage.ts index 527ed503c8faf..0d3aab8283688 100644 --- a/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/lib/fetch_stack_product_usage.ts +++ b/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/lib/fetch_stack_product_usage.ts @@ -10,7 +10,7 @@ import { ElasticsearchClient } from 'src/core/server'; import { estypes } from '@elastic/elasticsearch'; import { MonitoringConfig } from '../../../config'; // @ts-ignore -import { prefixIndexPattern } from '../../../lib/ccs_utils'; +import { prefixIndexPattern } from '../../../../common/ccs_utils'; import { StackProductUsage } from '../types'; interface ESResponse { diff --git a/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/lib/get_stack_products_usage.ts b/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/lib/get_stack_products_usage.ts index 7cce1b392112f..25a1892a9f38d 100644 --- a/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/lib/get_stack_products_usage.ts +++ b/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/lib/get_stack_products_usage.ts @@ -12,7 +12,7 @@ import { MonitoringConfig } from '../../../config'; // @ts-ignore import { getIndexPatterns } from '../../../lib/cluster/get_index_patterns'; // @ts-ignore -import { prefixIndexPattern } from '../../../lib/ccs_utils'; +import { prefixIndexPattern } from '../../../../common/ccs_utils'; import { INDEX_PATTERN_ELASTICSEARCH, INDEX_PATTERN_KIBANA, diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_ccr_read_exceptions.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_ccr_read_exceptions.ts index 560751d1297d5..e7a5923207d60 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_ccr_read_exceptions.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_ccr_read_exceptions.ts @@ -14,7 +14,8 @@ export async function fetchCCRReadExceptions( index: string, startMs: number, endMs: number, - size: number + size: number, + filterQuery?: string ): Promise { const params = { index, @@ -93,6 +94,15 @@ export async function fetchCCRReadExceptions( }, }; + try { + if (filterQuery) { + const filterQueryObject = JSON.parse(filterQuery); + params.body.query.bool.filter.push(filterQueryObject); + } + } catch (e) { + // meh + } + const { body: response } = await esClient.search(params); const stats: CCRReadExceptionsStats[] = []; // @ts-expect-error declare aggegations type explicitly diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_cluster_health.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_cluster_health.ts index 85bfbd9dbd049..b2004f0c7c710 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_cluster_health.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_cluster_health.ts @@ -11,7 +11,8 @@ import { ElasticsearchSource, ElasticsearchResponse } from '../../../common/type export async function fetchClusterHealth( esClient: ElasticsearchClient, clusters: AlertCluster[], - index: string + index: string, + filterQuery?: string ): Promise { const params = { index, @@ -59,6 +60,15 @@ export async function fetchClusterHealth( }, }; + try { + if (filterQuery) { + const filterQueryObject = JSON.parse(filterQuery); + params.body.query.bool.filter.push(filterQueryObject); + } + } catch (e) { + // meh + } + const result = await esClient.search(params); const response: ElasticsearchResponse = result.body as ElasticsearchResponse; return (response.hits?.hits ?? []).map((hit) => { diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_cpu_usage_node_stats.test.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_cpu_usage_node_stats.test.ts index 90cd456f18037..8f0083f1f533f 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_cpu_usage_node_stats.test.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_cpu_usage_node_stats.test.ts @@ -201,7 +201,9 @@ describe('fetchCpuUsageNodeStats', () => { {} as estypes.SearchResponse ); }); - await fetchCpuUsageNodeStats(esClient, clusters, index, startMs, endMs, size); + const filterQuery = + '{"bool":{"should":[{"exists":{"field":"cluster_uuid"}}],"minimum_should_match":1}}'; + await fetchCpuUsageNodeStats(esClient, clusters, index, startMs, endMs, size, filterQuery); expect(params).toStrictEqual({ index: '.monitoring-es-*', filter_path: ['aggregations'], @@ -213,6 +215,9 @@ describe('fetchCpuUsageNodeStats', () => { { terms: { cluster_uuid: ['abc123'] } }, { term: { type: 'node_stats' } }, { range: { timestamp: { format: 'epoch_millis', gte: 0, lte: 0 } } }, + { + bool: { should: [{ exists: { field: 'cluster_uuid' } }], minimum_should_match: 1 }, + }, ], }, }, diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_cpu_usage_node_stats.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_cpu_usage_node_stats.ts index 6f7d27916a7b1..2ad42870e9958 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_cpu_usage_node_stats.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_cpu_usage_node_stats.ts @@ -29,7 +29,8 @@ export async function fetchCpuUsageNodeStats( index: string, startMs: number, endMs: number, - size: number + size: number, + filterQuery?: string ): Promise { // Using pure MS didn't seem to work well with the date_histogram interval // but minutes does @@ -140,6 +141,15 @@ export async function fetchCpuUsageNodeStats( }, }; + try { + if (filterQuery) { + const filterQueryObject = JSON.parse(filterQuery); + params.body.query.bool.filter.push(filterQueryObject); + } + } catch (e) { + // meh + } + const { body: response } = await esClient.search(params); const stats: AlertCpuUsageNodeStats[] = []; const clusterBuckets = get( diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_disk_usage_node_stats.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_disk_usage_node_stats.ts index 70f05991d4229..2d4872c0bd895 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_disk_usage_node_stats.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_disk_usage_node_stats.ts @@ -14,7 +14,8 @@ export async function fetchDiskUsageNodeStats( clusters: AlertCluster[], index: string, duration: string, - size: number + size: number, + filterQuery?: string ): Promise { const clustersIds = clusters.map((cluster) => cluster.clusterUuid); const params = { @@ -99,6 +100,15 @@ export async function fetchDiskUsageNodeStats( }, }; + try { + if (filterQuery) { + const filterQueryObject = JSON.parse(filterQuery); + params.body.query.bool.filter.push(filterQueryObject); + } + } catch (e) { + // meh + } + const { body: response } = await esClient.search(params); const stats: AlertDiskUsageNodeStats[] = []; // @ts-expect-error declare type for aggregations explicitly diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_elasticsearch_versions.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_elasticsearch_versions.ts index f2f311ac870a5..6ca2e89048df9 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_elasticsearch_versions.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_elasticsearch_versions.ts @@ -12,7 +12,8 @@ export async function fetchElasticsearchVersions( esClient: ElasticsearchClient, clusters: AlertCluster[], index: string, - size: number + size: number, + filterQuery?: string ): Promise { const params = { index, @@ -60,6 +61,15 @@ export async function fetchElasticsearchVersions( }, }; + try { + if (filterQuery) { + const filterQueryObject = JSON.parse(filterQuery); + params.body.query.bool.filter.push(filterQueryObject); + } + } catch (e) { + // meh + } + const result = await esClient.search(params); const response: ElasticsearchResponse = result.body as ElasticsearchResponse; return (response.hits?.hits ?? []).map((hit) => { diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_index_shard_size.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_index_shard_size.ts index 7e7ea5e6bfdd2..98bb546b43ab9 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_index_shard_size.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_index_shard_size.ts @@ -35,7 +35,8 @@ export async function fetchIndexShardSize( index: string, threshold: number, shardIndexPatterns: string, - size: number + size: number, + filterQuery?: string ): Promise { const params = { index, @@ -104,6 +105,15 @@ export async function fetchIndexShardSize( }, }; + try { + if (filterQuery) { + const filterQueryObject = JSON.parse(filterQuery); + params.body.query.bool.must.push(filterQueryObject); + } + } catch (e) { + // meh + } + const { body: response } = await esClient.search(params); // @ts-expect-error declare aggegations type explicitly const { buckets: clusterBuckets } = response.aggregations?.clusters; diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_kibana_versions.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_kibana_versions.ts index e57b45e2570fa..71813f3a526de 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_kibana_versions.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_kibana_versions.ts @@ -16,7 +16,8 @@ export async function fetchKibanaVersions( esClient: ElasticsearchClient, clusters: AlertCluster[], index: string, - size: number + size: number, + filterQuery?: string ): Promise { const params = { index, @@ -89,6 +90,15 @@ export async function fetchKibanaVersions( }, }; + try { + if (filterQuery) { + const filterQueryObject = JSON.parse(filterQuery); + params.body.query.bool.filter.push(filterQueryObject); + } + } catch (e) { + // meh + } + const { body: response } = await esClient.search(params); const indexName = get(response, 'aggregations.index.buckets[0].key', ''); const clusterList = get(response, 'aggregations.cluster.buckets', []) as ESAggResponse[]; diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_licenses.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_licenses.ts index 38ff82cf29832..b7bdf2fb6be72 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_licenses.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_licenses.ts @@ -11,7 +11,8 @@ import { ElasticsearchSource } from '../../../common/types/es'; export async function fetchLicenses( esClient: ElasticsearchClient, clusters: AlertCluster[], - index: string + index: string, + filterQuery?: string ): Promise { const params = { index, @@ -59,6 +60,15 @@ export async function fetchLicenses( }, }; + try { + if (filterQuery) { + const filterQueryObject = JSON.parse(filterQuery); + params.body.query.bool.filter.push(filterQueryObject); + } + } catch (e) { + // meh + } + const { body: response } = await esClient.search(params); return ( response?.hits?.hits.map((hit) => { diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_logstash_versions.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_logstash_versions.ts index 774ee2551ec07..112c2fe798b10 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_logstash_versions.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_logstash_versions.ts @@ -16,7 +16,8 @@ export async function fetchLogstashVersions( esClient: ElasticsearchClient, clusters: AlertCluster[], index: string, - size: number + size: number, + filterQuery?: string ): Promise { const params = { index, @@ -89,6 +90,15 @@ export async function fetchLogstashVersions( }, }; + try { + if (filterQuery) { + const filterQueryObject = JSON.parse(filterQuery); + params.body.query.bool.filter.push(filterQueryObject); + } + } catch (e) { + // meh + } + const { body: response } = await esClient.search(params); const indexName = get(response, 'aggregations.index.buckets[0].key', ''); const clusterList = get(response, 'aggregations.cluster.buckets', []) as ESAggResponse[]; diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_memory_usage_node_stats.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_memory_usage_node_stats.ts index f34a8dcff1db7..9403ae5d79a70 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_memory_usage_node_stats.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_memory_usage_node_stats.ts @@ -15,7 +15,8 @@ export async function fetchMemoryUsageNodeStats( index: string, startMs: number, endMs: number, - size: number + size: number, + filterQuery?: string ): Promise { const clustersIds = clusters.map((cluster) => cluster.clusterUuid); const params = { @@ -92,6 +93,15 @@ export async function fetchMemoryUsageNodeStats( }, }; + try { + if (filterQuery) { + const filterQueryObject = JSON.parse(filterQuery); + params.body.query.bool.filter.push(filterQueryObject); + } + } catch (e) { + // meh + } + const { body: response } = await esClient.search(params); const stats: AlertMemoryUsageNodeStats[] = []; // @ts-expect-error declare type for aggregations explicitly diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_missing_monitoring_data.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_missing_monitoring_data.ts index 856ca7c919885..cdf0f21b52b09 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_missing_monitoring_data.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_missing_monitoring_data.ts @@ -47,7 +47,8 @@ export async function fetchMissingMonitoringData( index: string, size: number, nowInMs: number, - startMs: number + startMs: number, + filterQuery?: string ): Promise { const endMs = nowInMs; const params = { @@ -117,6 +118,15 @@ export async function fetchMissingMonitoringData( }, }; + try { + if (filterQuery) { + const filterQueryObject = JSON.parse(filterQuery); + params.body.query.bool.filter.push(filterQueryObject); + } + } catch (e) { + // meh + } + const { body: response } = await esClient.search(params); const clusterBuckets = get( response, diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_nodes_from_cluster_stats.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_nodes_from_cluster_stats.ts index dcc8e6516c69b..3dc3e315318fc 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_nodes_from_cluster_stats.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_nodes_from_cluster_stats.ts @@ -26,7 +26,8 @@ function formatNode( export async function fetchNodesFromClusterStats( esClient: ElasticsearchClient, clusters: AlertCluster[], - index: string + index: string, + filterQuery?: string ): Promise { const params = { index, @@ -88,6 +89,15 @@ export async function fetchNodesFromClusterStats( }, }; + try { + if (filterQuery) { + const filterQueryObject = JSON.parse(filterQuery); + params.body.query.bool.filter.push(filterQueryObject); + } + } catch (e) { + // meh + } + const { body: response } = await esClient.search(params); const nodes: AlertClusterStatsNodes[] = []; // @ts-expect-error declare type for aggregations explicitly diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_thread_pool_rejections_stats.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_thread_pool_rejections_stats.ts index 132f7692a7579..0d1d052b5f866 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_thread_pool_rejections_stats.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_thread_pool_rejections_stats.ts @@ -36,7 +36,8 @@ export async function fetchThreadPoolRejectionStats( index: string, size: number, threadType: string, - duration: string + duration: string, + filterQuery?: string ): Promise { const clustersIds = clusters.map((cluster) => cluster.clusterUuid); const params = { @@ -94,6 +95,15 @@ export async function fetchThreadPoolRejectionStats( }, }; + try { + if (filterQuery) { + const filterQueryObject = JSON.parse(filterQuery); + params.body.query.bool.filter.push(filterQueryObject); + } + } catch (e) { + // meh + } + const { body: response } = await esClient.search(params); const stats: AlertThreadPoolRejectionsStats[] = []; // @ts-expect-error declare type for aggregations explicitly diff --git a/x-pack/plugins/monitoring/server/lib/cluster/get_clusters_stats.ts b/x-pack/plugins/monitoring/server/lib/cluster/get_clusters_stats.ts index 6eb21165d7256..a2201ca958e35 100644 --- a/x-pack/plugins/monitoring/server/lib/cluster/get_clusters_stats.ts +++ b/x-pack/plugins/monitoring/server/lib/cluster/get_clusters_stats.ts @@ -12,7 +12,7 @@ import { createQuery } from '../create_query'; // @ts-ignore import { ElasticsearchMetric } from '../metrics'; // @ts-ignore -import { parseCrossClusterPrefix } from '../ccs_utils'; +import { parseCrossClusterPrefix } from '../../../common/ccs_utils'; import { getClustersState } from './get_clusters_state'; import { ElasticsearchResponse, ElasticsearchModifiedSource } from '../../../common/types/es'; import { LegacyRequest } from '../../types'; diff --git a/x-pack/plugins/monitoring/server/lib/cluster/get_index_patterns.ts b/x-pack/plugins/monitoring/server/lib/cluster/get_index_patterns.ts index d908d6180772e..ccfe380edec09 100644 --- a/x-pack/plugins/monitoring/server/lib/cluster/get_index_patterns.ts +++ b/x-pack/plugins/monitoring/server/lib/cluster/get_index_patterns.ts @@ -6,7 +6,7 @@ */ import { LegacyServer } from '../../types'; -import { prefixIndexPattern } from '../ccs_utils'; +import { prefixIndexPattern } from '../../../common/ccs_utils'; import { INDEX_PATTERN_ELASTICSEARCH, INDEX_PATTERN_KIBANA, diff --git a/x-pack/plugins/monitoring/server/lib/logs/init_infra_source.ts b/x-pack/plugins/monitoring/server/lib/logs/init_infra_source.ts index c0fa931676870..727e47b62bc92 100644 --- a/x-pack/plugins/monitoring/server/lib/logs/init_infra_source.ts +++ b/x-pack/plugins/monitoring/server/lib/logs/init_infra_source.ts @@ -6,7 +6,7 @@ */ // @ts-ignore -import { prefixIndexPattern } from '../ccs_utils'; +import { prefixIndexPattern } from '../../../common/ccs_utils'; import { INFRA_SOURCE_ID } from '../../../common/constants'; import { MonitoringConfig } from '../../config'; import { InfraPluginSetup } from '../../../../infra/server'; diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/apm/instance.js b/x-pack/plugins/monitoring/server/routes/api/v1/apm/instance.js index 4884b8151f61f..a0b00167101fe 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/apm/instance.js +++ b/x-pack/plugins/monitoring/server/routes/api/v1/apm/instance.js @@ -6,7 +6,7 @@ */ import { schema } from '@kbn/config-schema'; -import { prefixIndexPattern } from '../../../../lib/ccs_utils'; +import { prefixIndexPattern } from '../../../../../common/ccs_utils'; import { getMetrics } from '../../../../lib/details/get_metrics'; import { metricSet } from './metric_set_instance'; import { handleError } from '../../../../lib/errors'; diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/apm/instances.js b/x-pack/plugins/monitoring/server/routes/api/v1/apm/instances.js index 53afa4c3f01b4..95f378ff5b98d 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/apm/instances.js +++ b/x-pack/plugins/monitoring/server/routes/api/v1/apm/instances.js @@ -6,7 +6,7 @@ */ import { schema } from '@kbn/config-schema'; -import { prefixIndexPattern } from '../../../../lib/ccs_utils'; +import { prefixIndexPattern } from '../../../../../common/ccs_utils'; import { getStats, getApms } from '../../../../lib/apm'; import { handleError } from '../../../../lib/errors'; import { INDEX_PATTERN_BEATS } from '../../../../../common/constants'; diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/apm/overview.js b/x-pack/plugins/monitoring/server/routes/api/v1/apm/overview.js index 7a772594b4bc2..ea7f3f41b842e 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/apm/overview.js +++ b/x-pack/plugins/monitoring/server/routes/api/v1/apm/overview.js @@ -6,7 +6,7 @@ */ import { schema } from '@kbn/config-schema'; -import { prefixIndexPattern } from '../../../../lib/ccs_utils'; +import { prefixIndexPattern } from '../../../../../common/ccs_utils'; import { getMetrics } from '../../../../lib/details/get_metrics'; import { metricSet } from './metric_set_overview'; import { handleError } from '../../../../lib/errors'; diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/beats/beat_detail.js b/x-pack/plugins/monitoring/server/routes/api/v1/beats/beat_detail.js index 919efe98f3df3..851380fede77d 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/beats/beat_detail.js +++ b/x-pack/plugins/monitoring/server/routes/api/v1/beats/beat_detail.js @@ -6,7 +6,7 @@ */ import { schema } from '@kbn/config-schema'; -import { prefixIndexPattern } from '../../../../lib/ccs_utils'; +import { prefixIndexPattern } from '../../../../../common/ccs_utils'; import { getBeatSummary } from '../../../../lib/beats'; import { getMetrics } from '../../../../lib/details/get_metrics'; import { handleError } from '../../../../lib/errors'; diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/beats/beats.js b/x-pack/plugins/monitoring/server/routes/api/v1/beats/beats.js index 57b24e59e66ab..fa35ccb9371c2 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/beats/beats.js +++ b/x-pack/plugins/monitoring/server/routes/api/v1/beats/beats.js @@ -6,7 +6,7 @@ */ import { schema } from '@kbn/config-schema'; -import { prefixIndexPattern } from '../../../../lib/ccs_utils'; +import { prefixIndexPattern } from '../../../../../common/ccs_utils'; import { getStats, getBeats } from '../../../../lib/beats'; import { handleError } from '../../../../lib/errors'; import { INDEX_PATTERN_BEATS } from '../../../../../common/constants'; diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/beats/overview.js b/x-pack/plugins/monitoring/server/routes/api/v1/beats/overview.js index 5f1bb1778bc9a..4abf46b3ad1ce 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/beats/overview.js +++ b/x-pack/plugins/monitoring/server/routes/api/v1/beats/overview.js @@ -6,7 +6,7 @@ */ import { schema } from '@kbn/config-schema'; -import { prefixIndexPattern } from '../../../../lib/ccs_utils'; +import { prefixIndexPattern } from '../../../../../common/ccs_utils'; import { getMetrics } from '../../../../lib/details/get_metrics'; import { getLatestStats, getStats } from '../../../../lib/beats'; import { handleError } from '../../../../lib/errors'; diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch/ccr.ts b/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch/ccr.ts index 73b646126ce98..898cfc82463d9 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch/ccr.ts +++ b/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch/ccr.ts @@ -11,7 +11,7 @@ import { get, groupBy } from 'lodash'; // @ts-ignore import { handleError } from '../../../../lib/errors/handle_error'; // @ts-ignore -import { prefixIndexPattern } from '../../../../lib/ccs_utils'; +import { prefixIndexPattern } from '../../../../../common/ccs_utils'; import { INDEX_PATTERN_ELASTICSEARCH } from '../../../../../common/constants'; import { ElasticsearchResponse, diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch/ccr_shard.ts b/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch/ccr_shard.ts index 5ecb84d97618b..d07a660222407 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch/ccr_shard.ts +++ b/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch/ccr_shard.ts @@ -10,7 +10,7 @@ import { schema } from '@kbn/config-schema'; // @ts-ignore import { handleError } from '../../../../lib/errors/handle_error'; // @ts-ignore -import { prefixIndexPattern } from '../../../../lib/ccs_utils'; +import { prefixIndexPattern } from '../../../../../common/ccs_utils'; // @ts-ignore import { getMetrics } from '../../../../lib/details/get_metrics'; import { INDEX_PATTERN_ELASTICSEARCH } from '../../../../../common/constants'; diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch/index_detail.js b/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch/index_detail.js index 89ca911f44268..e99ae04ab282c 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch/index_detail.js +++ b/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch/index_detail.js @@ -12,7 +12,7 @@ import { getIndexSummary } from '../../../../lib/elasticsearch/indices'; import { getMetrics } from '../../../../lib/details/get_metrics'; import { getShardAllocation, getShardStats } from '../../../../lib/elasticsearch/shards'; import { handleError } from '../../../../lib/errors/handle_error'; -import { prefixIndexPattern } from '../../../../lib/ccs_utils'; +import { prefixIndexPattern } from '../../../../../common/ccs_utils'; import { metricSet } from './metric_set_index_detail'; import { INDEX_PATTERN_ELASTICSEARCH } from '../../../../../common/constants'; import { getLogs } from '../../../../lib/logs/get_logs'; diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch/indices.js b/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch/indices.js index 8099ecf3462cc..76e769ac030ba 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch/indices.js +++ b/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch/indices.js @@ -10,7 +10,7 @@ import { getClusterStats } from '../../../../lib/cluster/get_cluster_stats'; import { getClusterStatus } from '../../../../lib/cluster/get_cluster_status'; import { getIndices } from '../../../../lib/elasticsearch/indices'; import { handleError } from '../../../../lib/errors/handle_error'; -import { prefixIndexPattern } from '../../../../lib/ccs_utils'; +import { prefixIndexPattern } from '../../../../../common/ccs_utils'; import { INDEX_PATTERN_ELASTICSEARCH } from '../../../../../common/constants'; import { getIndicesUnassignedShardStats } from '../../../../lib/elasticsearch/shards/get_indices_unassigned_shard_stats'; diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch/ml_jobs.js b/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch/ml_jobs.js index e23c23f7a819d..5853cc3d6ee9d 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch/ml_jobs.js +++ b/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch/ml_jobs.js @@ -10,7 +10,7 @@ import { getClusterStats } from '../../../../lib/cluster/get_cluster_stats'; import { getClusterStatus } from '../../../../lib/cluster/get_cluster_status'; import { getMlJobs } from '../../../../lib/elasticsearch/get_ml_jobs'; import { handleError } from '../../../../lib/errors/handle_error'; -import { prefixIndexPattern } from '../../../../lib/ccs_utils'; +import { prefixIndexPattern } from '../../../../../common/ccs_utils'; import { INDEX_PATTERN_ELASTICSEARCH } from '../../../../../common/constants'; import { getIndicesUnassignedShardStats } from '../../../../lib/elasticsearch/shards/get_indices_unassigned_shard_stats'; diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch/node_detail.js b/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch/node_detail.js index 2122f8ceb2215..5f77d0394a4f1 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch/node_detail.js +++ b/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch/node_detail.js @@ -12,7 +12,7 @@ import { getNodeSummary } from '../../../../lib/elasticsearch/nodes'; import { getShardStats, getShardAllocation } from '../../../../lib/elasticsearch/shards'; import { getMetrics } from '../../../../lib/details/get_metrics'; import { handleError } from '../../../../lib/errors/handle_error'; -import { prefixIndexPattern } from '../../../../lib/ccs_utils'; +import { prefixIndexPattern } from '../../../../../common/ccs_utils'; import { metricSets } from './metric_set_node_detail'; import { INDEX_PATTERN_ELASTICSEARCH } from '../../../../../common/constants'; import { getLogs } from '../../../../lib/logs/get_logs'; diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch/nodes.js b/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch/nodes.js index db12e28916b65..7ea2e6e1e1440 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch/nodes.js +++ b/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch/nodes.js @@ -11,7 +11,7 @@ import { getClusterStatus } from '../../../../lib/cluster/get_cluster_status'; import { getNodes } from '../../../../lib/elasticsearch/nodes'; import { getNodesShardCount } from '../../../../lib/elasticsearch/shards/get_nodes_shard_count'; import { handleError } from '../../../../lib/errors/handle_error'; -import { prefixIndexPattern } from '../../../../lib/ccs_utils'; +import { prefixIndexPattern } from '../../../../../common/ccs_utils'; import { INDEX_PATTERN_ELASTICSEARCH } from '../../../../../common/constants'; import { getPaginatedNodes } from '../../../../lib/elasticsearch/nodes/get_nodes/get_paginated_nodes'; import { LISTING_METRICS_NAMES } from '../../../../lib/elasticsearch/nodes/get_nodes/nodes_listing_metrics'; diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch/overview.js b/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch/overview.js index c76513df721ba..a0fc524768eb9 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch/overview.js +++ b/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch/overview.js @@ -11,7 +11,7 @@ import { getClusterStatus } from '../../../../lib/cluster/get_cluster_status'; import { getLastRecovery } from '../../../../lib/elasticsearch/get_last_recovery'; import { getMetrics } from '../../../../lib/details/get_metrics'; import { handleError } from '../../../../lib/errors/handle_error'; -import { prefixIndexPattern } from '../../../../lib/ccs_utils'; +import { prefixIndexPattern } from '../../../../../common/ccs_utils'; import { metricSet } from './metric_set_overview'; import { INDEX_PATTERN_ELASTICSEARCH } from '../../../../../common/constants'; import { getLogs } from '../../../../lib/logs'; diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch_settings/check/internal_monitoring.ts b/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch_settings/check/internal_monitoring.ts index d05d60866d119..3cd2b8b73b315 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch_settings/check/internal_monitoring.ts +++ b/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch_settings/check/internal_monitoring.ts @@ -14,7 +14,7 @@ import { INDEX_PATTERN_LOGSTASH, } from '../../../../../../common/constants'; // @ts-ignore -import { prefixIndexPattern } from '../../../../../lib/ccs_utils'; +import { prefixIndexPattern } from '../../../../../../common/ccs_utils'; // @ts-ignore import { handleError } from '../../../../../lib/errors'; import { RouteDependencies, LegacyServer } from '../../../../../types'; diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/kibana/instance.ts b/x-pack/plugins/monitoring/server/routes/api/v1/kibana/instance.ts index d16f568b475b4..613ca39275c2d 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/kibana/instance.ts +++ b/x-pack/plugins/monitoring/server/routes/api/v1/kibana/instance.ts @@ -12,7 +12,7 @@ import { handleError } from '../../../../lib/errors'; // @ts-ignore import { getMetrics } from '../../../../lib/details/get_metrics'; // @ts-ignore -import { prefixIndexPattern } from '../../../../lib/ccs_utils'; +import { prefixIndexPattern } from '../../../../../common/ccs_utils'; // @ts-ignore import { metricSet } from './metric_set_instance'; import { INDEX_PATTERN_KIBANA } from '../../../../../common/constants'; diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/kibana/instances.js b/x-pack/plugins/monitoring/server/routes/api/v1/kibana/instances.js index 59618f0a217b5..f9b3498cd684e 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/kibana/instances.js +++ b/x-pack/plugins/monitoring/server/routes/api/v1/kibana/instances.js @@ -6,7 +6,7 @@ */ import { schema } from '@kbn/config-schema'; -import { prefixIndexPattern } from '../../../../lib/ccs_utils'; +import { prefixIndexPattern } from '../../../../../common/ccs_utils'; import { getKibanaClusterStatus } from './_get_kibana_cluster_status'; import { getKibanas } from '../../../../lib/kibana/get_kibanas'; import { handleError } from '../../../../lib/errors'; diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/kibana/overview.js b/x-pack/plugins/monitoring/server/routes/api/v1/kibana/overview.js index cca36d2aad1a7..f9a9443c3533b 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/kibana/overview.js +++ b/x-pack/plugins/monitoring/server/routes/api/v1/kibana/overview.js @@ -6,7 +6,7 @@ */ import { schema } from '@kbn/config-schema'; -import { prefixIndexPattern } from '../../../../lib/ccs_utils'; +import { prefixIndexPattern } from '../../../../../common/ccs_utils'; import { getKibanaClusterStatus } from './_get_kibana_cluster_status'; import { getMetrics } from '../../../../lib/details/get_metrics'; import { metricSet } from './metric_set_overview'; diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/logstash/node.js b/x-pack/plugins/monitoring/server/routes/api/v1/logstash/node.js index b81b4ea796c63..d3ecea95430ca 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/logstash/node.js +++ b/x-pack/plugins/monitoring/server/routes/api/v1/logstash/node.js @@ -9,7 +9,7 @@ import { schema } from '@kbn/config-schema'; import { getNodeInfo } from '../../../../lib/logstash/get_node_info'; import { handleError } from '../../../../lib/errors'; import { getMetrics } from '../../../../lib/details/get_metrics'; -import { prefixIndexPattern } from '../../../../lib/ccs_utils'; +import { prefixIndexPattern } from '../../../../../common/ccs_utils'; import { metricSets } from './metric_set_node'; import { INDEX_PATTERN_LOGSTASH } from '../../../../../common/constants'; diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/logstash/nodes.js b/x-pack/plugins/monitoring/server/routes/api/v1/logstash/nodes.js index 74b89ab41be92..051fb7d38fd41 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/logstash/nodes.js +++ b/x-pack/plugins/monitoring/server/routes/api/v1/logstash/nodes.js @@ -9,7 +9,7 @@ import { schema } from '@kbn/config-schema'; import { getClusterStatus } from '../../../../lib/logstash/get_cluster_status'; import { getNodes } from '../../../../lib/logstash/get_nodes'; import { handleError } from '../../../../lib/errors'; -import { prefixIndexPattern } from '../../../../lib/ccs_utils'; +import { prefixIndexPattern } from '../../../../../common/ccs_utils'; import { INDEX_PATTERN_LOGSTASH } from '../../../../../common/constants'; /* diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/logstash/overview.js b/x-pack/plugins/monitoring/server/routes/api/v1/logstash/overview.js index 23dd64a1afb74..89a6a93fb207d 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/logstash/overview.js +++ b/x-pack/plugins/monitoring/server/routes/api/v1/logstash/overview.js @@ -9,7 +9,7 @@ import { schema } from '@kbn/config-schema'; import { getClusterStatus } from '../../../../lib/logstash/get_cluster_status'; import { getMetrics } from '../../../../lib/details/get_metrics'; import { handleError } from '../../../../lib/errors'; -import { prefixIndexPattern } from '../../../../lib/ccs_utils'; +import { prefixIndexPattern } from '../../../../../common/ccs_utils'; import { metricSet } from './metric_set_overview'; import { INDEX_PATTERN_LOGSTASH } from '../../../../../common/constants'; diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/logstash/pipeline.js b/x-pack/plugins/monitoring/server/routes/api/v1/logstash/pipeline.js index 4243b2d6c3a5c..6b81059f0c256 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/logstash/pipeline.js +++ b/x-pack/plugins/monitoring/server/routes/api/v1/logstash/pipeline.js @@ -10,7 +10,7 @@ import { handleError } from '../../../../lib/errors'; import { getPipelineVersions } from '../../../../lib/logstash/get_pipeline_versions'; import { getPipeline } from '../../../../lib/logstash/get_pipeline'; import { getPipelineVertex } from '../../../../lib/logstash/get_pipeline_vertex'; -import { prefixIndexPattern } from '../../../../lib/ccs_utils'; +import { prefixIndexPattern } from '../../../../../common/ccs_utils'; import { INDEX_PATTERN_LOGSTASH } from '../../../../../common/constants'; function getPipelineVersion(versions, pipelineHash) { diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/logstash/pipelines/cluster_pipeline_ids.js b/x-pack/plugins/monitoring/server/routes/api/v1/logstash/pipelines/cluster_pipeline_ids.js index c881ff7b3d23c..7f14b74da207d 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/logstash/pipelines/cluster_pipeline_ids.js +++ b/x-pack/plugins/monitoring/server/routes/api/v1/logstash/pipelines/cluster_pipeline_ids.js @@ -7,7 +7,7 @@ import { schema } from '@kbn/config-schema'; import { handleError } from '../../../../../lib/errors'; -import { prefixIndexPattern } from '../../../../../lib/ccs_utils'; +import { prefixIndexPattern } from '../../../../../../common/ccs_utils'; import { INDEX_PATTERN_LOGSTASH } from '../../../../../../common/constants'; import { getLogstashPipelineIds } from '../../../../../lib/logstash/get_pipeline_ids'; diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/logstash/pipelines/cluster_pipelines.js b/x-pack/plugins/monitoring/server/routes/api/v1/logstash/pipelines/cluster_pipelines.js index 1f7a5e1d436b1..b7d86e86e7a07 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/logstash/pipelines/cluster_pipelines.js +++ b/x-pack/plugins/monitoring/server/routes/api/v1/logstash/pipelines/cluster_pipelines.js @@ -8,7 +8,7 @@ import { schema } from '@kbn/config-schema'; import { getClusterStatus } from '../../../../../lib/logstash/get_cluster_status'; import { handleError } from '../../../../../lib/errors'; -import { prefixIndexPattern } from '../../../../../lib/ccs_utils'; +import { prefixIndexPattern } from '../../../../../../common/ccs_utils'; import { INDEX_PATTERN_LOGSTASH } from '../../../../../../common/constants'; import { getPaginatedPipelines } from '../../../../../lib/logstash/get_paginated_pipelines'; diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/logstash/pipelines/node_pipelines.js b/x-pack/plugins/monitoring/server/routes/api/v1/logstash/pipelines/node_pipelines.js index 47b8fd81a4d44..f31e88b5b8b08 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/logstash/pipelines/node_pipelines.js +++ b/x-pack/plugins/monitoring/server/routes/api/v1/logstash/pipelines/node_pipelines.js @@ -8,7 +8,7 @@ import { schema } from '@kbn/config-schema'; import { getNodeInfo } from '../../../../../lib/logstash/get_node_info'; import { handleError } from '../../../../../lib/errors'; -import { prefixIndexPattern } from '../../../../../lib/ccs_utils'; +import { prefixIndexPattern } from '../../../../../../common/ccs_utils'; import { INDEX_PATTERN_LOGSTASH } from '../../../../../../common/constants'; import { getPaginatedPipelines } from '../../../../../lib/logstash/get_paginated_pipelines'; diff --git a/x-pack/plugins/observability/kibana.json b/x-pack/plugins/observability/kibana.json index 45fe0258dd142..07299f2e6ff1c 100644 --- a/x-pack/plugins/observability/kibana.json +++ b/x-pack/plugins/observability/kibana.json @@ -15,6 +15,7 @@ "home", "lens", "licensing", + "spaces", "usageCollection" ], "requiredPlugins": [ diff --git a/x-pack/plugins/observability/tsconfig.json b/x-pack/plugins/observability/tsconfig.json index 4e912ee4535b8..dc935f3f77787 100644 --- a/x-pack/plugins/observability/tsconfig.json +++ b/x-pack/plugins/observability/tsconfig.json @@ -26,6 +26,7 @@ { "path": "../cases/tsconfig.json" }, { "path": "../lens/tsconfig.json" }, { "path": "../rule_registry/tsconfig.json" }, + { "path": "../spaces/tsconfig.json" }, { "path": "../timelines/tsconfig.json"}, { "path": "../translations/tsconfig.json" } ] diff --git a/x-pack/plugins/reporting/server/lib/content_stream.ts b/x-pack/plugins/reporting/server/lib/content_stream.ts index 79ff9a6812137..23cc8a302dbef 100644 --- a/x-pack/plugins/reporting/server/lib/content_stream.ts +++ b/x-pack/plugins/reporting/server/lib/content_stream.ts @@ -93,11 +93,11 @@ export class ContentStream extends Duplex { this.parameters = { encoding }; } - private async decode(content: string) { + private decode(content: string) { return Buffer.from(content, this.parameters.encoding === 'base64' ? 'base64' : undefined); } - private async encode(buffer: Buffer) { + private encode(buffer: Buffer) { return buffer.toString(this.parameters.encoding === 'base64' ? 'base64' : undefined); } @@ -188,7 +188,7 @@ export class ContentStream extends Duplex { return; } - const buffer = await this.decode(content); + const buffer = this.decode(content); this.push(buffer); this.chunksRead++; @@ -252,7 +252,7 @@ export class ContentStream extends Duplex { private async flush(size = this.buffer.byteLength) { const chunk = this.buffer.slice(0, size); - const content = await this.encode(chunk); + const content = this.encode(chunk); if (!this.chunksWritten) { await this.removeChunks(); @@ -269,32 +269,29 @@ export class ContentStream extends Duplex { this.buffer = this.buffer.slice(size); } - async _write(chunk: Buffer | string, encoding: BufferEncoding, callback: Callback) { + private async flushAllFullChunks() { + const maxChunkSize = await this.getMaxChunkSize(); + + while (this.buffer.byteLength >= maxChunkSize) { + await this.flush(maxChunkSize); + } + } + + _write(chunk: Buffer | string, encoding: BufferEncoding, callback: Callback) { this.buffer = Buffer.concat([ this.buffer, Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk, encoding), ]); - try { - const maxChunkSize = await this.getMaxChunkSize(); - - while (this.buffer.byteLength >= maxChunkSize) { - await this.flush(maxChunkSize); - } - - callback(); - } catch (error) { - callback(error); - } + this.flushAllFullChunks() + .then(() => callback()) + .catch(callback); } - async _final(callback: Callback) { - try { - await this.flush(); - callback(); - } catch (error) { - callback(error); - } + _final(callback: Callback) { + this.flush() + .then(() => callback()) + .catch(callback); } getSeqNo(): number | undefined { diff --git a/x-pack/plugins/reporting/server/usage/__snapshots__/reporting_usage_collector.test.ts.snap b/x-pack/plugins/reporting/server/usage/__snapshots__/reporting_usage_collector.test.ts.snap index 12debe5c85d5e..2017ae0be59c7 100644 --- a/x-pack/plugins/reporting/server/usage/__snapshots__/reporting_usage_collector.test.ts.snap +++ b/x-pack/plugins/reporting/server/usage/__snapshots__/reporting_usage_collector.test.ts.snap @@ -37,6 +37,29 @@ Object { "type": "long", }, }, + "sizes": Object { + "1.0": Object { + "type": "long", + }, + "25.0": Object { + "type": "long", + }, + "5.0": Object { + "type": "long", + }, + "50.0": Object { + "type": "long", + }, + "75.0": Object { + "type": "long", + }, + "95.0": Object { + "type": "long", + }, + "99.0": Object { + "type": "long", + }, + }, "total": Object { "type": "long", }, @@ -73,6 +96,29 @@ Object { "type": "long", }, }, + "sizes": Object { + "1.0": Object { + "type": "long", + }, + "25.0": Object { + "type": "long", + }, + "5.0": Object { + "type": "long", + }, + "50.0": Object { + "type": "long", + }, + "75.0": Object { + "type": "long", + }, + "95.0": Object { + "type": "long", + }, + "99.0": Object { + "type": "long", + }, + }, "total": Object { "type": "long", }, @@ -118,6 +164,29 @@ Object { "type": "long", }, }, + "sizes": Object { + "1.0": Object { + "type": "long", + }, + "25.0": Object { + "type": "long", + }, + "5.0": Object { + "type": "long", + }, + "50.0": Object { + "type": "long", + }, + "75.0": Object { + "type": "long", + }, + "95.0": Object { + "type": "long", + }, + "99.0": Object { + "type": "long", + }, + }, "total": Object { "type": "long", }, @@ -154,6 +223,29 @@ Object { "type": "long", }, }, + "sizes": Object { + "1.0": Object { + "type": "long", + }, + "25.0": Object { + "type": "long", + }, + "5.0": Object { + "type": "long", + }, + "50.0": Object { + "type": "long", + }, + "75.0": Object { + "type": "long", + }, + "95.0": Object { + "type": "long", + }, + "99.0": Object { + "type": "long", + }, + }, "total": Object { "type": "long", }, @@ -190,6 +282,29 @@ Object { "type": "long", }, }, + "sizes": Object { + "1.0": Object { + "type": "long", + }, + "25.0": Object { + "type": "long", + }, + "5.0": Object { + "type": "long", + }, + "50.0": Object { + "type": "long", + }, + "75.0": Object { + "type": "long", + }, + "95.0": Object { + "type": "long", + }, + "99.0": Object { + "type": "long", + }, + }, "total": Object { "type": "long", }, @@ -230,6 +345,29 @@ Object { "type": "long", }, }, + "sizes": Object { + "1.0": Object { + "type": "long", + }, + "25.0": Object { + "type": "long", + }, + "5.0": Object { + "type": "long", + }, + "50.0": Object { + "type": "long", + }, + "75.0": Object { + "type": "long", + }, + "95.0": Object { + "type": "long", + }, + "99.0": Object { + "type": "long", + }, + }, "total": Object { "type": "long", }, @@ -266,6 +404,29 @@ Object { "type": "long", }, }, + "sizes": Object { + "1.0": Object { + "type": "long", + }, + "25.0": Object { + "type": "long", + }, + "5.0": Object { + "type": "long", + }, + "50.0": Object { + "type": "long", + }, + "75.0": Object { + "type": "long", + }, + "95.0": Object { + "type": "long", + }, + "99.0": Object { + "type": "long", + }, + }, "total": Object { "type": "long", }, @@ -305,6 +466,29 @@ Object { "type": "long", }, }, + "sizes": Object { + "1.0": Object { + "type": "long", + }, + "25.0": Object { + "type": "long", + }, + "5.0": Object { + "type": "long", + }, + "50.0": Object { + "type": "long", + }, + "75.0": Object { + "type": "long", + }, + "95.0": Object { + "type": "long", + }, + "99.0": Object { + "type": "long", + }, + }, "total": Object { "type": "long", }, @@ -341,6 +525,29 @@ Object { "type": "long", }, }, + "sizes": Object { + "1.0": Object { + "type": "long", + }, + "25.0": Object { + "type": "long", + }, + "5.0": Object { + "type": "long", + }, + "50.0": Object { + "type": "long", + }, + "75.0": Object { + "type": "long", + }, + "95.0": Object { + "type": "long", + }, + "99.0": Object { + "type": "long", + }, + }, "total": Object { "type": "long", }, @@ -377,10 +584,56 @@ Object { "type": "long", }, }, + "sizes": Object { + "1.0": Object { + "type": "long", + }, + "25.0": Object { + "type": "long", + }, + "5.0": Object { + "type": "long", + }, + "50.0": Object { + "type": "long", + }, + "75.0": Object { + "type": "long", + }, + "95.0": Object { + "type": "long", + }, + "99.0": Object { + "type": "long", + }, + }, "total": Object { "type": "long", }, }, + "output_size": Object { + "1.0": Object { + "type": "long", + }, + "25.0": Object { + "type": "long", + }, + "5.0": Object { + "type": "long", + }, + "50.0": Object { + "type": "long", + }, + "75.0": Object { + "type": "long", + }, + "95.0": Object { + "type": "long", + }, + "99.0": Object { + "type": "long", + }, + }, "printable_pdf": Object { "app": Object { "canvas workpad": Object { @@ -413,6 +666,29 @@ Object { "type": "long", }, }, + "sizes": Object { + "1.0": Object { + "type": "long", + }, + "25.0": Object { + "type": "long", + }, + "5.0": Object { + "type": "long", + }, + "50.0": Object { + "type": "long", + }, + "75.0": Object { + "type": "long", + }, + "95.0": Object { + "type": "long", + }, + "99.0": Object { + "type": "long", + }, + }, "total": Object { "type": "long", }, @@ -449,6 +725,29 @@ Object { "type": "long", }, }, + "sizes": Object { + "1.0": Object { + "type": "long", + }, + "25.0": Object { + "type": "long", + }, + "5.0": Object { + "type": "long", + }, + "50.0": Object { + "type": "long", + }, + "75.0": Object { + "type": "long", + }, + "95.0": Object { + "type": "long", + }, + "99.0": Object { + "type": "long", + }, + }, "total": Object { "type": "long", }, @@ -973,6 +1272,29 @@ Object { }, }, }, + "output_size": Object { + "1.0": Object { + "type": "long", + }, + "25.0": Object { + "type": "long", + }, + "5.0": Object { + "type": "long", + }, + "50.0": Object { + "type": "long", + }, + "75.0": Object { + "type": "long", + }, + "95.0": Object { + "type": "long", + }, + "99.0": Object { + "type": "long", + }, + }, "printable_pdf": Object { "app": Object { "canvas workpad": Object { @@ -1005,6 +1327,29 @@ Object { "type": "long", }, }, + "sizes": Object { + "1.0": Object { + "type": "long", + }, + "25.0": Object { + "type": "long", + }, + "5.0": Object { + "type": "long", + }, + "50.0": Object { + "type": "long", + }, + "75.0": Object { + "type": "long", + }, + "95.0": Object { + "type": "long", + }, + "99.0": Object { + "type": "long", + }, + }, "total": Object { "type": "long", }, @@ -1041,6 +1386,29 @@ Object { "type": "long", }, }, + "sizes": Object { + "1.0": Object { + "type": "long", + }, + "25.0": Object { + "type": "long", + }, + "5.0": Object { + "type": "long", + }, + "50.0": Object { + "type": "long", + }, + "75.0": Object { + "type": "long", + }, + "95.0": Object { + "type": "long", + }, + "99.0": Object { + "type": "long", + }, + }, "total": Object { "type": "long", }, @@ -1620,6 +1988,7 @@ Object { "preserve_layout": 0, "print": 0, }, + "output_size": undefined, "total": 4, }, "csv_searchsource": Object { @@ -1636,6 +2005,7 @@ Object { "preserve_layout": 0, "print": 0, }, + "output_size": undefined, "total": 5, }, "csv_searchsource_immediate": Object { @@ -1703,6 +2073,7 @@ Object { "preserve_layout": 0, "print": 0, }, + "output_size": undefined, "total": 4, }, "csv_searchsource": Object { @@ -1719,6 +2090,7 @@ Object { "preserve_layout": 0, "print": 0, }, + "output_size": undefined, "total": 5, }, "csv_searchsource_immediate": Object { @@ -1737,6 +2109,7 @@ Object { }, "total": 0, }, + "output_size": undefined, "printable_pdf": Object { "app": Object { "canvas workpad": 0, @@ -1784,6 +2157,7 @@ Object { }, }, }, + "output_size": undefined, "printable_pdf": Object { "app": Object { "canvas workpad": 0, @@ -2001,6 +2375,7 @@ Object { }, "total": 0, }, + "output_size": undefined, "printable_pdf": Object { "app": Object { "canvas workpad": 0, @@ -2039,6 +2414,7 @@ Object { }, "statuses": Object {}, }, + "output_size": undefined, "printable_pdf": Object { "app": Object { "canvas workpad": 0, @@ -2079,7 +2455,7 @@ Object { } `; -exports[`data modeling with normal looking usage data 1`] = ` +exports[`data modeling with sparse data 1`] = ` Object { "PNG": Object { "app": Object { @@ -2095,7 +2471,8 @@ Object { "preserve_layout": 0, "print": 0, }, - "total": 3, + "output_size": undefined, + "total": 1, }, "PNGV2": Object { "app": Object { @@ -2113,7 +2490,7 @@ Object { }, "total": 0, }, - "_all": 12, + "_all": 4, "available": true, "browser_type": undefined, "csv": Object { @@ -2124,13 +2501,14 @@ Object { "visualization": 0, }, "available": true, - "deprecated": 0, + "deprecated": 1, "layout": Object { "canvas": 0, "preserve_layout": 0, "print": 0, }, - "total": 0, + "output_size": undefined, + "total": 1, }, "csv_searchsource": Object { "app": Object { @@ -2180,6 +2558,7 @@ Object { "preserve_layout": 0, "print": 0, }, + "output_size": undefined, "total": 1, }, "PNGV2": Object { @@ -2198,7 +2577,7 @@ Object { }, "total": 0, }, - "_all": 1, + "_all": 4, "csv": Object { "app": Object { "canvas workpad": 0, @@ -2207,13 +2586,14 @@ Object { "visualization": 0, }, "available": true, - "deprecated": 0, + "deprecated": 1, "layout": Object { "canvas": 0, "preserve_layout": 0, "print": 0, }, - "total": 0, + "output_size": undefined, + "total": 1, }, "csv_searchsource": Object { "app": Object { @@ -2247,10 +2627,11 @@ Object { }, "total": 0, }, + "output_size": undefined, "printable_pdf": Object { "app": Object { - "canvas workpad": 0, - "dashboard": 0, + "canvas workpad": 1, + "dashboard": 1, "search": 0, "visualization": 0, }, @@ -2258,10 +2639,11 @@ Object { "deprecated": 0, "layout": Object { "canvas": 0, - "preserve_layout": 0, + "preserve_layout": 2, "print": 0, }, - "total": 0, + "output_size": undefined, + "total": 2, }, "printable_pdf_v2": Object { "app": Object { @@ -2280,33 +2662,39 @@ Object { "total": 0, }, "status": Object { - "completed": 0, - "completed_with_warnings": 1, + "completed": 4, "failed": 0, }, "statuses": Object { - "completed_with_warnings": Object { + "completed": Object { "PNG": Object { "dashboard": 1, }, + "csv": Object {}, + "printable_pdf": Object { + "canvas workpad": 1, + "dashboard": 1, + }, }, }, }, + "output_size": undefined, "printable_pdf": Object { "app": Object { - "canvas workpad": 6, - "dashboard": 0, + "canvas workpad": 1, + "dashboard": 1, "search": 0, - "visualization": 3, + "visualization": 0, }, "available": true, "deprecated": 0, "layout": Object { "canvas": 0, - "preserve_layout": 9, + "preserve_layout": 2, "print": 0, }, - "total": 9, + "output_size": undefined, + "total": 2, }, "printable_pdf_v2": Object { "app": Object { @@ -2325,27 +2713,17 @@ Object { "total": 0, }, "status": Object { - "completed": 10, - "completed_with_warnings": 1, - "failed": 1, + "completed": 4, + "failed": 0, }, "statuses": Object { "completed": Object { - "PNG": Object { - "visualization": 1, - }, - "printable_pdf": Object { - "canvas workpad": 6, - "visualization": 3, - }, - }, - "completed_with_warnings": Object { "PNG": Object { "dashboard": 1, }, - }, - "failed": Object { - "PNG": Object { + "csv": Object {}, + "printable_pdf": Object { + "canvas workpad": 1, "dashboard": 1, }, }, @@ -2353,7 +2731,7 @@ Object { } `; -exports[`data modeling with sparse data 1`] = ` +exports[`data modeling with usage data from the reporting/archived_reports es archive 1`] = ` Object { "PNG": Object { "app": Object { @@ -2369,6 +2747,7 @@ Object { "preserve_layout": 0, "print": 0, }, + "output_size": undefined, "total": 1, }, "PNGV2": Object { @@ -2387,7 +2766,7 @@ Object { }, "total": 0, }, - "_all": 4, + "_all": 11, "available": true, "browser_type": undefined, "csv": Object { @@ -2404,6 +2783,7 @@ Object { "preserve_layout": 0, "print": 0, }, + "output_size": undefined, "total": 1, }, "csv_searchsource": Object { @@ -2420,7 +2800,8 @@ Object { "preserve_layout": 0, "print": 0, }, - "total": 0, + "output_size": undefined, + "total": 3, }, "csv_searchsource_immediate": Object { "app": Object { @@ -2454,7 +2835,7 @@ Object { "preserve_layout": 0, "print": 0, }, - "total": 1, + "total": 0, }, "PNGV2": Object { "app": Object { @@ -2472,7 +2853,7 @@ Object { }, "total": 0, }, - "_all": 4, + "_all": 0, "csv": Object { "app": Object { "canvas workpad": 0, @@ -2481,13 +2862,13 @@ Object { "visualization": 0, }, "available": true, - "deprecated": 1, + "deprecated": 0, "layout": Object { "canvas": 0, "preserve_layout": 0, "print": 0, }, - "total": 1, + "total": 0, }, "csv_searchsource": Object { "app": Object { @@ -2521,10 +2902,11 @@ Object { }, "total": 0, }, + "output_size": undefined, "printable_pdf": Object { "app": Object { - "canvas workpad": 1, - "dashboard": 1, + "canvas workpad": 0, + "dashboard": 0, "search": 0, "visualization": 0, }, @@ -2532,10 +2914,10 @@ Object { "deprecated": 0, "layout": Object { "canvas": 0, - "preserve_layout": 2, + "preserve_layout": 0, "print": 0, }, - "total": 2, + "total": 0, }, "printable_pdf_v2": Object { "app": Object { @@ -2554,26 +2936,16 @@ Object { "total": 0, }, "status": Object { - "completed": 4, + "completed": 0, "failed": 0, }, - "statuses": Object { - "completed": Object { - "PNG": Object { - "dashboard": 1, - }, - "csv": Object {}, - "printable_pdf": Object { - "canvas workpad": 1, - "dashboard": 1, - }, - }, - }, + "statuses": Object {}, }, + "output_size": undefined, "printable_pdf": Object { "app": Object { - "canvas workpad": 1, - "dashboard": 1, + "canvas workpad": 0, + "dashboard": 6, "search": 0, "visualization": 0, }, @@ -2581,10 +2953,11 @@ Object { "deprecated": 0, "layout": Object { "canvas": 0, - "preserve_layout": 2, - "print": 0, + "preserve_layout": 5, + "print": 1, }, - "total": 2, + "output_size": undefined, + "total": 6, }, "printable_pdf_v2": Object { "app": Object { @@ -2603,17 +2976,38 @@ Object { "total": 0, }, "status": Object { - "completed": 4, - "failed": 0, + "completed": 6, + "completed_with_warnings": 2, + "failed": 2, + "pending": 1, }, "statuses": Object { "completed": Object { + "csv": Object { + "search": 1, + }, + "csv_searchsource": Object { + "search": 3, + }, + "printable_pdf": Object { + "dashboard": 2, + }, + }, + "completed_with_warnings": Object { "PNG": Object { "dashboard": 1, }, - "csv": Object {}, "printable_pdf": Object { - "canvas workpad": 1, + "dashboard": 1, + }, + }, + "failed": Object { + "printable_pdf": Object { + "dashboard": 2, + }, + }, + "pending": Object { + "printable_pdf": Object { "dashboard": 1, }, }, diff --git a/x-pack/plugins/reporting/server/usage/get_export_stats.test.ts b/x-pack/plugins/reporting/server/usage/get_export_stats.test.ts index 782f2e910038e..f74e176e6f21d 100644 --- a/x-pack/plugins/reporting/server/usage/get_export_stats.test.ts +++ b/x-pack/plugins/reporting/server/usage/get_export_stats.test.ts @@ -11,6 +11,15 @@ import { getExportTypesHandler } from './get_export_type_handler'; import { FeatureAvailabilityMap } from './types'; let featureMap: FeatureAvailabilityMap; +const sizesAggResponse = { + '1.0': 5093470.0, + '5.0': 5093470.0, + '25.0': 5093470.0, + '50.0': 8514532.0, + '75.0': 1.1935594e7, + '95.0': 1.1935594e7, + '99.0': 1.1935594e7, +}; beforeEach(() => { featureMap = { PNG: true, csv: true, csv_searchsource: true, printable_pdf: true }; @@ -67,14 +76,19 @@ test('Model of job status and status-by-pdf-app', () => { test('Model of jobTypes', () => { const result = getExportStats( { - PNG: { available: true, total: 3 }, + PNG: { available: true, total: 3, sizes: sizesAggResponse }, printable_pdf: { available: true, total: 3, + sizes: sizesAggResponse, app: { dashboard: 0, visualization: 0, 'canvas workpad': 3 }, layout: { preserve_layout: 3, print: 0 }, }, - csv_searchsource: { available: true, total: 3 }, + csv_searchsource: { + available: true, + total: 3, + sizes: sizesAggResponse, + }, }, featureMap, exportTypesHandler @@ -95,6 +109,15 @@ test('Model of jobTypes', () => { "preserve_layout": 0, "print": 0, }, + "output_size": Object { + "1.0": 5093470, + "25.0": 5093470, + "5.0": 5093470, + "50.0": 8514532, + "75.0": 11935594, + "95.0": 11935594, + "99.0": 11935594, + }, "total": 3, } `); @@ -131,6 +154,15 @@ test('Model of jobTypes', () => { "preserve_layout": 0, "print": 0, }, + "output_size": Object { + "1.0": 5093470, + "25.0": 5093470, + "5.0": 5093470, + "50.0": 8514532, + "75.0": 11935594, + "95.0": 11935594, + "99.0": 11935594, + }, "total": 3, } `); @@ -149,6 +181,15 @@ test('Model of jobTypes', () => { "preserve_layout": 3, "print": 0, }, + "output_size": Object { + "1.0": 5093470, + "25.0": 5093470, + "5.0": 5093470, + "50.0": 8514532, + "75.0": 11935594, + "95.0": 11935594, + "99.0": 11935594, + }, "total": 3, } `); @@ -156,7 +197,14 @@ test('Model of jobTypes', () => { test('PNG counts, provided count of deprecated jobs explicitly', () => { const result = getExportStats( - { PNG: { available: true, total: 15, deprecated: 5 } }, + { + PNG: { + available: true, + total: 15, + deprecated: 5, + sizes: sizesAggResponse, + }, + }, featureMap, exportTypesHandler ); @@ -175,6 +223,15 @@ test('PNG counts, provided count of deprecated jobs explicitly', () => { "preserve_layout": 0, "print": 0, }, + "output_size": Object { + "1.0": 5093470, + "25.0": 5093470, + "5.0": 5093470, + "50.0": 8514532, + "75.0": 11935594, + "95.0": 11935594, + "99.0": 11935594, + }, "total": 15, } `); @@ -182,7 +239,14 @@ test('PNG counts, provided count of deprecated jobs explicitly', () => { test('CSV counts, provides all jobs implicitly deprecated due to jobtype', () => { const result = getExportStats( - { csv: { available: true, total: 15, deprecated: 0 } }, + { + csv: { + available: true, + total: 15, + deprecated: 0, + sizes: sizesAggResponse, + }, + }, featureMap, exportTypesHandler ); @@ -201,6 +265,15 @@ test('CSV counts, provides all jobs implicitly deprecated due to jobtype', () => "preserve_layout": 0, "print": 0, }, + "output_size": Object { + "1.0": 5093470, + "25.0": 5093470, + "5.0": 5093470, + "50.0": 8514532, + "75.0": 11935594, + "95.0": 11935594, + "99.0": 11935594, + }, "total": 15, } `); diff --git a/x-pack/plugins/reporting/server/usage/get_export_stats.ts b/x-pack/plugins/reporting/server/usage/get_export_stats.ts index ffdb6cdc290d7..72c09f08017a1 100644 --- a/x-pack/plugins/reporting/server/usage/get_export_stats.ts +++ b/x-pack/plugins/reporting/server/usage/get_export_stats.ts @@ -33,6 +33,7 @@ function getAvailableTotalForFeature( available: isAvailable(featureAvailability, typeKey), total: jobType.total, deprecated, + output_size: jobType.sizes, app: { ...defaultTotalsForFeature.app, ...jobType.app }, layout: { ...defaultTotalsForFeature.layout, ...jobType.layout }, }; @@ -56,6 +57,7 @@ export const getExportStats = ( _all: rangeAll, status: rangeStatus, statuses: rangeStatusByApp, + output_size: outputSize, ...rangeStats } = rangeStatsInput; @@ -84,6 +86,7 @@ export const getExportStats = ( _all: rangeAll || 0, status: { completed: 0, failed: 0, ...rangeStatus }, statuses: rangeStatusByApp, + output_size: outputSize, } as RangeStats; return resultStats; diff --git a/x-pack/plugins/reporting/server/usage/get_reporting_usage.ts b/x-pack/plugins/reporting/server/usage/get_reporting_usage.ts index 9aba7841162c2..9a452943ff699 100644 --- a/x-pack/plugins/reporting/server/usage/get_reporting_usage.ts +++ b/x-pack/plugins/reporting/server/usage/get_reporting_usage.ts @@ -13,6 +13,7 @@ import type { GetLicense } from './'; import { getExportStats } from './get_export_stats'; import { getExportTypesHandler } from './get_export_type_handler'; import type { + AggregationBuckets, AggregationResultBuckets, AvailableTotal, FeatureAvailabilityMap, @@ -33,6 +34,8 @@ const OBJECT_TYPES_FIELD = 'meta.objectType.keyword'; const STATUS_TYPES_KEY = 'statusTypes'; const STATUS_BY_APP_KEY = 'statusByApp'; const STATUS_TYPES_FIELD = 'status'; +const OUTPUT_SIZES_KEY = 'sizes'; +const OUTPUT_SIZES_FIELD = 'output.size'; const DEFAULT_TERMS_SIZE = 10; const PRINTABLE_PDF_JOBTYPE = 'printable_pdf'; @@ -64,13 +67,14 @@ const getAppStatuses = (buckets: StatusByAppBucket[]) => }, {}); function getAggStats(aggs: AggregationResultBuckets): Partial { - const { buckets: jobBuckets } = aggs[JOB_TYPES_KEY]; + const { buckets: jobBuckets } = aggs[JOB_TYPES_KEY] as AggregationBuckets; const jobTypes = jobBuckets.reduce((accum: JobTypes, bucket) => { - const { key, doc_count: count, isDeprecated } = bucket; + const { key, doc_count: count, isDeprecated, sizes } = bucket; const deprecatedCount = isDeprecated?.doc_count; const total: Omit = { total: count, deprecated: deprecatedCount, + sizes: sizes?.values, }; return { ...accum, [key]: total }; }, {} as JobTypes); @@ -97,7 +101,13 @@ function getAggStats(aggs: AggregationResultBuckets): Partial { statusByApp = getAppStatuses(statusAppBuckets); } - return { _all: all, status: statusTypes, statuses: statusByApp, ...jobTypes }; + return { + _all: all, + status: statusTypes, + statuses: statusByApp, + output_size: get(aggs[OUTPUT_SIZES_KEY], 'values') ?? undefined, + ...jobTypes, + }; } type RangeStatSets = Partial & { @@ -135,7 +145,6 @@ export async function getReportingUsage( exportTypesRegistry: ExportTypesRegistry ): Promise { const reportingIndex = config.get('index'); - const params = { index: `${reportingIndex}-*`, filterPath: 'aggregations.*.buckets', @@ -152,8 +161,14 @@ export async function getReportingUsage( aggs: { [JOB_TYPES_KEY]: { terms: { field: JOB_TYPES_FIELD, size: DEFAULT_TERMS_SIZE }, - aggs: { isDeprecated: { filter: { term: { [OBJECT_TYPE_DEPRECATED_KEY]: true } } } }, + aggs: { + isDeprecated: { filter: { term: { [OBJECT_TYPE_DEPRECATED_KEY]: true } } }, + [OUTPUT_SIZES_KEY]: { + percentiles: { field: OUTPUT_SIZES_FIELD }, + }, + }, }, + [STATUS_TYPES_KEY]: { terms: { field: STATUS_TYPES_FIELD, size: DEFAULT_TERMS_SIZE } }, [STATUS_BY_APP_KEY]: { terms: { field: 'status', size: DEFAULT_TERMS_SIZE }, @@ -161,19 +176,24 @@ export async function getReportingUsage( jobTypes: { terms: { field: JOB_TYPES_FIELD, size: DEFAULT_TERMS_SIZE }, aggs: { - appNames: { terms: { field: OBJECT_TYPES_FIELD, size: DEFAULT_TERMS_SIZE } }, // NOTE Discover/CSV export is missing the 'meta.objectType' field, so Discover/CSV results are missing for this agg + appNames: { terms: { field: OBJECT_TYPES_FIELD, size: DEFAULT_TERMS_SIZE } }, }, }, }, }, [OBJECT_TYPES_KEY]: { filter: { term: { jobtype: PRINTABLE_PDF_JOBTYPE } }, - aggs: { pdf: { terms: { field: OBJECT_TYPES_FIELD, size: DEFAULT_TERMS_SIZE } } }, + aggs: { + pdf: { terms: { field: OBJECT_TYPES_FIELD, size: DEFAULT_TERMS_SIZE } }, + }, }, [LAYOUT_TYPES_KEY]: { filter: { term: { jobtype: PRINTABLE_PDF_JOBTYPE } }, aggs: { pdf: { terms: { field: LAYOUT_TYPES_FIELD, size: DEFAULT_TERMS_SIZE } } }, }, + [OUTPUT_SIZES_KEY]: { + percentiles: { field: OUTPUT_SIZES_FIELD }, + }, }, }, }, diff --git a/x-pack/plugins/reporting/server/usage/reporting_usage_collector.test.ts b/x-pack/plugins/reporting/server/usage/reporting_usage_collector.test.ts index e69e56d6272d5..447085810cfd0 100644 --- a/x-pack/plugins/reporting/server/usage/reporting_usage_collector.test.ts +++ b/x-pack/plugins/reporting/server/usage/reporting_usage_collector.test.ts @@ -18,7 +18,6 @@ import { getReportingUsageCollector, registerReportingUsageCollector, } from './reporting_usage_collector'; -import { SearchResponse } from './types'; const exportTypesRegistry = getExportTypesRegistry(); @@ -190,7 +189,7 @@ describe('data modeling', () => { beforeAll(async () => { mockCore = await createMockReportingCore(createMockConfigSchema()); }); - test('with normal looking usage data', async () => { + test('with usage data from the reporting/archived_reports es archive', async () => { const plugins = getPluginsMock(); const collector = getReportingUsageCollector( mockCore, @@ -202,39 +201,37 @@ describe('data modeling', () => { } ); collectorFetchContext = getMockFetchClients( - getResponseMock( - { + getResponseMock({ aggregations: { ranges: { + meta: {}, buckets: { all: { - doc_count: 12, - jobTypes: { buckets: [ { doc_count: 9, key: 'printable_pdf' }, { doc_count: 3, key: 'PNG' }, ], }, - layoutTypes: { doc_count: 9, pdf: { buckets: [{ doc_count: 9, key: 'preserve_layout' }] }, }, - objectTypes: { doc_count: 9, pdf: { buckets: [ { doc_count: 6, key: 'canvas workpad' }, { doc_count: 3, key: 'visualization' }, ], }, }, - statusByApp: { buckets: [ { doc_count: 10, jobTypes: { buckets: [ { appNames: { buckets: [ { doc_count: 6, key: 'canvas workpad' }, { doc_count: 3, key: 'visualization' }, ], }, doc_count: 9, key: 'printable_pdf', }, { appNames: { buckets: [{ doc_count: 1, key: 'visualization' }] }, doc_count: 1, key: 'PNG', }, ], }, key: 'completed', }, { doc_count: 1, jobTypes: { buckets: [ { appNames: { buckets: [{ doc_count: 1, key: 'dashboard' }] }, doc_count: 1, key: 'PNG', }, ], }, key: 'completed_with_warnings', }, { doc_count: 1, jobTypes: { buckets: [ { appNames: { buckets: [{ doc_count: 1, key: 'dashboard' }] }, doc_count: 1, key: 'PNG', }, ], }, key: 'failed', }, ], }, - statusTypes: { buckets: [ { doc_count: 10, key: 'completed' }, { doc_count: 1, key: 'completed_with_warnings' }, { doc_count: 1, key: 'failed' }, ], }, + doc_count: 11, + layoutTypes: { doc_count: 6, pdf: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [ { key: 'preserve_layout', doc_count: 5 }, { key: 'print', doc_count: 1 }, ] } }, + statusByApp: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [ { key: 'completed', doc_count: 6, jobTypes: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [ { key: 'csv_searchsource', doc_count: 3, appNames: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [ { key: 'search', doc_count: 3 }, ] } }, { key: 'printable_pdf', doc_count: 2, appNames: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [ { key: 'dashboard', doc_count: 2 }, ] } }, { key: 'csv', doc_count: 1, appNames: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [ { key: 'search', doc_count: 1 }, ] } }, ] } }, { key: 'completed_with_warnings', doc_count: 2, jobTypes: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [ { key: 'PNG', doc_count: 1, appNames: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [ { key: 'dashboard', doc_count: 1 }, ] } }, { key: 'printable_pdf', doc_count: 1, appNames: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [ { key: 'dashboard', doc_count: 1 }, ] } }, ] } }, { key: 'failed', doc_count: 2, jobTypes: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [ { key: 'printable_pdf', doc_count: 2, appNames: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [ { key: 'dashboard', doc_count: 2 }, ] } }, ] } }, { key: 'pending', doc_count: 1, jobTypes: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [ { key: 'printable_pdf', doc_count: 1, appNames: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [ { key: 'dashboard', doc_count: 1 }, ] } }, ] } }, ] }, + objectTypes: { doc_count: 6, pdf: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [ { key: 'dashboard', doc_count: 6 }, ] } }, + statusTypes: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [ { key: 'completed', doc_count: 6 }, { key: 'completed_with_warnings', doc_count: 2 }, { key: 'failed', doc_count: 2 }, { key: 'pending', doc_count: 1 }, ] }, + jobTypes: { meta: {}, doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [ { key: 'printable_pdf', doc_count: 6, isDeprecated: { meta: {}, doc_count: 0 }, sizeMax: { value: 1713303.0 }, sizeAvg: { value: 957215.0 }, sizeMin: { value: 43226.0 } }, { key: 'csv_searchsource', doc_count: 3, isDeprecated: { meta: {}, doc_count: 0 }, sizeMax: { value: 7557.0 }, sizeAvg: { value: 3684.6666666666665 }, sizeMin: { value: 204.0 } }, { key: 'PNG', doc_count: 1, isDeprecated: { meta: {}, doc_count: 0 }, sizeMax: { value: 37748.0 }, sizeAvg: { value: 37748.0 }, sizeMin: { value: 37748.0 } }, { key: 'csv', doc_count: 1, isDeprecated: { meta: {}, doc_count: 0 }, sizeMax: { value: 231.0 }, sizeAvg: { value: 231.0 }, sizeMin: { value: 231.0 } }, ] }, + sizeMax: { value: 1713303.0 }, + sizeMin: { value: 204.0 }, + sizeAvg: { value: 365084.75 }, }, last7Days: { - doc_count: 1, - jobTypes: { buckets: [{ doc_count: 1, key: 'PNG' }] }, - layoutTypes: { doc_count: 0, pdf: { buckets: [] } }, - objectTypes: { doc_count: 0, pdf: { buckets: [] } }, - statusByApp: { buckets: [ { doc_count: 1, jobTypes: { buckets: [ { appNames: { buckets: [{ doc_count: 1, key: 'dashboard' }] }, doc_count: 1, key: 'PNG', }, ], }, key: 'completed_with_warnings', }, ], }, - statusTypes: { buckets: [{ doc_count: 1, key: 'completed_with_warnings' }] }, - }, - lastDay: { - doc_count: 1, - jobTypes: { buckets: [{ doc_count: 1, key: 'PNG' }] }, - layoutTypes: { doc_count: 0, pdf: { buckets: [] } }, - objectTypes: { doc_count: 0, pdf: { buckets: [] } }, - statusByApp: { buckets: [ { doc_count: 1, jobTypes: { buckets: [ { appNames: { buckets: [{ doc_count: 1, key: 'dashboard' }] }, doc_count: 1, key: 'PNG', }, ], }, key: 'completed_with_warnings', }, ], }, - statusTypes: { buckets: [{ doc_count: 1, key: 'completed_with_warnings' }] }, + doc_count: 0, + layoutTypes: { doc_count: 0, pdf: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [] } }, + statusByApp: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [] }, + objectTypes: { doc_count: 0, pdf: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [] } }, + statusTypes: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [] }, + jobTypes: { meta: {}, doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [] }, + sizeMax: { value: null }, + sizeMin: { value: null }, + sizeAvg: { value: null }, }, }, - }, + }, // prettier-ignore }, - } as SearchResponse) // prettier-ignore + }) ); const usageStats = await collector.fetch(collectorFetchContext); expect(usageStats).toMatchSnapshot(); @@ -258,121 +255,21 @@ describe('data modeling', () => { buckets: { all: { doc_count: 9, - layoutTypes: { - doc_count: 0, - pdf: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [] }, - }, - statusByApp: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [ - { - key: 'completed', - doc_count: 9, - jobTypes: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [ - { - key: 'csv_searchsource', - doc_count: 5, - appNames: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [{ key: 'search', doc_count: 5 }], - }, - }, - { - key: 'csv', - doc_count: 4, - appNames: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [{ key: 'search', doc_count: 4 }], - }, - }, - ], - }, - }, - ], - }, - objectTypes: { - doc_count: 0, - pdf: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [] }, - }, - statusTypes: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [{ key: 'completed', doc_count: 9 }], - }, - jobTypes: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [ - { key: 'csv_searchsource', doc_count: 5, isDeprecated: { doc_count: 0 } }, - { key: 'csv', doc_count: 4, isDeprecated: { doc_count: 4 } }, - ], - }, + layoutTypes: { doc_count: 0, pdf: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [] } }, + statusByApp: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [ { key: 'completed', doc_count: 9, jobTypes: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [ { key: 'csv_searchsource', doc_count: 5, appNames: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [{ key: 'search', doc_count: 5 }] } }, { key: 'csv', doc_count: 4, appNames: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [{ key: 'search', doc_count: 4 }] } }, ] } }, ] }, + objectTypes: { doc_count: 0, pdf: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [] } }, + statusTypes: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [{ key: 'completed', doc_count: 9 }] }, + jobTypes: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [ { key: 'csv_searchsource', doc_count: 5, isDeprecated: { doc_count: 0 } }, { key: 'csv', doc_count: 4, isDeprecated: { doc_count: 4 } }, ] }, }, last7Days: { doc_count: 9, - layoutTypes: { - doc_count: 0, - pdf: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [] }, - }, - statusByApp: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [ - { - key: 'completed', - doc_count: 9, - jobTypes: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [ - { - key: 'csv_searchsource', - doc_count: 5, - appNames: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [{ key: 'search', doc_count: 5 }], - }, - }, - { - key: 'csv', - doc_count: 4, - appNames: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [{ key: 'search', doc_count: 4 }], - }, - }, - ], - }, - }, - ], - }, - objectTypes: { - doc_count: 0, - pdf: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [] }, - }, - statusTypes: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [{ key: 'completed', doc_count: 9 }], - }, - jobTypes: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [ - { key: 'csv_searchsource', doc_count: 5, isDeprecated: { doc_count: 0 } }, - { key: 'csv', doc_count: 4, isDeprecated: { doc_count: 4 } }, - ], - }, + layoutTypes: { doc_count: 0, pdf: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [] } }, + statusByApp: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [ { key: 'completed', doc_count: 9, jobTypes: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [ { key: 'csv_searchsource', doc_count: 5, appNames: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [{ key: 'search', doc_count: 5 }] } }, { key: 'csv', doc_count: 4, appNames: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [{ key: 'search', doc_count: 4 }] } }, ] } }, ] }, + objectTypes: { doc_count: 0, pdf: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [] } }, + statusTypes: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [{ key: 'completed', doc_count: 9 }] }, + jobTypes: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [ { key: 'csv_searchsource', doc_count: 5, isDeprecated: { doc_count: 0 } }, { key: 'csv', doc_count: 4, isDeprecated: { doc_count: 4 } }, ] }, }, - }, + }, // prettier-ignore }, }, }) @@ -393,39 +290,30 @@ describe('data modeling', () => { } ); collectorFetchContext = getMockFetchClients( - getResponseMock( - { + getResponseMock({ aggregations: { ranges: { buckets: { all: { doc_count: 4, - layoutTypes: { doc_count: 2, pdf: { buckets: [{ key: 'preserve_layout', doc_count: 2 }] }, }, - statusByApp: { buckets: [ { key: 'completed', doc_count: 4, jobTypes: { buckets: [ { key: 'printable_pdf', doc_count: 2, appNames: { buckets: [ { key: 'canvas workpad', doc_count: 1 }, { key: 'dashboard', doc_count: 1 }, ], }, }, { key: 'PNG', doc_count: 1, appNames: { buckets: [{ key: 'dashboard', doc_count: 1 }] }, }, { key: 'csv', doc_count: 1, appNames: { buckets: [] } }, ], }, }, ], }, - objectTypes: { doc_count: 2, pdf: { buckets: [ { key: 'canvas workpad', doc_count: 1 }, { key: 'dashboard', doc_count: 1 }, ], }, }, + layoutTypes: { doc_count: 2, pdf: { buckets: [{ key: 'preserve_layout', doc_count: 2 }] } }, + statusByApp: { buckets: [ { key: 'completed', doc_count: 4, jobTypes: { buckets: [ { key: 'printable_pdf', doc_count: 2, appNames: { buckets: [ { key: 'canvas workpad', doc_count: 1 }, { key: 'dashboard', doc_count: 1 }, ] } }, { key: 'PNG', doc_count: 1, appNames: { buckets: [{ key: 'dashboard', doc_count: 1 }] } }, { key: 'csv', doc_count: 1, appNames: { buckets: [] } }, ] } }, ] }, + objectTypes: { doc_count: 2, pdf: { buckets: [ { key: 'canvas workpad', doc_count: 1 }, { key: 'dashboard', doc_count: 1 }, ] } }, statusTypes: { buckets: [{ key: 'completed', doc_count: 4 }] }, - jobTypes: { buckets: [ { key: 'printable_pdf', doc_count: 2 }, { key: 'PNG', doc_count: 1 }, { key: 'csv', doc_count: 1 }, ], }, + jobTypes: { buckets: [ { key: 'printable_pdf', doc_count: 2 }, { key: 'PNG', doc_count: 1 }, { key: 'csv', doc_count: 1 }, ] }, }, last7Days: { doc_count: 4, - layoutTypes: { doc_count: 2, pdf: { buckets: [{ key: 'preserve_layout', doc_count: 2 }] }, }, - statusByApp: { buckets: [ { key: 'completed', doc_count: 4, jobTypes: { buckets: [ { key: 'printable_pdf', doc_count: 2, appNames: { buckets: [ { key: 'canvas workpad', doc_count: 1 }, { key: 'dashboard', doc_count: 1 }, ], }, }, { key: 'PNG', doc_count: 1, appNames: { buckets: [{ key: 'dashboard', doc_count: 1 }] }, }, { key: 'csv', doc_count: 1, appNames: { buckets: [] } }, ], }, }, ], }, - objectTypes: { doc_count: 2, pdf: { buckets: [ { key: 'canvas workpad', doc_count: 1 }, { key: 'dashboard', doc_count: 1 }, ], }, }, + layoutTypes: { doc_count: 2, pdf: { buckets: [{ key: 'preserve_layout', doc_count: 2 }] } }, + statusByApp: { buckets: [ { key: 'completed', doc_count: 4, jobTypes: { buckets: [ { key: 'printable_pdf', doc_count: 2, appNames: { buckets: [ { key: 'canvas workpad', doc_count: 1 }, { key: 'dashboard', doc_count: 1 }, ] } }, { key: 'PNG', doc_count: 1, appNames: { buckets: [{ key: 'dashboard', doc_count: 1 }] } }, { key: 'csv', doc_count: 1, appNames: { buckets: [] } }, ] } }, ] }, + objectTypes: { doc_count: 2, pdf: { buckets: [ { key: 'canvas workpad', doc_count: 1 }, { key: 'dashboard', doc_count: 1 }, ] } }, statusTypes: { buckets: [{ key: 'completed', doc_count: 4 }] }, - jobTypes: { buckets: [ { key: 'printable_pdf', doc_count: 2 }, { key: 'PNG', doc_count: 1 }, { key: 'csv', doc_count: 1 }, ], }, + jobTypes: { buckets: [ { key: 'printable_pdf', doc_count: 2 }, { key: 'PNG', doc_count: 1 }, { key: 'csv', doc_count: 1 }, ] }, }, - lastDay: { - doc_count: 4, - layoutTypes: { doc_count: 2, pdf: { buckets: [{ key: 'preserve_layout', doc_count: 2 }] }, }, - statusByApp: { buckets: [ { key: 'completed', doc_count: 4, jobTypes: { buckets: [ { key: 'printable_pdf', doc_count: 2, appNames: { buckets: [ { key: 'canvas workpad', doc_count: 1 }, { key: 'dashboard', doc_count: 1 }, ], }, }, { key: 'PNG', doc_count: 1, appNames: { buckets: [{ key: 'dashboard', doc_count: 1 }] }, }, { key: 'csv', doc_count: 1, appNames: { buckets: [] } }, ], }, }, ], }, - objectTypes: { doc_count: 2, pdf: { buckets: [ { key: 'canvas workpad', doc_count: 1 }, { key: 'dashboard', doc_count: 1 }, ], }, }, - statusTypes: { buckets: [{ key: 'completed', doc_count: 4 }] }, - jobTypes: { buckets: [ { key: 'printable_pdf', doc_count: 2 }, { key: 'PNG', doc_count: 1 }, { key: 'csv', doc_count: 1 }, ], }, - }, - }, + }, // prettier-ignore }, }, - } as SearchResponse) // prettier-ignore + }) ); const usageStats = await collector.fetch(collectorFetchContext); expect(usageStats).toMatchSnapshot(); @@ -445,9 +333,9 @@ describe('data modeling', () => { collectorFetchContext = getMockFetchClients( getResponseMock({ - aggregations: { - ranges: { - buckets: { + aggregations: { + ranges: { + buckets: { all: { doc_count: 0, jobTypes: { buckets: [] }, @@ -455,6 +343,9 @@ describe('data modeling', () => { objectTypes: { doc_count: 0, pdf: { buckets: [] } }, statusByApp: { buckets: [] }, statusTypes: { buckets: [] }, + sizeMax: { value: null}, + sizeMin: { value: null }, + sizeAvg: { value: null}, }, last7Days: { doc_count: 0, @@ -463,19 +354,15 @@ describe('data modeling', () => { objectTypes: { doc_count: 0, pdf: { buckets: [] } }, statusByApp: { buckets: [] }, statusTypes: { buckets: [] }, + sizeMax: { value: null}, + sizeMin: { value: null }, + sizeAvg: { value: null}, + }, - lastDay: { - doc_count: 0, - jobTypes: { buckets: [] }, - layoutTypes: { doc_count: 0, pdf: { buckets: [] } }, - objectTypes: { doc_count: 0, pdf: { buckets: [] } }, - statusByApp: { buckets: [] }, - statusTypes: { buckets: [] }, - }, + }, // prettier-ignore }, }, - }, - } as SearchResponse) // prettier-ignore + }) ); const usageStats = await collector.fetch(collectorFetchContext); expect(usageStats).toMatchSnapshot(); diff --git a/x-pack/plugins/reporting/server/usage/schema.ts b/x-pack/plugins/reporting/server/usage/schema.ts index 02bf65e7c5e4d..9580ddb935dfb 100644 --- a/x-pack/plugins/reporting/server/usage/schema.ts +++ b/x-pack/plugins/reporting/server/usage/schema.ts @@ -14,6 +14,7 @@ import { LayoutCounts, RangeStats, ReportingUsageType, + SizePercentiles, } from './types'; const appCountsSchema: MakeSchemaFrom = { @@ -39,10 +40,21 @@ const byAppCountsSchema: MakeSchemaFrom = { printable_pdf_v2: appCountsSchema, }; +const sizesSchema: MakeSchemaFrom = { + '1.0': { type: 'long' }, + '5.0': { type: 'long' }, + '25.0': { type: 'long' }, + '50.0': { type: 'long' }, + '75.0': { type: 'long' }, + '95.0': { type: 'long' }, + '99.0': { type: 'long' }, +}; + const availableTotalSchema: MakeSchemaFrom = { available: { type: 'boolean' }, total: { type: 'long' }, deprecated: { type: 'long' }, + sizes: sizesSchema, app: appCountsSchema, layout: layoutCountsSchema, }; @@ -74,6 +86,7 @@ const rangeStatsSchema: MakeSchemaFrom = { pending: byAppCountsSchema, processing: byAppCountsSchema, }, + output_size: sizesSchema, }; export const reportingSchema: MakeSchemaFrom = { diff --git a/x-pack/plugins/reporting/server/usage/types.ts b/x-pack/plugins/reporting/server/usage/types.ts index 7bd79de090b37..856d3ad10cb26 100644 --- a/x-pack/plugins/reporting/server/usage/types.ts +++ b/x-pack/plugins/reporting/server/usage/types.ts @@ -5,45 +5,57 @@ * 2.0. */ -export interface KeyCountBucket { - key: string; +export interface SizePercentiles { + '1.0': number | null; + '5.0': number | null; + '25.0': number | null; + '50.0': number | null; + '75.0': number | null; + '95.0': number | null; + '99.0': number | null; +} + +interface DocCount { doc_count: number; - isDeprecated?: { - doc_count: number; - }; +} + +interface SizeStats { + sizes?: { values: SizePercentiles }; +} + +export interface KeyCountBucket extends DocCount, SizeStats { + key: string; + isDeprecated?: DocCount; } export interface AggregationBuckets { buckets: KeyCountBucket[]; } -export interface StatusByAppBucket { +export interface StatusByAppBucket extends DocCount { key: string; - doc_count: number; jobTypes: { - buckets: Array<{ - doc_count: number; - key: string; - appNames: AggregationBuckets; - }>; + buckets: Array< + { + key: string; + appNames: AggregationBuckets; + } & DocCount + >; }; } -export interface AggregationResultBuckets { - jobTypes: AggregationBuckets; +export interface AggregationResultBuckets extends DocCount, SizeStats { + jobTypes?: AggregationBuckets; layoutTypes: { - doc_count: number; - pdf: AggregationBuckets; - }; + pdf?: AggregationBuckets; + } & DocCount; objectTypes: { - doc_count: number; - pdf: AggregationBuckets; - }; + pdf?: AggregationBuckets; + } & DocCount; statusTypes: AggregationBuckets; statusByApp: { buckets: StatusByAppBucket[]; }; - doc_count: number; } export interface SearchResponse { @@ -61,6 +73,7 @@ export interface AvailableTotal { available: boolean; total: number; deprecated?: number; + sizes?: SizePercentiles; app?: { search?: number; dashboard?: number; @@ -110,7 +123,8 @@ type StatusByAppCounts = { export type RangeStats = JobTypes & { _all: number; status: StatusCounts; - statuses: StatusByAppCounts; + statuses?: StatusByAppCounts; + output_size?: SizePercentiles; }; export type ReportingUsageType = RangeStats & { diff --git a/x-pack/plugins/security_solution/common/constants.ts b/x-pack/plugins/security_solution/common/constants.ts index 8076caf60f697..2e2dffa05c9fb 100644 --- a/x-pack/plugins/security_solution/common/constants.ts +++ b/x-pack/plugins/security_solution/common/constants.ts @@ -64,6 +64,7 @@ export const DEFAULT_INDICATOR_SOURCE_PATH = 'threatintel.indicator'; export const ENRICHMENT_DESTINATION_PATH = 'threat.enrichments'; export const DEFAULT_THREAT_INDEX_KEY = 'securitySolution:defaultThreatIndex'; export const DEFAULT_THREAT_INDEX_VALUE = ['filebeat-*']; +export const DEFAULT_THREAT_MATCH_QUERY = '@timestamp >= "now-30d"'; export enum SecurityPageName { administration = 'administration', @@ -197,7 +198,6 @@ export const EQL_RULE_TYPE_ID = `${RULE_TYPE_PREFIX}.eqlRule` as const; export const INDICATOR_RULE_TYPE_ID = `${RULE_TYPE_PREFIX}.indicatorRule` as const; export const ML_RULE_TYPE_ID = `${RULE_TYPE_PREFIX}.mlRule` as const; export const QUERY_RULE_TYPE_ID = `${RULE_TYPE_PREFIX}.queryRule` as const; -export const SAVED_QUERY_RULE_TYPE_ID = `${RULE_TYPE_PREFIX}.savedQueryRule` as const; export const THRESHOLD_RULE_TYPE_ID = `${RULE_TYPE_PREFIX}.thresholdRule` as const; /** diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_rules/indicator_match_rule.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_rules/indicator_match_rule.spec.ts index f8b3b426580b2..871e50821b58c 100644 --- a/x-pack/plugins/security_solution/cypress/integration/detection_rules/indicator_match_rule.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/detection_rules/indicator_match_rule.spec.ts @@ -108,6 +108,7 @@ import { loginAndWaitForPageWithoutDateRange } from '../../tasks/login'; import { goBackToAllRulesTable } from '../../tasks/rule_details'; import { ALERTS_URL, RULE_CREATION } from '../../urls/navigation'; +import { DEFAULT_THREAT_MATCH_QUERY } from '../../../common/constants'; describe('indicator match', () => { describe('Detection rules, Indicator Match', () => { @@ -180,8 +181,8 @@ describe('indicator match', () => { }); describe('custom indicator query input', () => { - it('Has a default set of *:*', () => { - getCustomIndicatorQueryInput().should('have.text', '*:*'); + it(`Has a default set of ${DEFAULT_THREAT_MATCH_QUERY}`, () => { + getCustomIndicatorQueryInput().should('have.text', DEFAULT_THREAT_MATCH_QUERY); }); it('Shows invalidation text if text is removed', () => { diff --git a/x-pack/plugins/security_solution/cypress/tasks/create_new_rule.ts b/x-pack/plugins/security_solution/cypress/tasks/create_new_rule.ts index c1210bf457b69..b7fb0785736f6 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/create_new_rule.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/create_new_rule.ts @@ -473,6 +473,7 @@ export const fillDefineIndicatorMatchRuleAndContinue = (rule: ThreatIndicatorRul indexField: rule.indicatorMappingField, indicatorIndexField: rule.indicatorIndexField, }); + getCustomIndicatorQueryInput().type('{selectall}{enter}*:*'); getDefineContinueButton().should('exist').click({ force: true }); cy.get(CUSTOM_QUERY_INPUT).should('not.exist'); }; diff --git a/x-pack/plugins/security_solution/public/common/experimental_features_service.ts b/x-pack/plugins/security_solution/public/common/experimental_features_service.ts new file mode 100644 index 0000000000000..813341f175408 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/experimental_features_service.ts @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ExperimentalFeatures } from '../../common/experimental_features'; + +export class ExperimentalFeaturesService { + private static experimentalFeatures?: ExperimentalFeatures; + + public static init({ experimentalFeatures }: { experimentalFeatures: ExperimentalFeatures }) { + this.experimentalFeatures = experimentalFeatures; + } + + public static get(): ExperimentalFeatures { + if (!this.experimentalFeatures) { + this.throwUninitializedError(); + } + + return this.experimentalFeatures; + } + + private static throwUninitializedError(): never { + throw new Error( + 'Experimental features services not initialized - are you trying to import this module from outside of the Security Solution app?' + ); + } +} diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/index.tsx index 3dfcc62e26a66..785afa49c9791 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/index.tsx @@ -11,7 +11,11 @@ import styled from 'styled-components'; import { isEqual } from 'lodash'; import { IndexPattern } from 'src/plugins/data/public'; -import { DEFAULT_INDEX_KEY, DEFAULT_THREAT_INDEX_KEY } from '../../../../../common/constants'; +import { + DEFAULT_INDEX_KEY, + DEFAULT_THREAT_INDEX_KEY, + DEFAULT_THREAT_MATCH_QUERY, +} from '../../../../../common/constants'; import { DEFAULT_TIMELINE_TITLE } from '../../../../timelines/components/timeline/translations'; import { isMlRule } from '../../../../../common/machine_learning/helpers'; import { hasMlAdminPermissions } from '../../../../../common/machine_learning/has_ml_admin_permissions'; @@ -72,7 +76,7 @@ const stepDefineDefaultValue: DefineStepRule = { saved_id: undefined, }, threatQueryBar: { - query: { query: '*:*', language: 'kuery' }, + query: { query: DEFAULT_THREAT_MATCH_QUERY, language: 'kuery' }, filters: [], saved_id: undefined, }, diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/components/search_bar.test.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/components/search_bar.test.tsx index 624a3c265c4c0..1cb21c7da1703 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/components/search_bar.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/components/search_bar.test.tsx @@ -92,7 +92,7 @@ describe('when rendering the endpoint list `AdminSearchBar`', () => { ])( 'should update the url and exclude the `admin_query` param when %s was entered', async (_, value) => { - await render(); + await render({ admin_query: "(language:kuery,query:'foo')" }); await submitQuery(value); expect(getQueryParamsFromStore().admin_query).toBe(undefined); diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/components/search_bar.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/components/search_bar.tsx index 2f2a1666b6f52..18d22e0cd1b15 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/components/search_bar.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/components/search_bar.tsx @@ -24,7 +24,7 @@ const AdminQueryBar = styled.div` export const AdminSearchBar = memo(() => { const history = useHistory(); - const queryParams = useEndpointSelector(selectors.uiQueryParams); + const { admin_query: _, ...queryParams } = useEndpointSelector(selectors.uiQueryParams); const searchBarIndexPatterns = useEndpointSelector(selectors.patterns); const searchBarQuery = useEndpointSelector(selectors.searchBarQuery); const clonedIndexPatterns = useMemo( diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/policy_response_friendly_names.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/policy_response_friendly_names.ts index a9c2676396e83..3551d00c50c73 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/policy_response_friendly_names.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/policy_response_friendly_names.ts @@ -243,6 +243,18 @@ responseMap.set( defaultMessage: 'Events', }) ); +responseMap.set( + 'memory_protection', + i18n.translate('xpack.securitySolution.endpoint.details.policyResponse.memory_protection', { + defaultMessage: 'Memory Threat', + }) +); +responseMap.set( + 'behavior_protection', + i18n.translate('xpack.securitySolution.endpoint.details.policyResponse.behavior_protection', { + defaultMessage: 'Malicious Behavior', + }) +); /** * Maps a server provided value to corresponding i18n'd string. diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/components/exception_items_summary.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/components/exception_items_summary.tsx index ed3d9967f318e..5f0c5cca0ad2c 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/components/exception_items_summary.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/components/exception_items_summary.tsx @@ -5,9 +5,10 @@ * 2.0. */ +import React, { FC, memo, useCallback } from 'react'; import { EuiBadge, EuiBadgeProps, EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; -import React, { FC, memo } from 'react'; import { i18n } from '@kbn/i18n'; +import styled from 'styled-components'; import { GetExceptionSummaryResponse } from '../../../../../../../../common/endpoint/types'; const SUMMARY_KEYS: Readonly> = [ @@ -36,46 +37,76 @@ const SUMMARY_LABELS: Readonly<{ [key in keyof GetExceptionSummaryResponse]: str ), }; +export const StyledEuiFlexGridGroup = styled(EuiFlexGroup)` + display: grid; + min-width: 240px; + grid-template-columns: 50% 50%; +`; + +const StyledEuiFlexGroup = styled(EuiFlexGroup)<{ + isSmall: boolean; +}>` + font-size: ${({ isSmall, theme }) => (isSmall ? theme.eui.euiFontSizeXS : 'innherit')}; + font-weight: ${({ isSmall }) => (isSmall ? '1px' : 'innherit')}; +`; + const CSS_BOLD: Readonly = { fontWeight: 'bold' }; interface ExceptionItemsSummaryProps { stats: GetExceptionSummaryResponse | undefined; + isSmall?: boolean; } -export const ExceptionItemsSummary = memo(({ stats }) => { - return ( - - {SUMMARY_KEYS.map((stat) => { - return ( - - - {SUMMARY_LABELS[stat]} - - - ); - })} - - ); -}); +export const ExceptionItemsSummary = memo( + ({ stats, isSmall = false }) => { + const getItem = useCallback( + (stat: keyof GetExceptionSummaryResponse) => ( + + + {SUMMARY_LABELS[stat]} + + + ), + [stats, isSmall] + ); + + return ( + + {SUMMARY_KEYS.map((stat) => getItem(stat))} + + ); + } +); ExceptionItemsSummary.displayName = 'ExceptionItemsSummary'; -const SummaryStat: FC<{ value: number; color?: EuiBadgeProps['color'] }> = memo( - ({ children, value, color, ...commonProps }) => { +const SummaryStat: FC<{ value: number; color?: EuiBadgeProps['color']; isSmall?: boolean }> = memo( + ({ children, value, color, isSmall = false, ...commonProps }) => { return ( - - + + {children} {value} - + ); } diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/components/fleet_event_filters_card.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/components/fleet_event_filters_card.tsx index 22e1c3a612eb7..41768f4be7d2e 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/components/fleet_event_filters_card.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/components/fleet_event_filters_card.tsx @@ -46,15 +46,17 @@ export const FleetEventFiltersCard = memo( setStats(summary); } } catch (error) { - toasts.addDanger( - i18n.translate( - 'xpack.securitySolution.endpoint.fleetCustomExtension.eventFiltersSummaryError', - { - defaultMessage: 'There was an error trying to fetch event filters stats: "{error}"', - values: { error }, - } - ) - ); + if (isMounted.current) { + toasts.addDanger( + i18n.translate( + 'xpack.securitySolution.endpoint.fleetCustomExtension.eventFiltersSummaryError', + { + defaultMessage: 'There was an error trying to fetch event filters stats: "{error}"', + values: { error }, + } + ) + ); + } } }; fetchStats(); @@ -78,12 +80,15 @@ export const FleetEventFiltersCard = memo( path: fleetPackageCustomUrlPath, }, ], - backButtonUrl: getAppUrl({ appId: INTEGRATIONS_PLUGIN_ID, path: fleetPackageCustomUrlPath }), + backButtonUrl: getAppUrl({ + appId: INTEGRATIONS_PLUGIN_ID, + path: fleetPackageCustomUrlPath, + }), }; }, [getAppUrl, pkgkey]); return ( - + diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/components/fleet_trusted_apps_card.test.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/components/fleet_trusted_apps_card.test.tsx index 22a7072caea02..aa4b36d548604 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/components/fleet_trusted_apps_card.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/components/fleet_trusted_apps_card.test.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { ThemeProvider } from 'styled-components'; import { I18nProvider } from '@kbn/i18n/react'; -import { FleetTrustedAppsCard } from './fleet_trusted_apps_card'; +import { FleetTrustedAppsCardWrapper } from './fleet_trusted_apps_card_wrapper'; import * as reactTestingLibrary from '@testing-library/react'; import { TrustedAppsHttpService } from '../../../../../trusted_apps/service'; import { useToasts } from '../../../../../../../common/lib/kibana'; @@ -67,7 +67,9 @@ describe('Fleet trusted apps card', () => { ); // @ts-ignore - const component = reactTestingLibrary.render(, { wrapper: Wrapper }); + const component = reactTestingLibrary.render(, { + wrapper: Wrapper, + }); try { // @ts-ignore await reactTestingLibrary.act(() => promise); diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/components/fleet_trusted_apps_card.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/components/fleet_trusted_apps_card.tsx index 4f10eceb6781c..08e8ec39dbaa8 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/components/fleet_trusted_apps_card.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/components/fleet_trusted_apps_card.tsx @@ -9,116 +9,87 @@ import React, { memo, useMemo, useState, useEffect, useRef } from 'react'; import { EuiPanel, EuiText } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { - PackageCustomExtensionComponentProps, - pagePathGetters, -} from '../../../../../../../../../fleet/public'; -import { getTrustedAppsListPath } from '../../../../../../common/routing'; -import { - ListPageRouteState, - GetExceptionSummaryResponse, -} from '../../../../../../../../common/endpoint/types'; -import { INTEGRATIONS_PLUGIN_ID } from '../../../../../../../../../fleet/common'; +import { GetExceptionSummaryResponse } from '../../../../../../../../common/endpoint/types'; -import { useAppUrl } from '../../../../../../../common/lib/kibana/hooks'; import { useKibana, useToasts } from '../../../../../../../common/lib/kibana'; -import { LinkWithIcon } from './link_with_icon'; import { ExceptionItemsSummary } from './exception_items_summary'; import { TrustedAppsHttpService } from '../../../../../trusted_apps/service'; import { StyledEuiFlexGridGroup, StyledEuiFlexGridItem } from './styled_components'; -export const FleetTrustedAppsCard = memo(({ pkgkey }) => { - const { getAppUrl } = useAppUrl(); - const { - services: { http }, - } = useKibana(); - const toasts = useToasts(); - const [stats, setStats] = useState(); - const trustedAppsApi = useMemo(() => new TrustedAppsHttpService(http), [http]); - const isMounted = useRef(); +interface FleetTrustedAppsCardProps { + customLink: React.ReactNode; + policyId?: string; + cardSize?: 'm' | 'l'; +} - useEffect(() => { - isMounted.current = true; - const fetchStats = async () => { - try { - const response = await trustedAppsApi.getTrustedAppsSummary(); - if (isMounted) { - setStats(response); +export const FleetTrustedAppsCard = memo( + ({ customLink, policyId, cardSize = 'l' }) => { + const { + services: { http }, + } = useKibana(); + const toasts = useToasts(); + const [stats, setStats] = useState(); + const trustedAppsApi = useMemo(() => new TrustedAppsHttpService(http), [http]); + const isMounted = useRef(); + + useEffect(() => { + isMounted.current = true; + const fetchStats = async () => { + try { + const response = await trustedAppsApi.getTrustedAppsSummary({ + kuery: policyId + ? `exception-list-agnostic.attributes.tags:"policy:${policyId}" OR exception-list-agnostic.attributes.tags:"policy:all"` + : undefined, + }); + if (isMounted) { + setStats(response); + } + } catch (error) { + if (isMounted.current) { + toasts.addDanger( + i18n.translate( + 'xpack.securitySolution.endpoint.fleetCustomExtension.trustedAppsSummaryError', + { + defaultMessage: + 'There was an error trying to fetch trusted apps stats: "{error}"', + values: { error }, + } + ) + ); + } } - } catch (error) { - toasts.addDanger( - i18n.translate( - 'xpack.securitySolution.endpoint.fleetCustomExtension.trustedAppsSummaryError', - { - defaultMessage: 'There was an error trying to fetch trusted apps stats: "{error}"', - values: { error }, - } - ) - ); - } - }; - fetchStats(); - return () => { - isMounted.current = false; - }; - }, [toasts, trustedAppsApi]); - const trustedAppsListUrlPath = getTrustedAppsListPath(); + }; + fetchStats(); + return () => { + isMounted.current = false; + }; + }, [toasts, trustedAppsApi, policyId]); - const trustedAppRouteState = useMemo(() => { - const fleetPackageCustomUrlPath = `#${ - pagePathGetters.integration_details_custom({ pkgkey })[1] - }`; + const getTitleMessage = () => ( + + ); - return { - backButtonLabel: i18n.translate( - 'xpack.securitySolution.endpoint.fleetCustomExtension.backButtonLabel', - { defaultMessage: 'Back to Endpoint Integration' } - ), - onBackButtonNavigateTo: [ - INTEGRATIONS_PLUGIN_ID, - { - path: fleetPackageCustomUrlPath, - }, - ], - backButtonUrl: getAppUrl({ appId: INTEGRATIONS_PLUGIN_ID, path: fleetPackageCustomUrlPath }), - }; - }, [getAppUrl, pkgkey]); - return ( - - - - -

    - -

    -
    -
    - - - - - <> - - - - - -
    -
    - ); -}); + return ( + + + + + {cardSize === 'l' ?

    {getTitleMessage()}

    :
    {getTitleMessage()}
    } +
    +
    + + + + + {customLink} + +
    +
    + ); + } +); FleetTrustedAppsCard.displayName = 'FleetTrustedAppsCard'; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/components/fleet_trusted_apps_card_wrapper.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/components/fleet_trusted_apps_card_wrapper.tsx new file mode 100644 index 0000000000000..5ac79a5dd5d5a --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/components/fleet_trusted_apps_card_wrapper.tsx @@ -0,0 +1,73 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { memo, useMemo } from 'react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { + PackageCustomExtensionComponentProps, + pagePathGetters, +} from '../../../../../../../../../fleet/public'; +import { getTrustedAppsListPath } from '../../../../../../common/routing'; +import { ListPageRouteState } from '../../../../../../../../common/endpoint/types'; +import { INTEGRATIONS_PLUGIN_ID } from '../../../../../../../../../fleet/common'; + +import { useAppUrl } from '../../../../../../../common/lib/kibana/hooks'; +import { LinkWithIcon } from './link_with_icon'; +import { FleetTrustedAppsCard } from './fleet_trusted_apps_card'; + +export const FleetTrustedAppsCardWrapper = memo( + ({ pkgkey }) => { + const { getAppUrl } = useAppUrl(); + const trustedAppsListUrlPath = getTrustedAppsListPath(); + + const trustedAppRouteState = useMemo(() => { + const fleetPackageCustomUrlPath = `#${ + pagePathGetters.integration_details_custom({ pkgkey })[1] + }`; + + return { + backButtonLabel: i18n.translate( + 'xpack.securitySolution.endpoint.fleetCustomExtension.backButtonLabel', + { defaultMessage: 'Back to Endpoint Integration' } + ), + onBackButtonNavigateTo: [ + INTEGRATIONS_PLUGIN_ID, + { + path: fleetPackageCustomUrlPath, + }, + ], + backButtonUrl: getAppUrl({ + appId: INTEGRATIONS_PLUGIN_ID, + path: fleetPackageCustomUrlPath, + }), + }; + }, [getAppUrl, pkgkey]); + + const customLink = useMemo( + () => ( + + + + ), + [getAppUrl, trustedAppRouteState, trustedAppsListUrlPath] + ); + return ; + } +); + +FleetTrustedAppsCardWrapper.displayName = 'FleetTrustedAppsCardWrapper'; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/components/link_with_icon.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/components/link_with_icon.tsx index 6600fcfddde0c..6aebb130eb896 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/components/link_with_icon.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/components/link_with_icon.tsx @@ -13,16 +13,23 @@ import { LinkToAppProps, } from '../../../../../../../common/components/endpoint/link_to_app'; -const LinkLabel = styled.span` +const LinkLabel = styled.span<{ + size?: 'm' | 'l'; +}>` display: inline-block; padding-right: ${(props) => props.theme.eui.paddingSizes.s}; + font-size: ${({ size, theme }) => (size === 'm' ? theme.eui.euiFontSizeXS : 'innherit')}; `; -export const LinkWithIcon: FC = memo(({ children, ...props }) => { +type ComponentProps = LinkToAppProps & { + size?: 'm' | 'l'; +}; + +export const LinkWithIcon: FC = memo(({ children, size = 'l', ...props }) => { return ( - {children} - + {children} + ); }); diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/components/styled_components.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/components/styled_components.tsx index cb128946d8efa..d2d5de5d43a3f 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/components/styled_components.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/components/styled_components.tsx @@ -7,9 +7,12 @@ import styled from 'styled-components'; import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -export const StyledEuiFlexGridGroup = styled(EuiFlexGroup)` +export const StyledEuiFlexGridGroup = styled(EuiFlexGroup)<{ + cardSize?: 'm' | 'l'; +}>` display: grid; - grid-template-columns: 25% 45% 30%; + grid-template-columns: ${({ cardSize = 'l' }) => + cardSize === 'l' ? '25% 45% 30%' : '30% 35% 35%'}; grid-template-areas: 'title summary link'; `; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/index.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/index.tsx index 094f1131d7034..0748a95f63c9f 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/index.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/index.tsx @@ -8,14 +8,14 @@ import { EuiSpacer } from '@elastic/eui'; import React, { memo } from 'react'; import { PackageCustomExtensionComponentProps } from '../../../../../../../../fleet/public'; -import { FleetTrustedAppsCard } from './components/fleet_trusted_apps_card'; +import { FleetTrustedAppsCardWrapper } from './components/fleet_trusted_apps_card_wrapper'; import { FleetEventFiltersCard } from './components/fleet_event_filters_card'; export const EndpointPackageCustomExtension = memo( (props) => { return (
    - +
    diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_policy_edit_extension.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_policy_edit_extension.tsx index fe321e6a321c2..0a912598c5722 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_policy_edit_extension.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_policy_edit_extension.tsx @@ -5,19 +5,27 @@ * 2.0. */ -import React, { memo, useEffect, useState } from 'react'; -import { EuiSpacer } from '@elastic/eui'; +import React, { memo, useEffect, useState, useMemo } from 'react'; +import { EuiSpacer, EuiText } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; import { useDispatch } from 'react-redux'; import { PackagePolicyEditExtensionComponentProps, NewPackagePolicy, + pagePathGetters, } from '../../../../../../../fleet/public'; -import { getPolicyDetailPath } from '../../../../common/routing'; +import { useIsExperimentalFeatureEnabled } from '../../../../../common/hooks/use_experimental_features'; +import { INTEGRATIONS_PLUGIN_ID } from '../../../../../../../fleet/common'; +import { useAppUrl } from '../../../../../common/lib/kibana/hooks'; +import { PolicyDetailsRouteState } from '../../../../../../common/endpoint/types'; +import { getPolicyDetailPath, getPolicyTrustedAppsPath } from '../../../../common/routing'; import { PolicyDetailsForm } from '../policy_details_form'; import { AppAction } from '../../../../../common/store/actions'; import { usePolicyDetailsSelector } from '../policy_hooks'; import { policyDetailsForUpdate } from '../../store/policy_details/selectors'; - +import { FleetTrustedAppsCard } from './endpoint_package_custom_extension/components/fleet_trusted_apps_card'; +import { LinkWithIcon } from './endpoint_package_custom_extension/components/link_with_icon'; /** * Exports Endpoint-specific package policy instructions * for use in the Ingest app create / edit package policy @@ -40,7 +48,12 @@ const WrappedPolicyDetailsForm = memo<{ }>(({ policyId, onChange }) => { const dispatch = useDispatch<(a: AppAction) => void>(); const updatedPolicy = usePolicyDetailsSelector(policyDetailsForUpdate); + const { getAppUrl } = useAppUrl(); const [, setLastUpdatedPolicy] = useState(updatedPolicy); + // TODO: Remove this and related code when removing FF + const isTrustedAppsByPolicyEnabled = useIsExperimentalFeatureEnabled( + 'trustedAppsByPolicyEnabled' + ); // When the form is initially displayed, trigger the Redux middleware which is based on // the location information stored via the `userChangedUrl` action. @@ -93,9 +106,91 @@ const WrappedPolicyDetailsForm = memo<{ }); }, [onChange, updatedPolicy]); + const policyTrustedAppsPath = useMemo(() => getPolicyTrustedAppsPath(policyId), [policyId]); + const policyTrustedAppRouteState = useMemo(() => { + const fleetPackageIntegrationCustomUrlPath = `#${ + pagePathGetters.integration_policy_edit({ packagePolicyId: policyId })[1] + }`; + + return { + backLink: { + label: i18n.translate( + 'xpack.securitySolution.endpoint.fleetCustomExtension.artifacts.backButtonLabel', + { + defaultMessage: `Back to Fleet integration policy`, + } + ), + navigateTo: [ + INTEGRATIONS_PLUGIN_ID, + { + path: fleetPackageIntegrationCustomUrlPath, + }, + ], + href: getAppUrl({ + appId: INTEGRATIONS_PLUGIN_ID, + path: fleetPackageIntegrationCustomUrlPath, + }), + }, + }; + }, [getAppUrl, policyId]); + + const policyTrustedAppsLink = useMemo( + () => ( + + + + ), + [getAppUrl, policyTrustedAppsPath, policyTrustedAppRouteState] + ); + return (
    - + {isTrustedAppsByPolicyEnabled ? ( + <> +
    + +
    + +
    +
    + + +
    + +
    + +
    + +
    +
    + + +
    + + ) : ( + + )}
    ); }); diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/with_security_context.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/with_security_context.tsx index b657dfc74bdbc..1135a29759315 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/with_security_context.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/with_security_context.tsx @@ -13,6 +13,8 @@ import { CurrentLicense } from '../../../../../common/components/current_license import { StartPlugins } from '../../../../../types'; import { managementReducer } from '../../../../store/reducer'; import { managementMiddlewareFactory } from '../../../../store/middleware'; +import { appReducer } from '../../../../../common/store/app'; +import { ExperimentalFeaturesService } from '../../../../../common/experimental_features_service'; type ComposeType = typeof compose; declare global { @@ -51,8 +53,15 @@ export const withSecurityContext =

    ({ store = createStore( combineReducers({ management: managementReducer, + app: appReducer, }), - { management: undefined }, + { + management: undefined, + // @ts-ignore ignore this error as we just need the enableExperimental and it's temporary + app: { + enableExperimental: ExperimentalFeaturesService.get(), + }, + }, composeEnhancers(applyMiddleware(...managementMiddlewareFactory(coreStart, depsStart))) ); } diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details_form.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details_form.tsx index c9d1b3b7882a0..ed6a33166ff59 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details_form.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details_form.tsx @@ -29,14 +29,14 @@ const LOCKED_CARD_RAMSOMWARE_TITLE = i18n.translate( const LOCKED_CARD_MEMORY_TITLE = i18n.translate( 'xpack.securitySolution.endpoint.policy.details.memory', { - defaultMessage: 'Memory', + defaultMessage: 'Memory Threat', } ); const LOCKED_CARD_BEHAVIOR_TITLE = i18n.translate( 'xpack.securitySolution.endpoint.policy.details.behavior', { - defaultMessage: 'Behavior', + defaultMessage: 'Malicious Behavior', } ); diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/service/index.ts b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/service/index.ts index 9d39ecd05ad8a..c643094e61126 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/service/index.ts +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/service/index.ts @@ -28,6 +28,7 @@ import { PutTrustedAppsRequestParams, GetOneTrustedAppRequestParams, GetOneTrustedAppResponse, + GetTrustedAppsSummaryRequest, } from '../../../../../common/endpoint/types/trusted_apps'; import { resolvePathVariables } from '../../../../common/utils/resolve_path_variables'; @@ -82,8 +83,10 @@ export class TrustedAppsHttpService implements TrustedAppsService { ); } - async getTrustedAppsSummary() { - return this.http.get(TRUSTED_APPS_SUMMARY_API); + async getTrustedAppsSummary(request: GetTrustedAppsSummaryRequest) { + return this.http.get(TRUSTED_APPS_SUMMARY_API, { + query: request, + }); } getPolicyList(options?: Parameters[1]) { diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/create_trusted_app_flyout.tsx b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/create_trusted_app_flyout.tsx index 62bdc446ddb9e..049ab5884b179 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/create_trusted_app_flyout.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/create_trusted_app_flyout.tsx @@ -171,11 +171,21 @@ export const CreateTrustedAppFlyout = memo( + +

    + {i18n.translate('xpack.securitySolution.trustedApps.detailsSectionTitle', { + defaultMessage: 'Details', + })} +

    +
    + {!isEditMode && ( - -

    {ABOUT_TRUSTED_APPS}

    + <> + +

    {ABOUT_TRUSTED_APPS}

    +
    -
    + )} { const getOsField = (dataTestSub: string = dataTestSubjForForm): HTMLButtonElement => { return renderResult.getByTestId(`${dataTestSub}-osSelectField`) as HTMLButtonElement; }; - const getGlobalSwitchField = (dataTestSub: string = dataTestSubjForForm): HTMLButtonElement => { - return renderResult.getByTestId( - `${dataTestSub}-effectedPolicies-globalSwitch` - ) as HTMLButtonElement; - }; const getDescriptionField = (dataTestSub: string = dataTestSubjForForm): HTMLTextAreaElement => { return renderResult.getByTestId(`${dataTestSub}-descriptionField`) as HTMLTextAreaElement; }; @@ -252,55 +247,50 @@ describe('When using the Trusted App Form', () => { }); describe('the Policy Selection area', () => { - it('should show loader when setting `policies.isLoading` to true', () => { + beforeEach(() => { + const policy = generator.generatePolicyPackagePolicy(); + policy.name = 'test policy A'; + policy.id = '123'; + + formProps.policies.options = [policy]; + }); + + it('should have `global` switch on if effective scope is global and policy options hidden', () => { + render(); + const globalButton = renderResult.getByTestId( + `${dataTestSubjForForm}-effectedPolicies-global` + ) as HTMLButtonElement; + + expect(globalButton.classList.contains('euiButtonGroupButton-isSelected')).toEqual(true); + expect(renderResult.queryByTestId('policy-123')).toBeNull(); + }); + + it('should have policy options visible and specific policies checked if scope is per-policy', () => { + (formProps.trustedApp as NewTrustedApp).effectScope = { + type: 'policy', + policies: ['123'], + }; + render(); + const perPolicyButton = renderResult.getByTestId( + `${dataTestSubjForForm}-effectedPolicies-perPolicy` + ) as HTMLButtonElement; + + expect(perPolicyButton.classList.contains('euiButtonGroupButton-isSelected')).toEqual(true); + expect(renderResult.getByTestId('policy-123').getAttribute('aria-disabled')).toEqual('false'); + expect(renderResult.getByTestId('policy-123').getAttribute('aria-selected')).toEqual('true'); + }); + it('should show loader when setting `policies.isLoading` to true and scope is per-policy', () => { formProps.policies.isLoading = true; + (formProps.trustedApp as NewTrustedApp).effectScope = { + type: 'policy', + policies: ['123'], + }; render(); expect( renderResult.getByTestId(`${dataTestSubjForForm}-effectedPolicies-policiesSelectable`) .textContent ).toEqual('Loading options'); }); - - describe('and policies exist', () => { - beforeEach(() => { - const policy = generator.generatePolicyPackagePolicy(); - policy.name = 'test policy A'; - policy.id = '123'; - - formProps.policies.options = [policy]; - }); - - it('should display the policies available, but disabled if ', () => { - render(); - expect(renderResult.getByTestId('policy-123')); - }); - - it('should have `global` switch on if effective scope is global and policy options disabled', () => { - render(); - expect(getGlobalSwitchField().getAttribute('aria-checked')).toEqual('true'); - expect(renderResult.getByTestId('policy-123').getAttribute('aria-disabled')).toEqual( - 'true' - ); - expect(renderResult.getByTestId('policy-123').getAttribute('aria-selected')).toEqual( - 'false' - ); - }); - - it('should have specific policies checked if scope is per-policy', () => { - (formProps.trustedApp as NewTrustedApp).effectScope = { - type: 'policy', - policies: ['123'], - }; - render(); - expect(getGlobalSwitchField().getAttribute('aria-checked')).toEqual('false'); - expect(renderResult.getByTestId('policy-123').getAttribute('aria-disabled')).toEqual( - 'false' - ); - expect(renderResult.getByTestId('policy-123').getAttribute('aria-selected')).toEqual( - 'true' - ); - }); - }); }); describe('the Policy Selection area under feature flag', () => { diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/create_trusted_app_form.tsx b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/create_trusted_app_form.tsx index f9b83fd69a75e..5db9a8557fa10 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/create_trusted_app_form.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/create_trusted_app_form.tsx @@ -14,6 +14,8 @@ import { EuiSuperSelect, EuiSuperSelectOption, EuiTextArea, + EuiText, + EuiSpacer, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { EuiFormProps } from '@elastic/eui/src/components/form/form'; @@ -458,6 +460,41 @@ export const CreateTrustedAppForm = memo( data-test-subj={getTestId('nameTextField')} />
    + + + + + +

    + {i18n.translate('xpack.securitySolution.trustedApps.conditionsSectionTitle', { + defaultMessage: 'Conditions', + })} +

    +
    + + +

    + {i18n.translate('xpack.securitySolution.trustedApps.conditionsSectionDescription', { + defaultMessage: + 'Select an operating system and add conditions. Availability of conditions may depend on your chosen OS.', + })} +

    +
    + ( data-test-subj={getTestId('conditionsBuilder')} /> - - - - {isTrustedAppsByPolicyEnabled ? ( <> diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/effected_policy_select/effected_policy_select.test.tsx b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/effected_policy_select/effected_policy_select.test.tsx index 427d880444d39..4837a816d0ed8 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/effected_policy_select/effected_policy_select.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/effected_policy_select/effected_policy_select.test.tsx @@ -56,7 +56,7 @@ describe('when using EffectedPolicySelect component', () => { describe('and no policy entries exist', () => { it('should display no options available message', () => { - const { getByTestId } = render(); + const { getByTestId } = render({ isGlobal: false }); expect(getByTestId('test-policiesSelectable').textContent).toEqual('No options available'); }); }); @@ -65,9 +65,15 @@ describe('when using EffectedPolicySelect component', () => { const policyId = 'abc123'; const policyTestSubj = `policy-${policyId}`; - const toggleGlobalSwitch = () => { + const selectGlobalPolicy = () => { act(() => { - fireEvent.click(renderResult.getByTestId('test-globalSwitch')); + fireEvent.click(renderResult.getByTestId('globalPolicy')); + }); + }; + + const selectPerPolicy = () => { + act(() => { + fireEvent.click(renderResult.getByTestId('perPolicy')); }); }; @@ -97,59 +103,41 @@ describe('when using EffectedPolicySelect component', () => { }); it('should display policies', () => { - const { getByTestId } = render(); + const { getByTestId } = render({ isGlobal: false }); expect(getByTestId(policyTestSubj)); }); - it('should disable policy items if global is checked', () => { - const { getByTestId } = render(); - expect(getByTestId(policyTestSubj).getAttribute('aria-disabled')).toEqual('true'); + it('should hide policy items if global is checked', () => { + const { queryByTestId } = render({ isGlobal: true }); + expect(queryByTestId(policyTestSubj)).toBeNull(); }); it('should enable policy items if global is unchecked', async () => { - const { getByTestId } = render(); - toggleGlobalSwitch(); + const { getByTestId } = render({ isGlobal: false }); + selectPerPolicy(); expect(getByTestId(policyTestSubj).getAttribute('aria-disabled')).toEqual('false'); }); it('should call onChange with selection when global is toggled', () => { render(); - toggleGlobalSwitch(); + selectPerPolicy(); expect(handleOnChange.mock.calls[0][0]).toEqual({ isGlobal: false, selected: [], }); - toggleGlobalSwitch(); + selectGlobalPolicy(); expect(handleOnChange.mock.calls[1][0]).toEqual({ isGlobal: true, selected: [], }); }); - it('should not allow clicking on policies when global is true', () => { - render(); - - clickOnPolicy(); - expect(handleOnChange.mock.calls.length).toBe(0); - - // Select a Policy, then switch back to global and try to click the policy again (should be disabled and trigger onChange()) - toggleGlobalSwitch(); - clickOnPolicy(); - toggleGlobalSwitch(); - clickOnPolicy(); - expect(handleOnChange.mock.calls.length).toBe(3); - expect(handleOnChange.mock.calls[2][0]).toEqual({ - isGlobal: true, - selected: [componentProps.options[0]], - }); - }); - - it('should maintain policies selection even if global was checked', () => { + it('should maintain policies selection even if global was checked, and user switched back to per policy', () => { render(); - toggleGlobalSwitch(); + selectPerPolicy(); clickOnPolicy(); expect(handleOnChange.mock.calls[1][0]).toEqual({ isGlobal: false, @@ -157,7 +145,7 @@ describe('when using EffectedPolicySelect component', () => { }); // Toggle isGlobal back to True - toggleGlobalSwitch(); + selectGlobalPolicy(); expect(handleOnChange.mock.calls[2][0]).toEqual({ isGlobal: true, selected: [componentProps.options[0]], diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/effected_policy_select/effected_policy_select.tsx b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/effected_policy_select/effected_policy_select.tsx index 99db45c0e4b84..bb620ee5e7c01 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/effected_policy_select/effected_policy_select.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/effected_policy_select/effected_policy_select.tsx @@ -7,12 +7,15 @@ import React, { memo, useCallback, useMemo } from 'react'; import { + EuiButtonGroup, + EuiButtonGroupOptionProps, EuiCheckbox, + EuiFlexGroup, + EuiFlexItem, EuiFormRow, EuiSelectable, EuiSelectableProps, - EuiSwitch, - EuiSwitchProps, + EuiSpacer, EuiText, htmlIdGenerator, } from '@elastic/eui'; @@ -70,6 +73,28 @@ export const EffectedPolicySelect = memo( const getTestId = useTestIdGenerator(dataTestSubj); + const toggleGlobal: EuiButtonGroupOptionProps[] = useMemo( + () => [ + { + id: 'globalPolicy', + label: i18n.translate('xpack.securitySolution.endpoint.trustedAppsByPolicy.global', { + defaultMessage: 'Global', + }), + iconType: isGlobal ? 'checkInCircleFilled' : '', + 'data-test-subj': getTestId('global'), + }, + { + id: 'perPolicy', + label: i18n.translate('xpack.securitySolution.endpoint.trustedAppsByPolicy.perPolicy', { + defaultMessage: 'Per Policy', + }), + iconType: !isGlobal ? 'checkInCircleFilled' : '', + 'data-test-subj': getTestId('perPolicy'), + }, + ], + [getTestId, isGlobal] + ); + const selectableOptions: EffectedPolicyOption[] = useMemo(() => { const isPolicySelected = new Set(selected.map((policy) => policy.id)); @@ -117,10 +142,10 @@ export const EffectedPolicySelect = memo( [isGlobal, onChange] )!; - const handleGlobalSwitchChange: EuiSwitchProps['onChange'] = useCallback( - ({ target: { checked } }) => { + const handleGlobalButtonChange = useCallback( + (selectedId) => { onChange({ - isGlobal: checked, + isGlobal: selectedId === 'globalPolicy', selected, }); }, @@ -138,48 +163,54 @@ export const EffectedPolicySelect = memo( return ( - +

    + +

    + + + + -

    - -

    +

    + {i18n.translate('xpack.securitySolution.trustedApps.assignmentSectionDescription', { + defaultMessage: + 'You can assign this trusted application globally across all policies or assign it to specific policies.', + })} +

    - } - > - -
    - - - {...otherSelectableProps} - options={selectableOptions} - listProps={listProps || DEFAULT_LIST_PROPS} - onChange={handleOnPolicySelectChange} - searchable={true} - data-test-subj={getTestId('policiesSelectable')} - > - {listBuilderCallback} - - + + + + + + + + + {!isGlobal && ( + + + {...otherSelectableProps} + options={selectableOptions} + listProps={listProps || DEFAULT_LIST_PROPS} + onChange={handleOnPolicySelectChange} + searchable={true} + data-test-subj={getTestId('policiesSelectable')} + > + {listBuilderCallback} + + + )}
    ); } diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.test.tsx b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.test.tsx index c696c4705912e..b9609fb43ada5 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.test.tsx @@ -430,8 +430,11 @@ describe('When on the Trusted Apps Page', () => { it('should have list of policies populated', async () => { useIsExperimentalFeatureEnabledMock.mockReturnValue(true); const resetEnv = forceHTMLElementOffsetWidth(); - const { getByTestId } = await renderAndClickAddButton(); - expect(getByTestId('policy-abc123')); + const renderResult = await renderAndClickAddButton(); + act(() => { + fireEvent.click(renderResult.getByTestId('perPolicy')); + }); + expect(renderResult.getByTestId('policy-abc123')); resetEnv(); }); diff --git a/x-pack/plugins/security_solution/public/plugin.tsx b/x-pack/plugins/security_solution/public/plugin.tsx index f8a2de61f5d6f..cd65808f28bce 100644 --- a/x-pack/plugins/security_solution/public/plugin.tsx +++ b/x-pack/plugins/security_solution/public/plugin.tsx @@ -53,6 +53,7 @@ import { import { SecurityAppStore } from './common/store/store'; import { licenseService } from './common/hooks/use_license'; import { SecuritySolutionUiConfigType } from './common/types'; +import { ExperimentalFeaturesService } from './common/experimental_features_service'; import { getLazyEndpointPolicyEditExtension } from './management/pages/policy/view/ingest_manager_integration/lazy_endpoint_policy_edit_extension'; import { LazyEndpointPolicyCreateExtension } from './management/pages/policy/view/ingest_manager_integration/lazy_endpoint_policy_create_extension'; @@ -184,6 +185,7 @@ export class Plugin implements IPlugin), + 'kibana.alert.depth': 1, + }, +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/bulk_create_factory.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/bulk_create_factory.ts index 5d6bc698adc56..af0a8a27f2b25 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/bulk_create_factory.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/bulk_create_factory.ts @@ -5,6 +5,8 @@ * 2.0. */ +import { ALERT_INSTANCE_ID } from '@kbn/rule-data-utils'; + import { performance } from 'perf_hooks'; import { countBy, isEmpty } from 'lodash'; @@ -64,11 +66,15 @@ export const bulkCreateFactory = ); const createdItems = wrappedDocs - .map((doc, index) => ({ - _id: response.body.items[index].index?._id ?? '', - _index: response.body.items[index].index?._index ?? '', - ...doc._source, - })) + .map((doc, index) => { + const responseIndex = response.body.items[index].index; + return { + _id: responseIndex?._id ?? '', + _index: responseIndex?._index ?? '', + [ALERT_INSTANCE_ID]: responseIndex?._id ?? '', + ...doc._source, + }; + }) .filter((_, index) => response.body.items[index].index?.status === 201); const createdItemsCount = createdItems.length; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/build_alert.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/build_alert.test.ts index d75458630dc75..70b17ab96ab00 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/build_alert.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/build_alert.test.ts @@ -102,7 +102,6 @@ describe('buildAlert', () => { status_date: '2020-02-22T16:47:50.047Z', last_success_at: '2020-02-22T16:47:50.047Z', last_success_message: 'succeeded', - output_index: '.siem-signals-default', max_signals: 100, risk_score: 55, risk_score_mapping: [], @@ -179,7 +178,6 @@ describe('buildAlert', () => { status_date: '2020-02-22T16:47:50.047Z', last_success_at: '2020-02-22T16:47:50.047Z', last_success_message: 'succeeded', - output_index: '.siem-signals-default', max_signals: 100, risk_score: 55, risk_score_mapping: [], diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/build_alert.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/build_alert.ts index 316c1365102d0..6bb14df48eac0 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/build_alert.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/build_alert.ts @@ -120,7 +120,7 @@ export const buildAlert = ( [] ); - const { id, ...mappedRule } = rule; + const { id, output_index: outputIndex, ...mappedRule } = rule; mappedRule.uuid = id; return { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/flatten_with_prefix.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/flatten_with_prefix.ts index d472dc5885e57..02f418a151888 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/flatten_with_prefix.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/flatten_with_prefix.ts @@ -5,16 +5,26 @@ * 2.0. */ +import { isPlainObject } from 'lodash'; import { SearchTypes } from '../../../../../../common/detection_engine/types'; export const flattenWithPrefix = ( prefix: string, - obj: Record + maybeObj: unknown ): Record => { - return Object.keys(obj).reduce((acc: Record, key) => { + if (maybeObj != null && isPlainObject(maybeObj)) { + return Object.keys(maybeObj as Record).reduce( + (acc: Record, key) => { + return { + ...acc, + ...flattenWithPrefix(`${prefix}.${key}`, (maybeObj as Record)[key]), + }; + }, + {} + ); + } else { return { - ...acc, - [`${prefix}.${key}`]: obj[key], + [prefix]: maybeObj as SearchTypes, }; - }, {}); + } }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/field_maps/alerts.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/field_maps/alerts.ts index 1c4b7f03fd73f..f21fc5b6ad393 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/field_maps/alerts.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/field_maps/alerts.ts @@ -43,11 +43,6 @@ export const alertsFieldMap: FieldMap = { array: false, required: true, }, - 'kibana.alert.group': { - type: 'object', - array: false, - required: false, - }, 'kibana.alert.group.id': { type: 'keyword', array: false, @@ -58,11 +53,6 @@ export const alertsFieldMap: FieldMap = { array: false, required: false, }, - 'kibana.alert.original_event': { - type: 'object', - array: false, - required: false, - }, 'kibana.alert.original_event.action': { type: 'keyword', array: false, @@ -198,81 +188,6 @@ export const alertsFieldMap: FieldMap = { array: false, required: false, }, - 'kibana.alert.threat': { - type: 'object', - array: false, - required: false, - }, - 'kibana.alert.threat.framework': { - type: 'keyword', - array: false, - required: true, - }, - 'kibana.alert.threat.tactic': { - type: 'object', - array: false, - required: true, - }, - 'kibana.alert.threat.tactic.id': { - type: 'keyword', - array: false, - required: true, - }, - 'kibana.alert.threat.tactic.name': { - type: 'keyword', - array: false, - required: true, - }, - 'kibana.alert.threat.tactic.reference': { - type: 'keyword', - array: false, - required: true, - }, - 'kibana.alert.threat.technique': { - type: 'object', - array: false, - required: true, - }, - 'kibana.alert.threat.technique.id': { - type: 'keyword', - array: false, - required: true, - }, - 'kibana.alert.threat.technique.name': { - type: 'keyword', - array: false, - required: true, - }, - 'kibana.alert.threat.technique.reference': { - type: 'keyword', - array: false, - required: true, - }, - 'kibana.alert.threat.technique.subtechnique': { - type: 'object', - array: false, - required: true, - }, - 'kibana.alert.threat.technique.subtechnique.id': { - type: 'keyword', - array: false, - required: true, - }, - 'kibana.alert.threat.technique.subtechnique.name': { - type: 'keyword', - array: false, - required: true, - }, - 'kibana.alert.threat.technique.subtechnique.reference': { - type: 'keyword', - array: false, - required: true, - }, - 'kibana.alert.threshold_result': { - type: 'object', - array: false, - required: false, - }, 'kibana.alert.threshold_result.cardinality': { type: 'object', array: false, @@ -300,7 +215,7 @@ export const alertsFieldMap: FieldMap = { }, 'kibana.alert.threshold_result.terms': { type: 'object', - array: false, + array: true, required: false, }, 'kibana.alert.threshold_result.terms.field': { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/field_maps/field_names.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/field_maps/field_names.ts index fb9e597a30448..68d08e08086a0 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/field_maps/field_names.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/field_maps/field_names.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { ALERT_NAMESPACE } from '@kbn/rule-data-utils'; +import { ALERT_NAMESPACE, ALERT_RULE_NAMESPACE } from '@kbn/rule-data-utils'; export const ALERT_ANCESTORS = `${ALERT_NAMESPACE}.ancestors` as const; export const ALERT_BUILDING_BLOCK_TYPE = `${ALERT_NAMESPACE}.building_block_type` as const; @@ -14,3 +14,6 @@ export const ALERT_GROUP_ID = `${ALERT_NAMESPACE}.group.id` as const; export const ALERT_GROUP_INDEX = `${ALERT_NAMESPACE}.group.index` as const; export const ALERT_ORIGINAL_EVENT = `${ALERT_NAMESPACE}.original_event` as const; export const ALERT_ORIGINAL_TIME = `${ALERT_NAMESPACE}.original_time` as const; + +const ALERT_RULE_THRESHOLD = `${ALERT_RULE_NAMESPACE}.threshold` as const; +export const ALERT_RULE_THRESHOLD_FIELD = `${ALERT_RULE_THRESHOLD}.field` as const; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/field_maps/rules.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/field_maps/rules.ts index 21405672fdf7f..87b55e092ec5d 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/field_maps/rules.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/field_maps/rules.ts @@ -11,6 +11,11 @@ export const rulesFieldMap = { array: false, required: false, }, + 'kibana.alert.rule.exceptions_list': { + type: 'object', + array: true, + required: false, + }, 'kibana.alert.rule.false_positives': { type: 'keyword', array: true, @@ -46,6 +51,56 @@ export const rulesFieldMap = { array: true, required: true, }, + 'kibana.alert.rule.threat.framework': { + type: 'keyword', + array: false, + required: true, + }, + 'kibana.alert.rule.threat.tactic.id': { + type: 'keyword', + array: false, + required: true, + }, + 'kibana.alert.rule.threat.tactic.name': { + type: 'keyword', + array: false, + required: true, + }, + 'kibana.alert.rule.threat.tactic.reference': { + type: 'keyword', + array: false, + required: true, + }, + 'kibana.alert.rule.threat.technique.id': { + type: 'keyword', + array: false, + required: true, + }, + 'kibana.alert.rule.threat.technique.name': { + type: 'keyword', + array: false, + required: true, + }, + 'kibana.alert.rule.threat.technique.reference': { + type: 'keyword', + array: false, + required: true, + }, + 'kibana.alert.rule.threat.technique.subtechnique.id': { + type: 'keyword', + array: false, + required: true, + }, + 'kibana.alert.rule.threat.technique.subtechnique.name': { + type: 'keyword', + array: false, + required: true, + }, + 'kibana.alert.rule.threat.technique.subtechnique.reference': { + type: 'keyword', + array: false, + required: true, + }, 'kibana.alert.rule.threat_filters': { type: 'keyword', array: true, @@ -91,11 +146,6 @@ export const rulesFieldMap = { array: true, required: false, }, - 'kibana.alert.rule.threshold': { - type: 'object', - array: true, - required: false, - }, 'kibana.alert.rule.threshold.field': { type: 'keyword', array: true, @@ -103,7 +153,7 @@ export const rulesFieldMap = { }, 'kibana.alert.rule.threshold.value': { type: 'float', // TODO: should be 'long' (eventually, after we stabilize) - array: true, + array: false, required: false, }, 'kibana.alert.rule.threshold.cardinality': { @@ -113,12 +163,12 @@ export const rulesFieldMap = { }, 'kibana.alert.rule.threshold.cardinality.field': { type: 'keyword', - array: true, + array: false, required: false, }, 'kibana.alert.rule.threshold.cardinality.value': { type: 'long', - array: true, + array: false, required: false, }, 'kibana.alert.rule.timeline_id': { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/index.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/index.ts index 39325cab2c762..1787a15588b51 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/index.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/index.ts @@ -7,5 +7,6 @@ export { createEqlAlertType } from './eql/create_eql_alert_type'; export { createIndicatorMatchAlertType } from './indicator_match/create_indicator_match_alert_type'; -export { createQueryAlertType } from './query/create_query_alert_type'; export { createMlAlertType } from './ml/create_ml_alert_type'; +export { createQueryAlertType } from './query/create_query_alert_type'; +export { createThresholdAlertType } from './threshold/create_threshold_alert_type'; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/query/create_query_alert_type.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/query/create_query_alert_type.test.ts index ed791af08890c..e45d8440386fe 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/query/create_query_alert_type.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/query/create_query_alert_type.test.ts @@ -24,7 +24,7 @@ jest.mock('../utils/get_list_client', () => ({ jest.mock('../../rule_execution_log/rule_execution_log_client'); -describe('Custom query alerts', () => { +describe('Custom Query Alerts', () => { it('does not send an alert when no events found', async () => { const { services, dependencies, executor } = createRuleTypeMocks(); const queryAlertType = createQueryAlertType({ diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/scripts/create_rule_threshold.sh b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/scripts/create_rule_threshold.sh new file mode 100644 index 0000000000000..47c5cb4eda2e9 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/scripts/create_rule_threshold.sh @@ -0,0 +1,63 @@ +#!/bin/sh +# +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License +# 2.0; you may not use this file except in compliance with the Elastic License +# 2.0. +# + +curl -X POST ${KIBANA_URL}${SPACE_URL}/api/alerts/alert \ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ + -H 'kbn-xsrf: true' \ + -H 'Content-Type: application/json' \ + --verbose \ + -d ' +{ + "params":{ + "author": [], + "description": "Basic threshold rule", + "exceptionsList": [], + "falsePositives": [], + "from": "now-300s", + "query": "*:*", + "immutable": false, + "index": ["*"], + "language": "kuery", + "maxSignals": 10, + "outputIndex": "", + "references": [], + "riskScore": 21, + "riskScoreMapping": [], + "ruleId": "52dec1ba-b779-469c-9667-6b0e865fb89a", + "severity": "low", + "severityMapping": [], + "threat": [], + "threshold": { + "field": ["source.ip"], + "value": 2, + "cardinality": [ + { + "field": "source.ip", + "value": 1 + } + ] + }, + "to": "now", + "type": "threshold", + "version": 1 + }, + "consumer":"alerts", + "alertTypeId":"siem.thresholdRule", + "schedule":{ + "interval":"1m" + }, + "actions":[], + "tags":[ + "custom", + "persistence" + ], + "notifyWhen":"onActionGroupChange", + "name":"Basic threshold rule" +}' + + diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/threshold/create_threshold_alert_type.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/threshold/create_threshold_alert_type.test.ts new file mode 100644 index 0000000000000..74435cb300472 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/threshold/create_threshold_alert_type.test.ts @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { allowedExperimentalValues } from '../../../../../common/experimental_features'; +import { createThresholdAlertType } from './create_threshold_alert_type'; +import { createRuleTypeMocks } from '../__mocks__/rule_type'; +import { getThresholdRuleParams } from '../../schemas/rule_schemas.mock'; + +jest.mock('../../rule_execution_log/rule_execution_log_client'); + +describe('Threshold Alerts', () => { + it('does not send an alert when no events found', async () => { + const params = getThresholdRuleParams(); + const { dependencies, executor } = createRuleTypeMocks('threshold', params); + const thresholdAlertTpe = createThresholdAlertType({ + experimentalFeatures: allowedExperimentalValues, + lists: dependencies.lists, + logger: dependencies.logger, + mergeStrategy: 'allFields', + ignoreFields: [], + ruleDataClient: dependencies.ruleDataClient, + ruleDataService: dependencies.ruleDataService, + version: '1.0.0', + }); + dependencies.alerting.registerType(thresholdAlertTpe); + + await executor({ params }); + expect(dependencies.ruleDataClient.getWriter).not.toBeCalled(); + }); +}); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/threshold/create_threshold_alert_type.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/threshold/create_threshold_alert_type.ts new file mode 100644 index 0000000000000..a503cf5aedbea --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/threshold/create_threshold_alert_type.ts @@ -0,0 +1,95 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { validateNonExact } from '@kbn/securitysolution-io-ts-utils'; + +import { PersistenceServices } from '../../../../../../rule_registry/server'; +import { THRESHOLD_RULE_TYPE_ID } from '../../../../../common/constants'; +import { thresholdRuleParams, ThresholdRuleParams } from '../../schemas/rule_schemas'; +import { thresholdExecutor } from '../../signals/executors/threshold'; +import { ThresholdAlertState } from '../../signals/types'; +import { createSecurityRuleTypeFactory } from '../create_security_rule_type_factory'; +import { CreateRuleOptions } from '../types'; + +export const createThresholdAlertType = (createOptions: CreateRuleOptions) => { + const { + experimentalFeatures, + lists, + logger, + mergeStrategy, + ignoreFields, + ruleDataClient, + version, + ruleDataService, + } = createOptions; + const createSecurityRuleType = createSecurityRuleTypeFactory({ + lists, + logger, + mergeStrategy, + ignoreFields, + ruleDataClient, + ruleDataService, + }); + return createSecurityRuleType({ + id: THRESHOLD_RULE_TYPE_ID, + name: 'Threshold Rule', + validate: { + params: { + validate: (object: unknown): ThresholdRuleParams => { + const [validated, errors] = validateNonExact(object, thresholdRuleParams); + if (errors != null) { + throw new Error(errors); + } + if (validated == null) { + throw new Error('Validation of rule params failed'); + } + return validated; + }, + }, + }, + actionGroups: [ + { + id: 'default', + name: 'Default', + }, + ], + defaultActionGroupId: 'default', + actionVariables: { + context: [{ name: 'server', description: 'the server' }], + }, + minimumLicenseRequired: 'basic', + isExportable: false, + producer: 'security-solution', + async executor(execOptions) { + const { + runOpts: { buildRuleMessage, bulkCreate, exceptionItems, rule, tuple, wrapHits }, + services, + startedAt, + state, + } = execOptions; + + // console.log(JSON.stringify(state)); + + const result = await thresholdExecutor({ + buildRuleMessage, + bulkCreate, + exceptionItems, + experimentalFeatures, + logger, + rule, + services, + startedAt, + state, + tuple, + version, + wrapHits, + }); + + return result; + }, + }); +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/find_rules.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/find_rules.test.ts index 0f7545c4df936..ebde1d0ad6df8 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/find_rules.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/find_rules.test.ts @@ -7,15 +7,19 @@ import { getFilter } from './find_rules'; import { + EQL_RULE_TYPE_ID, INDICATOR_RULE_TYPE_ID, ML_RULE_TYPE_ID, QUERY_RULE_TYPE_ID, + THRESHOLD_RULE_TYPE_ID, SIGNALS_ID, } from '../../../../common/constants'; -const allAlertTypeIds = `(alert.attributes.alertTypeId: ${ML_RULE_TYPE_ID} +const allAlertTypeIds = `(alert.attributes.alertTypeId: ${EQL_RULE_TYPE_ID} + OR alert.attributes.alertTypeId: ${ML_RULE_TYPE_ID} OR alert.attributes.alertTypeId: ${QUERY_RULE_TYPE_ID} - OR alert.attributes.alertTypeId: ${INDICATOR_RULE_TYPE_ID})`.replace(/[\n\r]/g, ''); + OR alert.attributes.alertTypeId: ${INDICATOR_RULE_TYPE_ID} + OR alert.attributes.alertTypeId: ${THRESHOLD_RULE_TYPE_ID})`.replace(/[\n\r]/g, ''); describe('find_rules', () => { const fullFilterTestCases: Array<[boolean, string]> = [ diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/schemas/rule_schemas.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/schemas/rule_schemas.ts index e9215084614c0..578d8c4926b69 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/schemas/rule_schemas.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/schemas/rule_schemas.ts @@ -69,6 +69,8 @@ import { INDICATOR_RULE_TYPE_ID, ML_RULE_TYPE_ID, QUERY_RULE_TYPE_ID, + EQL_RULE_TYPE_ID, + THRESHOLD_RULE_TYPE_ID, } from '../../../../common/constants'; const nonEqlLanguages = t.keyof({ kuery: null, lucene: null }); @@ -206,12 +208,11 @@ export const notifyWhen = t.union([ export const allRuleTypes = t.union([ t.literal(SIGNALS_ID), - // t.literal(EQL_RULE_TYPE_ID), + t.literal(EQL_RULE_TYPE_ID), t.literal(ML_RULE_TYPE_ID), t.literal(QUERY_RULE_TYPE_ID), - // t.literal(SAVED_QUERY_RULE_TYPE_ID), t.literal(INDICATOR_RULE_TYPE_ID), - // t.literal(THRESHOLD_RULE_TYPE_ID), + t.literal(THRESHOLD_RULE_TYPE_ID), ]); export type AllRuleTypes = t.TypeOf; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/threshold.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/threshold.test.ts index afcb3707591fc..5766390099e29 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/threshold.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/threshold.test.ts @@ -73,6 +73,7 @@ describe('threshold_executor', () => { exceptionItems, experimentalFeatures: allowedExperimentalValues, services: alertServices, + state: { initialized: true, signalHistory: {} }, version, logger, buildRuleMessage, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/threshold.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/threshold.ts index a6ea55797dc53..02cad1e8e508c 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/threshold.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/threshold.ts @@ -5,9 +5,12 @@ * 2.0. */ +import { SearchHit } from '@elastic/elasticsearch/api/types'; +import type { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types'; + import { Logger } from 'src/core/server'; import { SavedObject } from 'src/core/types'; -import type { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types'; + import { AlertInstanceContext, AlertInstanceState, @@ -28,6 +31,7 @@ import { BulkCreate, RuleRangeTuple, SearchAfterAndBulkCreateReturnType, + ThresholdAlertState, WrapHits, } from '../types'; import { @@ -37,6 +41,7 @@ import { } from '../utils'; import { BuildRuleMessage } from '../rule_messages'; import { ExperimentalFeatures } from '../../../../../common/experimental_features'; +import { buildThresholdSignalHistory } from '../threshold/build_signal_history'; export const thresholdExecutor = async ({ rule, @@ -48,6 +53,7 @@ export const thresholdExecutor = async ({ logger, buildRuleMessage, startedAt, + state, bulkCreate, wrapHits, }: { @@ -60,17 +66,48 @@ export const thresholdExecutor = async ({ logger: Logger; buildRuleMessage: BuildRuleMessage; startedAt: Date; + state: ThresholdAlertState; bulkCreate: BulkCreate; wrapHits: WrapHits; -}): Promise => { +}): Promise => { let result = createSearchAfterReturnType(); const ruleParams = rule.attributes.params; + + // Get state or build initial state (on upgrade) + const { signalHistory, searchErrors: previousSearchErrors } = state.initialized + ? { signalHistory: state.signalHistory, searchErrors: [] } + : await getThresholdSignalHistory({ + indexPattern: ['*'], // TODO: get outputIndex? + from: tuple.from.toISOString(), + to: tuple.to.toISOString(), + services, + logger, + ruleId: ruleParams.ruleId, + bucketByFields: ruleParams.threshold.field, + timestampOverride: ruleParams.timestampOverride, + buildRuleMessage, + }); + + if (!state.initialized) { + // Clean up any signal history that has fallen outside the window + const toDelete: string[] = []; + for (const [hash, entry] of Object.entries(signalHistory)) { + if (entry.lastSignalTimestamp < tuple.from.valueOf()) { + toDelete.push(hash); + } + } + for (const hash of toDelete) { + delete signalHistory[hash]; + } + } + if (hasLargeValueItem(exceptionItems)) { result.warningMessages.push( 'Exceptions that use "is in list" or "is not in list" operators are not applied to Threshold rules' ); result.warning = true; } + const inputIndex = await getInputIndex({ experimentalFeatures, services, @@ -78,21 +115,8 @@ export const thresholdExecutor = async ({ index: ruleParams.index, }); - const { thresholdSignalHistory, searchErrors: previousSearchErrors } = - await getThresholdSignalHistory({ - indexPattern: [ruleParams.outputIndex], - from: tuple.from.toISOString(), - to: tuple.to.toISOString(), - services, - logger, - ruleId: ruleParams.ruleId, - bucketByFields: ruleParams.threshold.field, - timestampOverride: ruleParams.timestampOverride, - buildRuleMessage, - }); - const bucketFilters = await getThresholdBucketFilters({ - thresholdSignalHistory, + signalHistory, timestampOverride: ruleParams.timestampOverride, }); @@ -134,7 +158,7 @@ export const thresholdExecutor = async ({ signalsIndex: ruleParams.outputIndex, startedAt, from: tuple.from.toDate(), - thresholdSignalHistory, + signalHistory, bulkCreate, wrapHits, }); @@ -154,5 +178,31 @@ export const thresholdExecutor = async ({ searchAfterTimes: [thresholdSearchDuration], }), ]); - return result; + + const createdAlerts = createdItems.map((alert) => { + const { _id, _index, ...source } = alert as { _id: string; _index: string }; + return { + _id, + _index, + _source: { + ...source, + }, + } as SearchHit; + }); + + const newSignalHistory = buildThresholdSignalHistory({ + alerts: createdAlerts, + }); + + return { + ...result, + state: { + ...state, + initialized: true, + signalHistory: { + ...signalHistory, + ...newSignalHistory, + }, + }, + }; }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts index 68d60f7757e4a..9a6c099ed1760 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts @@ -29,7 +29,7 @@ import { } from '../../../../common/detection_engine/utils'; import { SetupPlugins } from '../../../plugin'; import { getInputIndex } from './get_input_output_index'; -import { AlertAttributes, SignalRuleAlertTypeDefinition } from './types'; +import { AlertAttributes, SignalRuleAlertTypeDefinition, ThresholdAlertState } from './types'; import { getListsClient, getExceptions, @@ -125,6 +125,7 @@ export const signalRulesAlertType = ({ async executor({ previousStartedAt, startedAt, + state, alertId, services, params, @@ -316,6 +317,7 @@ export const signalRulesAlertType = ({ logger, buildRuleMessage, startedAt, + state: state as ThresholdAlertState, bulkCreate, wrapHits, }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_search_after.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_search_after.ts index 8957c5400d854..2b1d27fc2fcd0 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_search_after.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_search_after.ts @@ -74,9 +74,11 @@ export const singleSearchAfter = async ({ searchAfterQuery as estypes.SearchRequest ); const end = performance.now(); + const searchErrors = createErrorsFromShard({ errors: nextSearchAfterResult._shards.failures ?? [], }); + return { searchResult: nextSearchAfterResult, searchDuration: makeFloatString(end - start), diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/build_signal_history.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/build_signal_history.test.ts new file mode 100644 index 0000000000000..8362942af15b9 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/build_signal_history.test.ts @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ALERT_ORIGINAL_TIME } from '../../rule_types/field_maps/field_names'; +import { sampleThresholdAlert } from '../../rule_types/__mocks__/threshold'; +import { buildThresholdSignalHistory } from './build_signal_history'; + +describe('buildSignalHistory', () => { + it('builds a signal history from an alert', () => { + const signalHistory = buildThresholdSignalHistory({ alerts: [sampleThresholdAlert] }); + expect(signalHistory).toEqual({ + '7a75c5c2db61f57ec166c669cb8244b91f812f0b2f1d4f8afd528d4f8b4e199b': { + lastSignalTimestamp: Date.parse( + sampleThresholdAlert._source[ALERT_ORIGINAL_TIME] as string + ), + terms: [ + { + field: 'host.name', + value: 'garden-gnomes', + }, + { + field: 'source.ip', + value: '127.0.0.1', + }, + ], + }, + }); + }); +}); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/build_signal_history.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/build_signal_history.ts new file mode 100644 index 0000000000000..81b12d2d4f229 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/build_signal_history.ts @@ -0,0 +1,75 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { SearchHit } from '@elastic/elasticsearch/api/types'; +import { + ALERT_ORIGINAL_TIME, + ALERT_RULE_THRESHOLD_FIELD, +} from '../../rule_types/field_maps/field_names'; + +import { SimpleHit, ThresholdSignalHistory } from '../types'; +import { getThresholdTermsHash, isWrappedRACAlert, isWrappedSignalHit } from '../utils'; + +interface GetThresholdSignalHistoryParams { + alerts: Array>; +} + +const getTerms = (alert: SimpleHit) => { + if (isWrappedRACAlert(alert)) { + return (alert._source[ALERT_RULE_THRESHOLD_FIELD] as string[]).map((field) => ({ + field, + value: alert._source[field] as string, + })); + } else if (isWrappedSignalHit(alert)) { + return alert._source.signal?.threshold_result?.terms ?? []; + } else { + // We shouldn't be here + return []; + } +}; + +const getOriginalTime = (alert: SimpleHit) => { + if (isWrappedRACAlert(alert)) { + const originalTime = alert._source[ALERT_ORIGINAL_TIME]; + return originalTime != null ? new Date(originalTime as string).getTime() : undefined; + } else if (isWrappedSignalHit(alert)) { + const originalTime = alert._source.signal?.original_time; + return originalTime != null ? new Date(originalTime).getTime() : undefined; + } else { + // We shouldn't be here + return undefined; + } +}; + +export const buildThresholdSignalHistory = ({ + alerts, +}: GetThresholdSignalHistoryParams): ThresholdSignalHistory => { + const signalHistory = alerts.reduce((acc, alert) => { + if (!alert._source) { + return acc; + } + + const terms = getTerms(alert as SimpleHit); + const hash = getThresholdTermsHash(terms); + const existing = acc[hash]; + const originalTime = getOriginalTime(alert as SimpleHit); + + if (existing != null) { + if (originalTime && originalTime > existing.lastSignalTimestamp) { + acc[hash].lastSignalTimestamp = originalTime; + } + } else if (originalTime) { + acc[hash] = { + terms, + lastSignalTimestamp: originalTime, + }; + } + return acc; + }, {}); + + return signalHistory; +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/bulk_create_threshold_signals.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/bulk_create_threshold_signals.ts index afb0353c4ba03..ce8ee4542d603 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/bulk_create_threshold_signals.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/bulk_create_threshold_signals.ts @@ -46,7 +46,7 @@ interface BulkCreateThresholdSignalsParams { signalsIndex: string; startedAt: Date; from: Date; - thresholdSignalHistory: ThresholdSignalHistory; + signalHistory: ThresholdSignalHistory; bulkCreate: BulkCreate; wrapHits: WrapHits; } @@ -61,7 +61,7 @@ const getTransformedHits = ( ruleId: string, filter: unknown, timestampOverride: TimestampOverrideOrUndefined, - thresholdSignalHistory: ThresholdSignalHistory + signalHistory: ThresholdSignalHistory ) => { const aggParts = threshold.field.length ? results.aggregations && getThresholdAggregationParts(results.aggregations) @@ -148,7 +148,7 @@ const getTransformedHits = ( } const termsHash = getThresholdTermsHash(bucket.terms); - const signalHit = thresholdSignalHistory[termsHash]; + const signalHit = signalHistory[termsHash]; const source = { '@timestamp': timestamp, @@ -202,7 +202,7 @@ export const transformThresholdResultsToEcs = ( threshold: ThresholdNormalized, ruleId: string, timestampOverride: TimestampOverrideOrUndefined, - thresholdSignalHistory: ThresholdSignalHistory + signalHistory: ThresholdSignalHistory ): SignalSearchResponse => { const transformedHits = getTransformedHits( results, @@ -214,7 +214,7 @@ export const transformThresholdResultsToEcs = ( ruleId, filter, timestampOverride, - thresholdSignalHistory + signalHistory ); const thresholdResults = { ...results, @@ -246,7 +246,7 @@ export const bulkCreateThresholdSignals = async ( ruleParams.threshold, ruleParams.ruleId, ruleParams.timestampOverride, - params.thresholdSignalHistory + params.signalHistory ); return params.bulkCreate( diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/find_threshold_signals.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/find_threshold_signals.test.ts index e84b4f31fb15f..41d46925770bd 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/find_threshold_signals.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/find_threshold_signals.test.ts @@ -227,6 +227,7 @@ describe('findThresholdSignals', () => { 'threshold_1:user.name': { terms: { field: 'user.name', + order: { cardinality_count: 'desc' }, min_doc_count: 100, size: 10000, }, @@ -302,6 +303,7 @@ describe('findThresholdSignals', () => { lang: 'painless', }, min_doc_count: 200, + order: { cardinality_count: 'desc' }, }, aggs: { cardinality_count: { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/find_threshold_signals.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/find_threshold_signals.ts index ca7f22e4a7570..740ba281cfcfb 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/find_threshold_signals.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/find_threshold_signals.ts @@ -89,6 +89,13 @@ export const findThresholdSignals = async ({ const thresholdFields = threshold.field; + // order buckets by cardinality (https://github.com/elastic/kibana/issues/95258) + const thresholdFieldCount = thresholdFields.length; + const orderByCardinality = (i: number = 0) => + (thresholdFieldCount === 0 || i === thresholdFieldCount - 1) && threshold.cardinality?.length + ? { order: { cardinality_count: 'desc' } } + : {}; + // Generate a nested terms aggregation for each threshold grouping field provided, appending leaf // aggregations to 1) filter out buckets that don't meet the cardinality threshold, if provided, and // 2) return the latest hit for each bucket so that we can persist the timestamp of the event in the @@ -104,6 +111,7 @@ export const findThresholdSignals = async ({ set(acc, aggPath, { terms: { field, + ...orderByCardinality(i), min_doc_count: threshold.value, // not needed on parent agg, but can help narrow down result set size: 10000, // max 10k buckets }, @@ -121,6 +129,7 @@ export const findThresholdSignals = async ({ source: '""', // Group everything in the same bucket lang: 'painless', }, + ...orderByCardinality(), min_doc_count: threshold.value, }, aggs: leafAggs, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/get_threshold_bucket_filters.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/get_threshold_bucket_filters.test.ts index d621868a0956c..e67a6fa3dfa9c 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/get_threshold_bucket_filters.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/get_threshold_bucket_filters.test.ts @@ -11,7 +11,7 @@ import { getThresholdBucketFilters } from './get_threshold_bucket_filters'; describe('getThresholdBucketFilters', () => { it('should generate filters for threshold signal detection with dupe mitigation', async () => { const result = await getThresholdBucketFilters({ - thresholdSignalHistory: sampleThresholdSignalHistory(), + signalHistory: sampleThresholdSignalHistory(), timestampOverride: undefined, }); expect(result).toEqual([ diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/get_threshold_bucket_filters.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/get_threshold_bucket_filters.ts index a8b2ee31c6210..5cafff24c544b 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/get_threshold_bucket_filters.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/get_threshold_bucket_filters.ts @@ -9,14 +9,18 @@ import { Filter } from 'src/plugins/data/common'; import { ESFilter } from '../../../../../../../../src/core/types/elasticsearch'; import { ThresholdSignalHistory, ThresholdSignalHistoryRecord } from '../types'; +/* + * Returns a filter to exclude events that have already been included in a + * previous threshold signal. Uses the threshold signal history to achieve this. + */ export const getThresholdBucketFilters = async ({ - thresholdSignalHistory, + signalHistory, timestampOverride, }: { - thresholdSignalHistory: ThresholdSignalHistory; + signalHistory: ThresholdSignalHistory; timestampOverride: string | undefined; }): Promise => { - const filters = Object.values(thresholdSignalHistory).reduce( + const filters = Object.values(signalHistory).reduce( (acc: ESFilter[], bucket: ThresholdSignalHistoryRecord): ESFilter[] => { const filter = { bool: { @@ -24,6 +28,7 @@ export const getThresholdBucketFilters = async ({ { range: { [timestampOverride ?? '@timestamp']: { + // Timestamp of last event signaled on for this set of terms. lte: new Date(bucket.lastSignalTimestamp).toISOString(), }, }, @@ -32,6 +37,7 @@ export const getThresholdBucketFilters = async ({ }, } as ESFilter; + // Terms to filter events older than `lastSignalTimestamp`. bucket.terms.forEach((term) => { if (term.field != null) { (filter.bool!.filter as ESFilter[]).push({ diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/get_threshold_signal_history.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/get_threshold_signal_history.ts index ebfad5c7f9bec..276431c3bc929 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/get_threshold_signal_history.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/get_threshold_signal_history.ts @@ -5,7 +5,6 @@ * 2.0. */ -import { RulesSchema } from '../../../../../common/detection_engine/schemas/response/rules_schema'; import { TimestampOverrideOrUndefined } from '../../../../../common/detection_engine/schemas/common/schemas'; import { AlertInstanceContext, @@ -16,7 +15,7 @@ import { Logger } from '../../../../../../../../src/core/server'; import { ThresholdSignalHistory } from '../types'; import { BuildRuleMessage } from '../rule_messages'; import { findPreviousThresholdSignals } from './find_previous_threshold_signals'; -import { getThresholdTermsHash } from '../utils'; +import { buildThresholdSignalHistory } from './build_signal_history'; interface GetThresholdSignalHistoryParams { from: string; @@ -41,7 +40,7 @@ export const getThresholdSignalHistory = async ({ timestampOverride, buildRuleMessage, }: GetThresholdSignalHistoryParams): Promise<{ - thresholdSignalHistory: ThresholdSignalHistory; + signalHistory: ThresholdSignalHistory; searchErrors: string[]; }> => { const { searchResult, searchErrors } = await findPreviousThresholdSignals({ @@ -56,52 +55,10 @@ export const getThresholdSignalHistory = async ({ buildRuleMessage, }); - const thresholdSignalHistory = searchResult.hits.hits.reduce( - (acc, hit) => { - if (!hit._source) { - return acc; - } - - const terms = - hit._source.signal?.threshold_result?.terms != null - ? hit._source.signal.threshold_result.terms - : [ - // Pre-7.12 signals - { - field: - ( - (hit._source.signal?.rule as RulesSchema).threshold as unknown as { - field: string; - } - ).field ?? '', - value: (hit._source.signal?.threshold_result as unknown as { value: string }).value, - }, - ]; - - const hash = getThresholdTermsHash(terms); - const existing = acc[hash]; - const originalTime = - hit._source.signal?.original_time != null - ? new Date(hit._source.signal?.original_time).getTime() - : undefined; - - if (existing != null) { - if (originalTime && originalTime > existing.lastSignalTimestamp) { - acc[hash].lastSignalTimestamp = originalTime; - } - } else if (originalTime) { - acc[hash] = { - terms, - lastSignalTimestamp: originalTime, - }; - } - return acc; - }, - {} - ); - return { - thresholdSignalHistory, + signalHistory: buildThresholdSignalHistory({ + alerts: searchResult.hits.hits, + }), searchErrors, }; }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/types.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/types.ts index 3751f6f6e98f2..fc6b42c38549e 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/types.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/types.ts @@ -362,3 +362,8 @@ export interface ThresholdQueryBucket extends TermAggregationBucket { value_as_string: string; }; } + +export interface ThresholdAlertState extends AlertTypeState { + initialized: boolean; + signalHistory: ThresholdSignalHistory; +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.ts index efd7200202b59..5993dd626729f 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.ts @@ -63,10 +63,12 @@ import { WrappedRACAlert } from '../rule_types/types'; import { SearchTypes } from '../../../../common/detection_engine/types'; import { IRuleExecutionLogClient } from '../rule_execution_log/types'; import { + EQL_RULE_TYPE_ID, INDICATOR_RULE_TYPE_ID, ML_RULE_TYPE_ID, QUERY_RULE_TYPE_ID, SIGNALS_ID, + THRESHOLD_RULE_TYPE_ID, } from '../../../../common/constants'; interface SortExceptionsReturn { @@ -1013,10 +1015,10 @@ export const getField = (event: SimpleHit, field: string) * Maps legacy rule types to RAC rule type IDs. */ export const ruleTypeMappings = { - eql: SIGNALS_ID, + eql: EQL_RULE_TYPE_ID, machine_learning: ML_RULE_TYPE_ID, query: QUERY_RULE_TYPE_ID, saved_query: SIGNALS_ID, threat_match: INDICATOR_RULE_TYPE_ID, - threshold: SIGNALS_ID, + threshold: THRESHOLD_RULE_TYPE_ID, }; diff --git a/x-pack/plugins/security_solution/server/plugin.ts b/x-pack/plugins/security_solution/server/plugin.ts index f07137a118ab6..14da8ca650960 100644 --- a/x-pack/plugins/security_solution/server/plugin.ts +++ b/x-pack/plugins/security_solution/server/plugin.ts @@ -53,6 +53,7 @@ import { createIndicatorMatchAlertType, createMlAlertType, createQueryAlertType, + createThresholdAlertType, } from './lib/detection_engine/rule_types'; import { initRoutes } from './routes'; import { isAlertExecutor } from './lib/detection_engine/signals/types'; @@ -264,9 +265,10 @@ export class Plugin implements IPlugin) => ({ }, ja3: { terms: { - field: 'tls.server.ja3s', + field: 'tls.client.ja3', }, }, }, diff --git a/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json b/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json index 9b7e4bd0c9f6b..2dd7b96cfef95 100644 --- a/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json +++ b/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json @@ -4086,6 +4086,31 @@ "deprecated": { "type": "long" }, + "sizes": { + "properties": { + "1.0": { + "type": "long" + }, + "5.0": { + "type": "long" + }, + "25.0": { + "type": "long" + }, + "50.0": { + "type": "long" + }, + "75.0": { + "type": "long" + }, + "95.0": { + "type": "long" + }, + "99.0": { + "type": "long" + } + } + }, "app": { "properties": { "search": { @@ -4128,6 +4153,31 @@ "deprecated": { "type": "long" }, + "sizes": { + "properties": { + "1.0": { + "type": "long" + }, + "5.0": { + "type": "long" + }, + "25.0": { + "type": "long" + }, + "50.0": { + "type": "long" + }, + "75.0": { + "type": "long" + }, + "95.0": { + "type": "long" + }, + "99.0": { + "type": "long" + } + } + }, "app": { "properties": { "search": { @@ -4170,6 +4220,31 @@ "deprecated": { "type": "long" }, + "sizes": { + "properties": { + "1.0": { + "type": "long" + }, + "5.0": { + "type": "long" + }, + "25.0": { + "type": "long" + }, + "50.0": { + "type": "long" + }, + "75.0": { + "type": "long" + }, + "95.0": { + "type": "long" + }, + "99.0": { + "type": "long" + } + } + }, "app": { "properties": { "search": { @@ -4212,6 +4287,31 @@ "deprecated": { "type": "long" }, + "sizes": { + "properties": { + "1.0": { + "type": "long" + }, + "5.0": { + "type": "long" + }, + "25.0": { + "type": "long" + }, + "50.0": { + "type": "long" + }, + "75.0": { + "type": "long" + }, + "95.0": { + "type": "long" + }, + "99.0": { + "type": "long" + } + } + }, "app": { "properties": { "search": { @@ -4254,6 +4354,31 @@ "deprecated": { "type": "long" }, + "sizes": { + "properties": { + "1.0": { + "type": "long" + }, + "5.0": { + "type": "long" + }, + "25.0": { + "type": "long" + }, + "50.0": { + "type": "long" + }, + "75.0": { + "type": "long" + }, + "95.0": { + "type": "long" + }, + "99.0": { + "type": "long" + } + } + }, "app": { "properties": { "search": { @@ -4296,6 +4421,31 @@ "deprecated": { "type": "long" }, + "sizes": { + "properties": { + "1.0": { + "type": "long" + }, + "5.0": { + "type": "long" + }, + "25.0": { + "type": "long" + }, + "50.0": { + "type": "long" + }, + "75.0": { + "type": "long" + }, + "95.0": { + "type": "long" + }, + "99.0": { + "type": "long" + } + } + }, "app": { "properties": { "search": { @@ -4338,6 +4488,31 @@ "deprecated": { "type": "long" }, + "sizes": { + "properties": { + "1.0": { + "type": "long" + }, + "5.0": { + "type": "long" + }, + "25.0": { + "type": "long" + }, + "50.0": { + "type": "long" + }, + "75.0": { + "type": "long" + }, + "95.0": { + "type": "long" + }, + "99.0": { + "type": "long" + } + } + }, "app": { "properties": { "search": { @@ -4975,6 +5150,31 @@ } } }, + "output_size": { + "properties": { + "1.0": { + "type": "long" + }, + "5.0": { + "type": "long" + }, + "25.0": { + "type": "long" + }, + "50.0": { + "type": "long" + }, + "75.0": { + "type": "long" + }, + "95.0": { + "type": "long" + }, + "99.0": { + "type": "long" + } + } + }, "available": { "type": "boolean" }, @@ -4997,6 +5197,31 @@ "deprecated": { "type": "long" }, + "sizes": { + "properties": { + "1.0": { + "type": "long" + }, + "5.0": { + "type": "long" + }, + "25.0": { + "type": "long" + }, + "50.0": { + "type": "long" + }, + "75.0": { + "type": "long" + }, + "95.0": { + "type": "long" + }, + "99.0": { + "type": "long" + } + } + }, "app": { "properties": { "search": { @@ -5039,6 +5264,31 @@ "deprecated": { "type": "long" }, + "sizes": { + "properties": { + "1.0": { + "type": "long" + }, + "5.0": { + "type": "long" + }, + "25.0": { + "type": "long" + }, + "50.0": { + "type": "long" + }, + "75.0": { + "type": "long" + }, + "95.0": { + "type": "long" + }, + "99.0": { + "type": "long" + } + } + }, "app": { "properties": { "search": { @@ -5081,6 +5331,31 @@ "deprecated": { "type": "long" }, + "sizes": { + "properties": { + "1.0": { + "type": "long" + }, + "5.0": { + "type": "long" + }, + "25.0": { + "type": "long" + }, + "50.0": { + "type": "long" + }, + "75.0": { + "type": "long" + }, + "95.0": { + "type": "long" + }, + "99.0": { + "type": "long" + } + } + }, "app": { "properties": { "search": { @@ -5123,6 +5398,31 @@ "deprecated": { "type": "long" }, + "sizes": { + "properties": { + "1.0": { + "type": "long" + }, + "5.0": { + "type": "long" + }, + "25.0": { + "type": "long" + }, + "50.0": { + "type": "long" + }, + "75.0": { + "type": "long" + }, + "95.0": { + "type": "long" + }, + "99.0": { + "type": "long" + } + } + }, "app": { "properties": { "search": { @@ -5165,6 +5465,31 @@ "deprecated": { "type": "long" }, + "sizes": { + "properties": { + "1.0": { + "type": "long" + }, + "5.0": { + "type": "long" + }, + "25.0": { + "type": "long" + }, + "50.0": { + "type": "long" + }, + "75.0": { + "type": "long" + }, + "95.0": { + "type": "long" + }, + "99.0": { + "type": "long" + } + } + }, "app": { "properties": { "search": { @@ -5207,6 +5532,31 @@ "deprecated": { "type": "long" }, + "sizes": { + "properties": { + "1.0": { + "type": "long" + }, + "5.0": { + "type": "long" + }, + "25.0": { + "type": "long" + }, + "50.0": { + "type": "long" + }, + "75.0": { + "type": "long" + }, + "95.0": { + "type": "long" + }, + "99.0": { + "type": "long" + } + } + }, "app": { "properties": { "search": { @@ -5249,6 +5599,31 @@ "deprecated": { "type": "long" }, + "sizes": { + "properties": { + "1.0": { + "type": "long" + }, + "5.0": { + "type": "long" + }, + "25.0": { + "type": "long" + }, + "50.0": { + "type": "long" + }, + "75.0": { + "type": "long" + }, + "95.0": { + "type": "long" + }, + "99.0": { + "type": "long" + } + } + }, "app": { "properties": { "search": { @@ -5885,6 +6260,31 @@ } } } + }, + "output_size": { + "properties": { + "1.0": { + "type": "long" + }, + "5.0": { + "type": "long" + }, + "25.0": { + "type": "long" + }, + "50.0": { + "type": "long" + }, + "75.0": { + "type": "long" + }, + "95.0": { + "type": "long" + }, + "99.0": { + "type": "long" + } + } } } } @@ -6924,44 +7324,6 @@ } } } - }, - "ui_open": { - "properties": { - "elasticsearch": { - "type": "long", - "_meta": { - "description": "Number of times a user viewed the list of Elasticsearch deprecations." - } - }, - "overview": { - "type": "long", - "_meta": { - "description": "Number of times a user viewed the Overview page." - } - }, - "kibana": { - "type": "long", - "_meta": { - "description": "Number of times a user viewed the list of Kibana deprecations" - } - } - } - }, - "ui_reindex": { - "properties": { - "close": { - "type": "long" - }, - "open": { - "type": "long" - }, - "start": { - "type": "long" - }, - "stop": { - "type": "long" - } - } } } }, diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 8fd02c5dba865..20cb4c42b0181 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -6399,14 +6399,6 @@ "xpack.apm.correlations.correlationsTable.filterLabel": "フィルター", "xpack.apm.correlations.correlationsTable.loadingText": "読み込み中", "xpack.apm.correlations.correlationsTable.noDataText": "データなし", - "xpack.apm.correlations.customize.buttonLabel": "フィールドのカスタマイズ", - "xpack.apm.correlations.customize.fieldHelpText": "相関関係を分析するフィールドをカスタマイズまたは{reset}します。{docsLink}", - "xpack.apm.correlations.customize.fieldHelpTextDocsLink": "デフォルトフィールドの詳細。", - "xpack.apm.correlations.customize.fieldHelpTextReset": "リセット", - "xpack.apm.correlations.customize.fieldLabel": "フィールド", - "xpack.apm.correlations.customize.fieldPlaceholder": "オプションを選択または作成", - "xpack.apm.correlations.customize.thresholdLabel": "しきい値", - "xpack.apm.correlations.customize.thresholdPercentile": "{percentile}パーセンタイル", "xpack.apm.correlations.failedTransactions.correlationsTable.fieldNameLabel": "フィールド名", "xpack.apm.correlations.failedTransactions.correlationsTable.fieldValueLabel": "フィールド値", "xpack.apm.correlations.failedTransactions.correlationsTable.impactLabel": "インパクト", @@ -6960,7 +6952,6 @@ "xpack.apm.settings.unsupportedConfigs.errorToast.title": "APMサーバー設定を取り込めません", "xpack.apm.settingsLinkLabel": "設定", "xpack.apm.setupInstructionsButtonLabel": "セットアップの手順", - "xpack.apm.significanTerms.license.text": "相関関係APIを使用するには、Elastic Platinumライセンスのサブスクリプションが必要です。", "xpack.apm.stacktraceTab.causedByFramesToogleButtonLabel": "作成元", "xpack.apm.stacktraceTab.localVariablesToogleButtonLabel": "ローカル変数", "xpack.apm.stacktraceTab.noStacktraceAvailableLabel": "利用可能なスタックトレースがありません。", @@ -12536,7 +12527,6 @@ "xpack.indexLifecycleMgmt.editPolicy.dataTierAllocation.nodeAllocationFieldLabel": "ノード属性を選択", "xpack.indexLifecycleMgmt.editPolicy.dataTierHotLabel": "ホット", "xpack.indexLifecycleMgmt.editPolicy.dataTierWarmLabel": "ウォーム", - "xpack.indexLifecycleMgmt.editPolicy.daysOptionLabel": "日", "xpack.indexLifecycleMgmt.editPolicy.defaultToDataNodesDescription": "データを特定のデータノードに割り当てるには、{roleBasedGuidance}か、elasticsearch.ymlでカスタムノード属性を構成します。", "xpack.indexLifecycleMgmt.editPolicy.defaultToDataNodesDescription.migrationGuidanceMessage": "ユーザーロールに基づく割り当て", "xpack.indexLifecycleMgmt.editPolicy.deletePhase.activateWarmPhaseSwitchLabel": "削除フェーズを有効にする", @@ -12591,7 +12581,6 @@ "xpack.indexLifecycleMgmt.editPolicy.hotPhase.learnAboutRolloverLinkText": "詳細", "xpack.indexLifecycleMgmt.editPolicy.hotPhase.rolloverDefaultsTipContent": "インデックスが30日経過するか、50 GBに達したときにロールオーバーします。", "xpack.indexLifecycleMgmt.editPolicy.hotPhase.rolloverDescriptionMessage": "現在のインデックスが特定のサイズ、ドキュメント数、または年齢に達したときに、新しいインデックスへの書き込みを開始します。時系列データを操作するときに、パフォーマンスを最適化し、リソースの使用状況を管理できます。", - "xpack.indexLifecycleMgmt.editPolicy.hoursOptionLabel": "時間", "xpack.indexLifecycleMgmt.editPolicy.indexPriority.indexPriorityEnabledFieldLabel": "インデックスの優先度を設定", "xpack.indexLifecycleMgmt.editPolicy.indexPriorityLabel": "インデックスの優先順位", "xpack.indexLifecycleMgmt.editPolicy.indexPriorityText": "インデックスの優先順位", @@ -12602,16 +12591,12 @@ "xpack.indexLifecycleMgmt.editPolicy.lifecyclePoliciesReloadButton": "再試行", "xpack.indexLifecycleMgmt.editPolicy.loadSnapshotRepositoriesErrorBody": "このフィールドを更新し、既存のスナップショットリポジトリの名前を入力します。", "xpack.indexLifecycleMgmt.editPolicy.loadSnapshotRepositoriesErrorTitle": "スナップショットリポジトリを読み込めません", - "xpack.indexLifecycleMgmt.editPolicy.microSecondsOptionLabel": "マイクロ秒", - "xpack.indexLifecycleMgmt.editPolicy.milliSecondsOptionLabel": "ミリ秒", "xpack.indexLifecycleMgmt.editPolicy.minAgeSmallerThanColdPhaseError": "コールドフェーズ値({value})以上でなければなりません", "xpack.indexLifecycleMgmt.editPolicy.minAgeSmallerThanFrozenPhaseError": "フローズンフェーズ値({value})以上でなければなりません", "xpack.indexLifecycleMgmt.editPolicy.minAgeSmallerThanWarmPhaseError": "ウォームフェーズ値({value})以上でなければなりません", "xpack.indexLifecycleMgmt.editPolicy.minimumAge.minimumAgeFieldLabel": "次のときに、データをフェーズに移動します。", "xpack.indexLifecycleMgmt.editPolicy.minimumAge.minimumAgeFieldSuffixLabel": "古", "xpack.indexLifecycleMgmt.editPolicy.minimumAge.rolloverToolTipDescription": "データの年齢はロールオーバーから計算されます。ロールオーバーはホットフェーズで構成されます。", - "xpack.indexLifecycleMgmt.editPolicy.minutesOptionLabel": "分", - "xpack.indexLifecycleMgmt.editPolicy.nanoSecondsOptionLabel": "ナノ秒", "xpack.indexLifecycleMgmt.editPolicy.noCustomAttributesTitle": "カスタム属性が定義されていません", "xpack.indexLifecycleMgmt.editPolicy.nodeAllocation.allocateToDataNodesOption": "任意のデータノード", "xpack.indexLifecycleMgmt.editPolicy.nodeAllocation.customOption.description": "ノード属性を使用して、シャード割り当てを制御します。{learnMoreLink}。", @@ -12663,7 +12648,6 @@ "xpack.indexLifecycleMgmt.editPolicy.searchableSnapshotRepoFieldLabel": "スナップショットリポジトリ", "xpack.indexLifecycleMgmt.editPolicy.searchableSnapshotRepoRequiredError": "スナップショットリポジトリ名が必要です。", "xpack.indexLifecycleMgmt.editPolicy.searchableSnapshotStorageFieldLabel": "検索可能スナップショットストレージ", - "xpack.indexLifecycleMgmt.editPolicy.secondsOptionLabel": "秒", "xpack.indexLifecycleMgmt.editPolicy.showPolicyJsonButton": "リクエストを表示", "xpack.indexLifecycleMgmt.editPolicy.shrinkIndexExplanationText": "インデックス情報をプライマリシャードの少ない新規インデックスに縮小します。", "xpack.indexLifecycleMgmt.editPolicy.shrinkText": "縮小", @@ -12682,30 +12666,16 @@ "xpack.indexLifecycleMgmt.forcemerge.enableLabel": "データを強制結合", "xpack.indexLifecycleMgmt.forceMerge.numberOfSegmentsLabel": "セグメントの数", "xpack.indexLifecycleMgmt.frozePhase.freezeIndexLabel": "インデックスを凍結", - "xpack.indexLifecycleMgmt.hotPhase.bytesLabel": "バイト", - "xpack.indexLifecycleMgmt.hotPhase.daysLabel": "日", "xpack.indexLifecycleMgmt.hotPhase.enableRolloverLabel": "ロールオーバーを有効にする", - "xpack.indexLifecycleMgmt.hotPhase.gigabytesLabel": "ギガバイト", - "xpack.indexLifecycleMgmt.hotPhase.hoursLabel": "時間", "xpack.indexLifecycleMgmt.hotPhase.isUsingDefaultRollover": "推奨のデフォルト値を使用", - "xpack.indexLifecycleMgmt.hotPhase.kilobytesLabel": "キロバイト", "xpack.indexLifecycleMgmt.hotPhase.maximumAgeLabel": "最高年齢", "xpack.indexLifecycleMgmt.hotPhase.maximumAgeUnitsAriaLabel": "最高年齢の単位", "xpack.indexLifecycleMgmt.hotPhase.maximumDocumentsLabel": "最高ドキュメント数", "xpack.indexLifecycleMgmt.hotPhase.maximumIndexSizeDeprecationMessage": "最大インデックスサイズは廃止予定であり、将来のバージョンでは削除されます。代わりに最大プライマリシャードサイズを使用してください。", "xpack.indexLifecycleMgmt.hotPhase.maximumIndexSizeLabel": "最大インデックスサイズ", "xpack.indexLifecycleMgmt.hotPhase.maximumIndexSizeUnitsAriaLabel": "最大インデックスサイズの単位", - "xpack.indexLifecycleMgmt.hotPhase.maximumPrimaryShardSizeAriaLabel": "最大プライマリシャードサイズ単位", "xpack.indexLifecycleMgmt.hotPhase.maximumPrimaryShardSizeLabel": "最大プライマリシャードサイズ", - "xpack.indexLifecycleMgmt.hotPhase.megabytesLabel": "メガバイト", - "xpack.indexLifecycleMgmt.hotPhase.microsecondsLabel": "マイクロ秒", - "xpack.indexLifecycleMgmt.hotPhase.millisecondsLabel": "ミリ秒", - "xpack.indexLifecycleMgmt.hotPhase.minutesLabel": "分", - "xpack.indexLifecycleMgmt.hotPhase.nanosecondsLabel": "ナノ秒", - "xpack.indexLifecycleMgmt.hotPhase.petabytesLabel": "ペタバイト", "xpack.indexLifecycleMgmt.hotPhase.rolloverFieldTitle": "ロールオーバー", - "xpack.indexLifecycleMgmt.hotPhase.secondsLabel": "秒", - "xpack.indexLifecycleMgmt.hotPhase.terabytesLabel": "テラバイト", "xpack.indexLifecycleMgmt.indexLifecycleMgmtSummary.actionStatusTitle": "アクションステータス", "xpack.indexLifecycleMgmt.indexLifecycleMgmtSummary.headers.currentActionHeader": "現在のステータス", "xpack.indexLifecycleMgmt.indexLifecycleMgmtSummary.headers.currentActionTimeHeader": "現在のアクション時間", @@ -12808,7 +12778,6 @@ "xpack.indexLifecycleMgmt.searchableSnapshot.disallowedCalloutBody": "このフェーズで検索可能なスナップショットを使用するには、ホットフェーズで検索可能なスナップショットを無効にする必要があります。", "xpack.indexLifecycleMgmt.searchableSnapshot.disallowedCalloutTitle": "検索可能スナップショットが無効です", "xpack.indexLifecycleMgmt.searchSnapshotlicenseCheckErrorMessage": "検索可能なスナップショットを使用するには、1 つ以上のエンタープライズレベルのライセンスが必要です。", - "xpack.indexLifecycleMgmt.shrink.indexFieldLabel": "インデックスを縮小", "xpack.indexLifecycleMgmt.shrink.numberOfPrimaryShardsLabel": "プライマリシャードの数", "xpack.indexLifecycleMgmt.templateNotFoundMessage": "テンプレート{name}が見つかりません。", "xpack.indexLifecycleMgmt.timeline.coldPhaseSectionTitle": "コールドフェーズ", @@ -17602,7 +17571,6 @@ "xpack.monitoring.alerts.kibanaVersionMismatch.label": "Kibana バージョン不一致", "xpack.monitoring.alerts.kibanaVersionMismatch.shortAction": "すべてのインスタンスのバージョンが同じことを確認してください。", "xpack.monitoring.alerts.kibanaVersionMismatch.ui.firingMessage": "このクラスターでは、複数のバージョンの Kibana({versions})が実行されています。", - "xpack.monitoring.alerts.legacyAlert.expressionText": "構成するものがありません。", "xpack.monitoring.alerts.licenseExpiration.action": "ライセンスを更新してください。", "xpack.monitoring.alerts.licenseExpiration.actionVariables.clusterName": "ライセンスが属しているクラスター。", "xpack.monitoring.alerts.licenseExpiration.actionVariables.expiredDate": "ライセンスの有効期限。", @@ -23097,7 +23065,6 @@ "xpack.securitySolution.paginatedTable.tooManyResultsToastText": "クエリ範囲を縮めて結果をさらにフィルタリングしてください", "xpack.securitySolution.paginatedTable.tooManyResultsToastTitle": " - 結果が多すぎます", "xpack.securitySolution.policiesTab": "ポリシー", - "xpack.securitySolution.policySelect.policySpecificSectionTitle": "特定のエンドポイントポリシーに適用", "xpack.securitySolution.policyStatusText.failure": "失敗", "xpack.securitySolution.policyStatusText.success": "成功", "xpack.securitySolution.policyStatusText.unsupported": "サポートされていない", @@ -23508,8 +23475,6 @@ "xpack.securitySolution.trustedapps.logicalConditionBuilder.group.andOperator": "AND", "xpack.securitySolution.trustedapps.logicalConditionBuilder.noEntries": "条件が定義されていません", "xpack.securitySolution.trustedapps.middleware.editIdMissing": "IDが指定されていません", - "xpack.securitySolution.trustedapps.policySelect.globalSectionTitle": "割り当て", - "xpack.securitySolution.trustedapps.policySelect.globalSwitchTitle": "信頼できるアプリケーションをグローバルに適用", "xpack.securitySolution.trustedapps.trustedapp.entry.field": "フィールド", "xpack.securitySolution.trustedapps.trustedapp.entry.operator": "演算子", "xpack.securitySolution.trustedapps.trustedapp.entry.value": "値", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index bd1224a8fd057..efb613f5c91e4 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -6450,14 +6450,6 @@ "xpack.apm.correlations.correlationsTable.filterLabel": "筛选", "xpack.apm.correlations.correlationsTable.loadingText": "正在加载", "xpack.apm.correlations.correlationsTable.noDataText": "无数据", - "xpack.apm.correlations.customize.buttonLabel": "定制字段", - "xpack.apm.correlations.customize.fieldHelpText": "定制或{reset}要针对相关性分析的字段。{docsLink}", - "xpack.apm.correlations.customize.fieldHelpTextDocsLink": "详细了解默认字段。", - "xpack.apm.correlations.customize.fieldHelpTextReset": "重置", - "xpack.apm.correlations.customize.fieldLabel": "字段", - "xpack.apm.correlations.customize.fieldPlaceholder": "选择或创建选项", - "xpack.apm.correlations.customize.thresholdLabel": "阈值", - "xpack.apm.correlations.customize.thresholdPercentile": "第 {percentile} 个百分位数", "xpack.apm.correlations.failedTransactions.correlationsTable.fieldNameLabel": "字段名称", "xpack.apm.correlations.failedTransactions.correlationsTable.fieldValueLabel": "字段值", "xpack.apm.correlations.failedTransactions.correlationsTable.impactLabel": "影响", @@ -7016,7 +7008,6 @@ "xpack.apm.settings.unsupportedConfigs.errorToast.title": "无法获取 APM Server 设置", "xpack.apm.settingsLinkLabel": "设置", "xpack.apm.setupInstructionsButtonLabel": "设置说明", - "xpack.apm.significanTerms.license.text": "要使用相关性 API,必须订阅 Elastic 白金级许可证。", "xpack.apm.stacktraceTab.causedByFramesToogleButtonLabel": "原因", "xpack.apm.stacktraceTab.libraryFramesToogleButtonLabel": "{count, plural, other {# 个库帧}}", "xpack.apm.stacktraceTab.localVariablesToogleButtonLabel": "本地变量", @@ -12701,7 +12692,6 @@ "xpack.indexLifecycleMgmt.editPolicy.dataTierAllocation.nodeAllocationFieldLabel": "选择节点属性", "xpack.indexLifecycleMgmt.editPolicy.dataTierHotLabel": "热", "xpack.indexLifecycleMgmt.editPolicy.dataTierWarmLabel": "温", - "xpack.indexLifecycleMgmt.editPolicy.daysOptionLabel": "天", "xpack.indexLifecycleMgmt.editPolicy.defaultToDataNodesDescription": "要将数据分配给特定数据节点,请{roleBasedGuidance}或在 elasticsearch.yml 中配置定制节点属性。", "xpack.indexLifecycleMgmt.editPolicy.defaultToDataNodesDescription.migrationGuidanceMessage": "使用基于角色的分配", "xpack.indexLifecycleMgmt.editPolicy.deletePhase.activateWarmPhaseSwitchLabel": "激活删除阶段", @@ -12757,7 +12747,6 @@ "xpack.indexLifecycleMgmt.editPolicy.hotPhase.learnAboutRolloverLinkText": "了解详情", "xpack.indexLifecycleMgmt.editPolicy.hotPhase.rolloverDefaultsTipContent": "当索引已存在 30 天或任何主分片达到 50 GB 时滚动更新。", "xpack.indexLifecycleMgmt.editPolicy.hotPhase.rolloverDescriptionMessage": "在当前索引达到特定大小、文档计数或存在时间时,开始写入到新索引。允许您在使用时间序列数据时优化性能并管理资源使用。", - "xpack.indexLifecycleMgmt.editPolicy.hoursOptionLabel": "小时", "xpack.indexLifecycleMgmt.editPolicy.indexPriority.indexPriorityEnabledFieldLabel": "设置索引优先级", "xpack.indexLifecycleMgmt.editPolicy.indexPriorityLabel": "索引优先级", "xpack.indexLifecycleMgmt.editPolicy.indexPriorityText": "索引优先级", @@ -12770,16 +12759,12 @@ "xpack.indexLifecycleMgmt.editPolicy.linkedIndices": "{indicesCount, plural, other {# 个已链接索引}}", "xpack.indexLifecycleMgmt.editPolicy.loadSnapshotRepositoriesErrorBody": "刷新此字段并输入现有快照储存库的名称。", "xpack.indexLifecycleMgmt.editPolicy.loadSnapshotRepositoriesErrorTitle": "无法加载快照存储库", - "xpack.indexLifecycleMgmt.editPolicy.microSecondsOptionLabel": "微秒", - "xpack.indexLifecycleMgmt.editPolicy.milliSecondsOptionLabel": "毫秒", "xpack.indexLifecycleMgmt.editPolicy.minAgeSmallerThanColdPhaseError": "必须大于或等于冷阶段值 ({value})", "xpack.indexLifecycleMgmt.editPolicy.minAgeSmallerThanFrozenPhaseError": "必须大于或等于冻结阶段值 ({value})", "xpack.indexLifecycleMgmt.editPolicy.minAgeSmallerThanWarmPhaseError": "必须大于或等于温阶段值 ({value})", "xpack.indexLifecycleMgmt.editPolicy.minimumAge.minimumAgeFieldLabel": "在以下情况下将数据移到相应阶段:", "xpack.indexLifecycleMgmt.editPolicy.minimumAge.minimumAgeFieldSuffixLabel": "以前", "xpack.indexLifecycleMgmt.editPolicy.minimumAge.rolloverToolTipDescription": "数据存在时间计算自滚动更新。滚动更新配置于热阶段。", - "xpack.indexLifecycleMgmt.editPolicy.minutesOptionLabel": "分钟", - "xpack.indexLifecycleMgmt.editPolicy.nanoSecondsOptionLabel": "纳秒", "xpack.indexLifecycleMgmt.editPolicy.noCustomAttributesTitle": "未定义定制属性", "xpack.indexLifecycleMgmt.editPolicy.nodeAllocation.allocateToDataNodesOption": "任何数据节点", "xpack.indexLifecycleMgmt.editPolicy.nodeAllocation.customOption.description": "使用节点属性控制分片分配。{learnMoreLink}。", @@ -12831,7 +12816,6 @@ "xpack.indexLifecycleMgmt.editPolicy.searchableSnapshotRepoFieldLabel": "快照存储库", "xpack.indexLifecycleMgmt.editPolicy.searchableSnapshotRepoRequiredError": "快照存储库名称必填。", "xpack.indexLifecycleMgmt.editPolicy.searchableSnapshotStorageFieldLabel": "可搜索快照存储", - "xpack.indexLifecycleMgmt.editPolicy.secondsOptionLabel": "秒", "xpack.indexLifecycleMgmt.editPolicy.showPolicyJsonButton": "显示请求", "xpack.indexLifecycleMgmt.editPolicy.shrinkIndexExplanationText": "将索引缩小成具有较少主分片的新索引。", "xpack.indexLifecycleMgmt.editPolicy.shrinkText": "缩小", @@ -12850,30 +12834,16 @@ "xpack.indexLifecycleMgmt.forcemerge.enableLabel": "强制合并数据", "xpack.indexLifecycleMgmt.forceMerge.numberOfSegmentsLabel": "分段数目", "xpack.indexLifecycleMgmt.frozePhase.freezeIndexLabel": "冻结索引", - "xpack.indexLifecycleMgmt.hotPhase.bytesLabel": "字节", - "xpack.indexLifecycleMgmt.hotPhase.daysLabel": "天", "xpack.indexLifecycleMgmt.hotPhase.enableRolloverLabel": "启用滚动更新", - "xpack.indexLifecycleMgmt.hotPhase.gigabytesLabel": "千兆字节", - "xpack.indexLifecycleMgmt.hotPhase.hoursLabel": "小时", "xpack.indexLifecycleMgmt.hotPhase.isUsingDefaultRollover": "使用建议的默认值", - "xpack.indexLifecycleMgmt.hotPhase.kilobytesLabel": "千字节", "xpack.indexLifecycleMgmt.hotPhase.maximumAgeLabel": "最大存在时间", "xpack.indexLifecycleMgmt.hotPhase.maximumAgeUnitsAriaLabel": "最大存在时间单位", "xpack.indexLifecycleMgmt.hotPhase.maximumDocumentsLabel": "最大文档数", "xpack.indexLifecycleMgmt.hotPhase.maximumIndexSizeDeprecationMessage": "最大索引大小已弃用,将在未来版本中移除。改用最大主分片大小。", "xpack.indexLifecycleMgmt.hotPhase.maximumIndexSizeLabel": "最大索引大小", "xpack.indexLifecycleMgmt.hotPhase.maximumIndexSizeUnitsAriaLabel": "最大索引大小单位", - "xpack.indexLifecycleMgmt.hotPhase.maximumPrimaryShardSizeAriaLabel": "最大主分片大小单位", "xpack.indexLifecycleMgmt.hotPhase.maximumPrimaryShardSizeLabel": "最大主分片大小", - "xpack.indexLifecycleMgmt.hotPhase.megabytesLabel": "兆字节", - "xpack.indexLifecycleMgmt.hotPhase.microsecondsLabel": "微秒", - "xpack.indexLifecycleMgmt.hotPhase.millisecondsLabel": "毫秒", - "xpack.indexLifecycleMgmt.hotPhase.minutesLabel": "分钟", - "xpack.indexLifecycleMgmt.hotPhase.nanosecondsLabel": "纳秒", - "xpack.indexLifecycleMgmt.hotPhase.petabytesLabel": "万兆字节", "xpack.indexLifecycleMgmt.hotPhase.rolloverFieldTitle": "滚动更新", - "xpack.indexLifecycleMgmt.hotPhase.secondsLabel": "秒", - "xpack.indexLifecycleMgmt.hotPhase.terabytesLabel": "兆兆字节", "xpack.indexLifecycleMgmt.indexLifecycleMgmtSummary.actionStatusTitle": "操作状态", "xpack.indexLifecycleMgmt.indexLifecycleMgmtSummary.headers.currentActionHeader": "当前操作", "xpack.indexLifecycleMgmt.indexLifecycleMgmtSummary.headers.currentActionTimeHeader": "当前操作名称", @@ -12980,7 +12950,6 @@ "xpack.indexLifecycleMgmt.searchableSnapshot.disallowedCalloutBody": "要在此阶段使用可搜索快照,必须在热阶段禁用可搜索快照。", "xpack.indexLifecycleMgmt.searchableSnapshot.disallowedCalloutTitle": "可搜索快照已禁用", "xpack.indexLifecycleMgmt.searchSnapshotlicenseCheckErrorMessage": "要使用可搜索快照,至少需要企业级许可证。", - "xpack.indexLifecycleMgmt.shrink.indexFieldLabel": "缩小索引", "xpack.indexLifecycleMgmt.shrink.numberOfPrimaryShardsLabel": "主分片数目", "xpack.indexLifecycleMgmt.templateNotFoundMessage": "找不到模板 {name}。", "xpack.indexLifecycleMgmt.timeline.coldPhaseSectionTitle": "冷阶段", @@ -17877,7 +17846,6 @@ "xpack.monitoring.alerts.kibanaVersionMismatch.label": "Kibana 版本不匹配", "xpack.monitoring.alerts.kibanaVersionMismatch.shortAction": "确认所有实例具有相同的版本。", "xpack.monitoring.alerts.kibanaVersionMismatch.ui.firingMessage": "在此集群中正运行着多个 Kibana 版本 ({versions})。", - "xpack.monitoring.alerts.legacyAlert.expressionText": "没有可配置的内容。", "xpack.monitoring.alerts.licenseExpiration.action": "请更新您的许可证。", "xpack.monitoring.alerts.licenseExpiration.actionVariables.clusterName": "许可证所属的集群。", "xpack.monitoring.alerts.licenseExpiration.actionVariables.expiredDate": "许可证过期日期。", @@ -23476,7 +23444,6 @@ "xpack.securitySolution.paginatedTable.tooManyResultsToastText": "缩减您的查询范围,以更好地筛选结果", "xpack.securitySolution.paginatedTable.tooManyResultsToastTitle": " - 结果过多", "xpack.securitySolution.policiesTab": "策略", - "xpack.securitySolution.policySelect.policySpecificSectionTitle": "应用到特定终端策略", "xpack.securitySolution.policyStatusText.failure": "失败", "xpack.securitySolution.policyStatusText.success": "成功", "xpack.securitySolution.policyStatusText.unsupported": "不支持", @@ -23890,8 +23857,6 @@ "xpack.securitySolution.trustedapps.logicalConditionBuilder.group.andOperator": "AND", "xpack.securitySolution.trustedapps.logicalConditionBuilder.noEntries": "未定义条件", "xpack.securitySolution.trustedapps.middleware.editIdMissing": "未提供 ID", - "xpack.securitySolution.trustedapps.policySelect.globalSectionTitle": "分配", - "xpack.securitySolution.trustedapps.policySelect.globalSwitchTitle": "全局应用受信任的应用程序", "xpack.securitySolution.trustedapps.trustedapp.entry.field": "字段", "xpack.securitySolution.trustedapps.trustedapp.entry.operator": "运算符", "xpack.securitySolution.trustedapps.trustedapp.entry.value": "值", diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email.test.tsx index ed56ca05538b1..ec86f149e9a43 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email.test.tsx @@ -48,6 +48,7 @@ describe('connector validation', () => { secrets: { user: 'user', password: 'pass', + clientSecret: null, }, id: 'test', actionTypeId: '.email', @@ -70,12 +71,15 @@ describe('connector validation', () => { port: [], host: [], service: [], + clientId: [], + tenantId: [], }, }, secrets: { errors: { user: [], password: [], + clientSecret: [], }, }, }); @@ -86,6 +90,7 @@ describe('connector validation', () => { secrets: { user: null, password: null, + clientSecret: null, }, id: 'test', actionTypeId: '.email', @@ -108,12 +113,15 @@ describe('connector validation', () => { port: [], host: [], service: [], + clientId: [], + tenantId: [], }, }, secrets: { errors: { user: [], password: [], + clientSecret: [], }, }, }); @@ -141,12 +149,15 @@ describe('connector validation', () => { port: ['Port is required.'], host: ['Host is required.'], service: [], + clientId: [], + tenantId: [], }, }, secrets: { errors: { user: [], password: [], + clientSecret: [], }, }, }); @@ -156,6 +167,7 @@ describe('connector validation', () => { secrets: { user: 'user', password: null, + clientSecret: null, }, id: 'test', actionTypeId: '.email', @@ -178,12 +190,15 @@ describe('connector validation', () => { port: [], host: [], service: [], + clientId: [], + tenantId: [], }, }, secrets: { errors: { user: [], password: ['Password is required when username is used.'], + clientSecret: [], }, }, }); @@ -193,6 +208,7 @@ describe('connector validation', () => { secrets: { user: null, password: 'password', + clientSecret: null, }, id: 'test', actionTypeId: '.email', @@ -215,12 +231,15 @@ describe('connector validation', () => { port: [], host: [], service: [], + clientId: [], + tenantId: [], }, }, secrets: { errors: { user: ['Username is required when password is used.'], password: [], + clientSecret: [], }, }, }); @@ -253,12 +272,53 @@ describe('connector validation', () => { port: [], host: [], service: ['Service is required.'], + clientId: [], + tenantId: [], }, }, secrets: { errors: { user: [], password: [], + clientSecret: [], + }, + }, + }); + }); + test('connector validation fails when for exchange service selected, but clientId, tenantId and clientSecrets were not defined', async () => { + const actionConnector = { + secrets: { + user: 'user', + password: 'pass', + clientSecret: null, + }, + id: 'test', + actionTypeId: '.email', + name: 'email', + isPreconfigured: false, + config: { + from: 'test@test.com', + hasAuth: true, + service: 'exchange_server', + }, + } as EmailActionConnector; + + expect(await actionTypeModel.validateConnector(actionConnector)).toEqual({ + config: { + errors: { + from: [], + port: [], + host: [], + service: [], + clientId: ['Client ID is required.'], + tenantId: ['Tenant ID is required.'], + }, + }, + secrets: { + errors: { + clientSecret: ['Client Secret is required.'], + password: [], + user: [], }, }, }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email.tsx index fe0b18b1b2e61..709101396edf0 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email.tsx @@ -14,6 +14,7 @@ import { GenericValidationResult, } from '../../../../types'; import { EmailActionParams, EmailConfig, EmailSecrets, EmailActionConnector } from '../types'; +import { AdditionalEmailServices } from '../../../../../../actions/common'; const emailServices: EuiSelectOption[] = [ { @@ -106,10 +107,13 @@ export function getActionType(): ActionTypeModel(), host: new Array(), service: new Array(), + clientId: new Array(), + tenantId: new Array(), }; const secretsErrors = { user: new Array(), password: new Array(), + clientSecret: new Array(), }; const validationResult = { @@ -122,17 +126,29 @@ export function getActionType(): ActionTypeModel import('./exchange_form')); export const EmailActionConnectorFields: React.FunctionComponent< ActionConnectorFieldsProps > = ({ action, editActionConfig, editActionSecrets, errors, readOnly }) => { @@ -61,6 +63,88 @@ export const EmailActionConnectorFields: React.FunctionComponent< password !== undefined && errors.password !== undefined && errors.password.length > 0; const isUserInvalid: boolean = user !== undefined && errors.user !== undefined && errors.user.length > 0; + + const authForm = ( + <> + {getEncryptedFieldNotifyLabel( + !action.id, + 2, + action.isMissingSecrets ?? false, + i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.emailAction.reenterValuesLabel', + { + defaultMessage: + 'Username and password are encrypted. Please reenter values for these fields.', + } + ) + )} + + + + { + editActionSecrets('user', nullableString(e.target.value)); + }} + onBlur={() => { + if (!user) { + editActionSecrets('user', ''); + } + }} + /> + + + + + { + editActionSecrets('password', nullableString(e.target.value)); + }} + onBlur={() => { + if (!password) { + editActionSecrets('password', ''); + } + }} + /> + + + + + ); + return ( <> @@ -130,214 +214,149 @@ export const EmailActionConnectorFields: React.FunctionComponent< /> - - - { - editActionConfig('host', e.target.value); - }} - onBlur={() => { - if (!host) { - editActionConfig('host', ''); - } - }} - /> - - - + + {service === AdditionalEmailServices.EXCHANGE ? ( + + ) : ( + <> - { - editActionConfig('port', parseInt(e.target.value, 10)); + editActionConfig('host', e.target.value); }} onBlur={() => { - if (!port) { - editActionConfig('port', 0); + if (!host) { + editActionConfig('host', ''); } }} /> - - - + + { - editActionConfig('secure', e.target.checked); - }} - /> - - + > + { + editActionConfig('port', parseInt(e.target.value, 10)); + }} + onBlur={() => { + if (!port) { + editActionConfig('port', 0); + } + }} + /> + + + + + + { + editActionConfig('secure', e.target.checked); + }} + /> + + + + - - - - - - -

    - -

    -
    - - { - editActionConfig('hasAuth', e.target.checked); - if (!e.target.checked) { - editActionSecrets('user', null); - editActionSecrets('password', null); - } - }} - /> -
    -
    - {hasAuth ? ( - <> - {getEncryptedFieldNotifyLabel( - !action.id, - 2, - action.isMissingSecrets ?? false, - i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.emailAction.reenterValuesLabel', - { - defaultMessage: - 'Username and password are encrypted. Please reenter values for these fields.', - } - ) - )} - + - + +

    + +

    +
    + + - { - editActionSecrets('user', nullableString(e.target.value)); - }} - onBlur={() => { - if (!user) { - editActionSecrets('user', ''); - } - }} - /> -
    -
    - - { + editActionConfig('hasAuth', e.target.checked); + if (!e.target.checked) { + editActionSecrets('user', null); + editActionSecrets('password', null); } - )} - > - { - editActionSecrets('password', nullableString(e.target.value)); - }} - onBlur={() => { - if (!password) { - editActionSecrets('password', ''); - } - }} - /> - + }} + />
    + {hasAuth ? authForm : null} - ) : null} + )} ); }; // if the string == null or is empty, return null, else return string -function nullableString(str: string | null | undefined) { +export function nullableString(str: string | null | undefined) { if (str == null || str.trim() === '') return null; return str; } diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/exchange_form.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/exchange_form.test.tsx new file mode 100644 index 0000000000000..2a08c9b19e602 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/exchange_form.test.tsx @@ -0,0 +1,76 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { mountWithIntl } from '@kbn/test/jest'; +import { EmailActionConnector } from '../types'; +import ExchangeFormFields from './exchange_form'; + +jest.mock('../../../../common/lib/kibana'); + +describe('ExchangeFormFields renders', () => { + test('should display exchange form fields', () => { + const actionConnector = { + secrets: { + clientSecret: 'user', + }, + id: 'test', + actionTypeId: '.email', + name: 'exchange email', + config: { + from: 'test@test.com', + hasAuth: true, + service: 'exchange_server', + clientId: '123', + tenantId: '1234', + }, + } as EmailActionConnector; + const wrapper = mountWithIntl( + {}} + editActionSecrets={() => {}} + readOnly={false} + /> + ); + expect(wrapper.find('[data-test-subj="emailClientSecret"]').length > 0).toBeTruthy(); + expect(wrapper.find('[data-test-subj="emailClientId"]').length > 0).toBeTruthy(); + expect(wrapper.find('[data-test-subj="emailTenantId"]').length > 0).toBeTruthy(); + }); + + test('exchange field defaults to empty when not defined', () => { + const actionConnector = { + secrets: {}, + id: 'test', + actionTypeId: '.email', + name: 'email', + config: { + from: 'test@test.com', + hasAuth: true, + service: 'exchange_server', + }, + } as EmailActionConnector; + const wrapper = mountWithIntl( + {}} + editActionSecrets={() => {}} + readOnly={false} + /> + ); + expect(wrapper.find('[data-test-subj="emailClientSecret"]').length > 0).toBeTruthy(); + expect(wrapper.find('input[data-test-subj="emailClientSecret"]').prop('value')).toEqual(''); + + expect(wrapper.find('[data-test-subj="emailClientId"]').length > 0).toBeTruthy(); + expect(wrapper.find('input[data-test-subj="emailClientId"]').prop('value')).toEqual(''); + + expect(wrapper.find('[data-test-subj="emailTenantId"]').length > 0).toBeTruthy(); + expect(wrapper.find('input[data-test-subj="emailTenantId"]').prop('value')).toEqual(''); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/exchange_form.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/exchange_form.tsx new file mode 100644 index 0000000000000..52fa53da19cd8 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/exchange_form.tsx @@ -0,0 +1,164 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { + EuiFieldText, + EuiFlexItem, + EuiFlexGroup, + EuiFormRow, + EuiFieldPassword, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { IErrorObject } from '../../../../types'; +import { EmailActionConnector } from '../types'; +import { nullableString } from './email_connector'; +import { getEncryptedFieldNotifyLabel } from '../../get_encrypted_field_notify_label'; + +interface ExchangeFormFieldsProps { + action: EmailActionConnector; + editActionConfig: (property: string, value: unknown) => void; + editActionSecrets: (property: string, value: unknown) => void; + errors: IErrorObject; + readOnly: boolean; +} + +const ExchangeFormFields: React.FunctionComponent = ({ + action, + editActionConfig, + editActionSecrets, + errors, + readOnly, +}) => { + const { tenantId, clientId } = action.config; + const { clientSecret } = action.secrets; + + const isClientIdInvalid: boolean = + clientId !== undefined && errors.clientId !== undefined && errors.clientId.length > 0; + const isTenantIdInvalid: boolean = + tenantId !== undefined && errors.tenantId !== undefined && errors.tenantId.length > 0; + const isClientSecretInvalid: boolean = + clientSecret !== undefined && + errors.clientSecret !== undefined && + errors.clientSecret.length > 0; + + return ( + <> + + + + { + editActionConfig('tenantId', nullableString(e.target.value)); + }} + onBlur={() => { + if (!tenantId) { + editActionConfig('tenantId', ''); + } + }} + /> + + + + + { + editActionConfig('clientId', nullableString(e.target.value)); + }} + onBlur={() => { + if (!clientId) { + editActionConfig('clientId', ''); + } + }} + /> + + + + {getEncryptedFieldNotifyLabel( + !action.id, + 1, + action.isMissingSecrets ?? false, + i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.emailAction.reenterClientSecretLabel', + { + defaultMessage: 'Client Secret is encrypted. Please reenter value for this field.', + } + ) + )} + + + + { + editActionSecrets('clientSecret', nullableString(e.target.value)); + }} + onBlur={() => { + if (!clientSecret) { + editActionSecrets('clientSecret', ''); + } + }} + /> + + + + + ); +}; + +// eslint-disable-next-line import/no-default-export +export { ExchangeFormFields as default }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/translations.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/translations.ts index df68d0d1237ed..38e16f6046184 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/translations.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/translations.ts @@ -21,6 +21,27 @@ export const SENDER_NOT_VALID = i18n.translate( } ); +export const CLIENT_ID_REQUIRED = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.error.requiredClientIdText', + { + defaultMessage: 'Client ID is required.', + } +); + +export const TENANT_ID_REQUIRED = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.error.requiredTenantIdText', + { + defaultMessage: 'Tenant ID is required.', + } +); + +export const CLIENT_SECRET_REQUIRED = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.error.requiredClientSecretText', + { + defaultMessage: 'Client Secret is required.', + } +); + export const PORT_REQUIRED = i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.error.requiredPortText', { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/use_email_config.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/use_email_config.ts index fad71cf5d6385..9e6df1d1a1019 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/use_email_config.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/use_email_config.ts @@ -10,6 +10,7 @@ import { HttpSetup } from 'kibana/public'; import { isEmpty } from 'lodash'; import { EmailConfig } from '../types'; import { getServiceConfig } from './api'; +import { AdditionalEmailServices } from '../../../../../../actions/common'; export function useEmailConfig( http: HttpSetup, @@ -39,9 +40,12 @@ export function useEmailConfig( useEffect(() => { (async () => { if (emailService) { + editActionConfig('service', emailService); + if (emailService === AdditionalEmailServices.EXCHANGE) { + return; + } const serviceConfig = await getEmailServiceConfig(emailService); - editActionConfig('service', emailService); editActionConfig('host', serviceConfig?.host ? serviceConfig.host : ''); editActionConfig('port', serviceConfig?.port ? serviceConfig.port : 0); editActionConfig('secure', null != serviceConfig?.secure ? serviceConfig.secure : false); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/types.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/types.ts index 60e0a0f14b897..abacc5544c712 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/types.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/types.ts @@ -79,11 +79,14 @@ export interface EmailConfig { secure?: boolean; hasAuth: boolean; service: string; + clientId?: string; + tenantId?: string; } export interface EmailSecrets { user: string | null; password: string | null; + clientSecret: string | null; } export type EmailActionConnector = UserConfiguredActionConnector; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/health_check.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/health_check.test.tsx index b998067424edd..ff5992a6542b7 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/health_check.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/health_check.test.tsx @@ -114,7 +114,7 @@ describe('health check', () => { ); expect(action.getAttribute('href')).toMatchInlineSnapshot( - `"https://www.elastic.co/guide/en/kibana/mocked-test-branch/configuring-tls.html"` + `"https://www.elastic.co/guide/en/elasticsearch/reference/mocked-test-branch/security-basic-setup.html#encrypt-internode-communication"` ); }); diff --git a/x-pack/plugins/upgrade_assistant/README.md b/x-pack/plugins/upgrade_assistant/README.md index 255eb94a0318c..6570e7f8d7617 100644 --- a/x-pack/plugins/upgrade_assistant/README.md +++ b/x-pack/plugins/upgrade_assistant/README.md @@ -226,4 +226,29 @@ This is a non-exhaustive list of different error scenarios in Upgrade Assistant. - **Error updating deprecation logging status.** Mock a `404` status code to `PUT /api/upgrade_assistant/deprecation_logging`. Alternatively, edit [this line](https://github.com/elastic/kibana/blob/545c1420c285af8f5eee56f414bd6eca735aea11/x-pack/plugins/upgrade_assistant/public/application/lib/api.ts#L77) locally and replace `deprecation_logging` with `fake_deprecation_logging`. - **Unauthorized error fetching ES deprecations.** Mock a `403` status code to `GET /api/upgrade_assistant/es_deprecations` with the response payload: `{ "statusCode": 403 }` - **Partially upgraded error fetching ES deprecations.** Mock a `426` status code to `GET /api/upgrade_assistant/es_deprecations` with the response payload: `{ "statusCode": 426, "attributes": { "allNodesUpgraded": false } }` -- **Upgraded error fetching ES deprecations.** Mock a `426` status code to `GET /api/upgrade_assistant/es_deprecations` with the response payload: `{ "statusCode": 426, "attributes": { "allNodesUpgraded": true } }` \ No newline at end of file +- **Upgraded error fetching ES deprecations.** Mock a `426` status code to `GET /api/upgrade_assistant/es_deprecations` with the response payload: `{ "statusCode": 426, "attributes": { "allNodesUpgraded": true } }` + +### Telemetry + +The Upgrade Assistant tracks several triggered events in the UI, using Kibana Usage Collection service's [UI counters](https://github.com/elastic/kibana/blob/master/src/plugins/usage_collection/README.mdx#ui-counters). + +**Overview page** +- Component loaded +- Click event for "Create snapshot" button +- Click event for "View deprecation logs in Observability" link +- Click event for "Analyze logs in Discover" link +- Click event for "Reset counter" button + +**ES deprecations page** +- Component loaded +- Click events for starting and stopping reindex tasks +- Click events for upgrading or deleting a Machine Learning snapshot +- Click event for deleting a deprecated index setting + +**Kibana deprecations page** +- Component loaded +- Click event for "Quick resolve" button + +In addition to UI counters, the Upgrade Assistant has a [custom usage collector](https://github.com/elastic/kibana/blob/master/src/plugins/usage_collection/README.mdx#custom-collector). It currently is only responsible for tracking whether the user has deprecation logging enabled or not. + +For testing instructions, refer to the [Kibana Usage Collection service README](https://github.com/elastic/kibana/blob/master/src/plugins/usage_collection/README.mdx#testing). \ No newline at end of file diff --git a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/es_deprecations/deprecations_list.test.ts b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/es_deprecations/deprecations_list.test.ts index 390aeeb6d33e3..3b8a756b8e64c 100644 --- a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/es_deprecations/deprecations_list.test.ts +++ b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/es_deprecations/deprecations_list.test.ts @@ -62,19 +62,23 @@ describe('ES deprecations table', () => { const mlDeprecation = esDeprecationsMockResponse.deprecations[0]; const reindexDeprecation = esDeprecationsMockResponse.deprecations[3]; - // Since upgradeStatusMockResponse includes ML and reindex actions (which require fetching status), there will be 3 requests made - expect(server.requests.length).toBe(totalRequests + 3); - expect(server.requests[server.requests.length - 3].url).toBe( + // Since upgradeStatusMockResponse includes ML and reindex actions (which require fetching status), there will be 4 requests made + expect(server.requests.length).toBe(totalRequests + 4); + expect(server.requests[server.requests.length - 4].url).toBe( `${API_BASE_PATH}/es_deprecations` ); - expect(server.requests[server.requests.length - 2].url).toBe( + expect(server.requests[server.requests.length - 3].url).toBe( `${API_BASE_PATH}/ml_snapshots/${(mlDeprecation.correctiveAction as MlAction).jobId}/${ (mlDeprecation.correctiveAction as MlAction).snapshotId }` ); - expect(server.requests[server.requests.length - 1].url).toBe( + expect(server.requests[server.requests.length - 2].url).toBe( `${API_BASE_PATH}/reindex/${reindexDeprecation.index}` ); + + expect(server.requests[server.requests.length - 1].url).toBe( + `${API_BASE_PATH}/ml_upgrade_mode` + ); }); it('shows critical and warning deprecations count', () => { diff --git a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/es_deprecations/ml_snapshots_deprecation_flyout.test.ts b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/es_deprecations/ml_snapshots_deprecation_flyout.test.ts index bf20d52639699..6bcb3fa95985c 100644 --- a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/es_deprecations/ml_snapshots_deprecation_flyout.test.ts +++ b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/es_deprecations/ml_snapshots_deprecation_flyout.test.ts @@ -23,6 +23,7 @@ describe('Machine learning deprecation flyout', () => { beforeEach(async () => { httpRequestsMockHelpers.setLoadEsDeprecationsResponse(esDeprecationsMockResponse); + httpRequestsMockHelpers.setLoadMlUpgradeModeResponse({ mlUpgradeModeEnabled: false }); httpRequestsMockHelpers.setUpgradeMlSnapshotStatusResponse({ nodeId: 'my_node', snapshotId: MOCK_SNAPSHOT_ID, @@ -131,6 +132,27 @@ describe('Machine learning deprecation flyout', () => { // Verify the upgrade button text changes expect(find('mlSnapshotDetails.upgradeSnapshotButton').text()).toEqual('Retry upgrade'); }); + + it('Disables actions if ml_upgrade_mode is enabled', async () => { + httpRequestsMockHelpers.setLoadMlUpgradeModeResponse({ mlUpgradeModeEnabled: true }); + + await act(async () => { + testBed = await setupElasticsearchPage({ isReadOnlyMode: false }); + }); + + const { actions, exists, component } = testBed; + + component.update(); + + await actions.table.clickDeprecationRowAt('mlSnapshot', 0); + + // Shows an error callout with a docs link + expect(exists('mlSnapshotDetails.mlUpgradeModeEnabledError')).toBe(true); + expect(exists('mlSnapshotDetails.setUpgradeModeDocsLink')).toBe(true); + // Flyout actions should be hidden + expect(exists('mlSnapshotDetails.upgradeSnapshotButton')).toBe(false); + expect(exists('mlSnapshotDetails.deleteSnapshotButton')).toBe(false); + }); }); describe('delete snapshots', () => { diff --git a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/helpers/http_requests.ts b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/helpers/http_requests.ts index 2a3f7f5321b5b..e217ea4c5eff0 100644 --- a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/helpers/http_requests.ts +++ b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/helpers/http_requests.ts @@ -126,6 +126,17 @@ const registerHttpRequestMockHelpers = (server: SinonFakeServer) => { ]); }; + const setLoadMlUpgradeModeResponse = (response?: object, error?: ResponseError) => { + const status = error ? error.statusCode || 400 : 200; + const body = error ? error : response; + + server.respondWith('GET', `${API_BASE_PATH}/ml_upgrade_mode`, [ + status, + { 'Content-Type': 'application/json' }, + JSON.stringify(body), + ]); + }; + return { setLoadCloudBackupStatusResponse, setLoadEsDeprecationsResponse, @@ -136,6 +147,7 @@ const registerHttpRequestMockHelpers = (server: SinonFakeServer) => { setDeleteMlSnapshotResponse, setUpgradeMlSnapshotStatusResponse, setLoadDeprecationLogsCountResponse, + setLoadMlUpgradeModeResponse, }; }; diff --git a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/helpers/setup_environment.tsx b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/helpers/setup_environment.tsx index 8c164c3d66144..1e49bf2927fc1 100644 --- a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/helpers/setup_environment.tsx +++ b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/helpers/setup_environment.tsx @@ -13,6 +13,7 @@ import axiosXhrAdapter from 'axios/lib/adapters/xhr'; import { HttpSetup } from 'src/core/public'; +import { AuthorizationContext, Authorization, Privileges } from '../../../public/shared_imports'; import { AppContextProvider } from '../../../public/application/app_context'; import { apiService } from '../../../public/application/lib/api'; import { breadcrumbService } from '../../../public/application/lib/breadcrumbs'; @@ -25,8 +26,15 @@ const { GlobalFlyoutProvider } = GlobalFlyout; const mockHttpClient = axios.create({ adapter: axiosXhrAdapter }); +const createAuthorizationContextValue = (privileges: Privileges) => { + return { + isLoading: false, + privileges: privileges ?? { hasAllPrivileges: false, missingPrivileges: {} }, + } as Authorization; +}; + export const WithAppDependencies = - (Comp: any, overrides: Record = {}) => + (Comp: any, { privileges, ...overrides }: Record = {}) => (props: Record) => { apiService.setup(mockHttpClient as unknown as HttpSetup); breadcrumbService.setup(() => ''); @@ -34,11 +42,15 @@ export const WithAppDependencies = const appContextMock = getAppContextMock() as unknown as AppDependencies; return ( - - - - - + + + + + + + ); }; diff --git a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/overview/fix_logs_step/fix_logs_step.test.tsx b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/overview/fix_logs_step/fix_logs_step.test.tsx index 6da9612272992..7f58b36d29c36 100644 --- a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/overview/fix_logs_step/fix_logs_step.test.tsx +++ b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/overview/fix_logs_step/fix_logs_step.test.tsx @@ -22,10 +22,13 @@ jest.mock('../../../../public/application/lib/logs_checkpoint', () => { }); import { DeprecationLoggingStatus } from '../../../../common/types'; -import { DEPRECATION_LOGS_SOURCE_ID } from '../../../../common/constants'; import { OverviewTestBed, setupOverviewPage } from '../overview.helpers'; import { setupEnvironment, advanceTime } from '../../helpers'; -import { DEPRECATION_LOGS_COUNT_POLL_INTERVAL_MS } from '../../../../common/constants'; +import { + DEPRECATION_LOGS_INDEX, + DEPRECATION_LOGS_SOURCE_ID, + DEPRECATION_LOGS_COUNT_POLL_INTERVAL_MS, +} from '../../../../common/constants'; const getLoggingResponse = (toggle: boolean): DeprecationLoggingStatus => ({ isDeprecationLogIndexingEnabled: toggle, @@ -389,4 +392,39 @@ describe('Overview - Fix deprecation logs step', () => { expect(exists('apiCompatibilityNoteTitle')).toBe(true); }); }); + + describe('Privileges check', () => { + test(`permissions warning callout is hidden if user has the right privileges`, async () => { + const { exists } = testBed; + + // Index privileges warning callout should not be shown + expect(exists('noIndexPermissionsCallout')).toBe(false); + // Analyze logs and Resolve logs sections should be shown + expect(exists('externalLinksTitle')).toBe(true); + expect(exists('deprecationsCountTitle')).toBe(true); + }); + + test(`doesn't show analyze and resolve logs if it doesn't have the right privileges`, async () => { + await act(async () => { + testBed = await setupOverviewPage({ + privileges: { + hasAllPrivileges: false, + missingPrivileges: { + index: [DEPRECATION_LOGS_INDEX], + }, + }, + }); + }); + + const { exists, component } = testBed; + + component.update(); + + // No index privileges warning callout should be shown + expect(exists('noIndexPermissionsCallout')).toBe(true); + // Analyze logs and Resolve logs sections should be hidden + expect(exists('externalLinksTitle')).toBe(false); + expect(exists('deprecationsCountTitle')).toBe(false); + }); + }); }); diff --git a/x-pack/plugins/upgrade_assistant/common/types.ts b/x-pack/plugins/upgrade_assistant/common/types.ts index 7f2d675cb1b1f..6368e3b1e717c 100644 --- a/x-pack/plugins/upgrade_assistant/common/types.ts +++ b/x-pack/plugins/upgrade_assistant/common/types.ts @@ -138,32 +138,7 @@ export interface UIReindex { stop: boolean; } -export interface UpgradeAssistantTelemetrySavedObject { - ui_open: { - overview: number; - elasticsearch: number; - kibana: number; - }; - ui_reindex: { - close: number; - open: number; - start: number; - stop: number; - }; -} - export interface UpgradeAssistantTelemetry { - ui_open: { - overview: number; - elasticsearch: number; - kibana: number; - }; - ui_reindex: { - close: number; - open: number; - start: number; - stop: number; - }; features: { deprecation_logging: { enabled: boolean; @@ -171,10 +146,6 @@ export interface UpgradeAssistantTelemetry { }; } -export interface UpgradeAssistantTelemetrySavedObjectAttributes { - [key: string]: any; -} - export type MIGRATION_DEPRECATION_LEVEL = 'none' | 'info' | 'warning' | 'critical'; export interface DeprecationInfo { level: MIGRATION_DEPRECATION_LEVEL; diff --git a/x-pack/plugins/upgrade_assistant/public/application/app.tsx b/x-pack/plugins/upgrade_assistant/public/application/app.tsx index 0d32d1b661b84..be2092b46a79a 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/app.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/app.tsx @@ -10,8 +10,9 @@ import { Router, Switch, Route, Redirect } from 'react-router-dom'; import { ScopedHistory } from 'src/core/public'; import { RedirectAppLinks } from '../../../../../src/plugins/kibana_react/public'; -import { APP_WRAPPER_CLASS, GlobalFlyout } from '../shared_imports'; +import { APP_WRAPPER_CLASS, GlobalFlyout, AuthorizationProvider } from '../shared_imports'; import { AppDependencies } from '../types'; +import { API_BASE_PATH } from '../../common/constants'; import { AppContextProvider, useAppContext } from './app_context'; import { EsDeprecations, ComingSoonPrompt, KibanaDeprecations, Overview } from './components'; @@ -46,18 +47,20 @@ export const AppWithRouter = ({ history }: { history: ScopedHistory }) => { export const RootComponent = (dependencies: AppDependencies) => { const { history, - core: { i18n, application }, + core: { i18n, application, http }, } = dependencies.services; return ( - - - - - - - + + + + + + + + + ); }; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/index_settings/flyout.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/index_settings/flyout.tsx index 24c1897fbdd02..e00edb4f3b11d 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/index_settings/flyout.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/index_settings/flyout.tsx @@ -5,8 +5,9 @@ * 2.0. */ -import React from 'react'; +import React, { useCallback } from 'react'; import { i18n } from '@kbn/i18n'; +import { METRIC_TYPE } from '@kbn/analytics'; import { EuiButton, EuiButtonEmpty, @@ -25,6 +26,7 @@ import { } from '@elastic/eui'; import { EnrichedDeprecationInfo, IndexSettingAction } from '../../../../../../common/types'; import type { ResponseError } from '../../../../lib/api'; +import { uiMetricService, UIM_INDEX_SETTINGS_DELETE_CLICK } from '../../../../lib/ui_metric'; import type { Status } from '../../../types'; import { DeprecationBadge } from '../../../shared'; @@ -107,6 +109,11 @@ export const RemoveIndexSettingsFlyout = ({ // Flag used to hide certain parts of the UI if the deprecation has been resolved or is in progress const isResolvable = ['idle', 'error'].includes(statusType); + const onRemoveSettings = useCallback(() => { + uiMetricService.trackUiMetric(METRIC_TYPE.CLICK, UIM_INDEX_SETTINGS_DELETE_CLICK); + removeIndexSettings(index!, (correctiveAction as IndexSettingAction).deprecatedSettings); + }, [correctiveAction, index, removeIndexSettings]); + return ( <> @@ -190,12 +197,7 @@ export const RemoveIndexSettingsFlyout = ({ fill data-test-subj="deleteSettingsButton" color="danger" - onClick={() => - removeIndexSettings( - index!, - (correctiveAction as IndexSettingAction).deprecatedSettings - ) - } + onClick={onRemoveSettings} > {statusType === 'error' ? i18nTexts.retryRemoveButtonLabel diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/ml_snapshots/context.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/ml_snapshots/context.tsx index 972d640d18c5a..3a81c7f1cc8ea 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/ml_snapshots/context.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/ml_snapshots/context.tsx @@ -12,6 +12,7 @@ import { useSnapshotState, SnapshotState } from './use_snapshot_state'; export interface MlSnapshotContext { snapshotState: SnapshotState; + mlUpgradeModeEnabled: boolean; upgradeSnapshot: () => Promise; deleteSnapshot: () => Promise; } @@ -31,12 +32,14 @@ interface Props { children: React.ReactNode; snapshotId: string; jobId: string; + mlUpgradeModeEnabled: boolean; } export const MlSnapshotsStatusProvider: React.FunctionComponent = ({ api, snapshotId, jobId, + mlUpgradeModeEnabled, children, }) => { const { updateSnapshotStatus, snapshotState, upgradeSnapshot, deleteSnapshot } = useSnapshotState( @@ -57,6 +60,7 @@ export const MlSnapshotsStatusProvider: React.FunctionComponent = ({ snapshotState, upgradeSnapshot, deleteSnapshot, + mlUpgradeModeEnabled, }} > {children} diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/ml_snapshots/flyout.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/ml_snapshots/flyout.tsx index 40921981dfa5e..2a36f3e33e83c 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/ml_snapshots/flyout.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/ml_snapshots/flyout.tsx @@ -7,6 +7,8 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { METRIC_TYPE } from '@kbn/analytics'; import { EuiButton, @@ -24,8 +26,14 @@ import { } from '@elastic/eui'; import { EnrichedDeprecationInfo } from '../../../../../../common/types'; +import { + uiMetricService, + UIM_ML_SNAPSHOT_UPGRADE_CLICK, + UIM_ML_SNAPSHOT_DELETE_CLICK, +} from '../../../../lib/ui_metric'; import { DeprecationBadge } from '../../../shared'; import { MlSnapshotContext } from './context'; +import { useAppContext } from '../../../../app_context'; import { SnapshotState } from './use_snapshot_state'; export interface FixSnapshotsFlyoutProps extends MlSnapshotContext { @@ -97,6 +105,28 @@ const i18nTexts = { defaultMessage: 'Learn more about this deprecation', } ), + upgradeModeEnabledErrorTitle: i18n.translate( + 'xpack.upgradeAssistant.esDeprecations.mlSnapshots.upgradeModeEnabledErrorTitle', + { + defaultMessage: 'Machine Learning upgrade mode is enabled', + } + ), + upgradeModeEnabledErrorDescription: (docsLink: string) => ( + + + + ), + }} + /> + ), }; const getDeleteButtonLabel = (snapshotState: SnapshotState) => { @@ -139,15 +169,23 @@ export const FixSnapshotsFlyout = ({ snapshotState, upgradeSnapshot, deleteSnapshot, + mlUpgradeModeEnabled, }: FixSnapshotsFlyoutProps) => { + const { + services: { + core: { docLinks }, + }, + } = useAppContext(); const isResolved = snapshotState.status === 'complete'; const onUpgradeSnapshot = () => { + uiMetricService.trackUiMetric(METRIC_TYPE.CLICK, UIM_ML_SNAPSHOT_UPGRADE_CLICK); upgradeSnapshot(); closeFlyout(); }; const onDeleteSnapshot = () => { + uiMetricService.trackUiMetric(METRIC_TYPE.CLICK, UIM_ML_SNAPSHOT_DELETE_CLICK); deleteSnapshot(); closeFlyout(); }; @@ -179,6 +217,23 @@ export const FixSnapshotsFlyout = ({ )} + + {mlUpgradeModeEnabled && ( + <> + +

    + {i18nTexts.upgradeModeEnabledErrorDescription(docLinks.links.ml.setUpgradeMode)} +

    +
    + + + )} +

    {deprecation.details}

    @@ -196,7 +251,7 @@ export const FixSnapshotsFlyout = ({ - {!isResolved && ( + {!isResolved && !mlUpgradeModeEnabled && ( diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/ml_snapshots/table_row.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/ml_snapshots/table_row.tsx index 76d99373febb3..37dddd8171c83 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/ml_snapshots/table_row.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/ml_snapshots/table_row.tsx @@ -21,6 +21,7 @@ const { useGlobalFlyout } = GlobalFlyout; interface TableRowProps { deprecation: EnrichedDeprecationInfo; rowFieldNames: DeprecationTableColumns[]; + mlUpgradeModeEnabled: boolean; } export const MlSnapshotsTableRowCells: React.FunctionComponent = ({ @@ -85,6 +86,7 @@ export const MlSnapshotsTableRow: React.FunctionComponent = (prop diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/flyout/__snapshots__/checklist_step.test.tsx.snap b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/flyout/__snapshots__/checklist_step.test.tsx.snap index 9357e7d2d9b6c..ae03647042dbc 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/flyout/__snapshots__/checklist_step.test.tsx.snap +++ b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/flyout/__snapshots__/checklist_step.test.tsx.snap @@ -42,7 +42,7 @@ exports[`ChecklistFlyout renders 1`] = ` { + uiMetricService.trackUiMetric(METRIC_TYPE.CLICK, UIM_REINDEX_START_CLICK); + startReindex(); + }, [startReindex]); + + const onStopReindex = useCallback(() => { + uiMetricService.trackUiMetric(METRIC_TYPE.CLICK, UIM_REINDEX_STOP_CLICK); + cancelReindex(); + }, [cancelReindex]); + return ( @@ -124,7 +140,7 @@ export const ChecklistFlyoutStep: React.FunctionComponent<{ /> - + @@ -142,7 +158,7 @@ export const ChecklistFlyoutStep: React.FunctionComponent<{ fill color={status === ReindexStatus.paused ? 'warning' : 'primary'} iconType={status === ReindexStatus.paused ? 'play' : undefined} - onClick={startReindex} + onClick={onStartReindex} isLoading={loading} disabled={loading || !hasRequiredPrivileges} > diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/table_row.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/table_row.tsx index c2a14ca5be858..1059720e66a59 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/table_row.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/table_row.tsx @@ -7,9 +7,15 @@ import React, { useState, useEffect, useCallback } from 'react'; import { EuiTableRowCell } from '@elastic/eui'; +import { METRIC_TYPE } from '@kbn/analytics'; import { EnrichedDeprecationInfo } from '../../../../../../common/types'; import { GlobalFlyout } from '../../../../../shared_imports'; import { useAppContext } from '../../../../app_context'; +import { + uiMetricService, + UIM_REINDEX_CLOSE_FLYOUT_CLICK, + UIM_REINDEX_OPEN_FLYOUT_CLICK, +} from '../../../../lib/ui_metric'; import { DeprecationTableColumns } from '../../../types'; import { EsDeprecationsTableCells } from '../../es_deprecations_table_cells'; import { ReindexResolutionCell } from './resolution_table_cell'; @@ -29,9 +35,6 @@ const ReindexTableRowCells: React.FunctionComponent = ({ }) => { const [showFlyout, setShowFlyout] = useState(false); const reindexState = useReindexContext(); - const { - services: { api }, - } = useAppContext(); const { addContent: addContentToGlobalFlyout, removeContent: removeContentFromGlobalFlyout } = useGlobalFlyout(); @@ -39,8 +42,8 @@ const ReindexTableRowCells: React.FunctionComponent = ({ const closeFlyout = useCallback(async () => { removeContentFromGlobalFlyout('reindexFlyout'); setShowFlyout(false); - await api.sendReindexTelemetryData({ close: true }); - }, [api, removeContentFromGlobalFlyout]); + uiMetricService.trackUiMetric(METRIC_TYPE.CLICK, UIM_REINDEX_CLOSE_FLYOUT_CLICK); + }, [removeContentFromGlobalFlyout]); useEffect(() => { if (showFlyout) { @@ -64,13 +67,9 @@ const ReindexTableRowCells: React.FunctionComponent = ({ useEffect(() => { if (showFlyout) { - async function sendTelemetry() { - await api.sendReindexTelemetryData({ open: true }); - } - - sendTelemetry(); + uiMetricService.trackUiMetric(METRIC_TYPE.CLICK, UIM_REINDEX_OPEN_FLYOUT_CLICK); } - }, [showFlyout, api]); + }, [showFlyout]); return ( <> diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/use_reindex_state.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/use_reindex_state.tsx index b87a509d25a55..e1f01be2e0174 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/use_reindex_state.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/use_reindex_state.tsx @@ -131,8 +131,6 @@ export const useReindexStatus = ({ indexName, api }: { indexName: string; api: A cancelLoadingState: undefined, }); - api.sendReindexTelemetryData({ start: true }); - const { data, error } = await api.startReindexTask(indexName); if (error) { @@ -149,8 +147,6 @@ export const useReindexStatus = ({ indexName, api }: { indexName: string; api: A }, [api, indexName, reindexState, updateStatus]); const cancelReindex = useCallback(async () => { - api.sendReindexTelemetryData({ stop: true }); - const { error } = await api.cancelReindexTask(indexName); setReindexState({ diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/es_deprecations.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/es_deprecations.tsx index 4e1494fab1b0e..98611e3f4f781 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/es_deprecations.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/es_deprecations.tsx @@ -10,10 +10,12 @@ import { withRouter, RouteComponentProps } from 'react-router-dom'; import { EuiPageHeader, EuiSpacer, EuiPageContent } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import { METRIC_TYPE } from '@kbn/analytics'; import { EnrichedDeprecationInfo } from '../../../../common/types'; import { SectionLoading } from '../../../shared_imports'; import { useAppContext } from '../../app_context'; +import { uiMetricService, UIM_ES_DEPRECATIONS_PAGE_LOAD } from '../../lib/ui_metric'; import { getEsDeprecationError } from '../../lib/get_es_deprecation_error'; import { DeprecationsPageLoadingError, NoDeprecationsPrompt, DeprecationCount } from '../shared'; import { EsDeprecationsTable } from './es_deprecations_table'; @@ -54,13 +56,7 @@ export const EsDeprecations = withRouter(({ history }: RouteComponentProps) => { services: { api, breadcrumbs }, } = useAppContext(); - const { - data: esDeprecations, - isLoading, - error, - resendRequest, - isInitialRequest, - } = api.useLoadEsDeprecations(); + const { data: esDeprecations, isLoading, error, resendRequest } = api.useLoadEsDeprecations(); const deprecationsCountByLevel: { warningDeprecations: number; @@ -75,16 +71,8 @@ export const EsDeprecations = withRouter(({ history }: RouteComponentProps) => { }, [breadcrumbs]); useEffect(() => { - if (isLoading === false && isInitialRequest) { - async function sendTelemetryData() { - await api.sendPageTelemetryData({ - elasticsearch: true, - }); - } - - sendTelemetryData(); - } - }, [api, isLoading, isInitialRequest]); + uiMetricService.trackUiMetric(METRIC_TYPE.LOADED, UIM_ES_DEPRECATIONS_PAGE_LOAD); + }, []); if (error) { return ( diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/es_deprecations_table.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/es_deprecations_table.tsx index e37d8dc6f77fc..3d9b554913c5b 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/es_deprecations_table.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/es_deprecations_table.tsx @@ -26,6 +26,7 @@ import { Query, } from '@elastic/eui'; import { EnrichedDeprecationInfo } from '../../../../common/types'; +import { useAppContext } from '../../app_context'; import { MlSnapshotsTableRow, DefaultTableRow, @@ -101,10 +102,19 @@ const cellToLabelMap = { const cellTypes = Object.keys(cellToLabelMap) as DeprecationTableColumns[]; const pageSizeOptions = PAGINATION_CONFIG.pageSizeOptions; -const renderTableRowCells = (deprecation: EnrichedDeprecationInfo) => { +const renderTableRowCells = ( + deprecation: EnrichedDeprecationInfo, + mlUpgradeModeEnabled: boolean +) => { switch (deprecation.correctiveAction?.type) { case 'mlSnapshot': - return ; + return ( + + ); case 'indexSetting': return ; @@ -146,6 +156,13 @@ export const EsDeprecationsTable: React.FunctionComponent = ({ deprecations = [], reload, }) => { + const { + services: { api }, + } = useAppContext(); + + const { data } = api.useLoadMlUpgradeMode(); + const mlUpgradeModeEnabled = !!data?.mlUpgradeModeEnabled; + const [sortConfig, setSortConfig] = useState({ isSortAscending: true, sortField: 'isCritical', @@ -291,7 +308,7 @@ export const EsDeprecationsTable: React.FunctionComponent = ({ {visibleDeprecations.map((deprecation, index) => { return ( - {renderTableRowCells(deprecation)} + {renderTableRowCells(deprecation, mlUpgradeModeEnabled)} ); })} diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/kibana_deprecations/deprecation_details_flyout.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/kibana_deprecations/deprecation_details_flyout.tsx index d41b0a95e7679..6ec9ad175e112 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/kibana_deprecations/deprecation_details_flyout.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/kibana_deprecations/deprecation_details_flyout.tsx @@ -5,9 +5,10 @@ * 2.0. */ -import React from 'react'; +import React, { useCallback } from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; +import { METRIC_TYPE } from '@kbn/analytics'; import { EuiButtonEmpty, @@ -24,6 +25,7 @@ import { EuiSpacer, } from '@elastic/eui'; +import { uiMetricService, UIM_KIBANA_QUICK_RESOLVE_CLICK } from '../../lib/ui_metric'; import type { DeprecationResolutionState, KibanaDeprecationDetails } from './kibana_deprecations'; import { DeprecationBadge } from '../shared'; @@ -134,6 +136,11 @@ export const DeprecationDetailsFlyout = ({ const isCurrent = deprecationResolutionState?.id === deprecation.id; const isResolved = isCurrent && deprecationResolutionState?.resolveDeprecationStatus === 'ok'; + const onResolveDeprecation = useCallback(() => { + uiMetricService.trackUiMetric(METRIC_TYPE.CLICK, UIM_KIBANA_QUICK_RESOLVE_CLICK); + resolveDeprecation(deprecation); + }, [deprecation, resolveDeprecation]); + return ( <> @@ -225,7 +232,7 @@ export const DeprecationDetailsFlyout = ({ resolveDeprecation(deprecation)} + onClick={onResolveDeprecation} isLoading={Boolean( deprecationResolutionState?.resolveDeprecationStatus === 'in_progress' )} diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/kibana_deprecations/kibana_deprecations.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/kibana_deprecations/kibana_deprecations.tsx index 23697b00923c8..b488c84f255cc 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/kibana_deprecations/kibana_deprecations.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/kibana_deprecations/kibana_deprecations.tsx @@ -11,10 +11,12 @@ import { withRouter, RouteComponentProps } from 'react-router-dom'; import { EuiPageContent, EuiPageHeader, EuiSpacer, EuiCallOut } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; +import { METRIC_TYPE } from '@kbn/analytics'; import type { DomainDeprecationDetails } from 'kibana/public'; import { SectionLoading, GlobalFlyout } from '../../../shared_imports'; import { useAppContext } from '../../app_context'; +import { uiMetricService, UIM_KIBANA_DEPRECATIONS_PAGE_LOAD } from '../../lib/ui_metric'; import { DeprecationsPageLoadingError, NoDeprecationsPrompt, DeprecationCount } from '../shared'; import { KibanaDeprecationsTable } from './kibana_deprecations_table'; import { @@ -116,7 +118,6 @@ export const KibanaDeprecations = withRouter(({ history }: RouteComponentProps) services: { core: { deprecations }, breadcrumbs, - api, }, } = useAppContext(); @@ -225,14 +226,8 @@ export const KibanaDeprecations = withRouter(({ history }: RouteComponentProps) ]); useEffect(() => { - async function sendTelemetryData() { - await api.sendPageTelemetryData({ - kibana: true, - }); - } - - sendTelemetryData(); - }, [api]); + uiMetricService.trackUiMetric(METRIC_TYPE.LOADED, UIM_KIBANA_DEPRECATIONS_PAGE_LOAD); + }, []); useEffect(() => { breadcrumbs.setBreadcrumbs('kibanaDeprecations'); diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/overview/backup_step/cloud_backup.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/overview/backup_step/cloud_backup.tsx index 55a6ee8e5c73f..4ab860a0bf6a7 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/overview/backup_step/cloud_backup.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/overview/backup_step/cloud_backup.tsx @@ -9,6 +9,7 @@ import React, { useEffect } from 'react'; import moment from 'moment-timezone'; import { FormattedDate, FormattedTime, FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; +import { METRIC_TYPE } from '@kbn/analytics'; import { EuiLoadingContent, EuiFlexGroup, @@ -21,6 +22,7 @@ import { } from '@elastic/eui'; import { useAppContext } from '../../../app_context'; +import { uiMetricService, UIM_BACKUP_DATA_CLOUD_CLICK } from '../../../lib/ui_metric'; interface Props { cloudSnapshotsUrl: string; @@ -128,11 +130,13 @@ export const CloudBackup: React.FunctionComponent = ({ return ( <> {statusMessage} - - + {/* eslint-disable-next-line @elastic/eui/href-or-on-click */} { + uiMetricService.trackUiMetric(METRIC_TYPE.CLICK, UIM_BACKUP_DATA_CLOUD_CLICK); + }} data-test-subj="cloudSnapshotsLink" target="_blank" iconType="popout" diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/overview/backup_step/on_prem_backup.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/overview/backup_step/on_prem_backup.tsx index 2e2e2bd5ce48e..69100b36db7eb 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/overview/backup_step/on_prem_backup.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/overview/backup_step/on_prem_backup.tsx @@ -8,9 +8,11 @@ import React from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; +import { METRIC_TYPE } from '@kbn/analytics'; import { EuiText, EuiButton, EuiSpacer } from '@elastic/eui'; import { useAppContext } from '../../../app_context'; +import { uiMetricService, UIM_BACKUP_DATA_ON_PREM_CLICK } from '../../../lib/ui_metric'; const SnapshotRestoreAppLink: React.FunctionComponent = () => { const { @@ -22,7 +24,14 @@ const SnapshotRestoreAppLink: React.FunctionComponent = () => { ?.useUrl({ page: 'snapshots' }); return ( - + // eslint-disable-next-line @elastic/eui/href-or-on-click + { + uiMetricService.trackUiMetric(METRIC_TYPE.CLICK, UIM_BACKUP_DATA_ON_PREM_CLICK); + }} + data-test-subj="snapshotRestoreLink" + > ( @@ -30,8 +32,7 @@ const i18nTexts = { /> ), calloutBody: i18n.translate('xpack.upgradeAssistant.overview.verifyChanges.calloutBody', { - defaultMessage: - 'Reset the counter after making changes and continue monitoring to verify that you are no longer using deprecated APIs.', + defaultMessage: `After making changes, reset the counter and continue monitoring to verify you're no longer using deprecated features.`, }), loadingError: i18n.translate('xpack.upgradeAssistant.overview.verifyChanges.loadingError', { defaultMessage: 'An error occurred while retrieving the count of deprecation logs', @@ -72,6 +73,7 @@ export const DeprecationsCountCheckpoint: FunctionComponent = ({ const onResetClick = () => { const now = moment().toISOString(); + uiMetricService.trackUiMetric(METRIC_TYPE.CLICK, UIM_RESET_LOGS_COUNTER_CLICK); setCheckpoint(now); }; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_logs_step/external_links.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_logs_step/external_links.tsx index d027b2f262e9e..69f1f14d4eb58 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_logs_step/external_links.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_logs_step/external_links.tsx @@ -9,10 +9,17 @@ import { encode } from 'rison-node'; import React, { FunctionComponent, useState, useEffect } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; +import { METRIC_TYPE } from '@kbn/analytics'; import { EuiLink, EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiPanel, EuiText } from '@elastic/eui'; -import { useAppContext } from '../../../app_context'; import { DataPublicPluginStart } from '../../../../shared_imports'; +import { useAppContext } from '../../../app_context'; +import { + uiMetricService, + UIM_OBSERVABILITY_CLICK, + UIM_DISCOVER_CLICK, +} from '../../../lib/ui_metric'; + import { DEPRECATION_LOGS_INDEX_PATTERN, DEPRECATION_LOGS_SOURCE_ID, @@ -73,7 +80,14 @@ const DiscoverAppLink: FunctionComponent = ({ checkpoint }) => { }, [dataService, checkpoint, share.url.locators]); return ( - + // eslint-disable-next-line @elastic/eui/href-or-on-click + { + uiMetricService.trackUiMetric(METRIC_TYPE.CLICK, UIM_DISCOVER_CLICK); + }} + data-test-subj="viewDiscoverLogs" + > = ({ checkpoint }) => { ); return ( - + // eslint-disable-next-line @elastic/eui/href-or-on-click + { + uiMetricService.trackUiMetric(METRIC_TYPE.CLICK, UIM_OBSERVABILITY_CLICK); + }} + data-test-subj="viewObserveLogs" + > ( + // NOTE: hardcoding the missing privilege because the WithPrivileges HOC + // doesnt provide a way to retrieve which specific privileges an index + // is missing. + {privilegesMissing?.index?.join(', ')} + ), + privilegesCount: privilegesMissing?.index?.length, + }} + /> + ), }; interface Props { setIsComplete: OverviewStepProps['setIsComplete']; + hasPrivileges: boolean; + privilegesMissing: MissingPrivileges; } -const FixLogsStep: FunctionComponent = ({ setIsComplete }) => { +const FixLogsStep: FunctionComponent = ({ + setIsComplete, + hasPrivileges, + privilegesMissing, +}) => { const state = useDeprecationLogging(); const { services: { @@ -123,7 +151,21 @@ const FixLogsStep: FunctionComponent = ({ setIsComplete }) => { )} - {state.isDeprecationLogIndexingEnabled && ( + {!hasPrivileges && state.isDeprecationLogIndexingEnabled && ( + <> + + +

    {i18nTexts.deniedPrivilegeDescription(privilegesMissing)}

    + + + )} + + {hasPrivileges && state.isDeprecationLogIndexingEnabled && ( <> @@ -168,6 +210,16 @@ export const getFixLogsStep = ({ isComplete, setIsComplete }: OverviewStepProps) status, title: i18nTexts.identifyStepTitle, 'data-test-subj': `fixLogsStep-${status}`, - children: , + children: ( + + {({ hasPrivileges, privilegesMissing, isLoading }) => ( + + )} + + ), }; }; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/overview/overview.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/overview/overview.tsx index 010c9b7367158..1bd6b0059bc23 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/overview/overview.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/overview/overview.tsx @@ -18,9 +18,11 @@ import { EuiPageContent, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import { METRIC_TYPE } from '@kbn/analytics'; import { FormattedMessage } from '@kbn/i18n/react'; import { useAppContext } from '../../app_context'; +import { uiMetricService, UIM_OVERVIEW_PAGE_LOAD } from '../../lib/ui_metric'; import { getBackupStep } from './backup_step'; import { getFixIssuesStep } from './fix_issues_step'; import { getFixLogsStep } from './fix_logs_step'; @@ -33,21 +35,14 @@ export const Overview: FunctionComponent = () => { kibanaVersionInfo: { nextMajor }, services: { breadcrumbs, - api, core: { docLinks }, }, plugins: { cloud }, } = useAppContext(); useEffect(() => { - async function sendTelemetryData() { - await api.sendPageTelemetryData({ - overview: true, - }); - } - - sendTelemetryData(); - }, [api]); + uiMetricService.trackUiMetric(METRIC_TYPE.LOADED, UIM_OVERVIEW_PAGE_LOAD); + }, []); useEffect(() => { breadcrumbs.setBreadcrumbs('overview'); diff --git a/x-pack/plugins/upgrade_assistant/public/application/lib/api.ts b/x-pack/plugins/upgrade_assistant/public/application/lib/api.ts index 11f59822ba1e8..1d51510333ef4 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/lib/api.ts +++ b/x-pack/plugins/upgrade_assistant/public/application/lib/api.ts @@ -65,16 +65,6 @@ export class ApiService { }); } - public async sendPageTelemetryData(telemetryData: { [tabName: string]: boolean }) { - const result = await this.sendRequest({ - path: `${API_BASE_PATH}/stats/ui_open`, - method: 'put', - body: JSON.stringify(telemetryData), - }); - - return result; - } - public useLoadDeprecationLogging() { return this.useRequest<{ isDeprecationLogIndexingEnabled: boolean; @@ -150,16 +140,6 @@ export class ApiService { }); } - public async sendReindexTelemetryData(telemetryData: { [key: string]: boolean }) { - const result = await this.sendRequest({ - path: `${API_BASE_PATH}/stats/ui_reindex`, - method: 'put', - body: JSON.stringify(telemetryData), - }); - - return result; - } - public async getReindexStatus(indexName: string) { return await this.sendRequest({ path: `${API_BASE_PATH}/reindex/${indexName}`, @@ -180,6 +160,15 @@ export class ApiService { method: 'post', }); } + + public useLoadMlUpgradeMode() { + return this.useRequest<{ + mlUpgradeModeEnabled: boolean; + }>({ + path: `${API_BASE_PATH}/ml_upgrade_mode`, + method: 'get', + }); + } } export const apiService = new ApiService(); diff --git a/x-pack/plugins/upgrade_assistant/public/application/lib/ui_metric.ts b/x-pack/plugins/upgrade_assistant/public/application/lib/ui_metric.ts new file mode 100644 index 0000000000000..394f046a8bafe --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/public/application/lib/ui_metric.ts @@ -0,0 +1,49 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { UiCounterMetricType } from '@kbn/analytics'; +import { UsageCollectionSetup } from 'src/plugins/usage_collection/public'; + +export const UIM_APP_NAME = 'upgrade_assistant'; +export const UIM_ES_DEPRECATIONS_PAGE_LOAD = 'es_deprecations_page_load'; +export const UIM_KIBANA_DEPRECATIONS_PAGE_LOAD = 'kibana_deprecations_page_load'; +export const UIM_OVERVIEW_PAGE_LOAD = 'overview_page_load'; +export const UIM_REINDEX_OPEN_FLYOUT_CLICK = 'reindex_open_flyout_click'; +export const UIM_REINDEX_CLOSE_FLYOUT_CLICK = 'reindex_close_flyout_click'; +export const UIM_REINDEX_START_CLICK = 'reindex_start_click'; +export const UIM_REINDEX_STOP_CLICK = 'reindex_stop_click'; +export const UIM_BACKUP_DATA_CLOUD_CLICK = 'backup_data_cloud_click'; +export const UIM_BACKUP_DATA_ON_PREM_CLICK = 'backup_data_on_prem_click'; +export const UIM_RESET_LOGS_COUNTER_CLICK = 'reset_logs_counter_click'; +export const UIM_OBSERVABILITY_CLICK = 'observability_click'; +export const UIM_DISCOVER_CLICK = 'discover_click'; +export const UIM_ML_SNAPSHOT_UPGRADE_CLICK = 'ml_snapshot_upgrade_click'; +export const UIM_ML_SNAPSHOT_DELETE_CLICK = 'ml_snapshot_delete_click'; +export const UIM_INDEX_SETTINGS_DELETE_CLICK = 'index_settings_delete_click'; +export const UIM_KIBANA_QUICK_RESOLVE_CLICK = 'kibana_quick_resolve_click'; + +export class UiMetricService { + private usageCollection: UsageCollectionSetup | undefined; + + public setup(usageCollection: UsageCollectionSetup) { + this.usageCollection = usageCollection; + } + + private track(metricType: UiCounterMetricType, eventName: string | string[]) { + if (!this.usageCollection) { + // Usage collection might be disabled in Kibana config. + return; + } + return this.usageCollection.reportUiCounter(UIM_APP_NAME, metricType, eventName); + } + + public trackUiMetric(metricType: UiCounterMetricType, eventName: string | string[]) { + return this.track(metricType, eventName); + } +} + +export const uiMetricService = new UiMetricService(); diff --git a/x-pack/plugins/upgrade_assistant/public/plugin.ts b/x-pack/plugins/upgrade_assistant/public/plugin.ts index 64c21997b12c4..d688ee510ce1f 100644 --- a/x-pack/plugins/upgrade_assistant/public/plugin.ts +++ b/x-pack/plugins/upgrade_assistant/public/plugin.ts @@ -11,6 +11,7 @@ import { Plugin, CoreSetup, PluginInitializerContext } from 'src/core/public'; import { apiService } from './application/lib/api'; import { breadcrumbService } from './application/lib/breadcrumbs'; +import { uiMetricService } from './application/lib/ui_metric'; import { SetupDependencies, StartDependencies, AppDependencies } from './types'; import { Config } from '../common/config'; @@ -18,7 +19,10 @@ export class UpgradeAssistantUIPlugin implements Plugin { constructor(private ctx: PluginInitializerContext) {} - setup(coreSetup: CoreSetup, { management, cloud, share }: SetupDependencies) { + setup( + coreSetup: CoreSetup, + { management, cloud, share, usageCollection }: SetupDependencies + ) { const { readonly } = this.ctx.config.get(); const appRegistrar = management.sections.section.stack; @@ -34,6 +38,10 @@ export class UpgradeAssistantUIPlugin defaultMessage: 'Upgrade Assistant', }); + if (usageCollection) { + uiMetricService.setup(usageCollection); + } + appRegistrar.registerApp({ id: 'upgrade_assistant', title: pluginName, diff --git a/x-pack/plugins/upgrade_assistant/public/shared_imports.ts b/x-pack/plugins/upgrade_assistant/public/shared_imports.ts index 6fee29cf12938..1d0b1ae51f30f 100644 --- a/x-pack/plugins/upgrade_assistant/public/shared_imports.ts +++ b/x-pack/plugins/upgrade_assistant/public/shared_imports.ts @@ -14,6 +14,12 @@ export { UseRequestResponse, SectionLoading, GlobalFlyout, + WithPrivileges, + Privileges, + MissingPrivileges, + AuthorizationProvider, + AuthorizationContext, + Authorization, } from '../../../../src/plugins/es_ui_shared/public/'; export { Storage } from '../../../../src/plugins/kibana_utils/public'; diff --git a/x-pack/plugins/upgrade_assistant/public/types.ts b/x-pack/plugins/upgrade_assistant/public/types.ts index de5f29593b7c6..e58c90336d856 100644 --- a/x-pack/plugins/upgrade_assistant/public/types.ts +++ b/x-pack/plugins/upgrade_assistant/public/types.ts @@ -10,6 +10,7 @@ import { ManagementSetup } from 'src/plugins/management/public'; import { DataPublicPluginStart } from 'src/plugins/data/public'; import { SharePluginSetup } from 'src/plugins/share/public'; import { CoreStart } from 'src/core/public'; +import { UsageCollectionSetup } from 'src/plugins/usage_collection/public'; import { CloudSetup } from '../../cloud/public'; import { LicensingPluginStart } from '../../licensing/public'; import { BreadcrumbService } from './application/lib/breadcrumbs'; @@ -25,6 +26,7 @@ export interface SetupDependencies { management: ManagementSetup; share: SharePluginSetup; cloud?: CloudSetup; + usageCollection?: UsageCollectionSetup; } export interface StartDependencies { diff --git a/x-pack/plugins/upgrade_assistant/server/lib/telemetry/es_ui_open_apis.test.ts b/x-pack/plugins/upgrade_assistant/server/lib/telemetry/es_ui_open_apis.test.ts deleted file mode 100644 index caff78390b9d1..0000000000000 --- a/x-pack/plugins/upgrade_assistant/server/lib/telemetry/es_ui_open_apis.test.ts +++ /dev/null @@ -1,48 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { savedObjectsRepositoryMock } from 'src/core/server/mocks'; -import { UPGRADE_ASSISTANT_DOC_ID, UPGRADE_ASSISTANT_TYPE } from '../../../common/types'; - -import { upsertUIOpenOption } from './es_ui_open_apis'; - -/** - * Since these route callbacks are so thin, these serve simply as integration tests - * to ensure they're wired up to the lib functions correctly. Business logic is tested - * more thoroughly in the lib/telemetry tests. - */ -describe('Upgrade Assistant Telemetry SavedObject UIOpen', () => { - describe('Upsert UIOpen Option', () => { - it('call saved objects internal repository with the correct info', async () => { - const internalRepo = savedObjectsRepositoryMock.create(); - - await upsertUIOpenOption({ - overview: true, - elasticsearch: true, - kibana: true, - savedObjects: { createInternalRepository: () => internalRepo } as any, - }); - - expect(internalRepo.incrementCounter).toHaveBeenCalledTimes(3); - expect(internalRepo.incrementCounter).toHaveBeenCalledWith( - UPGRADE_ASSISTANT_TYPE, - UPGRADE_ASSISTANT_DOC_ID, - ['ui_open.overview'] - ); - expect(internalRepo.incrementCounter).toHaveBeenCalledWith( - UPGRADE_ASSISTANT_TYPE, - UPGRADE_ASSISTANT_DOC_ID, - ['ui_open.elasticsearch'] - ); - expect(internalRepo.incrementCounter).toHaveBeenCalledWith( - UPGRADE_ASSISTANT_TYPE, - UPGRADE_ASSISTANT_DOC_ID, - ['ui_open.kibana'] - ); - }); - }); -}); diff --git a/x-pack/plugins/upgrade_assistant/server/lib/telemetry/es_ui_open_apis.ts b/x-pack/plugins/upgrade_assistant/server/lib/telemetry/es_ui_open_apis.ts deleted file mode 100644 index 3d463fe4b03ed..0000000000000 --- a/x-pack/plugins/upgrade_assistant/server/lib/telemetry/es_ui_open_apis.ts +++ /dev/null @@ -1,57 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { SavedObjectsServiceStart } from 'src/core/server'; -import { - UIOpen, - UIOpenOption, - UPGRADE_ASSISTANT_DOC_ID, - UPGRADE_ASSISTANT_TYPE, -} from '../../../common/types'; - -interface IncrementUIOpenDependencies { - uiOpenOptionCounter: UIOpenOption; - savedObjects: SavedObjectsServiceStart; -} - -async function incrementUIOpenOptionCounter({ - savedObjects, - uiOpenOptionCounter, -}: IncrementUIOpenDependencies) { - const internalRepository = savedObjects.createInternalRepository(); - - await internalRepository.incrementCounter(UPGRADE_ASSISTANT_TYPE, UPGRADE_ASSISTANT_DOC_ID, [ - `ui_open.${uiOpenOptionCounter}`, - ]); -} - -type UpsertUIOpenOptionDependencies = UIOpen & { savedObjects: SavedObjectsServiceStart }; - -export async function upsertUIOpenOption({ - overview, - elasticsearch, - savedObjects, - kibana, -}: UpsertUIOpenOptionDependencies): Promise { - if (overview) { - await incrementUIOpenOptionCounter({ savedObjects, uiOpenOptionCounter: 'overview' }); - } - - if (elasticsearch) { - await incrementUIOpenOptionCounter({ savedObjects, uiOpenOptionCounter: 'elasticsearch' }); - } - - if (kibana) { - await incrementUIOpenOptionCounter({ savedObjects, uiOpenOptionCounter: 'kibana' }); - } - - return { - overview, - elasticsearch, - kibana, - }; -} diff --git a/x-pack/plugins/upgrade_assistant/server/lib/telemetry/es_ui_reindex_apis.test.ts b/x-pack/plugins/upgrade_assistant/server/lib/telemetry/es_ui_reindex_apis.test.ts deleted file mode 100644 index 6a05e8a697bb8..0000000000000 --- a/x-pack/plugins/upgrade_assistant/server/lib/telemetry/es_ui_reindex_apis.test.ts +++ /dev/null @@ -1,52 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { savedObjectsRepositoryMock } from 'src/core/server/mocks'; -import { UPGRADE_ASSISTANT_DOC_ID, UPGRADE_ASSISTANT_TYPE } from '../../../common/types'; -import { upsertUIReindexOption } from './es_ui_reindex_apis'; - -/** - * Since these route callbacks are so thin, these serve simply as integration tests - * to ensure they're wired up to the lib functions correctly. Business logic is tested - * more thoroughly in the lib/telemetry tests. - */ -describe('Upgrade Assistant Telemetry SavedObject UIReindex', () => { - describe('Upsert UIReindex Option', () => { - it('call saved objects internal repository with the correct info', async () => { - const internalRepo = savedObjectsRepositoryMock.create(); - await upsertUIReindexOption({ - close: true, - open: true, - start: true, - stop: true, - savedObjects: { createInternalRepository: () => internalRepo } as any, - }); - - expect(internalRepo.incrementCounter).toHaveBeenCalledTimes(4); - expect(internalRepo.incrementCounter).toHaveBeenCalledWith( - UPGRADE_ASSISTANT_TYPE, - UPGRADE_ASSISTANT_DOC_ID, - [`ui_reindex.close`] - ); - expect(internalRepo.incrementCounter).toHaveBeenCalledWith( - UPGRADE_ASSISTANT_TYPE, - UPGRADE_ASSISTANT_DOC_ID, - [`ui_reindex.open`] - ); - expect(internalRepo.incrementCounter).toHaveBeenCalledWith( - UPGRADE_ASSISTANT_TYPE, - UPGRADE_ASSISTANT_DOC_ID, - [`ui_reindex.start`] - ); - expect(internalRepo.incrementCounter).toHaveBeenCalledWith( - UPGRADE_ASSISTANT_TYPE, - UPGRADE_ASSISTANT_DOC_ID, - [`ui_reindex.stop`] - ); - }); - }); -}); diff --git a/x-pack/plugins/upgrade_assistant/server/lib/telemetry/es_ui_reindex_apis.ts b/x-pack/plugins/upgrade_assistant/server/lib/telemetry/es_ui_reindex_apis.ts deleted file mode 100644 index caee1a58a4006..0000000000000 --- a/x-pack/plugins/upgrade_assistant/server/lib/telemetry/es_ui_reindex_apis.ts +++ /dev/null @@ -1,63 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { SavedObjectsServiceStart } from 'src/core/server'; -import { - UIReindex, - UIReindexOption, - UPGRADE_ASSISTANT_DOC_ID, - UPGRADE_ASSISTANT_TYPE, -} from '../../../common/types'; - -interface IncrementUIReindexOptionDependencies { - uiReindexOptionCounter: UIReindexOption; - savedObjects: SavedObjectsServiceStart; -} - -async function incrementUIReindexOptionCounter({ - savedObjects, - uiReindexOptionCounter, -}: IncrementUIReindexOptionDependencies) { - const internalRepository = savedObjects.createInternalRepository(); - - await internalRepository.incrementCounter(UPGRADE_ASSISTANT_TYPE, UPGRADE_ASSISTANT_DOC_ID, [ - `ui_reindex.${uiReindexOptionCounter}`, - ]); -} - -type UpsertUIReindexOptionDepencies = UIReindex & { savedObjects: SavedObjectsServiceStart }; - -export async function upsertUIReindexOption({ - start, - close, - open, - stop, - savedObjects, -}: UpsertUIReindexOptionDepencies): Promise { - if (close) { - await incrementUIReindexOptionCounter({ savedObjects, uiReindexOptionCounter: 'close' }); - } - - if (open) { - await incrementUIReindexOptionCounter({ savedObjects, uiReindexOptionCounter: 'open' }); - } - - if (start) { - await incrementUIReindexOptionCounter({ savedObjects, uiReindexOptionCounter: 'start' }); - } - - if (stop) { - await incrementUIReindexOptionCounter({ savedObjects, uiReindexOptionCounter: 'stop' }); - } - - return { - close, - open, - start, - stop, - }; -} diff --git a/x-pack/plugins/upgrade_assistant/server/lib/telemetry/usage_collector.test.ts b/x-pack/plugins/upgrade_assistant/server/lib/telemetry/usage_collector.test.ts index 50c5b358aa5cb..34d329557f11e 100644 --- a/x-pack/plugins/upgrade_assistant/server/lib/telemetry/usage_collector.test.ts +++ b/x-pack/plugins/upgrade_assistant/server/lib/telemetry/usage_collector.test.ts @@ -47,26 +47,6 @@ describe('Upgrade Assistant Usage Collector', () => { }; dependencies = { usageCollection, - savedObjects: { - createInternalRepository: jest.fn().mockImplementation(() => { - return { - get: () => { - return { - attributes: { - 'ui_open.overview': 10, - 'ui_open.elasticsearch': 20, - 'ui_open.kibana': 15, - 'ui_reindex.close': 1, - 'ui_reindex.open': 4, - 'ui_reindex.start': 2, - 'ui_reindex.stop': 1, - 'ui_reindex.not_defined': 1, - }, - }; - }, - }; - }), - }, elasticsearch: { client: clusterClient, }, @@ -91,17 +71,6 @@ describe('Upgrade Assistant Usage Collector', () => { callClusterStub ); expect(upgradeAssistantStats).toEqual({ - ui_open: { - overview: 10, - elasticsearch: 20, - kibana: 15, - }, - ui_reindex: { - close: 1, - open: 4, - start: 2, - stop: 1, - }, features: { deprecation_logging: { enabled: true, diff --git a/x-pack/plugins/upgrade_assistant/server/lib/telemetry/usage_collector.ts b/x-pack/plugins/upgrade_assistant/server/lib/telemetry/usage_collector.ts index 56932f5e54b06..c535cd14f104d 100644 --- a/x-pack/plugins/upgrade_assistant/server/lib/telemetry/usage_collector.ts +++ b/x-pack/plugins/upgrade_assistant/server/lib/telemetry/usage_collector.ts @@ -5,43 +5,14 @@ * 2.0. */ -import { get } from 'lodash'; -import { - ElasticsearchClient, - ElasticsearchServiceStart, - ISavedObjectsRepository, - SavedObjectsServiceStart, -} from 'src/core/server'; +import { ElasticsearchClient, ElasticsearchServiceStart } from 'src/core/server'; import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; -import { - UPGRADE_ASSISTANT_DOC_ID, - UPGRADE_ASSISTANT_TYPE, - UpgradeAssistantTelemetry, - UpgradeAssistantTelemetrySavedObject, - UpgradeAssistantTelemetrySavedObjectAttributes, -} from '../../../common/types'; +import { UpgradeAssistantTelemetry } from '../../../common/types'; import { isDeprecationLogIndexingEnabled, isDeprecationLoggingEnabled, } from '../es_deprecation_logging_apis'; -async function getSavedObjectAttributesFromRepo( - savedObjectsRepository: ISavedObjectsRepository, - docType: string, - docID: string -) { - try { - return ( - await savedObjectsRepository.get( - docType, - docID - ) - ).attributes; - } catch (e) { - return null; - } -} - async function getDeprecationLoggingStatusValue(esClient: ElasticsearchClient): Promise { try { const { body: loggerDeprecationCallResult } = await esClient.cluster.getSettings({ @@ -57,58 +28,14 @@ async function getDeprecationLoggingStatusValue(esClient: ElasticsearchClient): } } -export async function fetchUpgradeAssistantMetrics( - { client: esClient }: ElasticsearchServiceStart, - savedObjects: SavedObjectsServiceStart -): Promise { - const savedObjectsRepository = savedObjects.createInternalRepository(); - const upgradeAssistantSOAttributes = await getSavedObjectAttributesFromRepo( - savedObjectsRepository, - UPGRADE_ASSISTANT_TYPE, - UPGRADE_ASSISTANT_DOC_ID - ); +export async function fetchUpgradeAssistantMetrics({ + client: esClient, +}: ElasticsearchServiceStart): Promise { const deprecationLoggingStatusValue = await getDeprecationLoggingStatusValue( esClient.asInternalUser ); - const getTelemetrySavedObject = ( - upgradeAssistantTelemetrySavedObjectAttrs: UpgradeAssistantTelemetrySavedObjectAttributes | null - ): UpgradeAssistantTelemetrySavedObject => { - const defaultTelemetrySavedObject = { - ui_open: { - overview: 0, - elasticsearch: 0, - kibana: 0, - }, - ui_reindex: { - close: 0, - open: 0, - start: 0, - stop: 0, - }, - }; - - if (!upgradeAssistantTelemetrySavedObjectAttrs) { - return defaultTelemetrySavedObject; - } - - return { - ui_open: { - overview: get(upgradeAssistantTelemetrySavedObjectAttrs, 'ui_open.overview', 0), - elasticsearch: get(upgradeAssistantTelemetrySavedObjectAttrs, 'ui_open.elasticsearch', 0), - kibana: get(upgradeAssistantTelemetrySavedObjectAttrs, 'ui_open.kibana', 0), - }, - ui_reindex: { - close: get(upgradeAssistantTelemetrySavedObjectAttrs, 'ui_reindex.close', 0), - open: get(upgradeAssistantTelemetrySavedObjectAttrs, 'ui_reindex.open', 0), - start: get(upgradeAssistantTelemetrySavedObjectAttrs, 'ui_reindex.start', 0), - stop: get(upgradeAssistantTelemetrySavedObjectAttrs, 'ui_reindex.stop', 0), - }, - } as UpgradeAssistantTelemetrySavedObject; - }; - return { - ...getTelemetrySavedObject(upgradeAssistantSOAttributes), features: { deprecation_logging: { enabled: deprecationLoggingStatusValue, @@ -119,14 +46,12 @@ export async function fetchUpgradeAssistantMetrics( interface Dependencies { elasticsearch: ElasticsearchServiceStart; - savedObjects: SavedObjectsServiceStart; usageCollection: UsageCollectionSetup; } export function registerUpgradeAssistantUsageCollector({ elasticsearch, usageCollection, - savedObjects, }: Dependencies) { const upgradeAssistantUsageCollector = usageCollection.makeUsageCollector({ @@ -143,34 +68,8 @@ export function registerUpgradeAssistantUsageCollector({ }, }, }, - ui_open: { - elasticsearch: { - type: 'long', - _meta: { - description: 'Number of times a user viewed the list of Elasticsearch deprecations.', - }, - }, - overview: { - type: 'long', - _meta: { - description: 'Number of times a user viewed the Overview page.', - }, - }, - kibana: { - type: 'long', - _meta: { - description: 'Number of times a user viewed the list of Kibana deprecations', - }, - }, - }, - ui_reindex: { - close: { type: 'long' }, - open: { type: 'long' }, - start: { type: 'long' }, - stop: { type: 'long' }, - }, }, - fetch: async () => fetchUpgradeAssistantMetrics(elasticsearch, savedObjects), + fetch: async () => fetchUpgradeAssistantMetrics(elasticsearch), }); usageCollection.registerCollector(upgradeAssistantUsageCollector); diff --git a/x-pack/plugins/upgrade_assistant/server/plugin.ts b/x-pack/plugins/upgrade_assistant/server/plugin.ts index 800aeecc57d55..717f03758f825 100644 --- a/x-pack/plugins/upgrade_assistant/server/plugin.ts +++ b/x-pack/plugins/upgrade_assistant/server/plugin.ts @@ -19,6 +19,7 @@ import { SecurityPluginStart } from '../../security/server'; import { InfraPluginSetup } from '../../infra/server'; import { PluginSetupContract as FeaturesPluginSetup } from '../../features/server'; +import { SecurityPluginSetup } from '../../security/server'; import { LicensingPluginSetup } from '../../licensing/server'; import { DEPRECATION_LOGS_SOURCE_ID, DEPRECATION_LOGS_INDEX } from '../common/constants'; @@ -42,6 +43,7 @@ interface PluginsSetup { licensing: LicensingPluginSetup; features: FeaturesPluginSetup; infra: InfraPluginSetup; + security?: SecurityPluginSetup; } interface PluginsStart { @@ -76,7 +78,7 @@ export class UpgradeAssistantServerPlugin implements Plugin { setup( { http, getStartServices, savedObjects }: CoreSetup, - { usageCollection, features, licensing, infra }: PluginsSetup + { usageCollection, features, licensing, infra, security }: PluginsSetup ) { this.licensing = licensing; @@ -129,6 +131,9 @@ export class UpgradeAssistantServerPlugin implements Plugin { lib: { handleEsError, }, + config: { + isSecurityEnabled: () => security !== undefined && security.license.isEnabled(), + }, }; // Initialize version service with current kibana version @@ -137,11 +142,10 @@ export class UpgradeAssistantServerPlugin implements Plugin { registerRoutes(dependencies, this.getWorker.bind(this)); if (usageCollection) { - getStartServices().then(([{ savedObjects: savedObjectsService, elasticsearch }]) => { + getStartServices().then(([{ elasticsearch }]) => { registerUpgradeAssistantUsageCollector({ elasticsearch, usageCollection, - savedObjects: savedObjectsService, }); }); } diff --git a/x-pack/plugins/upgrade_assistant/server/routes/app.ts b/x-pack/plugins/upgrade_assistant/server/routes/app.ts new file mode 100644 index 0000000000000..682dc83410f81 --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/server/routes/app.ts @@ -0,0 +1,80 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { API_BASE_PATH, DEPRECATION_LOGS_INDEX } from '../../common/constants'; +import { versionCheckHandlerWrapper } from '../lib/es_version_precheck'; +import { Privileges } from '../shared_imports'; +import { RouteDependencies } from '../types'; + +const extractMissingPrivileges = ( + privilegesObject: { [key: string]: Record } = {} +): string[] => + Object.keys(privilegesObject).reduce((privileges: string[], privilegeName: string): string[] => { + if (Object.values(privilegesObject[privilegeName]).some((e) => !e)) { + privileges.push(privilegeName); + } + return privileges; + }, []); + +export function registerAppRoutes({ + router, + lib: { handleEsError }, + config: { isSecurityEnabled }, +}: RouteDependencies) { + router.get( + { + path: `${API_BASE_PATH}/privileges`, + validate: false, + }, + versionCheckHandlerWrapper( + async ( + { + core: { + elasticsearch: { client }, + }, + }, + request, + response + ) => { + const privilegesResult: Privileges = { + hasAllPrivileges: true, + missingPrivileges: { + index: [], + }, + }; + + if (!isSecurityEnabled()) { + return response.ok({ body: privilegesResult }); + } + + try { + const { + body: { has_all_requested: hasAllPrivileges, index }, + } = await client.asCurrentUser.security.hasPrivileges({ + body: { + index: [ + { + names: [DEPRECATION_LOGS_INDEX], + privileges: ['read'], + }, + ], + }, + }); + + if (!hasAllPrivileges) { + privilegesResult.missingPrivileges.index = extractMissingPrivileges(index); + } + + privilegesResult.hasAllPrivileges = hasAllPrivileges; + return response.ok({ body: privilegesResult }); + } catch (error) { + return handleEsError({ error, response }); + } + } + ) + ); +} diff --git a/x-pack/plugins/upgrade_assistant/server/routes/ml_snapshots.test.ts b/x-pack/plugins/upgrade_assistant/server/routes/ml_snapshots.test.ts index 2e53f571ee904..995e3a46cef0e 100644 --- a/x-pack/plugins/upgrade_assistant/server/routes/ml_snapshots.test.ts +++ b/x-pack/plugins/upgrade_assistant/server/routes/ml_snapshots.test.ts @@ -175,6 +175,28 @@ describe('ML snapshots APIs', () => { }); }); + describe('GET /api/upgrade_assistant/ml_upgrade_mode', () => { + it('Retrieves ml upgrade mode', async () => { + ( + routeHandlerContextMock.core.elasticsearch.client.asCurrentUser.ml.info as jest.Mock + ).mockResolvedValue({ + body: { + upgrade_mode: true, + }, + }); + + const resp = await routeDependencies.router.getHandler({ + method: 'get', + pathPattern: '/api/upgrade_assistant/ml_upgrade_mode', + })(routeHandlerContextMock, createRequestMock({}), kibanaResponseFactory); + + expect(resp.status).toEqual(200); + expect(resp.payload).toEqual({ + mlUpgradeModeEnabled: true, + }); + }); + }); + describe('GET /api/upgrade_assistant/ml_snapshots/:jobId/:snapshotId', () => { it('returns "idle" status if saved object does not exist', async () => { ( diff --git a/x-pack/plugins/upgrade_assistant/server/routes/ml_snapshots.ts b/x-pack/plugins/upgrade_assistant/server/routes/ml_snapshots.ts index 9611ac90c2848..e3f3c43dedde3 100644 --- a/x-pack/plugins/upgrade_assistant/server/routes/ml_snapshots.ts +++ b/x-pack/plugins/upgrade_assistant/server/routes/ml_snapshots.ts @@ -300,6 +300,37 @@ export function registerMlSnapshotRoutes({ router, lib: { handleEsError } }: Rou ) ); + // Get the ml upgrade mode + router.get( + { + path: `${API_BASE_PATH}/ml_upgrade_mode`, + validate: false, + }, + versionCheckHandlerWrapper( + async ( + { + core: { + elasticsearch: { client: esClient }, + }, + }, + request, + response + ) => { + try { + const { body: mlInfo } = await esClient.asCurrentUser.ml.info(); + + return response.ok({ + body: { + mlUpgradeModeEnabled: mlInfo.upgrade_mode, + }, + }); + } catch (e) { + return handleEsError({ error: e, response }); + } + } + ) + ); + // Delete ML model snapshot router.delete( { diff --git a/x-pack/plugins/upgrade_assistant/server/routes/register_routes.ts b/x-pack/plugins/upgrade_assistant/server/routes/register_routes.ts index 813b25c4a79d0..002f34a489cff 100644 --- a/x-pack/plugins/upgrade_assistant/server/routes/register_routes.ts +++ b/x-pack/plugins/upgrade_assistant/server/routes/register_routes.ts @@ -7,22 +7,22 @@ import { RouteDependencies } from '../types'; +import { registerAppRoutes } from './app'; import { registerCloudBackupStatusRoutes } from './cloud_backup_status'; import { registerESDeprecationRoutes } from './es_deprecations'; import { registerDeprecationLoggingRoutes } from './deprecation_logging'; import { registerReindexIndicesRoutes } from './reindex_indices'; -import { registerTelemetryRoutes } from './telemetry'; import { registerUpdateSettingsRoute } from './update_index_settings'; import { registerMlSnapshotRoutes } from './ml_snapshots'; import { ReindexWorker } from '../lib/reindexing'; import { registerUpgradeStatusRoute } from './status'; export function registerRoutes(dependencies: RouteDependencies, getWorker: () => ReindexWorker) { + registerAppRoutes(dependencies); registerCloudBackupStatusRoutes(dependencies); registerESDeprecationRoutes(dependencies); registerDeprecationLoggingRoutes(dependencies); registerReindexIndicesRoutes(dependencies, getWorker); - registerTelemetryRoutes(dependencies); registerUpdateSettingsRoute(dependencies); registerMlSnapshotRoutes(dependencies); // Route for cloud to retrieve the upgrade status for ES and Kibana diff --git a/x-pack/plugins/upgrade_assistant/server/routes/telemetry.test.ts b/x-pack/plugins/upgrade_assistant/server/routes/telemetry.test.ts deleted file mode 100644 index 578cceb702751..0000000000000 --- a/x-pack/plugins/upgrade_assistant/server/routes/telemetry.test.ts +++ /dev/null @@ -1,187 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { kibanaResponseFactory } from 'src/core/server'; -import { savedObjectsServiceMock } from 'src/core/server/mocks'; -import { createMockRouter, MockRouter, routeHandlerContextMock } from './__mocks__/routes.mock'; -import { createRequestMock } from './__mocks__/request.mock'; - -jest.mock('../lib/telemetry/es_ui_open_apis', () => ({ - upsertUIOpenOption: jest.fn(), -})); - -jest.mock('../lib/telemetry/es_ui_reindex_apis', () => ({ - upsertUIReindexOption: jest.fn(), -})); - -import { upsertUIOpenOption } from '../lib/telemetry/es_ui_open_apis'; -import { upsertUIReindexOption } from '../lib/telemetry/es_ui_reindex_apis'; -import { registerTelemetryRoutes } from './telemetry'; - -/** - * Since these route callbacks are so thin, these serve simply as integration tests - * to ensure they're wired up to the lib functions correctly. Business logic is tested - * more thoroughly in the lib/telemetry tests. - */ -describe('Upgrade Assistant Telemetry API', () => { - let routeDependencies: any; - let mockRouter: MockRouter; - beforeEach(() => { - mockRouter = createMockRouter(); - routeDependencies = { - getSavedObjectsService: () => savedObjectsServiceMock.create(), - router: mockRouter, - }; - registerTelemetryRoutes(routeDependencies); - }); - afterEach(() => jest.clearAllMocks()); - - describe('PUT /api/upgrade_assistant/stats/ui_open', () => { - it('returns correct payload with single option', async () => { - const returnPayload = { - overview: true, - elasticsearch: false, - kibana: false, - }; - - (upsertUIOpenOption as jest.Mock).mockResolvedValue(returnPayload); - - const resp = await routeDependencies.router.getHandler({ - method: 'put', - pathPattern: '/api/upgrade_assistant/stats/ui_open', - })( - routeHandlerContextMock, - createRequestMock({ body: returnPayload }), - kibanaResponseFactory - ); - - expect(resp.payload).toEqual(returnPayload); - }); - - it('returns correct payload with multiple option', async () => { - const returnPayload = { - overview: true, - elasticsearch: true, - kibana: true, - }; - - (upsertUIOpenOption as jest.Mock).mockResolvedValue(returnPayload); - - const resp = await routeDependencies.router.getHandler({ - method: 'put', - pathPattern: '/api/upgrade_assistant/stats/ui_open', - })( - routeHandlerContextMock, - createRequestMock({ - body: { - overview: true, - elasticsearch: true, - kibana: true, - }, - }), - kibanaResponseFactory - ); - - expect(resp.payload).toEqual(returnPayload); - }); - - it('returns an error if it throws', async () => { - (upsertUIOpenOption as jest.Mock).mockRejectedValue(new Error(`scary error!`)); - - await expect( - routeDependencies.router.getHandler({ - method: 'put', - pathPattern: '/api/upgrade_assistant/stats/ui_open', - })( - routeHandlerContextMock, - createRequestMock({ - body: { - overview: false, - }, - }), - kibanaResponseFactory - ) - ).rejects.toThrowError('scary error!'); - }); - }); - - describe('PUT /api/upgrade_assistant/stats/ui_reindex', () => { - it('returns correct payload with single option', async () => { - const returnPayload = { - close: false, - open: false, - start: true, - stop: false, - }; - - (upsertUIReindexOption as jest.Mock).mockResolvedValue(returnPayload); - - const resp = await routeDependencies.router.getHandler({ - method: 'put', - pathPattern: '/api/upgrade_assistant/stats/ui_reindex', - })( - routeHandlerContextMock, - createRequestMock({ - body: { - overview: false, - }, - }), - kibanaResponseFactory - ); - - expect(resp.payload).toEqual(returnPayload); - }); - - it('returns correct payload with multiple option', async () => { - const returnPayload = { - close: true, - open: true, - start: true, - stop: true, - }; - - (upsertUIReindexOption as jest.Mock).mockResolvedValue(returnPayload); - - const resp = await routeDependencies.router.getHandler({ - method: 'put', - pathPattern: '/api/upgrade_assistant/stats/ui_reindex', - })( - routeHandlerContextMock, - createRequestMock({ - body: { - close: true, - open: true, - start: true, - stop: true, - }, - }), - kibanaResponseFactory - ); - - expect(resp.payload).toEqual(returnPayload); - }); - - it('returns an error if it throws', async () => { - (upsertUIReindexOption as jest.Mock).mockRejectedValue(new Error(`scary error!`)); - - await expect( - routeDependencies.router.getHandler({ - method: 'put', - pathPattern: '/api/upgrade_assistant/stats/ui_reindex', - })( - routeHandlerContextMock, - createRequestMock({ - body: { - start: false, - }, - }), - kibanaResponseFactory - ) - ).rejects.toThrowError('scary error!'); - }); - }); -}); diff --git a/x-pack/plugins/upgrade_assistant/server/routes/telemetry.ts b/x-pack/plugins/upgrade_assistant/server/routes/telemetry.ts deleted file mode 100644 index d083b38c7c240..0000000000000 --- a/x-pack/plugins/upgrade_assistant/server/routes/telemetry.ts +++ /dev/null @@ -1,64 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { schema } from '@kbn/config-schema'; -import { API_BASE_PATH } from '../../common/constants'; -import { upsertUIOpenOption } from '../lib/telemetry/es_ui_open_apis'; -import { upsertUIReindexOption } from '../lib/telemetry/es_ui_reindex_apis'; -import { RouteDependencies } from '../types'; - -export function registerTelemetryRoutes({ router, getSavedObjectsService }: RouteDependencies) { - router.put( - { - path: `${API_BASE_PATH}/stats/ui_open`, - validate: { - body: schema.object({ - overview: schema.boolean({ defaultValue: false }), - elasticsearch: schema.boolean({ defaultValue: false }), - kibana: schema.boolean({ defaultValue: false }), - }), - }, - }, - async (ctx, request, response) => { - const { elasticsearch, overview, kibana } = request.body; - return response.ok({ - body: await upsertUIOpenOption({ - savedObjects: getSavedObjectsService(), - elasticsearch, - overview, - kibana, - }), - }); - } - ); - - router.put( - { - path: `${API_BASE_PATH}/stats/ui_reindex`, - validate: { - body: schema.object({ - close: schema.boolean({ defaultValue: false }), - open: schema.boolean({ defaultValue: false }), - start: schema.boolean({ defaultValue: false }), - stop: schema.boolean({ defaultValue: false }), - }), - }, - }, - async (ctx, request, response) => { - const { close, open, start, stop } = request.body; - return response.ok({ - body: await upsertUIReindexOption({ - savedObjects: getSavedObjectsService(), - close, - open, - start, - stop, - }), - }); - } - ); -} diff --git a/x-pack/plugins/upgrade_assistant/server/saved_object_types/telemetry_saved_object_type.ts b/x-pack/plugins/upgrade_assistant/server/saved_object_types/telemetry_saved_object_type.ts index 42d5d339dd050..cb3fbcaef59b7 100644 --- a/x-pack/plugins/upgrade_assistant/server/saved_object_types/telemetry_saved_object_type.ts +++ b/x-pack/plugins/upgrade_assistant/server/saved_object_types/telemetry_saved_object_type.ts @@ -15,42 +15,6 @@ export const telemetrySavedObjectType: SavedObjectsType = { namespaceType: 'agnostic', mappings: { properties: { - ui_open: { - properties: { - overview: { - type: 'long', - null_value: 0, - }, - elasticsearch: { - type: 'long', - null_value: 0, - }, - kibana: { - type: 'long', - null_value: 0, - }, - }, - }, - ui_reindex: { - properties: { - close: { - type: 'long', - null_value: 0, - }, - open: { - type: 'long', - null_value: 0, - }, - start: { - type: 'long', - null_value: 0, - }, - stop: { - type: 'long', - null_value: 0, - }, - }, - }, features: { properties: { deprecation_logging: { diff --git a/x-pack/plugins/upgrade_assistant/server/shared_imports.ts b/x-pack/plugins/upgrade_assistant/server/shared_imports.ts index 7f55d189457c7..3193c0b4b54a0 100644 --- a/x-pack/plugins/upgrade_assistant/server/shared_imports.ts +++ b/x-pack/plugins/upgrade_assistant/server/shared_imports.ts @@ -6,3 +6,4 @@ */ export { handleEsError } from '../../../../src/plugins/es_ui_shared/server'; +export { Privileges } from '../../../../src/plugins/es_ui_shared/common'; diff --git a/x-pack/plugins/upgrade_assistant/server/types.ts b/x-pack/plugins/upgrade_assistant/server/types.ts index 09272d270333e..376514c59d494 100644 --- a/x-pack/plugins/upgrade_assistant/server/types.ts +++ b/x-pack/plugins/upgrade_assistant/server/types.ts @@ -21,4 +21,7 @@ export interface RouteDependencies { lib: { handleEsError: typeof handleEsError; }; + config: { + isSecurityEnabled: () => boolean; + }; } diff --git a/x-pack/plugins/uptime/common/constants/ui.ts b/x-pack/plugins/uptime/common/constants/ui.ts index 29df2614d0617..659d5727abc0c 100644 --- a/x-pack/plugins/uptime/common/constants/ui.ts +++ b/x-pack/plugins/uptime/common/constants/ui.ts @@ -17,6 +17,8 @@ export const STEP_DETAIL_ROUTE = '/journey/:checkGroupId/step/:stepIndex'; export const SYNTHETIC_CHECK_STEPS_ROUTE = '/journey/:checkGroupId/steps'; +export const MAPPING_ERROR_ROUTE = '/mapping-error'; + export enum STATUS { UP = 'up', DOWN = 'down', diff --git a/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_container.tsx b/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_container.tsx index 835a89e8f7272..726ef59827f9e 100644 --- a/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_container.tsx +++ b/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_container.tsx @@ -13,6 +13,7 @@ import { MonitorListComponent } from './monitor_list'; import { useUrlParams } from '../../../hooks'; import { UptimeRefreshContext } from '../../../contexts'; import { getConnectorsAction, getMonitorAlertsAction } from '../../../state/alerts/alerts'; +import { useMappingCheck } from '../../../hooks/use_mapping_check'; export interface MonitorListProps { filters?: string; @@ -41,6 +42,7 @@ export const MonitorList: React.FC = (props) => { const { lastRefresh } = useContext(UptimeRefreshContext); const monitorList = useSelector(monitorListSelector); + useMappingCheck(monitorList.error); useEffect(() => { dispatch( diff --git a/x-pack/plugins/uptime/public/hooks/use_mapping_check.test.ts b/x-pack/plugins/uptime/public/hooks/use_mapping_check.test.ts new file mode 100644 index 0000000000000..5f17e65d102b4 --- /dev/null +++ b/x-pack/plugins/uptime/public/hooks/use_mapping_check.test.ts @@ -0,0 +1,53 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { shouldRedirect } from './use_mapping_check'; + +describe('useMappingCheck', () => { + describe('should redirect', () => { + it('returns true for appropriate error', () => { + const error = { + request: {}, + response: {}, + body: { + statusCode: 400, + error: 'Bad Request', + message: + '[search_phase_execution_exception: [illegal_argument_exception] Reason: Text fields are not optimised for operations that require per-document field data like aggregations and sorting, so these operations are disabled by default. Please use a keyword field instead. Alternatively, set fielddata=true on [monitor.id] in order to load field data by uninverting the inverted index. Note that this can use significant memory.]: all shards failed', + }, + name: 'Error', + req: {}, + res: {}, + }; + expect(shouldRedirect(error)).toBe(true); + }); + + it('returns false for undefined', () => { + expect(shouldRedirect(undefined)).toBe(false); + }); + + it('returns false for missing body', () => { + expect(shouldRedirect({})).toBe(false); + }); + + it('returns false for incorrect error string', () => { + expect(shouldRedirect({ body: { error: 'not the right type' } })).toBe(false); + }); + + it('returns false for missing body message', () => { + expect(shouldRedirect({ body: { error: 'Bad Request' } })).toBe(false); + }); + + it('returns false for incorrect error message', () => { + expect( + shouldRedirect({ + body: { error: 'Bad Request', message: 'Not the correct kind of error message' }, + }) + ); + }); + }); +}); diff --git a/x-pack/plugins/uptime/public/hooks/use_mapping_check.ts b/x-pack/plugins/uptime/public/hooks/use_mapping_check.ts new file mode 100644 index 0000000000000..d8a7e0fac4065 --- /dev/null +++ b/x-pack/plugins/uptime/public/hooks/use_mapping_check.ts @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useEffect } from 'react'; +import { useHistory } from 'react-router-dom'; +import { MAPPING_ERROR_ROUTE } from '../../common/constants'; + +interface EsBadRequestError { + body?: { + error?: string; + message?: string; + }; +} + +function contains(message: string, phrase: string) { + return message.indexOf(phrase) !== -1; +} + +export function shouldRedirect(error?: EsBadRequestError) { + if (!error || !error.body || error.body.error !== 'Bad Request' || !error.body.message) { + return false; + } + const { message } = error.body; + return ( + contains(message, 'search_phase_execution_exception') || + contains(message, 'Please use a keyword field instead.') || + contains(message, 'set fielddata=true') + ); +} + +export function useMappingCheck(error?: EsBadRequestError) { + const history = useHistory(); + + useEffect(() => { + if (shouldRedirect(error)) { + history.push(MAPPING_ERROR_ROUTE); + } + }, [error, history]); +} diff --git a/x-pack/plugins/uptime/public/hooks/use_telemetry.ts b/x-pack/plugins/uptime/public/hooks/use_telemetry.ts index 05fbd349b8f0f..f5abdb473fb0d 100644 --- a/x-pack/plugins/uptime/public/hooks/use_telemetry.ts +++ b/x-pack/plugins/uptime/public/hooks/use_telemetry.ts @@ -12,6 +12,7 @@ import { API_URLS } from '../../common/constants'; export enum UptimePage { Overview = 'Overview', + MappingError = 'MappingError', Monitor = 'Monitor', Settings = 'Settings', Certificates = 'Certificates', diff --git a/x-pack/plugins/uptime/public/pages/index.ts b/x-pack/plugins/uptime/public/pages/index.ts index 5624f61c3abb5..352ceb39123e8 100644 --- a/x-pack/plugins/uptime/public/pages/index.ts +++ b/x-pack/plugins/uptime/public/pages/index.ts @@ -5,6 +5,7 @@ * 2.0. */ +export { MappingErrorPage } from './mapping_error'; export { MonitorPage } from './monitor'; export { StepDetailPage } from './synthetics/step_detail_page'; export { SettingsPage } from './settings'; diff --git a/x-pack/plugins/uptime/public/pages/mapping_error.tsx b/x-pack/plugins/uptime/public/pages/mapping_error.tsx new file mode 100644 index 0000000000000..9c234700136b0 --- /dev/null +++ b/x-pack/plugins/uptime/public/pages/mapping_error.tsx @@ -0,0 +1,78 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiCode, EuiEmptyPrompt, EuiLink, EuiTitle } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; + +import React from 'react'; + +import { useKibana } from '../../../../../src/plugins/kibana_react/public'; +import { useBreadcrumbs } from '../hooks/use_breadcrumbs'; +import { useTrackPageview } from '../../../observability/public'; + +export const MappingErrorPage = () => { + useTrackPageview({ app: 'uptime', path: 'mapping-error' }); + useTrackPageview({ app: 'uptime', path: 'mapping-error', delay: 15000 }); + + const docLinks = useKibana().services.docLinks; + + useBreadcrumbs([ + { + text: i18n.translate('xpack.uptime.mappingErrorRoute.breadcrumb', { + defaultMessage: 'Mapping error', + }), + }, + ]); + + return ( + +

    + +

    + + } + body={ +
    +

    + setup }} + /> +

    + {docLinks && ( +

    + + docs + + ), + }} + /> +

    + )} +
    + } + /> + ); +}; diff --git a/x-pack/plugins/uptime/public/routes.tsx b/x-pack/plugins/uptime/public/routes.tsx index e151e19180dd4..9f7310b43e556 100644 --- a/x-pack/plugins/uptime/public/routes.tsx +++ b/x-pack/plugins/uptime/public/routes.tsx @@ -11,13 +11,14 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; import { CERTIFICATES_ROUTE, + MAPPING_ERROR_ROUTE, MONITOR_ROUTE, OVERVIEW_ROUTE, SETTINGS_ROUTE, STEP_DETAIL_ROUTE, SYNTHETIC_CHECK_STEPS_ROUTE, } from '../common/constants'; -import { MonitorPage, StepDetailPage, NotFoundPage, SettingsPage } from './pages'; +import { MappingErrorPage, MonitorPage, StepDetailPage, NotFoundPage, SettingsPage } from './pages'; import { CertificatesPage } from './pages/certificates'; import { UptimePage, useUptimeTelemetry } from './hooks'; import { OverviewPageComponent } from './pages/overview'; @@ -142,6 +143,26 @@ const Routes: RouteProps[] = [ rightSideItems: [], }, }, + { + title: i18n.translate('xpack.uptime.mappingErrorRoute.title', { + defaultMessage: 'Synthetics | mapping error', + }), + path: MAPPING_ERROR_ROUTE, + component: MappingErrorPage, + dataTestSubj: 'uptimeMappingErrorPage', + telemetryId: UptimePage.MappingError, + pageHeader: { + pageTitle: ( +
    + +
    + ), + rightSideItems: [], + }, + }, ]; const RouteInit: React.FC> = ({ diff --git a/x-pack/plugins/uptime/server/rest_api/monitors/monitor_list.ts b/x-pack/plugins/uptime/server/rest_api/monitors/monitor_list.ts index d28645bcb21a1..36bc5a80ef47a 100644 --- a/x-pack/plugins/uptime/server/rest_api/monitors/monitor_list.ts +++ b/x-pack/plugins/uptime/server/rest_api/monitors/monitor_list.ts @@ -27,7 +27,7 @@ export const createMonitorListRoute: UMRestApiRouteFactory = (libs) => ({ options: { tags: ['access:uptime-read'], }, - handler: async ({ uptimeEsClient, request }): Promise => { + handler: async ({ uptimeEsClient, request, response }): Promise => { const { dateRangeStart, dateRangeEnd, filters, pagination, statusFilter, pageSize, query } = request.query; @@ -35,20 +35,29 @@ export const createMonitorListRoute: UMRestApiRouteFactory = (libs) => ({ ? JSON.parse(decodeURIComponent(pagination)) : CONTEXT_DEFAULTS.CURSOR_PAGINATION; - const result = await libs.requests.getMonitorStates({ - uptimeEsClient, - dateRangeStart, - dateRangeEnd, - pagination: decodedPagination, - pageSize, - filters, - query, - // this is added to make typescript happy, - // this sort of reassignment used to be further downstream but I've moved it here - // because this code is going to be decomissioned soon - statusFilter: statusFilter || undefined, - }); + try { + const result = await libs.requests.getMonitorStates({ + uptimeEsClient, + dateRangeStart, + dateRangeEnd, + pagination: decodedPagination, + pageSize, + filters, + query, + statusFilter, + }); - return result; + return result; + } catch (e) { + /** + * This particular error is usually indicative of a mapping problem within the user's + * indices. It's relevant for the UI because we will be able to provide the user with a + * tailored message to help them remediate this problem on their own with minimal effort. + */ + if (e.name === 'ResponseError') { + return response.badRequest({ body: e }); + } + throw e; + } }, }); diff --git a/x-pack/test/api_integration/apis/metrics_ui/constants.ts b/x-pack/test/api_integration/apis/metrics_ui/constants.ts index f0ba9b4c368d5..2ca89f2f9ab87 100644 --- a/x-pack/test/api_integration/apis/metrics_ui/constants.ts +++ b/x-pack/test/api_integration/apis/metrics_ui/constants.ts @@ -31,7 +31,8 @@ export const DATES = { 'alert-test-data': { gauge: { min: 1609459200000, // '2022-01-01T00:00:00Z' - max: 1609462800000, // '2021-01-01T01:00:00Z' + max: 1609462800000, // '2021-01-01T01:00:00Z', + midpoint: 1609461000000, // '2021-01-01T00:30:00Z' }, rate: { min: 1609545600000, // '2021-01-02T00:00:00Z' diff --git a/x-pack/test/api_integration/apis/metrics_ui/metric_threshold_alert.ts b/x-pack/test/api_integration/apis/metrics_ui/metric_threshold_alert.ts index 28910bbc6b0c8..66c40e2e6e92d 100644 --- a/x-pack/test/api_integration/apis/metrics_ui/metric_threshold_alert.ts +++ b/x-pack/test/api_integration/apis/metrics_ui/metric_threshold_alert.ts @@ -100,7 +100,7 @@ export default function ({ getService }: FtrProviderContext) { ], }; const timeFrame = { end: gauge.max }; - const results = await evaluateAlert(esClient, params, configuration, timeFrame); + const results = await evaluateAlert(esClient, params, configuration, [], timeFrame); expect(results).to.eql([ { '*': { @@ -123,7 +123,7 @@ export default function ({ getService }: FtrProviderContext) { it('should alert on the last value when the end date is the same as the last event', async () => { const params = { ...baseParams }; const timeFrame = { end: gauge.max }; - const results = await evaluateAlert(esClient, params, configuration, timeFrame); + const results = await evaluateAlert(esClient, params, configuration, [], timeFrame); expect(results).to.eql([ { '*': { @@ -160,7 +160,7 @@ export default function ({ getService }: FtrProviderContext) { ], }; const timeFrame = { end: gauge.max }; - const results = await evaluateAlert(esClient, params, configuration, timeFrame); + const results = await evaluateAlert(esClient, params, configuration, [], timeFrame); expect(results).to.eql([ { dev: { @@ -200,7 +200,7 @@ export default function ({ getService }: FtrProviderContext) { groupBy: ['env'], }; const timeFrame = { end: gauge.max }; - const results = await evaluateAlert(esClient, params, configuration, timeFrame); + const results = await evaluateAlert(esClient, params, configuration, [], timeFrame); expect(results).to.eql([ { dev: { @@ -234,6 +234,53 @@ export default function ({ getService }: FtrProviderContext) { }, ]); }); + + it('should report no data when one of the groups has a data gap', async () => { + const params = { + ...baseParams, + groupBy: ['env'], + }; + const timeFrame = { end: gauge.midpoint }; + const results = await evaluateAlert( + esClient, + params, + configuration, + ['dev', 'prod'], + timeFrame + ); + expect(results).to.eql([ + { + dev: { + timeSize: 5, + timeUnit: 'm', + threshold: [1], + comparator: '>=', + aggType: 'sum', + metric: 'value', + currentValue: null, + timestamp: '2021-01-01T00:25:00.000Z', + shouldFire: [false], + shouldWarn: [false], + isNoData: [true], + isError: false, + }, + prod: { + timeSize: 5, + timeUnit: 'm', + threshold: [1], + comparator: '>=', + aggType: 'sum', + metric: 'value', + currentValue: 0, + timestamp: '2021-01-01T00:25:00.000Z', + shouldFire: [false], + shouldWarn: [false], + isNoData: [false], + isError: false, + }, + }, + ]); + }); }); }); @@ -254,7 +301,7 @@ export default function ({ getService }: FtrProviderContext) { ], }; const timeFrame = { end: rate.max }; - const results = await evaluateAlert(esClient, params, configuration, timeFrame); + const results = await evaluateAlert(esClient, params, configuration, [], timeFrame); expect(results).to.eql([ { '*': { @@ -294,7 +341,7 @@ export default function ({ getService }: FtrProviderContext) { ], }; const timeFrame = { end: rate.max }; - const results = await evaluateAlert(esClient, params, configuration, timeFrame); + const results = await evaluateAlert(esClient, params, configuration, [], timeFrame); expect(results).to.eql([ { dev: { diff --git a/x-pack/test/api_integration/apis/search/search.ts b/x-pack/test/api_integration/apis/search/search.ts index 82b62a61a932d..45e8933bf715f 100644 --- a/x-pack/test/api_integration/apis/search/search.ts +++ b/x-pack/test/api_integration/apis/search/search.ts @@ -14,7 +14,8 @@ export default function ({ getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); describe('search', () => { - describe('post', () => { + // https://github.com/elastic/kibana/issues/113082 + describe.skip('post', () => { it('should return 200 with final response if wait_for_completion_timeout is long enough', async () => { const resp = await supertest .post(`/internal/search/ese`) diff --git a/x-pack/test/api_integration/apis/security_solution/tls.ts b/x-pack/test/api_integration/apis/security_solution/tls.ts index 2308ad7a0bf34..9fa251ded4e6b 100644 --- a/x-pack/test/api_integration/apis/security_solution/tls.ts +++ b/x-pack/test/api_integration/apis/security_solution/tls.ts @@ -24,7 +24,7 @@ const expectedResult = { _id: '16989191B1A93ECECD5FE9E63EBD4B5C3B606D26', subjects: ['CN=edgecert.googleapis.com,O=Google LLC,L=Mountain View,ST=California,C=US'], issuers: ['CN=GTS CA 1O1,O=Google Trust Services,C=US'], - ja3: [], + ja3: ['bd12d76eb0b6787e6a78a14d2ff96c2b'], notAfter: ['2020-05-06T11:52:15.000Z'], }; @@ -41,7 +41,7 @@ const expectedOverviewDestinationResult = { 'CN=*.cdn.mozilla.net,OU=Cloud Services,O=Mozilla Corporation,L=Mountain View,ST=California,C=US', ], issuers: ['CN=DigiCert SHA2 Secure Server CA,O=DigiCert Inc,C=US'], - ja3: [], + ja3: ['b20b44b18b853ef29ab773e921b03422'], notAfter: ['2020-12-09T12:00:00.000Z'], }, }, @@ -67,7 +67,7 @@ const expectedOverviewSourceResult = { 'CN=*.cdn.mozilla.net,OU=Cloud Services,O=Mozilla Corporation,L=Mountain View,ST=California,C=US', ], issuers: ['CN=DigiCert SHA2 Secure Server CA,O=DigiCert Inc,C=US'], - ja3: [], + ja3: ['b20b44b18b853ef29ab773e921b03422'], notAfter: ['2020-12-09T12:00:00.000Z'], }, }, diff --git a/x-pack/test/api_integration/apis/upgrade_assistant/index.ts b/x-pack/test/api_integration/apis/upgrade_assistant/index.ts index 8d753dd661f5f..f6b231f038817 100644 --- a/x-pack/test/api_integration/apis/upgrade_assistant/index.ts +++ b/x-pack/test/api_integration/apis/upgrade_assistant/index.ts @@ -11,6 +11,7 @@ export default function ({ loadTestFile }: FtrProviderContext) { describe('Upgrade Assistant', () => { loadTestFile(require.resolve('./upgrade_assistant')); loadTestFile(require.resolve('./cloud_backup_status')); + loadTestFile(require.resolve('./privileges')); loadTestFile(require.resolve('./es_deprecations')); }); } diff --git a/x-pack/test/api_integration/apis/upgrade_assistant/privileges.ts b/x-pack/test/api_integration/apis/upgrade_assistant/privileges.ts new file mode 100644 index 0000000000000..c5c00c9a33685 --- /dev/null +++ b/x-pack/test/api_integration/apis/upgrade_assistant/privileges.ts @@ -0,0 +1,58 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; + +import { FtrProviderContext } from '../../ftr_provider_context'; +import { DEPRECATION_LOGS_INDEX } from '../../../../plugins/upgrade_assistant/common/constants'; + +export default function ({ getService }: FtrProviderContext) { + const security = getService('security'); + const supertest = getService('supertest'); + const supertestWithoutAuth = getService('supertestWithoutAuth'); + + describe('Privileges', () => { + describe('GET /api/upgrade_assistant/privileges', () => { + it('User with with index privileges', async () => { + const { body } = await supertest + .get('/api/upgrade_assistant/privileges') + .set('kbn-xsrf', 'kibana') + .expect(200); + + expect(body.hasAllPrivileges).to.be(true); + expect(body.missingPrivileges.index.length).to.be(0); + }); + + it('User without index privileges', async () => { + const ROLE_NAME = 'test_role'; + const USER_NAME = 'test_user'; + const USER_PASSWORD = 'test_user'; + + try { + await security.role.create(ROLE_NAME, {}); + await security.user.create(USER_NAME, { + password: USER_PASSWORD, + roles: [ROLE_NAME], + }); + + const { body } = await supertestWithoutAuth + .get('/api/upgrade_assistant/privileges') + .auth(USER_NAME, USER_PASSWORD) + .set('kbn-xsrf', 'kibana') + .send() + .expect(200); + + expect(body.hasAllPrivileges).to.be(false); + expect(body.missingPrivileges.index[0]).to.be(DEPRECATION_LOGS_INDEX); + } finally { + await security.role.delete(ROLE_NAME); + await security.user.delete(USER_NAME); + } + }); + }); + }); +} diff --git a/x-pack/test/apm_api_integration/tests/correlations/errors_failed_transactions.ts b/x-pack/test/apm_api_integration/tests/correlations/errors_failed_transactions.ts deleted file mode 100644 index b3c5302ee2c6b..0000000000000 --- a/x-pack/test/apm_api_integration/tests/correlations/errors_failed_transactions.ts +++ /dev/null @@ -1,83 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import expect from '@kbn/expect'; -import { format } from 'url'; -import { APIReturnType } from '../../../../plugins/apm/public/services/rest/createCallApmApi'; -import archives_metadata from '../../common/fixtures/es_archiver/archives_metadata'; -import { FtrProviderContext } from '../../common/ftr_provider_context'; -import { registry } from '../../common/registry'; - -export default function ApiTest({ getService }: FtrProviderContext) { - const supertest = getService('legacySupertestAsApmReadUser'); - const archiveName = 'apm_8.0.0'; - const range = archives_metadata[archiveName]; - - const url = format({ - pathname: `/api/apm/correlations/errors/failed_transactions`, - query: { - start: range.start, - end: range.end, - fieldNames: 'http.response.status_code,user_agent.name,user_agent.os.name,url.original', - environment: 'ENVIRONMENT_ALL', - kuery: '', - }, - }); - registry.when( - 'correlations errors failed transactions without data', - { config: 'trial', archives: [] }, - () => { - it('handles the empty state', async () => { - const response = await supertest.get(url); - - expect(response.status).to.be(200); - expect(response.body.response).to.be(undefined); - }); - } - ); - - registry.when( - 'correlations errors failed transactions with data and default args', - { config: 'trial', archives: ['apm_8.0.0'] }, - () => { - type ResponseBody = APIReturnType<'GET /api/apm/correlations/errors/failed_transactions'>; - let response: { - status: number; - body: NonNullable; - }; - - before(async () => { - response = await supertest.get(url); - }); - - it('returns successfully', () => { - expect(response.status).to.eql(200); - }); - - it('returns significant terms', () => { - const { significantTerms } = response.body; - expect(significantTerms.length).to.be.greaterThan(0); - - const sortedFieldNames = significantTerms.map(({ fieldName }) => fieldName).sort(); - expectSnapshot(sortedFieldNames).toMatchInline(` - Array [ - "http.response.status_code", - ] - `); - }); - - it('returns a distribution per term', () => { - const { significantTerms } = response.body; - expectSnapshot(significantTerms.map((term) => term.timeseries.length)).toMatchInline(` - Array [ - 31, - ] - `); - }); - } - ); -} diff --git a/x-pack/test/apm_api_integration/tests/correlations/errors_overall.ts b/x-pack/test/apm_api_integration/tests/correlations/errors_overall.ts deleted file mode 100644 index f4e95816a3996..0000000000000 --- a/x-pack/test/apm_api_integration/tests/correlations/errors_overall.ts +++ /dev/null @@ -1,63 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import expect from '@kbn/expect'; -import { SupertestReturnType } from '../../common/apm_api_supertest'; -import archives_metadata from '../../common/fixtures/es_archiver/archives_metadata'; -import { FtrProviderContext } from '../../common/ftr_provider_context'; -import { registry } from '../../common/registry'; - -export default function ApiTest({ getService }: FtrProviderContext) { - const apmApiClient = getService('apmApiClient'); - const archiveName = 'apm_8.0.0'; - const range = archives_metadata[archiveName]; - - const urlConfig = { - endpoint: `GET /api/apm/correlations/errors/overall_timeseries` as const, - params: { - query: { - start: range.start, - end: range.end, - environment: 'ENVIRONMENT_ALL', - kuery: '', - }, - }, - }; - - registry.when( - 'correlations errors overall without data', - { config: 'trial', archives: [] }, - () => { - it('handles the empty state', async () => { - const response = await apmApiClient.readUser(urlConfig); - - expect(response.status).to.be(200); - expect(response.body.overall).to.be(null); - }); - } - ); - - registry.when( - 'correlations errors overall with data and default args', - { config: 'trial', archives: ['apm_8.0.0'] }, - () => { - let response: SupertestReturnType<'GET /api/apm/correlations/errors/overall_timeseries'>; - - before(async () => { - response = await apmApiClient.readUser(urlConfig); - }); - - it('returns successfully', () => { - expect(response.status).to.eql(200); - }); - - it('returns overall distribution', () => { - expectSnapshot(response.body?.overall?.timeseries.length).toMatchInline(`31`); - }); - } - ); -} diff --git a/x-pack/test/apm_api_integration/tests/correlations/latency_overall.ts b/x-pack/test/apm_api_integration/tests/correlations/latency_overall.ts deleted file mode 100644 index 722a9a2bc4fb7..0000000000000 --- a/x-pack/test/apm_api_integration/tests/correlations/latency_overall.ts +++ /dev/null @@ -1,71 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import expect from '@kbn/expect'; -import { format } from 'url'; -import { APIReturnType } from '../../../../plugins/apm/public/services/rest/createCallApmApi'; -import archives_metadata from '../../common/fixtures/es_archiver/archives_metadata'; -import { FtrProviderContext } from '../../common/ftr_provider_context'; -import { registry } from '../../common/registry'; - -export default function ApiTest({ getService }: FtrProviderContext) { - const supertest = getService('legacySupertestAsApmReadUser'); - const archiveName = 'apm_8.0.0'; - const range = archives_metadata[archiveName]; - - const url = format({ - pathname: `/api/apm/correlations/latency/overall_distribution`, - query: { - start: range.start, - end: range.end, - environment: 'ENVIRONMENT_ALL', - kuery: '', - }, - }); - - registry.when( - 'correlations latency overall without data', - { config: 'trial', archives: [] }, - () => { - it('handles the empty state', async () => { - const response = await supertest.get(url); - - expect(response.status).to.be(200); - expect(response.body.response).to.be(undefined); - }); - } - ); - - registry.when( - 'correlations latency overall with data and default args', - { config: 'trial', archives: ['apm_8.0.0'] }, - () => { - type ResponseBody = APIReturnType<'GET /api/apm/correlations/latency/overall_distribution'>; - let response: { - status: number; - body: NonNullable; - }; - - before(async () => { - response = await supertest.get(url); - }); - - it('returns successfully', () => { - expect(response.status).to.eql(200); - }); - - it('returns overall distribution', () => { - // less precision for distributionInterval as it is not exact - expectSnapshot(response.body?.distributionInterval?.toPrecision(2)).toMatchInline( - `"3.8e+5"` - ); - expectSnapshot(response.body?.maxLatency?.toPrecision(2)).toMatchInline(`"5.8e+6"`); - expectSnapshot(response.body?.overallDistribution?.length).toMatchInline(`15`); - }); - } - ); -} diff --git a/x-pack/test/apm_api_integration/tests/correlations/latency_slow_transactions.ts b/x-pack/test/apm_api_integration/tests/correlations/latency_slow_transactions.ts deleted file mode 100644 index c72753a86f6a6..0000000000000 --- a/x-pack/test/apm_api_integration/tests/correlations/latency_slow_transactions.ts +++ /dev/null @@ -1,96 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import expect from '@kbn/expect'; -import { format } from 'url'; -import { APIReturnType } from '../../../../plugins/apm/public/services/rest/createCallApmApi'; -import archives_metadata from '../../common/fixtures/es_archiver/archives_metadata'; -import { FtrProviderContext } from '../../common/ftr_provider_context'; -import { registry } from '../../common/registry'; - -export default function ApiTest({ getService }: FtrProviderContext) { - const supertest = getService('legacySupertestAsApmReadUser'); - const archiveName = 'apm_8.0.0'; - const range = archives_metadata[archiveName]; - - const url = format({ - pathname: `/api/apm/correlations/latency/slow_transactions`, - query: { - start: range.start, - end: range.end, - durationPercentile: 95, - fieldNames: 'user_agent.name,user_agent.os.name,url.original', - maxLatency: 3581640.00000003, - distributionInterval: 238776, - environment: 'ENVIRONMENT_ALL', - kuery: '', - }, - }); - registry.when( - 'correlations latency slow transactions without data', - { config: 'trial', archives: [] }, - () => { - it('handles the empty state', async () => { - const response = await supertest.get(url); - - expect(response.status).to.be(200); - expect(response.body.response).to.be(undefined); - }); - } - ); - - registry.when( - 'correlations latency slow transactions with data and default args', - { config: 'trial', archives: ['apm_8.0.0'] }, - () => { - type ResponseBody = APIReturnType<'GET /api/apm/correlations/latency/slow_transactions'>; - let response: { - status: number; - body: NonNullable; - }; - - before(async () => { - response = await supertest.get(url); - }); - - it('returns successfully', () => { - expect(response.status).to.eql(200); - }); - - it('returns significant terms', () => { - const { significantTerms } = response.body; - expect(significantTerms.length).to.be.greaterThan(0); - - const sortedFieldNames = significantTerms.map(({ fieldName }) => fieldName).sort(); - expectSnapshot(sortedFieldNames).toMatchInline(` - Array [ - "url.original", - "url.original", - "url.original", - "user_agent.name", - "user_agent.name", - "user_agent.os.name", - ] - `); - }); - - it('returns a distribution per term', () => { - const { significantTerms } = response.body; - expectSnapshot(significantTerms.map((term) => term.distribution.length)).toMatchInline(` - Array [ - 15, - 15, - 15, - 15, - 15, - 15, - ] - `); - }); - } - ); -} diff --git a/x-pack/test/apm_api_integration/tests/index.ts b/x-pack/test/apm_api_integration/tests/index.ts index 6c989e61bc6bf..5ea5ad78d9479 100644 --- a/x-pack/test/apm_api_integration/tests/index.ts +++ b/x-pack/test/apm_api_integration/tests/index.ts @@ -29,10 +29,6 @@ export default function apmApiIntegrationTests(providerContext: FtrProviderConte }); // correlations - describe('correlations/latency_slow_transactions', function () { - loadTestFile(require.resolve('./correlations/latency_slow_transactions')); - }); - describe('correlations/failed_transactions', function () { loadTestFile(require.resolve('./correlations/failed_transactions')); }); @@ -41,18 +37,6 @@ export default function apmApiIntegrationTests(providerContext: FtrProviderConte loadTestFile(require.resolve('./correlations/latency')); }); - describe('correlations/latency_overall', function () { - loadTestFile(require.resolve('./correlations/latency_overall')); - }); - - describe('correlations/errors_overall', function () { - loadTestFile(require.resolve('./correlations/errors_overall')); - }); - - describe('correlations/errors_failed_transactions', function () { - loadTestFile(require.resolve('./correlations/errors_failed_transactions')); - }); - describe('metrics_charts/metrics_charts', function () { loadTestFile(require.resolve('./metrics_charts/metrics_charts')); }); diff --git a/x-pack/test/case_api_integration/common/config.ts b/x-pack/test/case_api_integration/common/config.ts index 887e6e7894f98..514b54982ee42 100644 --- a/x-pack/test/case_api_integration/common/config.ts +++ b/x-pack/test/case_api_integration/common/config.ts @@ -93,6 +93,8 @@ export function createTestConfig(name: string, options: CreateTestConfigOptions) .isDirectory() ); + const casesConfig = ['--xpack.cases.enabled=true']; + return { testFiles: testFiles ? testFiles : [require.resolve('../tests/common')], servers, @@ -115,6 +117,7 @@ export function createTestConfig(name: string, options: CreateTestConfigOptions) ...xPackApiIntegrationTestsConfig.get('kbnTestServer'), serverArgs: [ ...xPackApiIntegrationTestsConfig.get('kbnTestServer.serverArgs'), + ...casesConfig, `--xpack.actions.allowedHosts=${JSON.stringify(['localhost', 'some.non.existent.com'])}`, `--xpack.actions.enabledActionTypes=${JSON.stringify(enabledActionTypes)}`, '--xpack.eventLog.logEntries=true', diff --git a/x-pack/test/case_api_integration/common/lib/utils.ts b/x-pack/test/case_api_integration/common/lib/utils.ts index 0021a228ee98b..7367641d71585 100644 --- a/x-pack/test/case_api_integration/common/lib/utils.ts +++ b/x-pack/test/case_api_integration/common/lib/utils.ts @@ -47,6 +47,7 @@ import { AlertResponse, ConnectorMappings, CasesByAlertId, + CaseResolveResponse, } from '../../../../plugins/cases/common/api'; import { getPostCaseRequest, postCollectionReq, postCommentGenAlertReq } from './mock'; import { getCaseUserActionUrl, getSubCasesUrl } from '../../../../plugins/cases/common/api/helpers'; @@ -1066,6 +1067,32 @@ export const getCase = async ({ return theCase; }; +export const resolveCase = async ({ + supertest, + caseId, + includeComments = false, + expectedHttpCode = 200, + auth = { user: superUser, space: null }, +}: { + supertest: SuperTest.SuperTest; + caseId: string; + includeComments?: boolean; + expectedHttpCode?: number; + auth?: { user: User; space: string | null }; +}): Promise => { + const { body: theResolvedCase } = await supertest + .get( + `${getSpaceUrlPrefix( + auth?.space + )}${CASES_URL}/${caseId}/resolve?includeComments=${includeComments}` + ) + .set('kbn-xsrf', 'true') + .auth(auth.user.username, auth.user.password) + .expect(expectedHttpCode); + + return theResolvedCase; +}; + export const findCases = async ({ supertest, query = {}, diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/migrations.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/migrations.ts index f21a0ab460424..af8bf464c198d 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/migrations.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/migrations.ts @@ -11,7 +11,7 @@ import { CASES_URL, SECURITY_SOLUTION_OWNER, } from '../../../../../../plugins/cases/common/constants'; -import { getCase, getCaseSavedObjectsFromES } from '../../../../common/lib/utils'; +import { getCase, getCaseSavedObjectsFromES, resolveCase } from '../../../../common/lib/utils'; // eslint-disable-next-line import/no-default-export export default function createGetTests({ getService }: FtrProviderContext) { @@ -207,5 +207,76 @@ export default function createGetTests({ getService }: FtrProviderContext) { }); }); }); + + describe('7.16.0', () => { + before(async () => { + await esArchiver.load('x-pack/test/functional/es_archives/cases/migrations/7.13.2'); + }); + + after(async () => { + await esArchiver.unload('x-pack/test/functional/es_archives/cases/migrations/7.13.2'); + }); + + describe('resolve', () => { + it('should return exactMatch outcome', async () => { + const { outcome } = await resolveCase({ + supertest, + caseId: 'e49ad6e0-cf9d-11eb-a603-13e7747d215c', + }); + + expect(outcome).to.be('exactMatch'); + }); + + it('should preserve the same case info', async () => { + const { case: theCase } = await resolveCase({ + supertest, + caseId: 'e49ad6e0-cf9d-11eb-a603-13e7747d215c', + }); + + expect(theCase.title).to.be('A case'); + expect(theCase.description).to.be('asdf'); + expect(theCase.owner).to.be(SECURITY_SOLUTION_OWNER); + }); + + it('should preserve the same connector', async () => { + const { case: theCase } = await resolveCase({ + supertest, + caseId: 'e49ad6e0-cf9d-11eb-a603-13e7747d215c', + }); + + expect(theCase.connector).to.eql({ + fields: { + issueType: '10002', + parent: null, + priority: null, + }, + id: 'd68508f0-cf9d-11eb-a603-13e7747d215c', + name: 'Test Jira', + type: '.jira', + }); + }); + + it('should preserve the same external service', async () => { + const { case: theCase } = await resolveCase({ + supertest, + caseId: 'e49ad6e0-cf9d-11eb-a603-13e7747d215c', + }); + + expect(theCase.external_service).to.eql({ + connector_id: 'd68508f0-cf9d-11eb-a603-13e7747d215c', + connector_name: 'Test Jira', + external_id: '10106', + external_title: 'TPN-99', + external_url: 'https://cases-testing.atlassian.net/browse/TPN-99', + pushed_at: '2021-06-17T18:57:45.524Z', + pushed_by: { + email: null, + full_name: 'j@j.com', + username: '711621466', + }, + }); + }); + }); + }); }); } diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/resolve_case.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/resolve_case.ts new file mode 100644 index 0000000000000..27eae507b9a84 --- /dev/null +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/resolve_case.ts @@ -0,0 +1,221 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; + +import { AttributesTypeUser } from '../../../../../../plugins/cases/common/api'; +import { CASES_URL } from '../../../../../../plugins/cases/common/constants'; +import { + defaultUser, + postCaseReq, + postCaseResp, + postCommentUserReq, + getPostCaseRequest, +} from '../../../../common/lib/mock'; +import { + deleteCasesByESQuery, + createCase, + resolveCase, + createComment, + removeServerGeneratedPropertiesFromCase, + removeServerGeneratedPropertiesFromSavedObject, +} from '../../../../common/lib/utils'; +import { + secOnly, + obsOnly, + globalRead, + superUser, + secOnlyRead, + obsOnlyRead, + obsSecRead, + noKibanaPrivileges, + obsSec, +} from '../../../../common/lib/authentication/users'; +import { getUserInfo } from '../../../../common/lib/authentication'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const supertest = getService('supertest'); + const supertestWithoutAuth = getService('supertestWithoutAuth'); + const es = getService('es'); + + describe('resolve_case', () => { + afterEach(async () => { + await deleteCasesByESQuery(es); + }); + + it('should resolve a case with no comments', async () => { + const postedCase = await createCase(supertest, getPostCaseRequest()); + const resolvedCase = await resolveCase({ + supertest, + caseId: postedCase.id, + includeComments: true, + }); + + const data = removeServerGeneratedPropertiesFromCase(resolvedCase.case); + expect(data).to.eql(postCaseResp()); + expect(data.comments?.length).to.eql(0); + }); + + it('should resolve a case with comments', async () => { + const postedCase = await createCase(supertest, postCaseReq); + await createComment({ supertest, caseId: postedCase.id, params: postCommentUserReq }); + const resolvedCase = await resolveCase({ + supertest, + caseId: postedCase.id, + includeComments: true, + }); + + const comment = removeServerGeneratedPropertiesFromSavedObject( + resolvedCase.case.comments![0] as AttributesTypeUser + ); + + expect(resolvedCase.case.comments?.length).to.eql(1); + expect(comment).to.eql({ + type: postCommentUserReq.type, + comment: postCommentUserReq.comment, + associationType: 'case', + created_by: defaultUser, + pushed_at: null, + pushed_by: null, + updated_by: null, + owner: 'securitySolutionFixture', + }); + }); + + it('should return a 400 when passing the includeSubCaseComments', async () => { + const { body } = await supertest + .get(`${CASES_URL}/case-id/resolve?includeSubCaseComments=true`) + .set('kbn-xsrf', 'true') + .send() + .expect(400); + + expect(body.message).to.contain('disabled'); + }); + + it('unhappy path - 404s when case is not there', async () => { + await supertest + .get(`${CASES_URL}/fake-id/resolve`) + .set('kbn-xsrf', 'true') + .send() + .expect(404); + }); + + describe('rbac', () => { + it('should resolve a case', async () => { + const newCase = await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + { + user: superUser, + space: 'space1', + } + ); + + for (const user of [globalRead, superUser, secOnly, secOnlyRead, obsSec, obsSecRead]) { + const resolvedCase = await resolveCase({ + supertest: supertestWithoutAuth, + caseId: newCase.id, + auth: { user, space: 'space1' }, + }); + + expect(resolvedCase.case.owner).to.eql('securitySolutionFixture'); + expect(resolvedCase.outcome).to.eql('exactMatch'); + expect(resolvedCase.alias_target_id).to.eql(undefined); + } + }); + + it('should resolve a case with comments', async () => { + const postedCase = await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + { + user: secOnly, + space: 'space1', + } + ); + + await createComment({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + params: postCommentUserReq, + expectedHttpCode: 200, + auth: { + user: secOnly, + space: 'space1', + }, + }); + + const resolvedCase = await resolveCase({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + includeComments: true, + auth: { user: secOnly, space: 'space1' }, + }); + + const comment = removeServerGeneratedPropertiesFromSavedObject( + resolvedCase.case.comments![0] as AttributesTypeUser + ); + + expect(resolvedCase.case.comments?.length).to.eql(1); + expect(comment).to.eql({ + type: postCommentUserReq.type, + comment: postCommentUserReq.comment, + associationType: 'case', + created_by: getUserInfo(secOnly), + pushed_at: null, + pushed_by: null, + updated_by: null, + owner: 'securitySolutionFixture', + }); + }); + + it('should not resolve a case when the user does not have access to owner', async () => { + const newCase = await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + { + user: superUser, + space: 'space1', + } + ); + + for (const user of [noKibanaPrivileges, obsOnly, obsOnlyRead]) { + await resolveCase({ + supertest: supertestWithoutAuth, + caseId: newCase.id, + expectedHttpCode: 403, + auth: { user, space: 'space1' }, + }); + } + }); + + it('should NOT resolve a case in a space with no permissions', async () => { + const newCase = await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + { + user: superUser, + space: 'space2', + } + ); + + await resolveCase({ + supertest: supertestWithoutAuth, + caseId: newCase.id, + expectedHttpCode: 403, + auth: { user: secOnly, space: 'space2' }, + }); + }); + }); + }); +}; diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/index.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/index.ts index fba60634cc3d7..0b933582d84a5 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/index.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/index.ts @@ -25,6 +25,7 @@ export default ({ loadTestFile }: FtrProviderContext): void => { loadTestFile(require.resolve('./cases/get_case')); loadTestFile(require.resolve('./cases/patch_cases')); loadTestFile(require.resolve('./cases/post_case')); + loadTestFile(require.resolve('./cases/resolve_case')); loadTestFile(require.resolve('./cases/reporters/get_reporters')); loadTestFile(require.resolve('./cases/status/get_status')); loadTestFile(require.resolve('./cases/tags/get_tags')); diff --git a/x-pack/test/case_api_integration/security_only/tests/common/cases/reporters/get_reporters.ts b/x-pack/test/case_api_integration/security_only/tests/common/cases/reporters/get_reporters.ts index f72db1ac1b27e..8266b456ea1f2 100644 --- a/x-pack/test/case_api_integration/security_only/tests/common/cases/reporters/get_reporters.ts +++ b/x-pack/test/case_api_integration/security_only/tests/common/cases/reporters/get_reporters.ts @@ -28,14 +28,17 @@ import { superUserDefaultSpaceAuth, obsSecDefaultSpaceAuth, } from '../../../../utils'; +import { UserInfo } from '../../../../../common/lib/authentication/types'; + +const sortReporters = (reporters: UserInfo[]) => + reporters.sort((a, b) => a.username.localeCompare(b.username)); // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { const supertestWithoutAuth = getService('supertestWithoutAuth'); const es = getService('es'); - // Failing: See https://github.com/elastic/kibana/issues/106658 - describe.skip('get_reporters', () => { + describe('get_reporters', () => { afterEach(async () => { await deleteCasesByESQuery(es); }); @@ -80,7 +83,10 @@ export default ({ getService }: FtrProviderContext): void => { }, }); - expect(reporters).to.eql(scenario.expectedReporters); + // sort reporters to prevent order failure + expect(sortReporters(reporters as unknown as UserInfo[])).to.eql( + sortReporters(scenario.expectedReporters) + ); } }); diff --git a/x-pack/test/functional/apps/discover/feature_controls/discover_spaces.ts b/x-pack/test/functional/apps/discover/feature_controls/discover_spaces.ts index c245b45917497..5e14cc6201ec2 100644 --- a/x-pack/test/functional/apps/discover/feature_controls/discover_spaces.ts +++ b/x-pack/test/functional/apps/discover/feature_controls/discover_spaces.ts @@ -28,6 +28,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await PageObjects.timePicker.setDefaultAbsoluteRange(); } + // FLAKY https://github.com/elastic/kibana/issues/113067 describe('spaces', () => { before(async () => { await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/logstash_functional'); diff --git a/x-pack/test/functional/apps/discover/saved_searches.ts b/x-pack/test/functional/apps/discover/saved_searches.ts index 1d8de9fe9fb6d..ec649935adec2 100644 --- a/x-pack/test/functional/apps/discover/saved_searches.ts +++ b/x-pack/test/functional/apps/discover/saved_searches.ts @@ -18,7 +18,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const panelActionsTimeRange = getService('dashboardPanelTimeRange'); const ecommerceSOPath = 'x-pack/test/functional/fixtures/kbn_archiver/reporting/ecommerce.json'; - describe('Discover Saved Searches', () => { + // FLAKY https://github.com/elastic/kibana/issues/104578 + describe.skip('Discover Saved Searches', () => { before('initialize tests', async () => { await esArchiver.load('x-pack/test/functional/es_archives/reporting/ecommerce'); await kibanaServer.importExport.load(ecommerceSOPath); diff --git a/x-pack/test/functional/apps/ml/anomaly_detection/anomaly_explorer.ts b/x-pack/test/functional/apps/ml/anomaly_detection/anomaly_explorer.ts index 3622c5dba1696..4373da71512e4 100644 --- a/x-pack/test/functional/apps/ml/anomaly_detection/anomaly_explorer.ts +++ b/x-pack/test/functional/apps/ml/anomaly_detection/anomaly_explorer.ts @@ -63,8 +63,7 @@ export default function ({ getService }: FtrProviderContext) { const ml = getService('ml'); const elasticChart = getService('elasticChart'); - // Failing: See https://github.com/elastic/kibana/issues/112405 - describe.skip('anomaly explorer', function () { + describe('anomaly explorer', function () { this.tags(['mlqa']); before(async () => { await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/ml/farequote'); diff --git a/x-pack/test/functional/apps/ml/anomaly_detection/date_nanos_job.ts b/x-pack/test/functional/apps/ml/anomaly_detection/date_nanos_job.ts index ca330bb8e6a0a..d351e8f7057e4 100644 --- a/x-pack/test/functional/apps/ml/anomaly_detection/date_nanos_job.ts +++ b/x-pack/test/functional/apps/ml/anomaly_detection/date_nanos_job.ts @@ -114,8 +114,7 @@ export default function ({ getService }: FtrProviderContext) { }, ]; - // Failing: See https://github.com/elastic/kibana/issues/112194 - describe.skip('job on data set with date_nanos time field', function () { + describe('job on data set with date_nanos time field', function () { this.tags(['mlqa']); before(async () => { await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/ml/event_rate_nanos'); diff --git a/x-pack/test/functional/apps/ml/anomaly_detection/multi_metric_job.ts b/x-pack/test/functional/apps/ml/anomaly_detection/multi_metric_job.ts index 9cc570256f8f1..0c1b1620eb413 100644 --- a/x-pack/test/functional/apps/ml/anomaly_detection/multi_metric_job.ts +++ b/x-pack/test/functional/apps/ml/anomaly_detection/multi_metric_job.ts @@ -71,8 +71,7 @@ export default function ({ getService }: FtrProviderContext) { const calendarId = `wizard-test-calendar_${Date.now()}`; - // Failing: See https://github.com/elastic/kibana/issues/112174 - describe.skip('multi metric', function () { + describe('multi metric', function () { this.tags(['mlqa']); before(async () => { await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/ml/farequote'); diff --git a/x-pack/test/functional/apps/ml/anomaly_detection/saved_search_job.ts b/x-pack/test/functional/apps/ml/anomaly_detection/saved_search_job.ts index 1637369142aa2..fb10414d2d9ef 100644 --- a/x-pack/test/functional/apps/ml/anomaly_detection/saved_search_job.ts +++ b/x-pack/test/functional/apps/ml/anomaly_detection/saved_search_job.ts @@ -265,8 +265,7 @@ export default function ({ getService }: FtrProviderContext) { }, ]; - // Failing: See https://github.com/elastic/kibana/issues/104174 - describe.skip('saved search', function () { + describe('saved search', function () { this.tags(['mlqa']); before(async () => { await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/ml/farequote'); diff --git a/x-pack/test/functional/apps/uptime/certificates.ts b/x-pack/test/functional/apps/uptime/certificates.ts index 70affdf836072..610f07c183782 100644 --- a/x-pack/test/functional/apps/uptime/certificates.ts +++ b/x-pack/test/functional/apps/uptime/certificates.ts @@ -9,19 +9,27 @@ import { FtrProviderContext } from '../../ftr_provider_context'; import { makeCheck } from '../../../api_integration/apis/uptime/rest/helper/make_checks'; import { getSha256 } from '../../../api_integration/apis/uptime/rest/helper/make_tls'; +const BLANK_INDEX_PATH = 'x-pack/test/functional/es_archives/uptime/blank'; + export default ({ getPageObjects, getService }: FtrProviderContext) => { const { uptime } = getPageObjects(['uptime']); const uptimeService = getService('uptime'); + const esArchiver = getService('esArchiver'); const es = getService('es'); describe('certificates', function () { describe('empty certificates', function () { before(async () => { + await esArchiver.load(BLANK_INDEX_PATH); await makeCheck({ es }); await uptime.goToRoot(true); }); + after(async () => { + await esArchiver.unload(BLANK_INDEX_PATH); + }); + it('go to certs page', async () => { await uptimeService.common.waitUntilDataIsLoaded(); await uptimeService.cert.hasViewCertButton(); @@ -34,10 +42,15 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { describe('with certs', function () { before(async () => { + await esArchiver.load(BLANK_INDEX_PATH); await makeCheck({ es, tls: true }); await uptime.goToRoot(true); }); + after(async () => { + await esArchiver.unload(BLANK_INDEX_PATH); + }); + beforeEach(async () => { await makeCheck({ es, tls: true }); }); diff --git a/x-pack/test/functional/apps/uptime/index.ts b/x-pack/test/functional/apps/uptime/index.ts index 501fec5002666..294ea9b393878 100644 --- a/x-pack/test/functional/apps/uptime/index.ts +++ b/x-pack/test/functional/apps/uptime/index.ts @@ -80,5 +80,9 @@ export default ({ loadTestFile, getService }: FtrProviderContext) => { loadTestFile(require.resolve('./ml_anomaly')); loadTestFile(require.resolve('./feature_controls')); }); + + describe('mappings error state', () => { + loadTestFile(require.resolve('./missing_mappings')); + }); }); }; diff --git a/x-pack/test/functional/apps/uptime/missing_mappings.ts b/x-pack/test/functional/apps/uptime/missing_mappings.ts new file mode 100644 index 0000000000000..2483aa45ecef9 --- /dev/null +++ b/x-pack/test/functional/apps/uptime/missing_mappings.ts @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrProviderContext } from '../../ftr_provider_context'; +import { makeCheck } from '../../../api_integration/apis/uptime/rest/helper/make_checks'; + +export default ({ getPageObjects, getService }: FtrProviderContext) => { + const { common } = getPageObjects(['common']); + const uptimeService = getService('uptime'); + + const es = getService('es'); + describe('missing mappings', function () { + before(async () => { + await makeCheck({ es }); + await common.navigateToApp('uptime'); + }); + + it('redirects to mappings error page', async () => { + await uptimeService.common.hasMappingsError(); + }); + }); +}; diff --git a/x-pack/test/functional/es_archives/infra/alerts_test_data/data.json.gz b/x-pack/test/functional/es_archives/infra/alerts_test_data/data.json.gz index 1c76205f4caa2..92815ba80a3a5 100644 Binary files a/x-pack/test/functional/es_archives/infra/alerts_test_data/data.json.gz and b/x-pack/test/functional/es_archives/infra/alerts_test_data/data.json.gz differ diff --git a/x-pack/test/functional/services/uptime/common.ts b/x-pack/test/functional/services/uptime/common.ts index 7d47bcf985943..e3c73a1e1ca97 100644 --- a/x-pack/test/functional/services/uptime/common.ts +++ b/x-pack/test/functional/services/uptime/common.ts @@ -115,5 +115,8 @@ export function UptimeCommonProvider({ getService, getPageObjects }: FtrProvider await testSubjects.missingOrFail('data-missing'); }); }, + async hasMappingsError() { + return testSubjects.exists('xpack.uptime.mappingsErrorPage'); + }, }; } diff --git a/x-pack/test/functional_embedded/tests/iframe_embedded.ts b/x-pack/test/functional_embedded/tests/iframe_embedded.ts index 21bc19424c893..0e2a461dd15f9 100644 --- a/x-pack/test/functional_embedded/tests/iframe_embedded.ts +++ b/x-pack/test/functional_embedded/tests/iframe_embedded.ts @@ -17,7 +17,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const testSubjects = getService('testSubjects'); const retry = getService('retry'); - describe('in iframe', () => { + // FLAKY https://github.com/elastic/kibana/issues/70928 + describe.skip('in iframe', () => { it('should open Kibana for logged-in user', async () => { const isChromeHiddenBefore = await PageObjects.common.isChromeHidden(); expect(isChromeHiddenBefore).to.be(true); diff --git a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alert_create_flyout.ts b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alert_create_flyout.ts index 2ce771f7b993f..88ba4c37559c5 100644 --- a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alert_create_flyout.ts +++ b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alert_create_flyout.ts @@ -103,7 +103,8 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await testSubjects.click('test.always-firing-SelectOption'); } - describe('create alert', function () { + // FLAKY https://github.com/elastic/kibana/issues/112749 + describe.skip('create alert', function () { before(async () => { await pageObjects.common.navigateToApp('triggersActions'); await testSubjects.click('rulesTab'); diff --git a/x-pack/test/search_sessions_integration/tests/apps/discover/sessions_in_space.ts b/x-pack/test/search_sessions_integration/tests/apps/discover/sessions_in_space.ts index 728ad056f4e6b..064c6bdc4495e 100644 --- a/x-pack/test/search_sessions_integration/tests/apps/discover/sessions_in_space.ts +++ b/x-pack/test/search_sessions_integration/tests/apps/discover/sessions_in_space.ts @@ -23,7 +23,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const browser = getService('browser'); const searchSessions = getService('searchSessions'); - describe('discover in space', () => { + // FLAKY https://github.com/elastic/kibana/issues/112913 + describe.skip('discover in space', () => { describe('Storing search sessions in space', () => { before(async () => { await esArchiver.load('x-pack/test/functional/es_archives/dashboard/session_in_space'); diff --git a/x-pack/test/security_solution_endpoint/apps/endpoint/policy_details.ts b/x-pack/test/security_solution_endpoint/apps/endpoint/policy_details.ts index a793582cb7295..95299d8a81f5c 100644 --- a/x-pack/test/security_solution_endpoint/apps/endpoint/policy_details.ts +++ b/x-pack/test/security_solution_endpoint/apps/endpoint/policy_details.ts @@ -21,7 +21,8 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { const testSubjects = getService('testSubjects'); const policyTestResources = getService('policyTestResources'); - describe('When on the Endpoint Policy Details Page', function () { + // FLAKY https://github.com/elastic/kibana/issues/100296 + describe.skip('When on the Endpoint Policy Details Page', function () { describe('with an invalid policy id', () => { it('should display an error', async () => { await pageObjects.policy.navigateToPolicyDetails('invalid-id'); @@ -879,6 +880,14 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { ); expect(await testSubjects.isSelected('policyWindowsEvent_dns')).to.be(wasSelected); }); + + it('should show trusted apps card and link should go back to policy', async () => { + await testSubjects.existOrFail('fleetTrustedAppsCard'); + await (await testSubjects.find('linkToTrustedApps')).click(); + await testSubjects.existOrFail('policyDetailsPage'); + await (await testSubjects.find('policyDetailsBackLink')).click(); + await testSubjects.existOrFail('endpointIntegrationPolicyForm'); + }); }); }); } diff --git a/x-pack/test/security_solution_endpoint/config.ts b/x-pack/test/security_solution_endpoint/config.ts index b00df7732ea4f..2bfb231887ac2 100644 --- a/x-pack/test/security_solution_endpoint/config.ts +++ b/x-pack/test/security_solution_endpoint/config.ts @@ -44,6 +44,8 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { // always install Endpoint package by default when Fleet sets up `--xpack.fleet.packages.0.name=endpoint`, `--xpack.fleet.packages.0.version=latest`, + // TODO: Remove feature flags once we're good to go + '--xpack.securitySolution.enableExperimental=["trustedAppsByPolicyEnabled"]', ], }, layout: { diff --git a/x-pack/test/stack_functional_integration/apps/filebeat/filebeat.js b/x-pack/test/stack_functional_integration/apps/filebeat/filebeat.ts similarity index 83% rename from x-pack/test/stack_functional_integration/apps/filebeat/filebeat.js rename to x-pack/test/stack_functional_integration/apps/filebeat/filebeat.ts index 85570fc8b0158..4bcdc75d7fa50 100644 --- a/x-pack/test/stack_functional_integration/apps/filebeat/filebeat.js +++ b/x-pack/test/stack_functional_integration/apps/filebeat/filebeat.ts @@ -6,8 +6,9 @@ */ import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../functional/ftr_provider_context'; -export default function ({ getService, getPageObjects }) { +export default function ({ getService, getPageObjects }: FtrProviderContext) { describe('check filebeat', function () { const retry = getService('retry'); const PageObjects = getPageObjects(['common', 'discover', 'timePicker']); @@ -17,7 +18,7 @@ export default function ({ getService, getPageObjects }) { await PageObjects.discover.selectIndexPattern('filebeat-*'); await PageObjects.timePicker.setCommonlyUsedTime('Last_30 days'); await retry.try(async () => { - const hitCount = parseInt(await PageObjects.discover.getHitCount()); + const hitCount = parseInt(await PageObjects.discover.getHitCount(), 10); expect(hitCount).to.be.greaterThan(0); }); }); diff --git a/x-pack/test/stack_functional_integration/apps/filebeat/index.js b/x-pack/test/stack_functional_integration/apps/filebeat/index.ts similarity index 70% rename from x-pack/test/stack_functional_integration/apps/filebeat/index.js rename to x-pack/test/stack_functional_integration/apps/filebeat/index.ts index 478914d395c39..24077f40c9324 100644 --- a/x-pack/test/stack_functional_integration/apps/filebeat/index.js +++ b/x-pack/test/stack_functional_integration/apps/filebeat/index.ts @@ -4,8 +4,9 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ +import { FtrProviderContext } from '../../../functional/ftr_provider_context'; -export default function ({ loadTestFile }) { +export default function ({ loadTestFile }: FtrProviderContext) { describe('filebeat app', function () { loadTestFile(require.resolve('./filebeat')); }); diff --git a/x-pack/test/stack_functional_integration/apps/heartbeat/_heartbeat.js b/x-pack/test/stack_functional_integration/apps/heartbeat/_heartbeat.ts similarity index 54% rename from x-pack/test/stack_functional_integration/apps/heartbeat/_heartbeat.js rename to x-pack/test/stack_functional_integration/apps/heartbeat/_heartbeat.ts index 05f03a115f616..801e651d8b92e 100644 --- a/x-pack/test/stack_functional_integration/apps/heartbeat/_heartbeat.js +++ b/x-pack/test/stack_functional_integration/apps/heartbeat/_heartbeat.ts @@ -6,19 +6,23 @@ */ import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../functional/ftr_provider_context'; -export default function ({ getService, getPageObjects }) { +export default function ({ getService, getPageObjects }: FtrProviderContext) { const retry = getService('retry'); const PageObjects = getPageObjects(['common', 'uptime']); - describe('check heartbeat', function () { - it('Uptime app should show snapshot count greater than zero', async function () { + describe('check heartbeat overview page', function () { + it('Uptime app should show 1 UP monitor', async function () { await PageObjects.common.navigateToApp('uptime', { insertTimestamp: false }); await retry.try(async function () { - const upCount = parseInt((await PageObjects.uptime.getSnapshotCount()).up); - expect(upCount).to.be.greaterThan(0); + const upCount = parseInt((await PageObjects.uptime.getSnapshotCount()).up, 10); + expect(upCount).to.eql(1); }); }); + it('Uptime app should show Kibana QA Monitor present', async function () { + await PageObjects.uptime.pageHasExpectedIds(['kibana-qa-monitor']); + }); }); } diff --git a/x-pack/test/stack_functional_integration/apps/heartbeat/index.js b/x-pack/test/stack_functional_integration/apps/heartbeat/index.ts similarity index 72% rename from x-pack/test/stack_functional_integration/apps/heartbeat/index.js rename to x-pack/test/stack_functional_integration/apps/heartbeat/index.ts index 226350a74afc0..85c195a9ceff4 100644 --- a/x-pack/test/stack_functional_integration/apps/heartbeat/index.js +++ b/x-pack/test/stack_functional_integration/apps/heartbeat/index.ts @@ -4,8 +4,9 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ +import { FtrProviderContext } from '../../../functional/ftr_provider_context'; -export default function ({ loadTestFile }) { +export default function ({ loadTestFile }: FtrProviderContext) { describe('heartbeat app', function () { require('./_heartbeat'); loadTestFile(require.resolve('./_heartbeat')); diff --git a/x-pack/test/stack_functional_integration/apps/metricbeat/_metricbeat.js b/x-pack/test/stack_functional_integration/apps/metricbeat/_metricbeat.ts similarity index 87% rename from x-pack/test/stack_functional_integration/apps/metricbeat/_metricbeat.js rename to x-pack/test/stack_functional_integration/apps/metricbeat/_metricbeat.ts index 34f7c924f5ddb..79dc213e5485a 100644 --- a/x-pack/test/stack_functional_integration/apps/metricbeat/_metricbeat.js +++ b/x-pack/test/stack_functional_integration/apps/metricbeat/_metricbeat.ts @@ -6,8 +6,9 @@ */ import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../functional/ftr_provider_context'; -export default function ({ getService, getPageObjects }) { +export default function ({ getService, getPageObjects }: FtrProviderContext) { const log = getService('log'); const retry = getService('retry'); const browser = getService('browser'); @@ -27,7 +28,7 @@ export default function ({ getService, getPageObjects }) { await PageObjects.discover.selectIndexPattern('metricbeat-*'); await PageObjects.timePicker.setCommonlyUsedTime('Today'); await retry.try(async function () { - const hitCount = parseInt(await PageObjects.discover.getHitCount()); + const hitCount = parseInt(await PageObjects.discover.getHitCount(), 10); expect(hitCount).to.be.greaterThan(0); }); }); diff --git a/x-pack/test/stack_functional_integration/apps/metricbeat/_metricbeat_dashboard.js b/x-pack/test/stack_functional_integration/apps/metricbeat/_metricbeat_dashboard.ts similarity index 93% rename from x-pack/test/stack_functional_integration/apps/metricbeat/_metricbeat_dashboard.js rename to x-pack/test/stack_functional_integration/apps/metricbeat/_metricbeat_dashboard.ts index ac911a941c146..d2e9adbfd2683 100644 --- a/x-pack/test/stack_functional_integration/apps/metricbeat/_metricbeat_dashboard.js +++ b/x-pack/test/stack_functional_integration/apps/metricbeat/_metricbeat_dashboard.ts @@ -8,11 +8,16 @@ import expect from '@kbn/expect'; import { resolve } from 'path'; import { REPO_ROOT } from '@kbn/dev-utils'; +import { FtrProviderContext } from '../../../functional/ftr_provider_context'; const INTEGRATION_TEST_ROOT = process.env.WORKSPACE || resolve(REPO_ROOT, '../integration-test'); const ARCHIVE = resolve(INTEGRATION_TEST_ROOT, 'test/es_archives/metricbeat'); -export default function ({ getService, getPageObjects, updateBaselines }) { +export default function ({ + getService, + getPageObjects, + updateBaselines, +}: FtrProviderContext & { updateBaselines: boolean }) { const screenshot = getService('screenshots'); const browser = getService('browser'); const log = getService('log'); diff --git a/x-pack/test/stack_functional_integration/apps/metricbeat/index.js b/x-pack/test/stack_functional_integration/apps/metricbeat/index.ts similarity index 74% rename from x-pack/test/stack_functional_integration/apps/metricbeat/index.js rename to x-pack/test/stack_functional_integration/apps/metricbeat/index.ts index 9ee04df965dcc..c4e0db2797b94 100644 --- a/x-pack/test/stack_functional_integration/apps/metricbeat/index.js +++ b/x-pack/test/stack_functional_integration/apps/metricbeat/index.ts @@ -4,8 +4,9 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ +import { FtrProviderContext } from '../../../functional/ftr_provider_context'; -export default function ({ loadTestFile }) { +export default function ({ loadTestFile }: FtrProviderContext) { describe('metricbeat app', function () { loadTestFile(require.resolve('./_metricbeat')); loadTestFile(require.resolve('./_metricbeat_dashboard')); diff --git a/x-pack/test/stack_functional_integration/apps/packetbeat/_packetbeat.js b/x-pack/test/stack_functional_integration/apps/packetbeat/_packetbeat.ts similarity index 88% rename from x-pack/test/stack_functional_integration/apps/packetbeat/_packetbeat.js rename to x-pack/test/stack_functional_integration/apps/packetbeat/_packetbeat.ts index a5d6e6e924667..d0d7e326441a0 100644 --- a/x-pack/test/stack_functional_integration/apps/packetbeat/_packetbeat.js +++ b/x-pack/test/stack_functional_integration/apps/packetbeat/_packetbeat.ts @@ -6,8 +6,9 @@ */ import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../functional/ftr_provider_context'; -export default function ({ getService, getPageObjects }) { +export default function ({ getService, getPageObjects }: FtrProviderContext) { const log = getService('log'); const retry = getService('retry'); const browser = getService('browser'); @@ -31,7 +32,7 @@ export default function ({ getService, getPageObjects }) { await PageObjects.discover.selectIndexPattern('packetbeat-*'); await PageObjects.timePicker.setCommonlyUsedTime('Today'); await retry.try(async function () { - const hitCount = parseInt(await PageObjects.discover.getHitCount()); + const hitCount = parseInt(await PageObjects.discover.getHitCount(), 10); expect(hitCount).to.be.greaterThan(0); }); }); diff --git a/x-pack/test/stack_functional_integration/apps/packetbeat/index.js b/x-pack/test/stack_functional_integration/apps/packetbeat/index.ts similarity index 71% rename from x-pack/test/stack_functional_integration/apps/packetbeat/index.js rename to x-pack/test/stack_functional_integration/apps/packetbeat/index.ts index ba0af98d21f6b..70e38b6284fbe 100644 --- a/x-pack/test/stack_functional_integration/apps/packetbeat/index.js +++ b/x-pack/test/stack_functional_integration/apps/packetbeat/index.ts @@ -4,8 +4,9 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ +import { FtrProviderContext } from '../../../functional/ftr_provider_context'; -export default function ({ loadTestFile }) { +export default function ({ loadTestFile }: FtrProviderContext) { describe('packetbeat app', function () { loadTestFile(require.resolve('./_packetbeat')); }); diff --git a/x-pack/test/stack_functional_integration/apps/winlogbeat/_winlogbeat.js b/x-pack/test/stack_functional_integration/apps/winlogbeat/_winlogbeat.ts similarity index 87% rename from x-pack/test/stack_functional_integration/apps/winlogbeat/_winlogbeat.js rename to x-pack/test/stack_functional_integration/apps/winlogbeat/_winlogbeat.ts index c983a9155ae6a..9ef8b85c0ec09 100644 --- a/x-pack/test/stack_functional_integration/apps/winlogbeat/_winlogbeat.js +++ b/x-pack/test/stack_functional_integration/apps/winlogbeat/_winlogbeat.ts @@ -6,8 +6,9 @@ */ import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../functional/ftr_provider_context'; -export default function ({ getService, getPageObjects }) { +export default function ({ getService, getPageObjects }: FtrProviderContext) { const log = getService('log'); const browser = getService('browser'); const PageObjects = getPageObjects(['common', 'discover', 'timePicker']); @@ -26,7 +27,7 @@ export default function ({ getService, getPageObjects }) { await PageObjects.discover.selectIndexPattern('winlogbeat-*'); await PageObjects.timePicker.setCommonlyUsedTime('Today'); await retry.try(async function () { - const hitCount = parseInt(await PageObjects.discover.getHitCount()); + const hitCount = parseInt(await PageObjects.discover.getHitCount(), 10); expect(hitCount).to.be.greaterThan(0); }); }); diff --git a/x-pack/test/stack_functional_integration/apps/winlogbeat/index.js b/x-pack/test/stack_functional_integration/apps/winlogbeat/index.ts similarity index 71% rename from x-pack/test/stack_functional_integration/apps/winlogbeat/index.js rename to x-pack/test/stack_functional_integration/apps/winlogbeat/index.ts index bb883ee498181..826a292de5659 100644 --- a/x-pack/test/stack_functional_integration/apps/winlogbeat/index.js +++ b/x-pack/test/stack_functional_integration/apps/winlogbeat/index.ts @@ -4,8 +4,9 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ +import { FtrProviderContext } from '../../../functional/ftr_provider_context'; -export default function ({ loadTestFile }) { +export default function ({ loadTestFile }: FtrProviderContext) { describe('winlogbeat app', function () { loadTestFile(require.resolve('./_winlogbeat')); }); diff --git a/yarn.lock b/yarn.lock index 27a90215a271a..2a8dff971791f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1886,14 +1886,14 @@ dependencies: "@hapi/hoek" "9.x.x" -"@hapi/boom@9.x.x", "@hapi/boom@^9.0.0", "@hapi/boom@^9.1.1": - version "9.1.1" - resolved "https://registry.yarnpkg.com/@hapi/boom/-/boom-9.1.1.tgz#89e6f0e01637c2a4228da0d113e8157c93677b04" - integrity sha512-VNR8eDbBrOxBgbkddRYIe7+8DZ+vSbV6qlmaN2x7eWjsUjy2VmQgChkOKcVZIeupEZYj+I0dqNg430OhwzagjA== +"@hapi/boom@9.x.x", "@hapi/boom@^9.0.0", "@hapi/boom@^9.1.0", "@hapi/boom@^9.1.4": + version "9.1.4" + resolved "https://registry.yarnpkg.com/@hapi/boom/-/boom-9.1.4.tgz#1f9dad367c6a7da9f8def24b4a986fc5a7bd9db6" + integrity sha512-Ls1oH8jaN1vNsqcaHVYJrKmgMcKsC1wcp8bujvXrHaAqD2iDYq3HoOwsxwo09Cuda5R5nC0o0IxlrlTuvPuzSw== dependencies: "@hapi/hoek" "9.x.x" -"@hapi/bounce@2.x.x": +"@hapi/bounce@2.x.x", "@hapi/bounce@^2.0.0": version "2.0.0" resolved "https://registry.yarnpkg.com/@hapi/bounce/-/bounce-2.0.0.tgz#e6ef56991c366b1e2738b2cd83b01354d938cf3d" integrity sha512-JesW92uyzOOyuzJKjoLHM1ThiOvHPOLDHw01YV8yh5nCso7sDwJho1h0Ad2N+E62bZyz46TG3xhAi/78Gsct6A== @@ -1906,7 +1906,7 @@ resolved "https://registry.yarnpkg.com/@hapi/bourne/-/bourne-2.0.0.tgz#5bb2193eb685c0007540ca61d166d4e1edaf918d" integrity sha512-WEezM1FWztfbzqIUbsDzFRVMxSoLy3HugVcux6KDDtTqzPsLE8NDRHfXvev66aH1i2oOKKar3/XDjbvh/OUBdg== -"@hapi/call@8.x.x": +"@hapi/call@^8.0.0": version "8.0.1" resolved "https://registry.yarnpkg.com/@hapi/call/-/call-8.0.1.tgz#9e64cd8ba6128eb5be6e432caaa572b1ed8cd7c0" integrity sha512-bOff6GTdOnoe5b8oXRV3lwkQSb/LAWylvDMae6RgEWWntd0SHtkYbQukDHKlfaYtVnSAgIavJ0kqszF/AIBb6g== @@ -1914,7 +1914,7 @@ "@hapi/boom" "9.x.x" "@hapi/hoek" "9.x.x" -"@hapi/catbox-memory@5.x.x": +"@hapi/catbox-memory@^5.0.0": version "5.0.0" resolved "https://registry.yarnpkg.com/@hapi/catbox-memory/-/catbox-memory-5.0.0.tgz#6c18dad1a80737480d1c33bfbefd5d028deec86d" integrity sha512-ByuxVJPHNaXwLzbBv4GdTr6ccpe1nG+AfYt+8ftDWEJY7EWBWzD+Klhy5oPTDGzU26pNUh1e7fcYI1ILZRxAXQ== @@ -1979,29 +1979,29 @@ "@hapi/validate" "1.x.x" "@hapi/wreck" "17.x.x" -"@hapi/hapi@^20.0.3": - version "20.0.3" - resolved "https://registry.yarnpkg.com/@hapi/hapi/-/hapi-20.0.3.tgz#e72cad460394e6d2c15f9c57abb5d3332dea27e3" - integrity sha512-aqJVHVjoY3phiZsgsGjDRG15CoUNIs1azScqLZDOCZUSKYGTbzPi+K0QP+RUjUJ0m8L9dRuTZ27c8HKxG3wEhA== +"@hapi/hapi@^20.2.0": + version "20.2.0" + resolved "https://registry.yarnpkg.com/@hapi/hapi/-/hapi-20.2.0.tgz#bf0eca9cc591e83f3d72d06a998d31be35d044a1" + integrity sha512-yPH/z8KvlSLV8lI4EuId9z595fKKk5n6YA7H9UddWYWsBXMcnCyoFmHtYq0PCV4sNgKLD6QW9e27R9V9Z9aqqw== dependencies: "@hapi/accept" "^5.0.1" "@hapi/ammo" "^5.0.1" - "@hapi/boom" "9.x.x" - "@hapi/bounce" "2.x.x" - "@hapi/call" "8.x.x" + "@hapi/boom" "^9.1.0" + "@hapi/bounce" "^2.0.0" + "@hapi/call" "^8.0.0" "@hapi/catbox" "^11.1.1" - "@hapi/catbox-memory" "5.x.x" + "@hapi/catbox-memory" "^5.0.0" "@hapi/heavy" "^7.0.1" - "@hapi/hoek" "9.x.x" - "@hapi/mimos" "5.x.x" + "@hapi/hoek" "^9.0.4" + "@hapi/mimos" "^6.0.0" "@hapi/podium" "^4.1.1" - "@hapi/shot" "^5.0.1" - "@hapi/somever" "3.x.x" + "@hapi/shot" "^5.0.5" + "@hapi/somever" "^3.0.0" "@hapi/statehood" "^7.0.3" "@hapi/subtext" "^7.0.3" - "@hapi/teamwork" "5.x.x" - "@hapi/topo" "5.x.x" - "@hapi/validate" "^1.1.0" + "@hapi/teamwork" "^5.1.0" + "@hapi/topo" "^5.0.0" + "@hapi/validate" "^1.1.1" "@hapi/heavy@^7.0.1": version "7.0.1" @@ -2012,15 +2012,15 @@ "@hapi/hoek" "9.x.x" "@hapi/validate" "1.x.x" -"@hapi/hoek@9.x.x", "@hapi/hoek@^9.0.0", "@hapi/hoek@^9.0.4", "@hapi/hoek@^9.1.1": - version "9.1.1" - resolved "https://registry.yarnpkg.com/@hapi/hoek/-/hoek-9.1.1.tgz#9daf5745156fd84b8e9889a2dc721f0c58e894aa" - integrity sha512-CAEbWH7OIur6jEOzaai83jq3FmKmv4PmX1JYfs9IrYcGEVI/lyL1EXJGCj7eFVJ0bg5QR8LMxBlEtA+xKiLpFw== +"@hapi/hoek@9.x.x", "@hapi/hoek@^9.0.0", "@hapi/hoek@^9.0.4", "@hapi/hoek@^9.2.0": + version "9.2.0" + resolved "https://registry.yarnpkg.com/@hapi/hoek/-/hoek-9.2.0.tgz#f3933a44e365864f4dad5db94158106d511e8131" + integrity sha512-sqKVVVOe5ivCaXDWivIJYVSaEgdQK9ul7a4Kity5Iw7u9+wBAPbX1RMSnLLmp7O4Vzj0WOWwMAJsTL00xwaNug== -"@hapi/inert@^6.0.3": - version "6.0.3" - resolved "https://registry.yarnpkg.com/@hapi/inert/-/inert-6.0.3.tgz#57af5d912893fabcb57eb4b956f84f6cd8020fe1" - integrity sha512-Z6Pi0Wsn2pJex5CmBaq+Dky9q40LGzXLUIUFrYpDtReuMkmfy9UuUeYc4064jQ1Xe9uuw7kbwE6Fq6rqKAdjAg== +"@hapi/inert@^6.0.4": + version "6.0.4" + resolved "https://registry.yarnpkg.com/@hapi/inert/-/inert-6.0.4.tgz#0544221eabc457110a426818358d006e70ff1f41" + integrity sha512-tpmNqtCCAd+5Ts07bJmMaA79+ZUIf0zSWnQMaWtbcO4nGrO/yXB2AzoslfzFX2JEV9vGeF3FfL8mYw0pHl8VGg== dependencies: "@hapi/ammo" "5.x.x" "@hapi/boom" "9.x.x" @@ -2040,10 +2040,10 @@ "@hapi/cryptiles" "5.x.x" "@hapi/hoek" "9.x.x" -"@hapi/mimos@5.x.x": - version "5.0.0" - resolved "https://registry.yarnpkg.com/@hapi/mimos/-/mimos-5.0.0.tgz#245c6c98b1cc2c13395755c730321b913de074eb" - integrity sha512-EVS6wJYeE73InTlPWt+2e3Izn319iIvffDreci3qDNT+t3lA5ylJ0/SoTaID8e0TPNUkHUSsgJZXEmLHvoYzrA== +"@hapi/mimos@^6.0.0": + version "6.0.0" + resolved "https://registry.yarnpkg.com/@hapi/mimos/-/mimos-6.0.0.tgz#daa523d9c07222c7e8860cb7c9c5501fd6506484" + integrity sha512-Op/67tr1I+JafN3R3XN5DucVSxKRT/Tc+tUszDwENoNpolxeXkhrJ2Czt6B6AAqrespHoivhgZBWYSuANN9QXg== dependencies: "@hapi/hoek" "9.x.x" mime-db "1.x.x" @@ -2074,24 +2074,24 @@ "@hapi/hoek" "9.x.x" "@hapi/nigel" "4.x.x" -"@hapi/podium@4.x.x", "@hapi/podium@^4.1.1": - version "4.1.1" - resolved "https://registry.yarnpkg.com/@hapi/podium/-/podium-4.1.1.tgz#106e5849f2cb19b8767cc16007e0107f27c3c791" - integrity sha512-jh7a6+5Z4FUWzx8fgmxjaAa1DTBu+Qfg+NbVdo0f++rE5DgsVidUYrLDp3db65+QjDLleA2MfKQXkpT8ylBDXA== +"@hapi/podium@4.x.x", "@hapi/podium@^4.1.1", "@hapi/podium@^4.1.3": + version "4.1.3" + resolved "https://registry.yarnpkg.com/@hapi/podium/-/podium-4.1.3.tgz#91e20838fc2b5437f511d664aabebbb393578a26" + integrity sha512-ljsKGQzLkFqnQxE7qeanvgGj4dejnciErYd30dbrYzUOF/FyS/DOF97qcrT3bhoVwCYmxa6PEMhxfCPlnUcD2g== dependencies: "@hapi/hoek" "9.x.x" "@hapi/teamwork" "5.x.x" "@hapi/validate" "1.x.x" -"@hapi/shot@^5.0.1": - version "5.0.4" - resolved "https://registry.yarnpkg.com/@hapi/shot/-/shot-5.0.4.tgz#6c978314f21a054c041f4becc50095dd78d3d775" - integrity sha512-PcEz0WJgFDA3xNSMeONgQmothFr7jhbbRRSAKaDh7chN7zOXBlhl13bvKZW6CMb2xVfJUmt34CW3e/oExMgBhQ== +"@hapi/shot@^5.0.5": + version "5.0.5" + resolved "https://registry.yarnpkg.com/@hapi/shot/-/shot-5.0.5.tgz#a25c23d18973bec93c7969c51bf9579632a5bebd" + integrity sha512-x5AMSZ5+j+Paa8KdfCoKh+klB78otxF+vcJR/IoN91Vo2e5ulXIW6HUsFTCU+4W6P/Etaip9nmdAx2zWDimB2A== dependencies: "@hapi/hoek" "9.x.x" "@hapi/validate" "1.x.x" -"@hapi/somever@3.x.x": +"@hapi/somever@^3.0.0": version "3.0.0" resolved "https://registry.yarnpkg.com/@hapi/somever/-/somever-3.0.0.tgz#f4e9b16a948415b926b4dd898013602b0cb45758" integrity sha512-Upw/kmKotC9iEmK4y047HMYe4LDKsE5NWfjgX41XNKmFvxsQL7OiaCWVhuyyhU0ShDGBfIAnCH8jZr49z/JzZA== @@ -2125,19 +2125,19 @@ "@hapi/pez" "^5.0.1" "@hapi/wreck" "17.x.x" -"@hapi/teamwork@5.x.x": +"@hapi/teamwork@5.x.x", "@hapi/teamwork@^5.1.0": version "5.1.0" resolved "https://registry.yarnpkg.com/@hapi/teamwork/-/teamwork-5.1.0.tgz#7801a61fc727f702fd2196ef7625eb4e389f4124" integrity sha512-llqoQTrAJDTXxG3c4Kz/uzhBS1TsmSBa/XG5SPcVXgmffHE1nFtyLIK0hNJHCB3EuBKT84adzd1hZNY9GJLWtg== -"@hapi/topo@5.x.x", "@hapi/topo@^5.0.0": +"@hapi/topo@^5.0.0": version "5.0.0" resolved "https://registry.yarnpkg.com/@hapi/topo/-/topo-5.0.0.tgz#c19af8577fa393a06e9c77b60995af959be721e7" integrity sha512-tFJlT47db0kMqVm3H4nQYgn6Pwg10GTZHb1pwmSiv1K4ks6drQOtfEF5ZnPjkvC+y4/bUPHK+bc87QvLcL+WMw== dependencies: "@hapi/hoek" "^9.0.0" -"@hapi/validate@1.x.x", "@hapi/validate@^1.1.0": +"@hapi/validate@1.x.x", "@hapi/validate@^1.1.1": version "1.1.3" resolved "https://registry.yarnpkg.com/@hapi/validate/-/validate-1.1.3.tgz#f750a07283929e09b51aa16be34affb44e1931ad" integrity sha512-/XMR0N0wjw0Twzq2pQOzPBZlDzkekGcoCtzO314BpIEsbXdYGthQUbxgkGDf4nhk1+IPDAsXqWjMohRQYO06UA== @@ -5170,41 +5170,42 @@ resolved "https://registry.yarnpkg.com/@types/hapi__catbox/-/hapi__catbox-10.2.3.tgz#c9279c16d709bf2987491c332e11d18124ae018f" integrity sha512-gs6MKMKXzWpSqeYsPaDIDAxD8jLNg7aFxgAJE6Jnc+ns072Z9fuh39/NF5gSk1KNoGCLnIpeZ0etT9gY9QDCKg== -"@types/hapi__cookie@^10.1.1": - version "10.1.1" - resolved "https://registry.yarnpkg.com/@types/hapi__cookie/-/hapi__cookie-10.1.1.tgz#4420c7f89ef466aa8c1f4d9975c62e6b5b066b1c" - integrity sha512-sWVS20wvqbYSjpjpfOwsD/gtDBba3mi+Y4Yg2qZMBs0/VAgvhOOmpBXzFf2rE8rrEuR44n7tzmEgPWRw5q7kaw== +"@types/hapi__cookie@^10.1.3": + version "10.1.3" + resolved "https://registry.yarnpkg.com/@types/hapi__cookie/-/hapi__cookie-10.1.3.tgz#b0ab2be28669e083c63253927262c43f24395c2c" + integrity sha512-v/hPXxOVfBdkTa+S4cGec88vZjvEbLaZp8xjg2MtjDhykx1/mLtY4EJHk6fI1cW5WGgFV9pgMjz5mOktjNwILw== dependencies: "@types/hapi__hapi" "*" + joi "^17.3.0" -"@types/hapi__h2o2@^8.3.2": - version "8.3.2" - resolved "https://registry.yarnpkg.com/@types/hapi__h2o2/-/hapi__h2o2-8.3.2.tgz#43cce95972c3097a2ca3efe6b7054a0c95fbf291" - integrity sha512-l36uuLHTwUQNbNUIkT14Z4WbJl1CIWpBZu7ZCBemGBypiNnbJxN3o0YyQ6QAid3YYa2C7LVDIdyY4MhpX8q9ZA== +"@types/hapi__h2o2@^8.3.3": + version "8.3.3" + resolved "https://registry.yarnpkg.com/@types/hapi__h2o2/-/hapi__h2o2-8.3.3.tgz#f6c5ac480a6fd421025f7d0f78dfa916703511b7" + integrity sha512-+qWZVFVGc5Y0wuNZvVe876VJjUBCJ8eQdXovg4Rg9laHpeERQejluI7aw31xXWfLojTuHz3ThZzC6Orqras05Q== dependencies: "@hapi/boom" "^9.0.0" "@hapi/wreck" "^17.0.0" "@types/hapi__hapi" "*" "@types/node" "*" -"@types/hapi__hapi@*", "@types/hapi__hapi@^20.0.2": - version "20.0.2" - resolved "https://registry.yarnpkg.com/@types/hapi__hapi/-/hapi__hapi-20.0.2.tgz#e7571451f7fb75e87ab3873ec91b92f92cd55fff" - integrity sha512-7FwFoaxSCtHXbHbDdArSeVABFOfMLgVkOvOUtWrqUBzw639B2rq9OHv3kOVDHY0bOao0f6ubMzUxio8WQ9QZfQ== +"@types/hapi__hapi@*", "@types/hapi__hapi@^20.0.9": + version "20.0.9" + resolved "https://registry.yarnpkg.com/@types/hapi__hapi/-/hapi__hapi-20.0.9.tgz#9d570846c96268266a14c970c13aeeaccfc8e172" + integrity sha512-fGpKScknCKZityRXdZgpCLGbm41R1ppFgnKHerfZlqOOlCX/jI129S6ghgBqkqCE8m9A0CIu1h7Ch04lD9KOoA== dependencies: "@hapi/boom" "^9.0.0" "@hapi/iron" "^6.0.0" + "@hapi/podium" "^4.1.3" "@types/hapi__catbox" "*" "@types/hapi__mimos" "*" - "@types/hapi__podium" "*" "@types/hapi__shot" "*" - "@types/joi" "*" "@types/node" "*" + joi "^17.3.0" -"@types/hapi__inert@^5.2.2": - version "5.2.2" - resolved "https://registry.yarnpkg.com/@types/hapi__inert/-/hapi__inert-5.2.2.tgz#6513c487d216ed9377c2c0efceb178fda0928bfa" - integrity sha512-Vp9HS2wi3Qbm1oUlcTvzA2Zd+f3Dwg+tgLqWA6KTCgKbQX4LCPKIvVssbaQAVncmcpH0aPrtkAfftJlS/sMsGg== +"@types/hapi__inert@^5.2.3": + version "5.2.3" + resolved "https://registry.yarnpkg.com/@types/hapi__inert/-/hapi__inert-5.2.3.tgz#f586eb240d5997c9968d1b4e8b37679517045ca1" + integrity sha512-I1mWQrEc7oMqGtofT0rwBgRBCBurz0wNzbq8QZsHWR+aXM0bk1j9GA6zwyGIeO53PNl2C1c2kpXlc084xCV+Tg== dependencies: "@types/hapi__hapi" "*" @@ -5215,11 +5216,6 @@ dependencies: "@types/mime-db" "*" -"@types/hapi__podium@*", "@types/hapi__podium@^3.4.1": - version "3.4.1" - resolved "https://registry.yarnpkg.com/@types/hapi__podium/-/hapi__podium-3.4.1.tgz#826ffed038979c844410e576b574f8237afd59bc" - integrity sha512-qgMyeXTZhGWvvUnXFavW2Pksf07IV1haBM/Fdq6cFi1lCIXhUHsaTrr2w651q+rhHZf+1dgP1vltJ0/quLxYYw== - "@types/hapi__shot@*": version "4.1.1" resolved "https://registry.yarnpkg.com/@types/hapi__shot/-/hapi__shot-4.1.1.tgz#c760322b90eb77f36a3003a442e8dc69e6ae3922" @@ -5361,11 +5357,6 @@ jest-diff "^25.2.1" pretty-format "^25.2.1" -"@types/joi@*": - version "14.3.4" - resolved "https://registry.yarnpkg.com/@types/joi/-/joi-14.3.4.tgz#eed1e14cbb07716079c814138831a520a725a1e0" - integrity sha512-1TQNDJvIKlgYXGNIABfgFp9y0FziDpuGrd799Q5RcnsDu+krD+eeW/0Fs5PHARvWWFelOhIG2OPCo6KbadBM4A== - "@types/joi@^17.2.3": version "17.2.3" resolved "https://registry.yarnpkg.com/@types/joi/-/joi-17.2.3.tgz#b7768ed9d84f1ebd393328b9f97c1cf3d2b94798" @@ -7732,6 +7723,13 @@ axios@^0.21.1: dependencies: follow-redirects "^1.10.0" +axios@^0.21.2: + version "0.21.4" + resolved "https://registry.yarnpkg.com/axios/-/axios-0.21.4.tgz#c67b90dc0568e5c1cf2b0b858c43ba28e2eda575" + integrity sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg== + dependencies: + follow-redirects "^1.14.0" + axobject-query@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/axobject-query/-/axobject-query-2.0.2.tgz#ea187abe5b9002b377f925d8bf7d1c561adf38f9" @@ -9441,13 +9439,13 @@ chrome-trace-event@^1.0.2: dependencies: tslib "^1.9.0" -chromedriver@^92.0.1: - version "92.0.1" - resolved "https://registry.yarnpkg.com/chromedriver/-/chromedriver-92.0.1.tgz#3e28b7e0c9fb94d693cf74d51af0c29d57f18dca" - integrity sha512-LptlDVCs1GgyFNVbRoHzzy948JDVzTgGiVPXjNj385qXKQP3hjAVBIgyvb/Hco0xSEW8fjwJfsm1eQRmu6t4pQ== +chromedriver@^93.0.1: + version "93.0.1" + resolved "https://registry.yarnpkg.com/chromedriver/-/chromedriver-93.0.1.tgz#3ed1f7baa98a754fc1788c42ac8e4bb1ab27db32" + integrity sha512-KDzbW34CvQLF5aTkm3b5VdlTrvdIt4wEpCzT2p4XJIQWQZEPco5pNce7Lu9UqZQGkhQ4mpZt4Ky6NKVyIS2N8A== dependencies: "@testim/chrome-version" "^1.0.7" - axios "^0.21.1" + axios "^0.21.2" del "^6.0.0" extract-zip "^2.0.1" https-proxy-agent "^5.0.0" @@ -13927,6 +13925,11 @@ follow-redirects@^1.0.0, follow-redirects@^1.10.0: resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.13.0.tgz#b42e8d93a2a7eea5ed88633676d6597bc8e384db" integrity sha512-aq6gF1BEKje4a9i9+5jimNFIpq4Q1WiwBToeRK5NvZBd/TRsmW8BsJfOEGkr76TbOyPVD3OVDN910EcUNtRYEA== +follow-redirects@^1.14.0: + version "1.14.3" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.3.tgz#6ada78118d8d24caee595595accdc0ac6abd022e" + integrity sha512-3MkHxknWMUtb23apkgz/83fDoe+y+qr0TdgacGIA7bew+QLBo3vdgEN2xEsuXNivpFy4CyDhBBZnNZOtalmenw== + font-awesome@4.7.0: version "4.7.0" resolved "https://registry.yarnpkg.com/font-awesome/-/font-awesome-4.7.0.tgz#8fa8cf0411a1a31afd07b06d2902bb9fc815a133"