From ec7578c8c35aee0413bad311814463493a73291b Mon Sep 17 00:00:00 2001 From: Ahmad Bamieh Date: Mon, 24 Aug 2020 18:01:17 +0300 Subject: [PATCH 01/71] [Telemetry] schema check throw on missing schema (#75750) --- .../src/cli/run_telemetry_check.ts | 3 ++- .../src/tools/tasks/check_matching_schemas_task.ts | 12 ++++++++++-- src/plugins/telemetry/schema/oss_plugins.json | 13 +++++++++++++ 3 files changed, 25 insertions(+), 3 deletions(-) diff --git a/packages/kbn-telemetry-tools/src/cli/run_telemetry_check.ts b/packages/kbn-telemetry-tools/src/cli/run_telemetry_check.ts index 116c484a5c36a..2f85fd2cdd2a4 100644 --- a/packages/kbn-telemetry-tools/src/cli/run_telemetry_check.ts +++ b/packages/kbn-telemetry-tools/src/cli/run_telemetry_check.ts @@ -65,7 +65,8 @@ export function runTelemetryCheck() { }, { title: 'Checking Matching collector.schema against stored json files', - task: (context) => new Listr(checkMatchingSchemasTask(context), { exitOnError: true }), + task: (context) => + new Listr(checkMatchingSchemasTask(context, !fix), { exitOnError: true }), }, { enabled: (_) => fix, diff --git a/packages/kbn-telemetry-tools/src/tools/tasks/check_matching_schemas_task.ts b/packages/kbn-telemetry-tools/src/tools/tasks/check_matching_schemas_task.ts index a1f23bcd44c76..2f73a0ee6ad4a 100644 --- a/packages/kbn-telemetry-tools/src/tools/tasks/check_matching_schemas_task.ts +++ b/packages/kbn-telemetry-tools/src/tools/tasks/check_matching_schemas_task.ts @@ -22,7 +22,7 @@ import { TaskContext } from './task_context'; import { checkMatchingMapping } from '../check_collector_integrity'; import { readFileAsync } from '../utils'; -export function checkMatchingSchemasTask({ roots }: TaskContext) { +export function checkMatchingSchemasTask({ roots }: TaskContext, throwOnDiff: boolean) { return roots.map((root) => ({ task: async () => { const fullPath = path.resolve(process.cwd(), root.config.output); @@ -31,8 +31,16 @@ export function checkMatchingSchemasTask({ roots }: TaskContext) { if (root.parsedCollections) { const differences = checkMatchingMapping(root.parsedCollections, esMapping); - root.esMappingDiffs = Object.keys(differences); + if (root.esMappingDiffs.length && throwOnDiff) { + throw Error( + `The following changes must be persisted in ${fullPath} file. Use '--fix' to update.\n${JSON.stringify( + differences, + null, + 2 + )}` + ); + } } }, title: `Checking in ${root.config.root}`, diff --git a/src/plugins/telemetry/schema/oss_plugins.json b/src/plugins/telemetry/schema/oss_plugins.json index 49e431324a49e..c306446b9780d 100644 --- a/src/plugins/telemetry/schema/oss_plugins.json +++ b/src/plugins/telemetry/schema/oss_plugins.json @@ -48,6 +48,19 @@ } } }, + "csp": { + "properties": { + "strict": { + "type": "boolean" + }, + "warnLegacyBrowsers": { + "type": "boolean" + }, + "rulesChangedFromDefault": { + "type": "boolean" + } + } + }, "telemetry": { "properties": { "opt_in_status": { From d5a698475c115bf34106a855aa896a5798727cc9 Mon Sep 17 00:00:00 2001 From: Pete Harverson Date: Mon, 24 Aug 2020 16:12:19 +0100 Subject: [PATCH 02/71] [ML] Disable Overview view links for analytics jobs with no results (#75740) --- .../overview/components/analytics_panel/actions.tsx | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/ml/public/application/overview/components/analytics_panel/actions.tsx b/x-pack/plugins/ml/public/application/overview/components/analytics_panel/actions.tsx index 081101e241990..c4508a8c19c5b 100644 --- a/x-pack/plugins/ml/public/application/overview/components/analytics_panel/actions.tsx +++ b/x-pack/plugins/ml/public/application/overview/components/analytics_panel/actions.tsx @@ -14,6 +14,7 @@ import { getResultsUrl, DataFrameAnalyticsListRow, } from '../../../data_frame_analytics/pages/analytics_management/components/analytics_list/common'; +import { getViewLinkStatus } from '../../../data_frame_analytics/pages/analytics_management/components/action_view/get_view_link_status'; interface Props { item: DataFrameAnalyticsListRow; @@ -27,23 +28,28 @@ export const ViewLink: FC = ({ item }) => { navigateToPath(getResultsUrl(item.id, analysisType)); }, []); - const openJobsInAnomalyExplorerText = i18n.translate( + const { disabled, tooltipContent } = getViewLinkStatus(item); + + const viewJobResultsButtonText = i18n.translate( 'xpack.ml.overview.analytics.resultActions.openJobText', { defaultMessage: 'View job results', } ); + const tooltipText = disabled === false ? viewJobResultsButtonText : tooltipContent; + return ( - + {i18n.translate('xpack.ml.overview.analytics.viewActionName', { defaultMessage: 'View', From 6f6566310a53e715d3c01e7f5181542a6e718017 Mon Sep 17 00:00:00 2001 From: Kevin Logan <56395104+kevinlog@users.noreply.github.com> Date: Mon, 24 Aug 2020 11:16:18 -0400 Subject: [PATCH 03/71] [SECURITY_SOLUTION] Host Details Tests need to wait for title component after loading (#75748) --- .../pages/endpoint_hosts/view/details/index.tsx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/index.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/index.tsx index 40227ec24a9e4..a37ddd0bd61d9 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/index.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/index.tsx @@ -81,9 +81,11 @@ export const EndpointDetailsFlyout = memo(() => { > -

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

+ {loading ? ( + + ) : ( +

{details?.host?.hostname}

+ )}
{details === undefined ? ( From 4ffef0a4813b3e636701086ee3f1632a3a7d0bd1 Mon Sep 17 00:00:00 2001 From: Stacey Gammon Date: Mon, 24 Aug 2020 11:31:27 -0400 Subject: [PATCH 04/71] Add asciidoc support for generated plugin list (#72292) * add asciidoc support for generated plugin list Try level offset "=+2" instead of "=+1" to stop the inlining of the includes. remove +2 back to +1 * Remove asciidoc, switch to regex. Rearrange dev guide to avoid nesting limit. * Add tests for regex * add a description to not throw off the table. Remove the heading from the paragraph snippet. * Fix more READMEs so table renders correctly * Update plugin list * Remove code-exploration file, moved to plugin-list * fix typo * Add link to developer examples * Update plugin list * fix typo --- .../src/test/prChanges.groovy | 2 +- .../architecture/code-exploration.asciidoc | 593 ------------------ docs/developer/architecture/index.asciidoc | 2 - docs/developer/best-practices/index.asciidoc | 2 +- docs/developer/getting-started/index.asciidoc | 2 +- docs/developer/index.asciidoc | 3 + docs/developer/plugin-list.asciidoc | 497 +++++++++++++++ .../src/plugin_list/discover_plugins.ts | 20 +- .../plugin_list/extract_asciidoc_info.test.ts | 86 +++ .../src/plugin_list/extract_asciidoc_info.ts | 32 + .../src/plugin_list/generate_plugin_list.ts | 55 +- .../src/plugin_list/run_plugin_list_cli.ts | 5 +- vars/prChanges.groovy | 2 +- .../dashboard_enhanced/README.asciidoc | 5 + x-pack/plugins/dashboard_enhanced/README.md | 1 - x-pack/plugins/grokdebugger/README.md | 0 x-pack/plugins/ui_actions_enhanced/README.md | 2 + 17 files changed, 681 insertions(+), 628 deletions(-) delete mode 100644 docs/developer/architecture/code-exploration.asciidoc create mode 100644 docs/developer/plugin-list.asciidoc create mode 100644 packages/kbn-dev-utils/src/plugin_list/extract_asciidoc_info.test.ts create mode 100644 packages/kbn-dev-utils/src/plugin_list/extract_asciidoc_info.ts create mode 100644 x-pack/plugins/dashboard_enhanced/README.asciidoc delete mode 100644 x-pack/plugins/dashboard_enhanced/README.md delete mode 100644 x-pack/plugins/grokdebugger/README.md diff --git a/.ci/pipeline-library/src/test/prChanges.groovy b/.ci/pipeline-library/src/test/prChanges.groovy index 0f354e7687246..e3f82e6102acc 100644 --- a/.ci/pipeline-library/src/test/prChanges.groovy +++ b/.ci/pipeline-library/src/test/prChanges.groovy @@ -90,7 +90,7 @@ class PrChangesTest extends KibanaBasePipelineTest { props([ githubPrs: [ getChanges: { [ - [filename: 'docs/developer/architecture/code-exploration.asciidoc'], + [filename: 'docs/developer/plugin-list.asciidoc'], ] }, ], ]) diff --git a/docs/developer/architecture/code-exploration.asciidoc b/docs/developer/architecture/code-exploration.asciidoc deleted file mode 100644 index d65456f2ad928..0000000000000 --- a/docs/developer/architecture/code-exploration.asciidoc +++ /dev/null @@ -1,593 +0,0 @@ -//// - -NOTE: - This is an automatically generated file. Please do not edit directly. Instead, run the - following from within the kibana repository: - - node scripts/build_plugin_list_docs - - You can update the template within packages/kbn-dev-utils/target/plugin_list/generate_plugin_list.js - -//// - -[[code-exploration]] -== Exploring Kibana code - -The goals of our folder heirarchy are: - -- Easy for developers to know where to add new services, plugins and applications. -- Easy for developers to know where to find the code from services, plugins and applications. -- Easy to browse and understand our folder structure. - -To that aim, we strive to: - -- Avoid too many files in any given folder. -- Choose clear, unambigious folder names. -- Organize by domain. -- Every folder should contain a README that describes the contents of that folder. - -[discrete] -[[kibana-services-applications]] -=== Services and Applications - -[discrete] -==== src/plugins - -- {kib-repo}blob/{branch}/src/plugins/advanced_settings[advancedSettings] - -WARNING: Missing README. - - -- {kib-repo}blob/{branch}/src/plugins/apm_oss[apmOss] - -WARNING: Missing README. - - -- {kib-repo}blob/{branch}/src/plugins/bfetch/README.md[bfetch] - -bfetch allows to batch HTTP requests and streams responses back. - - -- {kib-repo}blob/{branch}/src/plugins/charts/README.md[charts] - -The Charts plugin is a way to create easier integration of shared colors, themes, types and other utilities across all Kibana charts and visualizations. - - -- {kib-repo}blob/{branch}/src/plugins/console[console] - -WARNING: Missing README. - - -- {kib-repo}blob/{branch}/src/plugins/dashboard/README.md[dashboard] - -Contains the dashboard application. - - -- {kib-repo}blob/{branch}/src/plugins/data/README.md[data] - -data plugin provides common data access services. - - -- {kib-repo}blob/{branch}/src/plugins/dev_tools/README.md[devTools] - -The ui/registry/dev_tools is removed in favor of the devTools plugin which exposes a register method in the setup contract. -Registering app works mostly the same as registering apps in core.application.register. -Routing will be handled by the id of the dev tool - your dev tool will be mounted when the URL matches /app/dev_tools#/. -This API doesn't support angular, for registering angular dev tools, bootstrap a local module on mount into the given HTML element. - - -- {kib-repo}blob/{branch}/src/plugins/discover/README.md[discover] - -Contains the Discover application and the saved search embeddable. - - -- {kib-repo}blob/{branch}/src/plugins/embeddable/README.md[embeddable] - -Embeddables are re-usable widgets that can be rendered in any environment or plugin. Developers can embed them directly in their plugin. End users can dynamically add them to any embeddable containers. - - -- {kib-repo}blob/{branch}/src/plugins/es_ui_shared/README.md[esUiShared] - -This plugin contains reusable code in the form of self-contained modules (or libraries). Each of these modules exports a set of functionality relevant to the domain of the module. - - -- {kib-repo}blob/{branch}/src/plugins/expressions/README.md[expressions] - -This plugin provides methods which will parse & execute an expression pipeline -string for you, as well as a series of registries for advanced users who might -want to incorporate their own functions, types, and renderers into the service -for use in their own application. - - -- {kib-repo}blob/{branch}/src/plugins/home/README.md[home] - -Moves the legacy ui/registry/feature_catalogue module for registering "features" that should be shown in the home page's feature catalogue to a service within a "home" plugin. The feature catalogue refered to here should not be confused with the "feature" plugin for registering features used to derive UI capabilities for feature controls. - - -- {kib-repo}blob/{branch}/src/plugins/index_pattern_management[indexPatternManagement] - -WARNING: Missing README. - - -- {kib-repo}blob/{branch}/src/plugins/input_control_vis/README.md[inputControlVis] - -Contains the input control visualization allowing to place custom filter controls on a dashboard. - - -- {kib-repo}blob/{branch}/src/plugins/inspector/README.md[inspector] - -The inspector is a contextual tool to gain insights into different elements -in Kibana, e.g. visualizations. It has the form of a flyout panel. - - -- {kib-repo}blob/{branch}/src/plugins/kibana_legacy/README.md[kibanaLegacy] - -This plugin will contain several helpers and services to integrate pieces of the legacy Kibana app with the new Kibana platform. - - -- {kib-repo}blob/{branch}/src/plugins/kibana_react/README.md[kibanaReact] - -Tools for building React applications in Kibana. - - -- {kib-repo}blob/{branch}/src/plugins/kibana_usage_collection/README.md[kibanaUsageCollection] - -This plugin registers the basic usage collectors from Kibana: - - -- {kib-repo}blob/{branch}/src/plugins/kibana_utils/README.md[kibanaUtils] - -Utilities for building Kibana plugins. - - -- {kib-repo}blob/{branch}/src/plugins/legacy_export[legacyExport] - -WARNING: Missing README. - - -- {kib-repo}blob/{branch}/src/plugins/management[management] - -WARNING: Missing README. - - -- {kib-repo}blob/{branch}/src/plugins/maps_legacy[mapsLegacy] - -WARNING: Missing README. - - -- {kib-repo}blob/{branch}/src/plugins/navigation/README.md[navigation] - -The navigation plugins exports the TopNavMenu component. -It also provides a stateful version of it on the start contract. - - -- {kib-repo}blob/{branch}/src/plugins/newsfeed[newsfeed] - -WARNING: Missing README. - - -- {kib-repo}blob/{branch}/src/plugins/region_map[regionMap] - -WARNING: Missing README. - - -- {kib-repo}blob/{branch}/src/plugins/saved_objects[savedObjects] - -WARNING: Missing README. - - -- {kib-repo}blob/{branch}/src/plugins/saved_objects_management[savedObjectsManagement] - -WARNING: Missing README. - - -- {kib-repo}blob/{branch}/src/plugins/share/README.md[share] - -Replaces the legacy ui/share module for registering share context menus. - - -- {kib-repo}blob/{branch}/src/plugins/telemetry/README.md[telemetry] - -Telemetry allows Kibana features to have usage tracked in the wild. The general term "telemetry" refers to multiple things: - - -- {kib-repo}blob/{branch}/src/plugins/telemetry_collection_manager/README.md[telemetryCollectionManager] - -Telemetry's collection manager to go through all the telemetry sources when fetching it before reporting. - - -- {kib-repo}blob/{branch}/src/plugins/telemetry_management_section/README.md[telemetryManagementSection] - -This plugin adds the Advanced Settings section for the Usage Data collection (aka Telemetry). - - -- {kib-repo}blob/{branch}/src/plugins/tile_map[tileMap] - -WARNING: Missing README. - - -- {kib-repo}blob/{branch}/src/plugins/timelion/README.md[timelion] - -Contains the deprecated timelion application. For the timelion visualization, -which also contains the timelion APIs and backend, look at the vis_type_timelion plugin. - - -- {kib-repo}blob/{branch}/src/plugins/ui_actions/README.md[uiActions] - -An API for: - - -- {kib-repo}blob/{branch}/src/plugins/usage_collection/README.md[usageCollection] - -Usage Collection allows collecting usage data for other services to consume (telemetry and monitoring). -To integrate with the telemetry services for usage collection of your feature, there are 2 steps: - - -- {kib-repo}blob/{branch}/src/plugins/vis_type_markdown/README.md[visTypeMarkdown] - -The markdown visualization that can be used to place text panels on dashboards. - - -- {kib-repo}blob/{branch}/src/plugins/vis_type_metric/README.md[visTypeMetric] - -Contains the metric visualization. - - -- {kib-repo}blob/{branch}/src/plugins/vis_type_table/README.md[visTypeTable] - -Contains the data table visualization, that allows presenting data in a simple table format. - - -- {kib-repo}blob/{branch}/src/plugins/vis_type_tagcloud/README.md[visTypeTagcloud] - -Contains the tagcloud visualization. - - -- {kib-repo}blob/{branch}/src/plugins/vis_type_timelion/README.md[visTypeTimelion] - -Contains the timelion visualization and the timelion backend. - - -- {kib-repo}blob/{branch}/src/plugins/vis_type_timeseries/README.md[visTypeTimeseries] - -Contains everything around TSVB (the editor, visualizatin implementations and backends). - - -- {kib-repo}blob/{branch}/src/plugins/vis_type_vega/README.md[visTypeVega] - -Contains the Vega visualization. - - -- {kib-repo}blob/{branch}/src/plugins/vis_type_vislib/README.md[visTypeVislib] - -Contains the vislib visualizations. These are the classical area/line/bar, pie, gauge/goal and -heatmap charts. - - -- {kib-repo}blob/{branch}/src/plugins/vis_type_xy/README.md[visTypeXy] - -Contains the new xy-axis chart using the elastic-charts library, which will eventually -replace the vislib xy-axis (bar, area, line) charts. - - -- {kib-repo}blob/{branch}/src/plugins/visualizations/README.md[visualizations] - -Contains most of the visualization infrastructure, e.g. the visualization type registry or the -visualization embeddable. - - -- {kib-repo}blob/{branch}/src/plugins/visualize/README.md[visualize] - -Contains the visualize application which includes the listing page and the app frame, -which will load the visualization's editor. - - -[discrete] -==== x-pack/plugins - -- {kib-repo}blob/{branch}/x-pack/plugins/actions/README.md[actions] - -The Kibana actions plugin provides a framework to create executable actions. You can: - - -- {kib-repo}blob/{branch}/x-pack/plugins/alerting_builtins/README.md[alertingBuiltins] - -This plugin provides alertTypes shipped with Kibana for use with the -the alerts plugin. When enabled, it will register -the built-in alertTypes with the alerting plugin, register associated HTTP -routes, etc. - - -- {kib-repo}blob/{branch}/x-pack/plugins/alerts/README.md[alerts] - -The Kibana alerting plugin provides a common place to set up alerts. You can: - - -- {kib-repo}blob/{branch}/x-pack/plugins/apm/readme.md[apm] - -To access an elasticsearch instance that has live data you have two options: - - -- {kib-repo}blob/{branch}/x-pack/plugins/audit_trail[auditTrail] - -WARNING: Missing README. - - -- {kib-repo}blob/{branch}/x-pack/plugins/beats_management/readme.md[beatsManagement] - -Notes: -Failure to have auth enabled in Kibana will make for a broken UI. UI-based errors not yet in place - - -- {kib-repo}blob/{branch}/x-pack/plugins/canvas/README.md[canvas] - -"Never look back. The past is done. The future is a blank canvas." ― Suzy Kassem, Rise Up and Salute the Sun - - -- {kib-repo}blob/{branch}/x-pack/plugins/case/README.md[case] - -Experimental Feature - - -- {kib-repo}blob/{branch}/x-pack/plugins/cloud[cloud] - -WARNING: Missing README. - - -- {kib-repo}blob/{branch}/x-pack/plugins/code[code] - -WARNING: Missing README. - - -- {kib-repo}blob/{branch}/x-pack/plugins/console_extensions[consoleExtensions] - -WARNING: Missing README. - - -- {kib-repo}blob/{branch}/x-pack/plugins/cross_cluster_replication/README.md[crossClusterReplication] - -You can run a local cluster and simulate a remote cluster within a single Kibana directory. - - -- {kib-repo}blob/{branch}/x-pack/plugins/dashboard_enhanced/README.md[dashboardEnhanced] - -Contains the enhancements to the OSS dashboard app. - - -- {kib-repo}blob/{branch}/x-pack/plugins/dashboard_mode/README.md[dashboardMode] - -The deprecated dashboard only mode. - - -- {kib-repo}blob/{branch}/x-pack/plugins/data_enhanced[dataEnhanced] - -WARNING: Missing README. - - -- {kib-repo}blob/{branch}/x-pack/plugins/discover_enhanced/README.md[discoverEnhanced] - -Contains the enhancements to the OSS discover app. - - -- {kib-repo}blob/{branch}/x-pack/plugins/embeddable_enhanced[embeddableEnhanced] - -WARNING: Missing README. - - -- {kib-repo}blob/{branch}/x-pack/plugins/encrypted_saved_objects/README.md[encryptedSavedObjects] - -The purpose of this plugin is to provide a way to encrypt/decrypt attributes on the custom Saved Objects that works with -security and spaces filtering as well as performing audit logging. - - -- {kib-repo}blob/{branch}/x-pack/plugins/enterprise_search/README.md[enterpriseSearch] - -This plugin's goal is to provide a Kibana user interface to the Enterprise Search solution's products (App Search and Workplace Search). In it's current MVP state, the plugin provides the following with the goal of gathering user feedback and raising product awareness: - - -- {kib-repo}blob/{branch}/x-pack/plugins/event_log/README.md[eventLog] - -The purpose of this plugin is to provide a way to persist a history of events -occuring in Kibana, initially just for the Make It Action project - alerts -and actions. - - -- {kib-repo}blob/{branch}/x-pack/plugins/features[features] - -WARNING: Missing README. - - -- {kib-repo}blob/{branch}/x-pack/plugins/file_upload[fileUpload] - -WARNING: Missing README. - - -- {kib-repo}blob/{branch}/x-pack/plugins/global_search/README.md[globalSearch] - -The GlobalSearch plugin provides an easy way to search for various objects, such as applications -or dashboards from the Kibana instance, from both server and client-side plugins - - -- {kib-repo}blob/{branch}/x-pack/plugins/global_search_providers[globalSearchProviders] - -WARNING: Missing README. - - -- {kib-repo}blob/{branch}/x-pack/plugins/graph/README.md[graph] - -This is the main source folder of the Graph plugin. It contains all of the Kibana server and client source code. x-pack/test/functional/apps/graph contains additional functional tests. - - -- {kib-repo}blob/{branch}/x-pack/plugins/grokdebugger/README.md[grokdebugger] - -- {kib-repo}blob/{branch}/x-pack/plugins/index_lifecycle_management/README.md[indexLifecycleManagement] - -You can test that the Frozen badge, phase filtering, and lifecycle information is surfaced in -Index Management by running this series of requests in Console: - - -- {kib-repo}blob/{branch}/x-pack/plugins/index_management/README.md[indexManagement] - -Create a data stream using Console and you'll be able to view it in the UI: - - -- {kib-repo}blob/{branch}/x-pack/plugins/infra/README.md[infra] - -This is the home of the infra plugin, which aims to provide a solution for -the infrastructure monitoring use-case within Kibana. - - -- {kib-repo}blob/{branch}/x-pack/plugins/ingest_manager/README.md[ingestManager] - -Fleet needs to have Elasticsearch API keys enabled, and also to have TLS enabled on kibana, (if you want to run Kibana without TLS you can provide the following config flag --xpack.ingestManager.fleet.tlsCheckDisabled=false) - - -- {kib-repo}blob/{branch}/x-pack/plugins/ingest_pipelines/README.md[ingestPipelines] - -The ingest_pipelines plugin provides Kibana support for Elasticsearch's ingest nodes. Please refer to the Elasticsearch documentation for more details. - - -- {kib-repo}blob/{branch}/x-pack/plugins/lens/readme.md[lens] - -Run all tests from the x-pack root directory - - -- {kib-repo}blob/{branch}/x-pack/plugins/license_management[licenseManagement] - -WARNING: Missing README. - - -- {kib-repo}blob/{branch}/x-pack/plugins/licensing/README.md[licensing] - -The licensing plugin retrieves license data from Elasticsearch at regular configurable intervals. - - -- {kib-repo}blob/{branch}/x-pack/plugins/lists/README.md[lists] - -README.md for developers working on the backend lists on how to get started -using the CURL scripts in the scripts folder. - - -- {kib-repo}blob/{branch}/x-pack/plugins/logstash[logstash] - -WARNING: Missing README. - - -- {kib-repo}blob/{branch}/x-pack/plugins/maps/README.md[maps] - -Visualize geo data from Elasticsearch or 3rd party geo-services. - - -- {kib-repo}blob/{branch}/x-pack/plugins/maps_legacy_licensing/README.md[mapsLegacyLicensing] - -This plugin provides access to the detailed tile map services from Elastic. - - -- {kib-repo}blob/{branch}/x-pack/plugins/ml[ml] - -WARNING: Missing README. - - -- {kib-repo}blob/{branch}/x-pack/plugins/monitoring[monitoring] - -WARNING: Missing README. - - -- {kib-repo}blob/{branch}/x-pack/plugins/observability/README.md[observability] - -This plugin provides shared components and services for use across observability solutions, as well as the observability landing page UI. - - -- {kib-repo}blob/{branch}/x-pack/plugins/oss_telemetry[ossTelemetry] - -WARNING: Missing README. - - -- {kib-repo}blob/{branch}/x-pack/plugins/painless_lab[painlessLab] - -WARNING: Missing README. - - -- {kib-repo}blob/{branch}/x-pack/plugins/remote_clusters[remoteClusters] - -WARNING: Missing README. - - -- {kib-repo}blob/{branch}/x-pack/plugins/reporting/README.md[reporting] - -An awesome Kibana reporting plugin - - -- {kib-repo}blob/{branch}/x-pack/plugins/rollup/README.md[rollup] - -Welcome to the Kibana rollup plugin! This plugin provides Kibana support for Elasticsearch's rollup feature. Please refer to the Elasticsearch documentation to understand rollup indices and how to create rollup jobs. - - -- {kib-repo}blob/{branch}/x-pack/plugins/searchprofiler[searchprofiler] - -WARNING: Missing README. - - -- {kib-repo}blob/{branch}/x-pack/plugins/security/README.md[security] - -See Configuring security in Kibana. - - -- {kib-repo}blob/{branch}/x-pack/plugins/security_solution/README.md[securitySolution] - -Welcome to the Kibana Security Solution plugin! This README will go over getting started with development and testing. - - -- {kib-repo}blob/{branch}/x-pack/plugins/snapshot_restore/README.md[snapshotRestore] - -or - - -- {kib-repo}blob/{branch}/x-pack/plugins/spaces[spaces] - -WARNING: Missing README. - - -- {kib-repo}blob/{branch}/x-pack/plugins/task_manager[taskManager] - -WARNING: Missing README. - - -- {kib-repo}blob/{branch}/x-pack/plugins/telemetry_collection_xpack/README.md[telemetryCollectionXpack] - -Gathers all usage collection, retrieving them from both: OSS and X-Pack plugins. - - -- {kib-repo}blob/{branch}/x-pack/plugins/transform[transform] - -WARNING: Missing README. - - -- {kib-repo}blob/{branch}/x-pack/plugins/translations[translations] - -WARNING: Missing README. - - -- {kib-repo}blob/{branch}/x-pack/plugins/triggers_actions_ui/README.md[triggers_actions_ui] - -The Kibana alerts and actions UI plugin provides a user interface for managing alerts and actions. -As a developer you can reuse and extend built-in alerts and actions UI functionality: - - -- {kib-repo}blob/{branch}/x-pack/plugins/ui_actions_enhanced/README.md[uiActionsEnhanced] - -- {kib-repo}blob/{branch}/x-pack/plugins/upgrade_assistant[upgradeAssistant] - -WARNING: Missing README. - - -- {kib-repo}blob/{branch}/x-pack/plugins/uptime/README.md[uptime] - -The purpose of this plugin is to provide users of Heartbeat more visibility of what's happening -in their infrastructure. - - -- {kib-repo}blob/{branch}/x-pack/plugins/watcher/README.md[watcher] - -This plugins adopts some conventions in addition to or in place of conventions in Kibana (at the time of the plugin's creation): - diff --git a/docs/developer/architecture/index.asciidoc b/docs/developer/architecture/index.asciidoc index 2e6ab1a4ad6ac..ac25fe003df08 100644 --- a/docs/developer/architecture/index.asciidoc +++ b/docs/developer/architecture/index.asciidoc @@ -17,7 +17,6 @@ A few notable services are called out below. * <> * <> * <> -* <> include::add-data-tutorials.asciidoc[leveloffset=+1] @@ -25,4 +24,3 @@ include::development-visualize-index.asciidoc[leveloffset=+1] include::security/index.asciidoc[leveloffset=+1] -include::code-exploration.asciidoc[leveloffset=+1] diff --git a/docs/developer/best-practices/index.asciidoc b/docs/developer/best-practices/index.asciidoc index 90b0092d835a8..63a44b54d454f 100644 --- a/docs/developer/best-practices/index.asciidoc +++ b/docs/developer/best-practices/index.asciidoc @@ -99,7 +99,7 @@ Re-using these services will help create a consistent experience across [discrete] === Backward compatibility -Eventually we want to garauntee to our plugin developers that their plugins will not break from minor to minor. +Eventually we want to guarantee to our plugin developers that their plugins will not break from minor to minor. Any time you create or change a public API, keep this in mind, and consider potential backward compatibility issues. While we have a formal diff --git a/docs/developer/getting-started/index.asciidoc b/docs/developer/getting-started/index.asciidoc index 2ac51b6cf86f8..eaa35eece5a2c 100644 --- a/docs/developer/getting-started/index.asciidoc +++ b/docs/developer/getting-started/index.asciidoc @@ -99,7 +99,7 @@ preserving data inbetween runs, running remote cluster, etc. [discrete] === Run {kib} -In another terminal window, start up {kib}. Include developer examples by adding an optional `--run-examples` flag. +In another terminal window, start up {kib}. Include {kib-repo}tree/{branch}/examples[developer examples] by adding an optional `--run-examples` flag. [source,bash] ---- diff --git a/docs/developer/index.asciidoc b/docs/developer/index.asciidoc index db57815a1285a..5f032a3952173 100644 --- a/docs/developer/index.asciidoc +++ b/docs/developer/index.asciidoc @@ -12,6 +12,7 @@ running in no time. If you have any problems, file an issue in the https://githu * <> * <> * <> +* <> -- @@ -27,3 +28,5 @@ include::plugin/index.asciidoc[] include::advanced/index.asciidoc[] +include::plugin-list.asciidoc[] + diff --git a/docs/developer/plugin-list.asciidoc b/docs/developer/plugin-list.asciidoc new file mode 100644 index 0000000000000..b3180a7a03874 --- /dev/null +++ b/docs/developer/plugin-list.asciidoc @@ -0,0 +1,497 @@ +//// + +NOTE: + This is an automatically generated file. Please do not edit directly. Instead, run the + following from within the kibana repository: + + node scripts/build_plugin_list_docs + + You can update the template within packages/kbn-dev-utils/target/plugin_list/generate_plugin_list.js + +//// + +[[plugin-list]] +== List of {kib} plugins + +[discrete] +=== src/plugins + +[%header,cols=2*] +|=== +|Name +|Description + + +|{kib-repo}blob/{branch}/src/plugins/advanced_settings[advancedSettings] +|WARNING: Missing README. + + +|{kib-repo}blob/{branch}/src/plugins/apm_oss[apmOss] +|WARNING: Missing README. + + +|{kib-repo}blob/{branch}/src/plugins/bfetch/README.md[bfetch] +|bfetch allows to batch HTTP requests and streams responses back. + + +|{kib-repo}blob/{branch}/src/plugins/charts/README.md[charts] +|The Charts plugin is a way to create easier integration of shared colors, themes, types and other utilities across all Kibana charts and visualizations. + + +|{kib-repo}blob/{branch}/src/plugins/console[console] +|WARNING: Missing README. + + +|<> +|- Registers the dashboard application. +- Adds a dashboard embeddable that can be used in other applications. + + +|{kib-repo}blob/{branch}/src/plugins/data/README.md[data] +|data plugin provides common data access services. + + +|{kib-repo}blob/{branch}/src/plugins/dev_tools/README.md[devTools] +|The ui/registry/dev_tools is removed in favor of the devTools plugin which exposes a register method in the setup contract. +Registering app works mostly the same as registering apps in core.application.register. +Routing will be handled by the id of the dev tool - your dev tool will be mounted when the URL matches /app/dev_tools#/. +This API doesn't support angular, for registering angular dev tools, bootstrap a local module on mount into the given HTML element. + + +|{kib-repo}blob/{branch}/src/plugins/discover/README.md[discover] +|Contains the Discover application and the saved search embeddable. + + +|{kib-repo}blob/{branch}/src/plugins/embeddable/README.md[embeddable] +|Embeddables are re-usable widgets that can be rendered in any environment or plugin. Developers can embed them directly in their plugin. End users can dynamically add them to any embeddable containers. + + +|{kib-repo}blob/{branch}/src/plugins/es_ui_shared/README.md[esUiShared] +|This plugin contains reusable code in the form of self-contained modules (or libraries). Each of these modules exports a set of functionality relevant to the domain of the module. + + +|{kib-repo}blob/{branch}/src/plugins/expressions/README.md[expressions] +|This plugin provides methods which will parse & execute an expression pipeline +string for you, as well as a series of registries for advanced users who might +want to incorporate their own functions, types, and renderers into the service +for use in their own application. + + +|{kib-repo}blob/{branch}/src/plugins/home/README.md[home] +|Moves the legacy ui/registry/feature_catalogue module for registering "features" that should be shown in the home page's feature catalogue to a service within a "home" plugin. The feature catalogue refered to here should not be confused with the "feature" plugin for registering features used to derive UI capabilities for feature controls. + + +|{kib-repo}blob/{branch}/src/plugins/index_pattern_management[indexPatternManagement] +|WARNING: Missing README. + + +|{kib-repo}blob/{branch}/src/plugins/input_control_vis/README.md[inputControlVis] +|Contains the input control visualization allowing to place custom filter controls on a dashboard. + + +|{kib-repo}blob/{branch}/src/plugins/inspector/README.md[inspector] +|The inspector is a contextual tool to gain insights into different elements +in Kibana, e.g. visualizations. It has the form of a flyout panel. + + +|{kib-repo}blob/{branch}/src/plugins/kibana_legacy/README.md[kibanaLegacy] +|This plugin will contain several helpers and services to integrate pieces of the legacy Kibana app with the new Kibana platform. + + +|{kib-repo}blob/{branch}/src/plugins/kibana_react/README.md[kibanaReact] +|Tools for building React applications in Kibana. + + +|{kib-repo}blob/{branch}/src/plugins/kibana_usage_collection/README.md[kibanaUsageCollection] +|This plugin registers the basic usage collectors from Kibana: + + +|{kib-repo}blob/{branch}/src/plugins/kibana_utils/README.md[kibanaUtils] +|Utilities for building Kibana plugins. + + +|{kib-repo}blob/{branch}/src/plugins/legacy_export[legacyExport] +|WARNING: Missing README. + + +|{kib-repo}blob/{branch}/src/plugins/management[management] +|WARNING: Missing README. + + +|{kib-repo}blob/{branch}/src/plugins/maps_legacy[mapsLegacy] +|WARNING: Missing README. + + +|{kib-repo}blob/{branch}/src/plugins/navigation/README.md[navigation] +|The navigation plugins exports the TopNavMenu component. +It also provides a stateful version of it on the start contract. + + +|{kib-repo}blob/{branch}/src/plugins/newsfeed[newsfeed] +|WARNING: Missing README. + + +|{kib-repo}blob/{branch}/src/plugins/region_map[regionMap] +|WARNING: Missing README. + + +|{kib-repo}blob/{branch}/src/plugins/saved_objects[savedObjects] +|WARNING: Missing README. + + +|{kib-repo}blob/{branch}/src/plugins/saved_objects_management[savedObjectsManagement] +|WARNING: Missing README. + + +|{kib-repo}blob/{branch}/src/plugins/share/README.md[share] +|Replaces the legacy ui/share module for registering share context menus. + + +|{kib-repo}blob/{branch}/src/plugins/telemetry/README.md[telemetry] +|Telemetry allows Kibana features to have usage tracked in the wild. The general term "telemetry" refers to multiple things: + + +|{kib-repo}blob/{branch}/src/plugins/telemetry_collection_manager/README.md[telemetryCollectionManager] +|Telemetry's collection manager to go through all the telemetry sources when fetching it before reporting. + + +|{kib-repo}blob/{branch}/src/plugins/telemetry_management_section/README.md[telemetryManagementSection] +|This plugin adds the Advanced Settings section for the Usage Data collection (aka Telemetry). + + +|{kib-repo}blob/{branch}/src/plugins/tile_map[tileMap] +|WARNING: Missing README. + + +|{kib-repo}blob/{branch}/src/plugins/timelion/README.md[timelion] +|Contains the deprecated timelion application. For the timelion visualization, +which also contains the timelion APIs and backend, look at the vis_type_timelion plugin. + + +|{kib-repo}blob/{branch}/src/plugins/ui_actions/README.md[uiActions] +|An API for: + + +|{kib-repo}blob/{branch}/src/plugins/usage_collection/README.md[usageCollection] +|Usage Collection allows collecting usage data for other services to consume (telemetry and monitoring). +To integrate with the telemetry services for usage collection of your feature, there are 2 steps: + + +|{kib-repo}blob/{branch}/src/plugins/vis_type_markdown/README.md[visTypeMarkdown] +|The markdown visualization that can be used to place text panels on dashboards. + + +|{kib-repo}blob/{branch}/src/plugins/vis_type_metric/README.md[visTypeMetric] +|Contains the metric visualization. + + +|{kib-repo}blob/{branch}/src/plugins/vis_type_table/README.md[visTypeTable] +|Contains the data table visualization, that allows presenting data in a simple table format. + + +|{kib-repo}blob/{branch}/src/plugins/vis_type_tagcloud/README.md[visTypeTagcloud] +|Contains the tagcloud visualization. + + +|{kib-repo}blob/{branch}/src/plugins/vis_type_timelion/README.md[visTypeTimelion] +|Contains the timelion visualization and the timelion backend. + + +|{kib-repo}blob/{branch}/src/plugins/vis_type_timeseries/README.md[visTypeTimeseries] +|Contains everything around TSVB (the editor, visualizatin implementations and backends). + + +|{kib-repo}blob/{branch}/src/plugins/vis_type_vega/README.md[visTypeVega] +|Contains the Vega visualization. + + +|{kib-repo}blob/{branch}/src/plugins/vis_type_vislib/README.md[visTypeVislib] +|Contains the vislib visualizations. These are the classical area/line/bar, pie, gauge/goal and +heatmap charts. + + +|{kib-repo}blob/{branch}/src/plugins/vis_type_xy/README.md[visTypeXy] +|Contains the new xy-axis chart using the elastic-charts library, which will eventually +replace the vislib xy-axis (bar, area, line) charts. + + +|{kib-repo}blob/{branch}/src/plugins/visualizations/README.md[visualizations] +|Contains most of the visualization infrastructure, e.g. the visualization type registry or the +visualization embeddable. + + +|{kib-repo}blob/{branch}/src/plugins/visualize/README.md[visualize] +|Contains the visualize application which includes the listing page and the app frame, +which will load the visualization's editor. + + +|=== + +[discrete] +=== x-pack/plugins + +[%header,cols=2*] +|=== +|Name +|Description + + +|{kib-repo}blob/{branch}/x-pack/plugins/actions/README.md[actions] +|The Kibana actions plugin provides a framework to create executable actions. You can: + + +|{kib-repo}blob/{branch}/x-pack/plugins/alerting_builtins/README.md[alertingBuiltins] +|This plugin provides alertTypes shipped with Kibana for use with the +the alerts plugin. When enabled, it will register +the built-in alertTypes with the alerting plugin, register associated HTTP +routes, etc. + + +|{kib-repo}blob/{branch}/x-pack/plugins/alerts/README.md[alerts] +|The Kibana alerting plugin provides a common place to set up alerts. You can: + + +|{kib-repo}blob/{branch}/x-pack/plugins/apm/readme.md[apm] +|To access an elasticsearch instance that has live data you have two options: + + +|{kib-repo}blob/{branch}/x-pack/plugins/audit_trail[auditTrail] +|WARNING: Missing README. + + +|{kib-repo}blob/{branch}/x-pack/plugins/beats_management/readme.md[beatsManagement] +|Notes: +Failure to have auth enabled in Kibana will make for a broken UI. UI-based errors not yet in place + + +|{kib-repo}blob/{branch}/x-pack/plugins/canvas/README.md[canvas] +|"Never look back. The past is done. The future is a blank canvas." ― Suzy Kassem, Rise Up and Salute the Sun + + +|{kib-repo}blob/{branch}/x-pack/plugins/case/README.md[case] +|Experimental Feature + + +|{kib-repo}blob/{branch}/x-pack/plugins/cloud[cloud] +|WARNING: Missing README. + + +|{kib-repo}blob/{branch}/x-pack/plugins/code[code] +|WARNING: Missing README. + + +|{kib-repo}blob/{branch}/x-pack/plugins/console_extensions[consoleExtensions] +|WARNING: Missing README. + + +|{kib-repo}blob/{branch}/x-pack/plugins/cross_cluster_replication/README.md[crossClusterReplication] +|You can run a local cluster and simulate a remote cluster within a single Kibana directory. + + +|<> +|Adds drilldown capabilities to dashboard. Owned by the Kibana App team. + + +|{kib-repo}blob/{branch}/x-pack/plugins/dashboard_mode/README.md[dashboardMode] +|The deprecated dashboard only mode. + + +|{kib-repo}blob/{branch}/x-pack/plugins/data_enhanced[dataEnhanced] +|WARNING: Missing README. + + +|{kib-repo}blob/{branch}/x-pack/plugins/discover_enhanced/README.md[discoverEnhanced] +|Contains the enhancements to the OSS discover app. + + +|<> +|Enhances Embeddables by registering a custom factory provider. The enhanced factory provider +adds dynamic actions to every embeddables state, in order to support drilldowns. + + +|{kib-repo}blob/{branch}/x-pack/plugins/encrypted_saved_objects/README.md[encryptedSavedObjects] +|The purpose of this plugin is to provide a way to encrypt/decrypt attributes on the custom Saved Objects that works with +security and spaces filtering as well as performing audit logging. + + +|{kib-repo}blob/{branch}/x-pack/plugins/enterprise_search/README.md[enterpriseSearch] +|This plugin's goal is to provide a Kibana user interface to the Enterprise Search solution's products (App Search and Workplace Search). In it's current MVP state, the plugin provides the following with the goal of gathering user feedback and raising product awareness: + + +|{kib-repo}blob/{branch}/x-pack/plugins/event_log/README.md[eventLog] +|The purpose of this plugin is to provide a way to persist a history of events +occuring in Kibana, initially just for the Make It Action project - alerts +and actions. + + +|{kib-repo}blob/{branch}/x-pack/plugins/features[features] +|WARNING: Missing README. + + +|{kib-repo}blob/{branch}/x-pack/plugins/file_upload[fileUpload] +|WARNING: Missing README. + + +|{kib-repo}blob/{branch}/x-pack/plugins/global_search/README.md[globalSearch] +|The GlobalSearch plugin provides an easy way to search for various objects, such as applications +or dashboards from the Kibana instance, from both server and client-side plugins + + +|{kib-repo}blob/{branch}/x-pack/plugins/global_search_providers[globalSearchProviders] +|WARNING: Missing README. + + +|{kib-repo}blob/{branch}/x-pack/plugins/graph/README.md[graph] +|This is the main source folder of the Graph plugin. It contains all of the Kibana server and client source code. x-pack/test/functional/apps/graph contains additional functional tests. + + +|{kib-repo}blob/{branch}/x-pack/plugins/grokdebugger[grokdebugger] +|WARNING: Missing README. + + +|{kib-repo}blob/{branch}/x-pack/plugins/index_lifecycle_management/README.md[indexLifecycleManagement] +|You can test that the Frozen badge, phase filtering, and lifecycle information is surfaced in +Index Management by running this series of requests in Console: + + +|{kib-repo}blob/{branch}/x-pack/plugins/index_management/README.md[indexManagement] +|Create a data stream using Console and you'll be able to view it in the UI: + + +|{kib-repo}blob/{branch}/x-pack/plugins/infra/README.md[infra] +|This is the home of the infra plugin, which aims to provide a solution for +the infrastructure monitoring use-case within Kibana. + + +|{kib-repo}blob/{branch}/x-pack/plugins/ingest_manager/README.md[ingestManager] +|Fleet needs to have Elasticsearch API keys enabled, and also to have TLS enabled on kibana, (if you want to run Kibana without TLS you can provide the following config flag --xpack.ingestManager.fleet.tlsCheckDisabled=false) + + +|{kib-repo}blob/{branch}/x-pack/plugins/ingest_pipelines/README.md[ingestPipelines] +|The ingest_pipelines plugin provides Kibana support for Elasticsearch's ingest nodes. Please refer to the Elasticsearch documentation for more details. + + +|{kib-repo}blob/{branch}/x-pack/plugins/lens/readme.md[lens] +|Run all tests from the x-pack root directory + + +|{kib-repo}blob/{branch}/x-pack/plugins/license_management[licenseManagement] +|WARNING: Missing README. + + +|{kib-repo}blob/{branch}/x-pack/plugins/licensing/README.md[licensing] +|The licensing plugin retrieves license data from Elasticsearch at regular configurable intervals. + + +|{kib-repo}blob/{branch}/x-pack/plugins/lists/README.md[lists] +|README.md for developers working on the backend lists on how to get started +using the CURL scripts in the scripts folder. + + +|{kib-repo}blob/{branch}/x-pack/plugins/logstash[logstash] +|WARNING: Missing README. + + +|{kib-repo}blob/{branch}/x-pack/plugins/maps/README.md[maps] +|Visualize geo data from Elasticsearch or 3rd party geo-services. + + +|{kib-repo}blob/{branch}/x-pack/plugins/maps_legacy_licensing/README.md[mapsLegacyLicensing] +|This plugin provides access to the detailed tile map services from Elastic. + + +|{kib-repo}blob/{branch}/x-pack/plugins/ml[ml] +|WARNING: Missing README. + + +|{kib-repo}blob/{branch}/x-pack/plugins/monitoring[monitoring] +|WARNING: Missing README. + + +|{kib-repo}blob/{branch}/x-pack/plugins/observability/README.md[observability] +|This plugin provides shared components and services for use across observability solutions, as well as the observability landing page UI. + + +|{kib-repo}blob/{branch}/x-pack/plugins/oss_telemetry[ossTelemetry] +|WARNING: Missing README. + + +|{kib-repo}blob/{branch}/x-pack/plugins/painless_lab[painlessLab] +|WARNING: Missing README. + + +|{kib-repo}blob/{branch}/x-pack/plugins/remote_clusters[remoteClusters] +|WARNING: Missing README. + + +|{kib-repo}blob/{branch}/x-pack/plugins/reporting/README.md[reporting] +|An awesome Kibana reporting plugin + + +|{kib-repo}blob/{branch}/x-pack/plugins/rollup/README.md[rollup] +|Welcome to the Kibana rollup plugin! This plugin provides Kibana support for Elasticsearch's rollup feature. Please refer to the Elasticsearch documentation to understand rollup indices and how to create rollup jobs. + + +|{kib-repo}blob/{branch}/x-pack/plugins/searchprofiler[searchprofiler] +|WARNING: Missing README. + + +|{kib-repo}blob/{branch}/x-pack/plugins/security/README.md[security] +|See Configuring security in Kibana. + + +|{kib-repo}blob/{branch}/x-pack/plugins/security_solution/README.md[securitySolution] +|Welcome to the Kibana Security Solution plugin! This README will go over getting started with development and testing. + + +|{kib-repo}blob/{branch}/x-pack/plugins/snapshot_restore/README.md[snapshotRestore] +|or + + +|{kib-repo}blob/{branch}/x-pack/plugins/spaces[spaces] +|WARNING: Missing README. + + +|{kib-repo}blob/{branch}/x-pack/plugins/task_manager[taskManager] +|WARNING: Missing README. + + +|{kib-repo}blob/{branch}/x-pack/plugins/telemetry_collection_xpack/README.md[telemetryCollectionXpack] +|Gathers all usage collection, retrieving them from both: OSS and X-Pack plugins. + + +|{kib-repo}blob/{branch}/x-pack/plugins/transform[transform] +|WARNING: Missing README. + + +|{kib-repo}blob/{branch}/x-pack/plugins/translations[translations] +|WARNING: Missing README. + + +|{kib-repo}blob/{branch}/x-pack/plugins/triggers_actions_ui/README.md[triggers_actions_ui] +|The Kibana alerts and actions UI plugin provides a user interface for managing alerts and actions. +As a developer you can reuse and extend built-in alerts and actions UI functionality: + + +|{kib-repo}blob/{branch}/x-pack/plugins/ui_actions_enhanced/README.md[uiActionsEnhanced] +|Registers commercially licensed generic actions like per panel time range and contains some code that supports drilldown work. + + +|{kib-repo}blob/{branch}/x-pack/plugins/upgrade_assistant[upgradeAssistant] +|WARNING: Missing README. + + +|{kib-repo}blob/{branch}/x-pack/plugins/uptime/README.md[uptime] +|The purpose of this plugin is to provide users of Heartbeat more visibility of what's happening +in their infrastructure. + + +|{kib-repo}blob/{branch}/x-pack/plugins/watcher/README.md[watcher] +|This plugins adopts some conventions in addition to or in place of conventions in Kibana (at the time of the plugin's creation): + + +|=== + +include::{kibana-root}/src/plugins/dashboard/README.asciidoc[leveloffset=+1] +include::{kibana-root}/x-pack/plugins/dashboard_enhanced/README.asciidoc[leveloffset=+1] +include::{kibana-root}/x-pack/plugins/embeddable_enhanced/README.asciidoc[leveloffset=+1] diff --git a/packages/kbn-dev-utils/src/plugin_list/discover_plugins.ts b/packages/kbn-dev-utils/src/plugin_list/discover_plugins.ts index 733b9f23a5394..783d584656b17 100644 --- a/packages/kbn-dev-utils/src/plugin_list/discover_plugins.ts +++ b/packages/kbn-dev-utils/src/plugin_list/discover_plugins.ts @@ -25,12 +25,14 @@ import cheerio from 'cheerio'; import { REPO_ROOT } from '../repo_root'; import { simpleKibanaPlatformPluginDiscovery } from '../simple_kibana_platform_plugin_discovery'; +import { extractAsciidocInfo } from './extract_asciidoc_info'; export interface Plugin { id: string; relativeDir?: string; relativeReadmePath?: string; readmeSnippet?: string; + readmeAsciidocAnchor?: string; } export type Plugins = Plugin[]; @@ -38,14 +40,29 @@ export type Plugins = Plugin[]; const getReadmeName = (directory: string) => Fs.readdirSync(directory).find((name) => name.toLowerCase() === 'readme.md'); +const getReadmeAsciidocName = (directory: string) => + Fs.readdirSync(directory).find((name) => name.toLowerCase() === 'readme.asciidoc'); + export const discoverPlugins = (pluginsRootDir: string): Plugins => simpleKibanaPlatformPluginDiscovery([pluginsRootDir], []).map( ({ directory, manifest: { id } }): Plugin => { const readmeName = getReadmeName(directory); + const readmeAsciidocName = getReadmeAsciidocName(directory); let relativeReadmePath: string | undefined; let readmeSnippet: string | undefined; - if (readmeName) { + let readmeAsciidocAnchor: string | undefined; + + if (readmeAsciidocName) { + const readmePath = Path.resolve(directory, readmeAsciidocName); + relativeReadmePath = Path.relative(REPO_ROOT, readmePath); + + const readmeText = Fs.readFileSync(relativeReadmePath).toString(); + + const { firstParagraph, anchor } = extractAsciidocInfo(readmeText); + readmeSnippet = firstParagraph; + readmeAsciidocAnchor = anchor; + } else if (readmeName) { const readmePath = Path.resolve(directory, readmeName); relativeReadmePath = Path.relative(REPO_ROOT, readmePath); @@ -64,6 +81,7 @@ export const discoverPlugins = (pluginsRootDir: string): Plugins => relativeReadmePath, relativeDir: relativeReadmePath || Path.relative(REPO_ROOT, directory), readmeSnippet, + readmeAsciidocAnchor, }; } ); diff --git a/packages/kbn-dev-utils/src/plugin_list/extract_asciidoc_info.test.ts b/packages/kbn-dev-utils/src/plugin_list/extract_asciidoc_info.test.ts new file mode 100644 index 0000000000000..baa88bbe1d2ff --- /dev/null +++ b/packages/kbn-dev-utils/src/plugin_list/extract_asciidoc_info.test.ts @@ -0,0 +1,86 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { extractAsciidocInfo } from './extract_asciidoc_info'; + +it('Returns the info and anchor when there is only one paragraph', () => { + const { firstParagraph, anchor } = extractAsciidocInfo( + `[[this-is-the-anchor]] +== I'm the heading! + +Hello + +I'm an intro paragraph!` + ); + + expect(firstParagraph).toEqual(`Hello\n\nI'm an intro paragraph!`); + expect(anchor).toEqual('this-is-the-anchor'); +}); + +it('Returns the info and anchor when there are multiple paragraphs without an anchor', () => { + const { firstParagraph, anchor } = extractAsciidocInfo( + `[[this-is-the-anchor]] +== Heading here + +Intro. + +=== Another heading + +More details` + ); + + expect(firstParagraph).toEqual(`Intro.`); + expect(anchor).toEqual('this-is-the-anchor'); +}); + +it('Returns the info and anchor when there are multiple paragraphs with anchors', () => { + const { firstParagraph, anchor } = extractAsciidocInfo( + `[[this-is-the-anchor]] +== Heading here + +Intro. + +[[an-anchor]] +=== Another heading + +More details + ` + ); + + expect(firstParagraph).toEqual(`Intro.`); + expect(anchor).toEqual('this-is-the-anchor'); +}); + +it('Returns the info and anchor when there are multiple paragraphs with discrete prefixes', () => { + const { firstParagraph, anchor } = extractAsciidocInfo( + `[[this-is-the-anchor]] +== Heading here + +Intro. + +[discrete] +=== Another heading + +More details + ` + ); + + expect(firstParagraph).toEqual(`Intro.`); + expect(anchor).toEqual('this-is-the-anchor'); +}); diff --git a/packages/kbn-dev-utils/src/plugin_list/extract_asciidoc_info.ts b/packages/kbn-dev-utils/src/plugin_list/extract_asciidoc_info.ts new file mode 100644 index 0000000000000..85b63141a2172 --- /dev/null +++ b/packages/kbn-dev-utils/src/plugin_list/extract_asciidoc_info.ts @@ -0,0 +1,32 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export function extractAsciidocInfo(text: string): { firstParagraph?: string; anchor?: string } { + // First group is to grab the anchor - \[\[(.*)\]\] + // Tecond group, (== ), removes the equals from the header + // Third group could perhaps be done better, but is essentially: + // If there is a sub heading after the intro, match the intro and stop - (([\s\S]*?)(?=\=\=\=|\[\[))) + // If there is not a sub heading after the intro, match the intro - ([\s\S]*) + const matchAnchorAndIntro = /\[\[(.*)\]\]\n(== .*)\n(((([\s\S]*?)(?=\=\=\=|\[)))|([\s\S]*))/gm; + + const matches = matchAnchorAndIntro.exec(text); + const firstParagraph = matches && matches.length >= 4 ? matches[3].toString().trim() : undefined; + const anchor = matches && matches.length >= 2 ? matches[1].toString().trim() : undefined; + return { firstParagraph, anchor }; +} diff --git a/packages/kbn-dev-utils/src/plugin_list/generate_plugin_list.ts b/packages/kbn-dev-utils/src/plugin_list/generate_plugin_list.ts index f0f799862e24e..43dac1cb7d418 100644 --- a/packages/kbn-dev-utils/src/plugin_list/generate_plugin_list.ts +++ b/packages/kbn-dev-utils/src/plugin_list/generate_plugin_list.ts @@ -24,21 +24,29 @@ import normalizePath from 'normalize-path'; import { REPO_ROOT } from '../repo_root'; import { Plugins } from './discover_plugins'; -function* printPlugins(plugins: Plugins) { +function* printPlugins(plugins: Plugins, includes: string[]) { for (const plugin of plugins) { const path = plugin.relativeReadmePath || plugin.relativeDir; yield ''; - yield `- {kib-repo}blob/{branch}/${path}[${plugin.id}]`; + + if (plugin.readmeAsciidocAnchor) { + yield `|<<${plugin.readmeAsciidocAnchor}>>`; + + includes.push(`include::{kibana-root}/${path}[leveloffset=+1]`); + } else { + yield `|{kib-repo}blob/{branch}/${path}[${plugin.id}]`; + } if (!plugin.relativeReadmePath || plugin.readmeSnippet) { - yield ''; - yield plugin.readmeSnippet || 'WARNING: Missing README.'; + yield plugin.readmeSnippet ? `|${plugin.readmeSnippet}` : '|WARNING: Missing README.'; yield ''; } } } export function generatePluginList(ossPlugins: Plugins, xpackPlugins: Plugins) { + const includes: string[] = []; + return `//// NOTE: @@ -53,32 +61,33 @@ NOTE: //// -[[code-exploration]] -== Exploring Kibana code +[[plugin-list]] +== List of {kib} plugins -The goals of our folder heirarchy are: +[discrete] +=== src/plugins -- Easy for developers to know where to add new services, plugins and applications. -- Easy for developers to know where to find the code from services, plugins and applications. -- Easy to browse and understand our folder structure. +[%header,cols=2*] +|=== +|Name +|Description -To that aim, we strive to: +${Array.from(printPlugins(ossPlugins, includes)).join('\n')} -- Avoid too many files in any given folder. -- Choose clear, unambigious folder names. -- Organize by domain. -- Every folder should contain a README that describes the contents of that folder. +|=== [discrete] -[[kibana-services-applications]] -=== Services and Applications +=== x-pack/plugins -[discrete] -==== src/plugins -${Array.from(printPlugins(ossPlugins)).join('\n')} +[%header,cols=2*] +|=== +|Name +|Description -[discrete] -==== x-pack/plugins -${Array.from(printPlugins(xpackPlugins)).join('\n')} +${Array.from(printPlugins(xpackPlugins, includes)).join('\n')} + +|=== + +${Array.from(includes).join('\n')} `; } diff --git a/packages/kbn-dev-utils/src/plugin_list/run_plugin_list_cli.ts b/packages/kbn-dev-utils/src/plugin_list/run_plugin_list_cli.ts index 817534ba5b154..553eb1dd8afa0 100644 --- a/packages/kbn-dev-utils/src/plugin_list/run_plugin_list_cli.ts +++ b/packages/kbn-dev-utils/src/plugin_list/run_plugin_list_cli.ts @@ -28,10 +28,7 @@ import { generatePluginList } from './generate_plugin_list'; const OSS_PLUGIN_DIR = Path.resolve(REPO_ROOT, 'src/plugins'); const XPACK_PLUGIN_DIR = Path.resolve(REPO_ROOT, 'x-pack/plugins'); -const OUTPUT_PATH = Path.resolve( - REPO_ROOT, - 'docs/developer/architecture/code-exploration.asciidoc' -); +const OUTPUT_PATH = Path.resolve(REPO_ROOT, 'docs/developer/plugin-list.asciidoc'); export function runPluginListCli() { run(async ({ log }) => { diff --git a/vars/prChanges.groovy b/vars/prChanges.groovy index 5bdd62946cafc..d082672c065a8 100644 --- a/vars/prChanges.groovy +++ b/vars/prChanges.groovy @@ -22,7 +22,7 @@ def getSkippablePaths() { def getNotSkippablePaths() { return [ // this file is auto-generated and changes to it need to be validated with CI - /^docs\/developer\/architecture\/code-exploration.asciidoc$/, + /^docs\/developer\/plugin-list.asciidoc$/, // don't skip CI on prs with changes to plugin readme files (?i) is for case-insensitive matching /(?i)\/plugins\/[^\/]+\/readme\.(md|asciidoc)$/, ] diff --git a/x-pack/plugins/dashboard_enhanced/README.asciidoc b/x-pack/plugins/dashboard_enhanced/README.asciidoc new file mode 100644 index 0000000000000..2abeeb6a74e0c --- /dev/null +++ b/x-pack/plugins/dashboard_enhanced/README.asciidoc @@ -0,0 +1,5 @@ + +[[dashboard-enhanced-plugin]] +== Dashboard app enhancements plugin + +Adds drilldown capabilities to dashboard. Owned by the Kibana App team. diff --git a/x-pack/plugins/dashboard_enhanced/README.md b/x-pack/plugins/dashboard_enhanced/README.md deleted file mode 100644 index 0aeb156a99f1f..0000000000000 --- a/x-pack/plugins/dashboard_enhanced/README.md +++ /dev/null @@ -1 +0,0 @@ -Contains the enhancements to the OSS dashboard app. \ No newline at end of file diff --git a/x-pack/plugins/grokdebugger/README.md b/x-pack/plugins/grokdebugger/README.md deleted file mode 100644 index e69de29bb2d1d..0000000000000 diff --git a/x-pack/plugins/ui_actions_enhanced/README.md b/x-pack/plugins/ui_actions_enhanced/README.md index 1a72a431e3975..a4a37b559ff8d 100644 --- a/x-pack/plugins/ui_actions_enhanced/README.md +++ b/x-pack/plugins/ui_actions_enhanced/README.md @@ -1,3 +1,5 @@ # `ui_actions_enhanced` +Registers commercially licensed generic actions like per panel time range and contains some code that supports drilldown work. + - [__Dashboard drilldown user docs__](https://www.elastic.co/guide/en/kibana/master/drilldowns.html) From 33d3f4090a167bea2d686ccc92b3fb0920b08f68 Mon Sep 17 00:00:00 2001 From: Melissa Alvarez Date: Mon, 24 Aug 2020 11:46:18 -0400 Subject: [PATCH 05/71] [ML] DF Analytics list: ensure job messages and jobs load correctly (#75676) * ensure messages load on first open * ensure analytics management list does not load infinitely --- .../analytics_list/analytics_list.tsx | 6 +- .../expanded_row_messages_pane.tsx | 57 ++++++++----------- 2 files changed, 27 insertions(+), 36 deletions(-) diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/analytics_list.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/analytics_list.tsx index 81494a43193dc..c4c7a8a4ca11a 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/analytics_list.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/analytics_list.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { FC, useState, useEffect } from 'react'; +import React, { FC, useCallback, useState, useEffect } from 'react'; import { i18n } from '@kbn/i18n'; @@ -119,11 +119,13 @@ export const DataFrameAnalyticsList: FC = ({ } }, [selectedIdFromUrlInitialized, analytics]); + const getAnalyticsCallback = useCallback(() => getAnalytics(true), []); + // Subscribe to the refresh observable to trigger reloading the analytics list. useRefreshAnalyticsList( { isLoading: setIsLoading, - onRefresh: () => getAnalytics(true), + onRefresh: getAnalyticsCallback, }, isManagementTable ); diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/expanded_row_messages_pane.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/expanded_row_messages_pane.tsx index 0dd9eba172e1c..942e335526d68 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/expanded_row_messages_pane.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/expanded_row_messages_pane.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { FC, useState } from 'react'; +import React, { FC, useState, useEffect, useCallback } from 'react'; import { i18n } from '@kbn/i18n'; import { ml } from '../../../../../services/ml_api_service'; import { useRefreshAnalyticsList } from '../../../../common'; @@ -20,45 +20,34 @@ export const ExpandedRowMessagesPane: FC = ({ analyticsId }) => { const [isLoading, setIsLoading] = useState(false); const [errorMessage, setErrorMessage] = useState(''); - const getMessagesFactory = () => { - let concurrentLoads = 0; - return async function getMessages() { - try { - concurrentLoads++; - - if (concurrentLoads > 1) { - return; - } - - setIsLoading(true); - const messagesResp = await ml.dataFrameAnalytics.getAnalyticsAuditMessages(analyticsId); - setIsLoading(false); - setMessages(messagesResp); - - concurrentLoads--; - - if (concurrentLoads > 0) { - concurrentLoads = 0; - getMessages(); - } - } catch (error) { - setIsLoading(false); - setErrorMessage( - i18n.translate('xpack.ml.dfAnalyticsList.analyticsDetails.messagesPane.errorMessage', { - defaultMessage: 'Messages could not be loaded', - }) - ); - } - }; - }; - useRefreshAnalyticsList({ onRefresh: getMessagesFactory() }); + const getMessages = useCallback(async () => { + try { + setIsLoading(true); + const messagesResp = await ml.dataFrameAnalytics.getAnalyticsAuditMessages(analyticsId); + setIsLoading(false); + setMessages(messagesResp); + } catch (error) { + setIsLoading(false); + setErrorMessage( + i18n.translate('xpack.ml.dfAnalyticsList.analyticsDetails.messagesPane.errorMessage', { + defaultMessage: 'Messages could not be loaded', + }) + ); + } + }, []); + + useEffect(() => { + getMessages(); + }, []); + + useRefreshAnalyticsList({ onRefresh: getMessages }); return ( ); }; From f2f83b0f689a1638868bd76fafb70c075ac1e69f Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Mon, 24 Aug 2020 17:01:43 +0100 Subject: [PATCH 06/71] skip flaky suite (#75722) --- .../tests/actions/builtin_action_types/jira.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/jira.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/jira.ts index a0ba5331105bc..78831fe8ff061 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/jira.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/jira.ts @@ -78,7 +78,8 @@ export default function jiraTest({ getService }: FtrProviderContext) { let proxyServer: any; let proxyHaveBeenCalled = false; - describe('Jira', () => { + // FLAKY: https://github.com/elastic/kibana/issues/75722 + describe.skip('Jira', () => { before(() => { jiraSimulatorURL = kibanaServer.resolveUrl( getExternalServiceSimulatorPath(ExternalServiceSimulator.JIRA) From 3dea4444b9bbc5dd9ff04d8517ff86d66f4de563 Mon Sep 17 00:00:00 2001 From: Wylie Conlon Date: Mon, 24 Aug 2020 12:07:28 -0400 Subject: [PATCH 07/71] [Lens] Remove beta labels (#75574) * [Lens] Remove beta labels * Remove translations Co-authored-by: Elastic Machine --- .../editor_frame/workspace_panel/workspace_panel.tsx | 8 +------- x-pack/plugins/lens/public/vis_type_alias.ts | 4 ++-- x-pack/plugins/translations/translations/ja-JP.json | 1 - x-pack/plugins/translations/translations/zh-CN.json | 1 - 4 files changed, 3 insertions(+), 11 deletions(-) diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.tsx index 4f914bc65dc7c..06cd858eda210 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.tsx @@ -13,7 +13,6 @@ import { EuiIcon, EuiImage, EuiText, - EuiBetaBadge, EuiButtonEmpty, EuiLink, } from '@elastic/eui'; @@ -210,10 +209,6 @@ export function InnerWorkspacePanel({ } function renderEmptyWorkspace() { - const tooltipContent = i18n.translate('xpack.lens.editorFrame.tooltipContent', { - defaultMessage: - 'Lens is in beta and is subject to change. The design and code is less mature than official GA features and is being provided as-is with no warranties. Beta features are not subject to the support SLA of official GA features', - }); return (
@@ -232,8 +227,7 @@ export function InnerWorkspacePanel({ {' '} - + />

diff --git a/x-pack/plugins/lens/public/vis_type_alias.ts b/x-pack/plugins/lens/public/vis_type_alias.ts index 3bb2dbbae1f9c..d0dceed03db2f 100644 --- a/x-pack/plugins/lens/public/vis_type_alias.ts +++ b/x-pack/plugins/lens/public/vis_type_alias.ts @@ -27,7 +27,7 @@ export const getLensAliasConfig = (): VisTypeAlias => ({ defaultMessage: `Lens is a simpler way to create basic visualizations`, }), icon: 'lensApp', - stage: 'beta', + stage: 'production', appExtensions: { visualizations: { docTypes: ['lens'], @@ -42,7 +42,7 @@ export const getLensAliasConfig = (): VisTypeAlias => ({ editUrl: getEditPath(id), editApp: 'lens', icon: 'lensApp', - stage: 'beta', + stage: 'production', savedObjectType: type, typeTitle: i18n.translate('xpack.lens.visTypeAlias.type', { defaultMessage: 'Lens' }), }; diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 8287f8f42abdc..5572fc85bf130 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -9879,7 +9879,6 @@ "xpack.lens.editorFrame.quickFunctionsLabel": "クイック機能", "xpack.lens.editorFrame.requiredDimensionWarningLabel": "必要な次元", "xpack.lens.editorFrame.suggestionPanelTitle": "提案", - "xpack.lens.editorFrame.tooltipContent": "レンズはベータ段階で、変更される可能性があります。 デザインとコードはオフィシャル GA 機能よりも完成度が低く、現状のまま保証なしで提供されています。ベータ機能にはオフィシャル GA 機能の SLA が適用されません", "xpack.lens.embeddable.failure": "ビジュアライゼーションを表示できませんでした", "xpack.lens.embeddableDisplayName": "レンズ", "xpack.lens.excludeValueButtonAriaLabel": "{value}を除外", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index aff78ad79ae48..36691eeadb928 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -9882,7 +9882,6 @@ "xpack.lens.editorFrame.quickFunctionsLabel": "快选函数", "xpack.lens.editorFrame.requiredDimensionWarningLabel": "所需尺寸", "xpack.lens.editorFrame.suggestionPanelTitle": "建议", - "xpack.lens.editorFrame.tooltipContent": "Lens 为公测版,可能会进行更改。 设计和代码相对于正式发行版功能还不够成熟,将按原样提供,且不提供任何保证。公测版功能不受正式发行版功能支持 SLA 的约束", "xpack.lens.embeddable.failure": "无法显示可视化", "xpack.lens.embeddableDisplayName": "lens", "xpack.lens.excludeValueButtonAriaLabel": "排除 {value}", From 9fa43b4e476aae9ea19fe445ef59425bd581ef39 Mon Sep 17 00:00:00 2001 From: Pierre Gayvallet Date: Mon, 24 Aug 2020 18:25:02 +0200 Subject: [PATCH 08/71] avoid error when logging invalid response error (#75757) --- .../client/configure_client.test.ts | 38 +++++++++++++++++++ .../elasticsearch/client/configure_client.ts | 7 +--- 2 files changed, 40 insertions(+), 5 deletions(-) diff --git a/src/core/server/elasticsearch/client/configure_client.test.ts b/src/core/server/elasticsearch/client/configure_client.test.ts index 11e3199a79fd2..716e2fd98a5e1 100644 --- a/src/core/server/elasticsearch/client/configure_client.test.ts +++ b/src/core/server/elasticsearch/client/configure_client.test.ts @@ -157,6 +157,44 @@ describe('configureClient', () => { `); }); + it('logs default error info when the error response body is empty', () => { + const client = configureClient(config, { logger, scoped: false }); + + let response = createApiResponse({ + statusCode: 400, + headers: {}, + body: { + error: {}, + }, + }); + client.emit('response', new errors.ResponseError(response), response); + + expect(loggingSystemMock.collect(logger).error).toMatchInlineSnapshot(` + Array [ + Array [ + "[ResponseError]: Response Error", + ], + ] + `); + + logger.error.mockClear(); + + response = createApiResponse({ + statusCode: 400, + headers: {}, + body: {} as any, + }); + client.emit('response', new errors.ResponseError(response), response); + + expect(loggingSystemMock.collect(logger).error).toMatchInlineSnapshot(` + Array [ + Array [ + "[ResponseError]: Response Error", + ], + ] + `); + }); + it('logs each queries if `logQueries` is true', () => { const client = configureClient( createFakeConfig({ diff --git a/src/core/server/elasticsearch/client/configure_client.ts b/src/core/server/elasticsearch/client/configure_client.ts index 9746ecb538b75..a777344813068 100644 --- a/src/core/server/elasticsearch/client/configure_client.ts +++ b/src/core/server/elasticsearch/client/configure_client.ts @@ -21,7 +21,6 @@ import { stringify } from 'querystring'; import { Client } from '@elastic/elasticsearch'; import { Logger } from '../../logging'; import { parseClientOptions, ElasticsearchClientConfig } from './client_config'; -import { isResponseError } from './errors'; export const configureClient = ( config: ElasticsearchClientConfig, @@ -39,10 +38,8 @@ const addLogging = (client: Client, logger: Logger, logQueries: boolean) => { client.on('response', (error, event) => { if (error) { const errorMessage = - // error details for response errors provided by elasticsearch - isResponseError(error) - ? `[${event.body.error.type}]: ${event.body.error.reason}` - : `[${error.name}]: ${error.message}`; + // error details for response errors provided by elasticsearch, defaults to error name/message + `[${event.body?.error?.type ?? error.name}]: ${event.body?.error?.reason ?? error.message}`; logger.error(errorMessage); } From f49f010d906f4ed9de2014964e266e0195c73bb7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Fern=C3=A1ndez=20Haro?= Date: Mon, 24 Aug 2020 17:28:19 +0100 Subject: [PATCH 09/71] [Telemetry] Swallow errors in opt-in remote notification from the server (#75641) --- src/plugins/telemetry/server/plugin.ts | 1 + src/plugins/telemetry/server/routes/index.ts | 3 ++- .../telemetry/server/routes/telemetry_opt_in.ts | 13 ++++++++++--- 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/src/plugins/telemetry/server/plugin.ts b/src/plugins/telemetry/server/plugin.ts index 6c8888feafc1f..bd7a2a8c1a8ca 100644 --- a/src/plugins/telemetry/server/plugin.ts +++ b/src/plugins/telemetry/server/plugin.ts @@ -89,6 +89,7 @@ export class TelemetryPlugin implements Plugin { config$, currentKibanaVersion, isDev, + logger: this.logger, router, telemetryCollectionManager, }); diff --git a/src/plugins/telemetry/server/routes/index.ts b/src/plugins/telemetry/server/routes/index.ts index ad84cb9d2665d..f46c616a734e0 100644 --- a/src/plugins/telemetry/server/routes/index.ts +++ b/src/plugins/telemetry/server/routes/index.ts @@ -18,7 +18,7 @@ */ import { Observable } from 'rxjs'; -import { IRouter } from 'kibana/server'; +import { IRouter, Logger } from 'kibana/server'; import { TelemetryCollectionManagerPluginSetup } from 'src/plugins/telemetry_collection_manager/server'; import { registerTelemetryOptInRoutes } from './telemetry_opt_in'; import { registerTelemetryUsageStatsRoutes } from './telemetry_usage_stats'; @@ -28,6 +28,7 @@ import { TelemetryConfigType } from '../config'; interface RegisterRoutesParams { isDev: boolean; + logger: Logger; config$: Observable; currentKibanaVersion: string; router: IRouter; diff --git a/src/plugins/telemetry/server/routes/telemetry_opt_in.ts b/src/plugins/telemetry/server/routes/telemetry_opt_in.ts index 7dd15f73029e7..aa1de4b2443a4 100644 --- a/src/plugins/telemetry/server/routes/telemetry_opt_in.ts +++ b/src/plugins/telemetry/server/routes/telemetry_opt_in.ts @@ -21,7 +21,7 @@ import moment from 'moment'; import { Observable } from 'rxjs'; import { take } from 'rxjs/operators'; import { schema } from '@kbn/config-schema'; -import { IRouter } from 'kibana/server'; +import { IRouter, Logger } from 'kibana/server'; import { StatsGetterConfig, TelemetryCollectionManagerPluginSetup, @@ -39,12 +39,14 @@ import { TelemetryConfigType } from '../config'; interface RegisterOptInRoutesParams { currentKibanaVersion: string; router: IRouter; + logger: Logger; config$: Observable; telemetryCollectionManager: TelemetryCollectionManagerPluginSetup; } export function registerTelemetryOptInRoutes({ config$, + logger, router, currentKibanaVersion, telemetryCollectionManager, @@ -95,11 +97,16 @@ export function registerTelemetryOptInRoutes({ if (config.sendUsageFrom === 'server') { const optInStatusUrl = config.optInStatusUrl; - await sendTelemetryOptInStatus( + sendTelemetryOptInStatus( telemetryCollectionManager, { optInStatusUrl, newOptInStatus }, statsGetterConfig - ); + ).catch((err) => { + // The server is likely behind a firewall and can't reach the remote service + logger.warn( + `Failed to notify "${optInStatusUrl}" from the server about the opt-in selection. Possibly blocked by a firewall? - Error: ${err.message}` + ); + }); } await updateTelemetrySavedObject(context.core.savedObjects.client, attributes); From a3d3abd22d1a7c45d07768d0c3adc15d97a9029a Mon Sep 17 00:00:00 2001 From: Thomas Neirynck Date: Mon, 24 Aug 2020 12:29:52 -0400 Subject: [PATCH 10/71] [Maps] Introduce ILayer#isFittable (#75504) --- .../public/actions/data_request_actions.ts | 6 +- .../maps/public/classes/layers/layer.test.ts | 45 ++++++++++++ .../maps/public/classes/layers/layer.tsx | 5 ++ .../layers/vector_layer/vector_layer.d.ts | 1 + .../fit_to_data/fit_to_data.tsx | 73 ++++++++++++++----- .../toolbar_overlay/fit_to_data/index.ts | 4 +- .../maps/public/selectors/map_selectors.ts | 14 ---- 7 files changed, 111 insertions(+), 37 deletions(-) diff --git a/x-pack/plugins/maps/public/actions/data_request_actions.ts b/x-pack/plugins/maps/public/actions/data_request_actions.ts index 41d9f3fc13b5b..2876f3d668a69 100644 --- a/x-pack/plugins/maps/public/actions/data_request_actions.ts +++ b/x-pack/plugins/maps/public/actions/data_request_actions.ts @@ -15,7 +15,6 @@ import { LAYER_STYLE_TYPE, LAYER_TYPE, SOURCE_DATA_REQUEST_ID } from '../../comm import { getDataFilters, getDataRequestDescriptor, - getFittableLayers, getLayerById, getLayerList, } from '../selectors/map_selectors'; @@ -324,13 +323,16 @@ export function fitToLayerExtent(layerId: string) { export function fitToDataBounds(onNoBounds?: () => void) { return async (dispatch: Dispatch, getState: () => MapStoreState) => { - const layerList = getFittableLayers(getState()); + const layerList = getLayerList(getState()); if (!layerList.length) { return; } const boundsPromises = layerList.map(async (layer: ILayer) => { + if (!(await layer.isFittable())) { + return null; + } return layer.getBounds(getDataRequestContext(dispatch, getState, layer.getId())); }); diff --git a/x-pack/plugins/maps/public/classes/layers/layer.test.ts b/x-pack/plugins/maps/public/classes/layers/layer.test.ts index f25ecd7106457..7bc91d71f83e2 100644 --- a/x-pack/plugins/maps/public/classes/layers/layer.test.ts +++ b/x-pack/plugins/maps/public/classes/layers/layer.test.ts @@ -21,6 +21,10 @@ jest.mock('uuid/v4', () => { class MockLayer extends AbstractLayer {} class MockSource { + private readonly _fitToBounds: boolean; + constructor({ fitToBounds = true } = {}) { + this._fitToBounds = fitToBounds; + } cloneDescriptor() { return {}; } @@ -28,6 +32,10 @@ class MockSource { getDisplayName() { return 'mySource'; } + + async supportsFitToBounds() { + return this._fitToBounds; + } } class MockStyle {} @@ -126,3 +134,40 @@ describe('cloneDescriptor', () => { }); }); }); + +describe('isFittable', () => { + [ + { + isVisible: true, + fitToBounds: true, + canFit: true, + }, + { + isVisible: false, + fitToBounds: true, + canFit: false, + }, + { + isVisible: true, + fitToBounds: false, + canFit: false, + }, + { + isVisible: false, + fitToBounds: false, + canFit: false, + }, + ].forEach((test) => { + it(`Should take into account layer visibility and bounds-retrieval: ${JSON.stringify( + test + )}`, async () => { + const layerDescriptor = AbstractLayer.createDescriptor({ visible: test.isVisible }); + const layer = new MockLayer({ + layerDescriptor, + source: (new MockSource({ fitToBounds: test.fitToBounds }) as unknown) as ISource, + style: (new MockStyle() as unknown) as IStyle, + }); + expect(await layer.isFittable()).toBe(test.canFit); + }); + }); +}); diff --git a/x-pack/plugins/maps/public/classes/layers/layer.tsx b/x-pack/plugins/maps/public/classes/layers/layer.tsx index 424100c5a7e3a..8026f48fe6093 100644 --- a/x-pack/plugins/maps/public/classes/layers/layer.tsx +++ b/x-pack/plugins/maps/public/classes/layers/layer.tsx @@ -90,6 +90,7 @@ export interface ILayer { supportsLabelsOnTop: () => boolean; showJoinEditor(): boolean; getJoinsDisabledReason(): string | null; + isFittable(): Promise; } export type Footnote = { icon: ReactElement; @@ -233,6 +234,10 @@ export class AbstractLayer implements ILayer { return await this.getSource().supportsFitToBounds(); } + async isFittable(): Promise { + return (await this.supportsFitToBounds()) && this.isVisible(); + } + async getDisplayName(source?: ISource): Promise { if (this._descriptor.label) { return this._descriptor.label; diff --git a/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.d.ts b/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.d.ts index e6cb212daddae..ad4479d3a324b 100644 --- a/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.d.ts +++ b/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.d.ts @@ -83,4 +83,5 @@ export class VectorLayer extends AbstractLayer implements IVectorLayer { getFeatureById(id: string | number): Feature | null; getPropertiesForTooltip(properties: GeoJsonProperties): Promise; hasJoins(): boolean; + isFittable(): Promise; } diff --git a/x-pack/plugins/maps/public/connected_components/toolbar_overlay/fit_to_data/fit_to_data.tsx b/x-pack/plugins/maps/public/connected_components/toolbar_overlay/fit_to_data/fit_to_data.tsx index ca75060c4f8df..3f56d8d50b0f0 100644 --- a/x-pack/plugins/maps/public/connected_components/toolbar_overlay/fit_to_data/fit_to_data.tsx +++ b/x-pack/plugins/maps/public/connected_components/toolbar_overlay/fit_to_data/fit_to_data.tsx @@ -15,24 +15,59 @@ interface Props { fitToBounds: () => void; } -export const FitToData: React.FunctionComponent = ({ layerList, fitToBounds }: Props) => { - if (layerList.length === 0) { - return null; +interface State { + canFit: boolean; +} + +export class FitToData extends React.Component { + _isMounted: boolean = false; + + state = { canFit: false }; + + componentDidMount(): void { + this._isMounted = true; + this._loadCanFit(); } - return ( - - ); -}; + componentWillUnmount(): void { + this._isMounted = false; + } + + componentDidUpdate(): void { + this._loadCanFit(); + } + + async _loadCanFit() { + const promises = this.props.layerList.map(async (layer) => { + return await layer.isFittable(); + }); + const canFit = (await Promise.all(promises)).some((isFittable) => isFittable); + if (this._isMounted && this.state.canFit !== canFit) { + this.setState({ + canFit, + }); + } + } + + render() { + if (!this.state.canFit) { + return null; + } + + return ( + + ); + } +} diff --git a/x-pack/plugins/maps/public/connected_components/toolbar_overlay/fit_to_data/index.ts b/x-pack/plugins/maps/public/connected_components/toolbar_overlay/fit_to_data/index.ts index 51bf0a519e380..8790f6f35c574 100644 --- a/x-pack/plugins/maps/public/connected_components/toolbar_overlay/fit_to_data/index.ts +++ b/x-pack/plugins/maps/public/connected_components/toolbar_overlay/fit_to_data/index.ts @@ -8,12 +8,12 @@ import { AnyAction, Dispatch } from 'redux'; import { connect } from 'react-redux'; import { MapStoreState } from '../../../reducers/store'; import { fitToDataBounds } from '../../../actions'; -import { getFittableLayers } from '../../../selectors/map_selectors'; +import { getLayerList } from '../../../selectors/map_selectors'; import { FitToData } from './fit_to_data'; function mapStateToProps(state: MapStoreState) { return { - layerList: getFittableLayers(state), + layerList: getLayerList(state), }; } diff --git a/x-pack/plugins/maps/public/selectors/map_selectors.ts b/x-pack/plugins/maps/public/selectors/map_selectors.ts index d48ee24027561..03e0f753812c9 100644 --- a/x-pack/plugins/maps/public/selectors/map_selectors.ts +++ b/x-pack/plugins/maps/public/selectors/map_selectors.ts @@ -25,7 +25,6 @@ import { InnerJoin } from '../classes/joins/inner_join'; import { getSourceByType } from '../classes/sources/source_registry'; import { GeojsonFileSource } from '../classes/sources/geojson_file_source'; import { - LAYER_TYPE, SOURCE_DATA_REQUEST_ID, STYLE_TYPE, VECTOR_STYLES, @@ -307,19 +306,6 @@ export function getLayerById(layerId: string | null, state: MapStoreState): ILay }); } -export const getFittableLayers = createSelector(getLayerList, (layerList) => { - return layerList.filter((layer) => { - // These are the only layer-types that implement bounding-box retrieval reliably - // This will _not_ work if Maps will allow register custom layer types - const isFittable = - layer.getType() === LAYER_TYPE.VECTOR || - layer.getType() === LAYER_TYPE.BLENDED_VECTOR || - layer.getType() === LAYER_TYPE.HEATMAP; - - return isFittable && layer.isVisible(); - }); -}); - export const getHiddenLayerIds = createSelector(getLayerListRaw, (layers) => layers.filter((layer) => !layer.visible).map((layer) => layer.id) ); From 8fe62c33a5a6fc96e6b3f94ba0111cfb22760a20 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Fern=C3=A1ndez=20Haro?= Date: Mon, 24 Aug 2020 17:32:52 +0100 Subject: [PATCH 11/71] [Data Telemetry] Rename dataset.* to data_stream.* (#75415) Co-authored-by: Elastic Machine --- .../get_data_telemetry.test.ts | 56 +++++++++---------- .../get_data_telemetry/get_data_telemetry.ts | 56 ++++++++++--------- 2 files changed, 58 insertions(+), 54 deletions(-) diff --git a/src/plugins/telemetry/server/telemetry_collection/get_data_telemetry/get_data_telemetry.test.ts b/src/plugins/telemetry/server/telemetry_collection/get_data_telemetry/get_data_telemetry.test.ts index ad19def160200..dee718decdc1f 100644 --- a/src/plugins/telemetry/server/telemetry_collection/get_data_telemetry/get_data_telemetry.test.ts +++ b/src/plugins/telemetry/server/telemetry_collection/get_data_telemetry/get_data_telemetry.test.ts @@ -59,16 +59,16 @@ describe('get_data_telemetry', () => { test('matches some indices and puts them in their own category', () => { expect( buildDataTelemetryPayload([ - // APM Indices have known shipper (so we can infer the datasetType from mapping constant) + // APM Indices have known shipper (so we can infer the dataStreamType from mapping constant) { name: 'apm-7.7.0-error-000001', shipper: 'apm', isECS: true }, { name: 'apm-7.7.0-metric-000001', shipper: 'apm', isECS: true }, { name: 'apm-7.7.0-onboarding-2020.05.17', shipper: 'apm', isECS: true }, { name: 'apm-7.7.0-profile-000001', shipper: 'apm', isECS: true }, { name: 'apm-7.7.0-span-000001', shipper: 'apm', isECS: true }, { name: 'apm-7.7.0-transaction-000001', shipper: 'apm', isECS: true }, - // Packetbeat indices with known shipper (we can infer datasetType from mapping constant) + // Packetbeat indices with known shipper (we can infer dataStreamType from mapping constant) { name: 'packetbeat-7.7.0-2020.06.11-000001', shipper: 'packetbeat', isECS: true }, - // Matching patterns from the list => known datasetName but the rest is unknown + // Matching patterns from the list => known dataStreamDataset but the rest is unknown { name: 'filebeat-12314', docCount: 100, sizeInBytes: 10 }, { name: 'metricbeat-1234', docCount: 100, sizeInBytes: 10, isECS: false }, { name: '.app-search-1234', docCount: 0 }, @@ -76,8 +76,8 @@ describe('get_data_telemetry', () => { // New Indexing strategy: everything can be inferred from the constant_keyword values { name: '.ds-logs-nginx.access-default-000001', - datasetName: 'nginx.access', - datasetType: 'logs', + dataStreamDataset: 'nginx.access', + dataStreamType: 'logs', shipper: 'filebeat', isECS: true, docCount: 1000, @@ -85,8 +85,8 @@ describe('get_data_telemetry', () => { }, { name: '.ds-logs-nginx.access-default-000002', - datasetName: 'nginx.access', - datasetType: 'logs', + dataStreamDataset: 'nginx.access', + dataStreamType: 'logs', shipper: 'filebeat', isECS: true, docCount: 1000, @@ -94,8 +94,8 @@ describe('get_data_telemetry', () => { }, { name: '.ds-traces-something-default-000002', - datasetName: 'something', - datasetType: 'traces', + dataStreamDataset: 'something', + dataStreamType: 'traces', packageName: 'some-package', isECS: true, docCount: 1000, @@ -103,26 +103,26 @@ describe('get_data_telemetry', () => { }, { name: '.ds-metrics-something.else-default-000002', - datasetName: 'something.else', - datasetType: 'metrics', + dataStreamDataset: 'something.else', + dataStreamType: 'metrics', managedBy: 'ingest-manager', isECS: true, docCount: 1000, sizeInBytes: 60, }, - // Filter out if it has datasetName and datasetType but none of the shipper, packageName or managedBy === 'ingest-manager' + // Filter out if it has dataStreamDataset and dataStreamType but none of the shipper, packageName or managedBy === 'ingest-manager' { name: 'some-index-that-should-not-show', - datasetName: 'should-not-show', - datasetType: 'logs', + dataStreamDataset: 'should-not-show', + dataStreamType: 'logs', isECS: true, docCount: 1000, sizeInBytes: 60, }, { name: 'other-index-that-should-not-show', - datasetName: 'should-not-show-either', - datasetType: 'metrics', + dataStreamDataset: 'should-not-show-either', + dataStreamType: 'metrics', managedBy: 'me', isECS: true, docCount: 1000, @@ -167,7 +167,7 @@ describe('get_data_telemetry', () => { doc_count: 0, }, { - dataset: { name: 'nginx.access', type: 'logs' }, + data_stream: { dataset: 'nginx.access', type: 'logs' }, shipper: 'filebeat', index_count: 2, ecs_index_count: 2, @@ -175,7 +175,7 @@ describe('get_data_telemetry', () => { size_in_bytes: 1060, }, { - dataset: { name: 'something', type: 'traces' }, + data_stream: { dataset: 'something', type: 'traces' }, package: { name: 'some-package' }, index_count: 1, ecs_index_count: 1, @@ -183,7 +183,7 @@ describe('get_data_telemetry', () => { size_in_bytes: 60, }, { - dataset: { name: 'something.else', type: 'metrics' }, + data_stream: { dataset: 'something.else', type: 'metrics' }, index_count: 1, ecs_index_count: 1, doc_count: 1000, @@ -236,7 +236,7 @@ describe('get_data_telemetry', () => { test('find an index that does not match any index pattern but has mappings metadata', async () => { const callCluster = mockCallCluster( ['cannot_match_anything'], - { isECS: true, datasetType: 'traces', shipper: 'my-beat' }, + { isECS: true, dataStreamType: 'traces', shipper: 'my-beat' }, { indices: { cannot_match_anything: { @@ -247,7 +247,7 @@ describe('get_data_telemetry', () => { ); await expect(getDataTelemetry(callCluster)).resolves.toStrictEqual([ { - dataset: { name: undefined, type: 'traces' }, + data_stream: { dataset: undefined, type: 'traces' }, shipper: 'my-beat', index_count: 1, ecs_index_count: 1, @@ -266,7 +266,7 @@ describe('get_data_telemetry', () => { function mockCallCluster( indicesMappings: string[] = [], - { isECS = false, datasetName = '', datasetType = '', shipper = '' } = {}, + { isECS = false, dataStreamDataset = '', dataStreamType = '', shipper = '' } = {}, indexStats: any = {} ) { return jest.fn().mockImplementation(async (method: string, opts: any) => { @@ -279,14 +279,14 @@ function mockCallCluster( ...(shipper && { _meta: { beat: shipper } }), properties: { ...(isECS && { ecs: { properties: { version: { type: 'keyword' } } } }), - ...((datasetType || datasetName) && { - dataset: { + ...((dataStreamType || dataStreamDataset) && { + data_stream: { properties: { - ...(datasetName && { - name: { type: 'constant_keyword', value: datasetName }, + ...(dataStreamDataset && { + dataset: { type: 'constant_keyword', value: dataStreamDataset }, }), - ...(datasetType && { - type: { type: 'constant_keyword', value: datasetType }, + ...(dataStreamType && { + type: { type: 'constant_keyword', value: dataStreamType }, }), }, }, diff --git a/src/plugins/telemetry/server/telemetry_collection/get_data_telemetry/get_data_telemetry.ts b/src/plugins/telemetry/server/telemetry_collection/get_data_telemetry/get_data_telemetry.ts index 079f510bb256a..f4734dde251cc 100644 --- a/src/plugins/telemetry/server/telemetry_collection/get_data_telemetry/get_data_telemetry.ts +++ b/src/plugins/telemetry/server/telemetry_collection/get_data_telemetry/get_data_telemetry.ts @@ -32,9 +32,9 @@ export interface DataTelemetryBasePayload { } export interface DataTelemetryDocument extends DataTelemetryBasePayload { - dataset?: { - name?: string; - type?: DataTelemetryType | 'unknown' | string; // The union of types is to help autocompletion with some known `dataset.type`s + data_stream?: { + dataset?: string; + type?: DataTelemetryType | string; // The union of types is to help autocompletion with some known `data_stream.type`s }; package?: { name: string; @@ -49,8 +49,8 @@ export interface DataTelemetryIndex { name: string; packageName?: string; // Populated by Ingest Manager at `_meta.package.name` managedBy?: string; // Populated by Ingest Manager at `_meta.managed_by` - datasetName?: string; // To be obtained from `mappings.dataset.name` if it's a constant keyword - datasetType?: string; // To be obtained from `mappings.dataset.type` if it's a constant keyword + dataStreamDataset?: string; // To be obtained from `mappings.data_stream.dataset` if it's a constant keyword + dataStreamType?: string; // To be obtained from `mappings.data_stream.type` if it's a constant keyword shipper?: string; // To be obtained from `_meta.beat` if it's set isECS?: boolean; // Optional because it can't be obtained via Monitoring. @@ -64,8 +64,8 @@ type AtLeastOne }> = Partial & U[keyof U] type DataDescriptor = AtLeastOne<{ packageName: string; - datasetName: string; - datasetType: string; + dataStreamDataset: string; + dataStreamType: string; shipper: string; patternName: DataPatternName; // When found from the list of the index patterns }>; @@ -75,24 +75,24 @@ function findMatchingDescriptors({ shipper, packageName, managedBy, - datasetName, - datasetType, + dataStreamDataset, + dataStreamType, }: DataTelemetryIndex): DataDescriptor[] { // If we already have the data from the indices' mappings... if ( [shipper, packageName].some(Boolean) || - (managedBy === 'ingest-manager' && [datasetType, datasetName].some(Boolean)) + (managedBy === 'ingest-manager' && [dataStreamType, dataStreamDataset].some(Boolean)) ) { return [ { ...(shipper && { shipper }), ...(packageName && { packageName }), - ...(datasetName && { datasetName }), - ...(datasetType && { datasetType }), + ...(dataStreamDataset && { dataStreamDataset }), + ...(dataStreamType && { dataStreamType }), } as AtLeastOne<{ packageName: string; - datasetName: string; - datasetType: string; + dataStreamDataset: string; + dataStreamType: string; shipper: string; }>, // Using casting here because TS doesn't infer at least one exists from the if clause ]; @@ -149,15 +149,17 @@ export function buildDataTelemetryPayload(indices: DataTelemetryIndex[]): DataTe for (const indexCandidate of indexCandidates) { const matchingDescriptors = findMatchingDescriptors(indexCandidate); for (const { - datasetName, - datasetType, + dataStreamDataset, + dataStreamType, packageName, shipper, patternName, } of matchingDescriptors) { - const key = `${datasetName}-${datasetType}-${packageName}-${shipper}-${patternName}`; + const key = `${dataStreamDataset}-${dataStreamType}-${packageName}-${shipper}-${patternName}`; acc.set(key, { - ...((datasetName || datasetType) && { dataset: { name: datasetName, type: datasetType } }), + ...((dataStreamDataset || dataStreamType) && { + data_stream: { dataset: dataStreamDataset, type: dataStreamType }, + }), ...(packageName && { package: { name: packageName } }), ...(shipper && { shipper }), ...(patternName && { pattern_name: patternName }), @@ -198,9 +200,9 @@ interface IndexMappings { managed_by?: string; // Typically "ingest-manager" }; properties: { - dataset?: { + data_stream?: { properties: { - name?: { + dataset?: { type: string; value?: string; }; @@ -242,10 +244,10 @@ export async function getDataTelemetry(callCluster: LegacyAPICaller) { // Does it have `ecs.version` in the mappings? => It follows the ECS conventions '*.mappings.properties.ecs.properties.version.type', - // If `dataset.type` is a `constant_keyword`, it can be reported as a type - '*.mappings.properties.dataset.properties.type.value', - // If `dataset.name` is a `constant_keyword`, it can be reported as the dataset - '*.mappings.properties.dataset.properties.name.value', + // If `data_stream.type` is a `constant_keyword`, it can be reported as a type + '*.mappings.properties.data_stream.properties.type.value', + // If `data_stream.dataset` is a `constant_keyword`, it can be reported as the dataset + '*.mappings.properties.data_stream.properties.dataset.value', ], }), // GET /_stats/docs,store?level=indices&filter_path=indices.*.total @@ -265,8 +267,10 @@ export async function getDataTelemetry(callCluster: LegacyAPICaller) { shipper: indexMappings[name]?.mappings?._meta?.beat, packageName: indexMappings[name]?.mappings?._meta?.package?.name, managedBy: indexMappings[name]?.mappings?._meta?.managed_by, - datasetName: indexMappings[name]?.mappings?.properties.dataset?.properties.name?.value, - datasetType: indexMappings[name]?.mappings?.properties.dataset?.properties.type?.value, + dataStreamDataset: + indexMappings[name]?.mappings?.properties.data_stream?.properties.dataset?.value, + dataStreamType: + indexMappings[name]?.mappings?.properties.data_stream?.properties.type?.value, }; const stats = (indexStats?.indices || {})[name]; From f495b7def59bdf88d013551b0d7a504167736b51 Mon Sep 17 00:00:00 2001 From: Devon Thomson Date: Mon, 24 Aug 2020 12:42:11 -0400 Subject: [PATCH 12/71] Updated and unskipped lens breadcrumb test after #74523 (#75714) Co-authored-by: Elastic Machine --- x-pack/plugins/lens/public/app_plugin/app.test.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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 892058d82a80f..2b979f064b8eb 100644 --- a/x-pack/plugins/lens/public/app_plugin/app.test.tsx +++ b/x-pack/plugins/lens/public/app_plugin/app.test.tsx @@ -300,7 +300,7 @@ describe('Lens App', () => { ]); }); - it.skip('sets originatingApp breadcrumb when the document title changes', async () => { + it('sets originatingApp breadcrumb when the document title changes', async () => { const defaultArgs = makeDefaultArgs(); defaultArgs.originatingApp = 'ultraCoolDashboard'; defaultArgs.getAppNameFromId = () => 'The Coolest Container Ever Made'; @@ -315,11 +315,11 @@ describe('Lens App', () => { (defaultArgs.docStorage.load as jest.Mock).mockResolvedValue({ id: '1234', title: 'Daaaaaaadaumching!', - expression: 'valid expression', state: { query: 'fake query', - datasourceMetaData: { filterableIndexPatterns: [{ id: '1', title: 'saved' }] }, + filters: [], }, + references: [], }); await act(async () => { instance.setProps({ docId: '1234' }); From c5870589af97658cd48cccca247d9d68f9a6cf97 Mon Sep 17 00:00:00 2001 From: Josh Dover Date: Mon, 24 Aug 2020 11:41:47 -0600 Subject: [PATCH 13/71] Expose overall status to plugins (#75503) Co-authored-by: Elastic Machine --- ...a-plugin-core-server.statusservicesetup.md | 1 + ...core-server.statusservicesetup.overall_.md | 20 +++++++++++++++++++ src/core/server/legacy/legacy_service.ts | 1 + src/core/server/plugins/plugin_context.ts | 1 + src/core/server/server.api.md | 1 + src/core/server/status/status_service.mock.ts | 1 + src/core/server/status/types.ts | 15 ++++++++++---- 7 files changed, 36 insertions(+), 4 deletions(-) create mode 100644 docs/development/core/server/kibana-plugin-core-server.statusservicesetup.overall_.md diff --git a/docs/development/core/server/kibana-plugin-core-server.statusservicesetup.md b/docs/development/core/server/kibana-plugin-core-server.statusservicesetup.md index 0551a217520ad..3d3b73ccda25f 100644 --- a/docs/development/core/server/kibana-plugin-core-server.statusservicesetup.md +++ b/docs/development/core/server/kibana-plugin-core-server.statusservicesetup.md @@ -17,4 +17,5 @@ export interface StatusServiceSetup | Property | Type | Description | | --- | --- | --- | | [core$](./kibana-plugin-core-server.statusservicesetup.core_.md) | Observable<CoreStatus> | Current status for all Core services. | +| [overall$](./kibana-plugin-core-server.statusservicesetup.overall_.md) | Observable<ServiceStatus> | Overall system status for all of Kibana. | diff --git a/docs/development/core/server/kibana-plugin-core-server.statusservicesetup.overall_.md b/docs/development/core/server/kibana-plugin-core-server.statusservicesetup.overall_.md new file mode 100644 index 0000000000000..bb7c31311d520 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.statusservicesetup.overall_.md @@ -0,0 +1,20 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [StatusServiceSetup](./kibana-plugin-core-server.statusservicesetup.md) > [overall$](./kibana-plugin-core-server.statusservicesetup.overall_.md) + +## StatusServiceSetup.overall$ property + +Overall system status for all of Kibana. + +Signature: + +```typescript +overall$: Observable; +``` + +## Remarks + +The level of the overall status will reflect the most severe status of any core service or plugin. + +Exposed only for reporting purposes to outside systems and should not be used by plugins. Instead, plugins should only depend on the statuses of [Core](./kibana-plugin-core-server.statusservicesetup.core_.md) or their dependencies. + diff --git a/src/core/server/legacy/legacy_service.ts b/src/core/server/legacy/legacy_service.ts index 0c1e8562a1deb..f39282a6f9cb0 100644 --- a/src/core/server/legacy/legacy_service.ts +++ b/src/core/server/legacy/legacy_service.ts @@ -322,6 +322,7 @@ export class LegacyService implements CoreService { }, status: { core$: setupDeps.core.status.core$, + overall$: setupDeps.core.status.overall$, }, uiSettings: { register: setupDeps.core.uiSettings.register, diff --git a/src/core/server/plugins/plugin_context.ts b/src/core/server/plugins/plugin_context.ts index 5235f3ee6d580..62058f6d478e7 100644 --- a/src/core/server/plugins/plugin_context.ts +++ b/src/core/server/plugins/plugin_context.ts @@ -178,6 +178,7 @@ export function createPluginSetupContext( }, status: { core$: deps.status.core$, + overall$: deps.status.overall$, }, uiSettings: { register: deps.uiSettings.register, diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index afc71d39d4a62..cd7f4973f886c 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -2802,6 +2802,7 @@ export type StartServicesAccessor; + overall$: Observable; } // @public diff --git a/src/core/server/status/status_service.mock.ts b/src/core/server/status/status_service.mock.ts index c6eb11be6967c..47ef8659b4079 100644 --- a/src/core/server/status/status_service.mock.ts +++ b/src/core/server/status/status_service.mock.ts @@ -39,6 +39,7 @@ const availableCoreStatus: CoreStatus = { const createSetupContractMock = () => { const setupContract: jest.Mocked = { core$: new BehaviorSubject(availableCoreStatus), + overall$: new BehaviorSubject(available), }; return setupContract; diff --git a/src/core/server/status/types.ts b/src/core/server/status/types.ts index b04c25a1eee93..2ecf11deb2960 100644 --- a/src/core/server/status/types.ts +++ b/src/core/server/status/types.ts @@ -123,13 +123,20 @@ export interface StatusServiceSetup { * Current status for all Core services. */ core$: Observable; -} -/** @internal */ -export interface InternalStatusServiceSetup extends StatusServiceSetup { /** - * Overall system status used for HTTP API + * Overall system status for all of Kibana. + * + * @remarks + * The level of the overall status will reflect the most severe status of any core service or plugin. + * + * Exposed only for reporting purposes to outside systems and should not be used by plugins. Instead, plugins should + * only depend on the statuses of {@link StatusServiceSetup.core$ | Core} or their dependencies. */ overall$: Observable; +} + +/** @internal */ +export interface InternalStatusServiceSetup extends StatusServiceSetup { isStatusPageAnonymous: () => boolean; } From 9dfcde2ceeb8abec319efe4d16534d6ecddcefe2 Mon Sep 17 00:00:00 2001 From: Frank Hassanabad Date: Mon, 24 Aug 2020 12:12:48 -0600 Subject: [PATCH 14/71] Reduces field capabilities event loop block times by scaling linearly using hashes (#75718) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary On the `security_solution` project from different customers we have been getting reports of scaling issues and excessive NodeJS event blocking times. After in-depth tracing through some of the index and field capabilities calls we identified two of the "hot paths" running through `field_capabilities` to where it is using double looped arrays rather than hashes. By switching these two hot spots out for hashes we are now able to reduce the event loop block times by an order of magnitude. Before this PR you can see event loop block times as high as: ```ts field_cap: 575.131ms ``` And after this PR you will see event loop block times drop by an order of magnitude to: ```ts field_cap: 31.783ms ``` when you're calling into indexes as large as `filebeat-*`. This number can be higher if you're concatenating several large indexes together trying to get capabilities from each one all at once. We already only call `getFieldCapabilities` with one index at a time to spread out event block times. The fix is to use a hash within two key areas within these two files: ```ts src/plugins/data/server/index_patterns/fetcher/lib/field_capabilities/field_capabilities.ts src/plugins/data/server/index_patterns/fetcher/lib/field_capabilities/field_caps_response.ts ``` This effect happens during the query of `SourceQuery`/`IndexFields` within `security_solution` but you should be able to trigger it with any application code who calls into those code paths with large index sizes such as `filebeat-*` anywhere in Kibana. An explanation of how to see the block times for before and after --- Add, `console.time('field_cap');` and `console.timeEnd('field_cap');` to where the synchronize code is for testing the optimizations of before and after. For example around lines 45 with the original code: ```ts const esFieldCaps: FieldCapsResponse = await callFieldCapsApi(callCluster, indices); console.time('field_cap'); // <--- start timer const fieldsFromFieldCapsByName = keyBy(readFieldCapsResponse(esFieldCaps), 'name'); const allFieldsUnsorted = Object.keys(fieldsFromFieldCapsByName) .filter((name) => !name.startsWith('_')) .concat(metaFields) .reduce(concatIfUniq, [] as string[]) .map((name) => defaults({}, fieldsFromFieldCapsByName[name], { name, type: 'string', searchable: false, aggregatable: false, readFromDocValues: false, }) ) .map(mergeOverrides); const sorted = sortBy(allFieldsUnsorted, 'name'); console.timeEnd('field_cap'); // <--- outputs the end timer return sorted; ``` And around lines 45 with this pull request: ```ts const esFieldCaps: FieldCapsResponse = await callFieldCapsApi(callCluster, indices); console.time('field_cap'); // <--- start timer const fieldsFromFieldCapsByName = keyBy(readFieldCapsResponse(esFieldCaps), 'name'); const allFieldsUnsorted = Object.keys(fieldsFromFieldCapsByName) .filter((name) => !name.startsWith('_')) .concat(metaFields) .reduce<{ names: string[]; hash: Record }>( (agg, value) => { // This is intentionally using a "hash" and a "push" to be highly optimized with very large indexes if (agg.hash[value] != null) { return agg; } else { agg.hash[value] = value; agg.names.push(value); return agg; } }, { names: [], hash: {} } ) .names.map((name) => defaults({}, fieldsFromFieldCapsByName[name], { name, type: 'string', searchable: false, aggregatable: false, readFromDocValues: false, }) ) .map(mergeOverrides); const sorted = sortBy(allFieldsUnsorted, 'name'); console.timeEnd('field_cap'); // <--- outputs the end timer return sorted; ``` And then reload the security solutions application web page or generically anything that is going to call filebeat-* index or another large index or you could concatenate several indexes together as well to test out the performance. For security solutions we can just visit any page such as this one below which has a filebeat-* index: ``` http://localhost:5601/app/security/timelines/ ``` Be sure to load it _twice_ for testing as NodeJS will sometimes report better numbers the second time as it does optimizations after the first time it encounters some code paths. You should begin to see numbers similar to this in the before: ```ts field_cap: 575.131ms ``` This indicates that it is blocking the event loop for around half a second before this fix. If an application adds additional indexes on-top of `filebeat`, or if it tries to execute other code after this (which we do in security solutions) then the block times will climb even higher. However, after this fix, the m^n are changed to use hashing so this only climb by some constant * n where n is your fields and for filebeat-* it will should very low around: ```ts field_cap: 31.783ms ``` ### Checklist Unit tests already present, so this shouldn't break anything 🤞 . - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios --- .../field_capabilities/field_capabilities.ts | 20 +++++++---- .../field_capabilities/field_caps_response.ts | 34 ++++++++++++++----- 2 files changed, 39 insertions(+), 15 deletions(-) diff --git a/src/plugins/data/server/index_patterns/fetcher/lib/field_capabilities/field_capabilities.ts b/src/plugins/data/server/index_patterns/fetcher/lib/field_capabilities/field_capabilities.ts index b4b86b73a5f4a..6b26c82dc95e7 100644 --- a/src/plugins/data/server/index_patterns/fetcher/lib/field_capabilities/field_capabilities.ts +++ b/src/plugins/data/server/index_patterns/fetcher/lib/field_capabilities/field_capabilities.ts @@ -25,10 +25,6 @@ import { FieldCapsResponse, readFieldCapsResponse } from './field_caps_response' import { mergeOverrides } from './overrides'; import { FieldDescriptor } from '../../index_patterns_fetcher'; -export function concatIfUniq(arr: T[], value: T) { - return arr.includes(value) ? arr : arr.concat(value); -} - /** * Get the field capabilities for field in `indices`, excluding * all internal/underscore-prefixed fields that are not in `metaFields` @@ -49,8 +45,20 @@ export async function getFieldCapabilities( const allFieldsUnsorted = Object.keys(fieldsFromFieldCapsByName) .filter((name) => !name.startsWith('_')) .concat(metaFields) - .reduce(concatIfUniq, [] as string[]) - .map((name) => + .reduce<{ names: string[]; hash: Record }>( + (agg, value) => { + // This is intentionally using a "hash" and a "push" to be highly optimized with very large indexes + if (agg.hash[value] != null) { + return agg; + } else { + agg.hash[value] = value; + agg.names.push(value); + return agg; + } + }, + { names: [], hash: {} } + ) + .names.map((name) => defaults({}, fieldsFromFieldCapsByName[name], { name, type: 'string', diff --git a/src/plugins/data/server/index_patterns/fetcher/lib/field_capabilities/field_caps_response.ts b/src/plugins/data/server/index_patterns/fetcher/lib/field_capabilities/field_caps_response.ts index cb1ec6a2ebcf3..861b92569faf2 100644 --- a/src/plugins/data/server/index_patterns/fetcher/lib/field_capabilities/field_caps_response.ts +++ b/src/plugins/data/server/index_patterns/fetcher/lib/field_capabilities/field_caps_response.ts @@ -93,8 +93,12 @@ export interface FieldCapsResponse { */ export function readFieldCapsResponse(fieldCapsResponse: FieldCapsResponse): FieldDescriptor[] { const capsByNameThenType = fieldCapsResponse.fields; - const kibanaFormattedCaps: FieldDescriptor[] = Object.keys(capsByNameThenType).map( - (fieldName) => { + + const kibanaFormattedCaps = Object.keys(capsByNameThenType).reduce<{ + array: FieldDescriptor[]; + hash: Record; + }>( + (agg, fieldName) => { const capsByType = capsByNameThenType[fieldName]; const types = Object.keys(capsByType); @@ -119,7 +123,7 @@ export function readFieldCapsResponse(fieldCapsResponse: FieldCapsResponse): Fie // ignore the conflict and carry on (my wayward son) const uniqueKibanaTypes = uniq(types.map(castEsToKbnFieldTypeName)); if (uniqueKibanaTypes.length > 1) { - return { + const field = { name: fieldName, type: 'conflict', esTypes: types, @@ -134,10 +138,14 @@ export function readFieldCapsResponse(fieldCapsResponse: FieldCapsResponse): Fie {} ), }; + // This is intentionally using a "hash" and a "push" to be highly optimized with very large indexes + agg.array.push(field); + agg.hash[fieldName] = field; + return agg; } const esType = types[0]; - return { + const field = { name: fieldName, type: castEsToKbnFieldTypeName(esType), esTypes: types, @@ -145,11 +153,19 @@ export function readFieldCapsResponse(fieldCapsResponse: FieldCapsResponse): Fie aggregatable: isAggregatable, readFromDocValues: shouldReadFieldFromDocValues(isAggregatable, esType), }; + // This is intentionally using a "hash" and a "push" to be highly optimized with very large indexes + agg.array.push(field); + agg.hash[fieldName] = field; + return agg; + }, + { + array: [], + hash: {}, } ); // Get all types of sub fields. These could be multi fields or children of nested/object types - const subFields = kibanaFormattedCaps.filter((field) => { + const subFields = kibanaFormattedCaps.array.filter((field) => { return field.name.includes('.'); }); @@ -161,9 +177,9 @@ export function readFieldCapsResponse(fieldCapsResponse: FieldCapsResponse): Fie .map((_, index, parentFieldNameParts) => { return parentFieldNameParts.slice(0, index + 1).join('.'); }); - const parentFieldCaps = parentFieldNames.map((parentFieldName) => { - return kibanaFormattedCaps.find((caps) => caps.name === parentFieldName); - }); + const parentFieldCaps = parentFieldNames.map( + (parentFieldName) => kibanaFormattedCaps.hash[parentFieldName] + ); const parentFieldCapsAscending = parentFieldCaps.reverse(); if (parentFieldCaps && parentFieldCaps.length > 0) { @@ -188,7 +204,7 @@ export function readFieldCapsResponse(fieldCapsResponse: FieldCapsResponse): Fie } }); - return kibanaFormattedCaps.filter((field) => { + return kibanaFormattedCaps.array.filter((field) => { return !['object', 'nested'].includes(field.type); }); } From d20c653bb47e5ed1c58ff28baaeaf0aaa0bb5150 Mon Sep 17 00:00:00 2001 From: gchaps <33642766+gchaps@users.noreply.github.com> Date: Mon, 24 Aug 2020 11:56:14 -0700 Subject: [PATCH 15/71] [DOCS] Adds redirect for rbac content (#75803) --- docs/developer/architecture/security/rbac.asciidoc | 6 +++--- docs/redirects.asciidoc | 5 +++++ 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/docs/developer/architecture/security/rbac.asciidoc b/docs/developer/architecture/security/rbac.asciidoc index 7b35a91ca73d0..451e833651a70 100644 --- a/docs/developer/architecture/security/rbac.asciidoc +++ b/docs/developer/architecture/security/rbac.asciidoc @@ -1,4 +1,4 @@ -[[development-security-rbac]] +[[development-rbac]] == Role-based access control Role-based access control (RBAC) in {kib} relies upon the @@ -7,7 +7,7 @@ that {es} exposes. This allows {kib} to define the privileges that {kib} wishes to grant to users, assign them to the relevant users using roles, and then authorize the user to perform a specific action. This is handled within a secured instance of the `SavedObjectsClient` and available transparently to -consumers when using `request.getSavedObjectsClient()` or +consumers when using `request.getSavedObjectsClient()` or `savedObjects.getScopedSavedObjectsClient()`. [[development-rbac-privileges]] @@ -77,7 +77,7 @@ The application is created by concatenating the prefix of `kibana-` with the val } ---------------------------------- -Roles that grant <> should be managed using the <> or the *Management -> Security -> Roles* page, not directly using the {es} {ref}/security-api.html#security-role-apis[role management API]. This role can then be assigned to users using the {es} +Roles that grant <> should be managed using the <> or the *Management -> Security -> Roles* page, not directly using the {es} {ref}/security-api.html#security-role-apis[role management API]. This role can then be assigned to users using the {es} {ref}/security-api.html#security-user-apis[user management APIs]. [[development-rbac-authorization]] diff --git a/docs/redirects.asciidoc b/docs/redirects.asciidoc index 58687d99627b6..1a20c1df582e6 100644 --- a/docs/redirects.asciidoc +++ b/docs/redirects.asciidoc @@ -90,3 +90,8 @@ Watcher error reports have been removed and replaced with Kibana's <>. + +[role="exclude",id="development-security-rbac"] +== Role-based access control + +This content has moved to the <> page. From 4e3f47ac62f3a7cc4a882204f76b547fa8c93155 Mon Sep 17 00:00:00 2001 From: Pierre Gayvallet Date: Mon, 24 Aug 2020 21:39:57 +0200 Subject: [PATCH 16/71] migrate 'core' ui settings to core (#75544) * migrate ui settings to core * add basic test on service * add unit tests * adapt buildNum schema * use any for buildNum... * move i18n keys to core prefix * translate added validation messages * using number for schema for buildNum * move state:storeInSessionStorage setting to core * remove overrides config validation * remove defaultRoute from config schema --- .../settings/accessibility.test.ts | 44 +++ .../ui_settings/settings/accessibility.ts | 40 +++ .../ui_settings/settings/date_formats.test.ts | 104 ++++++ .../ui_settings/settings/date_formats.ts | 168 ++++++++++ .../server/ui_settings/settings/index.test.ts | 44 +++ src/core/server/ui_settings/settings/index.ts | 39 +++ .../server/ui_settings/settings/misc.test.ts | 42 +++ src/core/server/ui_settings/settings/misc.ts | 42 +++ .../ui_settings/settings/navigation.test.ts | 56 ++++ .../server/ui_settings/settings/navigation.ts | 72 +++++ .../settings/notifications.test.ts | 118 +++++++ .../ui_settings/settings/notifications.ts | 120 +++++++ .../server/ui_settings/settings/state.test.ts | 43 +++ src/core/server/ui_settings/settings/state.ts | 40 +++ .../server/ui_settings/settings/theme.test.ts | 57 ++++ src/core/server/ui_settings/settings/theme.ts | 51 +++ .../server/ui_settings/ui_settings_config.ts | 15 +- .../ui_settings_service.test.mock.ts | 6 +- .../ui_settings/ui_settings_service.test.ts | 11 +- .../server/ui_settings/ui_settings_service.ts | 3 + .../kibana/server/ui_setting_defaults.js | 300 ------------------ .../translations/translations/ja-JP.json | 82 ++--- .../translations/translations/zh-CN.json | 82 ++--- 23 files changed, 1181 insertions(+), 398 deletions(-) create mode 100644 src/core/server/ui_settings/settings/accessibility.test.ts create mode 100644 src/core/server/ui_settings/settings/accessibility.ts create mode 100644 src/core/server/ui_settings/settings/date_formats.test.ts create mode 100644 src/core/server/ui_settings/settings/date_formats.ts create mode 100644 src/core/server/ui_settings/settings/index.test.ts create mode 100644 src/core/server/ui_settings/settings/index.ts create mode 100644 src/core/server/ui_settings/settings/misc.test.ts create mode 100644 src/core/server/ui_settings/settings/misc.ts create mode 100644 src/core/server/ui_settings/settings/navigation.test.ts create mode 100644 src/core/server/ui_settings/settings/navigation.ts create mode 100644 src/core/server/ui_settings/settings/notifications.test.ts create mode 100644 src/core/server/ui_settings/settings/notifications.ts create mode 100644 src/core/server/ui_settings/settings/state.test.ts create mode 100644 src/core/server/ui_settings/settings/state.ts create mode 100644 src/core/server/ui_settings/settings/theme.test.ts create mode 100644 src/core/server/ui_settings/settings/theme.ts diff --git a/src/core/server/ui_settings/settings/accessibility.test.ts b/src/core/server/ui_settings/settings/accessibility.test.ts new file mode 100644 index 0000000000000..8d8f9d00fadaa --- /dev/null +++ b/src/core/server/ui_settings/settings/accessibility.test.ts @@ -0,0 +1,44 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { UiSettingsParams } from '../../../types'; +import { getAccessibilitySettings } from './accessibility'; + +describe('accessibility settings', () => { + const accessibilitySettings = getAccessibilitySettings(); + + const getValidationFn = (setting: UiSettingsParams) => (value: any) => + setting.schema.validate(value); + + describe('accessibility:disableAnimations', () => { + const validate = getValidationFn(accessibilitySettings['accessibility:disableAnimations']); + + it('should only accept boolean', () => { + expect(() => validate(true)).not.toThrow(); + expect(() => validate(false)).not.toThrow(); + + expect(() => validate(42)).toThrowErrorMatchingInlineSnapshot( + `"expected value of type [boolean] but got [number]"` + ); + expect(() => validate('foo')).toThrowErrorMatchingInlineSnapshot( + `"expected value of type [boolean] but got [string]"` + ); + }); + }); +}); diff --git a/src/core/server/ui_settings/settings/accessibility.ts b/src/core/server/ui_settings/settings/accessibility.ts new file mode 100644 index 0000000000000..ddf3e53d91189 --- /dev/null +++ b/src/core/server/ui_settings/settings/accessibility.ts @@ -0,0 +1,40 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { schema } from '@kbn/config-schema'; +import { i18n } from '@kbn/i18n'; +import { UiSettingsParams } from '../../../types'; + +export const getAccessibilitySettings = (): Record => { + return { + 'accessibility:disableAnimations': { + name: i18n.translate('core.ui_settings.params.disableAnimationsTitle', { + defaultMessage: 'Disable Animations', + }), + value: false, + description: i18n.translate('core.ui_settings.params.disableAnimationsText', { + defaultMessage: + 'Turn off all unnecessary animations in the Kibana UI. Refresh the page to apply the changes.', + }), + category: ['accessibility'], + requiresPageReload: true, + schema: schema.boolean(), + }, + }; +}; diff --git a/src/core/server/ui_settings/settings/date_formats.test.ts b/src/core/server/ui_settings/settings/date_formats.test.ts new file mode 100644 index 0000000000000..3c179af0b1d09 --- /dev/null +++ b/src/core/server/ui_settings/settings/date_formats.test.ts @@ -0,0 +1,104 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import moment from 'moment-timezone'; +import { UiSettingsParams } from '../../../types'; +import { getDateFormatSettings } from './date_formats'; + +describe('accessibility settings', () => { + const dateFormatSettings = getDateFormatSettings(); + + const getValidationFn = (setting: UiSettingsParams) => (value: any) => + setting.schema.validate(value); + + describe('dateFormat', () => { + const validate = getValidationFn(dateFormatSettings.dateFormat); + + it('should only accept string values', () => { + expect(() => validate('some format')).not.toThrow(); + + expect(() => validate(42)).toThrowErrorMatchingInlineSnapshot( + `"expected value of type [string] but got [number]"` + ); + expect(() => validate(true)).toThrowErrorMatchingInlineSnapshot( + `"expected value of type [string] but got [boolean]"` + ); + }); + }); + + describe('dateFormat:tz', () => { + const validate = getValidationFn(dateFormatSettings['dateFormat:tz']); + + it('should only accept valid timezones or `Browser`', () => { + expect(() => validate('Browser')).not.toThrow(); + expect(() => validate('UTC')).not.toThrow(); + + expect(() => validate('EST')).toThrowErrorMatchingInlineSnapshot(`"Invalid timezone: EST"`); + expect(() => validate('random string')).toThrowErrorMatchingInlineSnapshot( + `"Invalid timezone: random string"` + ); + }); + }); + + describe('dateFormat:scaled', () => { + const validate = getValidationFn(dateFormatSettings['dateFormat:scaled']); + + it('should only accept string values', () => { + expect(() => validate('some format')).not.toThrow(); + + expect(() => validate(42)).toThrowErrorMatchingInlineSnapshot( + `"expected value of type [string] but got [number]"` + ); + expect(() => validate(true)).toThrowErrorMatchingInlineSnapshot( + `"expected value of type [string] but got [boolean]"` + ); + }); + }); + + describe('dateFormat:dow', () => { + const [validDay] = moment.weekdays(); + const validate = getValidationFn(dateFormatSettings['dateFormat:dow']); + + it('should only accept DOW values', () => { + expect(() => validate(validDay)).not.toThrow(); + + expect(() => validate('invalid value')).toThrowErrorMatchingInlineSnapshot( + `"Invalid day of week: invalid value"` + ); + expect(() => validate(true)).toThrowErrorMatchingInlineSnapshot( + `"expected value of type [string] but got [boolean]"` + ); + }); + }); + + describe('dateNanosFormat', () => { + const validate = getValidationFn(dateFormatSettings.dateNanosFormat); + + it('should only accept string values', () => { + expect(() => validate('some format')).not.toThrow(); + + expect(() => validate(42)).toThrowErrorMatchingInlineSnapshot( + `"expected value of type [string] but got [number]"` + ); + expect(() => validate(true)).toThrowErrorMatchingInlineSnapshot( + `"expected value of type [string] but got [boolean]"` + ); + }); + }); +}); diff --git a/src/core/server/ui_settings/settings/date_formats.ts b/src/core/server/ui_settings/settings/date_formats.ts new file mode 100644 index 0000000000000..22351d36ac4bd --- /dev/null +++ b/src/core/server/ui_settings/settings/date_formats.ts @@ -0,0 +1,168 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import moment from 'moment-timezone'; +import { schema } from '@kbn/config-schema'; +import { i18n } from '@kbn/i18n'; +import { UiSettingsParams } from '../../../types'; + +export const getDateFormatSettings = (): Record => { + const weekdays = moment.weekdays().slice(); + const [defaultWeekday] = weekdays; + + const timezones = [ + 'Browser', + ...moment.tz + .names() + // We need to filter out some time zones, that moment.js knows about, but Elasticsearch + // does not understand and would fail thus with a 400 bad request when using them. + .filter((tz) => !['America/Nuuk', 'EST', 'HST', 'ROC', 'MST'].includes(tz)), + ]; + + return { + dateFormat: { + name: i18n.translate('core.ui_settings.params.dateFormatTitle', { + defaultMessage: 'Date format', + }), + value: 'MMM D, YYYY @ HH:mm:ss.SSS', + description: i18n.translate('core.ui_settings.params.dateFormatText', { + defaultMessage: 'When displaying a pretty formatted date, use this {formatLink}', + description: + 'Part of composite text: core.ui_settings.params.dateFormatText + ' + + 'core.ui_settings.params.dateFormat.optionsLinkText', + values: { + formatLink: + '' + + i18n.translate('core.ui_settings.params.dateFormat.optionsLinkText', { + defaultMessage: 'format', + }) + + '', + }, + }), + schema: schema.string(), + }, + 'dateFormat:tz': { + name: i18n.translate('core.ui_settings.params.dateFormat.timezoneTitle', { + defaultMessage: 'Timezone for date formatting', + }), + value: 'Browser', + description: i18n.translate('core.ui_settings.params.dateFormat.timezoneText', { + defaultMessage: + 'Which timezone should be used. {defaultOption} will use the timezone detected by your browser.', + values: { + defaultOption: '"Browser"', + }, + }), + type: 'select', + options: timezones, + requiresPageReload: true, + schema: schema.string({ + validate: (value) => { + if (!timezones.includes(value)) { + return i18n.translate( + 'core.ui_settings.params.dateFormat.timezone.invalidValidationMessage', + { + defaultMessage: 'Invalid timezone: {timezone}', + values: { + timezone: value, + }, + } + ); + } + }, + }), + }, + 'dateFormat:scaled': { + name: i18n.translate('core.ui_settings.params.dateFormat.scaledTitle', { + defaultMessage: 'Scaled date format', + }), + type: 'json', + value: `[ + ["", "HH:mm:ss.SSS"], + ["PT1S", "HH:mm:ss"], + ["PT1M", "HH:mm"], + ["PT1H", "YYYY-MM-DD HH:mm"], + ["P1DT", "YYYY-MM-DD"], + ["P1YT", "YYYY"] +]`, + description: i18n.translate('core.ui_settings.params.dateFormat.scaledText', { + defaultMessage: + 'Values that define the format used in situations where time-based ' + + 'data is rendered in order, and formatted timestamps should adapt to the ' + + 'interval between measurements. Keys are {intervalsLink}.', + description: + 'Part of composite text: core.ui_settings.params.dateFormat.scaledText + ' + + 'core.ui_settings.params.dateFormat.scaled.intervalsLinkText', + values: { + intervalsLink: + '' + + i18n.translate('core.ui_settings.params.dateFormat.scaled.intervalsLinkText', { + defaultMessage: 'ISO8601 intervals', + }) + + '', + }, + }), + schema: schema.string(), + }, + 'dateFormat:dow': { + name: i18n.translate('core.ui_settings.params.dateFormat.dayOfWeekTitle', { + defaultMessage: 'Day of week', + }), + value: defaultWeekday, + description: i18n.translate('core.ui_settings.params.dateFormat.dayOfWeekText', { + defaultMessage: 'What day should weeks start on?', + }), + type: 'select', + options: weekdays, + schema: schema.string({ + validate: (value) => { + if (!weekdays.includes(value)) { + return i18n.translate( + 'core.ui_settings.params.dayOfWeekText.invalidValidationMessage', + { + defaultMessage: 'Invalid day of week: {dayOfWeek}', + values: { + dayOfWeek: value, + }, + } + ); + } + }, + }), + }, + dateNanosFormat: { + name: i18n.translate('core.ui_settings.params.dateNanosFormatTitle', { + defaultMessage: 'Date with nanoseconds format', + }), + value: 'MMM D, YYYY @ HH:mm:ss.SSSSSSSSS', + description: i18n.translate('core.ui_settings.params.dateNanosFormatText', { + defaultMessage: 'Used for the {dateNanosLink} datatype of Elasticsearch', + values: { + dateNanosLink: + '' + + i18n.translate('core.ui_settings.params.dateNanosLinkTitle', { + defaultMessage: 'date_nanos', + }) + + '', + }, + }), + schema: schema.string(), + }, + }; +}; diff --git a/src/core/server/ui_settings/settings/index.test.ts b/src/core/server/ui_settings/settings/index.test.ts new file mode 100644 index 0000000000000..e234160fbb4a1 --- /dev/null +++ b/src/core/server/ui_settings/settings/index.test.ts @@ -0,0 +1,44 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { getAccessibilitySettings } from './accessibility'; +import { getDateFormatSettings } from './date_formats'; +import { getMiscUiSettings } from './misc'; +import { getNavigationSettings } from './navigation'; +import { getNotificationsSettings } from './notifications'; +import { getThemeSettings } from './theme'; +import { getCoreSettings } from './index'; +import { getStateSettings } from './state'; + +describe('getCoreSettings', () => { + it('should not have setting overlaps', () => { + const coreSettingsLength = Object.keys(getCoreSettings()).length; + const summedLength = [ + getAccessibilitySettings(), + getDateFormatSettings(), + getMiscUiSettings(), + getNavigationSettings(), + getNotificationsSettings(), + getThemeSettings(), + getStateSettings(), + ].reduce((sum, settings) => sum + Object.keys(settings).length, 0); + + expect(coreSettingsLength).toBe(summedLength); + }); +}); diff --git a/src/core/server/ui_settings/settings/index.ts b/src/core/server/ui_settings/settings/index.ts new file mode 100644 index 0000000000000..88baf7cd22eed --- /dev/null +++ b/src/core/server/ui_settings/settings/index.ts @@ -0,0 +1,39 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { UiSettingsParams } from '../../../types'; +import { getAccessibilitySettings } from './accessibility'; +import { getDateFormatSettings } from './date_formats'; +import { getMiscUiSettings } from './misc'; +import { getNavigationSettings } from './navigation'; +import { getNotificationsSettings } from './notifications'; +import { getThemeSettings } from './theme'; +import { getStateSettings } from './state'; + +export const getCoreSettings = (): Record => { + return { + ...getAccessibilitySettings(), + ...getDateFormatSettings(), + ...getMiscUiSettings(), + ...getNavigationSettings(), + ...getNotificationsSettings(), + ...getThemeSettings(), + ...getStateSettings(), + }; +}; diff --git a/src/core/server/ui_settings/settings/misc.test.ts b/src/core/server/ui_settings/settings/misc.test.ts new file mode 100644 index 0000000000000..db2c039d9b42c --- /dev/null +++ b/src/core/server/ui_settings/settings/misc.test.ts @@ -0,0 +1,42 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { UiSettingsParams } from '../../../types'; +import { getMiscUiSettings } from './misc'; + +describe('misc settings', () => { + const miscSettings = getMiscUiSettings(); + + const getValidationFn = (setting: UiSettingsParams) => (value: any) => + setting.schema.validate(value); + + describe('truncate:maxHeight', () => { + const validate = getValidationFn(miscSettings['truncate:maxHeight']); + + it('should only accept positive numeric values', () => { + expect(() => validate(127)).not.toThrow(); + expect(() => validate(-12)).toThrowErrorMatchingInlineSnapshot( + `"Value must be equal to or greater than [0]."` + ); + expect(() => validate('foo')).toThrowErrorMatchingInlineSnapshot( + `"expected value of type [number] but got [string]"` + ); + }); + }); +}); diff --git a/src/core/server/ui_settings/settings/misc.ts b/src/core/server/ui_settings/settings/misc.ts new file mode 100644 index 0000000000000..d158b07839c65 --- /dev/null +++ b/src/core/server/ui_settings/settings/misc.ts @@ -0,0 +1,42 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { i18n } from '@kbn/i18n'; +import { schema } from '@kbn/config-schema'; +import { UiSettingsParams } from '../types'; + +export const getMiscUiSettings = (): Record => { + return { + 'truncate:maxHeight': { + name: i18n.translate('core.ui_settings.params.maxCellHeightTitle', { + defaultMessage: 'Maximum table cell height', + }), + value: 115, + description: i18n.translate('core.ui_settings.params.maxCellHeightText', { + defaultMessage: + 'The maximum height that a cell in a table should occupy. Set to 0 to disable truncation', + }), + schema: schema.number({ min: 0 }), + }, + buildNum: { + readonly: true, + schema: schema.maybe(schema.number()), + }, + }; +}; diff --git a/src/core/server/ui_settings/settings/navigation.test.ts b/src/core/server/ui_settings/settings/navigation.test.ts new file mode 100644 index 0000000000000..40cd0e1724683 --- /dev/null +++ b/src/core/server/ui_settings/settings/navigation.test.ts @@ -0,0 +1,56 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { UiSettingsParams } from '../../../types'; +import { getNavigationSettings } from './navigation'; + +describe('navigation settings', () => { + const navigationSettings = getNavigationSettings(); + + const getValidationFn = (setting: UiSettingsParams) => (value: any) => + setting.schema.validate(value); + + describe('defaultRoute', () => { + const validate = getValidationFn(navigationSettings.defaultRoute); + + it('should only accept relative urls', () => { + expect(() => validate('/some-url')).not.toThrow(); + expect(() => validate('http://some-url')).toThrowErrorMatchingInlineSnapshot( + `"Must be a relative URL."` + ); + expect(() => validate(125)).toThrowErrorMatchingInlineSnapshot( + `"expected value of type [string] but got [number]"` + ); + }); + }); + + describe('pageNavigation', () => { + const validate = getValidationFn(navigationSettings.pageNavigation); + + it('should only accept valid values', () => { + expect(() => validate('modern')).not.toThrow(); + expect(() => validate('legacy')).not.toThrow(); + expect(() => validate('invalid')).toThrowErrorMatchingInlineSnapshot(` +"types that failed validation: +- [0]: expected value to equal [modern] +- [1]: expected value to equal [legacy]" +`); + }); + }); +}); diff --git a/src/core/server/ui_settings/settings/navigation.ts b/src/core/server/ui_settings/settings/navigation.ts new file mode 100644 index 0000000000000..6483e86a1395a --- /dev/null +++ b/src/core/server/ui_settings/settings/navigation.ts @@ -0,0 +1,72 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { schema } from '@kbn/config-schema'; +import { i18n } from '@kbn/i18n'; +import { UiSettingsParams } from '../../../types'; +import { isRelativeUrl } from '../../../utils'; + +export const getNavigationSettings = (): Record => { + return { + defaultRoute: { + name: i18n.translate('core.ui_settings.params.defaultRoute.defaultRouteTitle', { + defaultMessage: 'Default route', + }), + value: '/app/home', + schema: schema.string({ + validate(value) { + if (!value.startsWith('/') || !isRelativeUrl(value)) { + return i18n.translate( + 'core.ui_settings.params.defaultRoute.defaultRouteIsRelativeValidationMessage', + { + defaultMessage: 'Must be a relative URL.', + } + ); + } + }, + }), + description: i18n.translate('core.ui_settings.params.defaultRoute.defaultRouteText', { + defaultMessage: + 'This setting specifies the default route when opening Kibana. ' + + 'You can use this setting to modify the landing page when opening Kibana. ' + + 'The route must be a relative URL.', + }), + }, + pageNavigation: { + name: i18n.translate('core.ui_settings.params.pageNavigationName', { + defaultMessage: 'Side nav style', + }), + value: 'modern', + description: i18n.translate('core.ui_settings.params.pageNavigationDesc', { + defaultMessage: 'Change the style of navigation', + }), + type: 'select', + options: ['modern', 'legacy'], + optionLabels: { + modern: i18n.translate('core.ui_settings.params.pageNavigationModern', { + defaultMessage: 'Modern', + }), + legacy: i18n.translate('core.ui_settings.params.pageNavigationLegacy', { + defaultMessage: 'Legacy', + }), + }, + schema: schema.oneOf([schema.literal('modern'), schema.literal('legacy')]), + }, + }; +}; diff --git a/src/core/server/ui_settings/settings/notifications.test.ts b/src/core/server/ui_settings/settings/notifications.test.ts new file mode 100644 index 0000000000000..e1bdf63c7e0d5 --- /dev/null +++ b/src/core/server/ui_settings/settings/notifications.test.ts @@ -0,0 +1,118 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { UiSettingsParams } from '../../../types'; +import { getNotificationsSettings } from './notifications'; + +describe('notifications settings', () => { + const notificationsSettings = getNotificationsSettings(); + + const getValidationFn = (setting: UiSettingsParams) => (value: any) => + setting.schema.validate(value); + + describe('notifications:banner', () => { + const validate = getValidationFn(notificationsSettings['notifications:banner']); + + it('should only accept string values', () => { + expect(() => validate('some text')).not.toThrow(); + expect(() => validate(true)).toThrowErrorMatchingInlineSnapshot( + `"expected value of type [string] but got [boolean]"` + ); + expect(() => validate(12)).toThrowErrorMatchingInlineSnapshot( + `"expected value of type [string] but got [number]"` + ); + }); + }); + + describe('notifications:lifetime:banner', () => { + const validate = getValidationFn(notificationsSettings['notifications:lifetime:banner']); + + it('should only accept positive numeric values or `Infinity`', () => { + expect(() => validate(42)).not.toThrow(); + expect(() => validate('Infinity')).not.toThrow(); + expect(() => validate(-12)).toThrowErrorMatchingInlineSnapshot(` +"types that failed validation: +- [0]: Value must be equal to or greater than [0]. +- [1]: expected value to equal [Infinity]" +`); + expect(() => validate('foo')).toThrowErrorMatchingInlineSnapshot(` +"types that failed validation: +- [0]: expected value of type [number] but got [string] +- [1]: expected value to equal [Infinity]" +`); + }); + }); + + describe('notifications:lifetime:error', () => { + const validate = getValidationFn(notificationsSettings['notifications:lifetime:error']); + + it('should only accept positive numeric values or `Infinity`', () => { + expect(() => validate(42)).not.toThrow(); + expect(() => validate('Infinity')).not.toThrow(); + expect(() => validate(-12)).toThrowErrorMatchingInlineSnapshot(` +"types that failed validation: +- [0]: Value must be equal to or greater than [0]. +- [1]: expected value to equal [Infinity]" +`); + expect(() => validate('foo')).toThrowErrorMatchingInlineSnapshot(` +"types that failed validation: +- [0]: expected value of type [number] but got [string] +- [1]: expected value to equal [Infinity]" +`); + }); + }); + + describe('notifications:lifetime:warning', () => { + const validate = getValidationFn(notificationsSettings['notifications:lifetime:warning']); + + it('should only accept positive numeric values or `Infinity`', () => { + expect(() => validate(42)).not.toThrow(); + expect(() => validate('Infinity')).not.toThrow(); + expect(() => validate(-12)).toThrowErrorMatchingInlineSnapshot(` +"types that failed validation: +- [0]: Value must be equal to or greater than [0]. +- [1]: expected value to equal [Infinity]" +`); + expect(() => validate('foo')).toThrowErrorMatchingInlineSnapshot(` +"types that failed validation: +- [0]: expected value of type [number] but got [string] +- [1]: expected value to equal [Infinity]" +`); + }); + }); + + describe('notifications:lifetime:info', () => { + const validate = getValidationFn(notificationsSettings['notifications:lifetime:info']); + + it('should only accept positive numeric values or `Infinity`', () => { + expect(() => validate(42)).not.toThrow(); + expect(() => validate('Infinity')).not.toThrow(); + expect(() => validate(-12)).toThrowErrorMatchingInlineSnapshot(` +"types that failed validation: +- [0]: Value must be equal to or greater than [0]. +- [1]: expected value to equal [Infinity]" +`); + expect(() => validate('foo')).toThrowErrorMatchingInlineSnapshot(` +"types that failed validation: +- [0]: expected value of type [number] but got [string] +- [1]: expected value to equal [Infinity]" +`); + }); + }); +}); diff --git a/src/core/server/ui_settings/settings/notifications.ts b/src/core/server/ui_settings/settings/notifications.ts new file mode 100644 index 0000000000000..7d9e70dc90364 --- /dev/null +++ b/src/core/server/ui_settings/settings/notifications.ts @@ -0,0 +1,120 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { schema } from '@kbn/config-schema'; +import { i18n } from '@kbn/i18n'; +import { UiSettingsParams } from '../../../types'; + +export const getNotificationsSettings = (): Record => { + return { + 'notifications:banner': { + name: i18n.translate('core.ui_settings.params.notifications.bannerTitle', { + defaultMessage: 'Custom banner notification', + }), + value: '', + type: 'markdown', + description: i18n.translate('core.ui_settings.params.notifications.bannerText', { + defaultMessage: + 'A custom banner intended for temporary notices to all users. {markdownLink}.', + description: + 'Part of composite text: core.ui_settings.params.notifications.bannerText + ' + + 'core.ui_settings.params.notifications.banner.markdownLinkText', + values: { + markdownLink: + `` + + i18n.translate('core.ui_settings.params.notifications.banner.markdownLinkText', { + defaultMessage: 'Markdown supported', + }) + + '', + }, + }), + category: ['notifications'], + schema: schema.string(), + }, + 'notifications:lifetime:banner': { + name: i18n.translate('core.ui_settings.params.notifications.bannerLifetimeTitle', { + defaultMessage: 'Banner notification lifetime', + }), + value: 3000000, + description: i18n.translate('core.ui_settings.params.notifications.bannerLifetimeText', { + defaultMessage: + 'The time in milliseconds which a banner notification will be displayed on-screen for. ' + + 'Setting to {infinityValue} will disable the countdown.', + values: { + infinityValue: 'Infinity', + }, + }), + type: 'number', + category: ['notifications'], + schema: schema.oneOf([schema.number({ min: 0 }), schema.literal('Infinity')]), + }, + 'notifications:lifetime:error': { + name: i18n.translate('core.ui_settings.params.notifications.errorLifetimeTitle', { + defaultMessage: 'Error notification lifetime', + }), + value: 300000, + description: i18n.translate('core.ui_settings.params.notifications.errorLifetimeText', { + defaultMessage: + 'The time in milliseconds which an error notification will be displayed on-screen for. ' + + 'Setting to {infinityValue} will disable.', + values: { + infinityValue: 'Infinity', + }, + }), + type: 'number', + category: ['notifications'], + schema: schema.oneOf([schema.number({ min: 0 }), schema.literal('Infinity')]), + }, + 'notifications:lifetime:warning': { + name: i18n.translate('core.ui_settings.params.notifications.warningLifetimeTitle', { + defaultMessage: 'Warning notification lifetime', + }), + value: 10000, + description: i18n.translate('core.ui_settings.params.notifications.warningLifetimeText', { + defaultMessage: + 'The time in milliseconds which a warning notification will be displayed on-screen for. ' + + 'Setting to {infinityValue} will disable.', + values: { + infinityValue: 'Infinity', + }, + }), + type: 'number', + category: ['notifications'], + schema: schema.oneOf([schema.number({ min: 0 }), schema.literal('Infinity')]), + }, + 'notifications:lifetime:info': { + name: i18n.translate('core.ui_settings.params.notifications.infoLifetimeTitle', { + defaultMessage: 'Info notification lifetime', + }), + value: 5000, + description: i18n.translate('core.ui_settings.params.notifications.infoLifetimeText', { + defaultMessage: + 'The time in milliseconds which an information notification will be displayed on-screen for. ' + + 'Setting to {infinityValue} will disable.', + values: { + infinityValue: 'Infinity', + }, + }), + type: 'number', + category: ['notifications'], + schema: schema.oneOf([schema.number({ min: 0 }), schema.literal('Infinity')]), + }, + }; +}; diff --git a/src/core/server/ui_settings/settings/state.test.ts b/src/core/server/ui_settings/settings/state.test.ts new file mode 100644 index 0000000000000..7be30abe71bb0 --- /dev/null +++ b/src/core/server/ui_settings/settings/state.test.ts @@ -0,0 +1,43 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { UiSettingsParams } from '../../../types'; +import { getStateSettings } from './state'; + +describe('state settings', () => { + const state = getStateSettings(); + + const getValidationFn = (setting: UiSettingsParams) => (value: any) => + setting.schema.validate(value); + + describe('state:storeInSessionStorage', () => { + const validate = getValidationFn(state['state:storeInSessionStorage']); + + it('should only accept boolean values', () => { + expect(() => validate(true)).not.toThrow(); + expect(() => validate(false)).not.toThrow(); + expect(() => validate('foo')).toThrowErrorMatchingInlineSnapshot( + `"expected value of type [boolean] but got [string]"` + ); + expect(() => validate(12)).toThrowErrorMatchingInlineSnapshot( + `"expected value of type [boolean] but got [number]"` + ); + }); + }); +}); diff --git a/src/core/server/ui_settings/settings/state.ts b/src/core/server/ui_settings/settings/state.ts new file mode 100644 index 0000000000000..ee85cc8442599 --- /dev/null +++ b/src/core/server/ui_settings/settings/state.ts @@ -0,0 +1,40 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { schema } from '@kbn/config-schema'; +import { i18n } from '@kbn/i18n'; +import { UiSettingsParams } from '../../../types'; + +export const getStateSettings = (): Record => { + return { + 'state:storeInSessionStorage': { + name: i18n.translate('core.ui_settings.params.storeUrlTitle', { + defaultMessage: 'Store URLs in session storage', + }), + value: false, + description: i18n.translate('core.ui_settings.params.storeUrlText', { + defaultMessage: + 'The URL can sometimes grow to be too large for some browsers to handle. ' + + 'To counter-act this we are testing if storing parts of the URL in session storage could help. ' + + 'Please let us know how it goes!', + }), + schema: schema.boolean(), + }, + }; +}; diff --git a/src/core/server/ui_settings/settings/theme.test.ts b/src/core/server/ui_settings/settings/theme.test.ts new file mode 100644 index 0000000000000..eb18bcc2dd0c7 --- /dev/null +++ b/src/core/server/ui_settings/settings/theme.test.ts @@ -0,0 +1,57 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { UiSettingsParams } from '../../../types'; +import { getThemeSettings } from './theme'; + +describe('theme settings', () => { + const themeSettings = getThemeSettings(); + + const getValidationFn = (setting: UiSettingsParams) => (value: any) => + setting.schema.validate(value); + + describe('theme:darkMode', () => { + const validate = getValidationFn(themeSettings['theme:darkMode']); + + it('should only accept boolean values', () => { + expect(() => validate(true)).not.toThrow(); + expect(() => validate(false)).not.toThrow(); + expect(() => validate('foo')).toThrowErrorMatchingInlineSnapshot( + `"expected value of type [boolean] but got [string]"` + ); + expect(() => validate(12)).toThrowErrorMatchingInlineSnapshot( + `"expected value of type [boolean] but got [number]"` + ); + }); + }); + + describe('theme:version', () => { + const validate = getValidationFn(themeSettings['theme:version']); + + it('should only accept valid values', () => { + expect(() => validate('v7')).not.toThrow(); + expect(() => validate('v8 (beta)')).not.toThrow(); + expect(() => validate('v12')).toThrowErrorMatchingInlineSnapshot(` +"types that failed validation: +- [0]: expected value to equal [v7] +- [1]: expected value to equal [v8 (beta)]" +`); + }); + }); +}); diff --git a/src/core/server/ui_settings/settings/theme.ts b/src/core/server/ui_settings/settings/theme.ts new file mode 100644 index 0000000000000..9f1857932f010 --- /dev/null +++ b/src/core/server/ui_settings/settings/theme.ts @@ -0,0 +1,51 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { schema } from '@kbn/config-schema'; +import { i18n } from '@kbn/i18n'; +import { UiSettingsParams } from '../../../types'; + +export const getThemeSettings = (): Record => { + return { + 'theme:darkMode': { + name: i18n.translate('core.ui_settings.params.darkModeTitle', { + defaultMessage: 'Dark mode', + }), + value: false, + description: i18n.translate('core.ui_settings.params.darkModeText', { + defaultMessage: `Enable a dark mode for the Kibana UI. A page refresh is required for the setting to be applied.`, + }), + requiresPageReload: true, + schema: schema.boolean(), + }, + 'theme:version': { + name: i18n.translate('core.ui_settings.params.themeVersionTitle', { + defaultMessage: 'Theme version', + }), + value: 'v7', + type: 'select', + options: ['v7', 'v8 (beta)'], + description: i18n.translate('core.ui_settings.params.themeVersionText', { + defaultMessage: `Switch between the theme used for the current and next version of Kibana. A page refresh is required for the setting to be applied.`, + }), + requiresPageReload: true, + schema: schema.oneOf([schema.literal('v7'), schema.literal('v8 (beta)')]), + }, + }; +}; diff --git a/src/core/server/ui_settings/ui_settings_config.ts b/src/core/server/ui_settings/ui_settings_config.ts index a0ac48e2dd089..3a3573a06d492 100644 --- a/src/core/server/ui_settings/ui_settings_config.ts +++ b/src/core/server/ui_settings/ui_settings_config.ts @@ -27,20 +27,7 @@ const deprecations: ConfigDeprecationProvider = ({ unused, renameFromRoot }) => ]; const configSchema = schema.object({ - overrides: schema.object( - { - defaultRoute: schema.maybe( - schema.string({ - validate(value) { - if (!value.startsWith('/')) { - return 'must start with a slash'; - } - }, - }) - ), - }, - { unknowns: 'allow' } - ), + overrides: schema.object({}, { unknowns: 'allow' }), }); export type UiSettingsConfigType = TypeOf; diff --git a/src/core/server/ui_settings/ui_settings_service.test.mock.ts b/src/core/server/ui_settings/ui_settings_service.test.mock.ts index 586ad3049ed6a..b4e98f55e159b 100644 --- a/src/core/server/ui_settings/ui_settings_service.test.mock.ts +++ b/src/core/server/ui_settings/ui_settings_service.test.mock.ts @@ -18,7 +18,11 @@ */ export const MockUiSettingsClientConstructor = jest.fn(); - jest.doMock('./ui_settings_client', () => ({ UiSettingsClient: MockUiSettingsClientConstructor, })); + +export const getCoreSettingsMock = jest.fn(); +jest.doMock('./settings', () => ({ + getCoreSettings: getCoreSettingsMock, +})); diff --git a/src/core/server/ui_settings/ui_settings_service.test.ts b/src/core/server/ui_settings/ui_settings_service.test.ts index 096ca347e6f4b..0c17a3a614d60 100644 --- a/src/core/server/ui_settings/ui_settings_service.test.ts +++ b/src/core/server/ui_settings/ui_settings_service.test.ts @@ -19,7 +19,10 @@ import { BehaviorSubject } from 'rxjs'; import { schema } from '@kbn/config-schema'; -import { MockUiSettingsClientConstructor } from './ui_settings_service.test.mock'; +import { + MockUiSettingsClientConstructor, + getCoreSettingsMock, +} from './ui_settings_service.test.mock'; import { UiSettingsService, SetupDeps } from './ui_settings_service'; import { httpServiceMock } from '../http/http_service.mock'; import { savedObjectsClientMock } from '../mocks'; @@ -58,6 +61,7 @@ describe('uiSettings', () => { afterEach(() => { MockUiSettingsClientConstructor.mockClear(); + getCoreSettingsMock.mockClear(); }); describe('#setup', () => { @@ -67,6 +71,11 @@ describe('uiSettings', () => { expect(setupDeps.savedObjects.registerType).toHaveBeenCalledWith(uiSettingsType); }); + it('calls `getCoreSettings`', async () => { + await service.setup(setupDeps); + expect(getCoreSettingsMock).toHaveBeenCalledTimes(1); + }); + describe('#register', () => { it('throws if registers the same key twice', async () => { const setup = await service.setup(setupDeps); diff --git a/src/core/server/ui_settings/ui_settings_service.ts b/src/core/server/ui_settings/ui_settings_service.ts index 93593b29221da..8598cf7a62287 100644 --- a/src/core/server/ui_settings/ui_settings_service.ts +++ b/src/core/server/ui_settings/ui_settings_service.ts @@ -36,6 +36,7 @@ import { import { mapToObject } from '../../utils/'; import { uiSettingsType } from './saved_objects'; import { registerRoutes } from './routes'; +import { getCoreSettings } from './settings'; export interface SetupDeps { http: InternalHttpServiceSetup; @@ -60,6 +61,8 @@ export class UiSettingsService savedObjects.registerType(uiSettingsType); registerRoutes(http.createRouter('')); + this.register(getCoreSettings()); + const config = await this.config$.pipe(first()).toPromise(); this.overrides = config.overrides; diff --git a/src/legacy/core_plugins/kibana/server/ui_setting_defaults.js b/src/legacy/core_plugins/kibana/server/ui_setting_defaults.js index 625c2c02510db..2562657a71624 100644 --- a/src/legacy/core_plugins/kibana/server/ui_setting_defaults.js +++ b/src/legacy/core_plugins/kibana/server/ui_setting_defaults.js @@ -17,159 +17,11 @@ * under the License. */ -import moment from 'moment-timezone'; import { i18n } from '@kbn/i18n'; -import { schema } from '@kbn/config-schema'; - -import { isRelativeUrl } from '../../../../core/server'; export function getUiSettingDefaults() { - const weekdays = moment.weekdays().slice(); - const [defaultWeekday] = weekdays; - // wrapped in provider so that a new instance is given to each app/test return { - buildNum: { - readonly: true, - }, - 'state:storeInSessionStorage': { - name: i18n.translate('kbn.advancedSettings.storeUrlTitle', { - defaultMessage: 'Store URLs in session storage', - }), - value: false, - description: i18n.translate('kbn.advancedSettings.storeUrlText', { - defaultMessage: - 'The URL can sometimes grow to be too large for some browsers to handle. ' + - 'To counter-act this we are testing if storing parts of the URL in session storage could help. ' + - 'Please let us know how it goes!', - }), - }, - defaultRoute: { - name: i18n.translate('kbn.advancedSettings.defaultRoute.defaultRouteTitle', { - defaultMessage: 'Default route', - }), - value: '/app/home', - schema: schema.string({ - validate(value) { - if (!value.startsWith('/') || !isRelativeUrl(value)) { - return i18n.translate( - 'kbn.advancedSettings.defaultRoute.defaultRouteIsRelativeValidationMessage', - { - defaultMessage: 'Must be a relative URL.', - } - ); - } - }, - }), - description: i18n.translate('kbn.advancedSettings.defaultRoute.defaultRouteText', { - defaultMessage: - 'This setting specifies the default route when opening Kibana. ' + - 'You can use this setting to modify the landing page when opening Kibana. ' + - 'The route must be a relative URL.', - }), - }, - dateFormat: { - name: i18n.translate('kbn.advancedSettings.dateFormatTitle', { - defaultMessage: 'Date format', - }), - value: 'MMM D, YYYY @ HH:mm:ss.SSS', - description: i18n.translate('kbn.advancedSettings.dateFormatText', { - defaultMessage: 'When displaying a pretty formatted date, use this {formatLink}', - description: - 'Part of composite text: kbn.advancedSettings.dateFormatText + ' + - 'kbn.advancedSettings.dateFormat.optionsLinkText', - values: { - formatLink: - '' + - i18n.translate('kbn.advancedSettings.dateFormat.optionsLinkText', { - defaultMessage: 'format', - }) + - '', - }, - }), - }, - 'dateFormat:tz': { - name: i18n.translate('kbn.advancedSettings.dateFormat.timezoneTitle', { - defaultMessage: 'Timezone for date formatting', - }), - value: 'Browser', - description: i18n.translate('kbn.advancedSettings.dateFormat.timezoneText', { - defaultMessage: - 'Which timezone should be used. {defaultOption} will use the timezone detected by your browser.', - values: { - defaultOption: '"Browser"', - }, - }), - type: 'select', - options: [ - 'Browser', - ...moment.tz - .names() - // We need to filter out some time zones, that moment.js knows about, but Elasticsearch - // does not understand and would fail thus with a 400 bad request when using them. - .filter((tz) => !['America/Nuuk', 'EST', 'HST', 'ROC', 'MST'].includes(tz)), - ], - requiresPageReload: true, - }, - 'dateFormat:scaled': { - name: i18n.translate('kbn.advancedSettings.dateFormat.scaledTitle', { - defaultMessage: 'Scaled date format', - }), - type: 'json', - value: `[ - ["", "HH:mm:ss.SSS"], - ["PT1S", "HH:mm:ss"], - ["PT1M", "HH:mm"], - ["PT1H", "YYYY-MM-DD HH:mm"], - ["P1DT", "YYYY-MM-DD"], - ["P1YT", "YYYY"] -]`, - description: i18n.translate('kbn.advancedSettings.dateFormat.scaledText', { - defaultMessage: - 'Values that define the format used in situations where time-based ' + - 'data is rendered in order, and formatted timestamps should adapt to the ' + - 'interval between measurements. Keys are {intervalsLink}.', - description: - 'Part of composite text: kbn.advancedSettings.dateFormat.scaledText + ' + - 'kbn.advancedSettings.dateFormat.scaled.intervalsLinkText', - values: { - intervalsLink: - '' + - i18n.translate('kbn.advancedSettings.dateFormat.scaled.intervalsLinkText', { - defaultMessage: 'ISO8601 intervals', - }) + - '', - }, - }), - }, - 'dateFormat:dow': { - name: i18n.translate('kbn.advancedSettings.dateFormat.dayOfWeekTitle', { - defaultMessage: 'Day of week', - }), - value: defaultWeekday, - description: i18n.translate('kbn.advancedSettings.dateFormat.dayOfWeekText', { - defaultMessage: 'What day should weeks start on?', - }), - type: 'select', - options: weekdays, - }, - dateNanosFormat: { - name: i18n.translate('kbn.advancedSettings.dateNanosFormatTitle', { - defaultMessage: 'Date with nanoseconds format', - }), - value: 'MMM D, YYYY @ HH:mm:ss.SSSSSSSSS', - description: i18n.translate('kbn.advancedSettings.dateNanosFormatText', { - defaultMessage: 'Used for the {dateNanosLink} datatype of Elasticsearch', - values: { - dateNanosLink: - '' + - i18n.translate('kbn.advancedSettings.dateNanosLinkTitle', { - defaultMessage: 'date_nanos', - }) + - '', - }, - }), - }, 'visualization:tileMap:maxPrecision': { name: i18n.translate('kbn.advancedSettings.visualization.tileMap.maxPrecisionTitle', { defaultMessage: 'Maximum tile map precision', @@ -248,157 +100,5 @@ export function getUiSettingDefaults() { }), category: ['visualization'], }, - 'truncate:maxHeight': { - name: i18n.translate('kbn.advancedSettings.maxCellHeightTitle', { - defaultMessage: 'Maximum table cell height', - }), - value: 115, - description: i18n.translate('kbn.advancedSettings.maxCellHeightText', { - defaultMessage: - 'The maximum height that a cell in a table should occupy. Set to 0 to disable truncation', - }), - }, - 'theme:darkMode': { - name: i18n.translate('kbn.advancedSettings.darkModeTitle', { - defaultMessage: 'Dark mode', - }), - value: false, - description: i18n.translate('kbn.advancedSettings.darkModeText', { - defaultMessage: `Enable a dark mode for the Kibana UI. A page refresh is required for the setting to be applied.`, - }), - requiresPageReload: true, - }, - 'theme:version': { - name: i18n.translate('kbn.advancedSettings.themeVersionTitle', { - defaultMessage: 'Theme version', - }), - value: 'v7', - type: 'select', - options: ['v7', 'v8 (beta)'], - description: i18n.translate('kbn.advancedSettings.themeVersionText', { - defaultMessage: `Switch between the theme used for the current and next version of Kibana. A page refresh is required for the setting to be applied.`, - }), - requiresPageReload: true, - }, - 'notifications:banner': { - name: i18n.translate('kbn.advancedSettings.notifications.bannerTitle', { - defaultMessage: 'Custom banner notification', - }), - value: '', - type: 'markdown', - description: i18n.translate('kbn.advancedSettings.notifications.bannerText', { - defaultMessage: - 'A custom banner intended for temporary notices to all users. {markdownLink}.', - description: - 'Part of composite text: kbn.advancedSettings.notifications.bannerText + ' + - 'kbn.advancedSettings.notifications.banner.markdownLinkText', - values: { - markdownLink: - `` + - i18n.translate('kbn.advancedSettings.notifications.banner.markdownLinkText', { - defaultMessage: 'Markdown supported', - }) + - '', - }, - }), - category: ['notifications'], - }, - 'notifications:lifetime:banner': { - name: i18n.translate('kbn.advancedSettings.notifications.bannerLifetimeTitle', { - defaultMessage: 'Banner notification lifetime', - }), - value: 3000000, - description: i18n.translate('kbn.advancedSettings.notifications.bannerLifetimeText', { - defaultMessage: - 'The time in milliseconds which a banner notification will be displayed on-screen for. ' + - 'Setting to {infinityValue} will disable the countdown.', - values: { - infinityValue: 'Infinity', - }, - }), - type: 'number', - category: ['notifications'], - }, - 'notifications:lifetime:error': { - name: i18n.translate('kbn.advancedSettings.notifications.errorLifetimeTitle', { - defaultMessage: 'Error notification lifetime', - }), - value: 300000, - description: i18n.translate('kbn.advancedSettings.notifications.errorLifetimeText', { - defaultMessage: - 'The time in milliseconds which an error notification will be displayed on-screen for. ' + - 'Setting to {infinityValue} will disable.', - values: { - infinityValue: 'Infinity', - }, - }), - type: 'number', - category: ['notifications'], - }, - 'notifications:lifetime:warning': { - name: i18n.translate('kbn.advancedSettings.notifications.warningLifetimeTitle', { - defaultMessage: 'Warning notification lifetime', - }), - value: 10000, - description: i18n.translate('kbn.advancedSettings.notifications.warningLifetimeText', { - defaultMessage: - 'The time in milliseconds which a warning notification will be displayed on-screen for. ' + - 'Setting to {infinityValue} will disable.', - values: { - infinityValue: 'Infinity', - }, - }), - type: 'number', - category: ['notifications'], - }, - 'notifications:lifetime:info': { - name: i18n.translate('kbn.advancedSettings.notifications.infoLifetimeTitle', { - defaultMessage: 'Info notification lifetime', - }), - value: 5000, - description: i18n.translate('kbn.advancedSettings.notifications.infoLifetimeText', { - defaultMessage: - 'The time in milliseconds which an information notification will be displayed on-screen for. ' + - 'Setting to {infinityValue} will disable.', - values: { - infinityValue: 'Infinity', - }, - }), - type: 'number', - category: ['notifications'], - }, - 'accessibility:disableAnimations': { - name: i18n.translate('kbn.advancedSettings.disableAnimationsTitle', { - defaultMessage: 'Disable Animations', - }), - value: false, - description: i18n.translate('kbn.advancedSettings.disableAnimationsText', { - defaultMessage: - 'Turn off all unnecessary animations in the Kibana UI. Refresh the page to apply the changes.', - }), - category: ['accessibility'], - requiresPageReload: true, - }, - pageNavigation: { - name: i18n.translate('kbn.advancedSettings.pageNavigationName', { - defaultMessage: 'Side nav style', - }), - value: 'modern', - description: i18n.translate('kbn.advancedSettings.pageNavigationDesc', { - defaultMessage: 'Change the style of navigation', - }), - type: 'select', - options: ['modern', 'legacy'], - optionLabels: { - modern: i18n.translate('kbn.advancedSettings.pageNavigationModern', { - defaultMessage: 'Modern', - }), - legacy: i18n.translate('kbn.advancedSettings.pageNavigationLegacy', { - defaultMessage: 'Legacy', - }), - }, - schema: schema.oneOf([schema.literal('modern'), schema.literal('legacy')]), - }, }; } diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 5572fc85bf130..da5392848475b 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -526,6 +526,47 @@ "core.ui.securityNavList.label": "セキュリティ", "core.ui.welcomeErrorMessage": "Elasticが正常に読み込まれませんでした。詳細はサーバーアウトプットを確認してください。", "core.ui.welcomeMessage": "Elasticの読み込み中", + "core.ui_settings.params.darkModeText": "Kibana UI のダークモードを有効にします。この設定を適用するにはページの更新が必要です。", + "core.ui_settings.params.darkModeTitle": "ダークモード", + "core.ui_settings.params.dateFormat.dayOfWeekText": "週の初めの曜日を設定します", + "core.ui_settings.params.dateFormat.dayOfWeekTitle": "曜日", + "core.ui_settings.params.dateFormat.optionsLinkText": "フォーマット", + "core.ui_settings.params.dateFormat.scaled.intervalsLinkText": "ISO8601 間隔", + "core.ui_settings.params.dateFormat.scaledText": "時間ベースのデータが順番にレンダリングされ、フォーマットされたタイムスタンプが測定値の間隔に適応すべき状況で使用されるフォーマットを定義する値です。キーは {intervalsLink}。", + "core.ui_settings.params.dateFormat.scaledTitle": "スケーリングされたデータフォーマットです", + "core.ui_settings.params.dateFormat.timezoneText": "使用されるタイムゾーンです。{defaultOption} ではご使用のブラウザにより検知されたタイムゾーンが使用されます。", + "core.ui_settings.params.dateFormat.timezoneTitle": "データフォーマットのタイムゾーン", + "core.ui_settings.params.dateFormatText": "きちんとフォーマットされたデータを表示する際、この {formatLink} を使用します", + "core.ui_settings.params.dateFormatTitle": "データフォーマット", + "core.ui_settings.params.dateNanosFormatText": "Elasticsearch の {dateNanosLink} データタイプに使用されます", + "core.ui_settings.params.dateNanosFormatTitle": "ナノ秒フォーマットでの日付", + "core.ui_settings.params.dateNanosLinkTitle": "date_nanos", + "core.ui_settings.params.defaultRoute.defaultRouteIsRelativeValidationMessage": "相対 URL でなければなりません。", + "core.ui_settings.params.defaultRoute.defaultRouteText": "この設定は、Kibana 起動時のデフォルトのルートを設定します。この設定で、Kibana 起動時のランディングページを変更できます。経路は相対 URL でなければなりません。", + "core.ui_settings.params.defaultRoute.defaultRouteTitle": "デフォルトのルート", + "core.ui_settings.params.disableAnimationsText": "Kibana UI の不要なアニメーションをオフにします。変更を適用するにはページを更新してください。", + "core.ui_settings.params.disableAnimationsTitle": "アニメーションを無効にする", + "core.ui_settings.params.maxCellHeightText": "表のセルが使用する高さの上限です。この切り捨てを無効にするには 0 に設定します", + "core.ui_settings.params.maxCellHeightTitle": "表のセルの高さの上限", + "core.ui_settings.params.notifications.banner.markdownLinkText": "マークダウン対応", + "core.ui_settings.params.notifications.bannerLifetimeText": "バナー通知が画面に表示されるミリ秒単位での時間です。{infinityValue} に設定するとカウントダウンが無効になります。", + "core.ui_settings.params.notifications.bannerLifetimeTitle": "バナー通知時間", + "core.ui_settings.params.notifications.bannerText": "すべてのユーザーへの一時的な通知を目的としたカスタムバナーです。{markdownLink}", + "core.ui_settings.params.notifications.bannerTitle": "カスタムバナー通知", + "core.ui_settings.params.notifications.errorLifetimeText": "エラー通知が画面に表示されるミリ秒単位での時間です。{infinityValue} に設定すると無効になります。", + "core.ui_settings.params.notifications.errorLifetimeTitle": "エラー通知時間", + "core.ui_settings.params.notifications.infoLifetimeText": "情報通知が画面に表示されるミリ秒単位での時間です。{infinityValue} に設定すると無効になります。", + "core.ui_settings.params.notifications.infoLifetimeTitle": "情報通知時間", + "core.ui_settings.params.notifications.warningLifetimeText": "警告通知が画面に表示されるミリ秒単位での時間です。{infinityValue} に設定すると無効になります。", + "core.ui_settings.params.notifications.warningLifetimeTitle": "警告通知時間", + "core.ui_settings.params.pageNavigationDesc": "ナビゲーションのスタイルを変更", + "core.ui_settings.params.pageNavigationLegacy": "レガシー", + "core.ui_settings.params.pageNavigationModern": "モダン", + "core.ui_settings.params.pageNavigationName": "サイドナビゲーションスタイル", + "core.ui_settings.params.themeVersionText": "現在のバージョンと次のバージョンのKibanaで使用されるテーマを切り替えます。この設定を適用するにはページの更新が必要です。", + "core.ui_settings.params.themeVersionTitle": "テーマバージョン", + "core.ui_settings.params.storeUrlText": "URL は長くなりすぎてブラウザが対応できない場合があります。セッションストレージに URL の一部を保存することがで この問題に対処できるかテストしています。結果を教えてください!", + "core.ui_settings.params.storeUrlTitle": "セッションストレージに URL を格納", "dashboard.actions.toggleExpandPanelMenuItem.expandedDisplayName": "最小化", "dashboard.actions.toggleExpandPanelMenuItem.notExpandedDisplayName": "全画面", "dashboard.addExistingVisualizationLinkText": "既存のユーザーを追加", @@ -2734,47 +2775,6 @@ "inspector.requests.statisticsTabLabel": "統計", "inspector.title": "インスペクター", "inspector.view": "{viewName} を表示", - "kbn.advancedSettings.darkModeText": "Kibana UI のダークモードを有効にします。この設定を適用するにはページの更新が必要です。", - "kbn.advancedSettings.darkModeTitle": "ダークモード", - "kbn.advancedSettings.dateFormat.dayOfWeekText": "週の初めの曜日を設定します", - "kbn.advancedSettings.dateFormat.dayOfWeekTitle": "曜日", - "kbn.advancedSettings.dateFormat.optionsLinkText": "フォーマット", - "kbn.advancedSettings.dateFormat.scaled.intervalsLinkText": "ISO8601 間隔", - "kbn.advancedSettings.dateFormat.scaledText": "時間ベースのデータが順番にレンダリングされ、フォーマットされたタイムスタンプが測定値の間隔に適応すべき状況で使用されるフォーマットを定義する値です。キーは {intervalsLink}。", - "kbn.advancedSettings.dateFormat.scaledTitle": "スケーリングされたデータフォーマットです", - "kbn.advancedSettings.dateFormat.timezoneText": "使用されるタイムゾーンです。{defaultOption} ではご使用のブラウザにより検知されたタイムゾーンが使用されます。", - "kbn.advancedSettings.dateFormat.timezoneTitle": "データフォーマットのタイムゾーン", - "kbn.advancedSettings.dateFormatText": "きちんとフォーマットされたデータを表示する際、この {formatLink} を使用します", - "kbn.advancedSettings.dateFormatTitle": "データフォーマット", - "kbn.advancedSettings.dateNanosFormatText": "Elasticsearch の {dateNanosLink} データタイプに使用されます", - "kbn.advancedSettings.dateNanosFormatTitle": "ナノ秒フォーマットでの日付", - "kbn.advancedSettings.dateNanosLinkTitle": "date_nanos", - "kbn.advancedSettings.defaultRoute.defaultRouteIsRelativeValidationMessage": "相対 URL でなければなりません。", - "kbn.advancedSettings.defaultRoute.defaultRouteText": "この設定は、Kibana 起動時のデフォルトのルートを設定します。この設定で、Kibana 起動時のランディングページを変更できます。経路は相対 URL でなければなりません。", - "kbn.advancedSettings.defaultRoute.defaultRouteTitle": "デフォルトのルート", - "kbn.advancedSettings.disableAnimationsText": "Kibana UI の不要なアニメーションをオフにします。変更を適用するにはページを更新してください。", - "kbn.advancedSettings.disableAnimationsTitle": "アニメーションを無効にする", - "kbn.advancedSettings.maxCellHeightText": "表のセルが使用する高さの上限です。この切り捨てを無効にするには 0 に設定します", - "kbn.advancedSettings.maxCellHeightTitle": "表のセルの高さの上限", - "kbn.advancedSettings.notifications.banner.markdownLinkText": "マークダウン対応", - "kbn.advancedSettings.notifications.bannerLifetimeText": "バナー通知が画面に表示されるミリ秒単位での時間です。{infinityValue} に設定するとカウントダウンが無効になります。", - "kbn.advancedSettings.notifications.bannerLifetimeTitle": "バナー通知時間", - "kbn.advancedSettings.notifications.bannerText": "すべてのユーザーへの一時的な通知を目的としたカスタムバナーです。{markdownLink}", - "kbn.advancedSettings.notifications.bannerTitle": "カスタムバナー通知", - "kbn.advancedSettings.notifications.errorLifetimeText": "エラー通知が画面に表示されるミリ秒単位での時間です。{infinityValue} に設定すると無効になります。", - "kbn.advancedSettings.notifications.errorLifetimeTitle": "エラー通知時間", - "kbn.advancedSettings.notifications.infoLifetimeText": "情報通知が画面に表示されるミリ秒単位での時間です。{infinityValue} に設定すると無効になります。", - "kbn.advancedSettings.notifications.infoLifetimeTitle": "情報通知時間", - "kbn.advancedSettings.notifications.warningLifetimeText": "警告通知が画面に表示されるミリ秒単位での時間です。{infinityValue} に設定すると無効になります。", - "kbn.advancedSettings.notifications.warningLifetimeTitle": "警告通知時間", - "kbn.advancedSettings.pageNavigationDesc": "ナビゲーションのスタイルを変更", - "kbn.advancedSettings.pageNavigationLegacy": "レガシー", - "kbn.advancedSettings.pageNavigationModern": "モダン", - "kbn.advancedSettings.pageNavigationName": "サイドナビゲーションスタイル", - "kbn.advancedSettings.storeUrlText": "URL は長くなりすぎてブラウザが対応できない場合があります。セッションストレージに URL の一部を保存することがで この問題に対処できるかテストしています。結果を教えてください!", - "kbn.advancedSettings.storeUrlTitle": "セッションストレージに URL を格納", - "kbn.advancedSettings.themeVersionText": "現在のバージョンと次のバージョンのKibanaで使用されるテーマを切り替えます。この設定を適用するにはページの更新が必要です。", - "kbn.advancedSettings.themeVersionTitle": "テーマバージョン", "kbn.advancedSettings.visualization.showRegionMapWarningsText": "用語がマップの形に合わない場合に地域マップに警告を表示するかどうかです。", "kbn.advancedSettings.visualization.showRegionMapWarningsTitle": "地域マップに警告を表示", "kbn.advancedSettings.visualization.tileMap.maxPrecision.cellDimensionsLinkText": "ディメンションの説明", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 36691eeadb928..e892ff228cd49 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -526,6 +526,47 @@ "core.ui.securityNavList.label": "安全", "core.ui.welcomeErrorMessage": "Elastic 未正确加载。检查服务器输出以了解详情。", "core.ui.welcomeMessage": "正在加载 Elastic", + "core.ui_settings.params.darkModeText": "为 Kibana UI 启用深色模式需要刷新页面,才能应用设置。", + "core.ui_settings.params.darkModeTitle": "深色模式", + "core.ui_settings.params.dateFormat.dayOfWeekText": "一周从哪一日开始?", + "core.ui_settings.params.dateFormat.dayOfWeekTitle": "周内日", + "core.ui_settings.params.dateFormat.optionsLinkText": "格式", + "core.ui_settings.params.dateFormat.scaled.intervalsLinkText": "ISO8601 时间间隔", + "core.ui_settings.params.dateFormat.scaledText": "定义在基于时间的数据按顺序呈现且格式化时间戳应适应度量时间间隔时所用格式的值。键是 {intervalsLink}。", + "core.ui_settings.params.dateFormat.scaledTitle": "缩放的日期格式", + "core.ui_settings.params.dateFormat.timezoneText": "应使用哪个时区。{defaultOption} 将使用您的浏览器检测到的时区。", + "core.ui_settings.params.dateFormat.timezoneTitle": "用于设置日期格式的时区", + "core.ui_settings.params.dateFormatText": "显示格式正确的日期时,请使用此{formatLink}", + "core.ui_settings.params.dateFormatTitle": "日期格式", + "core.ui_settings.params.dateNanosFormatText": "用于 Elasticsearch 的 {dateNanosLink} 数据类型", + "core.ui_settings.params.dateNanosFormatTitle": "纳秒格式的日期", + "core.ui_settings.params.dateNanosLinkTitle": "date_nanos", + "core.ui_settings.params.defaultRoute.defaultRouteIsRelativeValidationMessage": "必须是相对 URL。", + "core.ui_settings.params.defaultRoute.defaultRouteText": "此设置指定打开 Kibana 时的默认路由。您可以使用此设置修改打开 Kibana 时的登陆页面。路由必须是相对 URL。", + "core.ui_settings.params.defaultRoute.defaultRouteTitle": "默认路由", + "core.ui_settings.params.disableAnimationsText": "在 Kibana UI 中关闭所有没有必要的动画。刷新页面以应用更改。", + "core.ui_settings.params.disableAnimationsTitle": "禁用动画", + "core.ui_settings.params.maxCellHeightText": "表中单元格应占用的最大高度。设置为 0 可禁用截短", + "core.ui_settings.params.maxCellHeightTitle": "最大表单元格高度", + "core.ui_settings.params.notifications.banner.markdownLinkText": "Markdown 受支持", + "core.ui_settings.params.notifications.bannerLifetimeText": "在屏幕上显示横幅通知的时间(毫秒)。设置为 {infinityValue} 将禁用倒计时。", + "core.ui_settings.params.notifications.bannerLifetimeTitle": "横幅通知生存时间", + "core.ui_settings.params.notifications.bannerText": "用于向所有用户发送临时通知的定制横幅。{markdownLink}", + "core.ui_settings.params.notifications.bannerTitle": "定制横幅通知", + "core.ui_settings.params.notifications.errorLifetimeText": "在屏幕上显示错误通知的时间(毫秒)。设置为 {infinityValue} 将禁用。", + "core.ui_settings.params.notifications.errorLifetimeTitle": "错误通知生存时间", + "core.ui_settings.params.notifications.infoLifetimeText": "在屏幕上显示信息通知的时间(毫秒)。设置为 {infinityValue} 将禁用。", + "core.ui_settings.params.notifications.infoLifetimeTitle": "信息通知生存时间", + "core.ui_settings.params.notifications.warningLifetimeText": "在屏幕上显示警告通知的时间(毫秒)。设置为 {infinityValue} 将禁用。", + "core.ui_settings.params.notifications.warningLifetimeTitle": "警告通知生存时间", + "core.ui_settings.params.pageNavigationDesc": "更改导航样式", + "core.ui_settings.params.pageNavigationLegacy": "旧版", + "core.ui_settings.params.pageNavigationModern": "现代", + "core.ui_settings.params.pageNavigationName": "侧边导航样式", + "core.ui_settings.params.themeVersionText": "在用于 Kibana 当前和下一版本的主题间切换。需要刷新页面,才能应用设置。", + "core.ui_settings.params.themeVersionTitle": "主题版本", + "core.ui_settings.params.storeUrlText": "URL 有时会变得过长,以使得某些浏览器无法处理。为此,我们正在测试将 URL 的各个组成部分存储在会话存储中是否会有帮助。请告知我们这样做的效果!", + "core.ui_settings.params.storeUrlTitle": "将 URL 存储在会话存储中", "dashboard.actions.toggleExpandPanelMenuItem.expandedDisplayName": "最小化", "dashboard.actions.toggleExpandPanelMenuItem.notExpandedDisplayName": "全屏", "dashboard.addExistingVisualizationLinkText": "将现有", @@ -2735,47 +2776,6 @@ "inspector.requests.statisticsTabLabel": "统计信息", "inspector.title": "检查器", "inspector.view": "视图:{viewName}", - "kbn.advancedSettings.darkModeText": "为 Kibana UI 启用深色模式需要刷新页面,才能应用设置。", - "kbn.advancedSettings.darkModeTitle": "深色模式", - "kbn.advancedSettings.dateFormat.dayOfWeekText": "一周从哪一日开始?", - "kbn.advancedSettings.dateFormat.dayOfWeekTitle": "周内日", - "kbn.advancedSettings.dateFormat.optionsLinkText": "格式", - "kbn.advancedSettings.dateFormat.scaled.intervalsLinkText": "ISO8601 时间间隔", - "kbn.advancedSettings.dateFormat.scaledText": "定义在基于时间的数据按顺序呈现且格式化时间戳应适应度量时间间隔时所用格式的值。键是 {intervalsLink}。", - "kbn.advancedSettings.dateFormat.scaledTitle": "缩放的日期格式", - "kbn.advancedSettings.dateFormat.timezoneText": "应使用哪个时区。{defaultOption} 将使用您的浏览器检测到的时区。", - "kbn.advancedSettings.dateFormat.timezoneTitle": "用于设置日期格式的时区", - "kbn.advancedSettings.dateFormatText": "显示格式正确的日期时,请使用此{formatLink}", - "kbn.advancedSettings.dateFormatTitle": "日期格式", - "kbn.advancedSettings.dateNanosFormatText": "用于 Elasticsearch 的 {dateNanosLink} 数据类型", - "kbn.advancedSettings.dateNanosFormatTitle": "纳秒格式的日期", - "kbn.advancedSettings.dateNanosLinkTitle": "date_nanos", - "kbn.advancedSettings.defaultRoute.defaultRouteIsRelativeValidationMessage": "必须是相对 URL。", - "kbn.advancedSettings.defaultRoute.defaultRouteText": "此设置指定打开 Kibana 时的默认路由。您可以使用此设置修改打开 Kibana 时的登陆页面。路由必须是相对 URL。", - "kbn.advancedSettings.defaultRoute.defaultRouteTitle": "默认路由", - "kbn.advancedSettings.disableAnimationsText": "在 Kibana UI 中关闭所有没有必要的动画。刷新页面以应用更改。", - "kbn.advancedSettings.disableAnimationsTitle": "禁用动画", - "kbn.advancedSettings.maxCellHeightText": "表中单元格应占用的最大高度。设置为 0 可禁用截短", - "kbn.advancedSettings.maxCellHeightTitle": "最大表单元格高度", - "kbn.advancedSettings.notifications.banner.markdownLinkText": "Markdown 受支持", - "kbn.advancedSettings.notifications.bannerLifetimeText": "在屏幕上显示横幅通知的时间(毫秒)。设置为 {infinityValue} 将禁用倒计时。", - "kbn.advancedSettings.notifications.bannerLifetimeTitle": "横幅通知生存时间", - "kbn.advancedSettings.notifications.bannerText": "用于向所有用户发送临时通知的定制横幅。{markdownLink}", - "kbn.advancedSettings.notifications.bannerTitle": "定制横幅通知", - "kbn.advancedSettings.notifications.errorLifetimeText": "在屏幕上显示错误通知的时间(毫秒)。设置为 {infinityValue} 将禁用。", - "kbn.advancedSettings.notifications.errorLifetimeTitle": "错误通知生存时间", - "kbn.advancedSettings.notifications.infoLifetimeText": "在屏幕上显示信息通知的时间(毫秒)。设置为 {infinityValue} 将禁用。", - "kbn.advancedSettings.notifications.infoLifetimeTitle": "信息通知生存时间", - "kbn.advancedSettings.notifications.warningLifetimeText": "在屏幕上显示警告通知的时间(毫秒)。设置为 {infinityValue} 将禁用。", - "kbn.advancedSettings.notifications.warningLifetimeTitle": "警告通知生存时间", - "kbn.advancedSettings.pageNavigationDesc": "更改导航样式", - "kbn.advancedSettings.pageNavigationLegacy": "旧版", - "kbn.advancedSettings.pageNavigationModern": "现代", - "kbn.advancedSettings.pageNavigationName": "侧边导航样式", - "kbn.advancedSettings.storeUrlText": "URL 有时会变得过长,以使得某些浏览器无法处理。为此,我们正在测试将 URL 的各个组成部分存储在会话存储中是否会有帮助。请告知我们这样做的效果!", - "kbn.advancedSettings.storeUrlTitle": "将 URL 存储在会话存储中", - "kbn.advancedSettings.themeVersionText": "在用于 Kibana 当前和下一版本的主题间切换。需要刷新页面,才能应用设置。", - "kbn.advancedSettings.themeVersionTitle": "主题版本", "kbn.advancedSettings.visualization.showRegionMapWarningsText": "词无法联接到地图上的形状时,区域地图是否显示警告。", "kbn.advancedSettings.visualization.showRegionMapWarningsTitle": "显示区域地图警告", "kbn.advancedSettings.visualization.tileMap.maxPrecision.cellDimensionsLinkText": "单元格维度的解释", From 0758df87fcf461ed90fa6d49bf14f2b2c921f031 Mon Sep 17 00:00:00 2001 From: Ryland Herrick Date: Mon, 24 Aug 2020 15:38:35 -0500 Subject: [PATCH 17/71] [Security Solution][Detections] Cleaning up mocks/tests (#74920) * Simplify our kibana mocks * Simpler mock factory that returns an object instead of a thunk * We can use mockReturnValue instead of mockImplementation to accomplish the same * Allows us to replace createStartServices mock * Uses unknown instead of any for mocks * Clean up our manual use of kibana mocks in tests * Since our useKibana mock returns a consistent mock, we can modify its return value instead of re-mocking the entire thing * Removes unnecessary uses of clearing/resetting mocks * If your mocks are configured at the beginning of each test this is usually unnecessary. * I left one case of clearAllMocks in all_cases/index.test since it defined several mock functions that were persistent across tests, and it was easier than moving their definitions to a beforeEach * Removes some unnecessary overrides that seemed due to storage previously not being mocked * Rename some old occurrences of SIEM * Cross-reference similar hooks via JSDoc There's a good chance that the consumer might want the OTHER hook, so let's make that discoverable. * Adds jest tests for our useListsConfig hook * adds mocks for the hooks upon which it depends * Add a mock for our useListsConfig hook Leverages this mock factory in our manual mock for this hook. * Remove unneeded eslint exception * Move kibana_react mocks into their own .mock file We're trying to consolidate mocks to this pattern so that they're easier to find and reuse. * Remove intermediate mock factory This was only being consumed by our general createStartServicesMock. * Replace security_solution's alias for a core mock This is just noise/tech debt, we should use the core mock directly when we can. * Remove unnecessary wrapper around core mocks Instead let's just reference the core mocks themselves. * Remove outdated references from upstream * More accurate mock Throw an error of the same type if an unexpected key is used. Co-authored-by: Elastic Machine --- .../lists/public/common/mocks/kibana_core.ts | 12 -- .../lists/public/exceptions/api.test.ts | 186 ++++++++---------- .../hooks/persist_exception_item.test.ts | 4 +- .../hooks/persist_exception_list.test.ts | 4 +- .../public/exceptions/hooks/use_api.test.ts | 4 +- .../hooks/use_exception_list.test.ts | 4 +- .../cases/components/all_cases/index.test.tsx | 16 +- .../configure_cases/__mock__/index.tsx | 10 - .../components/configure_cases/index.test.tsx | 46 ++--- .../use_all_cases_modal/index.test.tsx | 17 +- .../draggable_wrapper_hover_content.test.tsx | 4 +- .../add_exception_modal/index.test.tsx | 9 +- .../edit_exception_modal/index.test.tsx | 9 +- .../exceptions/use_add_exception.test.tsx | 4 +- ...tch_or_create_rule_exception_list.test.tsx | 4 +- .../ml/hooks/use_installed_security_jobs.ts | 3 + .../ml_popover/hooks/use_security_jobs.ts | 9 +- .../common/components/ml_popover/types.ts | 2 +- .../components/query_bar/index.test.tsx | 34 +--- .../super_date_picker/index.test.tsx | 2 +- .../common/components/top_n/index.test.tsx | 4 +- .../use_messages_storage.test.tsx | 6 +- .../common/lib/kibana/__mocks__/index.ts | 12 +- .../common/lib/kibana/kibana_react.mock.ts | 103 ++++++++++ .../mock/endpoint/app_context_render.tsx | 2 +- .../public/common/mock/index.ts | 1 - .../public/common/mock/kibana_core.ts | 15 -- .../public/common/mock/kibana_react.ts | 126 ------------ .../public/common/mock/test_providers.tsx | 7 +- .../lists/__mocks__/use_lists_config.tsx | 4 +- .../lists/use_lists_config.mock.ts | 15 ++ .../lists/use_lists_config.test.tsx | 86 ++++++++ .../lists/use_lists_index.mock.ts | 14 ++ .../lists/use_lists_privileges.mock.ts | 14 ++ .../detection_engine/rules/all/index.test.tsx | 56 +++--- .../recent_cases/no_cases/index.test.tsx | 20 +- .../data_providers/data_providers.test.tsx | 5 +- .../data_providers/providers.test.tsx | 4 +- .../components/timeline/header/index.test.tsx | 4 +- .../timeline/query_bar/index.test.tsx | 4 +- .../containers/local_storage/index.test.ts | 11 +- 41 files changed, 429 insertions(+), 467 deletions(-) delete mode 100644 x-pack/plugins/lists/public/common/mocks/kibana_core.ts create mode 100644 x-pack/plugins/security_solution/public/common/lib/kibana/kibana_react.mock.ts delete mode 100644 x-pack/plugins/security_solution/public/common/mock/kibana_core.ts delete mode 100644 x-pack/plugins/security_solution/public/common/mock/kibana_react.ts create mode 100644 x-pack/plugins/security_solution/public/detections/containers/detection_engine/lists/use_lists_config.mock.ts create mode 100644 x-pack/plugins/security_solution/public/detections/containers/detection_engine/lists/use_lists_config.test.tsx create mode 100644 x-pack/plugins/security_solution/public/detections/containers/detection_engine/lists/use_lists_index.mock.ts create mode 100644 x-pack/plugins/security_solution/public/detections/containers/detection_engine/lists/use_lists_privileges.mock.ts diff --git a/x-pack/plugins/lists/public/common/mocks/kibana_core.ts b/x-pack/plugins/lists/public/common/mocks/kibana_core.ts deleted file mode 100644 index c078e8ccd5ea1..0000000000000 --- a/x-pack/plugins/lists/public/common/mocks/kibana_core.ts +++ /dev/null @@ -1,12 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { coreMock } from '../../../../../../src/core/public/mocks'; -import { CoreStart } from '../../../../../../src/core/public'; - -export type GlobalServices = Pick; - -export const createKibanaCoreStartMock = (): GlobalServices => coreMock.createStart(); diff --git a/x-pack/plugins/lists/public/exceptions/api.test.ts b/x-pack/plugins/lists/public/exceptions/api.test.ts index 9add15c533d14..457a8708ec341 100644 --- a/x-pack/plugins/lists/public/exceptions/api.test.ts +++ b/x-pack/plugins/lists/public/exceptions/api.test.ts @@ -3,7 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { createKibanaCoreStartMock } from '../common/mocks/kibana_core'; +import { coreMock } from '../../../../../src/core/public/mocks'; import { getExceptionListSchemaMock } from '../../common/schemas/response/exception_list_schema.mock'; import { getExceptionListItemSchemaMock } from '../../common/schemas/response/exception_list_item_schema.mock'; import { getCreateExceptionListSchemaMock } from '../../common/schemas/request/create_exception_list_schema.mock'; @@ -34,39 +34,28 @@ import { ApiCallByIdProps, ApiCallByListIdProps } from './types'; const abortCtrl = new AbortController(); -jest.mock('../common/mocks/kibana_core', () => ({ - createKibanaCoreStartMock: (): jest.Mock => jest.fn(), -})); -const fetchMock = jest.fn(); +describe('Exceptions Lists API', () => { + let httpMock: ReturnType['http']; -/* - This is a little funky, in order for typescript to not - yell at us for converting 'Pick' to type 'Mock' - have to first convert to type 'unknown' - */ -const mockKibanaHttpService = ((createKibanaCoreStartMock() as unknown) as jest.Mock).mockReturnValue( - { - fetch: fetchMock, - } -); + beforeEach(() => { + httpMock = coreMock.createStart().http; + }); -describe('Exceptions Lists API', () => { describe('#addExceptionList', () => { beforeEach(() => { - fetchMock.mockClear(); - fetchMock.mockResolvedValue(getExceptionListSchemaMock()); + httpMock.fetch.mockResolvedValue(getExceptionListSchemaMock()); }); test('it invokes "addExceptionList" with expected url and body values', async () => { const payload = getCreateExceptionListSchemaMock(); await addExceptionList({ - http: mockKibanaHttpService(), + http: httpMock, list: payload, signal: abortCtrl.signal, }); // TODO Would like to just use getExceptionListSchemaMock() here, but // validation returns object in different order, making the strings not match - expect(fetchMock).toHaveBeenCalledWith('/api/exception_lists', { + expect(httpMock.fetch).toHaveBeenCalledWith('/api/exception_lists', { body: JSON.stringify(payload), method: 'POST', signal: abortCtrl.signal, @@ -76,7 +65,7 @@ describe('Exceptions Lists API', () => { test('it returns expected exception list on success', async () => { const payload = getCreateExceptionListSchemaMock(); const exceptionResponse = await addExceptionList({ - http: mockKibanaHttpService(), + http: httpMock, list: payload, signal: abortCtrl.signal, }); @@ -90,7 +79,7 @@ describe('Exceptions Lists API', () => { await expect( addExceptionList({ - http: mockKibanaHttpService(), + http: httpMock, list: (payload as unknown) as ExceptionListSchema, signal: abortCtrl.signal, }) @@ -101,11 +90,11 @@ describe('Exceptions Lists API', () => { const payload = getCreateExceptionListSchemaMock(); const badPayload = getExceptionListSchemaMock(); delete badPayload.id; - fetchMock.mockResolvedValue(badPayload); + httpMock.fetch.mockResolvedValue(badPayload); await expect( addExceptionList({ - http: mockKibanaHttpService(), + http: httpMock, list: payload, signal: abortCtrl.signal, }) @@ -115,20 +104,19 @@ describe('Exceptions Lists API', () => { describe('#addExceptionListItem', () => { beforeEach(() => { - fetchMock.mockClear(); - fetchMock.mockResolvedValue(getExceptionListItemSchemaMock()); + httpMock.fetch.mockResolvedValue(getExceptionListItemSchemaMock()); }); test('it invokes "addExceptionListItem" with expected url and body values', async () => { const payload = getCreateExceptionListItemSchemaMock(); await addExceptionListItem({ - http: mockKibanaHttpService(), + http: httpMock, listItem: payload, signal: abortCtrl.signal, }); // TODO Would like to just use getExceptionListSchemaMock() here, but // validation returns object in different order, making the strings not match - expect(fetchMock).toHaveBeenCalledWith('/api/exception_lists/items', { + expect(httpMock.fetch).toHaveBeenCalledWith('/api/exception_lists/items', { body: JSON.stringify(payload), method: 'POST', signal: abortCtrl.signal, @@ -138,7 +126,7 @@ describe('Exceptions Lists API', () => { test('it returns expected exception list on success', async () => { const payload = getCreateExceptionListItemSchemaMock(); const exceptionResponse = await addExceptionListItem({ - http: mockKibanaHttpService(), + http: httpMock, listItem: payload, signal: abortCtrl.signal, }); @@ -152,7 +140,7 @@ describe('Exceptions Lists API', () => { await expect( addExceptionListItem({ - http: mockKibanaHttpService(), + http: httpMock, listItem: (payload as unknown) as ExceptionListItemSchema, signal: abortCtrl.signal, }) @@ -163,11 +151,11 @@ describe('Exceptions Lists API', () => { const payload = getCreateExceptionListItemSchemaMock(); const badPayload = getExceptionListItemSchemaMock(); delete badPayload.id; - fetchMock.mockResolvedValue(badPayload); + httpMock.fetch.mockResolvedValue(badPayload); await expect( addExceptionListItem({ - http: mockKibanaHttpService(), + http: httpMock, listItem: payload, signal: abortCtrl.signal, }) @@ -177,20 +165,19 @@ describe('Exceptions Lists API', () => { describe('#updateExceptionList', () => { beforeEach(() => { - fetchMock.mockClear(); - fetchMock.mockResolvedValue(getExceptionListSchemaMock()); + httpMock.fetch.mockResolvedValue(getExceptionListSchemaMock()); }); test('it invokes "updateExceptionList" with expected url and body values', async () => { const payload = getUpdateExceptionListSchemaMock(); await updateExceptionList({ - http: mockKibanaHttpService(), + http: httpMock, list: payload, signal: abortCtrl.signal, }); // TODO Would like to just use getExceptionListSchemaMock() here, but // validation returns object in different order, making the strings not match - expect(fetchMock).toHaveBeenCalledWith('/api/exception_lists', { + expect(httpMock.fetch).toHaveBeenCalledWith('/api/exception_lists', { body: JSON.stringify(payload), method: 'PUT', signal: abortCtrl.signal, @@ -200,7 +187,7 @@ describe('Exceptions Lists API', () => { test('it returns expected exception list on success', async () => { const payload = getUpdateExceptionListSchemaMock(); const exceptionResponse = await updateExceptionList({ - http: mockKibanaHttpService(), + http: httpMock, list: payload, signal: abortCtrl.signal, }); @@ -213,7 +200,7 @@ describe('Exceptions Lists API', () => { await expect( updateExceptionList({ - http: mockKibanaHttpService(), + http: httpMock, list: payload, signal: abortCtrl.signal, }) @@ -224,11 +211,11 @@ describe('Exceptions Lists API', () => { const payload = getUpdateExceptionListSchemaMock(); const badPayload = getExceptionListSchemaMock(); delete badPayload.id; - fetchMock.mockResolvedValue(badPayload); + httpMock.fetch.mockResolvedValue(badPayload); await expect( updateExceptionList({ - http: mockKibanaHttpService(), + http: httpMock, list: payload, signal: abortCtrl.signal, }) @@ -238,20 +225,19 @@ describe('Exceptions Lists API', () => { describe('#updateExceptionListItem', () => { beforeEach(() => { - fetchMock.mockClear(); - fetchMock.mockResolvedValue(getExceptionListItemSchemaMock()); + httpMock.fetch.mockResolvedValue(getExceptionListItemSchemaMock()); }); test('it invokes "updateExceptionListItem" with expected url and body values', async () => { const payload = getUpdateExceptionListItemSchemaMock(); await updateExceptionListItem({ - http: mockKibanaHttpService(), + http: httpMock, listItem: payload, signal: abortCtrl.signal, }); // TODO Would like to just use getExceptionListSchemaMock() here, but // validation returns object in different order, making the strings not match - expect(fetchMock).toHaveBeenCalledWith('/api/exception_lists/items', { + expect(httpMock.fetch).toHaveBeenCalledWith('/api/exception_lists/items', { body: JSON.stringify(payload), method: 'PUT', signal: abortCtrl.signal, @@ -261,7 +247,7 @@ describe('Exceptions Lists API', () => { test('it returns expected exception list on success', async () => { const payload = getUpdateExceptionListItemSchemaMock(); const exceptionResponse = await updateExceptionListItem({ - http: mockKibanaHttpService(), + http: httpMock, listItem: payload, signal: abortCtrl.signal, }); @@ -274,7 +260,7 @@ describe('Exceptions Lists API', () => { await expect( updateExceptionListItem({ - http: mockKibanaHttpService(), + http: httpMock, listItem: payload, signal: abortCtrl.signal, }) @@ -285,11 +271,11 @@ describe('Exceptions Lists API', () => { const payload = getUpdateExceptionListItemSchemaMock(); const badPayload = getExceptionListItemSchemaMock(); delete badPayload.id; - fetchMock.mockResolvedValue(badPayload); + httpMock.fetch.mockResolvedValue(badPayload); await expect( updateExceptionListItem({ - http: mockKibanaHttpService(), + http: httpMock, listItem: payload, signal: abortCtrl.signal, }) @@ -299,18 +285,17 @@ describe('Exceptions Lists API', () => { describe('#fetchExceptionListById', () => { beforeEach(() => { - fetchMock.mockClear(); - fetchMock.mockResolvedValue(getExceptionListSchemaMock()); + httpMock.fetch.mockResolvedValue(getExceptionListSchemaMock()); }); test('it invokes "fetchExceptionListById" with expected url and body values', async () => { await fetchExceptionListById({ - http: mockKibanaHttpService(), + http: httpMock, id: '1', namespaceType: 'single', signal: abortCtrl.signal, }); - expect(fetchMock).toHaveBeenCalledWith('/api/exception_lists', { + expect(httpMock.fetch).toHaveBeenCalledWith('/api/exception_lists', { method: 'GET', query: { id: '1', @@ -322,7 +307,7 @@ describe('Exceptions Lists API', () => { test('it returns expected exception list on success', async () => { const exceptionResponse = await fetchExceptionListById({ - http: mockKibanaHttpService(), + http: httpMock, id: '1', namespaceType: 'single', signal: abortCtrl.signal, @@ -332,7 +317,7 @@ describe('Exceptions Lists API', () => { test('it returns error and does not make request if request payload fails decode', async () => { const payload = ({ - http: mockKibanaHttpService(), + http: httpMock, id: 1, namespaceType: 'single', signal: abortCtrl.signal, @@ -345,11 +330,11 @@ describe('Exceptions Lists API', () => { test('it returns error if response payload fails decode', async () => { const badPayload = getExceptionListSchemaMock(); delete badPayload.id; - fetchMock.mockResolvedValue(badPayload); + httpMock.fetch.mockResolvedValue(badPayload); await expect( fetchExceptionListById({ - http: mockKibanaHttpService(), + http: httpMock, id: '1', namespaceType: 'single', signal: abortCtrl.signal, @@ -360,14 +345,13 @@ describe('Exceptions Lists API', () => { describe('#fetchExceptionListsItemsByListIds', () => { beforeEach(() => { - fetchMock.mockClear(); - fetchMock.mockResolvedValue(getFoundExceptionListItemSchemaMock()); + httpMock.fetch.mockResolvedValue(getFoundExceptionListItemSchemaMock()); }); test('it invokes "fetchExceptionListsItemsByListIds" with expected url and body values', async () => { await fetchExceptionListsItemsByListIds({ filterOptions: [], - http: mockKibanaHttpService(), + http: httpMock, listIds: ['myList', 'myOtherListId'], namespaceTypes: ['single', 'single'], pagination: { @@ -377,7 +361,7 @@ describe('Exceptions Lists API', () => { signal: abortCtrl.signal, }); - expect(fetchMock).toHaveBeenCalledWith('/api/exception_lists/items/_find', { + expect(httpMock.fetch).toHaveBeenCalledWith('/api/exception_lists/items/_find', { method: 'GET', query: { list_id: 'myList,myOtherListId', @@ -397,7 +381,7 @@ describe('Exceptions Lists API', () => { tags: [], }, ], - http: mockKibanaHttpService(), + http: httpMock, listIds: ['myList'], namespaceTypes: ['single'], pagination: { @@ -407,7 +391,7 @@ describe('Exceptions Lists API', () => { signal: abortCtrl.signal, }); - expect(fetchMock).toHaveBeenCalledWith('/api/exception_lists/items/_find', { + expect(httpMock.fetch).toHaveBeenCalledWith('/api/exception_lists/items/_find', { method: 'GET', query: { filter: 'exception-list.attributes.entries.field:hello world*', @@ -428,7 +412,7 @@ describe('Exceptions Lists API', () => { tags: [], }, ], - http: mockKibanaHttpService(), + http: httpMock, listIds: ['myList'], namespaceTypes: ['agnostic'], pagination: { @@ -438,7 +422,7 @@ describe('Exceptions Lists API', () => { signal: abortCtrl.signal, }); - expect(fetchMock).toHaveBeenCalledWith('/api/exception_lists/items/_find', { + expect(httpMock.fetch).toHaveBeenCalledWith('/api/exception_lists/items/_find', { method: 'GET', query: { filter: 'exception-list-agnostic.attributes.entries.field:hello world*', @@ -459,7 +443,7 @@ describe('Exceptions Lists API', () => { tags: ['malware'], }, ], - http: mockKibanaHttpService(), + http: httpMock, listIds: ['myList'], namespaceTypes: ['agnostic'], pagination: { @@ -469,7 +453,7 @@ describe('Exceptions Lists API', () => { signal: abortCtrl.signal, }); - expect(fetchMock).toHaveBeenCalledWith('/api/exception_lists/items/_find', { + expect(httpMock.fetch).toHaveBeenCalledWith('/api/exception_lists/items/_find', { method: 'GET', query: { filter: 'exception-list-agnostic.attributes.tags:malware', @@ -490,7 +474,7 @@ describe('Exceptions Lists API', () => { tags: ['malware'], }, ], - http: mockKibanaHttpService(), + http: httpMock, listIds: ['myList'], namespaceTypes: ['agnostic'], pagination: { @@ -500,7 +484,7 @@ describe('Exceptions Lists API', () => { signal: abortCtrl.signal, }); - expect(fetchMock).toHaveBeenCalledWith('/api/exception_lists/items/_find', { + expect(httpMock.fetch).toHaveBeenCalledWith('/api/exception_lists/items/_find', { method: 'GET', query: { filter: @@ -517,7 +501,7 @@ describe('Exceptions Lists API', () => { test('it returns expected format when call succeeds', async () => { const exceptionResponse = await fetchExceptionListsItemsByListIds({ filterOptions: [], - http: mockKibanaHttpService(), + http: httpMock, listIds: ['endpoint_list_id'], namespaceTypes: ['single'], pagination: { @@ -532,7 +516,7 @@ describe('Exceptions Lists API', () => { test('it returns error and does not make request if request payload fails decode', async () => { const payload = ({ filterOptions: [], - http: mockKibanaHttpService(), + http: httpMock, listIds: ['myList'], namespaceTypes: ['not a namespace type'], pagination: { @@ -549,12 +533,12 @@ describe('Exceptions Lists API', () => { test('it returns error if response payload fails decode', async () => { const badPayload = getExceptionListItemSchemaMock(); delete badPayload.id; - fetchMock.mockResolvedValue(badPayload); + httpMock.fetch.mockResolvedValue(badPayload); await expect( fetchExceptionListsItemsByListIds({ filterOptions: [], - http: mockKibanaHttpService(), + http: httpMock, listIds: ['myList'], namespaceTypes: ['single'], pagination: { @@ -571,18 +555,17 @@ describe('Exceptions Lists API', () => { describe('#fetchExceptionListItemById', () => { beforeEach(() => { - fetchMock.mockClear(); - fetchMock.mockResolvedValue(getExceptionListItemSchemaMock()); + httpMock.fetch.mockResolvedValue(getExceptionListItemSchemaMock()); }); test('it invokes "fetchExceptionListItemById" with expected url and body values', async () => { await fetchExceptionListItemById({ - http: mockKibanaHttpService(), + http: httpMock, id: '1', namespaceType: 'single', signal: abortCtrl.signal, }); - expect(fetchMock).toHaveBeenCalledWith('/api/exception_lists/items', { + expect(httpMock.fetch).toHaveBeenCalledWith('/api/exception_lists/items', { method: 'GET', query: { id: '1', @@ -594,7 +577,7 @@ describe('Exceptions Lists API', () => { test('it returns expected format when call succeeds', async () => { const exceptionResponse = await fetchExceptionListItemById({ - http: mockKibanaHttpService(), + http: httpMock, id: '1', namespaceType: 'single', signal: abortCtrl.signal, @@ -604,7 +587,7 @@ describe('Exceptions Lists API', () => { test('it returns error and does not make request if request payload fails decode', async () => { const payload = ({ - http: mockKibanaHttpService(), + http: httpMock, id: '1', namespaceType: 'not a namespace type', signal: abortCtrl.signal, @@ -617,11 +600,11 @@ describe('Exceptions Lists API', () => { test('it returns error if response payload fails decode', async () => { const badPayload = getExceptionListItemSchemaMock(); delete badPayload.id; - fetchMock.mockResolvedValue(badPayload); + httpMock.fetch.mockResolvedValue(badPayload); await expect( fetchExceptionListItemById({ - http: mockKibanaHttpService(), + http: httpMock, id: '1', namespaceType: 'single', signal: abortCtrl.signal, @@ -632,18 +615,17 @@ describe('Exceptions Lists API', () => { describe('#deleteExceptionListById', () => { beforeEach(() => { - fetchMock.mockClear(); - fetchMock.mockResolvedValue(getExceptionListSchemaMock()); + httpMock.fetch.mockResolvedValue(getExceptionListSchemaMock()); }); test('check parameter url, body when deleting exception item', async () => { await deleteExceptionListById({ - http: mockKibanaHttpService(), + http: httpMock, id: '1', namespaceType: 'single', signal: abortCtrl.signal, }); - expect(fetchMock).toHaveBeenCalledWith('/api/exception_lists', { + expect(httpMock.fetch).toHaveBeenCalledWith('/api/exception_lists', { method: 'DELETE', query: { id: '1', @@ -655,7 +637,7 @@ describe('Exceptions Lists API', () => { test('it returns expected format when call succeeds', async () => { const exceptionResponse = await deleteExceptionListById({ - http: mockKibanaHttpService(), + http: httpMock, id: '1', namespaceType: 'single', signal: abortCtrl.signal, @@ -665,7 +647,7 @@ describe('Exceptions Lists API', () => { test('it returns error and does not make request if request payload fails decode', async () => { const payload = ({ - http: mockKibanaHttpService(), + http: httpMock, id: 1, namespaceType: 'single', signal: abortCtrl.signal, @@ -678,11 +660,11 @@ describe('Exceptions Lists API', () => { test('it returns error if response payload fails decode', async () => { const badPayload = getExceptionListSchemaMock(); delete badPayload.id; - fetchMock.mockResolvedValue(badPayload); + httpMock.fetch.mockResolvedValue(badPayload); await expect( deleteExceptionListById({ - http: mockKibanaHttpService(), + http: httpMock, id: '1', namespaceType: 'single', signal: abortCtrl.signal, @@ -693,18 +675,17 @@ describe('Exceptions Lists API', () => { describe('#deleteExceptionListItemById', () => { beforeEach(() => { - fetchMock.mockClear(); - fetchMock.mockResolvedValue(getExceptionListItemSchemaMock()); + httpMock.fetch.mockResolvedValue(getExceptionListItemSchemaMock()); }); test('check parameter url, body when deleting exception item', async () => { await deleteExceptionListItemById({ - http: mockKibanaHttpService(), + http: httpMock, id: '1', namespaceType: 'single', signal: abortCtrl.signal, }); - expect(fetchMock).toHaveBeenCalledWith('/api/exception_lists/items', { + expect(httpMock.fetch).toHaveBeenCalledWith('/api/exception_lists/items', { method: 'DELETE', query: { id: '1', @@ -716,7 +697,7 @@ describe('Exceptions Lists API', () => { test('it returns expected format when call succeeds', async () => { const exceptionResponse = await deleteExceptionListItemById({ - http: mockKibanaHttpService(), + http: httpMock, id: '1', namespaceType: 'single', signal: abortCtrl.signal, @@ -726,7 +707,7 @@ describe('Exceptions Lists API', () => { test('it returns error and does not make request if request payload fails decode', async () => { const payload = ({ - http: mockKibanaHttpService(), + http: httpMock, id: 1, namespaceType: 'single', signal: abortCtrl.signal, @@ -739,11 +720,11 @@ describe('Exceptions Lists API', () => { test('it returns error if response payload fails decode', async () => { const badPayload = getExceptionListItemSchemaMock(); delete badPayload.id; - fetchMock.mockResolvedValue(badPayload); + httpMock.fetch.mockResolvedValue(badPayload); await expect( deleteExceptionListItemById({ - http: mockKibanaHttpService(), + http: httpMock, id: '1', namespaceType: 'single', signal: abortCtrl.signal, @@ -754,16 +735,15 @@ describe('Exceptions Lists API', () => { describe('#addEndpointExceptionList', () => { beforeEach(() => { - fetchMock.mockClear(); - fetchMock.mockResolvedValue(getExceptionListSchemaMock()); + httpMock.fetch.mockResolvedValue(getExceptionListSchemaMock()); }); test('it invokes "addEndpointExceptionList" with expected url and body values', async () => { await addEndpointExceptionList({ - http: mockKibanaHttpService(), + http: httpMock, signal: abortCtrl.signal, }); - expect(fetchMock).toHaveBeenCalledWith('/api/endpoint_list', { + expect(httpMock.fetch).toHaveBeenCalledWith('/api/endpoint_list', { method: 'POST', signal: abortCtrl.signal, }); @@ -771,16 +751,16 @@ describe('Exceptions Lists API', () => { test('it returns expected exception list on success', async () => { const exceptionResponse = await addEndpointExceptionList({ - http: mockKibanaHttpService(), + http: httpMock, signal: abortCtrl.signal, }); expect(exceptionResponse).toEqual(getExceptionListSchemaMock()); }); test('it returns an empty object when list already exists', async () => { - fetchMock.mockResolvedValue({}); + httpMock.fetch.mockResolvedValue({}); const exceptionResponse = await addEndpointExceptionList({ - http: mockKibanaHttpService(), + http: httpMock, signal: abortCtrl.signal, }); expect(exceptionResponse).toEqual({}); diff --git a/x-pack/plugins/lists/public/exceptions/hooks/persist_exception_item.test.ts b/x-pack/plugins/lists/public/exceptions/hooks/persist_exception_item.test.ts index ebee2cbace9cc..9460432cbc9c9 100644 --- a/x-pack/plugins/lists/public/exceptions/hooks/persist_exception_item.test.ts +++ b/x-pack/plugins/lists/public/exceptions/hooks/persist_exception_item.test.ts @@ -6,16 +6,16 @@ import { act, renderHook } from '@testing-library/react-hooks'; +import { coreMock } from '../../../../../../src/core/public/mocks'; import * as api from '../api'; import { getCreateExceptionListItemSchemaMock } from '../../../common/schemas/request/create_exception_list_item_schema.mock'; import { getUpdateExceptionListItemSchemaMock } from '../../../common/schemas/request/update_exception_list_item_schema.mock'; import { getExceptionListItemSchemaMock } from '../../../common/schemas/response/exception_list_item_schema.mock'; -import { createKibanaCoreStartMock } from '../../common/mocks/kibana_core'; import { PersistHookProps } from '../types'; import { ReturnPersistExceptionItem, usePersistExceptionItem } from './persist_exception_item'; -const mockKibanaHttpService = createKibanaCoreStartMock().http; +const mockKibanaHttpService = coreMock.createStart().http; describe('usePersistExceptionItem', () => { const onError = jest.fn(); diff --git a/x-pack/plugins/lists/public/exceptions/hooks/persist_exception_list.test.ts b/x-pack/plugins/lists/public/exceptions/hooks/persist_exception_list.test.ts index 0541f893e2797..d5dfe1174d009 100644 --- a/x-pack/plugins/lists/public/exceptions/hooks/persist_exception_list.test.ts +++ b/x-pack/plugins/lists/public/exceptions/hooks/persist_exception_list.test.ts @@ -6,16 +6,16 @@ import { act, renderHook } from '@testing-library/react-hooks'; +import { coreMock } from '../../../../../../src/core/public/mocks'; import * as api from '../api'; import { getCreateExceptionListSchemaMock } from '../../../common/schemas/request/create_exception_list_schema.mock'; import { getUpdateExceptionListSchemaMock } from '../../../common/schemas/request/update_exception_list_schema.mock'; import { getExceptionListSchemaMock } from '../../../common/schemas/response/exception_list_schema.mock'; -import { createKibanaCoreStartMock } from '../../common/mocks/kibana_core'; import { PersistHookProps } from '../types'; import { ReturnPersistExceptionList, usePersistExceptionList } from './persist_exception_list'; -const mockKibanaHttpService = createKibanaCoreStartMock().http; +const mockKibanaHttpService = coreMock.createStart().http; describe('usePersistExceptionList', () => { const onError = jest.fn(); diff --git a/x-pack/plugins/lists/public/exceptions/hooks/use_api.test.ts b/x-pack/plugins/lists/public/exceptions/hooks/use_api.test.ts index c93155274937e..6469dc49c460f 100644 --- a/x-pack/plugins/lists/public/exceptions/hooks/use_api.test.ts +++ b/x-pack/plugins/lists/public/exceptions/hooks/use_api.test.ts @@ -6,8 +6,8 @@ import { act, renderHook } from '@testing-library/react-hooks'; +import { coreMock } from '../../../../../../src/core/public/mocks'; import * as api from '../api'; -import { createKibanaCoreStartMock } from '../../common/mocks/kibana_core'; import { getExceptionListSchemaMock } from '../../../common/schemas/response/exception_list_schema.mock'; import { getFoundExceptionListItemSchemaMock } from '../../../common/schemas/response/found_exception_list_item_schema.mock'; import { getExceptionListItemSchemaMock } from '../../../common/schemas/response/exception_list_item_schema.mock'; @@ -16,7 +16,7 @@ import { ApiCallByIdProps, ApiCallByListIdProps } from '../types'; import { ExceptionsApi, useApi } from './use_api'; -const mockKibanaHttpService = createKibanaCoreStartMock().http; +const mockKibanaHttpService = coreMock.createStart().http; describe('useApi', () => { const onErrorMock = jest.fn(); diff --git a/x-pack/plugins/lists/public/exceptions/hooks/use_exception_list.test.ts b/x-pack/plugins/lists/public/exceptions/hooks/use_exception_list.test.ts index 3a8b1713b901b..5c544c7e96e33 100644 --- a/x-pack/plugins/lists/public/exceptions/hooks/use_exception_list.test.ts +++ b/x-pack/plugins/lists/public/exceptions/hooks/use_exception_list.test.ts @@ -6,15 +6,15 @@ import { act, renderHook } from '@testing-library/react-hooks'; +import { coreMock } from '../../../../../../src/core/public/mocks'; import * as api from '../api'; -import { createKibanaCoreStartMock } from '../../common/mocks/kibana_core'; import { getFoundExceptionListItemSchemaMock } from '../../../common/schemas/response/found_exception_list_item_schema.mock'; import { ExceptionListItemSchema } from '../../../common/schemas'; import { UseExceptionListProps, UseExceptionListSuccess } from '../types'; import { ReturnExceptionListAndItems, useExceptionList } from './use_exception_list'; -const mockKibanaHttpService = createKibanaCoreStartMock().http; +const mockKibanaHttpService = coreMock.createStart().http; describe('useExceptionList', () => { const onErrorMock = jest.fn(); diff --git a/x-pack/plugins/security_solution/public/cases/components/all_cases/index.test.tsx b/x-pack/plugins/security_solution/public/cases/components/all_cases/index.test.tsx index f5ed151ebac3c..e6e0823214195 100644 --- a/x-pack/plugins/security_solution/public/cases/components/all_cases/index.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/all_cases/index.test.tsx @@ -15,7 +15,6 @@ import { useGetCasesMockState } from '../../containers/mock'; import * as i18n from './translations'; import { useKibana } from '../../../common/lib/kibana'; -import { createUseKibanaMock } from '../../../common/mock/kibana_react'; import { getEmptyTagValue } from '../../../common/components/empty_value'; import { useDeleteCases } from '../../containers/use_delete_cases'; import { useGetCases } from '../../containers/use_get_cases'; @@ -28,7 +27,7 @@ jest.mock('../../containers/use_delete_cases'); jest.mock('../../containers/use_get_cases'); jest.mock('../../containers/use_get_cases_status'); -const useKibanaMock = useKibana as jest.Mock; +const useKibanaMock = useKibana as jest.Mocked; const useDeleteCasesMock = useDeleteCases as jest.Mock; const useGetCasesMock = useGetCases as jest.Mock; const useGetCasesStatusMock = useGetCasesStatus as jest.Mock; @@ -97,23 +96,16 @@ describe('AllCases', () => { }); /* eslint-enable no-console */ beforeEach(() => { - jest.resetAllMocks(); + jest.clearAllMocks(); navigateToApp = jest.fn(); - const kibanaMock = createUseKibanaMock()(); - useKibanaMock.mockReturnValue({ - ...kibanaMock, - services: { - application: { - navigateToApp, - }, - }, - }); + useKibanaMock().services.application.navigateToApp = navigateToApp; useUpdateCasesMock.mockReturnValue(defaultUpdateCases); useGetCasesMock.mockReturnValue(defaultGetCases); useDeleteCasesMock.mockReturnValue(defaultDeleteCases); useGetCasesStatusMock.mockReturnValue(defaultCasesStatus); moment.tz.setDefault('UTC'); }); + it('should render AllCases', () => { const wrapper = mount( diff --git a/x-pack/plugins/security_solution/public/cases/components/configure_cases/__mock__/index.tsx b/x-pack/plugins/security_solution/public/cases/components/configure_cases/__mock__/index.tsx index 23c76953a6a0f..08303ddc9397e 100644 --- a/x-pack/plugins/security_solution/public/cases/components/configure_cases/__mock__/index.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/configure_cases/__mock__/index.tsx @@ -8,10 +8,7 @@ import { Connector } from '../../../containers/configure/types'; import { ReturnConnectors } from '../../../containers/configure/use_connectors'; import { connectorsMock } from '../../../containers/configure/mock'; import { ReturnUseCaseConfigure } from '../../../containers/configure/use_configure'; -import { createUseKibanaMock } from '../../../../common/mock/kibana_react'; export { mapping } from '../../../containers/configure/mock'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { actionTypeRegistryMock } from '../../../../../../triggers_actions_ui/public/application/action_type_registry.mock'; export const connectors: Connector[] = connectorsMock; @@ -46,10 +43,3 @@ export const useConnectorsResponse: ReturnConnectors = { connectors, refetchConnectors: jest.fn(), }; - -export const kibanaMockImplementationArgs = { - services: { - ...createUseKibanaMock()().services, - triggers_actions_ui: { actionTypeRegistry: actionTypeRegistryMock.create() }, - }, -}; diff --git a/x-pack/plugins/security_solution/public/cases/components/configure_cases/index.test.tsx b/x-pack/plugins/security_solution/public/cases/components/configure_cases/index.test.tsx index 7974116f4dc43..3c17a9191d20c 100644 --- a/x-pack/plugins/security_solution/public/cases/components/configure_cases/index.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/configure_cases/index.test.tsx @@ -15,38 +15,39 @@ import { ActionsConnectorsContextProvider, ConnectorAddFlyout, ConnectorEditFlyout, + TriggersAndActionsUIPublicPluginStart, } from '../../../../../triggers_actions_ui/public'; +import { actionTypeRegistryMock } from '../../../../../triggers_actions_ui/public/application/action_type_registry.mock'; import { useKibana } from '../../../common/lib/kibana'; import { useConnectors } from '../../containers/configure/use_connectors'; import { useCaseConfigure } from '../../containers/configure/use_configure'; import { useGetUrlSearch } from '../../../common/components/navigation/use_get_url_search'; -import { - connectors, - searchURL, - useCaseConfigureResponse, - useConnectorsResponse, - kibanaMockImplementationArgs, -} from './__mock__'; +import { connectors, searchURL, useCaseConfigureResponse, useConnectorsResponse } from './__mock__'; jest.mock('../../../common/lib/kibana'); jest.mock('../../containers/configure/use_connectors'); jest.mock('../../containers/configure/use_configure'); jest.mock('../../../common/components/navigation/use_get_url_search'); -const useKibanaMock = useKibana as jest.Mock; +const useKibanaMock = useKibana as jest.Mocked; const useConnectorsMock = useConnectors as jest.Mock; const useCaseConfigureMock = useCaseConfigure as jest.Mock; const useGetUrlSearchMock = useGetUrlSearch as jest.Mock; + describe('ConfigureCases', () => { + beforeEach(() => { + useKibanaMock().services.triggers_actions_ui = ({ + actionTypeRegistry: actionTypeRegistryMock.create(), + } as unknown) as TriggersAndActionsUIPublicPluginStart; + }); + describe('rendering', () => { let wrapper: ReactWrapper; beforeEach(() => { - jest.resetAllMocks(); useCaseConfigureMock.mockImplementation(() => useCaseConfigureResponse); useConnectorsMock.mockImplementation(() => ({ ...useConnectorsResponse, connectors: [] })); - useKibanaMock.mockImplementation(() => kibanaMockImplementationArgs); useGetUrlSearchMock.mockImplementation(() => searchURL); wrapper = mount(, { wrappingComponent: TestProviders }); @@ -84,8 +85,8 @@ describe('ConfigureCases', () => { describe('Unhappy path', () => { let wrapper: ReactWrapper; + beforeEach(() => { - jest.resetAllMocks(); useCaseConfigureMock.mockImplementation(() => ({ ...useCaseConfigureResponse, closureType: 'close-by-user', @@ -98,7 +99,6 @@ describe('ConfigureCases', () => { }, })); useConnectorsMock.mockImplementation(() => ({ ...useConnectorsResponse, connectors: [] })); - useKibanaMock.mockImplementation(() => kibanaMockImplementationArgs); useGetUrlSearchMock.mockImplementation(() => searchURL); wrapper = mount(, { wrappingComponent: TestProviders }); }); @@ -122,7 +122,6 @@ describe('ConfigureCases', () => { let wrapper: ReactWrapper; beforeEach(() => { - jest.resetAllMocks(); useCaseConfigureMock.mockImplementation(() => ({ ...useCaseConfigureResponse, mapping: connectors[0].config.incidentConfiguration.mapping, @@ -136,7 +135,6 @@ describe('ConfigureCases', () => { }, })); useConnectorsMock.mockImplementation(() => useConnectorsResponse); - useKibanaMock.mockImplementation(() => kibanaMockImplementationArgs); useGetUrlSearchMock.mockImplementation(() => searchURL); wrapper = mount(, { wrappingComponent: TestProviders }); @@ -211,9 +209,6 @@ describe('ConfigureCases', () => { let wrapper: ReactWrapper; beforeEach(() => { - jest.resetAllMocks(); - jest.restoreAllMocks(); - jest.clearAllMocks(); useCaseConfigureMock.mockImplementation(() => ({ ...useCaseConfigureResponse, mapping: connectors[1].config.incidentConfiguration.mapping, @@ -230,7 +225,6 @@ describe('ConfigureCases', () => { ...useConnectorsResponse, loading: true, })); - useKibanaMock.mockImplementation(() => kibanaMockImplementationArgs); useGetUrlSearchMock.mockImplementation(() => searchURL); wrapper = mount(, { wrappingComponent: TestProviders }); }); @@ -262,7 +256,6 @@ describe('ConfigureCases', () => { let wrapper: ReactWrapper; beforeEach(() => { - jest.resetAllMocks(); useCaseConfigureMock.mockImplementation(() => ({ ...useCaseConfigureResponse, connectorId: 'servicenow-1', @@ -270,7 +263,6 @@ describe('ConfigureCases', () => { })); useConnectorsMock.mockImplementation(() => useConnectorsResponse); - useKibanaMock.mockImplementation(() => kibanaMockImplementationArgs); useGetUrlSearchMock.mockImplementation(() => searchURL); wrapper = mount(, { wrappingComponent: TestProviders }); }); @@ -305,7 +297,6 @@ describe('ConfigureCases', () => { let wrapper: ReactWrapper; beforeEach(() => { - jest.resetAllMocks(); useCaseConfigureMock.mockImplementation(() => ({ ...useCaseConfigureResponse, loading: true, @@ -313,7 +304,6 @@ describe('ConfigureCases', () => { useConnectorsMock.mockImplementation(() => ({ ...useConnectorsResponse, })); - useKibanaMock.mockImplementation(() => kibanaMockImplementationArgs); useGetUrlSearchMock.mockImplementation(() => searchURL); wrapper = mount(, { wrappingComponent: TestProviders }); }); @@ -329,10 +319,10 @@ describe('ConfigureCases', () => { describe('connectors', () => { let wrapper: ReactWrapper; - const persistCaseConfigure = jest.fn(); + let persistCaseConfigure: jest.Mock; beforeEach(() => { - jest.resetAllMocks(); + persistCaseConfigure = jest.fn(); useCaseConfigureMock.mockImplementation(() => ({ ...useCaseConfigureResponse, mapping: connectors[0].config.incidentConfiguration.mapping, @@ -347,7 +337,6 @@ describe('ConfigureCases', () => { persistCaseConfigure, })); useConnectorsMock.mockImplementation(() => useConnectorsResponse); - useKibanaMock.mockImplementation(() => kibanaMockImplementationArgs); useGetUrlSearchMock.mockImplementation(() => searchURL); wrapper = mount(, { wrappingComponent: TestProviders }); @@ -396,10 +385,10 @@ describe('ConfigureCases', () => { describe('closure options', () => { let wrapper: ReactWrapper; - const persistCaseConfigure = jest.fn(); + let persistCaseConfigure: jest.Mock; beforeEach(() => { - jest.resetAllMocks(); + persistCaseConfigure = jest.fn(); useCaseConfigureMock.mockImplementation(() => ({ ...useCaseConfigureResponse, mapping: connectors[0].config.incidentConfiguration.mapping, @@ -414,7 +403,6 @@ describe('closure options', () => { persistCaseConfigure, })); useConnectorsMock.mockImplementation(() => useConnectorsResponse); - useKibanaMock.mockImplementation(() => kibanaMockImplementationArgs); useGetUrlSearchMock.mockImplementation(() => searchURL); wrapper = mount(, { wrappingComponent: TestProviders }); @@ -435,7 +423,6 @@ describe('closure options', () => { describe('user interactions', () => { beforeEach(() => { - jest.resetAllMocks(); useCaseConfigureMock.mockImplementation(() => ({ ...useCaseConfigureResponse, mapping: connectors[1].config.incidentConfiguration.mapping, @@ -449,7 +436,6 @@ describe('user interactions', () => { }, })); useConnectorsMock.mockImplementation(() => useConnectorsResponse); - useKibanaMock.mockImplementation(() => kibanaMockImplementationArgs); useGetUrlSearchMock.mockImplementation(() => searchURL); }); diff --git a/x-pack/plugins/security_solution/public/cases/components/use_all_cases_modal/index.test.tsx b/x-pack/plugins/security_solution/public/cases/components/use_all_cases_modal/index.test.tsx index b5bf68cbf6dc8..3b203e81cd074 100644 --- a/x-pack/plugins/security_solution/public/cases/components/use_all_cases_modal/index.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/use_all_cases_modal/index.test.tsx @@ -14,26 +14,17 @@ import '../../../common/mock/match_media'; import { TimelineId } from '../../../../common/types/timeline'; import { useAllCasesModal, UseAllCasesModalProps, UseAllCasesModalReturnedValues } from '.'; import { TestProviders } from '../../../common/mock'; -import { createUseKibanaMock } from '../../../common/mock/kibana_react'; jest.mock('../../../common/lib/kibana'); -const useKibanaMock = useKibana as jest.Mock; +const useKibanaMock = useKibana as jest.Mocked; describe('useAllCasesModal', () => { - const navigateToApp = jest.fn(() => Promise.resolve()); + let navigateToApp: jest.Mock; beforeEach(() => { - jest.clearAllMocks(); - const kibanaMock = createUseKibanaMock()(); - useKibanaMock.mockImplementation(() => ({ - ...kibanaMock, - services: { - application: { - navigateToApp, - }, - }, - })); + navigateToApp = jest.fn(); + useKibanaMock().services.application.navigateToApp = navigateToApp; }); it('init', async () => { diff --git a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper_hover_content.test.tsx b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper_hover_content.test.tsx index 8e76a88572e42..b53da42da55f8 100644 --- a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper_hover_content.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper_hover_content.test.tsx @@ -7,12 +7,12 @@ import { mount, ReactWrapper } from 'enzyme'; import React from 'react'; +import { coreMock } from '../../../../../../../src/core/public/mocks'; import { useWithSource } from '../../containers/source'; import { mockBrowserFields } from '../../containers/source/mock'; import '../../mock/match_media'; import { useKibana } from '../../lib/kibana'; import { TestProviders } from '../../mock'; -import { createKibanaCoreStartMock } from '../../mock/kibana_core'; import { FilterManager } from '../../../../../../../src/plugins/data/public'; import { useAddToTimeline } from '../../hooks/use_add_to_timeline'; @@ -60,7 +60,7 @@ jest.mock('../../../timelines/components/manage_timeline', () => { }; }); -const mockUiSettingsForFilterManager = createKibanaCoreStartMock().uiSettings; +const mockUiSettingsForFilterManager = coreMock.createStart().uiSettings; const timelineId = TimelineId.active; const field = 'process.name'; const value = 'nice'; diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.test.tsx index 2b713636862bb..cef92ce2a7817 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.test.tsx @@ -11,14 +11,13 @@ import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; import { act } from 'react-dom/test-utils'; import { AddExceptionModal } from './'; -import { useKibana, useCurrentUser } from '../../../../common/lib/kibana'; +import { useCurrentUser } from '../../../../common/lib/kibana'; import { getExceptionListSchemaMock } from '../../../../../../lists/common/schemas/response/exception_list_schema.mock'; import { useFetchIndexPatterns } from '../../../../detections/containers/detection_engine/rules'; import { stubIndexPattern } from 'src/plugins/data/common/index_patterns/index_pattern.stub'; import { useAddOrUpdateException } from '../use_add_exception'; import { useFetchOrCreateRuleExceptionList } from '../use_fetch_or_create_rule_exception_list'; import { useSignalIndex } from '../../../../detections/containers/detection_engine/alerts/use_signal_index'; -import { createUseKibanaMock } from '../../../mock/kibana_react'; import { TimelineNonEcsData, Ecs } from '../../../../graphql/types'; import * as builder from '../builder'; import * as helpers from '../helpers'; @@ -33,8 +32,6 @@ jest.mock('../use_add_exception'); jest.mock('../use_fetch_or_create_rule_exception_list'); jest.mock('../builder'); -const useKibanaMock = useKibana as jest.Mock; - describe('When the add exception modal is opened', () => { const ruleName = 'test rule'; let defaultEndpointItems: jest.SpyInstance { .spyOn(builder, 'ExceptionBuilderComponent') .mockReturnValue(<>); - const kibanaMock = createUseKibanaMock()(); - useKibanaMock.mockImplementation(() => ({ - ...kibanaMock, - })); (useAddOrUpdateException as jest.Mock).mockImplementation(() => [ { isLoading: false }, jest.fn(), diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.test.tsx index 8ad80eba569c7..6ff218ca06059 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.test.tsx @@ -11,7 +11,7 @@ import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; import { act } from 'react-dom/test-utils'; import { EditExceptionModal } from './'; -import { useKibana, useCurrentUser } from '../../../../common/lib/kibana'; +import { useCurrentUser } from '../../../../common/lib/kibana'; import { useFetchIndexPatterns } from '../../../../detections/containers/detection_engine/rules'; import { stubIndexPattern, @@ -19,7 +19,6 @@ import { } from 'src/plugins/data/common/index_patterns/index_pattern.stub'; import { useAddOrUpdateException } from '../use_add_exception'; import { useSignalIndex } from '../../../../detections/containers/detection_engine/alerts/use_signal_index'; -import { createUseKibanaMock } from '../../../mock/kibana_react'; import { getExceptionListItemSchemaMock } from '../../../../../../lists/common/schemas/response/exception_list_item_schema.mock'; import { EntriesArray } from '../../../../../../lists/common/schemas/types'; import * as builder from '../builder'; @@ -31,8 +30,6 @@ jest.mock('../use_fetch_or_create_rule_exception_list'); jest.mock('../../../../detections/containers/detection_engine/alerts/use_signal_index'); jest.mock('../builder'); -const useKibanaMock = useKibana as jest.Mock; - describe('When the edit exception modal is opened', () => { const ruleName = 'test rule'; @@ -45,10 +42,6 @@ describe('When the edit exception modal is opened', () => { .spyOn(builder, 'ExceptionBuilderComponent') .mockReturnValue(<>); - const kibanaMock = createUseKibanaMock()(); - useKibanaMock.mockImplementation(() => ({ - ...kibanaMock, - })); (useSignalIndex as jest.Mock).mockReturnValue({ loading: false, signalIndexName: 'test-signal', diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/use_add_exception.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/use_add_exception.test.tsx index cb1a80abedb27..6611ee2385d10 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/use_add_exception.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/use_add_exception.test.tsx @@ -5,6 +5,7 @@ */ import { act, renderHook, RenderHookResult } from '@testing-library/react-hooks'; +import { coreMock } from '../../../../../../../src/core/public/mocks'; import { KibanaServices } from '../../../common/lib/kibana'; import * as alertsApi from '../../../detections/containers/detection_engine/alerts/api'; @@ -14,7 +15,6 @@ import * as buildAlertStatusFilterHelper from '../../../detections/components/al import { getExceptionListItemSchemaMock } from '../../../../../lists/common/schemas/response/exception_list_item_schema.mock'; import { getCreateExceptionListItemSchemaMock } from '../../../../../lists/common/schemas/request/create_exception_list_item_schema.mock'; import { getUpdateExceptionListItemSchemaMock } from '../../../../../lists/common/schemas/request/update_exception_list_item_schema.mock'; -import { createKibanaCoreStartMock } from '../../../common/mock/kibana_core'; import { ExceptionListItemSchema, CreateExceptionListItemSchema, @@ -27,7 +27,7 @@ import { AddOrUpdateExceptionItemsFunc, } from './use_add_exception'; -const mockKibanaHttpService = createKibanaCoreStartMock().http; +const mockKibanaHttpService = coreMock.createStart().http; const mockKibanaServices = KibanaServices.get as jest.Mock; jest.mock('../../../common/lib/kibana'); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/use_fetch_or_create_rule_exception_list.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/use_fetch_or_create_rule_exception_list.test.tsx index 6dbf5922e0a97..39d88bd8e4724 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/use_fetch_or_create_rule_exception_list.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/use_fetch_or_create_rule_exception_list.test.tsx @@ -6,11 +6,11 @@ import { act, renderHook, RenderHookResult } from '@testing-library/react-hooks'; +import { coreMock } from '../../../../../../../src/core/public/mocks'; import * as rulesApi from '../../../detections/containers/detection_engine/rules/api'; import * as listsApi from '../../../../../lists/public/exceptions/api'; import { getExceptionListSchemaMock } from '../../../../../lists/common/schemas/response/exception_list_schema.mock'; import { savedRuleMock } from '../../../detections/containers/detection_engine/rules/mock'; -import { createKibanaCoreStartMock } from '../../mock/kibana_core'; import { ExceptionListType } from '../../../lists_plugin_deps'; import { ListArray } from '../../../../common/detection_engine/schemas/types'; import { getListArrayMock } from '../../../../common/detection_engine/schemas/types/lists.mock'; @@ -20,7 +20,7 @@ import { ReturnUseFetchOrCreateRuleExceptionList, } from './use_fetch_or_create_rule_exception_list'; -const mockKibanaHttpService = createKibanaCoreStartMock().http; +const mockKibanaHttpService = coreMock.createStart().http; jest.mock('../../../detections/containers/detection_engine/rules/api'); describe('useFetchOrCreateRuleExceptionList', () => { diff --git a/x-pack/plugins/security_solution/public/common/components/ml/hooks/use_installed_security_jobs.ts b/x-pack/plugins/security_solution/public/common/components/ml/hooks/use_installed_security_jobs.ts index a9a728f81cc6c..dde5eebe624bc 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml/hooks/use_installed_security_jobs.ts +++ b/x-pack/plugins/security_solution/public/common/components/ml/hooks/use_installed_security_jobs.ts @@ -29,6 +29,9 @@ export interface UseInstalledSecurityJobsReturn { * Use the corresponding helper functions to filter the job list as * necessary (running jobs, etc). * + * NOTE: If you need to include jobs that are not currently installed, try the + * {@link useInstalledSecurityJobs} hook. + * */ export const useInstalledSecurityJobs = (): UseInstalledSecurityJobsReturn => { const [jobs, setJobs] = useState([]); diff --git a/x-pack/plugins/security_solution/public/common/components/ml_popover/hooks/use_security_jobs.ts b/x-pack/plugins/security_solution/public/common/components/ml_popover/hooks/use_security_jobs.ts index e8809e8366eed..2ba5cb84d272d 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml_popover/hooks/use_security_jobs.ts +++ b/x-pack/plugins/security_solution/public/common/components/ml_popover/hooks/use_security_jobs.ts @@ -32,6 +32,7 @@ export interface UseSecurityJobsReturn { * list as necessary. E.g. installed jobs, running jobs, etc. * * NOTE: If the user is not an ml admin, jobs will be empty and isMlAdmin will be false. + * If you only need installed jobs, try the {@link useInstalledSecurityJobs} hook. * * @param refetchData */ @@ -39,7 +40,7 @@ export const useSecurityJobs = (refetchData: boolean): UseSecurityJobsReturn => const [jobs, setJobs] = useState([]); const [loading, setLoading] = useState(true); const mlCapabilities = useMlCapabilities(); - const [siemDefaultIndex] = useUiSetting$(DEFAULT_INDEX_KEY); + const [securitySolutionDefaultIndex] = useUiSetting$(DEFAULT_INDEX_KEY); const http = useHttp(); const { addError } = useAppToasts(); @@ -54,12 +55,12 @@ export const useSecurityJobs = (refetchData: boolean): UseSecurityJobsReturn => async function fetchSecurityJobIdsFromGroupsData() { if (isMlAdmin && isLicensed) { try { - // Batch fetch all installed jobs, ML modules, and check which modules are compatible with siemDefaultIndex + // Batch fetch all installed jobs, ML modules, and check which modules are compatible with securitySolutionDefaultIndex const [jobSummaryData, modulesData, compatibleModules] = await Promise.all([ getJobsSummary({ http, signal: abortCtrl.signal }), getModules({ signal: abortCtrl.signal }), checkRecognizer({ - indexPatternName: siemDefaultIndex, + indexPatternName: securitySolutionDefaultIndex, signal: abortCtrl.signal, }), ]); @@ -89,7 +90,7 @@ export const useSecurityJobs = (refetchData: boolean): UseSecurityJobsReturn => isSubscribed = false; abortCtrl.abort(); }; - }, [refetchData, isMlAdmin, isLicensed, siemDefaultIndex, addError, http]); + }, [refetchData, isMlAdmin, isLicensed, securitySolutionDefaultIndex, addError, http]); return { isLicensed, isMlAdmin, jobs, loading }; }; diff --git a/x-pack/plugins/security_solution/public/common/components/ml_popover/types.ts b/x-pack/plugins/security_solution/public/common/components/ml_popover/types.ts index c839f5110fe7f..7120fcf4a9e55 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml_popover/types.ts +++ b/x-pack/plugins/security_solution/public/common/components/ml_popover/types.ts @@ -111,7 +111,7 @@ export interface CustomURL { } /** - * Representation of an ML Job as used by the SIEM App -- a composition of ModuleJob and MlSummaryJob + * Representation of an ML Job as used by the Security Solution App -- a composition of ModuleJob and MlSummaryJob * that includes necessary metadata like moduleName, defaultIndexPattern, etc. */ export interface SecurityJob extends MlSummaryJob { diff --git a/x-pack/plugins/security_solution/public/common/components/query_bar/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/query_bar/index.test.tsx index aac83ce650d86..aa61688f1f986 100644 --- a/x-pack/plugins/security_solution/public/common/components/query_bar/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/query_bar/index.test.tsx @@ -7,14 +7,13 @@ import { mount } from 'enzyme'; import React from 'react'; +import { coreMock } from '../../../../../../../src/core/public/mocks'; import { DEFAULT_FROM, DEFAULT_TO } from '../../../../common/constants'; import { TestProviders, mockIndexPattern } from '../../mock'; -import { createKibanaCoreStartMock } from '../../mock/kibana_core'; import { FilterManager, SearchBar } from '../../../../../../../src/plugins/data/public'; import { QueryBar, QueryBarComponentProps } from '.'; -import { createKibanaContextProviderMock } from '../../mock/kibana_react'; -const mockUiSettingsForFilterManager = createKibanaCoreStartMock().uiSettings; +const mockUiSettingsForFilterManager = coreMock.createStart().uiSettings; describe('QueryBar ', () => { // We are doing that because we need to wrapped this component with redux @@ -187,13 +186,9 @@ describe('QueryBar ', () => { describe('state', () => { test('clears draftQuery when filterQueryDraft has been cleared', () => { - const KibanaWithStorageProvider = createKibanaContextProviderMock(); - const Proxy = (props: QueryBarComponentProps) => ( - - - + ); @@ -231,13 +226,9 @@ describe('QueryBar ', () => { describe('#onQueryChange', () => { test(' is the only reference that changed when filterQueryDraft props get updated', () => { - const KibanaWithStorageProvider = createKibanaContextProviderMock(); - const Proxy = (props: QueryBarComponentProps) => ( - - - + ); @@ -382,24 +373,9 @@ describe('QueryBar ', () => { describe('SavedQueryManagementComponent state', () => { test('popover should hidden when "Save current query" button was clicked', () => { - const KibanaWithStorageProvider = createKibanaContextProviderMock(); - const Proxy = (props: QueryBarComponentProps) => ( - - - + ); diff --git a/x-pack/plugins/security_solution/public/common/components/super_date_picker/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/super_date_picker/index.test.tsx index 0795e46c9e45f..956ee4b05f9d6 100644 --- a/x-pack/plugins/security_solution/public/common/components/super_date_picker/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/super_date_picker/index.test.tsx @@ -17,7 +17,7 @@ import { kibanaObservable, createSecuritySolutionStorageMock, } from '../../mock'; -import { createUseUiSetting$Mock } from '../../mock/kibana_react'; +import { createUseUiSetting$Mock } from '../../lib/kibana/kibana_react.mock'; import { createStore, State } from '../../store'; import { SuperDatePicker, makeMapStateToProps } from '.'; diff --git a/x-pack/plugins/security_solution/public/common/components/top_n/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/top_n/index.test.tsx index 1e93fdb936728..31318122eb564 100644 --- a/x-pack/plugins/security_solution/public/common/components/top_n/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/top_n/index.test.tsx @@ -18,7 +18,6 @@ import { createSecuritySolutionStorageMock, mockIndexPattern, } from '../../mock'; -import { createKibanaCoreStartMock } from '../../mock/kibana_core'; import { FilterManager } from '../../../../../../../src/plugins/data/public'; import { createStore, State } from '../../store'; @@ -29,6 +28,7 @@ import { getTimelineDefaults, } from '../../../timelines/components/manage_timeline'; import { TimelineId } from '../../../../common/types/timeline'; +import { coreMock } from '../../../../../../../src/core/public/mocks'; jest.mock('react-router-dom', () => { const original = jest.requireActual('react-router-dom'); @@ -45,7 +45,7 @@ jest.mock('../link_to'); jest.mock('../../lib/kibana'); jest.mock('../../../timelines/store/timeline/actions'); -const mockUiSettingsForFilterManager = createKibanaCoreStartMock().uiSettings; +const mockUiSettingsForFilterManager = coreMock.createStart().uiSettings; const field = 'process.name'; const value = 'nice'; diff --git a/x-pack/plugins/security_solution/public/common/containers/local_storage/use_messages_storage.test.tsx b/x-pack/plugins/security_solution/public/common/containers/local_storage/use_messages_storage.test.tsx index 7085894e4a51c..58f5c1a9beb2e 100644 --- a/x-pack/plugins/security_solution/public/common/containers/local_storage/use_messages_storage.test.tsx +++ b/x-pack/plugins/security_solution/public/common/containers/local_storage/use_messages_storage.test.tsx @@ -6,17 +6,13 @@ import { renderHook, act } from '@testing-library/react-hooks'; import { useKibana } from '../../lib/kibana'; -import { createUseKibanaMock } from '../../mock/kibana_react'; import { useMessagesStorage, UseMessagesStorage } from './use_messages_storage'; jest.mock('../../lib/kibana'); -const useKibanaMock = useKibana as jest.Mock; describe('useLocalStorage', () => { beforeEach(() => { - const services = { ...createUseKibanaMock()().services }; - useKibanaMock.mockImplementation(() => ({ services })); - services.storage.store.clear(); + useKibana().services.storage.clear(); }); it('should return an empty array when there is no messages', async () => { diff --git a/x-pack/plugins/security_solution/public/common/lib/kibana/__mocks__/index.ts b/x-pack/plugins/security_solution/public/common/lib/kibana/__mocks__/index.ts index 5f4285f2747ae..573ef92f7e069 100644 --- a/x-pack/plugins/security_solution/public/common/lib/kibana/__mocks__/index.ts +++ b/x-pack/plugins/security_solution/public/common/lib/kibana/__mocks__/index.ts @@ -9,19 +9,21 @@ import { createKibanaContextProviderMock, createUseUiSettingMock, createUseUiSetting$Mock, - createUseKibanaMock, + createStartServicesMock, createWithKibanaMock, -} from '../../../mock/kibana_react'; +} from '../kibana_react.mock'; export const KibanaServices = { get: jest.fn(), getKibanaVersion: jest.fn(() => '8.0.0') }; -export const useKibana = jest.fn(createUseKibanaMock()); +export const useKibana = jest.fn().mockReturnValue({ services: createStartServicesMock() }); export const useUiSetting = jest.fn(createUseUiSettingMock()); export const useUiSetting$ = jest.fn(createUseUiSetting$Mock()); -export const useHttp = jest.fn(() => useKibana().services.http); +export const useHttp = jest.fn().mockReturnValue(createStartServicesMock().http); export const useTimeZone = jest.fn(); export const useDateFormat = jest.fn(); export const useBasePath = jest.fn(() => '/test/base/path'); -export const useToasts = jest.fn(() => notificationServiceMock.createStartContract().toasts); +export const useToasts = jest + .fn() + .mockReturnValue(notificationServiceMock.createStartContract().toasts); export const useCurrentUser = jest.fn(); export const withKibana = jest.fn(createWithKibanaMock()); export const KibanaContextProvider = jest.fn(createKibanaContextProviderMock()); diff --git a/x-pack/plugins/security_solution/public/common/lib/kibana/kibana_react.mock.ts b/x-pack/plugins/security_solution/public/common/lib/kibana/kibana_react.mock.ts new file mode 100644 index 0000000000000..c026b65853a4c --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/lib/kibana/kibana_react.mock.ts @@ -0,0 +1,103 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +/* eslint-disable react/display-name */ + +import React from 'react'; + +import { coreMock } from '../../../../../../../src/core/public/mocks'; +import { KibanaContextProvider } from '../../../../../../../src/plugins/kibana_react/public'; +import { dataPluginMock } from '../../../../../../../src/plugins/data/public/mocks'; +import { securityMock } from '../../../../../../plugins/security/public/mocks'; +import { + DEFAULT_APP_TIME_RANGE, + DEFAULT_APP_REFRESH_INTERVAL, + DEFAULT_INDEX_KEY, + DEFAULT_DATE_FORMAT, + DEFAULT_DATE_FORMAT_TZ, + DEFAULT_DARK_MODE, + DEFAULT_TIME_RANGE, + DEFAULT_REFRESH_RATE_INTERVAL, + DEFAULT_FROM, + DEFAULT_TO, + DEFAULT_INTERVAL_PAUSE, + DEFAULT_INTERVAL_VALUE, + DEFAULT_BYTES_FORMAT, + DEFAULT_INDEX_PATTERN, +} from '../../../../common/constants'; +import { StartServices } from '../../../types'; +import { createSecuritySolutionStorageMock } from '../../mock/mock_local_storage'; + +const mockUiSettings: Record = { + [DEFAULT_TIME_RANGE]: { from: 'now-15m', to: 'now', mode: 'quick' }, + [DEFAULT_REFRESH_RATE_INTERVAL]: { pause: false, value: 0 }, + [DEFAULT_APP_TIME_RANGE]: { + from: DEFAULT_FROM, + to: DEFAULT_TO, + }, + [DEFAULT_APP_REFRESH_INTERVAL]: { + pause: DEFAULT_INTERVAL_PAUSE, + value: DEFAULT_INTERVAL_VALUE, + }, + [DEFAULT_INDEX_KEY]: DEFAULT_INDEX_PATTERN, + [DEFAULT_BYTES_FORMAT]: '0,0.[0]b', + [DEFAULT_DATE_FORMAT_TZ]: 'UTC', + [DEFAULT_DATE_FORMAT]: 'MMM D, YYYY @ HH:mm:ss.SSS', + [DEFAULT_DARK_MODE]: false, +}; + +export const createUseUiSettingMock = () => (key: string, defaultValue?: unknown): unknown => { + const result = mockUiSettings[key]; + + if (typeof result != null) return result; + + if (defaultValue != null) { + return defaultValue; + } + + throw new TypeError(`Unexpected config key: ${key}`); +}; + +export const createUseUiSetting$Mock = () => { + const useUiSettingMock = createUseUiSettingMock(); + + return (key: string, defaultValue?: unknown): [unknown, () => void] | undefined => [ + useUiSettingMock(key, defaultValue), + jest.fn(), + ]; +}; + +export const createStartServicesMock = (): StartServices => { + const core = coreMock.createStart(); + core.uiSettings.get.mockImplementation(createUseUiSettingMock()); + const { storage } = createSecuritySolutionStorageMock(); + const data = dataPluginMock.createStartContract(); + const security = securityMock.createSetup(); + + const services = ({ + ...core, + data, + security, + storage, + } as unknown) as StartServices; + + return services; +}; + +export const createWithKibanaMock = () => { + const services = createStartServicesMock(); + + return (Component: unknown) => (props: unknown) => { + return React.createElement(Component as string, { ...(props as object), kibana: { services } }); + }; +}; + +export const createKibanaContextProviderMock = () => { + const services = createStartServicesMock(); + + return ({ children }: { children: React.ReactNode }) => + React.createElement(KibanaContextProvider, { services }, children); +}; diff --git a/x-pack/plugins/security_solution/public/common/mock/endpoint/app_context_render.tsx b/x-pack/plugins/security_solution/public/common/mock/endpoint/app_context_render.tsx index 1ed459521cc79..1b9e95f7d0737 100644 --- a/x-pack/plugins/security_solution/public/common/mock/endpoint/app_context_render.tsx +++ b/x-pack/plugins/security_solution/public/common/mock/endpoint/app_context_render.tsx @@ -17,7 +17,7 @@ import { apolloClientObservable, kibanaObservable } from '../test_providers'; import { createStore, State } from '../../store'; import { AppRootProvider } from './app_root_provider'; import { managementMiddlewareFactory } from '../../../management/store/middleware'; -import { createKibanaContextProviderMock } from '../kibana_react'; +import { createKibanaContextProviderMock } from '../../lib/kibana/kibana_react.mock'; import { SUB_PLUGINS_REDUCER, mockGlobalState, createSecuritySolutionStorageMock } from '..'; type UiRender = (ui: React.ReactElement, options?: RenderOptions) => RenderResult; diff --git a/x-pack/plugins/security_solution/public/common/mock/index.ts b/x-pack/plugins/security_solution/public/common/mock/index.ts index 678ad4d84b586..7e076772c42fb 100644 --- a/x-pack/plugins/security_solution/public/common/mock/index.ts +++ b/x-pack/plugins/security_solution/public/common/mock/index.ts @@ -16,4 +16,3 @@ export * from './test_providers'; export * from './utils'; export * from './mock_ecs'; export * from './timeline_results'; -export * from './kibana_react'; diff --git a/x-pack/plugins/security_solution/public/common/mock/kibana_core.ts b/x-pack/plugins/security_solution/public/common/mock/kibana_core.ts deleted file mode 100644 index f8eed75cf9bf1..0000000000000 --- a/x-pack/plugins/security_solution/public/common/mock/kibana_core.ts +++ /dev/null @@ -1,15 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { coreMock } from '../../../../../../src/core/public/mocks'; -import { dataPluginMock } from '../../../../../../src/plugins/data/public/mocks'; -import { securityMock } from '../../../../../plugins/security/public/mocks'; - -export const createKibanaCoreStartMock = () => coreMock.createStart(); -export const createKibanaPluginsStartMock = () => ({ - data: dataPluginMock.createStartContract(), - security: securityMock.createSetup(), -}); diff --git a/x-pack/plugins/security_solution/public/common/mock/kibana_react.ts b/x-pack/plugins/security_solution/public/common/mock/kibana_react.ts deleted file mode 100644 index bdb8ca85b0d77..0000000000000 --- a/x-pack/plugins/security_solution/public/common/mock/kibana_react.ts +++ /dev/null @@ -1,126 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -/* eslint-disable react/display-name */ - -import React from 'react'; -import { KibanaContextProvider } from '../../../../../../src/plugins/kibana_react/public'; - -import { - DEFAULT_APP_TIME_RANGE, - DEFAULT_APP_REFRESH_INTERVAL, - DEFAULT_INDEX_KEY, - DEFAULT_DATE_FORMAT, - DEFAULT_DATE_FORMAT_TZ, - DEFAULT_DARK_MODE, - DEFAULT_TIME_RANGE, - DEFAULT_REFRESH_RATE_INTERVAL, - DEFAULT_FROM, - DEFAULT_TO, - DEFAULT_INTERVAL_PAUSE, - DEFAULT_INTERVAL_VALUE, - DEFAULT_BYTES_FORMAT, - DEFAULT_INDEX_PATTERN, -} from '../../../common/constants'; -import { createKibanaCoreStartMock, createKibanaPluginsStartMock } from './kibana_core'; -import { StartServices } from '../../types'; -import { createSecuritySolutionStorageMock } from './mock_local_storage'; - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export const mockUiSettings: Record = { - [DEFAULT_TIME_RANGE]: { from: 'now-15m', to: 'now', mode: 'quick' }, - [DEFAULT_REFRESH_RATE_INTERVAL]: { pause: false, value: 0 }, - [DEFAULT_APP_TIME_RANGE]: { - from: DEFAULT_FROM, - to: DEFAULT_TO, - }, - [DEFAULT_APP_REFRESH_INTERVAL]: { - pause: DEFAULT_INTERVAL_PAUSE, - value: DEFAULT_INTERVAL_VALUE, - }, - [DEFAULT_INDEX_KEY]: DEFAULT_INDEX_PATTERN, - [DEFAULT_BYTES_FORMAT]: '0,0.[0]b', - [DEFAULT_DATE_FORMAT_TZ]: 'UTC', - [DEFAULT_DATE_FORMAT]: 'MMM D, YYYY @ HH:mm:ss.SSS', - [DEFAULT_DARK_MODE]: false, -}; - -export const createUseUiSettingMock = () => ( - key: string, - defaultValue?: T -): T => { - const result = mockUiSettings[key]; - - if (typeof result != null) return result; - - if (defaultValue != null) { - return defaultValue; - } - - throw new Error(`Unexpected config key: ${key}`); -}; - -export const createUseUiSetting$Mock = () => { - const useUiSettingMock = createUseUiSettingMock(); - - return ( - key: string, - defaultValue?: T - ): [T, () => void] | undefined => [useUiSettingMock(key, defaultValue), jest.fn()]; -}; - -export const createKibanaObservable$Mock = createKibanaCoreStartMock; - -export const createUseKibanaMock = () => { - const core = createKibanaCoreStartMock(); - const plugins = createKibanaPluginsStartMock(); - const useUiSetting = createUseUiSettingMock(); - const { storage } = createSecuritySolutionStorageMock(); - - const services = { - ...core, - ...plugins, - uiSettings: { - ...core.uiSettings, - get: useUiSetting, - }, - storage, - }; - - return () => ({ services }); -}; - -export const createStartServices = () => { - const core = createKibanaCoreStartMock(); - const plugins = createKibanaPluginsStartMock(); - - const services = ({ - ...core, - ...plugins, - } as unknown) as StartServices; - - return services; -}; - -export const createWithKibanaMock = () => { - const kibana = createUseKibanaMock()(); - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return (Component: any) => (props: any) => { - return React.createElement(Component, { ...props, kibana }); - }; -}; - -export const createKibanaContextProviderMock = () => { - const kibana = createUseKibanaMock()(); - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return ({ services, ...rest }: any) => - React.createElement(KibanaContextProvider, { - ...rest, - services: { ...kibana.services, ...services }, - }); -}; diff --git a/x-pack/plugins/security_solution/public/common/mock/test_providers.tsx b/x-pack/plugins/security_solution/public/common/mock/test_providers.tsx index 010d2fac18af5..9ead8171bfef6 100644 --- a/x-pack/plugins/security_solution/public/common/mock/test_providers.tsx +++ b/x-pack/plugins/security_solution/public/common/mock/test_providers.tsx @@ -19,7 +19,10 @@ import { ThemeProvider } from 'styled-components'; import { createStore, State } from '../store'; import { mockGlobalState } from './global_state'; -import { createKibanaContextProviderMock, createStartServices } from './kibana_react'; +import { + createKibanaContextProviderMock, + createStartServicesMock, +} from '../lib/kibana/kibana_react.mock'; import { FieldHook, useForm } from '../../shared_imports'; import { SUB_PLUGINS_REDUCER } from './utils'; import { createSecuritySolutionStorageMock, localStorageMock } from './mock_local_storage'; @@ -38,7 +41,7 @@ export const apolloClient = new ApolloClient({ }); export const apolloClientObservable = new BehaviorSubject(apolloClient); -export const kibanaObservable = new BehaviorSubject(createStartServices()); +export const kibanaObservable = new BehaviorSubject(createStartServicesMock()); Object.defineProperty(window, 'localStorage', { value: localStorageMock(), diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/lists/__mocks__/use_lists_config.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/lists/__mocks__/use_lists_config.tsx index 0f8e0fba1e3af..291587e9f69c5 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/lists/__mocks__/use_lists_config.tsx +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/lists/__mocks__/use_lists_config.tsx @@ -4,4 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -export const useListsConfig = jest.fn().mockReturnValue({}); +import { getUseListsConfigMock } from '../use_lists_config.mock'; + +export const useListsConfig = jest.fn(getUseListsConfigMock); diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/lists/use_lists_config.mock.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/lists/use_lists_config.mock.ts new file mode 100644 index 0000000000000..90f47972a3a2e --- /dev/null +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/lists/use_lists_config.mock.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { UseListsConfigReturn } from './use_lists_config'; + +export const getUseListsConfigMock: () => jest.Mocked = () => ({ + canManageIndex: null, + canWriteIndex: null, + enabled: true, + loading: false, + needsConfiguration: false, +}); diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/lists/use_lists_config.test.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/lists/use_lists_config.test.tsx new file mode 100644 index 0000000000000..a5ff29e2091b0 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/lists/use_lists_config.test.tsx @@ -0,0 +1,86 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { renderHook } from '@testing-library/react-hooks'; + +import { useKibana } from '../../../../common/lib/kibana'; +import { useListsIndex } from './use_lists_index'; +import { useListsPrivileges } from './use_lists_privileges'; +import { getUseListsIndexMock } from './use_lists_index.mock'; +import { getUseListsPrivilegesMock } from './use_lists_privileges.mock'; +import { useListsConfig } from './use_lists_config'; + +jest.mock('../../../../common/lib/kibana'); +jest.mock('./use_lists_index'); +jest.mock('./use_lists_privileges'); + +describe('useListsConfig', () => { + let listsIndexMock: ReturnType; + let listsPrivilegesMock: ReturnType; + + beforeEach(() => { + listsIndexMock = getUseListsIndexMock(); + listsPrivilegesMock = getUseListsPrivilegesMock(); + (useListsIndex as jest.Mock).mockReturnValue(listsIndexMock); + (useListsPrivileges as jest.Mock).mockReturnValue(listsPrivilegesMock); + }); + + it("returns the user's write permissions", () => { + listsPrivilegesMock.canWriteIndex = false; + const { result } = renderHook(() => useListsConfig()); + expect(result.current.canWriteIndex).toEqual(false); + + listsPrivilegesMock.canWriteIndex = true; + const { result: result2 } = renderHook(() => useListsConfig()); + expect(result2.current.canWriteIndex).toEqual(true); + }); + + describe('when lists are disabled', () => { + beforeEach(() => { + useKibana().services.lists = undefined; + }); + + it('indicates that lists are not enabled, and need configuration', () => { + const { result } = renderHook(() => useListsConfig()); + expect(result.current.enabled).toEqual(false); + expect(result.current.needsConfiguration).toEqual(true); + }); + }); + + describe('when lists are enabled but indexes do not exist', () => { + beforeEach(() => { + useKibana().services.lists = {}; + listsIndexMock.indexExists = false; + }); + + it('needs configuration if the user cannot manage indexes', () => { + listsPrivilegesMock.canManageIndex = false; + + const { result } = renderHook(() => useListsConfig()); + expect(result.current.needsConfiguration).toEqual(true); + expect(listsIndexMock.createIndex).not.toHaveBeenCalled(); + }); + + it('attempts to create the indexes if the user can manage indexes', () => { + listsPrivilegesMock.canManageIndex = true; + + renderHook(() => useListsConfig()); + expect(listsIndexMock.createIndex).toHaveBeenCalled(); + }); + }); + + describe('when lists are enabled and indexes exist', () => { + beforeEach(() => { + useKibana().services.lists = {}; + listsIndexMock.indexExists = true; + }); + + it('does not need configuration', () => { + const { result } = renderHook(() => useListsConfig()); + expect(result.current.needsConfiguration).toEqual(false); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/lists/use_lists_index.mock.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/lists/use_lists_index.mock.ts new file mode 100644 index 0000000000000..e2169442d80e6 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/lists/use_lists_index.mock.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { UseListsIndexReturn } from './use_lists_index'; + +export const getUseListsIndexMock: () => jest.Mocked = () => ({ + createIndex: jest.fn(), + indexExists: null, + error: null, + loading: false, +}); diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/lists/use_lists_privileges.mock.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/lists/use_lists_privileges.mock.ts new file mode 100644 index 0000000000000..4f583a72460e2 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/lists/use_lists_privileges.mock.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { UseListsPrivilegesReturn } from './use_lists_privileges'; + +export const getUseListsPrivilegesMock: () => jest.Mocked = () => ({ + isAuthenticated: null, + canManageIndex: null, + canWriteIndex: null, + loading: false, +}); diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/index.test.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/index.test.tsx index b07caa754aec9..9f486dc11e99d 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/index.test.tsx @@ -9,7 +9,6 @@ import { shallow, mount } from 'enzyme'; import { act } from 'react-dom/test-utils'; import '../../../../../common/mock/match_media'; -import { createKibanaContextProviderMock } from '../../../../../common/mock/kibana_react'; import { TestProviders } from '../../../../../common/mock'; // we don't have the types for waitFor just yet, so using "as waitFor" until when we do import { wait as waitFor } from '@testing-library/react'; @@ -182,23 +181,20 @@ describe('AllRules', () => { }); it('renders rules tab', async () => { - const KibanaContext = createKibanaContextProviderMock(); const wrapper = mount( - - - + ); @@ -211,24 +207,20 @@ describe('AllRules', () => { }); it('renders monitoring tab when monitoring tab clicked', async () => { - const KibanaContext = createKibanaContextProviderMock(); - const wrapper = mount( - - - + ); const monitoringTab = wrapper.find('[data-test-subj="allRulesTableTab-monitoring"] button'); diff --git a/x-pack/plugins/security_solution/public/overview/components/recent_cases/no_cases/index.test.tsx b/x-pack/plugins/security_solution/public/overview/components/recent_cases/no_cases/index.test.tsx index 99902a31975d0..446679ae26d9e 100644 --- a/x-pack/plugins/security_solution/public/overview/components/recent_cases/no_cases/index.test.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/recent_cases/no_cases/index.test.tsx @@ -9,29 +9,19 @@ import { mount } from 'enzyme'; import { useKibana } from '../../../../common/lib/kibana'; import '../../../../common/mock/match_media'; -import { createUseKibanaMock, TestProviders } from '../../../../common/mock'; +import { TestProviders } from '../../../../common/mock'; import { NoCases } from '.'; jest.mock('../../../../common/lib/kibana'); -const useKibanaMock = useKibana as jest.Mock; - -let navigateToApp: jest.Mock; +const useKibanaMock = useKibana as jest.Mocked; describe('RecentCases', () => { + let navigateToApp: jest.Mock; + beforeEach(() => { - jest.resetAllMocks(); navigateToApp = jest.fn(); - const kibanaMock = createUseKibanaMock()(); - useKibanaMock.mockReturnValue({ - ...kibanaMock, - services: { - application: { - navigateToApp, - getUrlForApp: jest.fn(), - }, - }, - }); + useKibanaMock().services.application.navigateToApp = navigateToApp; }); it('if no cases, you should be able to create a case by clicking on the link "start a new case"', () => { diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/data_providers.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/data_providers.test.tsx index 754d7f9c47edf..d48be25b08897 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/data_providers.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/data_providers.test.tsx @@ -15,8 +15,9 @@ import { DataProvider } from './data_provider'; import { mockDataProviders } from './mock/mock_data_providers'; import { ManageGlobalTimeline, getTimelineDefaults } from '../../manage_timeline'; import { FilterManager } from '../../../../../../../../src/plugins/data/public/query/filter_manager'; -import { createKibanaCoreStartMock } from '../../../../common/mock/kibana_core'; -const mockUiSettingsForFilterManager = createKibanaCoreStartMock().uiSettings; +import { coreMock } from '../../../../../../../../src/core/public/mocks'; + +const mockUiSettingsForFilterManager = coreMock.createStart().uiSettings; const filterManager = new FilterManager(mockUiSettingsForFilterManager); describe('DataProviders', () => { diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/providers.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/providers.test.tsx index b788f70cb2e4a..3f371349aa750 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/providers.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/providers.test.tsx @@ -7,7 +7,7 @@ import { shallow } from 'enzyme'; import React from 'react'; -import { createKibanaCoreStartMock } from '../../../../common/mock/kibana_core'; +import { coreMock } from '../../../../../../../../src/core/public/mocks'; import { TestProviders } from '../../../../common/mock/test_providers'; import { DroppableWrapper } from '../../../../common/components/drag_and_drop/droppable_wrapper'; import { FilterManager } from '../../../../../../../../src/plugins/data/public'; @@ -18,7 +18,7 @@ import { DELETE_CLASS_NAME, ENABLE_CLASS_NAME, EXCLUDE_CLASS_NAME } from './prov import { useMountAppended } from '../../../../common/utils/use_mount_appended'; import { ManageGlobalTimeline, getTimelineDefaults } from '../../manage_timeline'; -const mockUiSettingsForFilterManager = createKibanaCoreStartMock().uiSettings; +const mockUiSettingsForFilterManager = coreMock.createStart().uiSettings; describe('Providers', () => { const isLoading: boolean = true; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/header/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/header/index.test.tsx index e7b0ce7b7428e..329bcf24ba7ed 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/header/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/header/index.test.tsx @@ -7,8 +7,8 @@ import { shallow } from 'enzyme'; import React from 'react'; +import { coreMock } from '../../../../../../../../src/core/public/mocks'; import { mockIndexPattern } from '../../../../common/mock'; -import { createKibanaCoreStartMock } from '../../../../common/mock/kibana_core'; import { TestProviders } from '../../../../common/mock/test_providers'; import { FilterManager } from '../../../../../../../../src/plugins/data/public'; import { mockDataProviders } from '../data_providers/mock/mock_data_providers'; @@ -17,7 +17,7 @@ import { useMountAppended } from '../../../../common/utils/use_mount_appended'; import { TimelineHeader } from '.'; import { TimelineStatus, TimelineType } from '../../../../../common/types/timeline'; -const mockUiSettingsForFilterManager = createKibanaCoreStartMock().uiSettings; +const mockUiSettingsForFilterManager = coreMock.createStart().uiSettings; jest.mock('../../../../common/lib/kibana'); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/query_bar/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/query_bar/index.test.tsx index 75f684c629c70..6c8fd4975c657 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/query_bar/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/query_bar/index.test.tsx @@ -7,11 +7,11 @@ import { mount } from 'enzyme'; import React from 'react'; +import { coreMock } from '../../../../../../../../src/core/public/mocks'; import { DEFAULT_FROM, DEFAULT_TO } from '../../../../../common/constants'; import { mockBrowserFields } from '../../../../common/containers/source/mock'; import { convertKueryToElasticSearchQuery } from '../../../../common/lib/keury'; import { mockIndexPattern, TestProviders } from '../../../../common/mock'; -import { createKibanaCoreStartMock } from '../../../../common/mock/kibana_core'; import { QueryBar } from '../../../../common/components/query_bar'; import { FilterManager } from '../../../../../../../../src/plugins/data/public'; import { mockDataProviders } from '../data_providers/mock/mock_data_providers'; @@ -19,7 +19,7 @@ import { buildGlobalQuery } from '../helpers'; import { QueryBarTimeline, QueryBarTimelineComponentProps, getDataProviderFilter } from './index'; -const mockUiSettingsForFilterManager = createKibanaCoreStartMock().uiSettings; +const mockUiSettingsForFilterManager = coreMock.createStart().uiSettings; jest.mock('../../../../common/lib/kibana'); diff --git a/x-pack/plugins/security_solution/public/timelines/containers/local_storage/index.test.ts b/x-pack/plugins/security_solution/public/timelines/containers/local_storage/index.test.ts index e1bccbdff4889..7a8750b279b85 100644 --- a/x-pack/plugins/security_solution/public/timelines/containers/local_storage/index.test.ts +++ b/x-pack/plugins/security_solution/public/timelines/containers/local_storage/index.test.ts @@ -15,23 +15,16 @@ import { import { TimelineId } from '../../../../common/types/timeline'; import { mockTimelineModel, createSecuritySolutionStorageMock } from '../../../common/mock'; import { useKibana } from '../../../common/lib/kibana'; -import { createUseKibanaMock } from '../../../common/mock/kibana_react'; jest.mock('../../../common/lib/kibana'); -const useKibanaMock = useKibana as jest.Mock; +const useKibanaMock = useKibana as jest.Mocked; describe('SiemLocalStorage', () => { const { localStorage, storage } = createSecuritySolutionStorageMock(); beforeEach(() => { - jest.resetAllMocks(); - useKibanaMock.mockImplementation(() => ({ - services: { - ...createUseKibanaMock()().services, - storage, - }, - })); + useKibanaMock().services.storage = storage; localStorage.clear(); }); From 6dbc2f7fd150052c468c1eec99475f04f250bc97 Mon Sep 17 00:00:00 2001 From: spalger Date: Mon, 24 Aug 2020 13:43:09 -0700 Subject: [PATCH 18/71] skip flaky suite (#75699) --- test/functional/apps/visualize/_vega_chart.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/functional/apps/visualize/_vega_chart.ts b/test/functional/apps/visualize/_vega_chart.ts index b59d9590bb62a..f599afa3afc32 100644 --- a/test/functional/apps/visualize/_vega_chart.ts +++ b/test/functional/apps/visualize/_vega_chart.ts @@ -50,7 +50,8 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { const retry = getService('retry'); const browser = getService('browser'); - describe('vega chart in visualize app', () => { + // FLAKY: https://github.com/elastic/kibana/issues/75699 + describe.skip('vega chart in visualize app', () => { before(async () => { log.debug('navigateToApp visualize'); await PageObjects.visualize.navigateToNewVisualization(); From 1fbb6e57a1d38eb376b22bcc0082805fd0671e0a Mon Sep 17 00:00:00 2001 From: spalger Date: Mon, 24 Aug 2020 13:46:58 -0700 Subject: [PATCH 19/71] skip flaky suite (#75697) --- .../cypress/integration/url_compatibility.spec.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/x-pack/plugins/security_solution/cypress/integration/url_compatibility.spec.ts b/x-pack/plugins/security_solution/cypress/integration/url_compatibility.spec.ts index 7146cf70dc8c8..d55a8faae021d 100644 --- a/x-pack/plugins/security_solution/cypress/integration/url_compatibility.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/url_compatibility.spec.ts @@ -18,6 +18,7 @@ const ABSOLUTE_DATE = { startTime: '2019-08-01T20:03:29.186Z', }; +// FLAKY: https://github.com/elastic/kibana/issues/75697 describe.skip('URL compatibility', () => { it('Redirects to Detection alerts from old Detections URL', () => { loginAndWaitForPage(DETECTIONS); From 637e87d0fbf9990cefe80aea055af2b377d2fc5c Mon Sep 17 00:00:00 2001 From: spalger Date: Mon, 24 Aug 2020 13:52:31 -0700 Subject: [PATCH 20/71] skip flaky suite (#75794) --- .../cypress/integration/timeline_local_storage.spec.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/security_solution/cypress/integration/timeline_local_storage.spec.ts b/x-pack/plugins/security_solution/cypress/integration/timeline_local_storage.spec.ts index 383ebe2220585..c2ff2c58687f3 100644 --- a/x-pack/plugins/security_solution/cypress/integration/timeline_local_storage.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/timeline_local_storage.spec.ts @@ -13,7 +13,8 @@ import { TABLE_COLUMN_EVENTS_MESSAGE } from '../screens/hosts/external_events'; import { waitsForEventsToBeLoaded, openEventsViewerFieldsBrowser } from '../tasks/hosts/events'; import { removeColumn, resetFields } from '../tasks/timeline'; -describe('persistent timeline', () => { +// FLAKY: https://github.com/elastic/kibana/issues/75794 +describe.skip('persistent timeline', () => { before(() => { loginAndWaitForPage(HOSTS_URL); openEvents(); From 0e3ba45ea93e231490429bbd1ebcc4ccba078e5a Mon Sep 17 00:00:00 2001 From: Caroline Horn <549577+cchaos@users.noreply.github.com> Date: Mon, 24 Aug 2020 17:03:25 -0400 Subject: [PATCH 21/71] Update CODEOWNERS for design (again) (#75801) --- .github/CODEOWNERS | 43 ++++++++++++++++++++++++++----------------- 1 file changed, 26 insertions(+), 17 deletions(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 52df586b8bda7..66fb31cc91d5a 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -8,7 +8,6 @@ /x-pack/plugins/lens/ @elastic/kibana-app /x-pack/plugins/graph/ @elastic/kibana-app /src/plugins/dashboard/ @elastic/kibana-app -/src/plugins/dashboard/**/*.scss @elastic/kibana-core-ui-designers /src/plugins/discover/ @elastic/kibana-app /src/plugins/input_control_vis/ @elastic/kibana-app /src/plugins/kibana_legacy/ @elastic/kibana-app @@ -59,7 +58,6 @@ # APM /x-pack/plugins/apm/ @elastic/apm-ui -/x-pack/plugins/apm/**/*.scss @elastic/observability-design /x-pack/test/functional/apps/apm/ @elastic/apm-ui /src/legacy/core_plugins/apm_oss/ @elastic/apm-ui /src/plugins/apm_oss/ @elastic/apm-ui @@ -70,7 +68,6 @@ # Canvas /x-pack/plugins/canvas/ @elastic/kibana-canvas -/x-pack/plugins/canvas/**/*.scss @elastic/kibana-core-ui-designers /x-pack/test/functional/apps/canvas/ @elastic/kibana-canvas # Core UI @@ -80,18 +77,14 @@ /src/plugins/home/server/services/ @elastic/kibana-core-ui # Exclude tutorial resources folder for now because they are not owned by Kibana app and most will move out soon /src/legacy/core_plugins/kibana/public/home/*.ts @elastic/kibana-core-ui -/src/legacy/core_plugins/kibana/public/home/**/*.scss @elastic/kibana-core-ui-designers /src/legacy/core_plugins/kibana/public/home/np_ready/ @elastic/kibana-core-ui # Observability UIs /x-pack/legacy/plugins/infra/ @elastic/logs-metrics-ui /x-pack/plugins/infra/ @elastic/logs-metrics-ui -/x-pack/plugins/infra/**/*.scss @elastic/observability-design /x-pack/plugins/ingest_manager/ @elastic/ingest-management -/x-pack/plugins/ingest_manager/**/*.scss @elastic/observability-design /x-pack/legacy/plugins/ingest_manager/ @elastic/ingest-management /x-pack/plugins/observability/ @elastic/observability-ui -/x-pack/plugins/observability/**/*.scss @elastic/observability-design /x-pack/legacy/plugins/monitoring/ @elastic/stack-monitoring-ui /x-pack/plugins/monitoring/ @elastic/stack-monitoring-ui /x-pack/plugins/uptime @elastic/uptime @@ -165,14 +158,10 @@ # Security /src/core/server/csp/ @elastic/kibana-security @elastic/kibana-platform /x-pack/legacy/plugins/security/ @elastic/kibana-security -/x-pack/legacy/plugins/security/**/*.scss @elastic/kibana-core-ui-designers /x-pack/legacy/plugins/spaces/ @elastic/kibana-security -/x-pack/legacy/plugins/spaces/**/*.scss @elastic/kibana-core-ui-designers /x-pack/plugins/spaces/ @elastic/kibana-security -/x-pack/plugins/spaces/**/*.scss @elastic/kibana-core-ui-designers /x-pack/plugins/encrypted_saved_objects/ @elastic/kibana-security /x-pack/plugins/security/ @elastic/kibana-security -/x-pack/plugins/security/**/*.scss @elastic/kibana-core-ui-designers /x-pack/test/api_integration/apis/security/ @elastic/kibana-security /x-pack/test/encrypted_saved_objects_api_integration/ @elastic/kibana-security /x-pack/test/functional/apps/security/ @elastic/kibana-security @@ -220,13 +209,9 @@ x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json @elastic/kib /x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/ @elastic/kibana-alerting-services /x-pack/test/functional_with_es_ssl/fixtures/plugins/alerts/ @elastic/kibana-alerting-services -# Design -**/*.scss @elastic/kibana-design - # Enterprise Search /x-pack/plugins/enterprise_search/ @elastic/app-search-frontend @elastic/workplace-search-frontend /x-pack/test/functional_enterprise_search/ @elastic/app-search-frontend @elastic/workplace-search-frontend -/x-pack/plugins/enterprise_search/**/*.scss @elastic/ent-search-design # Elasticsearch UI /src/plugins/dev_tools/ @elastic/es-ui @@ -255,7 +240,6 @@ x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json @elastic/kib # Endpoint /x-pack/plugins/endpoint/ @elastic/endpoint-app-team @elastic/siem -/x-pack/plugins/endpoint/**/*.scss @elastic/security-design /x-pack/test/api_integration/apis/endpoint/ @elastic/endpoint-app-team @elastic/siem /x-pack/test/endpoint_api_integration_no_ingest/ @elastic/endpoint-app-team @elastic/siem /x-pack/test/security_solution_endpoint/ @elastic/endpoint-app-team @elastic/siem @@ -265,7 +249,6 @@ x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json @elastic/kib # Security Solution /x-pack/plugins/security_solution/ @elastic/siem @elastic/endpoint-app-team -/x-pack/plugins/security_solution/**/*.scss @elastic/security-design /x-pack/test/detection_engine_api_integration @elastic/siem @elastic/endpoint-app-team /x-pack/test/lists_api_integration @elastic/siem @elastic/endpoint-app-team /x-pack/test/api_integration/apis/security_solution @elastic/siem @elastic/endpoint-app-team @@ -274,3 +257,29 @@ x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json @elastic/kib # Security Intelligence And Analytics /x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules @elastic/security-intelligence-analytics + +# Design (at the bottom for specificity of SASS files) +**/*.scss @elastic/kibana-design + +# Core design +/src/plugins/dashboard/**/*.scss @elastic/kibana-core-ui-designers +/x-pack/plugins/canvas/**/*.scss @elastic/kibana-core-ui-designers +/src/legacy/core_plugins/kibana/public/home/**/*.scss @elastic/kibana-core-ui-designers +/x-pack/legacy/plugins/security/**/*.scss @elastic/kibana-core-ui-designers +/x-pack/legacy/plugins/spaces/**/*.scss @elastic/kibana-core-ui-designers +/x-pack/plugins/spaces/**/*.scss @elastic/kibana-core-ui-designers +/x-pack/plugins/security/**/*.scss @elastic/kibana-core-ui-designers + +# Observability design +/x-pack/plugins/apm/**/*.scss @elastic/observability-design +/x-pack/plugins/infra/**/*.scss @elastic/observability-design +/x-pack/plugins/ingest_manager/**/*.scss @elastic/observability-design +/x-pack/plugins/observability/**/*.scss @elastic/observability-design + +# Ent. Search design +/x-pack/plugins/enterprise_search/**/*.scss @elastic/ent-search-design + +# Security design +/x-pack/plugins/endpoint/**/*.scss @elastic/security-design +/x-pack/plugins/security_solution/**/*.scss @elastic/security-design + From 3256992b351560ea4de9dd924dcec9c2cd10598f Mon Sep 17 00:00:00 2001 From: Catherine Liu Date: Mon, 24 Aug 2020 14:28:50 -0700 Subject: [PATCH 22/71] [Canvas] Adds function reference docs generator (#49402) Co-authored-by: Corey Robertson --- .../canvas/canvas-function-reference.asciidoc | 14 +- .../canvas_plugin_src/functions/common/if.ts | 3 +- .../canvas_plugin_src/functions/common/neq.ts | 3 + .../functions/common/switch.ts | 1 + .../functions/common/tail.ts | 1 + .../i18n/functions/dict/alter_column.ts | 6 +- .../plugins/canvas/i18n/functions/dict/as.ts | 2 +- .../canvas/i18n/functions/dict/axis_config.ts | 8 +- .../canvas/i18n/functions/dict/case.ts | 4 +- .../canvas/i18n/functions/dict/compare.ts | 30 +- .../i18n/functions/dict/container_style.ts | 2 +- .../canvas/i18n/functions/dict/date.ts | 3 +- .../i18n/functions/dict/dropdown_control.ts | 4 +- .../plugins/canvas/i18n/functions/dict/eq.ts | 2 +- .../canvas/i18n/functions/dict/filterrows.ts | 2 +- .../canvas/i18n/functions/dict/formatdate.ts | 2 +- .../i18n/functions/dict/formatnumber.ts | 6 +- .../canvas/i18n/functions/dict/get_cell.ts | 2 +- .../canvas/i18n/functions/dict/head.ts | 2 +- .../plugins/canvas/i18n/functions/dict/if.ts | 2 +- .../canvas/i18n/functions/dict/join_rows.ts | 10 +- .../canvas/i18n/functions/dict/location.ts | 3 +- .../canvas/i18n/functions/dict/map_center.ts | 4 +- .../canvas/i18n/functions/dict/map_column.ts | 6 +- .../canvas/i18n/functions/dict/markdown.ts | 6 +- .../canvas/i18n/functions/dict/math.ts | 5 +- .../canvas/i18n/functions/dict/metric.ts | 4 +- .../plugins/canvas/i18n/functions/dict/pie.ts | 12 +- .../canvas/i18n/functions/dict/plot.ts | 12 +- .../plugins/canvas/i18n/functions/dict/ply.ts | 4 +- .../canvas/i18n/functions/dict/pointseries.ts | 4 +- .../canvas/i18n/functions/dict/progress.ts | 2 +- .../canvas/i18n/functions/dict/render.ts | 2 +- .../i18n/functions/dict/repeat_image.ts | 2 +- .../canvas/i18n/functions/dict/replace.ts | 2 +- .../i18n/functions/dict/reveal_image.ts | 2 +- .../canvas/i18n/functions/dict/rounddate.ts | 2 +- .../canvas/i18n/functions/dict/row_count.ts | 2 +- .../canvas/i18n/functions/dict/saved_lens.ts | 6 +- .../canvas/i18n/functions/dict/saved_map.ts | 4 +- .../functions/dict/saved_visualization.ts | 8 +- .../i18n/functions/dict/series_style.ts | 2 +- .../canvas/i18n/functions/dict/shape.ts | 2 +- .../canvas/i18n/functions/dict/sort.ts | 9 +- .../i18n/functions/dict/static_column.ts | 6 +- .../canvas/i18n/functions/dict/switch.ts | 4 +- .../canvas/i18n/functions/dict/table.ts | 4 +- .../canvas/i18n/functions/dict/time_range.ts | 2 +- .../canvas/i18n/functions/dict/timefilter.ts | 2 +- .../canvas/i18n/functions/dict/timelion.ts | 2 +- .../plugins/canvas/i18n/functions/dict/to.ts | 3 +- .../canvas/i18n/functions/dict/urlparam.ts | 6 +- x-pack/plugins/canvas/public/application.tsx | 6 +- .../function_examples.ts | 444 ++++++++++++++++++ .../function_reference_generator.tsx | 36 ++ .../generate_function_reference.ts | 259 ++++++++++ .../function_reference_generator/index.ts | 7 + .../public/components/help_menu/help_menu.js | 45 -- .../public/components/help_menu/help_menu.tsx | 59 +++ .../help_menu/{index.js => index.ts} | 0 .../translations/translations/ja-JP.json | 8 - .../translations/translations/zh-CN.json | 8 - 62 files changed, 935 insertions(+), 180 deletions(-) create mode 100644 x-pack/plugins/canvas/public/components/function_reference_generator/function_examples.ts create mode 100644 x-pack/plugins/canvas/public/components/function_reference_generator/function_reference_generator.tsx create mode 100644 x-pack/plugins/canvas/public/components/function_reference_generator/generate_function_reference.ts create mode 100644 x-pack/plugins/canvas/public/components/function_reference_generator/index.ts delete mode 100644 x-pack/plugins/canvas/public/components/help_menu/help_menu.js create mode 100644 x-pack/plugins/canvas/public/components/help_menu/help_menu.tsx rename x-pack/plugins/canvas/public/components/help_menu/{index.js => index.ts} (100%) diff --git a/docs/canvas/canvas-function-reference.asciidoc b/docs/canvas/canvas-function-reference.asciidoc index 657e3ec8b8bb1..3ae513708f189 100644 --- a/docs/canvas/canvas-function-reference.asciidoc +++ b/docs/canvas/canvas-function-reference.asciidoc @@ -897,7 +897,7 @@ Default: `"-_index:.kibana"` |`string` |An index or index pattern. For example, `"logstash-*"`. -Default: `_all` +Default: `"_all"` |=== *Returns:* `number` @@ -965,7 +965,7 @@ Default: `1000` |`string` |An index or index pattern. For example, `"logstash-*"`. -Default: `_all` +Default: `"_all"` |`metaFields` |`string` @@ -1026,7 +1026,7 @@ Alias: `tz` |`string` |The timezone to use for date operations. Valid ISO8601 formats and UTC offsets both work. -Default: `UTC` +Default: `"UTC"` |=== *Returns:* `datatable` @@ -1238,7 +1238,7 @@ filters |`string` |The horizontal text alignment. -Default: `left` +Default: `"left"` |`color` |`string` @@ -1280,7 +1280,7 @@ Default: `false` |`string` |The font weight. For example, `"normal"`, `"bold"`, `"bolder"`, `"lighter"`, `"100"`, `"200"`, `"300"`, `"400"`, `"500"`, `"600"`, `"700"`, `"800"`, or `"900"`. -Default: `normal` +Default: `"normal"` |=== *Returns:* `style` @@ -2469,7 +2469,7 @@ Alias: `shape` |`string` |Pick a shape. -Default: `square` +Default: `"square"` |`border` @@ -2732,7 +2732,7 @@ Aliases: `c`, `field` |`string` |The column or field that you want to filter. -Default: `@timestamp` +Default: `"@timestamp"` |`compact` |`boolean` diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/if.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/if.ts index 6b9464843fca4..9cbd5ed3ee68a 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/if.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/if.ts @@ -20,9 +20,10 @@ export function ifFn(): ExpressionFunctionDefinition<'if', unknown, Arguments, u help, args: { condition: { - types: ['boolean', 'null'], + types: ['boolean'], aliases: ['_'], help: argHelp.condition, + required: true, }, then: { resolve: false, diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/neq.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/neq.ts index 4066a35ea41f2..c4ba5771408a5 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/neq.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/neq.ts @@ -20,6 +20,9 @@ export function neq(): ExpressionFunctionDefinition<'neq', Input, Arguments, boo name: 'neq', type: 'boolean', help, + context: { + types: ['boolean', 'number', 'string', 'null'], + }, args: { value: { aliases: ['_'], diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/switch.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/switch.ts index bb70bec561a11..453beb4c1106b 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/switch.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/switch.ts @@ -25,6 +25,7 @@ export function switchFn(): ExpressionFunctionDefinition<'switch', unknown, Argu aliases: ['_'], resolve: false, multi: true, + required: true, help: argHelp.case, }, default: { diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/tail.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/tail.ts index 5105beb586f72..568e67db7f86c 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/tail.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/tail.ts @@ -26,6 +26,7 @@ export function tail(): ExpressionFunctionDefinition<'tail', Datatable, Argument aliases: ['_'], types: ['number'], help: argHelp.count, + default: 1, }, }, fn: (input, args) => ({ diff --git a/x-pack/plugins/canvas/i18n/functions/dict/alter_column.ts b/x-pack/plugins/canvas/i18n/functions/dict/alter_column.ts index f201e73d717eb..5f206399b42da 100644 --- a/x-pack/plugins/canvas/i18n/functions/dict/alter_column.ts +++ b/x-pack/plugins/canvas/i18n/functions/dict/alter_column.ts @@ -13,14 +13,14 @@ import { DATATABLE_COLUMN_TYPES } from '../../../common/lib/constants'; export const help: FunctionHelp> = { help: i18n.translate('xpack.canvas.functions.alterColumnHelpText', { defaultMessage: - 'Converts between core types, including {list}, and {end}, and rename columns. ' + + 'Converts between core types, including {list}, and {end}, and renames columns. ' + 'See also {mapColumnFn} and {staticColumnFn}.', values: { list: Object.values(DATATABLE_COLUMN_TYPES) .slice(0, -1) .map((type) => `\`${type}\``) .join(', '), - end: Object.values(DATATABLE_COLUMN_TYPES).slice(-1)[0], + end: `\`${Object.values(DATATABLE_COLUMN_TYPES).slice(-1)[0]}\``, mapColumnFn: '`mapColumn`', staticColumnFn: '`staticColumn`', }, @@ -33,7 +33,7 @@ export const help: FunctionHelp> = { defaultMessage: 'The resultant column name. Leave blank to not rename.', }), type: i18n.translate('xpack.canvas.functions.alterColumn.args.typeHelpText', { - defaultMessage: 'The type to convert the column to. Leave blank to not change type.', + defaultMessage: 'The type to convert the column to. Leave blank to not change the type.', }), }, }; diff --git a/x-pack/plugins/canvas/i18n/functions/dict/as.ts b/x-pack/plugins/canvas/i18n/functions/dict/as.ts index e95aa641c71b8..6154159d5e452 100644 --- a/x-pack/plugins/canvas/i18n/functions/dict/as.ts +++ b/x-pack/plugins/canvas/i18n/functions/dict/as.ts @@ -20,7 +20,7 @@ export const help: FunctionHelp> = { }), args: { name: i18n.translate('xpack.canvas.functions.as.args.nameHelpText', { - defaultMessage: 'A name to give the column.', + defaultMessage: 'The name to give the column.', }), }, }; diff --git a/x-pack/plugins/canvas/i18n/functions/dict/axis_config.ts b/x-pack/plugins/canvas/i18n/functions/dict/axis_config.ts index 7cf0ec6c58761..bedd677209baa 100644 --- a/x-pack/plugins/canvas/i18n/functions/dict/axis_config.ts +++ b/x-pack/plugins/canvas/i18n/functions/dict/axis_config.ts @@ -21,14 +21,14 @@ export const help: FunctionHelp> = { args: { max: i18n.translate('xpack.canvas.functions.axisConfig.args.maxHelpText', { defaultMessage: - 'The maximum value displayed in the axis. Must be a number or a date in milliseconds since epoch or {ISO8601} string.', + 'The maximum value displayed in the axis. Must be a number, a date in milliseconds since epoch, or an {ISO8601} string.', values: { ISO8601, }, }), min: i18n.translate('xpack.canvas.functions.axisConfig.args.minHelpText', { defaultMessage: - 'The minimum value displayed in the axis. Must be a number or a date in milliseconds since epoch or {ISO8601} string.', + 'The minimum value displayed in the axis. Must be a number, a date in milliseconds since epoch, or an {ISO8601} string.', values: { ISO8601, }, @@ -40,14 +40,14 @@ export const help: FunctionHelp> = { .slice(0, -1) .map((position) => `\`"${position}"\``) .join(', '), - end: Object.values(Position).slice(-1)[0], + end: `\`"${Object.values(Position).slice(-1)[0]}"\``, }, }), show: i18n.translate('xpack.canvas.functions.axisConfig.args.showHelpText', { defaultMessage: 'Show the axis labels?', }), tickSize: i18n.translate('xpack.canvas.functions.axisConfig.args.tickSizeHelpText', { - defaultMessage: 'The increment size between each tick. Use for `number` axes only', + defaultMessage: 'The increment size between each tick. Use for `number` axes only.', }), }, }; diff --git a/x-pack/plugins/canvas/i18n/functions/dict/case.ts b/x-pack/plugins/canvas/i18n/functions/dict/case.ts index 8f0689e5e3837..884542420999c 100644 --- a/x-pack/plugins/canvas/i18n/functions/dict/case.ts +++ b/x-pack/plugins/canvas/i18n/functions/dict/case.ts @@ -34,14 +34,14 @@ export const help: FunctionHelp> = { }), if: i18n.translate('xpack.canvas.functions.case.args.ifHelpText', { defaultMessage: - 'This value indicates whether the condition is met, usually using a sub-expression. The {IF_ARG} argument overrides the {WHEN_ARG} argument when both are provided.', + 'This value indicates whether the condition is met. The {IF_ARG} argument overrides the {WHEN_ARG} argument when both are provided.', values: { IF_ARG, WHEN_ARG, }, }), then: i18n.translate('xpack.canvas.functions.case.args.thenHelpText', { - defaultMessage: 'The value to return if the condition is met.', + defaultMessage: 'The value returned if the condition is met.', }), }, }; diff --git a/x-pack/plugins/canvas/i18n/functions/dict/compare.ts b/x-pack/plugins/canvas/i18n/functions/dict/compare.ts index 5697881503b84..cb57fde0cfcb6 100644 --- a/x-pack/plugins/canvas/i18n/functions/dict/compare.ts +++ b/x-pack/plugins/canvas/i18n/functions/dict/compare.ts @@ -22,20 +22,20 @@ export const help: FunctionHelp> = { help: i18n.translate('xpack.canvas.functions.compareHelpText', { defaultMessage: 'Compares the {CONTEXT} to specified value to determine {BOOLEAN_TRUE} or {BOOLEAN_FALSE}. Usually used in combination with `{ifFn}` or `{caseFn}`. ' + - 'This only works with primitive types, such as {examples}. See also `{eqFn}`, `{gtFn}`, `{gteFn}`, `{ltFn}`, `{lteFn}`, `{neqFn}`', + 'This only works with primitive types, such as {examples}. See also {eqFn}, {gtFn}, {gteFn}, {ltFn}, {lteFn}, {neqFn}', values: { CONTEXT, BOOLEAN_TRUE, BOOLEAN_FALSE, - ifFn: 'if', + ifFn: '`if`', caseFn: 'case', examples: [TYPE_NUMBER, TYPE_STRING, TYPE_BOOLEAN, TYPE_NULL].join(', '), - eqFn: 'eq', - gtFn: 'gt', - gteFn: 'gte', - ltFn: 'lt', - lteFn: 'lte', - neqFn: 'neq', + eqFn: '`eq`', + gtFn: '`gt`', + gteFn: '`gte`', + ltFn: '`lt`', + lteFn: '`lte`', + neqFn: '`neq`', }, }), args: { @@ -44,13 +44,13 @@ export const help: FunctionHelp> = { 'The operator to use in the comparison: {eq} (equal to), {gt} (greater than), {gte} (greater than or equal to)' + ', {lt} (less than), {lte} (less than or equal to), {ne} or {neq} (not equal to).', values: { - eq: Operation.EQ, - gt: Operation.GT, - gte: Operation.GTE, - lt: Operation.LT, - lte: Operation.LTE, - ne: Operation.NE, - neq: Operation.NEQ, + eq: `\`"${Operation.EQ}"\``, + gt: `\`"${Operation.GT}"\``, + gte: `\`"${Operation.GTE}"\``, + lt: `\`"${Operation.LT}"\``, + lte: `\`"${Operation.LTE}"\``, + ne: `\`"${Operation.NE}"\``, + neq: `\`"${Operation.NEQ}"\``, }, }), to: i18n.translate('xpack.canvas.functions.compare.args.toHelpText', { diff --git a/x-pack/plugins/canvas/i18n/functions/dict/container_style.ts b/x-pack/plugins/canvas/i18n/functions/dict/container_style.ts index bef2ccc2a8e3b..f51206d7990a9 100644 --- a/x-pack/plugins/canvas/i18n/functions/dict/container_style.ts +++ b/x-pack/plugins/canvas/i18n/functions/dict/container_style.ts @@ -74,7 +74,7 @@ export const help: FunctionHelp> = { }, }), padding: i18n.translate('xpack.canvas.functions.containerStyle.args.paddingHelpText', { - defaultMessage: 'The distance of the content, in pixels, from border.', + defaultMessage: 'The distance of the content, in pixels, from the border.', }), }, }; diff --git a/x-pack/plugins/canvas/i18n/functions/dict/date.ts b/x-pack/plugins/canvas/i18n/functions/dict/date.ts index 6964b62bcc582..1ccab1eb775af 100644 --- a/x-pack/plugins/canvas/i18n/functions/dict/date.ts +++ b/x-pack/plugins/canvas/i18n/functions/dict/date.ts @@ -29,7 +29,8 @@ export const help: FunctionHelp> = { }, }), format: i18n.translate('xpack.canvas.functions.date.args.formatHelpText', { - defaultMessage: 'The {MOMENTJS} format used to parse the specified date string. See {url}.', + defaultMessage: + 'The {MOMENTJS} format used to parse the specified date string. For more information, see {url}.', values: { MOMENTJS, url: 'https://momentjs.com/docs/#/displaying/', diff --git a/x-pack/plugins/canvas/i18n/functions/dict/dropdown_control.ts b/x-pack/plugins/canvas/i18n/functions/dict/dropdown_control.ts index 0d051a4f5f068..312e0e795ed0b 100644 --- a/x-pack/plugins/canvas/i18n/functions/dict/dropdown_control.ts +++ b/x-pack/plugins/canvas/i18n/functions/dict/dropdown_control.ts @@ -11,7 +11,7 @@ import { FunctionFactory } from '../../../types'; export const help: FunctionHelp> = { help: i18n.translate('xpack.canvas.functions.dropdownControlHelpText', { - defaultMessage: 'Configures a drop-down filter control element.', + defaultMessage: 'Configures a dropdown filter control element.', }), args: { filterColumn: i18n.translate( @@ -22,7 +22,7 @@ export const help: FunctionHelp> = { ), valueColumn: i18n.translate('xpack.canvas.functions.dropdownControl.args.valueColumnHelpText', { defaultMessage: - 'The column or field from which to extract the unique values for the drop-down control.', + 'The column or field from which to extract the unique values for the dropdown control.', }), filterGroup: i18n.translate('xpack.canvas.functions.dropdownControl.args.filterGroupHelpText', { defaultMessage: 'The group name for the filter.', diff --git a/x-pack/plugins/canvas/i18n/functions/dict/eq.ts b/x-pack/plugins/canvas/i18n/functions/dict/eq.ts index a856a81452cd7..23f74068afa74 100644 --- a/x-pack/plugins/canvas/i18n/functions/dict/eq.ts +++ b/x-pack/plugins/canvas/i18n/functions/dict/eq.ts @@ -12,7 +12,7 @@ import { CONTEXT } from '../../constants'; export const help: FunctionHelp> = { help: i18n.translate('xpack.canvas.functions.eqHelpText', { - defaultMessage: 'Return whether the {CONTEXT} is equal to the argument.', + defaultMessage: 'Returns whether the {CONTEXT} is equal to the argument.', values: { CONTEXT, }, diff --git a/x-pack/plugins/canvas/i18n/functions/dict/filterrows.ts b/x-pack/plugins/canvas/i18n/functions/dict/filterrows.ts index 3c1b6d87a9be5..26f1cab51b459 100644 --- a/x-pack/plugins/canvas/i18n/functions/dict/filterrows.ts +++ b/x-pack/plugins/canvas/i18n/functions/dict/filterrows.ts @@ -12,7 +12,7 @@ import { DATATABLE, TYPE_BOOLEAN, BOOLEAN_TRUE, BOOLEAN_FALSE } from '../../cons export const help: FunctionHelp> = { help: i18n.translate('xpack.canvas.functions.filterrowsHelpText', { - defaultMessage: 'Filter rows in a {DATATABLE} based on the return value of a sub-expression.', + defaultMessage: 'Filters rows in a {DATATABLE} based on the return value of a sub-expression.', values: { DATATABLE, }, diff --git a/x-pack/plugins/canvas/i18n/functions/dict/formatdate.ts b/x-pack/plugins/canvas/i18n/functions/dict/formatdate.ts index 9b60c2f69f120..385403ce75573 100644 --- a/x-pack/plugins/canvas/i18n/functions/dict/formatdate.ts +++ b/x-pack/plugins/canvas/i18n/functions/dict/formatdate.ts @@ -25,7 +25,7 @@ export const help: FunctionHelp> = { defaultMessage: 'A {MOMENTJS} format. For example, {example}. See {url}.', values: { MOMENTJS, - example: `"MM/DD/YYYY"`, + example: '`"MM/DD/YYYY"`', url: 'https://momentjs.com/docs/#/displaying/', }, }), diff --git a/x-pack/plugins/canvas/i18n/functions/dict/formatnumber.ts b/x-pack/plugins/canvas/i18n/functions/dict/formatnumber.ts index f3e8a8858fc36..3dfcf3a9e476f 100644 --- a/x-pack/plugins/canvas/i18n/functions/dict/formatnumber.ts +++ b/x-pack/plugins/canvas/i18n/functions/dict/formatnumber.ts @@ -12,7 +12,7 @@ import { NUMERALJS } from '../../constants'; export const help: FunctionHelp> = { help: i18n.translate('xpack.canvas.functions.formatnumberHelpText', { - defaultMessage: 'Formats a number into a formatted number string using {NUMERALJS}.', + defaultMessage: 'Formats a number into a formatted number string using the {NUMERALJS}.', values: { NUMERALJS, }, @@ -22,8 +22,8 @@ export const help: FunctionHelp> = { format: i18n.translate('xpack.canvas.functions.formatnumber.args.formatHelpText', { defaultMessage: 'A {NUMERALJS} format string. For example, {example1} or {example2}.', values: { - example1: `"0.0a"`, - example2: `"0%"`, + example1: '`"0.0a"`', + example2: '`"0%"`', NUMERALJS, }, }), diff --git a/x-pack/plugins/canvas/i18n/functions/dict/get_cell.ts b/x-pack/plugins/canvas/i18n/functions/dict/get_cell.ts index 79cc4f7e5c303..1cd4cd054d5d9 100644 --- a/x-pack/plugins/canvas/i18n/functions/dict/get_cell.ts +++ b/x-pack/plugins/canvas/i18n/functions/dict/get_cell.ts @@ -12,7 +12,7 @@ import { DATATABLE } from '../../constants'; export const help: FunctionHelp> = { help: i18n.translate('xpack.canvas.functions.getCellHelpText', { - defaultMessage: 'Fetchs a single cell from a {DATATABLE}.', + defaultMessage: 'Fetches a single cell from a {DATATABLE}.', values: { DATATABLE, }, diff --git a/x-pack/plugins/canvas/i18n/functions/dict/head.ts b/x-pack/plugins/canvas/i18n/functions/dict/head.ts index 4c61339c29c28..8aef4afd63ef6 100644 --- a/x-pack/plugins/canvas/i18n/functions/dict/head.ts +++ b/x-pack/plugins/canvas/i18n/functions/dict/head.ts @@ -12,7 +12,7 @@ import { DATATABLE } from '../../constants'; export const help: FunctionHelp> = { help: i18n.translate('xpack.canvas.functions.headHelpText', { - defaultMessage: 'Retrieves the first {n} rows from the {DATATABLE}. See also {tailFn}', + defaultMessage: 'Retrieves the first {n} rows from the {DATATABLE}. See also {tailFn}.', values: { n: 'N', DATATABLE, diff --git a/x-pack/plugins/canvas/i18n/functions/dict/if.ts b/x-pack/plugins/canvas/i18n/functions/dict/if.ts index 9cac3d10b2834..5f840fad91e5c 100644 --- a/x-pack/plugins/canvas/i18n/functions/dict/if.ts +++ b/x-pack/plugins/canvas/i18n/functions/dict/if.ts @@ -12,7 +12,7 @@ import { BOOLEAN_TRUE, BOOLEAN_FALSE, CONTEXT } from '../../constants'; export const help: FunctionHelp> = { help: i18n.translate('xpack.canvas.functions.ifHelpText', { - defaultMessage: 'Perform conditional logic', + defaultMessage: 'Performs conditional logic.', }), args: { condition: i18n.translate('xpack.canvas.functions.if.args.conditionHelpText', { diff --git a/x-pack/plugins/canvas/i18n/functions/dict/join_rows.ts b/x-pack/plugins/canvas/i18n/functions/dict/join_rows.ts index 59684f7cf1cd8..36293d41a5279 100644 --- a/x-pack/plugins/canvas/i18n/functions/dict/join_rows.ts +++ b/x-pack/plugins/canvas/i18n/functions/dict/join_rows.ts @@ -11,20 +11,20 @@ import { FunctionFactory } from '../../../types'; export const help: FunctionHelp> = { help: i18n.translate('xpack.canvas.functions.joinRowsHelpText', { - defaultMessage: 'Join values from rows in a datatable into a string', + defaultMessage: 'Concatenates values from rows in a `datatable` into a single string.', }), args: { column: i18n.translate('xpack.canvas.functions.joinRows.args.columnHelpText', { - defaultMessage: 'The column to join values from', + defaultMessage: 'The column or field from which to extract the values.', }), separator: i18n.translate('xpack.canvas.functions.joinRows.args.separatorHelpText', { - defaultMessage: 'The separator to use between row values', + defaultMessage: 'The delimiter to insert between each extracted value.', }), quote: i18n.translate('xpack.canvas.functions.joinRows.args.quoteHelpText', { - defaultMessage: 'The quote character around values', + defaultMessage: 'The quote character to wrap around each extracted value.', }), distinct: i18n.translate('xpack.canvas.functions.joinRows.args.distinctHelpText', { - defaultMessage: 'Removes duplicate values?', + defaultMessage: 'Extract only unique values?', }), }, }; diff --git a/x-pack/plugins/canvas/i18n/functions/dict/location.ts b/x-pack/plugins/canvas/i18n/functions/dict/location.ts index 7c0497da8361d..3bd98914ecb11 100644 --- a/x-pack/plugins/canvas/i18n/functions/dict/location.ts +++ b/x-pack/plugins/canvas/i18n/functions/dict/location.ts @@ -14,10 +14,11 @@ export const help: FunctionHelp> = { defaultMessage: 'Find your current location using the {geolocationAPI} of the browser. ' + 'Performance can vary, but is fairly accurate. ' + - 'See {url}.', + 'See {url}. Don’t use {locationFn} if you plan to generate PDFs as this function requires user input.', values: { geolocationAPI: 'Geolocation API', url: 'https://developer.mozilla.org/en-US/docs/Web/API/Navigator/geolocation', + locationFn: '`location`', }, }), args: {}, diff --git a/x-pack/plugins/canvas/i18n/functions/dict/map_center.ts b/x-pack/plugins/canvas/i18n/functions/dict/map_center.ts index 3022ad07089d2..5409808752687 100644 --- a/x-pack/plugins/canvas/i18n/functions/dict/map_center.ts +++ b/x-pack/plugins/canvas/i18n/functions/dict/map_center.ts @@ -11,7 +11,7 @@ import { FunctionFactory } from '../../../types'; export const help: FunctionHelp> = { help: i18n.translate('xpack.canvas.functions.mapCenterHelpText', { - defaultMessage: `Returns an object with the center coordinates and zoom level of the map`, + defaultMessage: `Returns an object with the center coordinates and zoom level of the map.`, }), args: { lat: i18n.translate('xpack.canvas.functions.mapCenter.args.latHelpText', { @@ -21,7 +21,7 @@ export const help: FunctionHelp> = { defaultMessage: `Longitude for the center of the map`, }), zoom: i18n.translate('xpack.canvas.functions.savedMap.args.zoomHelpText', { - defaultMessage: `The zoom level of the map`, + defaultMessage: `Zoom level of the map`, }), }, }; diff --git a/x-pack/plugins/canvas/i18n/functions/dict/map_column.ts b/x-pack/plugins/canvas/i18n/functions/dict/map_column.ts index 589dd9b1dad87..2666a08999fb8 100644 --- a/x-pack/plugins/canvas/i18n/functions/dict/map_column.ts +++ b/x-pack/plugins/canvas/i18n/functions/dict/map_column.ts @@ -14,10 +14,10 @@ export const help: FunctionHelp> = { help: i18n.translate('xpack.canvas.functions.mapColumnHelpText', { defaultMessage: 'Adds a column calculated as the result of other columns. ' + - 'Changes are made only when you provide arguments. ' + - 'See also {mapColumnFn} and {staticColumnFn}.', + 'Changes are made only when you provide arguments.' + + 'See also {alterColumnFn} and {staticColumnFn}.', values: { - mapColumnFn: '`mapColumn`', + alterColumnFn: '`alterColumn`', staticColumnFn: '`staticColumn`', }, }), diff --git a/x-pack/plugins/canvas/i18n/functions/dict/markdown.ts b/x-pack/plugins/canvas/i18n/functions/dict/markdown.ts index aa2845ba4ec3a..093bdaecccb35 100644 --- a/x-pack/plugins/canvas/i18n/functions/dict/markdown.ts +++ b/x-pack/plugins/canvas/i18n/functions/dict/markdown.ts @@ -33,13 +33,13 @@ export const help: FunctionHelp> = { 'The {CSS} font properties for the content. For example, {fontFamily} or {fontWeight}.', values: { CSS, - fontFamily: 'font-family', - fontWeight: 'font-weight', + fontFamily: '"font-family"', + fontWeight: '"font-weight"', }, }), openLinksInNewTab: i18n.translate('xpack.canvas.functions.markdown.args.openLinkHelpText', { defaultMessage: - 'A true/false value for opening links in a new tab. Default value is false. Setting to true will open all links in a new tab.', + 'A true or false value for opening links in a new tab. The default value is `false`. Setting to `true` opens all links in a new tab.', }), }, }; diff --git a/x-pack/plugins/canvas/i18n/functions/dict/math.ts b/x-pack/plugins/canvas/i18n/functions/dict/math.ts index 752009fb9c320..4469c629fa6fd 100644 --- a/x-pack/plugins/canvas/i18n/functions/dict/math.ts +++ b/x-pack/plugins/canvas/i18n/functions/dict/math.ts @@ -8,12 +8,12 @@ import { i18n } from '@kbn/i18n'; import { math } from '../../../canvas_plugin_src/functions/common/math'; import { FunctionHelp } from '../function_help'; import { FunctionFactory } from '../../../types'; -import { DATATABLE, CONTEXT, TINYMATH, TINYMATH_URL } from '../../constants'; +import { DATATABLE, CONTEXT, TINYMATH, TINYMATH_URL, TYPE_NUMBER } from '../../constants'; export const help: FunctionHelp> = { help: i18n.translate('xpack.canvas.functions.mathHelpText', { defaultMessage: - 'Interprets a {TINYMATH} math expression using a number or {DATATABLE} as {CONTEXT}. ' + + 'Interprets a {TINYMATH} math expression using a {TYPE_NUMBER} or {DATATABLE} as {CONTEXT}. ' + 'The {DATATABLE} columns are available by their column name. ' + 'If the {CONTEXT} is a number it is available as {value}.', values: { @@ -21,6 +21,7 @@ export const help: FunctionHelp> = { CONTEXT, DATATABLE, value: '`value`', + TYPE_NUMBER, }, }), args: { diff --git a/x-pack/plugins/canvas/i18n/functions/dict/metric.ts b/x-pack/plugins/canvas/i18n/functions/dict/metric.ts index 8258226e5dfc3..f84456b03a86e 100644 --- a/x-pack/plugins/canvas/i18n/functions/dict/metric.ts +++ b/x-pack/plugins/canvas/i18n/functions/dict/metric.ts @@ -40,8 +40,8 @@ export const help: FunctionHelp> = { metricFormat: i18n.translate('xpack.canvas.functions.metric.args.metricFormatHelpText', { defaultMessage: 'A {NUMERALJS} format string. For example, {example1} or {example2}.', values: { - example1: `"0.0a"`, - example2: `"0%"`, + example1: '`"0.0a"`', + example2: '`"0%"`', NUMERALJS, }, }), diff --git a/x-pack/plugins/canvas/i18n/functions/dict/pie.ts b/x-pack/plugins/canvas/i18n/functions/dict/pie.ts index 2e4bfc88a273a..149c2f8f1e634 100644 --- a/x-pack/plugins/canvas/i18n/functions/dict/pie.ts +++ b/x-pack/plugins/canvas/i18n/functions/dict/pie.ts @@ -8,12 +8,12 @@ import { i18n } from '@kbn/i18n'; import { pie } from '../../../canvas_plugin_src/functions/common/pie'; import { FunctionHelp } from '../function_help'; import { FunctionFactory } from '../../../types'; -import { Position } from '../../../types'; +import { Legend } from '../../../types'; import { CSS, FONT_FAMILY, FONT_WEIGHT, BOOLEAN_FALSE } from '../../constants'; export const help: FunctionHelp> = { help: i18n.translate('xpack.canvas.functions.pieHelpText', { - defaultMessage: 'Configure a pie chart element.', + defaultMessage: 'Configures a pie chart element.', }), args: { font: i18n.translate('xpack.canvas.functions.pie.args.fontHelpText', { @@ -38,20 +38,18 @@ export const help: FunctionHelp> = { }), legend: i18n.translate('xpack.canvas.functions.pie.args.legendHelpText', { defaultMessage: - 'The legend position. For example, {positions}, or {BOOLEAN_FALSE}. When {BOOLEAN_FALSE}, the legend is hidden.', + 'The legend position. For example, {legend}, or {BOOLEAN_FALSE}. When {BOOLEAN_FALSE}, the legend is hidden.', values: { - positions: Object.values(Position) + legend: Object.values(Legend) .map((position) => `\`"${position}"\``) .join(', '), BOOLEAN_FALSE, }, }), palette: i18n.translate('xpack.canvas.functions.pie.args.paletteHelpText', { - defaultMessage: - 'A {palette} object for describing the colors to use in this pie chart. See {paletteFn}.', + defaultMessage: 'A {palette} object for describing the colors to use in this pie chart.', values: { palette: '`palette`', - paletteFn: '`palette`', }, }), radius: i18n.translate('xpack.canvas.functions.pie.args.radiusHelpText', { diff --git a/x-pack/plugins/canvas/i18n/functions/dict/plot.ts b/x-pack/plugins/canvas/i18n/functions/dict/plot.ts index 068156f14c91b..aca2476a6592e 100644 --- a/x-pack/plugins/canvas/i18n/functions/dict/plot.ts +++ b/x-pack/plugins/canvas/i18n/functions/dict/plot.ts @@ -8,12 +8,12 @@ import { i18n } from '@kbn/i18n'; import { plot } from '../../../canvas_plugin_src/functions/common/plot'; import { FunctionHelp } from '../function_help'; import { FunctionFactory } from '../../../types'; -import { Position } from '../../../types'; +import { Legend } from '../../../types'; import { CSS, FONT_FAMILY, FONT_WEIGHT, BOOLEAN_FALSE } from '../../constants'; export const help: FunctionHelp> = { help: i18n.translate('xpack.canvas.functions.plotHelpText', { - defaultMessage: 'Configure a chart element', + defaultMessage: 'Configures a chart element.', }), args: { defaultStyle: i18n.translate('xpack.canvas.functions.plot.args.defaultStyleHelpText', { @@ -30,20 +30,18 @@ export const help: FunctionHelp> = { }), legend: i18n.translate('xpack.canvas.functions.plot.args.legendHelpText', { defaultMessage: - 'The legend position. For example, {positions}, or {BOOLEAN_FALSE}. When {BOOLEAN_FALSE}, the legend is hidden.', + 'The legend position. For example, {legend}, or {BOOLEAN_FALSE}. When {BOOLEAN_FALSE}, the legend is hidden.', values: { - positions: Object.values(Position) + legend: Object.values(Legend) .map((position) => `\`"${position}"\``) .join(', '), BOOLEAN_FALSE, }, }), palette: i18n.translate('xpack.canvas.functions.plot.args.paletteHelpText', { - defaultMessage: - 'A {palette} object for describing the colors to use in this chart. See {paletteFn}.', + defaultMessage: 'A {palette} object for describing the colors to use in this chart.', values: { palette: '`palette`', - paletteFn: '`palette`', }, }), seriesStyle: i18n.translate('xpack.canvas.functions.plot.args.seriesStyleHelpText', { diff --git a/x-pack/plugins/canvas/i18n/functions/dict/ply.ts b/x-pack/plugins/canvas/i18n/functions/dict/ply.ts index f341965aaa8b2..3bb9c1b3e46a3 100644 --- a/x-pack/plugins/canvas/i18n/functions/dict/ply.ts +++ b/x-pack/plugins/canvas/i18n/functions/dict/ply.ts @@ -13,8 +13,8 @@ import { DATATABLE } from '../../constants'; export const help: FunctionHelp> = { help: i18n.translate('xpack.canvas.functions.plyHelpText', { defaultMessage: - 'Subdivides a {DATATABLE} by the unique values of the specified column, ' + - 'and passes the resulting tables into an expression, then merges the outputs of each expression', + 'Subdivides a {DATATABLE} by the unique values of the specified columns, ' + + 'and passes the resulting tables into an expression, then merges the outputs of each expression.', values: { DATATABLE, }, diff --git a/x-pack/plugins/canvas/i18n/functions/dict/pointseries.ts b/x-pack/plugins/canvas/i18n/functions/dict/pointseries.ts index 1e7c67bb750e3..2579db77ff1b9 100644 --- a/x-pack/plugins/canvas/i18n/functions/dict/pointseries.ts +++ b/x-pack/plugins/canvas/i18n/functions/dict/pointseries.ts @@ -35,10 +35,10 @@ export const help: FunctionHelp> = { defaultMessage: 'The text to show on the mark. Only applicable to supported elements.', }), x: i18n.translate('xpack.canvas.functions.pointseries.args.xHelpText', { - defaultMessage: 'The values along the x-axis.', + defaultMessage: 'The values along the X-axis.', }), y: i18n.translate('xpack.canvas.functions.pointseries.args.yHelpText', { - defaultMessage: 'The values along the y-axis.', + defaultMessage: 'The values along the Y-axis.', }), }, }; diff --git a/x-pack/plugins/canvas/i18n/functions/dict/progress.ts b/x-pack/plugins/canvas/i18n/functions/dict/progress.ts index 1880c5dc807f0..199d5d926f277 100644 --- a/x-pack/plugins/canvas/i18n/functions/dict/progress.ts +++ b/x-pack/plugins/canvas/i18n/functions/dict/progress.ts @@ -34,7 +34,7 @@ export const help: FunctionHelp> = { }), label: i18n.translate('xpack.canvas.functions.progress.args.labelHelpText', { defaultMessage: - 'To show or hide labels, use {BOOLEAN_TRUE} or {BOOLEAN_FALSE}. Alternatively, provide a string to display as a label.', + 'To show or hide the label, use {BOOLEAN_TRUE} or {BOOLEAN_FALSE}. Alternatively, provide a string to display as a label.', values: { BOOLEAN_TRUE, BOOLEAN_FALSE, diff --git a/x-pack/plugins/canvas/i18n/functions/dict/render.ts b/x-pack/plugins/canvas/i18n/functions/dict/render.ts index bf0a5a50b8726..7ddb04de490e5 100644 --- a/x-pack/plugins/canvas/i18n/functions/dict/render.ts +++ b/x-pack/plugins/canvas/i18n/functions/dict/render.ts @@ -13,7 +13,7 @@ import { CONTEXT, CSS } from '../../constants'; export const help: FunctionHelp> = { help: i18n.translate('xpack.canvas.functions.renderHelpText', { defaultMessage: - 'Render the {CONTEXT} as a specific element and sets element level options, such as background and border styling.', + 'Renders the {CONTEXT} as a specific element and sets element level options, such as background and border styling.', values: { CONTEXT, }, diff --git a/x-pack/plugins/canvas/i18n/functions/dict/repeat_image.ts b/x-pack/plugins/canvas/i18n/functions/dict/repeat_image.ts index 222947779a758..4de92b0552bf5 100644 --- a/x-pack/plugins/canvas/i18n/functions/dict/repeat_image.ts +++ b/x-pack/plugins/canvas/i18n/functions/dict/repeat_image.ts @@ -17,7 +17,7 @@ export const help: FunctionHelp> = { args: { emptyImage: i18n.translate('xpack.canvas.functions.repeatImage.args.emptyImageHelpText', { defaultMessage: - 'Fills the difference between the {CONTEXT} and {maxArg} parameter for the element with this image' + + 'Fills the difference between the {CONTEXT} and {maxArg} parameter for the element with this image. ' + 'Provide an image asset as a {BASE64} data {URL}, or pass in a sub-expression.', values: { BASE64, diff --git a/x-pack/plugins/canvas/i18n/functions/dict/replace.ts b/x-pack/plugins/canvas/i18n/functions/dict/replace.ts index 085f42b439c46..e99c9740c57d6 100644 --- a/x-pack/plugins/canvas/i18n/functions/dict/replace.ts +++ b/x-pack/plugins/canvas/i18n/functions/dict/replace.ts @@ -12,7 +12,7 @@ import { JS } from '../../constants'; export const help: FunctionHelp> = { help: i18n.translate('xpack.canvas.functions.replaceImageHelpText', { - defaultMessage: 'Use a regular expression to replace parts of a string.', + defaultMessage: 'Uses a regular expression to replace parts of a string.', }), args: { pattern: i18n.translate('xpack.canvas.functions.replace.args.patternHelpText', { diff --git a/x-pack/plugins/canvas/i18n/functions/dict/reveal_image.ts b/x-pack/plugins/canvas/i18n/functions/dict/reveal_image.ts index 410ca29d7b4d4..6a8909f4acdde 100644 --- a/x-pack/plugins/canvas/i18n/functions/dict/reveal_image.ts +++ b/x-pack/plugins/canvas/i18n/functions/dict/reveal_image.ts @@ -13,7 +13,7 @@ import { BASE64, URL } from '../../constants'; export const help: FunctionHelp> = { help: i18n.translate('xpack.canvas.functions.revealImageHelpText', { - defaultMessage: 'Configure an image reveal element.', + defaultMessage: 'Configures an image reveal element.', }), args: { image: i18n.translate('xpack.canvas.functions.revealImage.args.imageHelpText', { diff --git a/x-pack/plugins/canvas/i18n/functions/dict/rounddate.ts b/x-pack/plugins/canvas/i18n/functions/dict/rounddate.ts index 4805fe16a94f0..d2728b6371398 100644 --- a/x-pack/plugins/canvas/i18n/functions/dict/rounddate.ts +++ b/x-pack/plugins/canvas/i18n/functions/dict/rounddate.ts @@ -21,7 +21,7 @@ export const help: FunctionHelp> = { args: { format: i18n.translate('xpack.canvas.functions.rounddate.args.formatHelpText', { defaultMessage: - 'The {MOMENTJS} format to use for bucketing. For example, {example} would round each date to months. See {url}.', + 'The {MOMENTJS} format to use for bucketing. For example, {example} rounds to months. See {url}.', values: { example: '`"YYYY-MM"`', MOMENTJS, diff --git a/x-pack/plugins/canvas/i18n/functions/dict/row_count.ts b/x-pack/plugins/canvas/i18n/functions/dict/row_count.ts index 5b0cecd47fd79..fd7c651238c28 100644 --- a/x-pack/plugins/canvas/i18n/functions/dict/row_count.ts +++ b/x-pack/plugins/canvas/i18n/functions/dict/row_count.ts @@ -12,7 +12,7 @@ import { FunctionFactory } from '../../../types'; export const help: FunctionHelp> = { help: i18n.translate('xpack.canvas.functions.rowCountHelpText', { defaultMessage: - 'Returns the number of rows. Pair with {plyFn} to get the count of unique column ' + + 'Returns the number of rows. Pairs with {plyFn} to get the count of unique column ' + 'values, or combinations of unique column values.', values: { plyFn: '`ply`', diff --git a/x-pack/plugins/canvas/i18n/functions/dict/saved_lens.ts b/x-pack/plugins/canvas/i18n/functions/dict/saved_lens.ts index e146a6ca68449..1121aa43f3509 100644 --- a/x-pack/plugins/canvas/i18n/functions/dict/saved_lens.ts +++ b/x-pack/plugins/canvas/i18n/functions/dict/saved_lens.ts @@ -11,17 +11,17 @@ import { FunctionFactory } from '../../../types'; export const help: FunctionHelp> = { help: i18n.translate('xpack.canvas.functions.savedLensHelpText', { - defaultMessage: `Returns an embeddable for a saved lens object`, + defaultMessage: `Returns an embeddable for a saved Lens visualization object.`, }), args: { id: i18n.translate('xpack.canvas.functions.savedLens.args.idHelpText', { - defaultMessage: `The ID of the Saved Lens Object`, + defaultMessage: `The ID of the saved Lens visualization object`, }), timerange: i18n.translate('xpack.canvas.functions.savedLens.args.timerangeHelpText', { defaultMessage: `The timerange of data that should be included`, }), title: i18n.translate('xpack.canvas.functions.savedLens.args.titleHelpText', { - defaultMessage: `The title for the lens emebeddable`, + defaultMessage: `The title for the Lens visualization object`, }), }, }; diff --git a/x-pack/plugins/canvas/i18n/functions/dict/saved_map.ts b/x-pack/plugins/canvas/i18n/functions/dict/saved_map.ts index 8615565897434..bacaca523ed2e 100644 --- a/x-pack/plugins/canvas/i18n/functions/dict/saved_map.ts +++ b/x-pack/plugins/canvas/i18n/functions/dict/saved_map.ts @@ -11,11 +11,11 @@ import { FunctionFactory } from '../../../types'; export const help: FunctionHelp> = { help: i18n.translate('xpack.canvas.functions.savedMapHelpText', { - defaultMessage: `Returns an embeddable for a saved map object`, + defaultMessage: `Returns an embeddable for a saved map object.`, }), args: { id: i18n.translate('xpack.canvas.functions.savedMap.args.idHelpText', { - defaultMessage: `The ID of the Saved Map Object`, + defaultMessage: `The ID of the saved map object`, }), center: i18n.translate('xpack.canvas.functions.savedMap.args.centerHelpText', { defaultMessage: `The center and zoom level the map should have`, diff --git a/x-pack/plugins/canvas/i18n/functions/dict/saved_visualization.ts b/x-pack/plugins/canvas/i18n/functions/dict/saved_visualization.ts index 30f88b51e7576..e8cbddc5c1102 100644 --- a/x-pack/plugins/canvas/i18n/functions/dict/saved_visualization.ts +++ b/x-pack/plugins/canvas/i18n/functions/dict/saved_visualization.ts @@ -11,22 +11,22 @@ import { FunctionFactory } from '../../../types'; export const help: FunctionHelp> = { help: i18n.translate('xpack.canvas.functions.savedVisualizationHelpText', { - defaultMessage: `Returns an embeddable for a saved visualization object`, + defaultMessage: `Returns an embeddable for a saved visualization object.`, }), args: { id: i18n.translate('xpack.canvas.functions.savedVisualization.args.idHelpText', { - defaultMessage: `The ID of the Saved Visualization Object`, + defaultMessage: `The ID of the saved visualization object`, }), timerange: i18n.translate('xpack.canvas.functions.savedVisualization.args.timerangeHelpText', { defaultMessage: `The timerange of data that should be included`, }), colors: i18n.translate('xpack.canvas.functions.savedVisualization.args.colorsHelpText', { - defaultMessage: `Define the color to use for a specific series`, + defaultMessage: `Defines the color to use for a specific series`, }), hideLegend: i18n.translate( 'xpack.canvas.functions.savedVisualization.args.hideLegendHelpText', { - defaultMessage: `Should the legend be hidden`, + defaultMessage: `Specifies the option to hide the legend`, } ), }, diff --git a/x-pack/plugins/canvas/i18n/functions/dict/series_style.ts b/x-pack/plugins/canvas/i18n/functions/dict/series_style.ts index 7b3855b528201..3f6daa588b730 100644 --- a/x-pack/plugins/canvas/i18n/functions/dict/series_style.ts +++ b/x-pack/plugins/canvas/i18n/functions/dict/series_style.ts @@ -43,7 +43,7 @@ export const help: FunctionHelp> = { defaultMessage: 'The width of the line.', }), points: i18n.translate('xpack.canvas.functions.seriesStyle.args.pointsHelpText', { - defaultMessage: 'Size of points on line', + defaultMessage: 'The size of points on line.', }), stack: i18n.translate('xpack.canvas.functions.seriesStyle.args.stackHelpText', { defaultMessage: diff --git a/x-pack/plugins/canvas/i18n/functions/dict/shape.ts b/x-pack/plugins/canvas/i18n/functions/dict/shape.ts index bcd6d90faa3f0..ddc988873f113 100644 --- a/x-pack/plugins/canvas/i18n/functions/dict/shape.ts +++ b/x-pack/plugins/canvas/i18n/functions/dict/shape.ts @@ -12,7 +12,7 @@ import { SVG } from '../../constants'; export const help: FunctionHelp> = { help: i18n.translate('xpack.canvas.functions.shapeHelpText', { - defaultMessage: 'Create a shape.', + defaultMessage: 'Creates a shape.', }), args: { shape: i18n.translate('xpack.canvas.functions.shape.args.shapeHelpText', { diff --git a/x-pack/plugins/canvas/i18n/functions/dict/sort.ts b/x-pack/plugins/canvas/i18n/functions/dict/sort.ts index d539449253058..b768362dd0770 100644 --- a/x-pack/plugins/canvas/i18n/functions/dict/sort.ts +++ b/x-pack/plugins/canvas/i18n/functions/dict/sort.ts @@ -12,12 +12,15 @@ import { DATATABLE } from '../../constants'; export const help: FunctionHelp> = { help: i18n.translate('xpack.canvas.functions.sortHelpText', { - defaultMessage: 'Sorts a datatable by the specified column.', + defaultMessage: 'Sorts a {DATATABLE} by the specified column.', + values: { + DATATABLE, + }, }), args: { by: i18n.translate('xpack.canvas.functions.sort.args.byHelpText', { defaultMessage: - 'The column to sort by. When unspecified, the `{DATATABLE}` ' + + 'The column to sort by. When unspecified, the {DATATABLE} ' + 'is sorted by the first column.', values: { DATATABLE, @@ -25,7 +28,7 @@ export const help: FunctionHelp> = { }), reverse: i18n.translate('xpack.canvas.functions.sort.args.reverseHelpText', { defaultMessage: - 'Reverses the sorting order. When unspecified, the `{DATATABLE}` ' + + 'Reverses the sorting order. When unspecified, the {DATATABLE} ' + 'is sorted in ascending order.', values: { DATATABLE, diff --git a/x-pack/plugins/canvas/i18n/functions/dict/static_column.ts b/x-pack/plugins/canvas/i18n/functions/dict/static_column.ts index 82dbd9910ea3b..f0f7b46a2c0bc 100644 --- a/x-pack/plugins/canvas/i18n/functions/dict/static_column.ts +++ b/x-pack/plugins/canvas/i18n/functions/dict/static_column.ts @@ -12,7 +12,7 @@ import { FunctionFactory } from '../../../types'; export const help: FunctionHelp> = { help: i18n.translate('xpack.canvas.functions.staticColumnHelpText', { defaultMessage: - 'Add a column with the same static value in every row. See also {alterColumnFn} and {mapColumnFn}.', + 'Adds a column with the same static value in every row. See also {alterColumnFn} and {mapColumnFn}.', values: { alterColumnFn: '`alterColumn`', mapColumnFn: '`mapColumn`', @@ -20,11 +20,11 @@ export const help: FunctionHelp> = { }), args: { name: i18n.translate('xpack.canvas.functions.staticColumn.args.nameHelpText', { - defaultMessage: 'The name of the new column column.', + defaultMessage: 'The name of the new column.', }), value: i18n.translate('xpack.canvas.functions.staticColumn.args.valueHelpText', { defaultMessage: - 'The value to insert in each row in the new column. Tip: use a sub-expression to rollup ' + + 'The value to insert in each row in the new column. TIP: use a sub-expression to rollup ' + 'other columns into a static value.', }), }, diff --git a/x-pack/plugins/canvas/i18n/functions/dict/switch.ts b/x-pack/plugins/canvas/i18n/functions/dict/switch.ts index f65ff7c6fd240..aaf53d2c47c3a 100644 --- a/x-pack/plugins/canvas/i18n/functions/dict/switch.ts +++ b/x-pack/plugins/canvas/i18n/functions/dict/switch.ts @@ -14,7 +14,7 @@ export const help: FunctionHelp> = { help: i18n.translate('xpack.canvas.functions.switchHelpText', { defaultMessage: 'Performs conditional logic with multiple conditions. ' + - 'See also {caseFn} which builds a {case} to pass to the {switchFn} function.', + 'See also {caseFn}, which builds a {case} to pass to the {switchFn} function.', values: { case: '`case`', caseFn: '`case`', @@ -23,7 +23,7 @@ export const help: FunctionHelp> = { }), args: { case: i18n.translate('xpack.canvas.functions.switch.args.caseHelpText', { - defaultMessage: 'The conditions to check', + defaultMessage: 'The conditions to check.', }), default: i18n.translate('xpack.canvas.functions.switch.args.defaultHelpText', { defaultMessage: diff --git a/x-pack/plugins/canvas/i18n/functions/dict/table.ts b/x-pack/plugins/canvas/i18n/functions/dict/table.ts index 91a9ae7488234..9fe93b2136fb5 100644 --- a/x-pack/plugins/canvas/i18n/functions/dict/table.ts +++ b/x-pack/plugins/canvas/i18n/functions/dict/table.ts @@ -12,7 +12,7 @@ import { CSS, FONT_FAMILY, FONT_WEIGHT, BOOLEAN_FALSE } from '../../constants'; export const help: FunctionHelp> = { help: i18n.translate('xpack.canvas.functions.tableHelpText', { - defaultMessage: 'Configures a table element', + defaultMessage: 'Configures a table element.', }), args: { font: i18n.translate('xpack.canvas.functions.table.args.fontHelpText', { @@ -35,7 +35,7 @@ export const help: FunctionHelp> = { defaultMessage: 'The number of rows to display on each page.', }), showHeader: i18n.translate('xpack.canvas.functions.table.args.showHeaderHelpText', { - defaultMessage: 'Show/hide the header row with titles for each column.', + defaultMessage: 'Show or hide the header row with titles for each column.', }), }, }; diff --git a/x-pack/plugins/canvas/i18n/functions/dict/time_range.ts b/x-pack/plugins/canvas/i18n/functions/dict/time_range.ts index 476a9978800df..e3fa931a8f07b 100644 --- a/x-pack/plugins/canvas/i18n/functions/dict/time_range.ts +++ b/x-pack/plugins/canvas/i18n/functions/dict/time_range.ts @@ -11,7 +11,7 @@ import { FunctionFactory } from '../../../types'; export const help: FunctionHelp> = { help: i18n.translate('xpack.canvas.functions.timerangeHelpText', { - defaultMessage: `An object that represents a span of time`, + defaultMessage: `An object that represents a span of time.`, }), args: { from: i18n.translate('xpack.canvas.functions.timerange.args.fromHelpText', { diff --git a/x-pack/plugins/canvas/i18n/functions/dict/timefilter.ts b/x-pack/plugins/canvas/i18n/functions/dict/timefilter.ts index aedcdc9441885..80f2544e11a4e 100644 --- a/x-pack/plugins/canvas/i18n/functions/dict/timefilter.ts +++ b/x-pack/plugins/canvas/i18n/functions/dict/timefilter.ts @@ -12,7 +12,7 @@ import { ISO8601, ELASTICSEARCH, DATEMATH } from '../../constants'; export const help: FunctionHelp> = { help: i18n.translate('xpack.canvas.functions.timefilterHelpText', { - defaultMessage: 'Create a time filter for querying a source.', + defaultMessage: 'Creates a time filter for querying a source.', }), args: { column: i18n.translate('xpack.canvas.functions.timefilter.args.columnHelpText', { diff --git a/x-pack/plugins/canvas/i18n/functions/dict/timelion.ts b/x-pack/plugins/canvas/i18n/functions/dict/timelion.ts index 41bf86055f1e3..d76e30c1ef814 100644 --- a/x-pack/plugins/canvas/i18n/functions/dict/timelion.ts +++ b/x-pack/plugins/canvas/i18n/functions/dict/timelion.ts @@ -12,7 +12,7 @@ import { ELASTICSEARCH, DATEMATH, MOMENTJS_TIMEZONE_URL } from '../../constants' export const help: FunctionHelp>> = { help: i18n.translate('xpack.canvas.functions.timelionHelpText', { - defaultMessage: 'Use Timelion to extract one or more timeseries from many sources.', + defaultMessage: 'Uses Timelion to extract one or more time series from many sources.', }), args: { query: i18n.translate('xpack.canvas.functions.timelion.args.query', { diff --git a/x-pack/plugins/canvas/i18n/functions/dict/to.ts b/x-pack/plugins/canvas/i18n/functions/dict/to.ts index c618f84aeaf2b..177e4367b6ece 100644 --- a/x-pack/plugins/canvas/i18n/functions/dict/to.ts +++ b/x-pack/plugins/canvas/i18n/functions/dict/to.ts @@ -12,7 +12,8 @@ import { CONTEXT } from '../../constants'; export const help: FunctionHelp>> = { help: i18n.translate('xpack.canvas.functions.toHelpText', { - defaultMessage: 'Explicitly casts the type of the {CONTEXT} to the specified type.', + defaultMessage: + 'Explicitly casts the type of the {CONTEXT} from one type to the specified type.', values: { CONTEXT, }, diff --git a/x-pack/plugins/canvas/i18n/functions/dict/urlparam.ts b/x-pack/plugins/canvas/i18n/functions/dict/urlparam.ts index b8c044f521029..0331d239d48a9 100644 --- a/x-pack/plugins/canvas/i18n/functions/dict/urlparam.ts +++ b/x-pack/plugins/canvas/i18n/functions/dict/urlparam.ts @@ -13,11 +13,11 @@ import { TYPE_STRING, URL } from '../../constants'; export const help: FunctionHelp> = { help: i18n.translate('xpack.canvas.functions.urlparamHelpText', { defaultMessage: - 'Retreives a {URL} parameter to use in an expression. ' + + 'Retrieves a {URL} parameter to use in an expression. ' + 'The {urlparamFn} function always returns a {TYPE_STRING}. ' + - 'For example, you can retrieve the value {value} from the parameter {myVar} from the {URL} {example}).', + 'For example, you can retrieve the value {value} from the parameter {myVar} from the {URL} {example}.', values: { - example: 'https://localhost:5601/app/canvas?myVar=20', + example: '`https://localhost:5601/app/canvas?myVar=20`', myVar: '`myVar`', TYPE_STRING, URL, diff --git a/x-pack/plugins/canvas/public/application.tsx b/x-pack/plugins/canvas/public/application.tsx index 482cd04373105..463fb1efbd3b5 100644 --- a/x-pack/plugins/canvas/public/application.tsx +++ b/x-pack/plugins/canvas/public/application.tsx @@ -22,7 +22,6 @@ import { registerLanguage } from './lib/monaco_language_def'; import { SetupRegistries } from './plugin_api'; import { initRegistries, populateRegistries, destroyRegistries } from './registries'; import { getDocumentationLinks } from './lib/documentation_links'; -// @ts-expect-error untyped component import { HelpMenu } from './components/help_menu/help_menu'; import { createStore } from './store'; @@ -128,7 +127,10 @@ export const initializeCanvas = async ( }, ], content: (domNode) => { - ReactDOM.render(, domNode); + ReactDOM.render( + , + domNode + ); return () => ReactDOM.unmountComponentAtNode(domNode); }, }); diff --git a/x-pack/plugins/canvas/public/components/function_reference_generator/function_examples.ts b/x-pack/plugins/canvas/public/components/function_reference_generator/function_examples.ts new file mode 100644 index 0000000000000..61c1c1588a290 --- /dev/null +++ b/x-pack/plugins/canvas/public/components/function_reference_generator/function_examples.ts @@ -0,0 +1,444 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export interface FunctionExample { + syntax: string; + usage: { + expression: string; + help?: string; + }; +} + +interface FunctionExampleDict { + [key: string]: FunctionExample; +} + +export const getFunctionExamples = (): FunctionExampleDict => ({ + all: { + syntax: `all {neq "foo"} {neq "bar"} {neq "fizz"} +all condition={gt 10} condition={lt 20}`, + usage: { + expression: `filters +| demodata +| math "mean(percent_uptime)" +| formatnumber "0.0%" +| metric "Average uptime" + metricFont={ + font size=48 family="'Open Sans', Helvetica, Arial, sans-serif" + color={ + if {all {gte 0} {lt 0.8}} then="red" else="green" + } + align="center" lHeight=48 + } +| render`, + help: + 'This sets the color of the metric text to `"red"` if the context passed into `metric` is greater than or equal to 0 and less than 0.8. Otherwise, the color is set to `"green"`.', + }, + }, + alterColumn: { + syntax: `alterColumn "cost" type="string" +alterColumn column="@timestamp" name="foo"`, + usage: { + expression: `filters +| demodata +| alterColumn "time" name="time_in_ms" type="number" +| table +| render`, + help: + 'This renames the `time` column to `time_in_ms` and converts the type of the column’s values from `date` to `number`.', + }, + }, + any: { + syntax: `any {eq "foo"} {eq "bar"} {eq "fizz"} +any condition={lte 10} condition={gt 30}`, + usage: { + expression: `filters +| demodata +| filterrows { + getCell "project" | any {eq "elasticsearch"} {eq "kibana"} {eq "x-pack"} + } +| pointseries color="project" size="max(price)" +| pie +| render`, + help: + 'This filters out any rows that don’t contain `"elasticsearch"`, `"kibana"` or `"x-pack"` in the `project` field.', + }, + }, + as: { + syntax: `as +as "foo" +as name="bar"`, + usage: { + expression: `filters +| demodata +| ply by="project" fn={math "count(username)" | as "num_users"} fn={math "mean(price)" | as "price"} +| pointseries x="project" y="num_users" size="price" color="project" +| plot +| render`, + help: `\`as\` casts any primitive value (\`string\`, \`number\`, \`date\`, \`null\`) into a \`datatable\` with a single row and a single column with the given name (or defaults to \`"value"\` if no name is provided). This is useful when piping a primitive value into a function that only takes \`datatable\` as an input. + +In the example, \`ply\` expects each \`fn\` subexpression to return a \`datatable\` in order to merge the results of each \`fn\` back into a \`datatable\`, but using a \`math\` aggregation in the subexpressions returns a single \`math\` value, which is then cast into a \`datatable\` using \`as\`.`, + }, + }, + asset: { + syntax: `asset "asset-52f14f2b-fee6-4072-92e8-cd2642665d02" +asset id="asset-498f7429-4d56-42a2-a7e4-8bf08d98d114"`, + usage: { + expression: `image dataurl={asset "asset-c661a7cc-11be-45a1-a401-d7592ea7917a"} mode="contain" +| render`, + help: + 'The image asset stored with the ID `"asset-c661a7cc-11be-45a1-a401-d7592ea7917a"` is passed into the `dataurl` argument of the `image` function to display the stored asset.', + }, + }, + axisConfig: { + syntax: `axisConfig show=false +axisConfig position="right" min=0 max=10 tickSize=1`, + usage: { + expression: `filters +| demodata +| pointseries x="size(cost)" y="project" color="project" +| plot defaultStyle={seriesStyle bars=0.75 horizontalBars=true} + legend=false + xaxis={axisConfig position="top" min=0 max=400 tickSize=100} + yaxis={axisConfig position="right"} +| render`, + help: + 'This sets the `x-axis` to display on the top of the chart and sets the range of values to `0-400` with ticks displayed at `100` intervals. The `y-axis` is configured to display on the `right`.', + }, + }, + case: { + syntax: `case 0 then="red" +case when=5 then="yellow" +case if={lte 50} then="green"`, + usage: { + expression: `math "random()" +| progress shape="gauge" label={formatnumber "0%"} + font={ + font size=24 family="'Open Sans', Helvetica, Arial, sans-serif" align="center" + color={ + switch {case if={lte 0.5} then="green"} + {case if={all {gt 0.5} {lte 0.75}} then="orange"} + default="red" + } + } + valueColor={ + switch {case if={lte 0.5} then="green"} + {case if={all {gt 0.5} {lte 0.75}} then="orange"} + default="red" + } +| render`, + help: + 'This sets the color of the progress indicator and the color of the label to `"green"` if the value is less than or equal to `0.5`, `"orange"` if the value is greater than `0.5` and less than or equal to `0.75`, and `"red"` if `none` of the case conditions are met.', + }, + }, + columns: { + syntax: `columns include="@timestamp, projects, cost" +columns exclude="username, country, age"`, + usage: { + expression: `filters +| demodata +| columns include="price, cost, state, project" +| table +| render`, + help: + 'This only keeps the `price`, `cost`, `state`, and `project` columns from the `demodata` data source and removes all other columns.', + }, + }, + compare: { + syntax: `compare "neq" to="elasticsearch" +compare op="lte" to=100`, + usage: { + expression: `filters +| demodata +| mapColumn project + fn={getCell project | + switch + {case if={compare eq to=kibana} then=kibana} + {case if={compare eq to=elasticsearch} then=elasticsearch} + default="other" + } +| pointseries size="size(cost)" color="project" +| pie +| render`, + help: + 'This maps all `project` values that aren’t `"kibana"` and `"elasticsearch"` to `"other"`. Alternatively, you can use the individual comparator functions instead of compare.', + }, + }, + containerStyle: { + syntax: `containerStyle backgroundColor="red"’ +containerStyle borderRadius="50px" +containerStyle border="1px solid black" +containerStyle padding="5px" +containerStyle opacity="0.5" +containerStyle overflow="hidden" +containerStyle backgroundImage={asset id=asset-f40d2292-cf9e-4f2c-8c6f-a504a25e949c} + backgroundRepeat="no-repeat" + backgroundSize="cover"`, + usage: { + expression: `shape "star" fill="#E61D35" maintainAspect=true +| render containerStyle={ + containerStyle backgroundColor="#F8D546" + borderRadius="200px" + border="4px solid #05509F" + padding="0px" + opacity="0.9" + overflow="hidden" + }`, + }, + }, + context: { + syntax: `context`, + usage: { + expression: `date +| formatdate "LLLL" +| markdown "Last updated: " {context} +| render`, + help: + 'Using the `context` function allows us to pass the output, or _context_, of the previous function as a value to an argument in the next function. Here we get the formatted date string from the previous function and pass it as `content` for the markdown element.', + }, + }, + csv: { + syntax: `csv "fruit, stock + kiwi, 10 + Banana, 5"`, + usage: { + expression: `csv "fruit,stock + kiwi,10 + banana,5" +| pointseries color=fruit size=stock +| pie +| render`, + help: + 'This creates a `datatable` with `fruit` and `stock` columns with two rows. This is useful for quickly mocking data.', + }, + }, + date: { + syntax: `date +date value=1558735195 +date "2019-05-24T21:59:55+0000" +date "01/31/2019" format="MM/DD/YYYY"`, + usage: { + expression: `date +| formatdate "LLL" +| markdown {context} + font={font family="Arial, sans-serif" size=30 align="left" + color="#000000" + weight="normal" + underline=false + italic=false} +| render`, + help: 'Using `date` without passing any arguments will return the current date and time.', + }, + }, + demodata: { + syntax: `demodata +demodata "ci" +demodata type="shirts"`, + usage: { + expression: `filters +| demodata +| table +| render`, + help: '`demodata` is a mock data set that you can use to start playing around in Canvas.', + }, + }, + dropdownControl: { + syntax: `dropdownControl valueColumn=project filterColumn=project +dropdownControl valueColumn=agent filterColumn=agent.keyword filterGroup=group1`, + usage: { + expression: `demodata +| dropdownControl valueColumn=project filterColumn=project +| render`, + help: + 'This creates a dropdown filter element. It requires a data source and uses the unique values from the given `valueColumn` (i.e. `project`) and applies the filter to the `project` column. Note: `filterColumn` should point to a keyword type field for Elasticsearch data sources.', + }, + }, + eq: { + syntax: `eq true +eq null +eq 10 +eq "foo"`, + usage: { + expression: `filters +| demodata +| mapColumn project + fn={getCell project | + switch + {case if={eq kibana} then=kibana} + {case if={eq elasticsearch} then=elasticsearch} + default="other" + } +| pointseries size="size(cost)" color="project" +| pie +| render`, + help: + 'This changes all values in the project column that don’t equal `"kibana"` or `"elasticsearch"` to `"other"`.', + }, + }, + escount: { + syntax: `escount index="logstash-*" +escount "currency:\"EUR\"" index="kibana_sample_data_ecommerce" +escount query="response:404" index="kibana_sample_data_logs"`, + usage: { + expression: `filters +| escount "Cancelled:true" index="kibana_sample_data_flights" +| math "value" +| progress shape="semicircle" + label={formatnumber 0,0} + font={font size=24 family="'Open Sans', Helvetica, Arial, sans-serif" color="#000000" align=center} + max={filters | escount index="kibana_sample_data_flights"} +| render`, + help: + 'The first `escount` expression retrieves the number of flights that were cancelled. The second `escount` expression retrieves the total number of flights.', + }, + }, + esdocs: { + syntax: `esdocs index="logstash-*" +esdocs "currency:\"EUR\"" index="kibana_sample_data_ecommerce" +esdocs query="response:404" index="kibana_sample_data_logs" +esdocs index="kibana_sample_data_flights" count=100 +esdocs index="kibana_sample_data_flights" sort="AvgTicketPrice, asc"`, + usage: { + expression: `filters +| esdocs index="kibana_sample_data_ecommerce" + fields="customer_gender, taxful_total_price, order_date" + sort="order_date, asc" + count=10000 +| mapColumn "order_date" + fn={getCell "order_date" | date {context} | rounddate "YYYY-MM-DD"} +| alterColumn "order_date" type="date" +| pointseries x="order_date" y="sum(taxful_total_price)" color="customer_gender" +| plot defaultStyle={seriesStyle lines=3} + palette={palette "#7ECAE3" "#003A4D" gradient=true} +| render`, + help: + 'This retrieves the first 10000 documents data from the `kibana_sample_data_ecommerce` index sorted by `order_date` in ascending order, and only requests the `customer_gender`, `taxful_total_price`, and `order_date` fields.', + }, + }, + essql: { + syntax: `essql query="SELECT * FROM \"logstash*\"" +essql "SELECT * FROM \"apm*\"" count=10000`, + usage: { + expression: `filters +| essql query="SELECT Carrier, FlightDelayMin, AvgTicketPrice FROM \"kibana_sample_data_flights\"" +| table +| render`, + help: + 'This retrieves the `Carrier`, `FlightDelayMin`, and `AvgTicketPrice` fields from the "kibana_sample_data_flights" index.', + }, + }, + exactly: { + syntax: `exactly "state" value="running" +exactly "age" value=50 filterGroup="group2" +exactly column="project" value="beats"`, + usage: { + expression: `filters +| exactly column=project value=elasticsearch +| demodata +| pointseries x=project y="mean(age)" +| plot defaultStyle={seriesStyle bars=1} +| render`, + help: + 'The `exactly` filter here is added to existing filters retrieved by the `filters` function and further filters down the data to only have `"elasticsearch"` data. The `exactly` filter only applies to this one specific element and will not affect other elements in the workpad.', + }, + }, + filterrows: { + syntax: `filterrows {getCell "project" | eq "kibana"} +filterrows fn={getCell "age" | gt 50}`, + usage: { + expression: `filters +| demodata +| filterrows {getCell "country" | any {eq "IN"} {eq "US"} {eq "CN"}} +| mapColumn "@timestamp" + fn={getCell "@timestamp" | rounddate "YYYY-MM"} +| alterColumn "@timestamp" type="date" +| pointseries x="@timestamp" y="mean(cost)" color="country" +| plot defaultStyle={seriesStyle points="2" lines="1"} + palette={palette "#01A4A4" "#CC6666" "#D0D102" "#616161" "#00A1CB" "#32742C" "#F18D05" "#113F8C" "#61AE24" "#D70060" gradient=false} +| render`, + help: + 'This uses `filterrows` to only keep data from India (`IN`), the United States (`US`), and China (`CN`).', + }, + }, + filters: { + syntax: `filters +filters group="timefilter1" +filters group="timefilter2" group="dropdownfilter1" ungrouped=true`, + usage: { + expression: `filters group=group2 ungrouped=true +| demodata +| pointseries x="project" y="size(cost)" color="project" +| plot defaultStyle={seriesStyle bars=0.75} legend=false + font={ + font size=14 + family="'Open Sans', Helvetica, Arial, sans-serif" + align="left" + color="#FFFFFF" + weight="lighter" + underline=true + italic=true + } +| render`, + help: + '`filters` sets the existing filters as context and accepts a `group` parameter to opt into specific filter groups. Setting `ungrouped` to `true` opts out of using global filters.', + }, + }, + font: { + syntax: `font size=12 +font family=Arial +font align=middle +font color=pink +font weight=lighter +font underline=true +font italic=false +font lHeight=32`, + usage: { + expression: `filters +| demodata +| pointseries x="project" y="size(cost)" color="project" +| plot defaultStyle={seriesStyle bars=0.75} legend=false + font={ + font size=14 + family="'Open Sans', Helvetica, Arial, sans-serif" + align="left" + color="#FFFFFF" + weight="lighter" + underline=true + italic=true + } +| render`, + }, + }, + formatdate: { + syntax: `formatdate format="YYYY-MM-DD" +formatdate "MM/DD/YYYY"`, + usage: { + expression: `filters +| demodata +| mapColumn "time" fn={getCell time | formatdate "MMM 'YY"} +| pointseries x="time" y="sum(price)" color="state" +| plot defaultStyle={seriesStyle points=5} +| render`, + help: + 'This transforms the dates in the `time` field into strings that look like `"Jan ‘19"`, `"Feb ‘19"`, etc. using a MomentJS format.', + }, + }, + formatnumber: { + syntax: `formatnumber format="$0,0.00" +formatnumber "0.0a"`, + usage: { + expression: `filters +| demodata +| math "mean(percent_uptime)" +| progress shape="gauge" + label={formatnumber "0%"} + font={font size=24 family="'Open Sans', Helvetica, Arial, sans-serif" color="#000000" align="center"} +| render`, + help: + 'The `formatnumber` subexpression receives the same `context` as the `progress` function, which is the output of the `math` function. It formats the value into a percentage.', + }, + }, +}); diff --git a/x-pack/plugins/canvas/public/components/function_reference_generator/function_reference_generator.tsx b/x-pack/plugins/canvas/public/components/function_reference_generator/function_reference_generator.tsx new file mode 100644 index 0000000000000..c527b322dba57 --- /dev/null +++ b/x-pack/plugins/canvas/public/components/function_reference_generator/function_reference_generator.tsx @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FC } from 'react'; +import { ExpressionFunction } from 'src/plugins/expressions'; +import { EuiButtonEmpty } from '@elastic/eui'; +import copy from 'copy-to-clipboard'; +import { notifyService } from '../../services'; +import { generateFunctionReference } from './generate_function_reference'; + +interface Props { + functionRegistry: Record; +} + +export const FunctionReferenceGenerator: FC = ({ functionRegistry }) => { + const functionDefinitions = Object.values(functionRegistry); + + const copyDocs = () => { + copy(generateFunctionReference(functionDefinitions)); + notifyService + .getService() + .success( + `Please paste updated docs into '/kibana/docs/canvas/canvas-function-reference.asciidoc' and commit your changes.`, + { title: 'Copied function docs to clipboard' } + ); + }; + + return ( + + Generate function reference + + ); +}; diff --git a/x-pack/plugins/canvas/public/components/function_reference_generator/generate_function_reference.ts b/x-pack/plugins/canvas/public/components/function_reference_generator/generate_function_reference.ts new file mode 100644 index 0000000000000..bd77fbf62ec5a --- /dev/null +++ b/x-pack/plugins/canvas/public/components/function_reference_generator/generate_function_reference.ts @@ -0,0 +1,259 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +// @ts-expect-error untyped lib +import pluralize from 'pluralize'; +import { ExpressionFunction, ExpressionFunctionParameter } from 'src/plugins/expressions'; +import { functions as browserFunctions } from '../../../canvas_plugin_src/functions/browser'; +import { functions as serverFunctions } from '../../../canvas_plugin_src/functions/server'; +import { isValidDataUrl, DATATABLE_COLUMN_TYPES } from '../../../common/lib'; +import { getFunctionExamples, FunctionExample } from './function_examples'; + +const ALPHABET = 'abcdefghijklmnopqrstuvwxyz'.split(''); +const REQUIRED_ARG_ANNOTATION = '***'; +const MULTI_ARG_ANNOTATION = '†'; +const UNNAMED_ARG = '_Unnamed_'; +const ANY_TYPE = '`any`'; + +const examplesDict = getFunctionExamples(); + +const fnList = [ + ...browserFunctions.map((fn) => fn().name), + ...serverFunctions.map((fn) => fn().name), + 'asset', + 'filters', + 'timelion', + 'to', + 'font', + 'var', + 'var_set', + // ignore unsupported embeddables functions for now +].filter((fn) => !['savedSearch'].includes(fn)); + +interface FunctionDictionary { + [key: string]: ExpressionFunction[]; +} + +const wrapInBackTicks = (str: string) => `\`${str}\``; +const wrapInDoubleQuotes = (str: string) => (str.includes('"') ? str : `"${str}"`); +const stringSorter = (a: string, b: string) => { + if (a < b) { + return -1; + } + if (a > b) { + return 1; + } + return 0; +}; + +// Converts reference to another function in a function's help text into an Asciidoc link +const addFunctionLinks = (help: string, options?: { ignoreList?: string[] }) => { + const { ignoreList = [] } = options || {}; + fnList.forEach((name: string) => { + const nameWithBackTicks = wrapInBackTicks(name); + + // ignore functions with the same name as data types, i.e. string, date + if ( + !ignoreList.includes(name) && + !DATATABLE_COLUMN_TYPES.includes(name) && + help.includes(nameWithBackTicks) + ) { + help = help.replace(nameWithBackTicks, `<<${name}_fn>>`); + } + }); + + return help; +}; + +export const generateFunctionReference = (functionDefinitions: ExpressionFunction[]) => { + const functionDefs = functionDefinitions.filter((fn: ExpressionFunction) => + fnList.includes(fn.name) + ); + const functionDictionary: FunctionDictionary = {}; + functionDefs.forEach((fn: ExpressionFunction) => { + const firstLetter = fn.name[0]; + + if (!functionDictionary[firstLetter]) { + functionDictionary[firstLetter] = []; + } + + functionDictionary[firstLetter].push(fn); + }); + return `[role="xpack"] +[[canvas-function-reference]] +== Canvas function reference + +Behind the scenes, Canvas is driven by a powerful expression language, +with dozens of functions and other capabilities, including table transforms, +type casting, and sub-expressions. + +The Canvas expression language also supports <>, which +perform complex math calculations. + +A ${REQUIRED_ARG_ANNOTATION} denotes a required argument. + +A ${MULTI_ARG_ANNOTATION} denotes an argument can be passed multiple times. + +${createAlphabetLinks(functionDictionary)} + +${createFunctionDocs(functionDictionary)}`; +}; + +const createAlphabetLinks = (functionDictionary: FunctionDictionary) => { + return ALPHABET.map((letter: string) => + functionDictionary[letter] ? `<<${letter}_fns>>` : letter.toUpperCase() + ).join(' | '); +}; + +const createFunctionDocs = (functionDictionary: FunctionDictionary) => { + return Object.keys(functionDictionary) + .sort() + .map( + (letter: string) => `[float] +[[${letter}_fns]] +== ${letter.toUpperCase()} + +${functionDictionary[letter] + .sort((a, b) => stringSorter(a.name, b.name)) + .map(getDocBlock) + .join('\n')}` + ) + .join(''); +}; + +const getDocBlock = (fn: ExpressionFunction) => { + const header = `[float] +[[${fn.name}_fn]] +=== \`${fn.name}\``; + + const input = fn.inputTypes; + const output = fn.type; + const args = fn.args; + const examples = examplesDict[fn.name]; + const help = addFunctionLinks(fn.help); + + const argBlock = + !args || Object.keys(args).length === 0 + ? '' + : `\n[cols="3*^<"] +|=== +|Argument |Type |Description + +${getArgsTable(args)} +|===\n`; + + const examplesBlock = !examples ? `` : `${getExamplesBlock(examples)}`; + + return `${header}\n +${help} +${examplesBlock} +*Accepts:* ${input ? input.map(wrapInBackTicks).join(', ') : ANY_TYPE}\n${argBlock} +*Returns:* ${output ? wrapInBackTicks(output) : 'Depends on your input and arguments'}\n\n`; +}; + +const getArgsTable = (args: { [key: string]: ExpressionFunctionParameter }) => { + if (!args || Object.keys(args).length === 0) { + return 'None'; + } + + const argNames = Object.keys(args); + + return argNames + .sort((a: string, b: string) => { + const argA = args[a]; + const argB = args[b]; + + // sorts unnamed arg to the front + if (a === '_' || (argA.aliases && argA.aliases.includes('_'))) { + return -1; + } + if (b === '_' || (argB.aliases && argB.aliases.includes('_'))) { + return 1; + } + return stringSorter(a, b); + }) + .map((argName: string) => { + const arg = args[argName]; + const types = arg.types; + const aliases = arg.aliases ? [...arg.aliases] : []; + let defaultValue = arg.default; + const requiredAnnotation = arg.required === true ? ` ${REQUIRED_ARG_ANNOTATION}` : ''; + const multiAnnotation = arg.multi === true ? ` ${MULTI_ARG_ANNOTATION}` : ''; + + if (typeof defaultValue === 'string') { + defaultValue = defaultValue.replace('{', '${').replace(/[\r\n/]+/g, ''); + if (types && types.includes('string')) { + defaultValue = wrapInDoubleQuotes(defaultValue); + } + } + + let displayName = ''; + + if (argName === '_') { + displayName = UNNAMED_ARG; + } else if (aliases && aliases.includes('_')) { + displayName = UNNAMED_ARG; + aliases[aliases.indexOf('_')] = argName; + } else { + displayName = wrapInBackTicks(argName); + } + + const aliasList = + aliases && aliases.length + ? `\n\n${pluralize('Alias', aliases.length)}: ${aliases + .sort() + .map(wrapInBackTicks) + .join(', ')}` + : ''; + + let defaultBlock = ''; + + if (isValidDataUrl(arg.default)) { + defaultBlock = getDataUrlExampleBlock(displayName, arg.default); + } else { + defaultBlock = + typeof defaultValue !== 'undefined' ? `\n\nDefault: \`${defaultValue}\`` : ''; + } + + return `|${displayName}${requiredAnnotation}${multiAnnotation}${aliasList} +|${types && types.length ? types.map(wrapInBackTicks).join(', ') : ANY_TYPE} +|${arg.help ? addFunctionLinks(arg.help, { ignoreList: argNames }) : ''}${defaultBlock}`; + }) + .join('\n\n'); +}; + +const getDataUrlExampleBlock = ( + argName: string, + value: string +) => `\n\nExample value for the ${argName} argument, formatted as a \`base64\` data URL: +[source, url] +------------ +${value} +------------`; + +const getExamplesBlock = (examples: FunctionExample) => { + const { syntax, usage } = examples; + const { expression, help } = usage || {}; + const syntaxBlock = syntax + ? `\n*Expression syntax* +[source,js] +---- +${syntax} +----\n` + : ''; + + const codeBlock = expression + ? `\n*Code example* +[source,text] +---- +${expression} +----\n` + : ''; + + const codeHelp = help ? `${help}\n` : ''; + + return `${syntaxBlock}${codeBlock}${codeHelp}`; +}; diff --git a/x-pack/plugins/canvas/public/components/function_reference_generator/index.ts b/x-pack/plugins/canvas/public/components/function_reference_generator/index.ts new file mode 100644 index 0000000000000..337809238bb59 --- /dev/null +++ b/x-pack/plugins/canvas/public/components/function_reference_generator/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { FunctionReferenceGenerator } from './function_reference_generator'; diff --git a/x-pack/plugins/canvas/public/components/help_menu/help_menu.js b/x-pack/plugins/canvas/public/components/help_menu/help_menu.js deleted file mode 100644 index 4512ce2b4992e..0000000000000 --- a/x-pack/plugins/canvas/public/components/help_menu/help_menu.js +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React, { Fragment, PureComponent } from 'react'; -import { EuiButtonEmpty, EuiPortal } from '@elastic/eui'; -import { KeyboardShortcutsDoc } from '../keyboard_shortcuts_doc'; -import { ComponentStrings } from '../../../i18n'; - -const { HelpMenu: strings } = ComponentStrings; - -export class HelpMenu extends PureComponent { - state = { isFlyoutVisible: false }; - - showFlyout = () => { - this.setState({ isFlyoutVisible: true }); - }; - - hideFlyout = () => { - this.setState({ isFlyoutVisible: false }); - }; - - render() { - return ( - - - {strings.getKeyboardShortcutsLinkLabel()} - - - {this.state.isFlyoutVisible && ( - - - - )} - - ); - } -} diff --git a/x-pack/plugins/canvas/public/components/help_menu/help_menu.tsx b/x-pack/plugins/canvas/public/components/help_menu/help_menu.tsx new file mode 100644 index 0000000000000..7122ec88f68a9 --- /dev/null +++ b/x-pack/plugins/canvas/public/components/help_menu/help_menu.tsx @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FC, useState, lazy, Suspense } from 'react'; +import { EuiButtonEmpty, EuiPortal, EuiSpacer } from '@elastic/eui'; +import { ExpressionFunction } from 'src/plugins/expressions'; +import { ComponentStrings } from '../../../i18n'; +import { KeyboardShortcutsDoc } from '../keyboard_shortcuts_doc'; + +let FunctionReferenceGenerator: null | React.LazyExoticComponent = null; +if (process.env.NODE_ENV === 'development') { + FunctionReferenceGenerator = lazy(() => + import('../function_reference_generator').then((module) => ({ + default: module.FunctionReferenceGenerator, + })) + ); +} + +const { HelpMenu: strings } = ComponentStrings; + +interface Props { + functionRegistry: Record; +} + +export const HelpMenu: FC = ({ functionRegistry }) => { + const [isFlyoutVisible, setFlyoutVisible] = useState(false); + + const showFlyout = () => { + setFlyoutVisible(true); + }; + + const hideFlyout = () => { + setFlyoutVisible(false); + }; + + return ( + <> + + {strings.getKeyboardShortcutsLinkLabel()} + + + {FunctionReferenceGenerator ? ( + + + + + ) : null} + + {isFlyoutVisible && ( + + + + )} + + ); +}; diff --git a/x-pack/plugins/canvas/public/components/help_menu/index.js b/x-pack/plugins/canvas/public/components/help_menu/index.ts similarity index 100% rename from x-pack/plugins/canvas/public/components/help_menu/index.js rename to x-pack/plugins/canvas/public/components/help_menu/index.ts diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index da5392848475b..b51ccbb64767a 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -5667,7 +5667,6 @@ "xpack.canvas.functions.joinRows.args.separatorHelpText": "行の値の間で使用する区切り文字", "xpack.canvas.functions.joinRows.columnNotFoundErrorMessage": "列が見つかりません。'{column}'", "xpack.canvas.functions.joinRowsHelpText": "データベースの行の値を文字列に結合", - "xpack.canvas.functions.locationHelpText": "ブラウザの {geolocationAPI} を使用して現在位置を取得します。パフォーマンスに違いはありますが、比較的正確です。{url} を参照。", "xpack.canvas.functions.lt.args.valueHelpText": "{CONTEXT} と比較される値です。", "xpack.canvas.functions.lte.args.valueHelpText": "{CONTEXT} と比較される値です。", "xpack.canvas.functions.lteHelpText": "{CONTEXT} が引数以下かを戻します。", @@ -5676,7 +5675,6 @@ "xpack.canvas.functions.mapCenterHelpText": "マップの中央座標とズームレベルのオブジェクトに戻ります。", "xpack.canvas.functions.mapColumn.args.expressionHelpText": "単一行 {DATATABLE} として各行に渡される {CANVAS} 表現です。", "xpack.canvas.functions.mapColumn.args.nameHelpText": "結果の列の名前です。", - "xpack.canvas.functions.mapColumnHelpText": "他の列の結果として計算された列を追加します。引数が提供された場合のみ変更が加えられます。{mapColumnFn} と {staticColumnFn} もご参照ください。", "xpack.canvas.functions.markdown.args.contentHelpText": "{MARKDOWN} を含むテキストの文字列です。連結させるには、{stringFn} 関数を複数回渡します。", "xpack.canvas.functions.markdown.args.fontHelpText": "コンテンツの {CSS} フォントプロパティです。例: {fontFamily} または {fontWeight}。", "xpack.canvas.functions.markdown.args.openLinkHelpText": "新しいタブでリンクを開くための true/false 値。デフォルト値は false です。true に設定するとすべてのリンクが新しいタブで開くようになります。", @@ -5686,7 +5684,6 @@ "xpack.canvas.functions.math.emptyExpressionErrorMessage": "空の表現", "xpack.canvas.functions.math.executionFailedErrorMessage": "数式の実行に失敗しました。列名を確認してください", "xpack.canvas.functions.math.tooManyResultsErrorMessage": "表現は 1 つの数字を返す必要があります。表現を {mean} または {sum} で囲んでみてください", - "xpack.canvas.functions.mathHelpText": "数字または {DATATABLE} を {CONTEXT} として使用して {TINYMATH} 数式を解釈します。{DATATABLE} 列は列名で表示されます。{CONTEXT} が数字の場合は、{value} と表示されます。", "xpack.canvas.functions.metric.args.labelFontHelpText": "ラベルの {CSS} フォントプロパティです。例: {FONT_FAMILY} または {FONT_WEIGHT}。", "xpack.canvas.functions.metric.args.labelHelpText": "メトリックを説明するテキストです。", "xpack.canvas.functions.metric.args.metricFontHelpText": "メトリックの {CSS} フォントプロパティです。例: {FONT_FAMILY} または {FONT_WEIGHT}。", @@ -5702,16 +5699,12 @@ "xpack.canvas.functions.pie.args.holeHelpText": "円グラフに穴をあけます、0~100 で円グラフの半径のパーセンテージを指定します。", "xpack.canvas.functions.pie.args.labelRadiusHelpText": "ラベルの円の半径として使用する、コンテナーの面積のパーセンテージです。", "xpack.canvas.functions.pie.args.labelsHelpText": "円グラフのラベルを表示しますか?", - "xpack.canvas.functions.pie.args.legendHelpText": "凡例の配置です。例: {positions}、または {BOOLEAN_FALSE}。{BOOLEAN_FALSE} の場合、凡例は非表示になります。", - "xpack.canvas.functions.pie.args.paletteHelpText": "この円グラフに使用されている色を説明する {palette} オブジェクトです。{paletteFn} をご覧ください。", "xpack.canvas.functions.pie.args.radiusHelpText": "利用可能なスペースのパーセンテージで示された円グラフの半径です (0 から 1 の間)。半径を自動的に設定するには {auto} を使用します。", "xpack.canvas.functions.pie.args.seriesStyleHelpText": "特定の数列のスタイルです", "xpack.canvas.functions.pie.args.tiltHelpText": "「1」 が完全に垂直、「0」が完全に水平を表す傾きのパーセンテージです。", "xpack.canvas.functions.pieHelpText": "円グラフのエレメントを構成します。", "xpack.canvas.functions.plot.args.defaultStyleHelpText": "すべての数列に使用するデフォルトのスタイルです。", "xpack.canvas.functions.plot.args.fontHelpText": "表の {CSS} フォントプロパティです。例: {FONT_FAMILY} または {FONT_WEIGHT}。", - "xpack.canvas.functions.plot.args.legendHelpText": "凡例の配置です。例: {positions}、または {BOOLEAN_FALSE}。{BOOLEAN_FALSE} の場合、凡例は非表示になります。", - "xpack.canvas.functions.plot.args.paletteHelpText": "このチャートに使用される色を説明する {palette} オブジェクトです。{paletteFn} をご覧ください。", "xpack.canvas.functions.plot.args.seriesStyleHelpText": "特定の数列のスタイルです", "xpack.canvas.functions.plot.args.xaxisHelpText": "軸の構成です。{BOOLEAN_FALSE} の場合、軸は非表示になります。", "xpack.canvas.functions.plot.args.yaxisHelpText": "軸の構成です。{BOOLEAN_FALSE} の場合、軸は非表示になります。", @@ -5795,7 +5788,6 @@ "xpack.canvas.functions.shapeHelpText": "図形を作成します。", "xpack.canvas.functions.sort.args.byHelpText": "並べ替えの基準となる列です。指定されていない場合、「{DATATABLE}」は初めの列で並べられます。", "xpack.canvas.functions.sort.args.reverseHelpText": "並び順を反転させます。指定されていない場合、「{DATATABLE}」は昇順で並べられます。", - "xpack.canvas.functions.sortHelpText": "データ表を指定された列で並べ替えます。", "xpack.canvas.functions.staticColumn.args.nameHelpText": "新しい列の名前です。", "xpack.canvas.functions.staticColumn.args.valueHelpText": "新しい列の各行に挿入する値です。ヒント: 部分式を使用して他の列を静的値にロールアップします。", "xpack.canvas.functions.staticColumnHelpText": "すべての行に同じ静的値の列を追加します。{alterColumnFn} および {mapColumnFn} もご参照ください。", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index e892ff228cd49..a2b612cd6dad1 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -5669,7 +5669,6 @@ "xpack.canvas.functions.joinRows.args.separatorHelpText": "用于分隔行值的分隔符", "xpack.canvas.functions.joinRows.columnNotFoundErrorMessage": "找不到列:“{column}”", "xpack.canvas.functions.joinRowsHelpText": "将数据库中的行的值联接成字符串", - "xpack.canvas.functions.locationHelpText": "使用浏览器的 {geolocationAPI} 查找您的当前位置。性能可能会因浏览器而异,但相当准确。请参见 {url}。", "xpack.canvas.functions.lt.args.valueHelpText": "与 {CONTEXT} 比较的值。", "xpack.canvas.functions.lte.args.valueHelpText": "与 {CONTEXT} 比较的值。", "xpack.canvas.functions.lteHelpText": "返回 {CONTEXT} 是否小于或等于参数。", @@ -5678,7 +5677,6 @@ "xpack.canvas.functions.mapCenterHelpText": "返回具有地图中心坐标和缩放级别的对象", "xpack.canvas.functions.mapColumn.args.expressionHelpText": "作为单行 {DATATABLE} 传递到每一行的 {CANVAS} 表达式。", "xpack.canvas.functions.mapColumn.args.nameHelpText": "结果列的名称。", - "xpack.canvas.functions.mapColumnHelpText": "添加计算为其他列的结果的列。只有提供参数时,才会进行更改。另请参见 {mapColumnFn} 和 {staticColumnFn}。", "xpack.canvas.functions.markdown.args.contentHelpText": "包含 {MARKDOWN} 的文本字符串。要进行串联,请传递 {stringFn} 函数多次。", "xpack.canvas.functions.markdown.args.fontHelpText": "内容的 {CSS} 字体属性。例如:{fontFamily} 或 {fontWeight}。", "xpack.canvas.functions.markdown.args.openLinkHelpText": "表示是否在新选项卡中打开链接的 true/false 值。默认值为 false。设置为 true 将在新选项卡中打开所有链接。", @@ -5688,7 +5686,6 @@ "xpack.canvas.functions.math.emptyExpressionErrorMessage": "空表达式", "xpack.canvas.functions.math.executionFailedErrorMessage": "无法执行数学表达式。检查您的列名称", "xpack.canvas.functions.math.tooManyResultsErrorMessage": "表达式必须返回单个数字。尝试将您的表达式包装在 {mean} 或 {sum} 中", - "xpack.canvas.functions.mathHelpText": "通过将数字或 {DATATABLE} 用作 {CONTEXT} 来解析 {TINYMATH} 数学表达式。{DATATABLE} 列可通过列名来使用。如果 {CONTEXT} 是数字,其可用作 {value}。", "xpack.canvas.functions.metric.args.labelFontHelpText": "标签的 {CSS} 字体属性。例如 {FONT_FAMILY} 或 {FONT_WEIGHT}。", "xpack.canvas.functions.metric.args.labelHelpText": "描述指标的文本。", "xpack.canvas.functions.metric.args.metricFontHelpText": "指标的 {CSS} 字体属性。例如 {FONT_FAMILY} 或 {FONT_WEIGHT}。", @@ -5704,16 +5701,12 @@ "xpack.canvas.functions.pie.args.holeHelpText": "在饼图中绘制介于 `0` and `100`(饼图半径的百分比)之间的孔洞。", "xpack.canvas.functions.pie.args.labelRadiusHelpText": "要用作标签圆形半径的容器面积百分比。", "xpack.canvas.functions.pie.args.labelsHelpText": "显示饼图标签?", - "xpack.canvas.functions.pie.args.legendHelpText": "图例位置。例如 {positions} 或 {BOOLEAN_FALSE}。为 {BOOLEAN_FALSE} 时,图例隐藏。", - "xpack.canvas.functions.pie.args.paletteHelpText": "用于描述要在饼图上使用的颜色的 {palette} 对象。请参见 {paletteFn}。", "xpack.canvas.functions.pie.args.radiusHelpText": "饼图的半径,表示为可用空间的百分比(介于 `0` 和 `1` 之间)。要自动设置半径,请使用 {auto}。", "xpack.canvas.functions.pie.args.seriesStyleHelpText": "特定序列的样式", "xpack.canvas.functions.pie.args.tiltHelpText": "倾斜百分比,其中 `1` 为完全垂直,`0` 为完全水平。", "xpack.canvas.functions.pieHelpText": "配置饼图元素。", "xpack.canvas.functions.plot.args.defaultStyleHelpText": "要用于每个序列的默认样式。", "xpack.canvas.functions.plot.args.fontHelpText": "标签的 {CSS} 字体属性。例如 {FONT_FAMILY} 或 {FONT_WEIGHT}。", - "xpack.canvas.functions.plot.args.legendHelpText": "图例位置。例如 {positions} 或 {BOOLEAN_FALSE}。为 {BOOLEAN_FALSE} 时,图例隐藏。", - "xpack.canvas.functions.plot.args.paletteHelpText": "用于描述要在此图表上使用的颜色的 {palette} 对象。请参见 {paletteFn}。", "xpack.canvas.functions.plot.args.seriesStyleHelpText": "特定序列的样式", "xpack.canvas.functions.plot.args.xaxisHelpText": "轴配置。为 {BOOLEAN_FALSE} 时,轴隐藏。", "xpack.canvas.functions.plot.args.yaxisHelpText": "轴配置。为 {BOOLEAN_FALSE} 时,轴隐藏。", @@ -5797,7 +5790,6 @@ "xpack.canvas.functions.shapeHelpText": "创建形状。", "xpack.canvas.functions.sort.args.byHelpText": "排序要依据的列。未指定时,将按第一列排序 `{DATATABLE}`。", "xpack.canvas.functions.sort.args.reverseHelpText": "反转排序顺序。未指定时,将升序排序 `{DATATABLE}`。", - "xpack.canvas.functions.sortHelpText": "按指定列排序数据库。", "xpack.canvas.functions.staticColumn.args.nameHelpText": "新列的名称。", "xpack.canvas.functions.staticColumn.args.valueHelpText": "在每一行新列中要插入的值。提示:使用子表达式将其他列汇总为静态值。", "xpack.canvas.functions.staticColumnHelpText": "在每一行添加具有相同静态值的列。另见 {alterColumnFn} 和 {mapColumnFn}。", From 90bd654d7e7b43b64eaf2053c2803d3b44ba6c72 Mon Sep 17 00:00:00 2001 From: Constance Date: Mon, 24 Aug 2020 15:07:00 -0700 Subject: [PATCH 23/71] [Enterprise Search] Create HttpLogic Kea store, add http interceptors, and manage error connecting at top app-level (#75790) * [Setup] Change error connecting status code to 502 - For clearer error handling * Set up new HttpProvider/Logic Kea store & listeners - This allows us to: - connect() directly to HttpLogic in other Kea logic files that need to make http calls, instead of passing in http manually via args - Set http interceptors & remove them interceptors on unmount within Kea - Share state derived from http (e.g. errorConnecting, readOnlyMode) between both AS & WS (but allow each app to handle that state differently if needed) + Refactors necessary for these changes: - Kea types - add events key, clarify that mount returns an unmount function, fix reducer state type - ReactDOM unmount - remove resetContext({}), was preventing logic from unmounting properly * Update AS & WS to show error connecting component at app level * [WS] Remove errorConnecting logic & http arg from Overview - Since main app is now handling errorConnecting - http can now be connected directly from HttpLogic Kea store, so no need to pass it + minor cleanup in logic_overview.test.ts - remove unneeded unmount(), act(), switch to HttpLogic mock * [AS] Add top-level ErrorConnecting component & remove error logic from EngineOverview * [AS] Clean up/move EngineOverview child components into subfolder - delete old ErrorState component - move LoadingState, EmptyState, and EngineOverviewHeader into subfolders in engine_overview * PR feedback: Update test assertions 404 copy --- .../components/empty_state.scss} | 0 .../components/empty_state.test.tsx} | 27 +---- .../components}/empty_state.tsx | 12 +- .../components/header.test.tsx} | 8 +- .../components/header.tsx} | 4 +- .../components}/index.ts | 2 +- .../components/loading_state.test.tsx | 19 +++ .../components}/loading_state.tsx | 8 +- .../engine_overview/engine_overview.test.tsx | 12 +- .../engine_overview/engine_overview.tsx | 18 +-- .../error_connecting.test.tsx | 19 +++ .../error_connecting.tsx} | 6 +- .../index.ts | 2 +- .../applications/app_search/index.test.tsx | 21 +++- .../public/applications/app_search/index.tsx | 23 ++-- .../public/applications/index.tsx | 5 +- .../shared/http/http_logic.test.ts | 110 ++++++++++++++++++ .../applications/shared/http/http_logic.ts | 86 ++++++++++++++ .../shared/http/http_provider.test.tsx | 44 +++++++ .../shared/http/http_provider.tsx | 28 +++++ .../public/applications/shared/http/index.ts | 8 ++ .../public/applications/shared/types.ts | 8 +- .../overview/__mocks__/overview_logic.mock.ts | 1 - .../components/overview/overview.test.tsx | 8 -- .../components/overview/overview.tsx | 11 +- .../overview/overview_logic.test.ts | 52 +-------- .../components/overview/overview_logic.ts | 27 +---- .../workplace_search/index.test.tsx | 49 ++++---- .../applications/workplace_search/index.tsx | 23 ++-- .../enterprise_search/public/plugin.ts | 2 + .../server/routes/app_search/engines.test.ts | 10 +- .../server/routes/app_search/engines.ts | 2 +- .../routes/workplace_search/overview.test.ts | 10 +- .../routes/workplace_search/overview.ts | 2 +- 34 files changed, 460 insertions(+), 207 deletions(-) rename x-pack/plugins/enterprise_search/public/applications/app_search/components/{empty_states/empty_states.scss => engine_overview/components/empty_state.scss} (100%) rename x-pack/plugins/enterprise_search/public/applications/app_search/components/{empty_states/empty_states.test.tsx => engine_overview/components/empty_state.test.tsx} (55%) rename x-pack/plugins/enterprise_search/public/applications/app_search/components/{empty_states => engine_overview/components}/empty_state.tsx (84%) rename x-pack/plugins/enterprise_search/public/applications/app_search/components/{engine_overview_header/engine_overview_header.test.tsx => engine_overview/components/header.test.tsx} (77%) rename x-pack/plugins/enterprise_search/public/applications/app_search/components/{engine_overview_header/engine_overview_header.tsx => engine_overview/components/header.tsx} (92%) rename x-pack/plugins/enterprise_search/public/applications/app_search/components/{empty_states => engine_overview/components}/index.ts (87%) create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/loading_state.test.tsx rename x-pack/plugins/enterprise_search/public/applications/app_search/components/{empty_states => engine_overview/components}/loading_state.tsx (73%) create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/error_connecting/error_connecting.test.tsx rename x-pack/plugins/enterprise_search/public/applications/app_search/components/{empty_states/error_state.tsx => error_connecting/error_connecting.tsx} (81%) rename x-pack/plugins/enterprise_search/public/applications/app_search/components/{engine_overview_header => error_connecting}/index.ts (78%) create mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/http/http_logic.test.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/http/http_logic.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/http/http_provider.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/http/http_provider.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/http/index.ts diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/empty_states/empty_states.scss b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/empty_state.scss similarity index 100% rename from x-pack/plugins/enterprise_search/public/applications/app_search/components/empty_states/empty_states.scss rename to x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/empty_state.scss diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/empty_states/empty_states.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/empty_state.test.tsx similarity index 55% rename from x-pack/plugins/enterprise_search/public/applications/app_search/components/empty_states/empty_states.test.tsx rename to x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/empty_state.test.tsx index 25a9fa7430c40..7e6876bc9b3a4 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/empty_states/empty_states.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/empty_state.test.tsx @@ -4,28 +4,19 @@ * you may not use this file except in compliance with the Elastic License. */ -import '../../../__mocks__/shallow_usecontext.mock'; +import '../../../../__mocks__/shallow_usecontext.mock'; import React from 'react'; import { shallow } from 'enzyme'; -import { EuiEmptyPrompt, EuiButton, EuiLoadingContent } from '@elastic/eui'; -import { ErrorStatePrompt } from '../../../shared/error_state'; +import { EuiEmptyPrompt, EuiButton } from '@elastic/eui'; -jest.mock('../../../shared/telemetry', () => ({ +jest.mock('../../../../shared/telemetry', () => ({ sendTelemetry: jest.fn(), SendAppSearchTelemetry: jest.fn(), })); -import { sendTelemetry } from '../../../shared/telemetry'; +import { sendTelemetry } from '../../../../shared/telemetry'; -import { ErrorState, EmptyState, LoadingState } from './'; - -describe('ErrorState', () => { - it('renders', () => { - const wrapper = shallow(); - - expect(wrapper.find(ErrorStatePrompt)).toHaveLength(1); - }); -}); +import { EmptyState } from './'; describe('EmptyState', () => { it('renders', () => { @@ -44,11 +35,3 @@ describe('EmptyState', () => { (sendTelemetry as jest.Mock).mockClear(); }); }); - -describe('LoadingState', () => { - it('renders', () => { - const wrapper = shallow(); - - expect(wrapper.find(EuiLoadingContent)).toHaveLength(2); - }); -}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/empty_states/empty_state.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/empty_state.tsx similarity index 84% rename from x-pack/plugins/enterprise_search/public/applications/app_search/components/empty_states/empty_state.tsx rename to x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/empty_state.tsx index 9b0edb423bc52..58691cf09b4a5 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/empty_states/empty_state.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/empty_state.tsx @@ -8,14 +8,14 @@ import React, { useContext } from 'react'; import { EuiPageContent, EuiEmptyPrompt, EuiButton } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { sendTelemetry } from '../../../shared/telemetry'; -import { SetAppSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome'; -import { KibanaContext, IKibanaContext } from '../../../index'; -import { CREATE_ENGINES_PATH } from '../../routes'; +import { sendTelemetry } from '../../../../shared/telemetry'; +import { SetAppSearchChrome as SetPageChrome } from '../../../../shared/kibana_chrome'; +import { KibanaContext, IKibanaContext } from '../../../../index'; +import { CREATE_ENGINES_PATH } from '../../../routes'; -import { EngineOverviewHeader } from '../engine_overview_header'; +import { EngineOverviewHeader } from './header'; -import './empty_states.scss'; +import './empty_state.scss'; export const EmptyState: React.FC = () => { const { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview_header/engine_overview_header.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/header.test.tsx similarity index 77% rename from x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview_header/engine_overview_header.test.tsx rename to x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/header.test.tsx index 7d2106f2a56f7..7f22ce132d405 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview_header/engine_overview_header.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/header.test.tsx @@ -4,15 +4,15 @@ * you may not use this file except in compliance with the Elastic License. */ -import '../../../__mocks__/shallow_usecontext.mock'; +import '../../../../__mocks__/shallow_usecontext.mock'; import React from 'react'; import { shallow } from 'enzyme'; -jest.mock('../../../shared/telemetry', () => ({ sendTelemetry: jest.fn() })); -import { sendTelemetry } from '../../../shared/telemetry'; +jest.mock('../../../../shared/telemetry', () => ({ sendTelemetry: jest.fn() })); +import { sendTelemetry } from '../../../../shared/telemetry'; -import { EngineOverviewHeader } from '../engine_overview_header'; +import { EngineOverviewHeader } from './'; describe('EngineOverviewHeader', () => { it('renders', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview_header/engine_overview_header.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/header.tsx similarity index 92% rename from x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview_header/engine_overview_header.tsx rename to x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/header.tsx index 7f67d00f5df91..1a1ae295d4828 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview_header/engine_overview_header.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/header.tsx @@ -15,8 +15,8 @@ import { } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { sendTelemetry } from '../../../shared/telemetry'; -import { KibanaContext, IKibanaContext } from '../../../index'; +import { sendTelemetry } from '../../../../shared/telemetry'; +import { KibanaContext, IKibanaContext } from '../../../../index'; export const EngineOverviewHeader: React.FC = () => { const { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/empty_states/index.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/index.ts similarity index 87% rename from x-pack/plugins/enterprise_search/public/applications/app_search/components/empty_states/index.ts rename to x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/index.ts index e92bf214c4cc7..794053f184f8c 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/empty_states/index.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/index.ts @@ -4,6 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ +export { EngineOverviewHeader } from './header'; export { LoadingState } from './loading_state'; export { EmptyState } from './empty_state'; -export { ErrorState } from './error_state'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/loading_state.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/loading_state.test.tsx new file mode 100644 index 0000000000000..c894500550a0b --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/loading_state.test.tsx @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { shallow } from 'enzyme'; +import { EuiLoadingContent } from '@elastic/eui'; + +import { LoadingState } from './'; + +describe('LoadingState', () => { + it('renders', () => { + const wrapper = shallow(); + + expect(wrapper.find(EuiLoadingContent)).toHaveLength(2); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/empty_states/loading_state.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/loading_state.tsx similarity index 73% rename from x-pack/plugins/enterprise_search/public/applications/app_search/components/empty_states/loading_state.tsx rename to x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/loading_state.tsx index 221091b79dc54..07643560df3c7 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/empty_states/loading_state.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/loading_state.tsx @@ -7,17 +7,15 @@ import React from 'react'; import { EuiPageContent, EuiSpacer, EuiLoadingContent } from '@elastic/eui'; -import { SetAppSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome'; -import { EngineOverviewHeader } from '../engine_overview_header'; - -import './empty_states.scss'; +import { SetAppSearchChrome as SetPageChrome } from '../../../../shared/kibana_chrome'; +import { EngineOverviewHeader } from './header'; export const LoadingState: React.FC = () => { return ( <> - + diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.test.tsx index 45ab5dc5b9ab1..c2379fb33bd71 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.test.tsx @@ -12,7 +12,7 @@ import { shallow, ReactWrapper } from 'enzyme'; import { mountWithAsyncContext, mockKibanaContext } from '../../../__mocks__'; -import { LoadingState, EmptyState, ErrorState } from '../empty_states'; +import { LoadingState, EmptyState } from './components'; import { EngineTable } from './engine_table'; import { EngineOverview } from './'; @@ -40,16 +40,6 @@ describe('EngineOverview', () => { expect(wrapper.find(EmptyState)).toHaveLength(1); }); - - it('hasErrorConnecting', async () => { - const wrapper = await mountWithAsyncContext(, { - http: { - ...mockHttp, - get: () => ({ invalidPayload: true }), - }, - }); - expect(wrapper.find(ErrorState)).toHaveLength(1); - }); }); describe('happy-path states', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.tsx index acac5d17665b7..74bcd9aeafb28 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.tsx @@ -22,8 +22,7 @@ import { KibanaContext, IKibanaContext } from '../../../index'; import EnginesIcon from '../../assets/engine.svg'; import MetaEnginesIcon from '../../assets/meta_engine.svg'; -import { LoadingState, EmptyState, ErrorState } from '../empty_states'; -import { EngineOverviewHeader } from '../engine_overview_header'; +import { EngineOverviewHeader, LoadingState, EmptyState } from './components'; import { EngineTable } from './engine_table'; import './engine_overview.scss'; @@ -42,8 +41,6 @@ export const EngineOverview: React.FC = () => { const { license } = useContext(LicenseContext) as ILicenseContext; const [isLoading, setIsLoading] = useState(true); - const [hasErrorConnecting, setHasErrorConnecting] = useState(false); - const [engines, setEngines] = useState([]); const [enginesPage, setEnginesPage] = useState(1); const [enginesTotal, setEnginesTotal] = useState(0); @@ -57,16 +54,12 @@ export const EngineOverview: React.FC = () => { }); }; const setEnginesData = async (params: IGetEnginesParams, callbacks: ISetEnginesCallbacks) => { - try { - const response = await getEnginesData(params); + const response = await getEnginesData(params); - callbacks.setResults(response.results); - callbacks.setResultsTotal(response.meta.page.total_results); + callbacks.setResults(response.results); + callbacks.setResultsTotal(response.meta.page.total_results); - setIsLoading(false); - } catch (error) { - setHasErrorConnecting(true); - } + setIsLoading(false); }; useEffect(() => { @@ -85,7 +78,6 @@ export const EngineOverview: React.FC = () => { } }, [license, metaEnginesPage]); - if (hasErrorConnecting) return ; if (isLoading) return ; if (!engines.length) return ; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/error_connecting/error_connecting.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/error_connecting/error_connecting.test.tsx new file mode 100644 index 0000000000000..8d48875a8e1f5 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/error_connecting/error_connecting.test.tsx @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { shallow } from 'enzyme'; + +import { ErrorStatePrompt } from '../../../shared/error_state'; +import { ErrorConnecting } from './'; + +describe('ErrorConnecting', () => { + it('renders', () => { + const wrapper = shallow(); + + expect(wrapper.find(ErrorStatePrompt)).toHaveLength(1); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/empty_states/error_state.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/error_connecting/error_connecting.tsx similarity index 81% rename from x-pack/plugins/enterprise_search/public/applications/app_search/components/empty_states/error_state.tsx rename to x-pack/plugins/enterprise_search/public/applications/app_search/components/error_connecting/error_connecting.tsx index c5a5f1fbb921f..34eb76d11a663 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/empty_states/error_state.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/error_connecting/error_connecting.tsx @@ -10,17 +10,13 @@ import { EuiPageContent } from '@elastic/eui'; import { ErrorStatePrompt } from '../../../shared/error_state'; import { SetAppSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome'; import { SendAppSearchTelemetry as SendTelemetry } from '../../../shared/telemetry'; -import { EngineOverviewHeader } from '../engine_overview_header'; -import './empty_states.scss'; - -export const ErrorState: React.FC = () => { +export const ErrorConnecting: React.FC = () => { return ( <> - diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview_header/index.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/error_connecting/index.ts similarity index 78% rename from x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview_header/index.ts rename to x-pack/plugins/enterprise_search/public/applications/app_search/components/error_connecting/index.ts index 2d37f037e21e5..c8b71e1a6e791 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview_header/index.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/error_connecting/index.ts @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export { EngineOverviewHeader } from './engine_overview_header'; +export { ErrorConnecting } from './error_connecting'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/index.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/index.test.tsx index 0f4072c591bc7..94e9127bbed74 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/index.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/index.test.tsx @@ -12,8 +12,10 @@ import { Redirect } from 'react-router-dom'; import { shallow } from 'enzyme'; import { useValues, useActions } from 'kea'; -import { SetupGuide } from './components/setup_guide'; import { Layout, SideNav, SideNavLink } from '../shared/layout'; +import { SetupGuide } from './components/setup_guide'; +import { ErrorConnecting } from './components/error_connecting'; +import { EngineOverview } from './components/engine_overview'; import { AppSearch, AppSearchUnconfigured, AppSearchConfigured, AppSearchNav } from './'; describe('AppSearch', () => { @@ -42,12 +44,17 @@ describe('AppSearchUnconfigured', () => { }); describe('AppSearchConfigured', () => { - it('renders with layout', () => { + beforeEach(() => { + // Mock resets + (useValues as jest.Mock).mockImplementation(() => ({})); (useActions as jest.Mock).mockImplementation(() => ({ initializeAppData: () => {} })); + }); + it('renders with layout', () => { const wrapper = shallow(); expect(wrapper.find(Layout)).toHaveLength(1); + expect(wrapper.find(EngineOverview)).toHaveLength(1); }); it('initializes app data with passed props', () => { @@ -62,12 +69,20 @@ describe('AppSearchConfigured', () => { it('does not re-initialize app data', () => { const initializeAppData = jest.fn(); (useActions as jest.Mock).mockImplementation(() => ({ initializeAppData })); - (useValues as jest.Mock).mockImplementationOnce(() => ({ hasInitialized: true })); + (useValues as jest.Mock).mockImplementation(() => ({ hasInitialized: true })); shallow(); expect(initializeAppData).not.toHaveBeenCalled(); }); + + it('renders ErrorConnecting', () => { + (useValues as jest.Mock).mockImplementation(() => ({ errorConnecting: true })); + + const wrapper = shallow(); + + expect(wrapper.find(ErrorConnecting)).toHaveLength(1); + }); }); describe('AppSearchNav', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/index.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/index.tsx index 5f4734630624c..234201a157ec9 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/index.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/index.tsx @@ -11,6 +11,7 @@ import { useActions, useValues } from 'kea'; import { i18n } from '@kbn/i18n'; import { KibanaContext, IKibanaContext } from '../index'; +import { HttpLogic, IHttpLogicValues } from '../shared/http'; import { AppLogic, IAppLogicActions, IAppLogicValues } from './app_logic'; import { IInitialAppData } from '../../../common/types'; @@ -27,6 +28,7 @@ import { } from './routes'; import { SetupGuide } from './components/setup_guide'; +import { ErrorConnecting } from './components/error_connecting'; import { EngineOverview } from './components/engine_overview'; export const AppSearch: React.FC = (props) => { @@ -48,6 +50,7 @@ export const AppSearchUnconfigured: React.FC = () => ( export const AppSearchConfigured: React.FC = (props) => { const { hasInitialized } = useValues(AppLogic) as IAppLogicValues; const { initializeAppData } = useActions(AppLogic) as IAppLogicActions; + const { errorConnecting } = useValues(HttpLogic) as IHttpLogicValues; useEffect(() => { if (!hasInitialized) initializeAppData(props); @@ -60,14 +63,18 @@ export const AppSearchConfigured: React.FC = (props) => { }> - - - - - - - - + {errorConnecting ? ( + + ) : ( + + + + + + + + + )} diff --git a/x-pack/plugins/enterprise_search/public/applications/index.tsx b/x-pack/plugins/enterprise_search/public/applications/index.tsx index d6cc6e81509b2..60e4cedf413f2 100644 --- a/x-pack/plugins/enterprise_search/public/applications/index.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/index.tsx @@ -22,6 +22,7 @@ import { } from 'src/core/public'; import { ClientConfigType, ClientData, PluginsSetup } from '../plugin'; import { LicenseProvider } from './shared/licensing'; +import { HttpProvider } from './shared/http'; import { IExternalUrl } from './shared/enterprise_search_url'; import { IInitialAppData } from '../../common/types'; @@ -48,7 +49,7 @@ export const renderApp = ( core: CoreStart, plugins: PluginsSetup, config: ClientConfigType, - { externalUrl, ...initialData }: ClientData + { externalUrl, errorConnecting, ...initialData }: ClientData ) => { resetContext({ createStore: true }); const store = getContext().store as Store; @@ -67,6 +68,7 @@ export const renderApp = ( > + @@ -77,7 +79,6 @@ export const renderApp = ( params.element ); return () => { - resetContext({}); ReactDOM.unmountComponentAtNode(params.element); }; }; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/http/http_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/shared/http/http_logic.test.ts new file mode 100644 index 0000000000000..a6957340d33d3 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/http/http_logic.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { resetContext } from 'kea'; + +import { httpServiceMock } from 'src/core/public/mocks'; + +import { HttpLogic } from './http_logic'; + +describe('HttpLogic', () => { + const mockHttp = httpServiceMock.createSetupContract(); + const DEFAULT_VALUES = { + http: null, + httpInterceptors: [], + errorConnecting: false, + }; + + beforeEach(() => { + jest.clearAllMocks(); + resetContext({}); + }); + + it('has expected default values', () => { + HttpLogic.mount(); + expect(HttpLogic.values).toEqual(DEFAULT_VALUES); + }); + + describe('initializeHttp()', () => { + it('sets values based on passed props', () => { + HttpLogic.mount(); + HttpLogic.actions.initializeHttp({ http: mockHttp, errorConnecting: true }); + + expect(HttpLogic.values).toEqual({ + http: mockHttp, + httpInterceptors: [], + errorConnecting: true, + }); + }); + }); + + describe('setErrorConnecting()', () => { + it('sets errorConnecting value', () => { + HttpLogic.mount(); + HttpLogic.actions.setErrorConnecting(true); + expect(HttpLogic.values.errorConnecting).toEqual(true); + + HttpLogic.actions.setErrorConnecting(false); + expect(HttpLogic.values.errorConnecting).toEqual(false); + }); + }); + + describe('http interceptors', () => { + describe('initializeHttpInterceptors()', () => { + beforeEach(() => { + HttpLogic.mount(); + jest.spyOn(HttpLogic.actions, 'setHttpInterceptors'); + jest.spyOn(HttpLogic.actions, 'setErrorConnecting'); + HttpLogic.actions.initializeHttp({ http: mockHttp }); + + HttpLogic.actions.initializeHttpInterceptors(); + }); + + it('calls http.intercept and sets an array of interceptors', () => { + mockHttp.intercept.mockImplementationOnce(() => 'removeInterceptorFn' as any); + HttpLogic.actions.initializeHttpInterceptors(); + + expect(mockHttp.intercept).toHaveBeenCalled(); + expect(HttpLogic.actions.setHttpInterceptors).toHaveBeenCalledWith(['removeInterceptorFn']); + }); + + describe('errorConnectingInterceptor', () => { + it('handles errors connecting to Enterprise Search', async () => { + const { responseError } = mockHttp.intercept.mock.calls[0][0] as any; + await responseError({ response: { url: '/api/app_search/engines', status: 502 } }); + + expect(HttpLogic.actions.setErrorConnecting).toHaveBeenCalled(); + }); + + it('does not handle non-502 Enterprise Search errors', async () => { + const { responseError } = mockHttp.intercept.mock.calls[0][0] as any; + await responseError({ response: { url: '/api/workplace_search/overview', status: 404 } }); + + expect(HttpLogic.actions.setErrorConnecting).not.toHaveBeenCalled(); + }); + + it('does not handle errors for unrelated calls', async () => { + const { responseError } = mockHttp.intercept.mock.calls[0][0] as any; + await responseError({ response: { url: '/api/some_other_plugin/', status: 502 } }); + + expect(HttpLogic.actions.setErrorConnecting).not.toHaveBeenCalled(); + }); + }); + }); + + it('sets httpInterceptors and calls all valid remove functions on unmount', () => { + const unmount = HttpLogic.mount(); + const httpInterceptors = [jest.fn(), undefined, jest.fn()] as any; + + HttpLogic.actions.setHttpInterceptors(httpInterceptors); + expect(HttpLogic.values.httpInterceptors).toEqual(httpInterceptors); + + unmount(); + expect(httpInterceptors[0]).toHaveBeenCalledTimes(1); + expect(httpInterceptors[2]).toHaveBeenCalledTimes(1); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/http/http_logic.ts b/x-pack/plugins/enterprise_search/public/applications/shared/http/http_logic.ts new file mode 100644 index 0000000000000..7bf7a19ed451f --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/http/http_logic.ts @@ -0,0 +1,86 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { kea } from 'kea'; + +import { HttpSetup } from 'src/core/public'; + +import { IKeaLogic, IKeaParams, TKeaReducers } from '../../shared/types'; + +export interface IHttpLogicValues { + http: HttpSetup; + httpInterceptors: Function[]; + errorConnecting: boolean; +} +export interface IHttpLogicActions { + initializeHttp({ http, errorConnecting }: { http: HttpSetup; errorConnecting?: boolean }): void; + initializeHttpInterceptors(): void; + setHttpInterceptors(httpInterceptors: Function[]): void; + setErrorConnecting(errorConnecting: boolean): void; +} + +export const HttpLogic = kea({ + actions: (): IHttpLogicActions => ({ + initializeHttp: ({ http, errorConnecting }) => ({ http, errorConnecting }), + initializeHttpInterceptors: () => null, + setHttpInterceptors: (httpInterceptors) => ({ httpInterceptors }), + setErrorConnecting: (errorConnecting) => ({ errorConnecting }), + }), + reducers: (): TKeaReducers => ({ + http: [ + (null as unknown) as HttpSetup, + { + initializeHttp: (_, { http }) => http, + }, + ], + httpInterceptors: [ + [], + { + setHttpInterceptors: (_, { httpInterceptors }) => httpInterceptors, + }, + ], + errorConnecting: [ + false, + { + initializeHttp: (_, { errorConnecting }) => !!errorConnecting, + setErrorConnecting: (_, { errorConnecting }) => errorConnecting, + }, + ], + }), + listeners: ({ values, actions }) => ({ + initializeHttpInterceptors: () => { + const httpInterceptors = []; + + const errorConnectingInterceptor = values.http.intercept({ + responseError: async (httpResponse) => { + const { url, status } = httpResponse.response!; + const hasErrorConnecting = status === 502; + const isApiResponse = + url.includes('/api/app_search/') || url.includes('/api/workplace_search/'); + + if (isApiResponse && hasErrorConnecting) { + actions.setErrorConnecting(true); + } + return httpResponse; + }, + }); + httpInterceptors.push(errorConnectingInterceptor); + + // TODO: Read only mode interceptor + actions.setHttpInterceptors(httpInterceptors); + }, + }), + events: ({ values }) => ({ + beforeUnmount: () => { + values.httpInterceptors.forEach((removeInterceptorFn?: Function) => { + if (removeInterceptorFn) removeInterceptorFn(); + }); + }, + }), +} as IKeaParams) as IKeaLogic< + IHttpLogicValues, + IHttpLogicActions +>; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/http/http_provider.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/http/http_provider.test.tsx new file mode 100644 index 0000000000000..81106235780d6 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/http/http_provider.test.tsx @@ -0,0 +1,44 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import '../../__mocks__/shallow_usecontext.mock'; +import '../../__mocks__/kea.mock'; + +import React from 'react'; +import { shallow } from 'enzyme'; +import { useActions } from 'kea'; + +import { HttpProvider } from './'; + +describe('HttpProvider', () => { + const props = { + http: {} as any, + errorConnecting: false, + }; + const initializeHttp = jest.fn(); + const initializeHttpInterceptors = jest.fn(); + + beforeEach(() => { + jest.clearAllMocks(); + (useActions as jest.Mock).mockImplementationOnce(() => ({ + initializeHttp, + initializeHttpInterceptors, + })); + }); + + it('does not render', () => { + const wrapper = shallow(); + + expect(wrapper.isEmptyRender()).toBe(true); + }); + + it('calls initialization actions on mount', () => { + shallow(); + + expect(initializeHttp).toHaveBeenCalledWith(props); + expect(initializeHttpInterceptors).toHaveBeenCalled(); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/http/http_provider.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/http/http_provider.tsx new file mode 100644 index 0000000000000..6febc1869054f --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/http/http_provider.tsx @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useEffect } from 'react'; +import { useActions } from 'kea'; + +import { HttpSetup } from 'src/core/public'; + +import { HttpLogic, IHttpLogicActions } from './http_logic'; + +interface IHttpProviderProps { + http: HttpSetup; + errorConnecting?: boolean; +} + +export const HttpProvider: React.FC = (props) => { + const { initializeHttp, initializeHttpInterceptors } = useActions(HttpLogic) as IHttpLogicActions; + + useEffect(() => { + initializeHttp(props); + initializeHttpInterceptors(); + }, []); + + return null; +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/http/index.ts b/x-pack/plugins/enterprise_search/public/applications/shared/http/index.ts new file mode 100644 index 0000000000000..449ff9d56debf --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/http/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { HttpLogic, IHttpLogicValues, IHttpLogicActions } from './http_logic'; +export { HttpProvider } from './http_provider'; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/types.ts b/x-pack/plugins/enterprise_search/public/applications/shared/types.ts index 74bb53ef3a954..a8e08323c5e3b 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/types.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/types.ts @@ -14,7 +14,7 @@ export interface IFlashMessagesProps { } export interface IKeaLogic { - mount(): void; + mount(): Function; values: IKeaValues; actions: IKeaActions; } @@ -33,6 +33,7 @@ export interface IKeaLogic { export interface IKeaParams { selectors?(params: { selectors: IKeaValues }): void; listeners?(params: { actions: IKeaActions; values: IKeaValues }): void; + events?(params: { actions: IKeaActions; values: IKeaValues }): void; } /** @@ -47,7 +48,10 @@ export type TKeaReducers = { [Value in keyof IKeaValues]?: [ IKeaValues[Value], { - [Action in keyof IKeaActions]?: (state: IKeaValues, payload: IKeaValues) => IKeaValues[Value]; + [Action in keyof IKeaActions]?: ( + state: IKeaValues[Value], + payload: IKeaValues + ) => IKeaValues[Value]; } ]; }; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/__mocks__/overview_logic.mock.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/__mocks__/overview_logic.mock.ts index 395d2044e7dbc..5588c4fc53b67 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/__mocks__/overview_logic.mock.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/__mocks__/overview_logic.mock.ts @@ -22,7 +22,6 @@ export const mockLogicValues = { personalSourcesCount: 0, sourcesCount: 0, dataLoading: true, - hasErrorConnecting: false, flashMessages: {}, } as IOverviewValues; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/overview.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/overview.test.tsx index 744fd8aeb1951..fee966a56923d 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/overview.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/overview.test.tsx @@ -11,7 +11,6 @@ import { mockLogicActions, setMockValues } from './__mocks__'; import React from 'react'; import { shallow, mount } from 'enzyme'; -import { ErrorState } from '../error_state'; import { Loading } from '../shared/loading'; import { ViewContentHeader } from '../shared/view_content_header'; @@ -27,13 +26,6 @@ describe('Overview', () => { expect(wrapper.find(Loading)).toHaveLength(1); }); - - it('hasErrorConnecting', () => { - setMockValues({ hasErrorConnecting: true }); - const wrapper = shallow(); - - expect(wrapper.find(ErrorState)).toHaveLength(1); - }); }); describe('happy-path states', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/overview.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/overview.tsx index b816eb2973207..6aa3e1e608bfe 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/overview.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/overview.tsx @@ -6,19 +6,16 @@ // TODO: Remove EuiPage & EuiPageBody before exposing full app -import React, { useContext, useEffect } from 'react'; +import React, { useEffect } from 'react'; import { EuiPage, EuiPageBody, EuiSpacer } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { useActions, useValues } from 'kea'; import { SetWorkplaceSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome'; import { SendWorkplaceSearchTelemetry as SendTelemetry } from '../../../shared/telemetry'; -import { KibanaContext, IKibanaContext } from '../../../index'; import { OverviewLogic, IOverviewActions, IOverviewValues } from './overview_logic'; -import { ErrorState } from '../error_state'; - import { Loading } from '../shared/loading'; import { ProductButton } from '../shared/product_button'; import { ViewContentHeader } from '../shared/view_content_header'; @@ -47,13 +44,10 @@ const HEADER_DESCRIPTION = i18n.translate( ); export const Overview: React.FC = () => { - const { http } = useContext(KibanaContext) as IKibanaContext; - const { initializeOverview } = useActions(OverviewLogic) as IOverviewActions; const { dataLoading, - hasErrorConnecting, hasUsers, hasOrgSources, isOldAccount, @@ -61,10 +55,9 @@ export const Overview: React.FC = () => { } = useValues(OverviewLogic) as IOverviewValues; useEffect(() => { - initializeOverview({ http }); + initializeOverview(); }, [initializeOverview]); - if (hasErrorConnecting) return ; if (dataLoading) return ; const hideOnboarding = hasUsers && hasOrgSources && isOldAccount && orgName !== defaultOrgName; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/overview_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/overview_logic.test.ts index 7df4de4719f31..3fbf0e60b5b49 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/overview_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/overview_logic.test.ts @@ -5,24 +5,18 @@ */ import { resetContext } from 'kea'; -import { act } from 'react-dom/test-utils'; -import { mockKibanaContext } from '../../../__mocks__'; +jest.mock('../../../shared/http', () => ({ HttpLogic: { values: { http: { get: jest.fn() } } } })); +import { HttpLogic } from '../../../shared/http'; import { mockLogicValues } from './__mocks__'; import { OverviewLogic } from './overview_logic'; describe('OverviewLogic', () => { - let unmount: any; - beforeEach(() => { - resetContext({}); - unmount = OverviewLogic.mount() as any; jest.clearAllMocks(); - }); - - afterEach(() => { - unmount(); + resetContext({}); + OverviewLogic.mount(); }); it('has expected default values', () => { @@ -91,48 +85,14 @@ describe('OverviewLogic', () => { }); }); - describe('setHasErrorConnecting', () => { - it('will set `hasErrorConnecting`', () => { - OverviewLogic.actions.setHasErrorConnecting(true); - - expect(OverviewLogic.values.hasErrorConnecting).toEqual(true); - expect(OverviewLogic.values.dataLoading).toEqual(false); - }); - }); - describe('initializeOverview', () => { it('calls API and sets values', async () => { - const mockHttp = mockKibanaContext.http; - const mockApi = jest.fn(() => mockLogicValues as any); const setServerDataSpy = jest.spyOn(OverviewLogic.actions, 'setServerData'); - await act(async () => - OverviewLogic.actions.initializeOverview({ - http: { - ...mockHttp, - get: mockApi, - }, - }) - ); + await OverviewLogic.actions.initializeOverview(); - expect(mockApi).toHaveBeenCalledWith('/api/workplace_search/overview'); + expect(HttpLogic.values.http.get).toHaveBeenCalledWith('/api/workplace_search/overview'); expect(setServerDataSpy).toHaveBeenCalled(); }); - - it('handles error state', async () => { - const mockHttp = mockKibanaContext.http; - const setHasErrorConnectingSpy = jest.spyOn(OverviewLogic.actions, 'setHasErrorConnecting'); - - await act(async () => - OverviewLogic.actions.initializeOverview({ - http: { - ...mockHttp, - get: () => Promise.reject(), - }, - }) - ); - - expect(setHasErrorConnectingSpy).toHaveBeenCalled(); - }); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/overview_logic.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/overview_logic.ts index 8bb177a2e742b..057bce1b4056c 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/overview_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/overview_logic.ts @@ -4,9 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { HttpSetup } from 'src/core/public'; - import { kea } from 'kea'; +import { HttpLogic } from '../../../shared/http'; import { IAccount, IOrganization } from '../../types'; import { IFlashMessagesProps, IKeaLogic, TKeaReducers, IKeaParams } from '../../../shared/types'; @@ -32,13 +31,11 @@ export interface IOverviewServerData { export interface IOverviewActions { setServerData(serverData: IOverviewServerData): void; setFlashMessages(flashMessages: IFlashMessagesProps): void; - setHasErrorConnecting(hasErrorConnecting: boolean): void; - initializeOverview({ http }: { http: HttpSetup }): void; + initializeOverview(): void; } export interface IOverviewValues extends IOverviewServerData { dataLoading: boolean; - hasErrorConnecting: boolean; flashMessages: IFlashMessagesProps; } @@ -46,8 +43,7 @@ export const OverviewLogic = kea({ actions: (): IOverviewActions => ({ setServerData: (serverData) => serverData, setFlashMessages: (flashMessages) => ({ flashMessages }), - setHasErrorConnecting: (hasErrorConnecting) => ({ hasErrorConnecting }), - initializeOverview: ({ http }) => ({ http }), + initializeOverview: () => null, }), reducers: (): TKeaReducers => ({ organization: [ @@ -138,24 +134,13 @@ export const OverviewLogic = kea({ true, { setServerData: () => false, - setHasErrorConnecting: () => false, - }, - ], - hasErrorConnecting: [ - false, - { - setHasErrorConnecting: (_, { hasErrorConnecting }) => hasErrorConnecting, }, ], }), listeners: ({ actions }): Partial => ({ - initializeOverview: async ({ http }: { http: HttpSetup }) => { - try { - const response = await http.get('/api/workplace_search/overview'); - actions.setServerData(response); - } catch (error) { - actions.setHasErrorConnecting(true); - } + initializeOverview: async () => { + const response = await HttpLogic.values.http.get('/api/workplace_search/overview'); + actions.setServerData(response); }, }), } as IKeaParams) as IKeaLogic; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.test.tsx index a55ff64014130..654f4dce0ebf3 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.test.tsx @@ -5,35 +5,44 @@ */ import '../__mocks__/shallow_usecontext.mock'; +import '../__mocks__/kea.mock'; import React, { useContext } from 'react'; import { Redirect } from 'react-router-dom'; import { shallow } from 'enzyme'; +import { useValues } from 'kea'; import { Overview } from './components/overview'; +import { ErrorState } from './components/error_state'; import { WorkplaceSearch } from './'; describe('Workplace Search', () => { - describe('/', () => { - it('redirects to Setup Guide when enterpriseSearchUrl is not set', () => { - (useContext as jest.Mock).mockImplementationOnce(() => ({ - config: { host: '' }, - })); - const wrapper = shallow(); - - expect(wrapper.find(Redirect)).toHaveLength(1); - expect(wrapper.find(Overview)).toHaveLength(0); - }); - - it('renders the Overview when enterpriseSearchUrl is set', () => { - (useContext as jest.Mock).mockImplementationOnce(() => ({ - config: { host: 'https://foo.bar' }, - })); - const wrapper = shallow(); - - expect(wrapper.find(Overview)).toHaveLength(1); - expect(wrapper.find(Redirect)).toHaveLength(0); - }); + it('redirects to Setup Guide when enterpriseSearchUrl is not set', () => { + (useContext as jest.Mock).mockImplementationOnce(() => ({ + config: { host: '' }, + })); + const wrapper = shallow(); + + expect(wrapper.find(Redirect)).toHaveLength(1); + expect(wrapper.find(Overview)).toHaveLength(0); + }); + + it('renders the Overview when enterpriseSearchUrl is set', () => { + (useContext as jest.Mock).mockImplementationOnce(() => ({ + config: { host: 'https://foo.bar' }, + })); + const wrapper = shallow(); + + expect(wrapper.find(Overview)).toHaveLength(1); + expect(wrapper.find(Redirect)).toHaveLength(0); + }); + + it('renders ErrorState when the app cannot connect to Enterprise Search', () => { + (useValues as jest.Mock).mockImplementationOnce(() => ({ errorConnecting: true })); + const wrapper = shallow(); + + expect(wrapper.find(ErrorState).exists()).toBe(true); + expect(wrapper.find(Overview)).toHaveLength(0); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx index 94462aa8de7d1..b261c83e30dde 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx @@ -6,19 +6,24 @@ import React, { useContext } from 'react'; import { Route, Redirect, Switch } from 'react-router-dom'; +import { useValues } from 'kea'; import { IInitialAppData } from '../../../common/types'; import { KibanaContext, IKibanaContext } from '../index'; +import { HttpLogic, IHttpLogicValues } from '../shared/http'; import { Layout } from '../shared/layout'; import { WorkplaceSearchNav } from './components/layout/nav'; import { SETUP_GUIDE_PATH } from './routes'; import { SetupGuide } from './components/setup_guide'; +import { ErrorState } from './components/error_state'; import { Overview } from './components/overview'; export const WorkplaceSearch: React.FC = (props) => { const { config } = useContext(KibanaContext) as IKibanaContext; + const { errorConnecting } = useValues(HttpLogic) as IHttpLogicValues; + if (!config.host) return ( @@ -37,16 +42,20 @@ export const WorkplaceSearch: React.FC = (props) => { - + {errorConnecting ? : } }> - - - {/* Will replace with groups component subsequent PR */} -

- - + {errorConnecting ? ( + + ) : ( + + + {/* Will replace with groups component subsequent PR */} +
+ + + )} diff --git a/x-pack/plugins/enterprise_search/public/plugin.ts b/x-pack/plugins/enterprise_search/public/plugin.ts index 148a50fb4a5ce..881fe02af5b09 100644 --- a/x-pack/plugins/enterprise_search/public/plugin.ts +++ b/x-pack/plugins/enterprise_search/public/plugin.ts @@ -31,6 +31,7 @@ export interface ClientConfigType { } export interface ClientData extends IInitialAppData { externalUrl: IExternalUrl; + errorConnecting?: boolean; } export interface PluginsSetup { @@ -123,6 +124,7 @@ export class EnterpriseSearchPlugin implements Plugin { this.hasInitialized = true; } catch { + this.data.errorConnecting = true; // The plugin will attempt to re-fetch config data on page change } } diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/engines.test.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/engines.test.ts index 968ecb95fd931..1ea023ecacdbe 100644 --- a/x-pack/plugins/enterprise_search/server/routes/app_search/engines.test.ts +++ b/x-pack/plugins/enterprise_search/server/routes/app_search/engines.test.ts @@ -68,10 +68,11 @@ describe('engine routes', () => { ).andReturnError(); }); - it('should return 404 with a message', async () => { + it('should return 502 with a message', async () => { await mockRouter.callRoute(mockRequest); - expect(mockRouter.response.notFound).toHaveBeenCalledWith({ + expect(mockRouter.response.customError).toHaveBeenCalledWith({ + statusCode: 502, body: 'cannot-connect', }); expect(mockLogger.error).toHaveBeenCalledWith('Cannot connect to App Search: Failed'); @@ -87,10 +88,11 @@ describe('engine routes', () => { ).andReturnInvalidData(); }); - it('should return 404 with a message', async () => { + it('should return 502 with a message', async () => { await mockRouter.callRoute(mockRequest); - expect(mockRouter.response.notFound).toHaveBeenCalledWith({ + expect(mockRouter.response.customError).toHaveBeenCalledWith({ + statusCode: 502, body: 'cannot-connect', }); expect(mockLogger.error).toHaveBeenCalledWith( diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/engines.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/engines.ts index ca83c0e187ddb..7190772fb92bb 100644 --- a/x-pack/plugins/enterprise_search/server/routes/app_search/engines.ts +++ b/x-pack/plugins/enterprise_search/server/routes/app_search/engines.ts @@ -52,7 +52,7 @@ export function registerEnginesRoute({ router, config, log }: IRouteDependencies log.error(`Cannot connect to App Search: ${e.toString()}`); if (e instanceof Error) log.debug(e.stack as string); - return response.notFound({ body: 'cannot-connect' }); + return response.customError({ statusCode: 502, body: 'cannot-connect' }); } } ); diff --git a/x-pack/plugins/enterprise_search/server/routes/workplace_search/overview.test.ts b/x-pack/plugins/enterprise_search/server/routes/workplace_search/overview.test.ts index 3a4e28b0de5ff..f6534b27b5da0 100644 --- a/x-pack/plugins/enterprise_search/server/routes/workplace_search/overview.test.ts +++ b/x-pack/plugins/enterprise_search/server/routes/workplace_search/overview.test.ts @@ -63,10 +63,11 @@ describe('engine routes', () => { }).andReturnError(); }); - it('should return 404 with a message', async () => { + it('should return 502 with a message', async () => { await mockRouter.callRoute(mockRequest); - expect(mockRouter.response.notFound).toHaveBeenCalledWith({ + expect(mockRouter.response.customError).toHaveBeenCalledWith({ + statusCode: 502, body: 'cannot-connect', }); expect(mockLogger.error).toHaveBeenCalledWith('Cannot connect to Workplace Search: Failed'); @@ -81,10 +82,11 @@ describe('engine routes', () => { }).andReturnInvalidData(); }); - it('should return 404 with a message', async () => { + it('should return 502 with a message', async () => { await mockRouter.callRoute(mockRequest); - expect(mockRouter.response.notFound).toHaveBeenCalledWith({ + expect(mockRouter.response.customError).toHaveBeenCalledWith({ + statusCode: 502, body: 'cannot-connect', }); expect(mockLogger.error).toHaveBeenCalledWith( diff --git a/x-pack/plugins/enterprise_search/server/routes/workplace_search/overview.ts b/x-pack/plugins/enterprise_search/server/routes/workplace_search/overview.ts index d1e2f4f5f180d..9e5d94ac1b4fe 100644 --- a/x-pack/plugins/enterprise_search/server/routes/workplace_search/overview.ts +++ b/x-pack/plugins/enterprise_search/server/routes/workplace_search/overview.ts @@ -39,7 +39,7 @@ export function registerWSOverviewRoute({ router, config, log }: IRouteDependenc log.error(`Cannot connect to Workplace Search: ${e.toString()}`); if (e instanceof Error) log.debug(e.stack as string); - return response.notFound({ body: 'cannot-connect' }); + return response.customError({ statusCode: 502, body: 'cannot-connect' }); } } ); From eddd39a1c19fbde4370e137f468f5489cca205b4 Mon Sep 17 00:00:00 2001 From: Rashmi Kulkarni Date: Mon, 24 Aug 2020 15:28:36 -0700 Subject: [PATCH 24/71] Adding sorting test to scripted fields in discover (#75520) ...sorting functional UI tests added. --- .../apps/management/_scripted_fields.js | 86 +++++++++++++++++++ 1 file changed, 86 insertions(+) diff --git a/test/functional/apps/management/_scripted_fields.js b/test/functional/apps/management/_scripted_fields.js index 116d1eac90cea..6da9ebed0538a 100644 --- a/test/functional/apps/management/_scripted_fields.js +++ b/test/functional/apps/management/_scripted_fields.js @@ -165,6 +165,27 @@ export default function ({ getService, getPageObjects }) { }); }); + //add a test to sort numeric scripted field + it('should sort scripted field value in Discover', async function () { + await testSubjects.click(`docTableHeaderFieldSort_${scriptedPainlessFieldName}`); + // after the first click on the scripted field, it becomes secondary sort after time. + // click on the timestamp twice to make it be the secondary sort key. + await testSubjects.click('docTableHeaderFieldSort_@timestamp'); + await testSubjects.click('docTableHeaderFieldSort_@timestamp'); + await PageObjects.header.waitUntilLoadingHasFinished(); + await retry.try(async function () { + const rowData = await PageObjects.discover.getDocTableIndex(1); + expect(rowData).to.be('Sep 17, 2015 @ 10:53:14.181\n-1'); + }); + + await testSubjects.click(`docTableHeaderFieldSort_${scriptedPainlessFieldName}`); + await PageObjects.header.waitUntilLoadingHasFinished(); + await retry.try(async function () { + const rowData = await PageObjects.discover.getDocTableIndex(1); + expect(rowData).to.be('Sep 17, 2015 @ 06:32:29.479\n20'); + }); + }); + it('should filter by scripted field value in Discover', async function () { await PageObjects.discover.clickFieldListItem(scriptedPainlessFieldName); await log.debug('filter by the first value (14) in the expanded scripted field list'); @@ -252,6 +273,27 @@ export default function ({ getService, getPageObjects }) { }); }); + //add a test to sort string scripted field + it('should sort scripted field value in Discover', async function () { + await testSubjects.click(`docTableHeaderFieldSort_${scriptedPainlessFieldName2}`); + // after the first click on the scripted field, it becomes secondary sort after time. + // click on the timestamp twice to make it be the secondary sort key. + await testSubjects.click('docTableHeaderFieldSort_@timestamp'); + await testSubjects.click('docTableHeaderFieldSort_@timestamp'); + await PageObjects.header.waitUntilLoadingHasFinished(); + await retry.try(async function () { + const rowData = await PageObjects.discover.getDocTableIndex(1); + expect(rowData).to.be('Sep 17, 2015 @ 09:48:40.594\nbad'); + }); + + await testSubjects.click(`docTableHeaderFieldSort_${scriptedPainlessFieldName2}`); + await PageObjects.header.waitUntilLoadingHasFinished(); + await retry.try(async function () { + const rowData = await PageObjects.discover.getDocTableIndex(1); + expect(rowData).to.be('Sep 17, 2015 @ 06:32:29.479\ngood'); + }); + }); + it('should filter by scripted field value in Discover', async function () { await PageObjects.discover.clickFieldListItem(scriptedPainlessFieldName2); await log.debug('filter by "bad" in the expanded scripted field list'); @@ -330,6 +372,28 @@ export default function ({ getService, getPageObjects }) { await filterBar.removeAllFilters(); }); + //add a test to sort boolean + //existing bug: https://github.com/elastic/kibana/issues/75519 hence the issue is skipped. + it.skip('should sort scripted field value in Discover', async function () { + await testSubjects.click(`docTableHeaderFieldSort_${scriptedPainlessFieldName2}`); + // after the first click on the scripted field, it becomes secondary sort after time. + // click on the timestamp twice to make it be the secondary sort key. + await testSubjects.click('docTableHeaderFieldSort_@timestamp'); + await testSubjects.click('docTableHeaderFieldSort_@timestamp'); + await PageObjects.header.waitUntilLoadingHasFinished(); + await retry.try(async function () { + const rowData = await PageObjects.discover.getDocTableIndex(1); + expect(rowData).to.be('updateExpectedResultHere\ntrue'); + }); + + await testSubjects.click(`docTableHeaderFieldSort_${scriptedPainlessFieldName2}`); + await PageObjects.header.waitUntilLoadingHasFinished(); + await retry.try(async function () { + const rowData = await PageObjects.discover.getDocTableIndex(1); + expect(rowData).to.be('updateExpectedResultHere\nfalse'); + }); + }); + it('should visualize scripted field in vertical bar chart', async function () { await PageObjects.discover.clickFieldListItemVisualize(scriptedPainlessFieldName2); await PageObjects.header.waitUntilLoadingHasFinished(); @@ -384,6 +448,28 @@ export default function ({ getService, getPageObjects }) { }); }); + //add a test to sort date scripted field + //https://github.com/elastic/kibana/issues/75711 + it.skip('should sort scripted field value in Discover', async function () { + await testSubjects.click(`docTableHeaderFieldSort_${scriptedPainlessFieldName2}`); + // after the first click on the scripted field, it becomes secondary sort after time. + // click on the timestamp twice to make it be the secondary sort key. + await testSubjects.click('docTableHeaderFieldSort_@timestamp'); + await testSubjects.click('docTableHeaderFieldSort_@timestamp'); + await PageObjects.header.waitUntilLoadingHasFinished(); + await retry.try(async function () { + const rowData = await PageObjects.discover.getDocTableIndex(1); + expect(rowData).to.be('updateExpectedResultHere\n2015-09-18 07:00'); + }); + + await testSubjects.click(`docTableHeaderFieldSort_${scriptedPainlessFieldName2}`); + await PageObjects.header.waitUntilLoadingHasFinished(); + await retry.try(async function () { + const rowData = await PageObjects.discover.getDocTableIndex(1); + expect(rowData).to.be('updateExpectedResultHere\n2015-09-18 07:00'); + }); + }); + it('should filter by scripted field value in Discover', async function () { await PageObjects.discover.clickFieldListItem(scriptedPainlessFieldName2); await log.debug('filter by "Sep 17, 2015 @ 23:00" in the expanded scripted field list'); From 7c2eb85a7d23265554ee989e86712429510eccd6 Mon Sep 17 00:00:00 2001 From: Catherine Liu Date: Mon, 24 Aug 2020 15:55:53 -0700 Subject: [PATCH 25/71] [Canvas][Docs] Adds `var` and `var_set` to expression function reference (#74291) --- .../canvas/canvas-function-reference.asciidoc | 55 ++++++++++++++++++- .../common/expression_functions/specs/var.ts | 4 +- .../expression_functions/specs/var_set.ts | 6 +- 3 files changed, 59 insertions(+), 6 deletions(-) diff --git a/docs/canvas/canvas-function-reference.asciidoc b/docs/canvas/canvas-function-reference.asciidoc index 3ae513708f189..6a6c840074f02 100644 --- a/docs/canvas/canvas-function-reference.asciidoc +++ b/docs/canvas/canvas-function-reference.asciidoc @@ -13,7 +13,7 @@ A *** denotes a required argument. A † denotes an argument can be passed multiple times. -<> | B | <> | <> | <> | <> | <> | <> | <> | <> | K | <> | <> | <> | O | <> | Q | <> | <> | <> | <> | V | W | X | Y | Z +<> | B | <> | <> | <> | <> | <> | <> | <> | <> | K | <> | <> | <> | O | <> | Q | <> | <> | <> | <> | <> | W | X | Y | Z [float] [[a_fns]] @@ -2871,3 +2871,56 @@ Default: `""` |=== *Returns:* `string` + +[float] +[[v_fns]] +== V + +[float] +[[var_fn]] +=== `var` + +Updates the Kibana global context. + +*Accepts:* `any` + +[cols="3*^<"] +|=== +|Argument |Type |Description + +|_Unnamed_ *** + +Alias: `name` +|`string` +|Specify the name of the variable. +|=== + +*Returns:* Depends on your input and arguments + + +[float] +[[var_set_fn]] +=== `var_set` + +Updates the Kibana global context. + +*Accepts:* `any` + +[cols="3*^<"] +|=== +|Argument |Type |Description + +|_Unnamed_ *** + +Alias: `name` +|`string` +|Specify the name of the variable. + +|`value` + +Alias: `val` +|`any` +|Specify the value for the variable. When unspecified, the input context is used. +|=== + +*Returns:* Depends on your input and arguments diff --git a/src/plugins/expressions/common/expression_functions/specs/var.ts b/src/plugins/expressions/common/expression_functions/specs/var.ts index 4bc185a4cadfd..7d95c9816b99c 100644 --- a/src/plugins/expressions/common/expression_functions/specs/var.ts +++ b/src/plugins/expressions/common/expression_functions/specs/var.ts @@ -34,7 +34,7 @@ export type ExpressionFunctionVar = ExpressionFunctionDefinition< export const variable: ExpressionFunctionVar = { name: 'var', help: i18n.translate('expressions.functions.var.help', { - defaultMessage: 'Updates kibana global context', + defaultMessage: 'Updates the Kibana global context.', }), args: { name: { @@ -42,7 +42,7 @@ export const variable: ExpressionFunctionVar = { aliases: ['_'], required: true, help: i18n.translate('expressions.functions.var.name.help', { - defaultMessage: 'Specify name of the variable', + defaultMessage: 'Specify the name of the variable.', }), }, }, diff --git a/src/plugins/expressions/common/expression_functions/specs/var_set.ts b/src/plugins/expressions/common/expression_functions/specs/var_set.ts index 8f15bc8b90042..c45ca593f020c 100644 --- a/src/plugins/expressions/common/expression_functions/specs/var_set.ts +++ b/src/plugins/expressions/common/expression_functions/specs/var_set.ts @@ -35,7 +35,7 @@ export type ExpressionFunctionVarSet = ExpressionFunctionDefinition< export const variableSet: ExpressionFunctionVarSet = { name: 'var_set', help: i18n.translate('expressions.functions.varset.help', { - defaultMessage: 'Updates kibana global context', + defaultMessage: 'Updates the Kibana global context.', }), args: { name: { @@ -43,14 +43,14 @@ export const variableSet: ExpressionFunctionVarSet = { aliases: ['_'], required: true, help: i18n.translate('expressions.functions.varset.name.help', { - defaultMessage: 'Specify name of the variable', + defaultMessage: 'Specify the name of the variable.', }), }, value: { aliases: ['val'], help: i18n.translate('expressions.functions.varset.val.help', { defaultMessage: - 'Specify value for the variable. If not provided input context will be used', + 'Specify the value for the variable. When unspecified, the input context is used.', }), }, }, From 6b9092609ad0b54809ca4def0cde7c6986c513bf Mon Sep 17 00:00:00 2001 From: Tyler Smalley Date: Mon, 24 Aug 2020 16:11:45 -0700 Subject: [PATCH 26/71] [build] Produce Docker target consistent with stack (#75621) The release manager is currently expecting a Docker asset image with the format of `kibana-8.0.0-SNAPSHOT-docker-image.tar.gz`. If this target is not found, it will re-export the image. Making this change to produce the expected filename will remove that duplicated effort. Additionally, the release manager plans to remove this fallback in the future anyways. Signed-off-by: Tyler Smalley Co-authored-by: Elastic Machine --- src/dev/build/tasks/os_packages/docker_generator/run.ts | 6 +++--- .../tasks/os_packages/docker_generator/template_context.ts | 2 +- .../docker_generator/templates/build_docker_sh.template.ts | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) 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 6cf4a7af70840..362c34d416743 100644 --- a/src/dev/build/tasks/os_packages/docker_generator/run.ts +++ b/src/dev/build/tasks/os_packages/docker_generator/run.ts @@ -58,8 +58,8 @@ export async function runDockerGenerator( 'kibana-docker', build.isOss() ? `oss` : `default${ubiImageFlavor}` ); - const dockerOutputDir = config.resolveFromTarget( - `kibana${imageFlavor}${ubiImageFlavor}-${version}-docker.tar.gz` + const dockerTargetFilename = config.resolveFromTarget( + `kibana${imageFlavor}${ubiImageFlavor}-${version}-docker-image.tar.gz` ); const scope: TemplateContext = { artifactTarball, @@ -69,7 +69,7 @@ export async function runDockerGenerator( artifactsDir, imageTag, dockerBuildDir, - dockerOutputDir, + dockerTargetFilename, baseOSImage, ubiImageFlavor, dockerBuildDate, diff --git a/src/dev/build/tasks/os_packages/docker_generator/template_context.ts b/src/dev/build/tasks/os_packages/docker_generator/template_context.ts index a7c40db44b87e..49fb173c5a896 100644 --- a/src/dev/build/tasks/os_packages/docker_generator/template_context.ts +++ b/src/dev/build/tasks/os_packages/docker_generator/template_context.ts @@ -25,7 +25,7 @@ export interface TemplateContext { artifactsDir: string; imageTag: string; dockerBuildDir: string; - dockerOutputDir: string; + dockerTargetFilename: string; baseOSImage: string; ubiImageFlavor: string; dockerBuildDate: string; diff --git a/src/dev/build/tasks/os_packages/docker_generator/templates/build_docker_sh.template.ts b/src/dev/build/tasks/os_packages/docker_generator/templates/build_docker_sh.template.ts index 699bba758e1c9..86a02d74dea15 100644 --- a/src/dev/build/tasks/os_packages/docker_generator/templates/build_docker_sh.template.ts +++ b/src/dev/build/tasks/os_packages/docker_generator/templates/build_docker_sh.template.ts @@ -25,7 +25,7 @@ function generator({ imageTag, imageFlavor, version, - dockerOutputDir, + dockerTargetFilename, baseOSImage, ubiImageFlavor, }: TemplateContext) { @@ -41,7 +41,7 @@ function generator({ echo "Building: kibana${imageFlavor}${ubiImageFlavor}-docker"; \\ docker build -t ${imageTag}${imageFlavor}${ubiImageFlavor}:${version} -f Dockerfile . || exit 1; - docker save ${imageTag}${imageFlavor}${ubiImageFlavor}:${version} | gzip -c > ${dockerOutputDir} + docker save ${imageTag}${imageFlavor}${ubiImageFlavor}:${version} | gzip -c > ${dockerTargetFilename} exit 0 `); From f28a9e6e2d100c3c692323adff647808283cbd84 Mon Sep 17 00:00:00 2001 From: Yuliia Naumenko Date: Mon, 24 Aug 2020 16:25:05 -0700 Subject: [PATCH 27/71] Rename Whitelist to AllowList in Actions and Alerting (#75099) * Rename Whitelist to AllowList in Actions and Alerting * revert not related change * Fixed due to comments and tests failing * Fixed failing tests * Fixed due to comments --- x-pack/plugins/actions/README.md | 20 ++-- .../actions/server/actions_client.test.ts | 2 +- .../actions/server/actions_config.mock.ts | 8 +- .../actions/server/actions_config.test.ts | 104 ++++++++---------- .../plugins/actions/server/actions_config.ts | 50 ++++----- .../builtin_action_types/case/validators.ts | 6 +- .../server/builtin_action_types/email.test.ts | 34 +++--- .../server/builtin_action_types/email.ts | 8 +- .../builtin_action_types/pagerduty.test.ts | 12 +- .../server/builtin_action_types/pagerduty.ts | 6 +- .../servicenow/validators.ts | 6 +- .../server/builtin_action_types/slack.test.ts | 12 +- .../server/builtin_action_types/slack.ts | 6 +- .../builtin_action_types/webhook.test.ts | 10 +- .../server/builtin_action_types/webhook.ts | 6 +- x-pack/plugins/actions/server/config.test.ts | 12 +- x-pack/plugins/actions/server/config.ts | 10 +- x-pack/plugins/actions/server/plugin.test.ts | 4 +- x-pack/plugins/actions/server/types.ts | 6 +- .../translations/translations/ja-JP.json | 2 +- .../translations/translations/zh-CN.json | 2 +- .../alerting_api_integration/common/config.ts | 5 +- .../actions/builtin_action_types/email.ts | 14 +-- .../actions/builtin_action_types/jira.ts | 4 +- .../actions/builtin_action_types/pagerduty.ts | 4 +- .../actions/builtin_action_types/resilient.ts | 4 +- .../builtin_action_types/servicenow.ts | 4 +- .../actions/builtin_action_types/slack.ts | 4 +- .../actions/builtin_action_types/webhook.ts | 6 +- .../tests/actions/execute.ts | 2 +- .../case_api_integration/common/config.ts | 5 +- .../common/config.ts | 5 +- 32 files changed, 181 insertions(+), 202 deletions(-) diff --git a/x-pack/plugins/actions/README.md b/x-pack/plugins/actions/README.md index 3470ede0f15c7..868f6f180cc91 100644 --- a/x-pack/plugins/actions/README.md +++ b/x-pack/plugins/actions/README.md @@ -19,7 +19,7 @@ Table of Contents - [Usage](#usage) - [Kibana Actions Configuration](#kibana-actions-configuration) - [Configuration Options](#configuration-options) - - [Whitelisting Built-in Action Types](#whitelisting-built-in-action-types) + - [Adding Built-in Action Types to allowedHosts](#adding-built-in-action-types-to-hosts-allow-list) - [Configuration Utilities](#configuration-utilities) - [Action types](#action-types) - [Methods](#methods) @@ -106,15 +106,15 @@ Built-In-Actions are configured using the _xpack.actions_ namespoace under _kiba | Namespaced Key | Description | Type | | -------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------- | | _xpack.actions._**enabled** | Feature toggle which enabled Actions in Kibana. | boolean | -| _xpack.actions._**whitelistedHosts** | Which _hostnames_ are whitelisted for the Built-In-Action? This list should contain hostnames of every external service you wish to interact with using Webhooks, Email or any other built in Action. Note that you may use the string "\*" in place of a specific hostname to enable Kibana to target any URL, but keep in mind the potential use of such a feature to execute [SSRF](https://www.owasp.org/index.php/Server_Side_Request_Forgery) attacks from your server. | Array | +| _xpack.actions._**allowedHosts** | Which _hostnames_ are allowed for the Built-In-Action? This list should contain hostnames of every external service you wish to interact with using Webhooks, Email or any other built in Action. Note that you may use the string "\*" in place of a specific hostname to enable Kibana to target any URL, but keep in mind the potential use of such a feature to execute [SSRF](https://www.owasp.org/index.php/Server_Side_Request_Forgery) attacks from your server. | Array | | _xpack.actions._**enabledActionTypes** | A list of _actionTypes_ id's that are enabled. A "\*" may be used as an element to indicate all registered actionTypes should be enabled. The actionTypes registered for Kibana are `.server-log`, `.slack`, `.email`, `.index`, `.pagerduty`, `.webhook`. Default: `["*"]` | Array | | _xpack.actions._**preconfigured** | A object of action id / preconfigured actions. Default: `{}` | Array | -#### Whitelisting Built-in Action Types +#### Adding Built-in Action Types to allowedHosts -It is worth noting that the **whitelistedHosts** configuation applies to built-in action types (such as Slack, or PagerDuty) as well. +It is worth noting that the **allowedHosts** configuation applies to built-in action types (such as Slack, or PagerDuty) as well. -Uniquely, the _PagerDuty Action Type_ has been configured to support the service's Events API (at _https://events.pagerduty.com/v2/enqueue_, which you can read about [here](https://v2.developer.pagerduty.com/docs/events-api-v2)) as a default, but this too, must be included in the whitelist before the PagerDuty action can be used. +Uniquely, the _PagerDuty Action Type_ has been configured to support the service's Events API (at _https://events.pagerduty.com/v2/enqueue_, which you can read about [here](https://v2.developer.pagerduty.com/docs/events-api-v2)) as a default, but this too, must be included in the allowedHosts before the PagerDuty action can be used. ### Configuration Utilities @@ -122,11 +122,11 @@ This module provides a Utilities for interacting with the configuration. | Method | Arguments | Description | Return Type | | ------------------------- | ------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------- | -| isWhitelistedUri | _uri_: The URI you wish to validate is whitelisted | Validates whether the URI is whitelisted. This checks the configuration and validates that the hostname of the URI is in the list of whitelisted Hosts and returns `true` if it is whitelisted. If the configuration says that all URI's are whitelisted (using an "\*") then it will always return `true`. | Boolean | -| isWhitelistedHostname | _hostname_: The Hostname you wish to validate is whitelisted | Validates whether the Hostname is whitelisted. This checks the configuration and validates that the hostname is in the list of whitelisted Hosts and returns `true` if it is whitelisted. If the configuration says that all Hostnames are whitelisted (using an "\*") then it will always return `true`. | Boolean | +| isUriAllowed | _uri_: The URI you wish to validate is allowed | Validates whether the URI is allowed. This checks the configuration and validates that the hostname of the URI is in the list of allowed Hosts and returns `true` if it is allowed. If the configuration says that all URI's are allowed (using an "\*") then it will always return `true`. | Boolean | +| isHostnameAllowed | _hostname_: The Hostname you wish to validate is allowed | Validates whether the Hostname is allowed. This checks the configuration and validates that the hostname is in the list of allowed Hosts and returns `true` if it is allowed. If the configuration says that all Hostnames are allowed (using an "\*") then it will always return `true`. | Boolean | | isActionTypeEnabled | _actionType_: The actionType to check to see if it's enabled | Returns true if the actionType is enabled, otherwise false. | Boolean | -| ensureWhitelistedUri | _uri_: The URI you wish to validate is whitelisted | Validates whether the URI is whitelisted. This checks the configuration and validates that the hostname of the URI is in the list of whitelisted Hosts and throws an error if it is not whitelisted. If the configuration says that all URI's are whitelisted (using an "\*") then it will never throw. | No return value, throws if URI isn't whitelisted | -| ensureWhitelistedHostname | _hostname_: The Hostname you wish to validate is whitelisted | Validates whether the Hostname is whitelisted. This checks the configuration and validates that the hostname is in the list of whitelisted Hosts and throws an error if it is not whitelisted. If the configuration says that all Hostnames are whitelisted (using an "\*") then it will never throw | No return value, throws if Hostname isn't whitelisted | +| ensureUriAllowed | _uri_: The URI you wish to validate is allowed | Validates whether the URI is allowed. This checks the configuration and validates that the hostname of the URI is in the list of allowed Hosts and throws an error if it is not allowed. If the configuration says that all URI's are allowed (using an "\*") then it will never throw. | No return value, throws if URI isn't allowed | +| ensureHostnameAllowed | _hostname_: The Hostname you wish to validate is allowed | Validates whether the Hostname is allowed. This checks the configuration and validates that the hostname is in the list of allowed Hosts and throws an error if it is not allowed. If the configuration says that all Hostnames are allowed (using an "\*") then it will never throw | No return value, throws if Hostname isn't allowed . | | ensureActionTypeEnabled | _actionType_: The actionType to check to see if it's enabled | Throws an error if the actionType is not enabled | No return value, throws if actionType isn't enabled | ## Action types @@ -666,7 +666,7 @@ Currently actions are licensed as "basic" if the action only interacts with the Currently actions that are licensed as "basic" **MUST** be implemented in the actions plugin, other actions can be implemented in any other plugin that pre-reqs the actions plugin. If the new action is generic across the stack, it probably belongs in the actions plugin, but if your action is very specific to a plugin/solution, it might be easiest to implement it in the plugin/solution. Keep in mind that if Kibana is run without the plugin being enabled, any actions defined in that plugin will not run, nor will those actions be available via APIs or UI. -Actions that take URLs or hostnames should check that those values are whitelisted. The whitelisting utilities are currently internal to the actions plugin, and so such actions will need to be implemented in the actions plugin. Longer-term, we will expose these utilities so they can be used by alerts implemented in other plugins; see [issue #64659](https://github.com/elastic/kibana/issues/64659). +Actions that take URLs or hostnames should check that those values are allowed. The allowed host list utilities are currently internal to the actions plugin, and so such actions will need to be implemented in the actions plugin. Longer-term, we will expose these utilities so they can be used by alerts implemented in other plugins; see [issue #64659](https://github.com/elastic/kibana/issues/64659). ## documentation diff --git a/x-pack/plugins/actions/server/actions_client.test.ts b/x-pack/plugins/actions/server/actions_client.test.ts index 16a5a59882dd6..573fb0e1be580 100644 --- a/x-pack/plugins/actions/server/actions_client.test.ts +++ b/x-pack/plugins/actions/server/actions_client.test.ts @@ -295,7 +295,7 @@ describe('create()', () => { const localConfigUtils = getActionsConfigurationUtilities({ enabled: true, enabledActionTypes: ['some-not-ignored-action-type'], - whitelistedHosts: ['*'], + allowedHosts: ['*'], }); const localActionTypeRegistryParams = { diff --git a/x-pack/plugins/actions/server/actions_config.mock.ts b/x-pack/plugins/actions/server/actions_config.mock.ts index addd35ae4f5f3..67ab495fc9678 100644 --- a/x-pack/plugins/actions/server/actions_config.mock.ts +++ b/x-pack/plugins/actions/server/actions_config.mock.ts @@ -8,11 +8,11 @@ import { ActionsConfigurationUtilities } from './actions_config'; const createActionsConfigMock = () => { const mocked: jest.Mocked = { - isWhitelistedHostname: jest.fn().mockReturnValue(true), - isWhitelistedUri: jest.fn().mockReturnValue(true), + isHostnameAllowed: jest.fn().mockReturnValue(true), + isUriAllowed: jest.fn().mockReturnValue(true), isActionTypeEnabled: jest.fn().mockReturnValue(true), - ensureWhitelistedHostname: jest.fn().mockReturnValue({}), - ensureWhitelistedUri: jest.fn().mockReturnValue({}), + ensureHostnameAllowed: jest.fn().mockReturnValue({}), + ensureUriAllowed: jest.fn().mockReturnValue({}), ensureActionTypeEnabled: jest.fn().mockReturnValue({}), }; return mocked; diff --git a/x-pack/plugins/actions/server/actions_config.test.ts b/x-pack/plugins/actions/server/actions_config.test.ts index 7d9d431d1c1be..56c58054ca799 100644 --- a/x-pack/plugins/actions/server/actions_config.test.ts +++ b/x-pack/plugins/actions/server/actions_config.test.ts @@ -7,163 +7,151 @@ import { ActionsConfigType } from './types'; import { getActionsConfigurationUtilities, - WhitelistedHosts, + AllowedHosts, EnabledActionTypes, } from './actions_config'; const DefaultActionsConfig: ActionsConfigType = { enabled: false, - whitelistedHosts: [], + allowedHosts: [], enabledActionTypes: [], }; -describe('ensureWhitelistedUri', () => { +describe('ensureUriAllowed', () => { test('returns true when "any" hostnames are allowed', () => { const config: ActionsConfigType = { enabled: false, - whitelistedHosts: [WhitelistedHosts.Any], + allowedHosts: [AllowedHosts.Any], enabledActionTypes: [], }; expect( - getActionsConfigurationUtilities(config).ensureWhitelistedUri( - 'https://github.com/elastic/kibana' - ) + getActionsConfigurationUtilities(config).ensureUriAllowed('https://github.com/elastic/kibana') ).toBeUndefined(); }); - test('throws when the hostname in the requested uri is not in the whitelist', () => { + test('throws when the hostname in the requested uri is not in the allowedHosts', () => { const config: ActionsConfigType = DefaultActionsConfig; expect(() => - getActionsConfigurationUtilities(config).ensureWhitelistedUri( - 'https://github.com/elastic/kibana' - ) + getActionsConfigurationUtilities(config).ensureUriAllowed('https://github.com/elastic/kibana') ).toThrowErrorMatchingInlineSnapshot( - `"target url \\"https://github.com/elastic/kibana\\" is not whitelisted in the Kibana config xpack.actions.whitelistedHosts"` + `"target url \\"https://github.com/elastic/kibana\\" is not added to the Kibana config xpack.actions.allowedHosts"` ); }); test('throws when the uri cannot be parsed as a valid URI', () => { const config: ActionsConfigType = DefaultActionsConfig; expect(() => - getActionsConfigurationUtilities(config).ensureWhitelistedUri('github.com/elastic') + getActionsConfigurationUtilities(config).ensureUriAllowed('github.com/elastic') ).toThrowErrorMatchingInlineSnapshot( - `"target url \\"github.com/elastic\\" is not whitelisted in the Kibana config xpack.actions.whitelistedHosts"` + `"target url \\"github.com/elastic\\" is not added to the Kibana config xpack.actions.allowedHosts"` ); }); - test('returns true when the hostname in the requested uri is in the whitelist', () => { + test('returns true when the hostname in the requested uri is in the allowedHosts', () => { const config: ActionsConfigType = { enabled: false, - whitelistedHosts: ['github.com'], + allowedHosts: ['github.com'], enabledActionTypes: [], }; expect( - getActionsConfigurationUtilities(config).ensureWhitelistedUri( - 'https://github.com/elastic/kibana' - ) + getActionsConfigurationUtilities(config).ensureUriAllowed('https://github.com/elastic/kibana') ).toBeUndefined(); }); }); -describe('ensureWhitelistedHostname', () => { +describe('ensureHostnameAllowed', () => { test('returns true when "any" hostnames are allowed', () => { const config: ActionsConfigType = { enabled: false, - whitelistedHosts: [WhitelistedHosts.Any], + allowedHosts: [AllowedHosts.Any], enabledActionTypes: [], }; expect( - getActionsConfigurationUtilities(config).ensureWhitelistedHostname('github.com') + getActionsConfigurationUtilities(config).ensureHostnameAllowed('github.com') ).toBeUndefined(); }); - test('throws when the hostname in the requested uri is not in the whitelist', () => { + test('throws when the hostname in the requested uri is not in the allowedHosts', () => { const config: ActionsConfigType = DefaultActionsConfig; expect(() => - getActionsConfigurationUtilities(config).ensureWhitelistedHostname('github.com') + getActionsConfigurationUtilities(config).ensureHostnameAllowed('github.com') ).toThrowErrorMatchingInlineSnapshot( - `"target hostname \\"github.com\\" is not whitelisted in the Kibana config xpack.actions.whitelistedHosts"` + `"target hostname \\"github.com\\" is not added to the Kibana config xpack.actions.allowedHosts"` ); }); - test('returns true when the hostname in the requested uri is in the whitelist', () => { + test('returns true when the hostname in the requested uri is in the allowedHosts', () => { const config: ActionsConfigType = { enabled: false, - whitelistedHosts: ['github.com'], + allowedHosts: ['github.com'], enabledActionTypes: [], }; expect( - getActionsConfigurationUtilities(config).ensureWhitelistedHostname('github.com') + getActionsConfigurationUtilities(config).ensureHostnameAllowed('github.com') ).toBeUndefined(); }); }); -describe('isWhitelistedUri', () => { +describe('isUriAllowed', () => { test('returns true when "any" hostnames are allowed', () => { const config: ActionsConfigType = { enabled: false, - whitelistedHosts: [WhitelistedHosts.Any], + allowedHosts: [AllowedHosts.Any], enabledActionTypes: [], }; expect( - getActionsConfigurationUtilities(config).isWhitelistedUri('https://github.com/elastic/kibana') + getActionsConfigurationUtilities(config).isUriAllowed('https://github.com/elastic/kibana') ).toEqual(true); }); - test('throws when the hostname in the requested uri is not in the whitelist', () => { + test('throws when the hostname in the requested uri is not in the allowedHosts', () => { const config: ActionsConfigType = DefaultActionsConfig; expect( - getActionsConfigurationUtilities(config).isWhitelistedUri('https://github.com/elastic/kibana') + getActionsConfigurationUtilities(config).isUriAllowed('https://github.com/elastic/kibana') ).toEqual(false); }); test('throws when the uri cannot be parsed as a valid URI', () => { const config: ActionsConfigType = DefaultActionsConfig; - expect(getActionsConfigurationUtilities(config).isWhitelistedUri('github.com/elastic')).toEqual( + expect(getActionsConfigurationUtilities(config).isUriAllowed('github.com/elastic')).toEqual( false ); }); - test('returns true when the hostname in the requested uri is in the whitelist', () => { + test('returns true when the hostname in the requested uri is in the allowedHosts', () => { const config: ActionsConfigType = { enabled: false, - whitelistedHosts: ['github.com'], + allowedHosts: ['github.com'], enabledActionTypes: [], }; expect( - getActionsConfigurationUtilities(config).isWhitelistedUri('https://github.com/elastic/kibana') + getActionsConfigurationUtilities(config).isUriAllowed('https://github.com/elastic/kibana') ).toEqual(true); }); }); -describe('isWhitelistedHostname', () => { +describe('isHostnameAllowed', () => { test('returns true when "any" hostnames are allowed', () => { const config: ActionsConfigType = { enabled: false, - whitelistedHosts: [WhitelistedHosts.Any], + allowedHosts: [AllowedHosts.Any], enabledActionTypes: [], }; - expect(getActionsConfigurationUtilities(config).isWhitelistedHostname('github.com')).toEqual( - true - ); + expect(getActionsConfigurationUtilities(config).isHostnameAllowed('github.com')).toEqual(true); }); - test('throws when the hostname in the requested uri is not in the whitelist', () => { + test('throws when the hostname in the requested uri is not in the allowedHosts', () => { const config: ActionsConfigType = DefaultActionsConfig; - expect(getActionsConfigurationUtilities(config).isWhitelistedHostname('github.com')).toEqual( - false - ); + expect(getActionsConfigurationUtilities(config).isHostnameAllowed('github.com')).toEqual(false); }); - test('returns true when the hostname in the requested uri is in the whitelist', () => { + test('returns true when the hostname in the requested uri is in the allowedHosts', () => { const config: ActionsConfigType = { enabled: false, - whitelistedHosts: ['github.com'], + allowedHosts: ['github.com'], enabledActionTypes: [], }; - expect(getActionsConfigurationUtilities(config).isWhitelistedHostname('github.com')).toEqual( - true - ); + expect(getActionsConfigurationUtilities(config).isHostnameAllowed('github.com')).toEqual(true); }); }); @@ -171,7 +159,7 @@ describe('isActionTypeEnabled', () => { test('returns true when "any" actionTypes are allowed', () => { const config: ActionsConfigType = { enabled: false, - whitelistedHosts: [], + allowedHosts: [], enabledActionTypes: ['ignore', EnabledActionTypes.Any], }; expect(getActionsConfigurationUtilities(config).isActionTypeEnabled('foo')).toEqual(true); @@ -180,7 +168,7 @@ describe('isActionTypeEnabled', () => { test('returns false when no actionType is allowed', () => { const config: ActionsConfigType = { enabled: false, - whitelistedHosts: [], + allowedHosts: [], enabledActionTypes: [], }; expect(getActionsConfigurationUtilities(config).isActionTypeEnabled('foo')).toEqual(false); @@ -189,7 +177,7 @@ describe('isActionTypeEnabled', () => { test('returns false when the actionType is not in the enabled list', () => { const config: ActionsConfigType = { enabled: false, - whitelistedHosts: [], + allowedHosts: [], enabledActionTypes: ['foo'], }; expect(getActionsConfigurationUtilities(config).isActionTypeEnabled('bar')).toEqual(false); @@ -198,7 +186,7 @@ describe('isActionTypeEnabled', () => { test('returns true when the actionType is in the enabled list', () => { const config: ActionsConfigType = { enabled: false, - whitelistedHosts: [], + allowedHosts: [], enabledActionTypes: ['ignore', 'foo'], }; expect(getActionsConfigurationUtilities(config).isActionTypeEnabled('foo')).toEqual(true); @@ -209,7 +197,7 @@ describe('ensureActionTypeEnabled', () => { test('does not throw when any actionType is allowed', () => { const config: ActionsConfigType = { enabled: false, - whitelistedHosts: [], + allowedHosts: [], enabledActionTypes: ['ignore', EnabledActionTypes.Any], }; expect(getActionsConfigurationUtilities(config).ensureActionTypeEnabled('foo')).toBeUndefined(); @@ -227,7 +215,7 @@ describe('ensureActionTypeEnabled', () => { test('throws when actionType is not enabled', () => { const config: ActionsConfigType = { enabled: false, - whitelistedHosts: [], + allowedHosts: [], enabledActionTypes: ['ignore'], }; expect(() => @@ -240,7 +228,7 @@ describe('ensureActionTypeEnabled', () => { test('does not throw when actionType is enabled', () => { const config: ActionsConfigType = { enabled: false, - whitelistedHosts: [], + allowedHosts: [], enabledActionTypes: ['ignore', 'foo'], }; expect(getActionsConfigurationUtilities(config).ensureActionTypeEnabled('foo')).toBeUndefined(); diff --git a/x-pack/plugins/actions/server/actions_config.ts b/x-pack/plugins/actions/server/actions_config.ts index b15fe5b4007c5..609e4969222f9 100644 --- a/x-pack/plugins/actions/server/actions_config.ts +++ b/x-pack/plugins/actions/server/actions_config.ts @@ -13,7 +13,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { ActionsConfigType } from './types'; import { ActionTypeDisabledError } from './lib'; -export enum WhitelistedHosts { +export enum AllowedHosts { Any = '*', } @@ -21,24 +21,24 @@ export enum EnabledActionTypes { Any = '*', } -enum WhitelistingField { +enum AllowListingField { url = 'url', hostname = 'hostname', } export interface ActionsConfigurationUtilities { - isWhitelistedHostname: (hostname: string) => boolean; - isWhitelistedUri: (uri: string) => boolean; + isHostnameAllowed: (hostname: string) => boolean; + isUriAllowed: (uri: string) => boolean; isActionTypeEnabled: (actionType: string) => boolean; - ensureWhitelistedHostname: (hostname: string) => void; - ensureWhitelistedUri: (uri: string) => void; + ensureHostnameAllowed: (hostname: string) => void; + ensureUriAllowed: (uri: string) => void; ensureActionTypeEnabled: (actionType: string) => void; } -function whitelistingErrorMessage(field: WhitelistingField, value: string) { - return i18n.translate('xpack.actions.urlWhitelistConfigurationError', { +function allowListErrorMessage(field: AllowListingField, value: string) { + return i18n.translate('xpack.actions.urlAllowedHostsConfigurationError', { defaultMessage: - 'target {field} "{value}" is not whitelisted in the Kibana config xpack.actions.whitelistedHosts', + 'target {field} "{value}" is not added to the Kibana config xpack.actions.allowedHosts', values: { value, field, @@ -56,18 +56,18 @@ function disabledActionTypeErrorMessage(actionType: string) { }); } -function isWhitelisted({ whitelistedHosts }: ActionsConfigType, hostname: string): boolean { - const whitelisted = new Set(whitelistedHosts); - if (whitelisted.has(WhitelistedHosts.Any)) return true; - if (whitelisted.has(hostname)) return true; +function isAllowed({ allowedHosts }: ActionsConfigType, hostname: string): boolean { + const allowed = new Set(allowedHosts); + if (allowed.has(AllowedHosts.Any)) return true; + if (allowed.has(hostname)) return true; return false; } -function isWhitelistedHostnameInUri(config: ActionsConfigType, uri: string): boolean { +function isHostnameAllowedInUri(config: ActionsConfigType, uri: string): boolean { return pipe( tryCatch(() => new URL(uri)), map((url) => url.hostname), - mapNullable((hostname) => isWhitelisted(config, hostname)), + mapNullable((hostname) => isAllowed(config, hostname)), getOrElse(() => false) ); } @@ -85,21 +85,21 @@ function isActionTypeEnabledInConfig( export function getActionsConfigurationUtilities( config: ActionsConfigType ): ActionsConfigurationUtilities { - const isWhitelistedHostname = curry(isWhitelisted)(config); - const isWhitelistedUri = curry(isWhitelistedHostnameInUri)(config); + const isHostnameAllowed = curry(isAllowed)(config); + const isUriAllowed = curry(isHostnameAllowedInUri)(config); const isActionTypeEnabled = curry(isActionTypeEnabledInConfig)(config); return { - isWhitelistedHostname, - isWhitelistedUri, + isHostnameAllowed, + isUriAllowed, isActionTypeEnabled, - ensureWhitelistedUri(uri: string) { - if (!isWhitelistedUri(uri)) { - throw new Error(whitelistingErrorMessage(WhitelistingField.url, uri)); + ensureUriAllowed(uri: string) { + if (!isUriAllowed(uri)) { + throw new Error(allowListErrorMessage(AllowListingField.url, uri)); } }, - ensureWhitelistedHostname(hostname: string) { - if (!isWhitelistedHostname(hostname)) { - throw new Error(whitelistingErrorMessage(WhitelistingField.hostname, hostname)); + ensureHostnameAllowed(hostname: string) { + if (!isHostnameAllowed(hostname)) { + throw new Error(allowListErrorMessage(AllowListingField.hostname, hostname)); } }, ensureActionTypeEnabled(actionType: string) { diff --git a/x-pack/plugins/actions/server/builtin_action_types/case/validators.ts b/x-pack/plugins/actions/server/builtin_action_types/case/validators.ts index 80e301e5be082..08e8a8be6a3e6 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/case/validators.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/case/validators.ts @@ -23,9 +23,9 @@ export const validateCommonConfig = ( return i18n.MAPPING_EMPTY; } - configurationUtilities.ensureWhitelistedUri(configObject.apiUrl); - } catch (whitelistError) { - return i18n.WHITE_LISTED_ERROR(whitelistError.message); + configurationUtilities.ensureUriAllowed(configObject.apiUrl); + } catch (allowListError) { + return i18n.WHITE_LISTED_ERROR(allowListError.message); } }; diff --git a/x-pack/plugins/actions/server/builtin_action_types/email.test.ts b/x-pack/plugins/actions/server/builtin_action_types/email.test.ts index 62f369816d714..7147483998d98 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/email.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/email.test.ts @@ -121,56 +121,56 @@ describe('config validation', () => { const NODEMAILER_AOL_SERVICE = 'AOL'; const NODEMAILER_AOL_SERVICE_HOST = 'smtp.aol.com'; - test('config validation handles email host whitelisting', () => { + test('config validation handles email host in allowedHosts', () => { actionType = getActionType({ logger: mockedLogger, configurationUtilities: { ...actionsConfigMock.create(), - isWhitelistedHostname: (hostname) => hostname === NODEMAILER_AOL_SERVICE_HOST, + isHostnameAllowed: (hostname) => hostname === NODEMAILER_AOL_SERVICE_HOST, }, }); const baseConfig = { from: 'bob@example.com', }; - const whitelistedConfig1 = { + const allowedHosts1 = { ...baseConfig, service: NODEMAILER_AOL_SERVICE, }; - const whitelistedConfig2 = { + const allowedHosts2 = { ...baseConfig, host: NODEMAILER_AOL_SERVICE_HOST, port: 42, }; - const notWhitelistedConfig1 = { + const notAllowedHosts1 = { ...baseConfig, service: 'gmail', }; - const notWhitelistedConfig2 = { + const notAllowedHosts2 = { ...baseConfig, host: 'smtp.gmail.com', port: 42, }; - const validatedConfig1 = validateConfig(actionType, whitelistedConfig1); - expect(validatedConfig1.service).toEqual(whitelistedConfig1.service); - expect(validatedConfig1.from).toEqual(whitelistedConfig1.from); + const validatedConfig1 = validateConfig(actionType, allowedHosts1); + expect(validatedConfig1.service).toEqual(allowedHosts1.service); + expect(validatedConfig1.from).toEqual(allowedHosts1.from); - const validatedConfig2 = validateConfig(actionType, whitelistedConfig2); - expect(validatedConfig2.host).toEqual(whitelistedConfig2.host); - expect(validatedConfig2.port).toEqual(whitelistedConfig2.port); - expect(validatedConfig2.from).toEqual(whitelistedConfig2.from); + const validatedConfig2 = validateConfig(actionType, allowedHosts2); + expect(validatedConfig2.host).toEqual(allowedHosts2.host); + expect(validatedConfig2.port).toEqual(allowedHosts2.port); + expect(validatedConfig2.from).toEqual(allowedHosts2.from); expect(() => { - validateConfig(actionType, notWhitelistedConfig1); + validateConfig(actionType, notAllowedHosts1); }).toThrowErrorMatchingInlineSnapshot( - `"error validating action type config: [service] value 'gmail' resolves to host 'smtp.gmail.com' which is not in the whitelistedHosts configuration"` + `"error validating action type config: [service] value 'gmail' resolves to host 'smtp.gmail.com' which is not in the allowedHosts configuration"` ); expect(() => { - validateConfig(actionType, notWhitelistedConfig2); + validateConfig(actionType, notAllowedHosts2); }).toThrowErrorMatchingInlineSnapshot( - `"error validating action type config: [host] value 'smtp.gmail.com' is not in the whitelistedHosts configuration"` + `"error validating action type config: [host] value 'smtp.gmail.com' is not in the allowedHosts configuration"` ); }); }); 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 e9dc4eea5dcfc..6fd2d694b06f7 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/email.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/email.ts @@ -66,16 +66,16 @@ function validateConfig( return '[port] is required if [service] is not provided'; } - if (!configurationUtilities.isWhitelistedHostname(config.host)) { - return `[host] value '${config.host}' is not in the whitelistedHosts configuration`; + if (!configurationUtilities.isHostnameAllowed(config.host)) { + return `[host] value '${config.host}' is not in the allowedHosts configuration`; } } else { const host = getServiceNameHost(config.service); if (host == null) { return `[service] value '${config.service}' is not valid`; } - if (!configurationUtilities.isWhitelistedHostname(host)) { - return `[service] value '${config.service}' resolves to host '${host}' which is not in the whitelistedHosts configuration`; + if (!configurationUtilities.isHostnameAllowed(host)) { + return `[service] value '${config.service}' resolves to host '${host}' which is not in the allowedHosts configuration`; } } } diff --git a/x-pack/plugins/actions/server/builtin_action_types/pagerduty.test.ts b/x-pack/plugins/actions/server/builtin_action_types/pagerduty.test.ts index c379c05ee88e3..772e7df416979 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/pagerduty.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/pagerduty.test.ts @@ -64,12 +64,12 @@ describe('validateConfig()', () => { ); }); - test('should validate and pass when the pagerduty url is whitelisted', () => { + test('should validate and pass when the pagerduty url is added to allowedHosts', () => { actionType = getActionType({ logger: mockedLogger, configurationUtilities: { ...actionsConfigMock.create(), - ensureWhitelistedUri: (url) => { + ensureUriAllowed: (url) => { expect(url).toEqual('https://events.pagerduty.com/v2/enqueue'); }, }, @@ -80,13 +80,13 @@ describe('validateConfig()', () => { ).toEqual({ apiUrl: 'https://events.pagerduty.com/v2/enqueue' }); }); - test('config validation returns an error if the specified URL isnt whitelisted', () => { + test('config validation returns an error if the specified URL isnt added to allowedHosts', () => { actionType = getActionType({ logger: mockedLogger, configurationUtilities: { ...actionsConfigMock.create(), - ensureWhitelistedUri: (_) => { - throw new Error(`target url is not whitelisted`); + ensureUriAllowed: (_) => { + throw new Error(`target url is not added to allowedHosts`); }, }, }); @@ -94,7 +94,7 @@ describe('validateConfig()', () => { expect(() => { validateConfig(actionType, { apiUrl: 'https://events.pagerduty.com/v2/enqueue' }); }).toThrowErrorMatchingInlineSnapshot( - `"error validating action type config: error configuring pagerduty action: target url is not whitelisted"` + `"error validating action type config: error configuring pagerduty action: target url is not added to allowedHosts"` ); }); }); diff --git a/x-pack/plugins/actions/server/builtin_action_types/pagerduty.ts b/x-pack/plugins/actions/server/builtin_action_types/pagerduty.ts index c0edfc530e738..640a38d77b6c2 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/pagerduty.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/pagerduty.ts @@ -135,12 +135,12 @@ function valdiateActionTypeConfig( configObject: ActionTypeConfigType ) { try { - configurationUtilities.ensureWhitelistedUri(getPagerDutyApiUrl(configObject)); - } catch (whitelistError) { + configurationUtilities.ensureUriAllowed(getPagerDutyApiUrl(configObject)); + } catch (allowListError) { return i18n.translate('xpack.actions.builtin.pagerduty.pagerdutyConfigurationError', { defaultMessage: 'error configuring pagerduty action: {message}', values: { - message: whitelistError.message, + message: allowListError.message, }, }); } diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/validators.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/validators.ts index 65bbe9aea8119..6eec3b8d63b86 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/validators.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/validators.ts @@ -26,9 +26,9 @@ export const validateCommonConfig = ( } try { - configurationUtilities.ensureWhitelistedUri(configObject.apiUrl); - } catch (whitelistError) { - return i18n.WHITE_LISTED_ERROR(whitelistError.message); + configurationUtilities.ensureUriAllowed(configObject.apiUrl); + } catch (allowListError) { + return i18n.WHITE_LISTED_ERROR(allowListError.message); } }; diff --git a/x-pack/plugins/actions/server/builtin_action_types/slack.test.ts b/x-pack/plugins/actions/server/builtin_action_types/slack.test.ts index 812657138152c..81fa5553b331e 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/slack.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/slack.test.ts @@ -96,12 +96,12 @@ describe('validateActionTypeSecrets()', () => { ); }); - test('should validate and pass when the slack webhookUrl is whitelisted', () => { + test('should validate and pass when the slack webhookUrl is added to allowedHosts', () => { actionType = getActionType({ logger: mockedLogger, configurationUtilities: { ...actionsConfigMock.create(), - ensureWhitelistedUri: (url) => { + ensureUriAllowed: (url) => { expect(url).toEqual('https://api.slack.com/'); }, }, @@ -112,13 +112,13 @@ describe('validateActionTypeSecrets()', () => { }); }); - test('config validation returns an error if the specified URL isnt whitelisted', () => { + test('config validation returns an error if the specified URL isnt added to allowedHosts', () => { actionType = getActionType({ logger: mockedLogger, configurationUtilities: { ...actionsConfigMock.create(), - ensureWhitelistedHostname: () => { - throw new Error(`target hostname is not whitelisted`); + ensureHostnameAllowed: () => { + throw new Error(`target hostname is not added to allowedHosts`); }, }, }); @@ -126,7 +126,7 @@ describe('validateActionTypeSecrets()', () => { expect(() => { validateSecrets(actionType, { webhookUrl: 'https://api.slack.com/' }); }).toThrowErrorMatchingInlineSnapshot( - `"error validating action type secrets: error configuring slack action: target hostname is not whitelisted"` + `"error validating action type secrets: error configuring slack action: target hostname is not added to allowedHosts"` ); }); }); diff --git a/x-pack/plugins/actions/server/builtin_action_types/slack.ts b/x-pack/plugins/actions/server/builtin_action_types/slack.ts index 293328c809435..639b4448b5a89 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/slack.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/slack.ts @@ -91,12 +91,12 @@ function valdiateActionTypeConfig( } try { - configurationUtilities.ensureWhitelistedHostname(url.hostname); - } catch (whitelistError) { + configurationUtilities.ensureHostnameAllowed(url.hostname); + } catch (allowListError) { return i18n.translate('xpack.actions.builtin.slack.slackConfigurationError', { defaultMessage: 'error configuring slack action: {message}', values: { - message: whitelistError.message, + message: allowListError.message, }, }); } diff --git a/x-pack/plugins/actions/server/builtin_action_types/webhook.test.ts b/x-pack/plugins/actions/server/builtin_action_types/webhook.test.ts index ea9f30452918c..23ce527d4ae0d 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/webhook.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/webhook.test.ts @@ -176,7 +176,7 @@ describe('config validation', () => { `); }); - test('config validation passes when kibana config whitelists the url', () => { + test('config validation passes when kibana config url does not present in allowedHosts', () => { // any for testing // eslint-disable-next-line @typescript-eslint/no-explicit-any const config: Record = { @@ -192,13 +192,13 @@ describe('config validation', () => { }); }); - test('config validation returns an error if the specified URL isnt whitelisted', () => { + test('config validation returns an error if the specified URL isnt added to allowedHosts', () => { actionType = getActionType({ logger: mockedLogger, configurationUtilities: { ...actionsConfigMock.create(), - ensureWhitelistedUri: (_) => { - throw new Error(`target url is not whitelisted`); + ensureUriAllowed: (_) => { + throw new Error(`target url is not present in allowedHosts`); }, }, }); @@ -215,7 +215,7 @@ describe('config validation', () => { expect(() => { validateConfig(actionType, config); }).toThrowErrorMatchingInlineSnapshot( - `"error validating action type config: error configuring webhook action: target url is not whitelisted"` + `"error validating action type config: error configuring webhook action: target url is not present in allowedHosts"` ); }); }); diff --git a/x-pack/plugins/actions/server/builtin_action_types/webhook.ts b/x-pack/plugins/actions/server/builtin_action_types/webhook.ts index d9a005565498d..d0ec31721685e 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/webhook.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/webhook.ts @@ -111,12 +111,12 @@ function validateActionTypeConfig( } try { - configurationUtilities.ensureWhitelistedUri(url.toString()); - } catch (whitelistError) { + configurationUtilities.ensureUriAllowed(url.toString()); + } catch (allowListError) { return i18n.translate('xpack.actions.builtin.webhook.webhookConfigurationError', { defaultMessage: 'error configuring webhook action: {message}', values: { - message: whitelistError.message, + message: allowListError.message, }, }); } diff --git a/x-pack/plugins/actions/server/config.test.ts b/x-pack/plugins/actions/server/config.test.ts index 795fbbf84145b..ac815a425a2b7 100644 --- a/x-pack/plugins/actions/server/config.test.ts +++ b/x-pack/plugins/actions/server/config.test.ts @@ -10,15 +10,15 @@ describe('config validation', () => { const config: Record = {}; expect(configSchema.validate(config)).toMatchInlineSnapshot(` Object { + "allowedHosts": Array [ + "*", + ], "enabled": true, "enabledActionTypes": Array [ "*", ], "preconfigured": Object {}, "rejectUnauthorizedCertificates": true, - "whitelistedHosts": Array [ - "*", - ], } `); }); @@ -38,6 +38,9 @@ describe('config validation', () => { }; expect(configSchema.validate(config)).toMatchInlineSnapshot(` Object { + "allowedHosts": Array [ + "*", + ], "enabled": true, "enabledActionTypes": Array [ "*", @@ -53,9 +56,6 @@ describe('config validation', () => { }, }, "rejectUnauthorizedCertificates": false, - "whitelistedHosts": Array [ - "*", - ], } `); }); diff --git a/x-pack/plugins/actions/server/config.ts b/x-pack/plugins/actions/server/config.ts index ba80915ebe243..087a08f572c65 100644 --- a/x-pack/plugins/actions/server/config.ts +++ b/x-pack/plugins/actions/server/config.ts @@ -5,7 +5,7 @@ */ import { schema, TypeOf } from '@kbn/config-schema'; -import { WhitelistedHosts, EnabledActionTypes } from './actions_config'; +import { AllowedHosts, EnabledActionTypes } from './actions_config'; const preconfiguredActionSchema = schema.object({ name: schema.string({ minLength: 1 }), @@ -16,16 +16,16 @@ const preconfiguredActionSchema = schema.object({ export const configSchema = schema.object({ enabled: schema.boolean({ defaultValue: true }), - whitelistedHosts: schema.arrayOf( - schema.oneOf([schema.string({ hostname: true }), schema.literal(WhitelistedHosts.Any)]), + allowedHosts: schema.arrayOf( + schema.oneOf([schema.string({ hostname: true }), schema.literal(AllowedHosts.Any)]), { - defaultValue: [WhitelistedHosts.Any], + defaultValue: [AllowedHosts.Any], } ), enabledActionTypes: schema.arrayOf( schema.oneOf([schema.string(), schema.literal(EnabledActionTypes.Any)]), { - defaultValue: [WhitelistedHosts.Any], + defaultValue: [AllowedHosts.Any], } ), preconfigured: schema.recordOf(schema.string(), preconfiguredActionSchema, { diff --git a/x-pack/plugins/actions/server/plugin.test.ts b/x-pack/plugins/actions/server/plugin.test.ts index 341a17889923f..4fdf9f2523568 100644 --- a/x-pack/plugins/actions/server/plugin.test.ts +++ b/x-pack/plugins/actions/server/plugin.test.ts @@ -32,7 +32,7 @@ describe('Actions Plugin', () => { context = coreMock.createPluginInitializerContext({ enabled: true, enabledActionTypes: ['*'], - whitelistedHosts: ['*'], + allowedHosts: ['*'], preconfigured: {}, rejectUnauthorizedCertificates: true, }); @@ -186,7 +186,7 @@ describe('Actions Plugin', () => { const context = coreMock.createPluginInitializerContext({ enabled: true, enabledActionTypes: ['*'], - whitelistedHosts: ['*'], + allowedHosts: ['*'], preconfigured: { preconfiguredServerLog: { actionTypeId: '.server-log', diff --git a/x-pack/plugins/actions/server/types.ts b/x-pack/plugins/actions/server/types.ts index bf7bd709a4a88..0a7d6bf01b7ec 100644 --- a/x-pack/plugins/actions/server/types.ts +++ b/x-pack/plugins/actions/server/types.ts @@ -47,7 +47,7 @@ export interface ActionsPlugin { export interface ActionsConfigType { enabled: boolean; - whitelistedHosts: string[]; + allowedHosts: string[]; enabledActionTypes: string[]; } @@ -100,8 +100,8 @@ interface ValidatorType { } export interface ActionValidationService { - isWhitelistedHostname(hostname: string): boolean; - isWhitelistedUri(uri: string): boolean; + isHostnameAllowed(hostname: string): boolean; + isUriAllowed(uri: string): boolean; } export interface ActionType< diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index b51ccbb64767a..411aa3424c855 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -4599,7 +4599,7 @@ "xpack.actions.serverSideErrors.predefinedActionUpdateDisabled": "あらかじめ構成されたアクション{id}は更新できません。", "xpack.actions.serverSideErrors.unavailableLicenseErrorMessage": "現時点でライセンス情報を入手できないため、アクションタイプ {actionTypeId} は無効です。", "xpack.actions.serverSideErrors.unavailableLicenseInformationErrorMessage": "グラフを利用できません。現在ライセンス情報が利用できません。", - "xpack.actions.urlWhitelistConfigurationError": "target {field} \"{value}\" は Kibana 構成 xpack.actions.whitelistedHosts にはホワイトリスト化されていません。", + "xpack.actions.urlAllowedHostsConfigurationError": "target {field} \"{value}\" は Kibana 構成 xpack.actions.allowedHosts にはホワイトリスト化されていません。", "xpack.alertingBuiltins.indexThreshold.actionGroupThresholdMetTitle": "しきい値一致", "xpack.alertingBuiltins.indexThreshold.actionVariableContextDateLabel": "アラートがしきい値を超えた日付。", "xpack.alertingBuiltins.indexThreshold.actionVariableContextGroupLabel": "しきい値を超えたグループ。", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index a2b612cd6dad1..c46135633a3c8 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -4600,7 +4600,7 @@ "xpack.actions.serverSideErrors.predefinedActionUpdateDisabled": "不允许更新预配置的操作 {id}。", "xpack.actions.serverSideErrors.unavailableLicenseErrorMessage": "操作类型 {actionTypeId} 已禁用,因为许可证信息当前不可用。", "xpack.actions.serverSideErrors.unavailableLicenseInformationErrorMessage": "操作不可用 - 许可信息当前不可用。", - "xpack.actions.urlWhitelistConfigurationError": "目标 {field}“{value}”在 Kibana 配置 xpack.actions.whitelistedHosts 中未列入白名单", + "xpack.actions.urlAllowedHostsConfigurationError": "目标 {field}“{value}”在 Kibana 配置 xpack.actions.allowedHosts 中未列入白名单", "xpack.alertingBuiltins.indexThreshold.actionGroupThresholdMetTitle": "阈值已达到", "xpack.alertingBuiltins.indexThreshold.actionVariableContextDateLabel": "告警超过阈值的日期。", "xpack.alertingBuiltins.indexThreshold.actionVariableContextGroupLabel": "超过阈值的组。", diff --git a/x-pack/test/alerting_api_integration/common/config.ts b/x-pack/test/alerting_api_integration/common/config.ts index 34e23a2dba0b2..117a59a304368 100644 --- a/x-pack/test/alerting_api_integration/common/config.ts +++ b/x-pack/test/alerting_api_integration/common/config.ts @@ -85,10 +85,7 @@ export function createTestConfig(name: string, options: CreateTestConfigOptions) ...xPackApiIntegrationTestsConfig.get('kbnTestServer'), serverArgs: [ ...xPackApiIntegrationTestsConfig.get('kbnTestServer.serverArgs'), - `--xpack.actions.whitelistedHosts=${JSON.stringify([ - 'localhost', - 'some.non.existent.com', - ])}`, + `--xpack.actions.allowedHosts=${JSON.stringify(['localhost', 'some.non.existent.com'])}`, '--xpack.encryptedSavedObjects.encryptionKey="wuGNaIhoMpk5sO4UBxgr3NyW1sFcLgIf"', `--xpack.actions.enabledActionTypes=${JSON.stringify(enabledActionTypes)}`, ...actionsProxyUrl, diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/email.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/email.ts index 1c3d3e3d713e2..329bd3433d388 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/email.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/email.ts @@ -153,7 +153,7 @@ export default function emailTest({ getService }: FtrProviderContext) { }); }); - it('should respond with a 400 Bad Request when creating an email action with non-whitelisted server', async () => { + it('should respond with a 400 Bad Request when creating an email action with a server not added to allowedHosts', async () => { await supertest .post('/api/actions/action') .set('kbn-xsrf', 'foo') @@ -161,7 +161,7 @@ export default function emailTest({ getService }: FtrProviderContext) { name: 'An email action', actionTypeId: '.email', config: { - service: 'gmail', // not whitelisted in the config for this test + service: 'gmail', // not added to allowedHosts in the config for this test from: 'bob@example.com', }, secrets: { @@ -175,7 +175,7 @@ export default function emailTest({ getService }: FtrProviderContext) { statusCode: 400, error: 'Bad Request', message: - "error validating action type config: [service] value 'gmail' resolves to host 'smtp.gmail.com' which is not in the whitelistedHosts configuration", + "error validating action type config: [service] value 'gmail' resolves to host 'smtp.gmail.com' which is not in the allowedHosts configuration", }); }); @@ -186,7 +186,7 @@ export default function emailTest({ getService }: FtrProviderContext) { name: 'An email action', actionTypeId: '.email', config: { - host: 'stmp.gmail.com', // not whitelisted in the config for this test + host: 'stmp.gmail.com', // not added to allowedHosts in the config for this test port: 666, from: 'bob@example.com', }, @@ -201,12 +201,12 @@ export default function emailTest({ getService }: FtrProviderContext) { statusCode: 400, error: 'Bad Request', message: - "error validating action type config: [host] value 'stmp.gmail.com' is not in the whitelistedHosts configuration", + "error validating action type config: [host] value 'stmp.gmail.com' is not in the allowedHosts configuration", }); }); }); - it('should handle creating an email action with a whitelisted server', async () => { + it('should handle creating an email action with a server added to allowedHosts', async () => { const { body: createdAction } = await supertest .post('/api/actions/action') .set('kbn-xsrf', 'foo') @@ -214,7 +214,7 @@ export default function emailTest({ getService }: FtrProviderContext) { name: 'An email action', actionTypeId: '.email', config: { - host: 'some.non.existent.com', // whitelisted in the config for this test + host: 'some.non.existent.com', // added to allowedHosts in the config for this test port: 666, from: 'bob@example.com', }, diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/jira.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/jira.ts index 78831fe8ff061..932d2658d2893 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/jira.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/jira.ts @@ -176,7 +176,7 @@ export default function jiraTest({ getService }: FtrProviderContext) { }); }); - it('should respond with a 400 Bad Request when creating a jira action with a non whitelisted apiUrl', async () => { + it('should respond with a 400 Bad Request when creating a jira action with a not present in allowedHosts apiUrl', async () => { await supertest .post('/api/actions/action') .set('kbn-xsrf', 'foo') @@ -196,7 +196,7 @@ export default function jiraTest({ getService }: FtrProviderContext) { statusCode: 400, error: 'Bad Request', message: - 'error validating action type config: error configuring connector action: target url "http://jira.mynonexistent.com" is not whitelisted in the Kibana config xpack.actions.whitelistedHosts', + 'error validating action type config: error configuring connector action: target url "http://jira.mynonexistent.com" is not added to the Kibana config xpack.actions.allowedHosts', }); }); }); diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/pagerduty.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/pagerduty.ts index e81219152c248..33df85b469b58 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/pagerduty.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/pagerduty.ts @@ -106,7 +106,7 @@ export default function pagerdutyTest({ getService }: FtrProviderContext) { }); }); - it('should return unsuccessfully when default pagerduty url is not whitelisted', async () => { + it('should return unsuccessfully when default pagerduty url is not present in allowedHosts', async () => { await supertest .post('/api/actions/action') .set('kbn-xsrf', 'foo') @@ -121,7 +121,7 @@ export default function pagerdutyTest({ getService }: FtrProviderContext) { statusCode: 400, error: 'Bad Request', message: - 'error validating action type config: error configuring pagerduty action: target url "https://events.pagerduty.com/v2/enqueue" is not whitelisted in the Kibana config xpack.actions.whitelistedHosts', + 'error validating action type config: error configuring pagerduty action: target url "https://events.pagerduty.com/v2/enqueue" is not added to the Kibana config xpack.actions.allowedHosts', }); }); }); diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/resilient.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/resilient.ts index 5085c87550d01..6a674d769dc16 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/resilient.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/resilient.ts @@ -175,7 +175,7 @@ export default function resilientTest({ getService }: FtrProviderContext) { }); }); - it('should respond with a 400 Bad Request when creating a ibm resilient action with a non whitelisted apiUrl', async () => { + it('should respond with a 400 Bad Request when creating a ibm resilient action with a not present in allowedHosts apiUrl', async () => { await supertest .post('/api/actions/action') .set('kbn-xsrf', 'foo') @@ -195,7 +195,7 @@ export default function resilientTest({ getService }: FtrProviderContext) { statusCode: 400, error: 'Bad Request', message: - 'error validating action type config: error configuring connector action: target url "http://resilient.mynonexistent.com" is not whitelisted in the Kibana config xpack.actions.whitelistedHosts', + 'error validating action type config: error configuring connector action: target url "http://resilient.mynonexistent.com" is not added to the Kibana config xpack.actions.allowedHosts', }); }); }); diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/servicenow.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/servicenow.ts index 3c8fc78b7f872..07826acf34a78 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/servicenow.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/servicenow.ts @@ -157,7 +157,7 @@ export default function servicenowTest({ getService }: FtrProviderContext) { }); }); - it('should respond with a 400 Bad Request when creating a servicenow action with a non whitelisted apiUrl', async () => { + it('should respond with a 400 Bad Request when creating a servicenow action with a not present in allowedHosts apiUrl', async () => { await supertest .post('/api/actions/action') .set('kbn-xsrf', 'foo') @@ -177,7 +177,7 @@ export default function servicenowTest({ getService }: FtrProviderContext) { statusCode: 400, error: 'Bad Request', message: - 'error validating action type config: error configuring connector action: target url "http://servicenow.mynonexistent.com" is not whitelisted in the Kibana config xpack.actions.whitelistedHosts', + 'error validating action type config: error configuring connector action: target url "http://servicenow.mynonexistent.com" is not added to the Kibana config xpack.actions.allowedHosts', }); }); }); diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/slack.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/slack.ts index 45f9ba369dc23..f7ac281f40343 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/slack.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/slack.ts @@ -94,7 +94,7 @@ export default function slackTest({ getService }: FtrProviderContext) { }); }); - it('should respond with a 400 Bad Request when creating a slack action with a non whitelisted webhookUrl', async () => { + it('should respond with a 400 Bad Request when creating a slack action with not present in allowedHosts webhookUrl', async () => { await supertest .post('/api/actions/action') .set('kbn-xsrf', 'foo') @@ -111,7 +111,7 @@ export default function slackTest({ getService }: FtrProviderContext) { statusCode: 400, error: 'Bad Request', message: - 'error validating action type secrets: error configuring slack action: target hostname "slack.mynonexistent.com" is not whitelisted in the Kibana config xpack.actions.whitelistedHosts', + 'error validating action type secrets: error configuring slack action: target hostname "slack.mynonexistent.com" is not added to the Kibana config xpack.actions.allowedHosts', }); }); }); diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/webhook.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/webhook.ts index 896026611043f..5dff07de8077c 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/webhook.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/webhook.ts @@ -191,7 +191,7 @@ export default function webhookTest({ getService }: FtrProviderContext) { expect(result.status).to.eql('ok'); }); - it('should handle target webhooks that are not whitelisted', async () => { + it('should handle target webhooks that are not added to allowedHosts', async () => { const { body: result } = await supertest .post('/api/actions/action') .set('kbn-xsrf', 'test') @@ -203,13 +203,13 @@ export default function webhookTest({ getService }: FtrProviderContext) { password: 'mypassphrase', }, config: { - url: 'http://a.none.whitelisted.webhook/endpoint', + url: 'http://a.none.allowedHosts.webhook/endpoint', }, }) .expect(400); expect(result.error).to.eql('Bad Request'); - expect(result.message).to.match(/is not whitelisted in the Kibana config/); + expect(result.message).to.match(/is not added to the Kibana config/); }); it('should handle unreachable webhook targets', async () => { diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/execute.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/execute.ts index a45eee400b445..5c4eb5f5d4c54 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/execute.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/execute.ts @@ -344,7 +344,7 @@ export default function ({ getService }: FtrProviderContext) { actionTypeId: '.email', config: { from: 'email-from-1@example.com', - // this host is specifically whitelisted in: + // this host is specifically added to allowedHosts in: // x-pack/test/alerting_api_integration/common/config.ts host: 'some.non.existent.com', port: 666, diff --git a/x-pack/test/case_api_integration/common/config.ts b/x-pack/test/case_api_integration/common/config.ts index 9b048813b479a..5d34f8b04981a 100644 --- a/x-pack/test/case_api_integration/common/config.ts +++ b/x-pack/test/case_api_integration/common/config.ts @@ -74,10 +74,7 @@ export function createTestConfig(name: string, options: CreateTestConfigOptions) ...xPackApiIntegrationTestsConfig.get('kbnTestServer'), serverArgs: [ ...xPackApiIntegrationTestsConfig.get('kbnTestServer.serverArgs'), - `--xpack.actions.whitelistedHosts=${JSON.stringify([ - 'localhost', - 'some.non.existent.com', - ])}`, + `--xpack.actions.allowedHosts=${JSON.stringify(['localhost', 'some.non.existent.com'])}`, `--xpack.actions.enabledActionTypes=${JSON.stringify(enabledActionTypes)}`, '--xpack.eventLog.logEntries=true', ...disabledPlugins.map((key) => `--xpack.${key}.enabled=false`), diff --git a/x-pack/test/detection_engine_api_integration/common/config.ts b/x-pack/test/detection_engine_api_integration/common/config.ts index 46fb877e94f23..c21e6d0fdecf0 100644 --- a/x-pack/test/detection_engine_api_integration/common/config.ts +++ b/x-pack/test/detection_engine_api_integration/common/config.ts @@ -67,10 +67,7 @@ export function createTestConfig(name: string, options: CreateTestConfigOptions) ...xPackApiIntegrationTestsConfig.get('kbnTestServer'), serverArgs: [ ...xPackApiIntegrationTestsConfig.get('kbnTestServer.serverArgs'), - `--xpack.actions.whitelistedHosts=${JSON.stringify([ - 'localhost', - 'some.non.existent.com', - ])}`, + `--xpack.actions.allowedHosts=${JSON.stringify(['localhost', 'some.non.existent.com'])}`, `--xpack.actions.enabledActionTypes=${JSON.stringify(enabledActionTypes)}`, '--xpack.eventLog.logEntries=true', ...disabledPlugins.map((key) => `--xpack.${key}.enabled=false`), From e31a0c27e66535acc0eb74026f118ffa0e8f4d6a Mon Sep 17 00:00:00 2001 From: Yuliia Naumenko Date: Mon, 24 Aug 2020 16:43:44 -0700 Subject: [PATCH 28/71] Fixed alerting_api_integration/security_and_spaces tests failing if actions proxy set on for parallel process running using commands 'scripts/functional_tests_server' and 'scripts/functional_test_runner' (#75232) * Fixed alerting_api_integration/security_and_spaces tests failing if actions proxy set on for parallel process running using commands 'scripts/functional_tests_server' and 'scripts/functional_test_runner' * - * Fixed get port from range for Slack and webhook simulators, removed some test warnings * Added check for listening proxy server * changed logger to debug removed not useful error * - * changed proxy to dynamic target in a single place * test retry * - * - * - * - * test with no cleanup * - * - * - * - * Added environment variable ALERTING_PROXY_PORT * fixed type checks * fixed clean up proxy server port --- vars/kibanaPipeline.groovy | 2 + x-pack/package.json | 2 +- .../servicenow/service.ts | 2 - .../server/builtin_action_types/slack.test.ts | 2 +- .../server/builtin_action_types/slack.ts | 4 +- .../alerting_api_integration/common/config.ts | 8 +++- .../server/slack_simulation.ts | 3 ++ .../common/lib/get_proxy_server.ts | 41 +++++++++++++------ .../actions/builtin_action_types/jira.ts | 18 +------- .../actions/builtin_action_types/pagerduty.ts | 18 +------- .../actions/builtin_action_types/resilient.ts | 15 ------- .../builtin_action_types/servicenow.ts | 18 +------- .../actions/builtin_action_types/slack.ts | 20 +++------ .../actions/builtin_action_types/webhook.ts | 17 ++------ .../tests/actions/index.ts | 21 +++++++++- yarn.lock | 8 ++-- 16 files changed, 77 insertions(+), 122 deletions(-) diff --git a/vars/kibanaPipeline.groovy b/vars/kibanaPipeline.groovy index 00668f2ccdaa7..e5b39584a519b 100644 --- a/vars/kibanaPipeline.groovy +++ b/vars/kibanaPipeline.groovy @@ -86,6 +86,7 @@ def withFunctionalTestEnv(List additionalEnvs = [], Closure closure) { def esPort = "61${parallelId}2" def esTransportPort = "61${parallelId}3" def ingestManagementPackageRegistryPort = "61${parallelId}4" + def alertingProxyPort = "61${parallelId}5" withEnv([ "CI_GROUP=${parallelId}", @@ -98,6 +99,7 @@ def withFunctionalTestEnv(List additionalEnvs = [], Closure closure) { "TEST_ES_TRANSPORT_PORT=${esTransportPort}", "KBN_NP_PLUGINS_BUILT=true", "INGEST_MANAGEMENT_PACKAGE_REGISTRY_PORT=${ingestManagementPackageRegistryPort}", + "ALERTING_PROXY_PORT=${alertingProxyPort}" ] + additionalEnvs) { closure() } diff --git a/x-pack/package.json b/x-pack/package.json index a9ffb85924562..992a186d41d78 100644 --- a/x-pack/package.json +++ b/x-pack/package.json @@ -269,7 +269,7 @@ "font-awesome": "4.7.0", "formsy-react": "^1.1.5", "fp-ts": "^2.3.1", - "get-port": "^4.2.0", + "get-port": "^5.0.0", "getos": "^3.1.0", "git-url-parse": "11.1.2", "github-markdown-css": "^2.10.0", diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/service.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/service.ts index cf1c26e6462a2..9b1da4b4007c6 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/service.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/service.ts @@ -78,8 +78,6 @@ export const createExternalService = ( const createIncident = async ({ incident }: ExternalServiceParams) => { try { - logger.warn(`incident error : ${JSON.stringify(proxySettings)}`); - logger.warn(`incident error : ${url}`); const res = await request({ axios: axiosInstance, url: `${incidentUrl}`, diff --git a/x-pack/plugins/actions/server/builtin_action_types/slack.test.ts b/x-pack/plugins/actions/server/builtin_action_types/slack.test.ts index 81fa5553b331e..b15d92cecba62 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/slack.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/slack.test.ts @@ -209,7 +209,7 @@ describe('execute()', () => { rejectUnauthorizedCertificates: false, }, }); - expect(mockedLogger.info).toHaveBeenCalledWith( + expect(mockedLogger.debug).toHaveBeenCalledWith( 'IncomingWebhook was called with proxyUrl https://someproxyhost' ); }); diff --git a/x-pack/plugins/actions/server/builtin_action_types/slack.ts b/x-pack/plugins/actions/server/builtin_action_types/slack.ts index 639b4448b5a89..1605cd4b69f5e 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/slack.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/slack.ts @@ -119,7 +119,7 @@ async function slackExecutor( let proxyAgent: HttpsProxyAgent | HttpProxyAgent | undefined; if (execOptions.proxySettings) { proxyAgent = getProxyAgent(execOptions.proxySettings, logger); - logger.info(`IncomingWebhook was called with proxyUrl ${execOptions.proxySettings.proxyUrl}`); + logger.debug(`IncomingWebhook was called with proxyUrl ${execOptions.proxySettings.proxyUrl}`); } try { @@ -130,8 +130,6 @@ async function slackExecutor( }); result = await webhook.send(message); } catch (err) { - logger.error(`error on ${actionId} slack event: ${err.message}`); - if (err.original == null || err.original.response == null) { return serviceErrorResult(actionId, err.message); } diff --git a/x-pack/test/alerting_api_integration/common/config.ts b/x-pack/test/alerting_api_integration/common/config.ts index 117a59a304368..67dd8c877e378 100644 --- a/x-pack/test/alerting_api_integration/common/config.ts +++ b/x-pack/test/alerting_api_integration/common/config.ts @@ -58,8 +58,13 @@ export function createTestConfig(name: string, options: CreateTestConfigOptions) fs.statSync(path.resolve(__dirname, 'fixtures', 'plugins', file)).isDirectory() ); + const proxyPort = + process.env.ALERTING_PROXY_PORT ?? (await getPort({ port: getPort.makeRange(6200, 6300) })); const actionsProxyUrl = options.enableActionsProxy - ? [`--xpack.actions.proxyUrl=http://localhost:${await getPort()}`] + ? [ + `--xpack.actions.proxyUrl=http://localhost:${proxyPort}`, + '--xpack.actions.rejectUnauthorizedCertificates=false', + ] : []; return { @@ -89,7 +94,6 @@ export function createTestConfig(name: string, options: CreateTestConfigOptions) '--xpack.encryptedSavedObjects.encryptionKey="wuGNaIhoMpk5sO4UBxgr3NyW1sFcLgIf"', `--xpack.actions.enabledActionTypes=${JSON.stringify(enabledActionTypes)}`, ...actionsProxyUrl, - '--xpack.actions.rejectUnauthorizedCertificates=false', '--xpack.eventLog.logEntries=true', `--xpack.actions.preconfigured=${JSON.stringify({ diff --git a/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/server/slack_simulation.ts b/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/server/slack_simulation.ts index 5032112e702e2..8f5b1ea75d188 100644 --- a/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/server/slack_simulation.ts +++ b/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/server/slack_simulation.ts @@ -64,6 +64,9 @@ export async function initPlugin() { response.statusCode = 400; response.end('unknown request to slack simulator'); }); + } else { + response.writeHead(400, { 'Content-Type': 'text/plain' }); + response.end('Not supported http method to request slack simulator'); } }); } diff --git a/x-pack/test/alerting_api_integration/common/lib/get_proxy_server.ts b/x-pack/test/alerting_api_integration/common/lib/get_proxy_server.ts index 4540556e73c5f..7528b00f926d0 100644 --- a/x-pack/test/alerting_api_integration/common/lib/get_proxy_server.ts +++ b/x-pack/test/alerting_api_integration/common/lib/get_proxy_server.ts @@ -4,27 +4,42 @@ * you may not use this file except in compliance with the Elastic License. */ +import http from 'http'; import httpProxy from 'http-proxy'; +import { ToolingLog } from '@kbn/dev-utils'; -export const getHttpProxyServer = ( - targetUrl: string, - onProxyResHandler: (proxyRes?: unknown, req?: unknown, res?: unknown) => void -): httpProxy => { - const proxyServer = httpProxy.createProxyServer({ - target: targetUrl, - secure: false, - selfHandleResponse: false, - }); - proxyServer.on('proxyRes', (proxyRes: unknown, req: unknown, res: unknown) => { - onProxyResHandler(proxyRes, req, res); +export const getHttpProxyServer = async ( + defaultKibanaTargetUrl: string, + kbnTestServerConfig: any, + log: ToolingLog +): Promise => { + const proxy = httpProxy.createProxyServer({ secure: false, selfHandleResponse: false }); + + const proxyPort = getProxyPort(kbnTestServerConfig); + const proxyServer = http.createServer((req: http.IncomingMessage, res: http.ServerResponse) => { + const targetUrl = new URL(req.url ?? defaultKibanaTargetUrl); + + if (targetUrl.hostname !== 'some.non.existent.com') { + proxy.web(req, res, { + target: `${targetUrl.protocol}//${targetUrl.hostname}:${targetUrl.port}`, + }); + } else { + res.writeHead(500, { 'Content-Type': 'text/plain' }); + res.write('error on call some.non.existent.com'); + res.end(); + } }); + + proxyServer.listen(proxyPort); + return proxyServer; }; -export const getProxyUrl = (kbnTestServerConfig: any) => { +export const getProxyPort = (kbnTestServerConfig: any): number => { const proxyUrl = kbnTestServerConfig .find((val: string) => val.startsWith('--xpack.actions.proxyUrl=')) .replace('--xpack.actions.proxyUrl=', ''); - return new URL(proxyUrl); + const urlObject = new URL(proxyUrl); + return Number(urlObject.port); }; diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/jira.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/jira.ts index 932d2658d2893..78a1df0b9c1c7 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/jira.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/jira.ts @@ -6,7 +6,6 @@ import expect from '@kbn/expect'; -import { getHttpProxyServer, getProxyUrl } from '../../../../common/lib/get_proxy_server'; import { FtrProviderContext } from '../../../../common/ftr_provider_context'; import { @@ -36,7 +35,6 @@ const mapping = [ export default function jiraTest({ getService }: FtrProviderContext) { const supertest = getService('supertest'); const kibanaServer = getService('kibanaServer'); - const config = getService('config'); const mockJira = { config: { @@ -75,20 +73,12 @@ export default function jiraTest({ getService }: FtrProviderContext) { }; let jiraSimulatorURL: string = ''; - let proxyServer: any; - let proxyHaveBeenCalled = false; - // FLAKY: https://github.com/elastic/kibana/issues/75722 - describe.skip('Jira', () => { + describe('Jira', () => { before(() => { jiraSimulatorURL = kibanaServer.resolveUrl( getExternalServiceSimulatorPath(ExternalServiceSimulator.JIRA) ); - proxyServer = getHttpProxyServer(kibanaServer.resolveUrl('/'), () => { - proxyHaveBeenCalled = true; - }); - const proxyUrl = getProxyUrl(config.get('kbnTestServer.serverArgs')); - proxyServer.listen(Number(proxyUrl.port)); }); describe('Jira - Action Creation', () => { @@ -539,8 +529,6 @@ export default function jiraTest({ getService }: FtrProviderContext) { }) .expect(200); - expect(proxyHaveBeenCalled).to.equal(true); - expect(body).to.eql({ status: 'ok', actionId: simulatedActionId, @@ -554,9 +542,5 @@ export default function jiraTest({ getService }: FtrProviderContext) { }); }); }); - - after(() => { - proxyServer.close(); - }); }); } diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/pagerduty.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/pagerduty.ts index 33df85b469b58..76b3e8e39791a 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/pagerduty.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/pagerduty.ts @@ -6,7 +6,6 @@ import expect from '@kbn/expect'; -import { getHttpProxyServer, getProxyUrl } from '../../../../common/lib/get_proxy_server'; import { FtrProviderContext } from '../../../../common/ftr_provider_context'; import { @@ -18,26 +17,16 @@ import { export default function pagerdutyTest({ getService }: FtrProviderContext) { const supertest = getService('supertest'); const kibanaServer = getService('kibanaServer'); - const config = getService('config'); - // FLAKY: https://github.com/elastic/kibana/issues/75386 - describe.skip('pagerduty action', () => { + describe('pagerduty action', () => { let simulatedActionId = ''; let pagerdutySimulatorURL: string = ''; - let proxyServer: any; - let proxyHaveBeenCalled = false; // need to wait for kibanaServer to settle ... before(() => { pagerdutySimulatorURL = kibanaServer.resolveUrl( getExternalServiceSimulatorPath(ExternalServiceSimulator.PAGERDUTY) ); - - proxyServer = getHttpProxyServer(kibanaServer.resolveUrl('/'), () => { - proxyHaveBeenCalled = true; - }); - const proxyUrl = getProxyUrl(config.get('kbnTestServer.serverArgs')); - proxyServer.listen(Number(proxyUrl.port)); }); it('should return successfully when passed valid create parameters', async () => { @@ -155,7 +144,6 @@ export default function pagerdutyTest({ getService }: FtrProviderContext) { }, }) .expect(200); - expect(proxyHaveBeenCalled).to.equal(true); expect(result).to.eql({ status: 'ok', @@ -215,9 +203,5 @@ export default function pagerdutyTest({ getService }: FtrProviderContext) { expect(result.message).to.match(/error posting pagerduty event: http status 502/); expect(result.retry).to.equal(true); }); - - after(() => { - proxyServer.close(); - }); }); } diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/resilient.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/resilient.ts index 6a674d769dc16..8adaf9f121931 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/resilient.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/resilient.ts @@ -6,7 +6,6 @@ import expect from '@kbn/expect'; -import { getHttpProxyServer, getProxyUrl } from '../../../../common/lib/get_proxy_server'; import { FtrProviderContext } from '../../../../common/ftr_provider_context'; import { @@ -36,7 +35,6 @@ const mapping = [ export default function resilientTest({ getService }: FtrProviderContext) { const supertest = getService('supertest'); const kibanaServer = getService('kibanaServer'); - const config = getService('config'); const mockResilient = { config: { @@ -75,19 +73,12 @@ export default function resilientTest({ getService }: FtrProviderContext) { }; let resilientSimulatorURL: string = ''; - let proxyServer: any; - let proxyHaveBeenCalled = false; describe('IBM Resilient', () => { before(() => { resilientSimulatorURL = kibanaServer.resolveUrl( getExternalServiceSimulatorPath(ExternalServiceSimulator.RESILIENT) ); - proxyServer = getHttpProxyServer(kibanaServer.resolveUrl('/'), () => { - proxyHaveBeenCalled = true; - }); - const proxyUrl = getProxyUrl(config.get('kbnTestServer.serverArgs')); - proxyServer.listen(Number(proxyUrl.port)); }); describe('IBM Resilient - Action Creation', () => { @@ -538,8 +529,6 @@ export default function resilientTest({ getService }: FtrProviderContext) { }) .expect(200); - expect(proxyHaveBeenCalled).to.equal(true); - expect(body).to.eql({ status: 'ok', actionId: simulatedActionId, @@ -553,9 +542,5 @@ export default function resilientTest({ getService }: FtrProviderContext) { }); }); }); - - after(() => { - proxyServer.close(); - }); }); } diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/servicenow.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/servicenow.ts index 07826acf34a78..2dad6f2c425e5 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/servicenow.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/servicenow.ts @@ -6,7 +6,6 @@ import expect from '@kbn/expect'; -import { getHttpProxyServer, getProxyUrl } from '../../../../common/lib/get_proxy_server'; import { FtrProviderContext } from '../../../../common/ftr_provider_context'; import { @@ -36,7 +35,6 @@ const mapping = [ export default function servicenowTest({ getService }: FtrProviderContext) { const supertest = getService('supertest'); const kibanaServer = getService('kibanaServer'); - const config = getService('config'); const mockServiceNow = { config: { @@ -74,21 +72,12 @@ export default function servicenowTest({ getService }: FtrProviderContext) { }; let servicenowSimulatorURL: string = ''; - let proxyServer: any; - let proxyHaveBeenCalled = false; - // FLAKY: https://github.com/elastic/kibana/issues/75522 - describe.skip('ServiceNow', () => { + describe('ServiceNow', () => { before(() => { servicenowSimulatorURL = kibanaServer.resolveUrl( getExternalServiceSimulatorPath(ExternalServiceSimulator.SERVICENOW) ); - - proxyServer = getHttpProxyServer(kibanaServer.resolveUrl('/'), () => { - proxyHaveBeenCalled = true; - }); - const proxyUrl = getProxyUrl(config.get('kbnTestServer.serverArgs')); - proxyServer.listen(Number(proxyUrl.port)); }); describe('ServiceNow - Action Creation', () => { @@ -459,7 +448,6 @@ export default function servicenowTest({ getService }: FtrProviderContext) { }, }) .expect(200); - expect(proxyHaveBeenCalled).to.equal(true); expect(result).to.eql({ status: 'ok', @@ -474,9 +462,5 @@ export default function servicenowTest({ getService }: FtrProviderContext) { }); }); }); - - after(() => { - proxyServer.close(); - }); }); } diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/slack.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/slack.ts index f7ac281f40343..1712c31187b02 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/slack.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/slack.ts @@ -7,7 +7,6 @@ import expect from '@kbn/expect'; import http from 'http'; import getPort from 'get-port'; -import { getHttpProxyServer, getProxyUrl } from '../../../../common/lib/get_proxy_server'; import { FtrProviderContext } from '../../../../common/ftr_provider_context'; import { getSlackServer } from '../../../../common/fixtures/plugins/actions_simulators/server/plugin'; @@ -15,27 +14,20 @@ import { getSlackServer } from '../../../../common/fixtures/plugins/actions_simu // eslint-disable-next-line import/no-default-export export default function slackTest({ getService }: FtrProviderContext) { const supertest = getService('supertest'); - const config = getService('config'); describe('slack action', () => { let simulatedActionId = ''; - let slackSimulatorURL: string = ''; let slackServer: http.Server; - let proxyServer: any; - let proxyHaveBeenCalled = false; + // need to wait for kibanaServer to settle ... before(async () => { slackServer = await getSlackServer(); - const availablePort = await getPort({ port: 9000 }); - slackServer.listen(availablePort); + const availablePort = await getPort({ port: getPort.makeRange(9000, 9100) }); + if (!slackServer.listening) { + slackServer.listen(availablePort); + } slackSimulatorURL = `http://localhost:${availablePort}`; - - proxyServer = getHttpProxyServer(slackSimulatorURL, () => { - proxyHaveBeenCalled = true; - }); - const proxyUrl = getProxyUrl(config.get('kbnTestServer.serverArgs')); - proxyServer.listen(Number(proxyUrl.port)); }); it('should return 200 when creating a slack action successfully', async () => { @@ -165,7 +157,6 @@ export default function slackTest({ getService }: FtrProviderContext) { }) .expect(200); expect(result.status).to.eql('ok'); - expect(proxyHaveBeenCalled).to.equal(true); }); it('should handle an empty message error', async () => { @@ -233,7 +224,6 @@ export default function slackTest({ getService }: FtrProviderContext) { after(() => { slackServer.close(); - proxyServer.close(); }); }); } diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/webhook.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/webhook.ts index 5dff07de8077c..abebb2650ad08 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/webhook.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/webhook.ts @@ -5,10 +5,9 @@ */ import http from 'http'; -import getPort from 'get-port'; import expect from '@kbn/expect'; import { URL, format as formatUrl } from 'url'; -import { getHttpProxyServer, getProxyUrl } from '../../../../common/lib/get_proxy_server'; +import getPort from 'get-port'; import { FtrProviderContext } from '../../../../common/ftr_provider_context'; import { getExternalServiceSimulatorPath, @@ -32,7 +31,6 @@ function parsePort(url: Record): Record { webhookServer = await getWebhookServer(); - const availablePort = await getPort({ port: 9000 }); + const availablePort = await getPort({ port: getPort.makeRange(9000, 9100) }); webhookServer.listen(availablePort); webhookSimulatorURL = `http://localhost:${availablePort}`; - proxyServer = getHttpProxyServer(webhookSimulatorURL, () => { - proxyHaveBeenCalled = true; - }); - const proxyUrl = getProxyUrl(configService.get('kbnTestServer.serverArgs')); - proxyServer.listen(Number(proxyUrl.port)); - kibanaURL = kibanaServer.resolveUrl( getExternalServiceSimulatorPath(ExternalServiceSimulator.WEBHOOK) ); @@ -150,7 +141,6 @@ export default function webhookTest({ getService }: FtrProviderContext) { .expect(200); expect(result.status).to.eql('ok'); - expect(proxyHaveBeenCalled).to.equal(true); }); it('should support the POST method against webhook target', async () => { @@ -251,7 +241,6 @@ export default function webhookTest({ getService }: FtrProviderContext) { after(() => { webhookServer.close(); - proxyServer.close(); }); }); } diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/index.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/index.ts index 9cdc0c9fa663e..54484ba34636f 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/index.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/index.ts @@ -4,11 +4,24 @@ * you may not use this file except in compliance with the Elastic License. */ +import http from 'http'; +import { getHttpProxyServer } from '../../../common/lib/get_proxy_server'; import { FtrProviderContext } from '../../../common/ftr_provider_context'; // eslint-disable-next-line import/no-default-export -export default function actionsTests({ loadTestFile }: FtrProviderContext) { +export default function actionsTests({ loadTestFile, getService }: FtrProviderContext) { + const configService = getService('config'); + const kibanaServer = getService('kibanaServer'); + const log = getService('log'); describe('Actions', () => { + let proxyServer: http.Server | undefined; + before(async () => { + proxyServer = await getHttpProxyServer( + kibanaServer.resolveUrl('/'), + configService.get('kbnTestServer.serverArgs'), + log + ); + }); loadTestFile(require.resolve('./builtin_action_types/email')); loadTestFile(require.resolve('./builtin_action_types/es_index')); loadTestFile(require.resolve('./builtin_action_types/es_index_preconfigured')); @@ -26,5 +39,11 @@ export default function actionsTests({ loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./get')); loadTestFile(require.resolve('./list_action_types')); loadTestFile(require.resolve('./update')); + + after(() => { + if (proxyServer) { + proxyServer.close(); + } + }); }); } diff --git a/yarn.lock b/yarn.lock index f00bee1d13dc2..845685ff36f7d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -13694,10 +13694,10 @@ get-own-enumerable-property-symbols@^3.0.0: resolved "https://registry.yarnpkg.com/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.0.tgz#b877b49a5c16aefac3655f2ed2ea5b684df8d203" integrity sha512-CIJYJC4GGF06TakLg8z4GQKvDsx9EMspVxOYih7LerEL/WosUnFIww45CGfxfeKHqlg3twgUrYRT1O3WQqjGCg== -get-port@^4.2.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/get-port/-/get-port-4.2.0.tgz#e37368b1e863b7629c43c5a323625f95cf24b119" - integrity sha512-/b3jarXkH8KJoOMQc3uVGHASwGLPq3gSFJ7tgJm2diza+bydJPTGOibin2steecKeOylE8oY2JERlVWkAJO6yw== +get-port@^5.0.0: + version "5.1.1" + resolved "https://registry.yarnpkg.com/get-port/-/get-port-5.1.1.tgz#0469ed07563479de6efb986baf053dcd7d4e3193" + integrity sha512-g/Q1aTSDOxFpchXC4i8ZWvxA1lnPqx/JHqcpIw0/LX9T8x/GBbi6YnlN5nhaKIFkT8oFsscUKgDJYxfwfS6QsQ== get-stdin@^4.0.1: version "4.0.1" From b82e4d8a845f8571c6409663ed52139113540efd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cau=C3=AA=20Marcondes?= <55978943+cauemarcondes@users.noreply.github.com> Date: Tue, 25 Aug 2020 08:03:22 +0100 Subject: [PATCH 29/71] [APM] User can't navigate back home using browser nav when clicking link (#75755) * replaces the route when parmeter is missing * fixing unit test --- .../shared/DatePicker/__test__/DatePicker.test.tsx | 14 ++++++++------ .../public/components/shared/DatePicker/index.tsx | 9 ++++++++- 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/x-pack/plugins/apm/public/components/shared/DatePicker/__test__/DatePicker.test.tsx b/x-pack/plugins/apm/public/components/shared/DatePicker/__test__/DatePicker.test.tsx index 2434d898389d8..7a63b9e767fe7 100644 --- a/x-pack/plugins/apm/public/components/shared/DatePicker/__test__/DatePicker.test.tsx +++ b/x-pack/plugins/apm/public/components/shared/DatePicker/__test__/DatePicker.test.tsx @@ -20,6 +20,7 @@ import { wait } from '@testing-library/react'; import { MockApmPluginContextWrapper } from '../../../../context/ApmPluginContext/MockApmPluginContext'; const mockHistoryPush = jest.spyOn(history, 'push'); +const mockHistoryReplace = jest.spyOn(history, 'replace'); const mockRefreshTimeRange = jest.fn(); function MockUrlParamsProvider({ params = {}, @@ -69,8 +70,8 @@ describe('DatePicker', () => { it('sets default query params in the URL', () => { mountDatePicker(); - expect(mockHistoryPush).toHaveBeenCalledTimes(1); - expect(mockHistoryPush).toHaveBeenCalledWith( + expect(mockHistoryReplace).toHaveBeenCalledTimes(1); + expect(mockHistoryReplace).toHaveBeenCalledWith( expect.objectContaining({ search: 'rangeFrom=now-15m&rangeTo=now', }) @@ -82,8 +83,8 @@ describe('DatePicker', () => { rangeTo: 'now', refreshInterval: 5000, }); - expect(mockHistoryPush).toHaveBeenCalledTimes(1); - expect(mockHistoryPush).toHaveBeenCalledWith( + expect(mockHistoryReplace).toHaveBeenCalledTimes(1); + expect(mockHistoryReplace).toHaveBeenCalledWith( expect.objectContaining({ search: 'rangeFrom=now-15m&rangeTo=now&refreshInterval=5000', }) @@ -97,18 +98,19 @@ describe('DatePicker', () => { refreshPaused: false, refreshInterval: 5000, }); - expect(mockHistoryPush).toHaveBeenCalledTimes(0); + expect(mockHistoryReplace).toHaveBeenCalledTimes(0); }); it('updates the URL when the date range changes', () => { const datePicker = mountDatePicker(); + expect(mockHistoryReplace).toHaveBeenCalledTimes(1); datePicker.find(EuiSuperDatePicker).props().onTimeChange({ start: 'updated-start', end: 'updated-end', isInvalid: false, isQuickSelection: true, }); - expect(mockHistoryPush).toHaveBeenCalledTimes(2); + expect(mockHistoryPush).toHaveBeenCalledTimes(1); expect(mockHistoryPush).toHaveBeenLastCalledWith( expect.objectContaining({ search: 'rangeFrom=updated-start&rangeTo=updated-end', diff --git a/x-pack/plugins/apm/public/components/shared/DatePicker/index.tsx b/x-pack/plugins/apm/public/components/shared/DatePicker/index.tsx index 403a8cad854cd..35b9525733e99 100644 --- a/x-pack/plugins/apm/public/components/shared/DatePicker/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/DatePicker/index.tsx @@ -89,7 +89,14 @@ export function DatePicker() { ...timePickerURLParams, }; if (!isEqual(nextParams, timePickerURLParams)) { - updateUrl(nextParams); + // When the default parameters are not availbale in the url, replace it adding the necessary parameters. + history.replace({ + ...location, + search: fromQuery({ + ...toQuery(location.search), + ...nextParams, + }), + }); } return ( From 40d8edc2a07ee782bd9521d815f69638ef5be7da Mon Sep 17 00:00:00 2001 From: Peter Pisljar Date: Tue, 25 Aug 2020 09:31:03 +0200 Subject: [PATCH 30/71] cleaning up embeddable types (#75560) --- .../public/book/edit_book_action.tsx | 4 ++-- .../public/list_container_example.tsx | 21 +++++++++---------- .../actions/add_to_library_action.test.tsx | 9 ++++++-- .../actions/clone_panel_action.tsx | 8 +++++-- .../unlink_from_library_action.test.tsx | 8 +++++-- .../application/dashboard_app_controller.tsx | 3 ++- ...embeddable_saved_object_converters.test.ts | 5 +++-- .../public/lib/containers/i_container.ts | 4 ++-- .../public/lib/embeddables/i_embeddable.ts | 2 -- .../embeddable/public/tests/container.test.ts | 12 ++++++++--- .../public/embeddable/visualize_embeddable.ts | 3 ++- .../drilldown.tsx | 3 ++- .../explore_data/explore_data_chart_action.ts | 7 +++++-- 13 files changed, 56 insertions(+), 33 deletions(-) diff --git a/examples/embeddable_examples/public/book/edit_book_action.tsx b/examples/embeddable_examples/public/book/edit_book_action.tsx index b31d69696598e..5b14dc85b1fc7 100644 --- a/examples/embeddable_examples/public/book/edit_book_action.tsx +++ b/examples/embeddable_examples/public/book/edit_book_action.tsx @@ -65,8 +65,8 @@ export const createEditBookAction = (getStartServices: () => Promise { const newInput = await attributeService.wrapAttributes(attributes, useRefType, embeddable); if (!useRefType && (embeddable.getInput() as SavedObjectEmbeddableInput).savedObjectId) { - // Remove the savedObejctId when un-linking - newInput.savedObjectId = null; + // Set the saved object ID to null so that update input will remove the existing savedObjectId... + (newInput as BookByValueInput & { savedObjectId: unknown }).savedObjectId = null; } embeddable.updateInput(newInput); if (useRefType) { diff --git a/examples/embeddable_explorer/public/list_container_example.tsx b/examples/embeddable_explorer/public/list_container_example.tsx index b9bd825ed0240..d9d9c49249ab3 100644 --- a/examples/embeddable_explorer/public/list_container_example.tsx +++ b/examples/embeddable_explorer/public/list_container_example.tsx @@ -29,11 +29,7 @@ import { EuiText, EuiTitle, } from '@elastic/eui'; -import { - EmbeddableInput, - EmbeddableRenderer, - ViewMode, -} from '../../../src/plugins/embeddable/public'; +import { EmbeddableRenderer, ViewMode } from '../../../src/plugins/embeddable/public'; import { HELLO_WORLD_EMBEDDABLE, MULTI_TASK_TODO_EMBEDDABLE, @@ -41,6 +37,9 @@ import { ListContainerFactory, SearchableListContainerFactory, } from '../../embeddable_examples/public'; +import { SearchableContainerInput } from '../../embeddable_examples/public/searchable_list_container/searchable_list_container'; +import { TodoInput } from '../../embeddable_examples/public/todo'; +import { MultiTaskTodoInput } from '../../embeddable_examples/public/multi_task_todo'; interface Props { listContainerEmbeddableFactory: ListContainerFactory; @@ -51,7 +50,7 @@ export function ListContainerExample({ listContainerEmbeddableFactory, searchableListContainerEmbeddableFactory, }: Props) { - const listInput: EmbeddableInput = { + const listInput: SearchableContainerInput = { id: 'hello', title: 'My todo list', viewMode: ViewMode.VIEW, @@ -69,7 +68,7 @@ export function ListContainerExample({ task: 'Goes out on Wednesdays!', icon: 'broom', title: 'Take out the trash', - }, + } as TodoInput, }, '3': { type: TODO_EMBEDDABLE, @@ -77,12 +76,12 @@ export function ListContainerExample({ id: '3', icon: 'broom', title: 'Vaccum the floor', - }, + } as TodoInput, }, }, }; - const searchableInput: EmbeddableInput = { + const searchableInput: SearchableContainerInput = { id: '1', title: 'My searchable todo list', viewMode: ViewMode.VIEW, @@ -101,7 +100,7 @@ export function ListContainerExample({ task: 'Goes out on Wednesdays!', icon: 'broom', title: 'Take out the trash', - }, + } as TodoInput, }, '3': { type: MULTI_TASK_TODO_EMBEDDABLE, @@ -110,7 +109,7 @@ export function ListContainerExample({ icon: 'searchProfilerApp', title: 'Learn more', tasks: ['Go to school', 'Watch planet earth', 'Read the encyclopedia'], - }, + } as MultiTaskTodoInput, }, }, }; diff --git a/src/plugins/dashboard/public/application/actions/add_to_library_action.test.tsx b/src/plugins/dashboard/public/application/actions/add_to_library_action.test.tsx index 9fa7fff9ad087..755269d1a31be 100644 --- a/src/plugins/dashboard/public/application/actions/add_to_library_action.test.tsx +++ b/src/plugins/dashboard/public/application/actions/add_to_library_action.test.tsx @@ -16,7 +16,12 @@ * specific language governing permissions and limitations * under the License. */ -import { isErrorEmbeddable, IContainer, ReferenceOrValueEmbeddable } from '../../embeddable_plugin'; +import { + isErrorEmbeddable, + IContainer, + ReferenceOrValueEmbeddable, + EmbeddableInput, +} from '../../embeddable_plugin'; import { DashboardContainer } from '../embeddable'; import { getSampleDashboardInput } from '../test_helpers'; import { @@ -145,7 +150,7 @@ test('Add to library returns reference type input', async () => { embeddable = embeddablePluginMock.mockRefOrValEmbeddable(embeddable, { mockedByReferenceInput: { savedObjectId: 'testSavedObjectId', id: embeddable.id }, - mockedByValueInput: { attributes: complicatedAttributes, id: embeddable.id }, + mockedByValueInput: { attributes: complicatedAttributes, id: embeddable.id } as EmbeddableInput, }); const dashboard = embeddable.getRoot() as IContainer; const originalPanelKeySet = new Set(Object.keys(dashboard.getInput().panels)); diff --git a/src/plugins/dashboard/public/application/actions/clone_panel_action.tsx b/src/plugins/dashboard/public/application/actions/clone_panel_action.tsx index 26af13b4410fe..dc5887ee0e644 100644 --- a/src/plugins/dashboard/public/application/actions/clone_panel_action.tsx +++ b/src/plugins/dashboard/public/application/actions/clone_panel_action.tsx @@ -24,7 +24,11 @@ import _ from 'lodash'; import { ActionByType, IncompatibleActionError } from '../../ui_actions_plugin'; import { ViewMode, PanelState, IEmbeddable } from '../../embeddable_plugin'; import { SavedObject } from '../../../../saved_objects/public'; -import { PanelNotFoundError, EmbeddableInput } from '../../../../embeddable/public'; +import { + PanelNotFoundError, + EmbeddableInput, + SavedObjectEmbeddableInput, +} from '../../../../embeddable/public'; import { placePanelBeside, IPanelPlacementBesideArgs, @@ -143,7 +147,7 @@ export class ClonePanelAction implements ActionByType }, { references: _.cloneDeep(savedObjectToClone.references) } ); - panelState.explicitInput.savedObjectId = clonedSavedObject.id; + (panelState.explicitInput as SavedObjectEmbeddableInput).savedObjectId = clonedSavedObject.id; } this.core.notifications.toasts.addSuccess({ title: i18n.translate('dashboard.panel.clonedToast', { diff --git a/src/plugins/dashboard/public/application/actions/unlink_from_library_action.test.tsx b/src/plugins/dashboard/public/application/actions/unlink_from_library_action.test.tsx index 681a6a734a532..b4178fd40c768 100644 --- a/src/plugins/dashboard/public/application/actions/unlink_from_library_action.test.tsx +++ b/src/plugins/dashboard/public/application/actions/unlink_from_library_action.test.tsx @@ -30,7 +30,7 @@ import { coreMock } from '../../../../../core/public/mocks'; import { CoreStart } from 'kibana/public'; import { UnlinkFromLibraryAction } from '.'; import { embeddablePluginMock } from 'src/plugins/embeddable/public/mocks'; -import { ViewMode } from '../../../../embeddable/public'; +import { ViewMode, SavedObjectEmbeddableInput } from '../../../../embeddable/public'; const { setup, doStart } = embeddablePluginMock.createInstance(); setup.registerEmbeddableFactory( @@ -142,7 +142,11 @@ test('Unlink unwraps all attributes from savedObject', async () => { attribute4: { nestedattribute: 'hello from the nest' }, }; - embeddable = embeddablePluginMock.mockRefOrValEmbeddable(embeddable, { + embeddable = embeddablePluginMock.mockRefOrValEmbeddable< + ContactCardEmbeddable, + { attributes: unknown; id: string }, + SavedObjectEmbeddableInput + >(embeddable, { mockedByReferenceInput: { savedObjectId: 'testSavedObjectId', id: embeddable.id }, mockedByValueInput: { attributes: complicatedAttributes, id: embeddable.id }, }); diff --git a/src/plugins/dashboard/public/application/dashboard_app_controller.tsx b/src/plugins/dashboard/public/application/dashboard_app_controller.tsx index 7a19514eebe17..e10265376f2de 100644 --- a/src/plugins/dashboard/public/application/dashboard_app_controller.tsx +++ b/src/plugins/dashboard/public/application/dashboard_app_controller.tsx @@ -474,7 +474,8 @@ export class DashboardAppController { : undefined; container.addOrUpdateEmbeddable( incomingEmbeddable.type, - explicitInput, + // This ugly solution is temporary - https://github.com/elastic/kibana/pull/70272 fixes this whole section + (explicitInput as unknown) as EmbeddableInput, embeddableId ); } diff --git a/src/plugins/dashboard/public/application/lib/embeddable_saved_object_converters.test.ts b/src/plugins/dashboard/public/application/lib/embeddable_saved_object_converters.test.ts index 25ce203332422..926d5f405b384 100644 --- a/src/plugins/dashboard/public/application/lib/embeddable_saved_object_converters.test.ts +++ b/src/plugins/dashboard/public/application/lib/embeddable_saved_object_converters.test.ts @@ -23,6 +23,7 @@ import { } from './embeddable_saved_object_converters'; import { SavedDashboardPanel } from '../../types'; import { DashboardPanelState } from '../embeddable'; +import { EmbeddableInput } from '../../../../embeddable/public'; test('convertSavedDashboardPanelToPanelState', () => { const savedDashboardPanel: SavedDashboardPanel = { @@ -93,7 +94,7 @@ test('convertPanelStateToSavedDashboardPanel', () => { something: 'hi!', id: '123', savedObjectId: 'savedObjectId', - }, + } as EmbeddableInput, type: 'search', }; @@ -127,7 +128,7 @@ test('convertPanelStateToSavedDashboardPanel will not add an undefined id when n explicitInput: { id: '123', something: 'hi!', - }, + } as EmbeddableInput, type: 'search', }; diff --git a/src/plugins/embeddable/public/lib/containers/i_container.ts b/src/plugins/embeddable/public/lib/containers/i_container.ts index 31a7cd4f2e559..db219fa8b7314 100644 --- a/src/plugins/embeddable/public/lib/containers/i_container.ts +++ b/src/plugins/embeddable/public/lib/containers/i_container.ts @@ -25,7 +25,7 @@ import { IEmbeddable, } from '../embeddables'; -export interface PanelState { +export interface PanelState { // The type of embeddable in this panel. Will be used to find the factory in which to // load the embeddable. type: string; @@ -43,7 +43,7 @@ export interface ContainerOutput extends EmbeddableOutput { export interface ContainerInput extends EmbeddableInput { hidePanelTitles?: boolean; panels: { - [key: string]: PanelState; + [key: string]: PanelState; }; } diff --git a/src/plugins/embeddable/public/lib/embeddables/i_embeddable.ts b/src/plugins/embeddable/public/lib/embeddables/i_embeddable.ts index 9c4a1b5602c49..e8aecdba0abc4 100644 --- a/src/plugins/embeddable/public/lib/embeddables/i_embeddable.ts +++ b/src/plugins/embeddable/public/lib/embeddables/i_embeddable.ts @@ -70,8 +70,6 @@ export interface EmbeddableInput { * Visualization filters used to narrow down results. */ filters?: Filter[]; - - [key: string]: unknown; } export interface EmbeddableOutput { diff --git a/src/plugins/embeddable/public/tests/container.test.ts b/src/plugins/embeddable/public/tests/container.test.ts index 621ffe4c9dad6..69c21fdf3f072 100644 --- a/src/plugins/embeddable/public/tests/container.test.ts +++ b/src/plugins/embeddable/public/tests/container.test.ts @@ -19,7 +19,13 @@ import * as Rx from 'rxjs'; import { skip } from 'rxjs/operators'; -import { isErrorEmbeddable, EmbeddableOutput, ContainerInput, ViewMode } from '../lib'; +import { + isErrorEmbeddable, + EmbeddableOutput, + ContainerInput, + ViewMode, + SavedObjectEmbeddableInput, +} from '../lib'; import { FilterableEmbeddableInput, FilterableEmbeddable, @@ -648,7 +654,7 @@ test('container stores ErrorEmbeddables when a saved object cannot be found', as panels: { '123': { type: 'vis', - explicitInput: { id: '123', savedObjectId: '456' }, + explicitInput: { id: '123', savedObjectId: '456' } as SavedObjectEmbeddableInput, }, }, viewMode: ViewMode.EDIT, @@ -669,7 +675,7 @@ test('ErrorEmbeddables get updated when parent does', async (done) => { panels: { '123': { type: 'vis', - explicitInput: { id: '123', savedObjectId: '456' }, + explicitInput: { id: '123', savedObjectId: '456' } as SavedObjectEmbeddableInput, }, }, viewMode: ViewMode.EDIT, diff --git a/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts b/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts index 4efdfd2911cbc..cc278a6ee9b3d 100644 --- a/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts +++ b/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts @@ -42,7 +42,7 @@ import { ExpressionRenderError, } from '../../../../plugins/expressions/public'; import { buildPipeline } from '../legacy/build_pipeline'; -import { Vis } from '../vis'; +import { Vis, SerializedVis } from '../vis'; import { getExpressions, getUiActions } from '../services'; import { VIS_EVENT_TO_TRIGGER } from './events'; import { VisualizeEmbeddableFactoryDeps } from './visualize_embeddable_factory'; @@ -63,6 +63,7 @@ export interface VisualizeInput extends EmbeddableInput { vis?: { colors?: { [key: string]: string }; }; + savedVis?: SerializedVis; table?: unknown; } diff --git a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/drilldown.tsx b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/drilldown.tsx index a17d95c37c5ce..056feeb2b2167 100644 --- a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/drilldown.tsx +++ b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/drilldown.tsx @@ -25,6 +25,7 @@ import { import { StartServicesGetter } from '../../../../../../../src/plugins/kibana_utils/public'; import { StartDependencies } from '../../../plugin'; import { Config, FactoryContext } from './types'; +import { SearchInput } from '../../../../../../../src/plugins/discover/public'; export interface Params { start: StartServicesGetter>; @@ -89,7 +90,7 @@ export class DashboardToDashboardDrilldown }; if (context.embeddable) { - const input = context.embeddable.getInput(); + const input = context.embeddable.getInput() as Readonly; if (isQuery(input.query) && config.useCurrentFilters) state.query = input.query; // if useCurrentDashboardDataRange is enabled, then preserve current time range diff --git a/x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_chart_action.ts b/x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_chart_action.ts index 660739f26c70d..44ad3c57f7e24 100644 --- a/x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_chart_action.ts +++ b/x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_chart_action.ts @@ -5,7 +5,10 @@ */ import { Action } from '../../../../../../src/plugins/ui_actions/public'; -import { DiscoverUrlGeneratorState } from '../../../../../../src/plugins/discover/public'; +import { + DiscoverUrlGeneratorState, + SearchInput, +} from '../../../../../../src/plugins/discover/public'; import { ApplyGlobalFilterActionContext, esFilters, @@ -59,7 +62,7 @@ export class ExploreDataChartAction extends AbstractExploreDataAction; if (input.timeRange && !state.timeRange) state.timeRange = input.timeRange; if (input.query) state.query = input.query; From 6718f5494d8952f78706f9b402668145731572f2 Mon Sep 17 00:00:00 2001 From: Liza Katz Date: Tue, 25 Aug 2020 12:36:30 +0300 Subject: [PATCH 31/71] Don't overwrite sync strategy in xpack (#75556) * Don't override sync strategy in XPACK * search name * docs * mock * Use enhancement pattern Co-authored-by: Elastic Machine --- ...plugin-plugins-data-server.plugin.setup.md | 2 ++ src/plugins/data/common/search/index.ts | 3 --- src/plugins/data/server/plugin.ts | 23 +++++++++++++++---- src/plugins/data/server/search/index.ts | 8 ++++++- src/plugins/data/server/search/mocks.ts | 1 + .../data/server/search/search_service.ts | 16 ++++++++----- src/plugins/data/server/search/types.ts | 9 ++++++++ src/plugins/data/server/server.api.md | 8 +++++++ x-pack/plugins/data_enhanced/common/index.ts | 7 +++++- .../data_enhanced/common/search/index.ts | 7 +++++- .../data_enhanced/common/search/types.ts | 2 ++ .../public/search/search_interceptor.ts | 7 +++--- x-pack/plugins/data_enhanced/server/plugin.ts | 10 ++++++-- 13 files changed, 81 insertions(+), 22 deletions(-) diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.plugin.setup.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.plugin.setup.md index 18fca3d2c8a66..139c5794f0146 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.plugin.setup.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.plugin.setup.md @@ -8,6 +8,7 @@ ```typescript setup(core: CoreSetup, { expressions, usageCollection }: DataPluginSetupDependencies): { + __enhance: (enhancements: DataEnhancements) => void; search: ISearchSetup; fieldFormats: { register: (customFieldFormat: import("../public").FieldFormatInstanceType) => number; @@ -25,6 +26,7 @@ setup(core: CoreSetup, { expressio Returns: `{ + __enhance: (enhancements: DataEnhancements) => void; search: ISearchSetup; fieldFormats: { register: (customFieldFormat: import("../public").FieldFormatInstanceType) => number; diff --git a/src/plugins/data/common/search/index.ts b/src/plugins/data/common/search/index.ts index 557ab64079d16..d8184551b7f3d 100644 --- a/src/plugins/data/common/search/index.ts +++ b/src/plugins/data/common/search/index.ts @@ -23,9 +23,6 @@ export * from './expressions'; export * from './tabify'; export * from './types'; -import { ES_SEARCH_STRATEGY } from './es_search'; -export const DEFAULT_SEARCH_STRATEGY = ES_SEARCH_STRATEGY; - export { IEsSearchRequest, IEsSearchResponse, diff --git a/src/plugins/data/server/plugin.ts b/src/plugins/data/server/plugin.ts index 5163bfcb17d40..588885391262e 100644 --- a/src/plugins/data/server/plugin.ts +++ b/src/plugins/data/server/plugin.ts @@ -21,7 +21,7 @@ import { PluginInitializerContext, CoreSetup, CoreStart, Plugin, Logger } from ' import { ExpressionsServerSetup } from 'src/plugins/expressions/server'; import { ConfigSchema } from '../config'; import { IndexPatternsService, IndexPatternsServiceStart } from './index_patterns'; -import { ISearchSetup, ISearchStart } from './search'; +import { ISearchSetup, ISearchStart, SearchEnhancements } from './search'; import { SearchService } from './search/search_service'; import { QueryService } from './query/query_service'; import { ScriptsService } from './scripts'; @@ -31,9 +31,17 @@ import { AutocompleteService } from './autocomplete'; import { FieldFormatsService, FieldFormatsSetup, FieldFormatsStart } from './field_formats'; import { getUiSettings } from './ui_settings'; +export interface DataEnhancements { + search: SearchEnhancements; +} + export interface DataPluginSetup { search: ISearchSetup; fieldFormats: FieldFormatsSetup; + /** + * @internal + */ + __enhance: (enhancements: DataEnhancements) => void; } export interface DataPluginStart { @@ -87,11 +95,16 @@ export class DataServerPlugin core.uiSettings.register(getUiSettings()); + const searchSetup = this.searchService.setup(core, { + registerFunction: expressions.registerFunction, + usageCollection, + }); + return { - search: this.searchService.setup(core, { - registerFunction: expressions.registerFunction, - usageCollection, - }), + __enhance: (enhancements: DataEnhancements) => { + searchSetup.__enhance(enhancements.search); + }, + search: searchSetup, fieldFormats: this.fieldFormats.setup(), }; } diff --git a/src/plugins/data/server/search/index.ts b/src/plugins/data/server/search/index.ts index 4a3990621ca39..02c21c3254645 100644 --- a/src/plugins/data/server/search/index.ts +++ b/src/plugins/data/server/search/index.ts @@ -17,7 +17,13 @@ * under the License. */ -export { ISearchStrategy, ISearchOptions, ISearchSetup, ISearchStart } from './types'; +export { + ISearchStrategy, + ISearchOptions, + ISearchSetup, + ISearchStart, + SearchEnhancements, +} from './types'; export { getDefaultSearchParams, getTotalLoaded } from './es_search'; diff --git a/src/plugins/data/server/search/mocks.ts b/src/plugins/data/server/search/mocks.ts index 578a170f468bf..0c74ecb4b2c9d 100644 --- a/src/plugins/data/server/search/mocks.ts +++ b/src/plugins/data/server/search/mocks.ts @@ -24,6 +24,7 @@ export function createSearchSetupMock(): jest.Mocked { return { aggs: searchAggsSetupMock(), registerSearchStrategy: jest.fn(), + __enhance: jest.fn(), }; } diff --git a/src/plugins/data/server/search/search_service.ts b/src/plugins/data/server/search/search_service.ts index cc23c455bed26..edc94961c79d8 100644 --- a/src/plugins/data/server/search/search_service.ts +++ b/src/plugins/data/server/search/search_service.ts @@ -25,7 +25,7 @@ import { PluginInitializerContext, RequestHandlerContext, } from '../../../../core/server'; -import { ISearchSetup, ISearchStart, ISearchStrategy } from './types'; +import { ISearchSetup, ISearchStart, ISearchStrategy, SearchEnhancements } from './types'; import { AggsService, AggsSetupDependencies } from './aggs'; @@ -57,6 +57,7 @@ export interface SearchServiceStartDependencies { export class SearchService implements Plugin { private readonly aggsService = new AggsService(); + private defaultSearchStrategyName: string = ES_SEARCH_STRATEGY; private searchStrategies: StrategyMap = {}; constructor( @@ -87,6 +88,11 @@ export class SearchService implements Plugin { registerSearchRoute(core); return { + __enhance: (enhancements: SearchEnhancements) => { + if (this.searchStrategies.hasOwnProperty(enhancements.defaultStrategy)) { + this.defaultSearchStrategyName = enhancements.defaultStrategy; + } + }, aggs: this.aggsService.setup({ registerFunction }), registerSearchStrategy: this.registerSearchStrategy, usage, @@ -98,11 +104,9 @@ export class SearchService implements Plugin { searchRequest: IEsSearchRequest, options: Record ) { - return this.getSearchStrategy(options.strategy || ES_SEARCH_STRATEGY).search( - context, - searchRequest, - { signal: options.signal } - ); + return this.getSearchStrategy( + options.strategy || this.defaultSearchStrategyName + ).search(context, searchRequest, { signal: options.signal }); } public start( diff --git a/src/plugins/data/server/search/types.ts b/src/plugins/data/server/search/types.ts index 56f803512aa19..5ce1bb3e6b9f8 100644 --- a/src/plugins/data/server/search/types.ts +++ b/src/plugins/data/server/search/types.ts @@ -23,6 +23,10 @@ import { AggsSetup, AggsStart } from './aggs'; import { SearchUsage } from './collectors/usage'; import { IEsSearchRequest, IEsSearchResponse } from './es_search'; +export interface SearchEnhancements { + defaultStrategy: string; +} + export interface ISearchOptions { /** * An `AbortSignal` that allows the caller of `search` to abort a search request. @@ -49,6 +53,11 @@ export interface ISearchSetup { * Used internally for telemetry */ usage?: SearchUsage; + + /** + * @internal + */ + __enhance: (enhancements: SearchEnhancements) => void; } export interface ISearchStart< diff --git a/src/plugins/data/server/server.api.md b/src/plugins/data/server/server.api.md index f870030ae9562..9f114f2132009 100644 --- a/src/plugins/data/server/server.api.md +++ b/src/plugins/data/server/server.api.md @@ -685,6 +685,10 @@ export interface ISearchOptions { // // @public (undocumented) export interface ISearchSetup { + // Warning: (ae-forgotten-export) The symbol "SearchEnhancements" needs to be exported by the entry point index.d.ts + // + // @internal (undocumented) + __enhance: (enhancements: SearchEnhancements) => void; // Warning: (ae-forgotten-export) The symbol "AggsSetup" needs to be exported by the entry point index.d.ts // // (undocumented) @@ -855,6 +859,7 @@ export class Plugin implements Plugin_2); // (undocumented) setup(core: CoreSetup, { expressions, usageCollection }: DataPluginSetupDependencies): { + __enhance: (enhancements: DataEnhancements) => void; search: ISearchSetup; fieldFormats: { register: (customFieldFormat: import("../public").FieldFormatInstanceType) => number; @@ -883,6 +888,8 @@ export function plugin(initializerContext: PluginInitializerContext void; // Warning: (ae-forgotten-export) The symbol "FieldFormatsSetup" needs to be exported by the entry point index.d.ts // // (undocumented) @@ -1090,6 +1097,7 @@ export function usageProvider(core: CoreSetup_2): SearchUsage; // src/plugins/data/server/index.ts:240:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts // src/plugins/data/server/index.ts:244:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts // src/plugins/data/server/index.ts:247:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/plugin.ts:88:66 - (ae-forgotten-export) The symbol "DataEnhancements" needs to be exported by the entry point index.d.ts // (No @packageDocumentation comment for this package) diff --git a/x-pack/plugins/data_enhanced/common/index.ts b/x-pack/plugins/data_enhanced/common/index.ts index 1f1cd938c97d1..d6a3c73aaf363 100644 --- a/x-pack/plugins/data_enhanced/common/index.ts +++ b/x-pack/plugins/data_enhanced/common/index.ts @@ -4,4 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -export { EnhancedSearchParams, IEnhancedEsSearchRequest, IAsyncSearchRequest } from './search'; +export { + EnhancedSearchParams, + IEnhancedEsSearchRequest, + IAsyncSearchRequest, + ENHANCED_ES_SEARCH_STRATEGY, +} from './search'; diff --git a/x-pack/plugins/data_enhanced/common/search/index.ts b/x-pack/plugins/data_enhanced/common/search/index.ts index 129e412a47ccf..2ae422bd6b7d7 100644 --- a/x-pack/plugins/data_enhanced/common/search/index.ts +++ b/x-pack/plugins/data_enhanced/common/search/index.ts @@ -4,4 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -export { EnhancedSearchParams, IEnhancedEsSearchRequest, IAsyncSearchRequest } from './types'; +export { + EnhancedSearchParams, + IEnhancedEsSearchRequest, + IAsyncSearchRequest, + ENHANCED_ES_SEARCH_STRATEGY, +} from './types'; diff --git a/x-pack/plugins/data_enhanced/common/search/types.ts b/x-pack/plugins/data_enhanced/common/search/types.ts index a5d7d326cecd5..0d3d3a69e1e57 100644 --- a/x-pack/plugins/data_enhanced/common/search/types.ts +++ b/x-pack/plugins/data_enhanced/common/search/types.ts @@ -6,6 +6,8 @@ import { IEsSearchRequest, ISearchRequestParams } from '../../../../../src/plugins/data/common'; +export const ENHANCED_ES_SEARCH_STRATEGY = 'ese'; + export interface EnhancedSearchParams extends ISearchRequestParams { ignoreThrottled: boolean; } diff --git a/x-pack/plugins/data_enhanced/public/search/search_interceptor.ts b/x-pack/plugins/data_enhanced/public/search/search_interceptor.ts index 47099e32fcc72..6f7899d1188b4 100644 --- a/x-pack/plugins/data_enhanced/public/search/search_interceptor.ts +++ b/x-pack/plugins/data_enhanced/public/search/search_interceptor.ts @@ -14,7 +14,7 @@ import { } from '../../../../../src/plugins/data/public'; import { AbortError, toPromise } from '../../../../../src/plugins/data/common'; import { IAsyncSearchOptions } from '.'; -import { IAsyncSearchRequest } from '../../common'; +import { IAsyncSearchRequest, ENHANCED_ES_SEARCH_STRATEGY } from '../../common'; export class EnhancedSearchInterceptor extends SearchInterceptor { /** @@ -76,10 +76,11 @@ export class EnhancedSearchInterceptor extends SearchInterceptor { const { combinedSignal, cleanup } = this.setupTimers(options); const aborted$ = from(toPromise(combinedSignal)); + const strategy = options?.strategy || ENHANCED_ES_SEARCH_STRATEGY; this.pendingCount$.next(this.pendingCount$.getValue() + 1); - return this.runSearch(request, combinedSignal, options?.strategy).pipe( + return this.runSearch(request, combinedSignal, strategy).pipe( expand((response) => { // If the response indicates of an error, stop polling and complete the observable if (!response || (!response.isRunning && response.isPartial)) { @@ -96,7 +97,7 @@ export class EnhancedSearchInterceptor extends SearchInterceptor { return timer(pollInterval).pipe( // Send future requests using just the ID from the response mergeMap(() => { - return this.runSearch({ ...request, id }, combinedSignal, options?.strategy); + return this.runSearch({ ...request, id }, combinedSignal, strategy); }) ); }), diff --git a/x-pack/plugins/data_enhanced/server/plugin.ts b/x-pack/plugins/data_enhanced/server/plugin.ts index 0e9731a414119..f9b6fd4e9ad64 100644 --- a/x-pack/plugins/data_enhanced/server/plugin.ts +++ b/x-pack/plugins/data_enhanced/server/plugin.ts @@ -11,7 +11,6 @@ import { Plugin, Logger, } from '../../../../src/core/server'; -import { ES_SEARCH_STRATEGY } from '../../../../src/plugins/data/common'; import { PluginSetup as DataPluginSetup, PluginStart as DataPluginStart, @@ -19,6 +18,7 @@ import { } from '../../../../src/plugins/data/server'; import { enhancedEsSearchStrategyProvider } from './search'; import { UsageCollectionSetup } from '../../../../src/plugins/usage_collection/server'; +import { ENHANCED_ES_SEARCH_STRATEGY } from '../common'; interface SetupDependencies { data: DataPluginSetup; @@ -36,13 +36,19 @@ export class EnhancedDataServerPlugin implements Plugin Date: Tue, 25 Aug 2020 11:23:57 +0100 Subject: [PATCH 32/71] [Logs UI] Log alerts chart previews (#75296) * Add chart previews for log threshold alerts --- .../infra/common/alerting/logs/types.ts | 58 +++- .../infra/common/color_palette.test.ts | 30 +- x-pack/plugins/infra/common/color_palette.ts | 49 +-- x-pack/plugins/infra/common/http_api/index.ts | 1 + .../http_api/log_alerts/chart_preview_data.ts | 63 ++++ .../infra/common/http_api/log_alerts/index.ts | 7 + .../components/expression_chart.tsx | 16 +- .../logs/expression_editor/criteria.tsx | 35 +- .../criterion_preview_chart.tsx | 326 ++++++++++++++++++ .../logs/expression_editor/editor.tsx | 13 +- .../hooks/use_chart_preview_data.tsx | 80 +++++ .../criterion_preview_chart.tsx | 136 ++++++++ .../helpers/calculate_domian.test.ts | 6 +- .../components/helpers/create_tsvb_link.ts | 6 +- .../metrics_explorer/components/metrics.tsx | 6 +- .../components/series_chart.tsx | 10 +- .../hooks/use_metrics_explorer_options.ts | 15 +- x-pack/plugins/infra/server/infra_server.ts | 2 + .../log_threshold_chart_preview.ts | 186 ++++++++++ .../log_threshold/log_threshold_executor.ts | 13 +- .../routes/log_alerts/chart_preview_data.ts | 64 ++++ .../infra/server/routes/log_alerts/index.ts | 7 + 22 files changed, 1017 insertions(+), 112 deletions(-) create mode 100644 x-pack/plugins/infra/common/http_api/log_alerts/chart_preview_data.ts create mode 100644 x-pack/plugins/infra/common/http_api/log_alerts/index.ts create mode 100644 x-pack/plugins/infra/public/components/alerting/logs/expression_editor/criterion_preview_chart.tsx create mode 100644 x-pack/plugins/infra/public/components/alerting/logs/expression_editor/hooks/use_chart_preview_data.tsx create mode 100644 x-pack/plugins/infra/public/components/alerting/shared/criterion_preview_chart/criterion_preview_chart.tsx create mode 100644 x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_chart_preview.ts create mode 100644 x-pack/plugins/infra/server/routes/log_alerts/chart_preview_data.ts create mode 100644 x-pack/plugins/infra/server/routes/log_alerts/index.ts diff --git a/x-pack/plugins/infra/common/alerting/logs/types.ts b/x-pack/plugins/infra/common/alerting/logs/types.ts index 884a813d74c86..1b736f52aa7e2 100644 --- a/x-pack/plugins/infra/common/alerting/logs/types.ts +++ b/x-pack/plugins/infra/common/alerting/logs/types.ts @@ -96,40 +96,64 @@ const DocumentCountRT = rt.type({ export type DocumentCount = rt.TypeOf; -const CriterionRT = rt.type({ +export const CriterionRT = rt.type({ field: rt.string, comparator: ComparatorRT, value: rt.union([rt.string, rt.number]), }); export type Criterion = rt.TypeOf; +export const criteriaRT = rt.array(CriterionRT); -const TimeUnitRT = rt.union([rt.literal('s'), rt.literal('m'), rt.literal('h'), rt.literal('d')]); +export const TimeUnitRT = rt.union([ + rt.literal('s'), + rt.literal('m'), + rt.literal('h'), + rt.literal('d'), +]); export type TimeUnit = rt.TypeOf; +export const timeSizeRT = rt.number; +export const groupByRT = rt.array(rt.string); + export const LogDocumentCountAlertParamsRT = rt.intersection([ rt.type({ count: DocumentCountRT, - criteria: rt.array(CriterionRT), + criteria: criteriaRT, timeUnit: TimeUnitRT, - timeSize: rt.number, + timeSize: timeSizeRT, }), rt.partial({ - groupBy: rt.array(rt.string), + groupBy: groupByRT, }), ]); export type LogDocumentCountAlertParams = rt.TypeOf; +const chartPreviewHistogramBucket = rt.type({ + key: rt.number, + doc_count: rt.number, +}); + export const UngroupedSearchQueryResponseRT = rt.intersection([ commonSearchSuccessResponseFieldsRT, - rt.type({ - hits: rt.type({ - total: rt.type({ - value: rt.number, + rt.intersection([ + rt.type({ + hits: rt.type({ + total: rt.type({ + value: rt.number, + }), }), }), - }), + // Chart preview buckets + rt.partial({ + aggregations: rt.type({ + histogramBuckets: rt.type({ + buckets: rt.array(chartPreviewHistogramBucket), + }), + }), + }), + ]), ]); export type UngroupedSearchQueryResponse = rt.TypeOf; @@ -144,9 +168,17 @@ export const GroupedSearchQueryResponseRT = rt.intersection([ rt.type({ key: rt.record(rt.string, rt.string), doc_count: rt.number, - filtered_results: rt.type({ - doc_count: rt.number, - }), + filtered_results: rt.intersection([ + rt.type({ + doc_count: rt.number, + }), + // Chart preview buckets + rt.partial({ + histogramBuckets: rt.type({ + buckets: rt.array(chartPreviewHistogramBucket), + }), + }), + ]), }) ), }), diff --git a/x-pack/plugins/infra/common/color_palette.test.ts b/x-pack/plugins/infra/common/color_palette.test.ts index ced45c39c710c..1e814d6f67fec 100644 --- a/x-pack/plugins/infra/common/color_palette.test.ts +++ b/x-pack/plugins/infra/common/color_palette.test.ts @@ -4,35 +4,35 @@ * you may not use this file except in compliance with the Elastic License. */ -import { sampleColor, MetricsExplorerColor, colorTransformer } from './color_palette'; +import { sampleColor, Color, colorTransformer } from './color_palette'; describe('Color Palette', () => { describe('sampleColor()', () => { it('should just work', () => { - const usedColors = [MetricsExplorerColor.color0]; + const usedColors = [Color.color0]; const color = sampleColor(usedColors); - expect(color).toBe(MetricsExplorerColor.color1); + expect(color).toBe(Color.color1); }); it('should return color0 when nothing is available', () => { const usedColors = [ - MetricsExplorerColor.color0, - MetricsExplorerColor.color1, - MetricsExplorerColor.color2, - MetricsExplorerColor.color3, - MetricsExplorerColor.color4, - MetricsExplorerColor.color5, - MetricsExplorerColor.color6, - MetricsExplorerColor.color7, - MetricsExplorerColor.color8, - MetricsExplorerColor.color9, + Color.color0, + Color.color1, + Color.color2, + Color.color3, + Color.color4, + Color.color5, + Color.color6, + Color.color7, + Color.color8, + Color.color9, ]; const color = sampleColor(usedColors); - expect(color).toBe(MetricsExplorerColor.color0); + expect(color).toBe(Color.color0); }); }); describe('colorTransformer()', () => { it('should just work', () => { - expect(colorTransformer(MetricsExplorerColor.color0)).toBe('#6092C0'); + expect(colorTransformer(Color.color0)).toBe('#6092C0'); }); }); }); diff --git a/x-pack/plugins/infra/common/color_palette.ts b/x-pack/plugins/infra/common/color_palette.ts index 51962150d8424..2b72b3f0c1dfa 100644 --- a/x-pack/plugins/infra/common/color_palette.ts +++ b/x-pack/plugins/infra/common/color_palette.ts @@ -6,7 +6,7 @@ import { difference, first, values } from 'lodash'; import { euiPaletteColorBlind } from '@elastic/eui'; -export enum MetricsExplorerColor { +export enum Color { color0 = 'color0', color1 = 'color1', color2 = 'color2', @@ -19,41 +19,30 @@ export enum MetricsExplorerColor { color9 = 'color9', } -export interface MetricsExplorerPalette { - [MetricsExplorerColor.color0]: string; - [MetricsExplorerColor.color1]: string; - [MetricsExplorerColor.color2]: string; - [MetricsExplorerColor.color3]: string; - [MetricsExplorerColor.color4]: string; - [MetricsExplorerColor.color5]: string; - [MetricsExplorerColor.color6]: string; - [MetricsExplorerColor.color7]: string; - [MetricsExplorerColor.color8]: string; - [MetricsExplorerColor.color9]: string; -} +export type Palette = { + [K in keyof typeof Color]: string; +}; const euiPalette = euiPaletteColorBlind(); -export const defaultPalette: MetricsExplorerPalette = { - [MetricsExplorerColor.color0]: euiPalette[1], // (blue) - [MetricsExplorerColor.color1]: euiPalette[2], // (pink) - [MetricsExplorerColor.color2]: euiPalette[0], // (green-ish) - [MetricsExplorerColor.color3]: euiPalette[3], // (purple) - [MetricsExplorerColor.color4]: euiPalette[4], // (light pink) - [MetricsExplorerColor.color5]: euiPalette[5], // (yellow) - [MetricsExplorerColor.color6]: euiPalette[6], // (tan) - [MetricsExplorerColor.color7]: euiPalette[7], // (orange) - [MetricsExplorerColor.color8]: euiPalette[8], // (brown) - [MetricsExplorerColor.color9]: euiPalette[9], // (red) +export const defaultPalette: Palette = { + [Color.color0]: euiPalette[1], // (blue) + [Color.color1]: euiPalette[2], // (pink) + [Color.color2]: euiPalette[0], // (green-ish) + [Color.color3]: euiPalette[3], // (purple) + [Color.color4]: euiPalette[4], // (light pink) + [Color.color5]: euiPalette[5], // (yellow) + [Color.color6]: euiPalette[6], // (tan) + [Color.color7]: euiPalette[7], // (orange) + [Color.color8]: euiPalette[8], // (brown) + [Color.color9]: euiPalette[9], // (red) }; -export const createPaletteTransformer = (palette: MetricsExplorerPalette) => ( - color: MetricsExplorerColor -) => palette[color]; +export const createPaletteTransformer = (palette: Palette) => (color: Color) => palette[color]; export const colorTransformer = createPaletteTransformer(defaultPalette); -export const sampleColor = (usedColors: MetricsExplorerColor[] = []): MetricsExplorerColor => { - const available = difference(values(MetricsExplorerColor) as MetricsExplorerColor[], usedColors); - return first(available) || MetricsExplorerColor.color0; +export const sampleColor = (usedColors: Color[] = []): Color => { + const available = difference(values(Color) as Color[], usedColors); + return first(available) || Color.color0; }; diff --git a/x-pack/plugins/infra/common/http_api/index.ts b/x-pack/plugins/infra/common/http_api/index.ts index 9ec8bf5231066..818009417fb1c 100644 --- a/x-pack/plugins/infra/common/http_api/index.ts +++ b/x-pack/plugins/infra/common/http_api/index.ts @@ -9,3 +9,4 @@ export * from './metadata_api'; export * from './log_entries'; export * from './metrics_explorer'; export * from './metrics_api'; +export * from './log_alerts'; diff --git a/x-pack/plugins/infra/common/http_api/log_alerts/chart_preview_data.ts b/x-pack/plugins/infra/common/http_api/log_alerts/chart_preview_data.ts new file mode 100644 index 0000000000000..15914bd1b2209 --- /dev/null +++ b/x-pack/plugins/infra/common/http_api/log_alerts/chart_preview_data.ts @@ -0,0 +1,63 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import * as rt from 'io-ts'; +import { criteriaRT, TimeUnitRT, timeSizeRT, groupByRT } from '../../alerting/logs/types'; + +export const LOG_ALERTS_CHART_PREVIEW_DATA_PATH = '/api/infra/log_alerts/chart_preview_data'; + +const pointRT = rt.type({ + timestamp: rt.number, + value: rt.number, +}); + +export type Point = rt.TypeOf; + +const serieRT = rt.type({ + id: rt.string, + points: rt.array(pointRT), +}); + +const seriesRT = rt.array(serieRT); + +export type Series = rt.TypeOf; + +export const getLogAlertsChartPreviewDataSuccessResponsePayloadRT = rt.type({ + data: rt.type({ + series: seriesRT, + }), +}); + +export type GetLogAlertsChartPreviewDataSuccessResponsePayload = rt.TypeOf< + typeof getLogAlertsChartPreviewDataSuccessResponsePayloadRT +>; + +export const getLogAlertsChartPreviewDataAlertParamsSubsetRT = rt.intersection([ + rt.type({ + criteria: criteriaRT, + timeUnit: TimeUnitRT, + timeSize: timeSizeRT, + }), + rt.partial({ + groupBy: groupByRT, + }), +]); + +export type GetLogAlertsChartPreviewDataAlertParamsSubset = rt.TypeOf< + typeof getLogAlertsChartPreviewDataAlertParamsSubsetRT +>; + +export const getLogAlertsChartPreviewDataRequestPayloadRT = rt.type({ + data: rt.type({ + sourceId: rt.string, + alertParams: getLogAlertsChartPreviewDataAlertParamsSubsetRT, + buckets: rt.number, + }), +}); + +export type GetLogAlertsChartPreviewDataRequestPayload = rt.TypeOf< + typeof getLogAlertsChartPreviewDataRequestPayloadRT +>; diff --git a/x-pack/plugins/infra/common/http_api/log_alerts/index.ts b/x-pack/plugins/infra/common/http_api/log_alerts/index.ts new file mode 100644 index 0000000000000..5634fda043a52 --- /dev/null +++ b/x-pack/plugins/infra/common/http_api/log_alerts/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export * from './chart_preview_data'; diff --git a/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression_chart.tsx b/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression_chart.tsx index c90c534193fdc..94ad074b72e9c 100644 --- a/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression_chart.tsx +++ b/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression_chart.tsx @@ -28,7 +28,7 @@ import { Comparator, // eslint-disable-next-line @kbn/eslint/no-restricted-paths } from '../../../../server/lib/alerting/metric_threshold/types'; -import { MetricsExplorerColor, colorTransformer } from '../../../../common/color_palette'; +import { Color, colorTransformer } from '../../../../common/color_palette'; import { MetricsExplorerRow, MetricsExplorerAggregation } from '../../../../common/http_api'; import { MetricExplorerSeriesChart } from '../../../pages/metrics/metrics_explorer/components/series_chart'; import { MetricExpression, AlertContextMeta } from '../types'; @@ -80,7 +80,7 @@ export const ExpressionChart: React.FC = ({ const metric = { field: expression.metric, aggregation: expression.aggType as MetricsExplorerAggregation, - color: MetricsExplorerColor.color0, + color: Color.color0, }; const isDarkMode = context.uiSettings?.get('theme:darkMode') || false; const dateFormatter = useMemo(() => { @@ -176,7 +176,7 @@ export const ExpressionChart: React.FC = ({ style={{ line: { strokeWidth: 2, - stroke: colorTransformer(MetricsExplorerColor.color1), + stroke: colorTransformer(Color.color1), opacity: 1, }, }} @@ -186,7 +186,7 @@ export const ExpressionChart: React.FC = ({ = ({ = ({ = ({ = ({ ) => void; removeCriterion: (idx: number) => void; errors: IErrorObject; + alertParams: Partial; + context: AlertsContext; + sourceId: string; } export const Criteria: React.FC = ({ @@ -29,6 +34,9 @@ export const Criteria: React.FC = ({ updateCriterion, removeCriterion, errors, + alertParams, + context, + sourceId, }) => { if (!criteria) return null; return ( @@ -36,16 +44,23 @@ export const Criteria: React.FC = ({ {criteria.map((criterion, idx) => { return ( - 1} - errors={errors[idx.toString()] as IErrorObject} - /> + + 1} + errors={errors[idx.toString()] as IErrorObject} + /> + + ); })} diff --git a/x-pack/plugins/infra/public/components/alerting/logs/expression_editor/criterion_preview_chart.tsx b/x-pack/plugins/infra/public/components/alerting/logs/expression_editor/criterion_preview_chart.tsx new file mode 100644 index 0000000000000..31f9a64015c07 --- /dev/null +++ b/x-pack/plugins/infra/public/components/alerting/logs/expression_editor/criterion_preview_chart.tsx @@ -0,0 +1,326 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useMemo } from 'react'; +import { useDebounce } from 'react-use'; +import { + ScaleType, + AnnotationDomainTypes, + Position, + Axis, + BarSeries, + Chart, + Settings, + RectAnnotation, + LineAnnotation, +} from '@elastic/charts'; +import { EuiText } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { + ChartContainer, + LoadingState, + NoDataState, + ErrorState, + TIME_LABELS, + getDomain, + tooltipProps, + useDateFormatter, + getChartTheme, + yAxisFormatter, + NUM_BUCKETS, +} from '../../shared/criterion_preview_chart/criterion_preview_chart'; +import { + LogDocumentCountAlertParams, + Criterion, + Comparator, +} from '../../../../../common/alerting/logs/types'; +import { Color, colorTransformer } from '../../../../../common/color_palette'; +import { + GetLogAlertsChartPreviewDataAlertParamsSubset, + getLogAlertsChartPreviewDataAlertParamsSubsetRT, +} from '../../../../../common/http_api/log_alerts/'; +import { AlertsContext } from './editor'; +import { useChartPreviewData } from './hooks/use_chart_preview_data'; +import { decodeOrThrow } from '../../../../../common/runtime_types'; + +const GROUP_LIMIT = 5; + +interface Props { + alertParams: Partial; + context: AlertsContext; + chartCriterion: Partial; + sourceId: string; +} + +export const CriterionPreview: React.FC = ({ + alertParams, + context, + chartCriterion, + sourceId, +}) => { + const chartAlertParams: GetLogAlertsChartPreviewDataAlertParamsSubset | null = useMemo(() => { + const { field, comparator, value } = chartCriterion; + const criteria = field && comparator && value ? [{ field, comparator, value }] : []; + const params = { + criteria, + timeSize: alertParams.timeSize, + timeUnit: alertParams.timeUnit, + groupBy: alertParams.groupBy, + }; + + try { + return decodeOrThrow(getLogAlertsChartPreviewDataAlertParamsSubsetRT)(params); + } catch (error) { + return null; + } + }, [alertParams.timeSize, alertParams.timeUnit, alertParams.groupBy, chartCriterion]); + + // Check for the existence of properties that are necessary for a meaningful chart. + if (chartAlertParams === null || chartAlertParams.criteria.length === 0) return null; + + return ( + + ); +}; + +interface ChartProps { + buckets: number; + context: AlertsContext; + sourceId: string; + threshold?: LogDocumentCountAlertParams['count']; + chartAlertParams: GetLogAlertsChartPreviewDataAlertParamsSubset; +} + +const CriterionPreviewChart: React.FC = ({ + buckets, + context, + sourceId, + threshold, + chartAlertParams, +}) => { + const isDarkMode = context.uiSettings?.get('theme:darkMode') || false; + + const { + getChartPreviewData, + isLoading, + hasError, + chartPreviewData: series, + } = useChartPreviewData({ + context, + sourceId, + alertParams: chartAlertParams, + buckets, + }); + + useDebounce( + () => { + getChartPreviewData(); + }, + 500, + [getChartPreviewData] + ); + + const isStacked = false; + + const { timeSize, timeUnit, groupBy } = chartAlertParams; + + const isGrouped = groupBy && groupBy.length > 0 ? true : false; + + const isAbove = + threshold && threshold.comparator + ? [Comparator.GT, Comparator.GT_OR_EQ].includes(threshold.comparator) + : false; + + const isBelow = + threshold && threshold.comparator + ? [Comparator.LT, Comparator.LT_OR_EQ].includes(threshold.comparator) + : false; + + // For grouped scenarios we want to limit the groups displayed, for "isAbove" thresholds we'll show + // groups with the highest doc counts. And for "isBelow" thresholds we'll show groups with the lowest doc counts. + const filteredSeries = useMemo(() => { + if (!isGrouped) { + return series; + } + + const sortedByMax = series.sort((a, b) => { + const aMax = Math.max(...a.points.map((point) => point.value)); + const bMax = Math.max(...b.points.map((point) => point.value)); + return bMax - aMax; + }); + const sortedSeries = (!isAbove && !isBelow) || isAbove ? sortedByMax : sortedByMax.reverse(); + return sortedSeries.slice(0, GROUP_LIMIT); + }, [series, isGrouped, isAbove, isBelow]); + + const barSeries = useMemo(() => { + return filteredSeries.reduce>( + (acc, serie) => { + const barPoints = serie.points.reduce< + Array<{ timestamp: number; value: number; groupBy: string }> + >((pointAcc, point) => { + return [...pointAcc, { ...point, groupBy: serie.id }]; + }, []); + return [...acc, ...barPoints]; + }, + [] + ); + }, [filteredSeries]); + + const lookback = timeSize * buckets; + const hasData = series.length > 0; + const { yMin, yMax, xMin, xMax } = getDomain(filteredSeries, isStacked); + const chartDomain = { + max: threshold && threshold.value ? Math.max(yMax, threshold.value) * 1.1 : yMax * 1.1, // Add 10% headroom. + min: threshold && threshold.value ? Math.min(yMin, threshold.value) : yMin, + }; + + if (threshold && threshold.value && chartDomain.min === threshold.value) { + chartDomain.min = chartDomain.min * 0.9; // Allow some padding so the threshold annotation has better visibility + } + + const THRESHOLD_OPACITY = 0.3; + const groupByLabel = groupBy && groupBy.length > 0 ? groupBy.join(', ') : null; + const dateFormatter = useDateFormatter(xMin, xMax); + const timeLabel = TIME_LABELS[timeUnit as keyof typeof TIME_LABELS]; + + if (isLoading) { + return ; + } else if (hasError) { + return ; + } else if (!hasData) { + return ; + } + + return ( + <> + + + + {threshold && threshold.value ? ( + + ) : null} + {threshold && threshold.value && isBelow ? ( + + ) : null} + {threshold && threshold.value && isAbove ? ( + + ) : null} + + + + + +
+ {groupByLabel != null ? ( + + + + ) : ( + + + + )} +
+ + ); +}; diff --git a/x-pack/plugins/infra/public/components/alerting/logs/expression_editor/editor.tsx b/x-pack/plugins/infra/public/components/alerting/logs/expression_editor/editor.tsx index 295e60552cce5..e063b880ab843 100644 --- a/x-pack/plugins/infra/public/components/alerting/logs/expression_editor/editor.tsx +++ b/x-pack/plugins/infra/public/components/alerting/logs/expression_editor/editor.tsx @@ -34,12 +34,14 @@ interface LogsContextMeta { isInternal?: boolean; } +export type AlertsContext = AlertsContextValue; interface Props { errors: IErrorObject; alertParams: Partial; setAlertParams(key: string, value: any): void; setAlertProperty(key: string, value: any): void; - alertsContext: AlertsContextValue; + alertsContext: AlertsContext; + sourceId: string; } const DEFAULT_CRITERIA = { field: 'log.level', comparator: Comparator.EQ, value: 'error' }; @@ -62,12 +64,12 @@ export const ExpressionEditor: React.FC = (props) => { <> {isInternal ? ( - + ) : ( - + )} @@ -119,7 +121,7 @@ export const SourceStatusWrapper: React.FC = (props) => { }; export const Editor: React.FC = (props) => { - const { setAlertParams, alertParams, errors } = props; + const { setAlertParams, alertParams, errors, alertsContext, sourceId } = props; const [hasSetDefaults, setHasSetDefaults] = useState(false); const { sourceStatus } = useLogSourceContext(); useMount(() => { @@ -227,6 +229,9 @@ export const Editor: React.FC = (props) => { updateCriterion={updateCriterion} removeCriterion={removeCriterion} errors={errors.criteria as IErrorObject} + alertParams={alertParams} + context={alertsContext} + sourceId={sourceId} /> { + const [chartPreviewData, setChartPreviewData] = useState< + GetLogAlertsChartPreviewDataSuccessResponsePayload['data']['series'] + >([]); + const [hasError, setHasError] = useState(false); + const [getChartPreviewDataRequest, getChartPreviewData] = useTrackedPromise( + { + cancelPreviousOn: 'creation', + createPromise: async () => { + setHasError(false); + return await callGetChartPreviewDataAPI(sourceId, context.http.fetch, alertParams, buckets); + }, + onResolve: ({ data: { series } }) => { + setHasError(false); + setChartPreviewData(series); + }, + onReject: (error) => { + setHasError(true); + }, + }, + [sourceId, context.http.fetch, alertParams, buckets] + ); + + const isLoading = useMemo(() => getChartPreviewDataRequest.state === 'pending', [ + getChartPreviewDataRequest.state, + ]); + + return { + chartPreviewData, + hasError, + isLoading, + getChartPreviewData, + }; +}; + +export const callGetChartPreviewDataAPI = async ( + sourceId: string, + fetch: AlertsContext['http']['fetch'], + alertParams: GetLogAlertsChartPreviewDataAlertParamsSubset, + buckets: number +) => { + const response = await fetch(LOG_ALERTS_CHART_PREVIEW_DATA_PATH, { + method: 'POST', + body: JSON.stringify( + getLogAlertsChartPreviewDataRequestPayloadRT.encode({ + data: { + sourceId, + alertParams, + buckets, + }, + }) + ), + }); + + return decodeOrThrow(getLogAlertsChartPreviewDataSuccessResponsePayloadRT)(response); +}; diff --git a/x-pack/plugins/infra/public/components/alerting/shared/criterion_preview_chart/criterion_preview_chart.tsx b/x-pack/plugins/infra/public/components/alerting/shared/criterion_preview_chart/criterion_preview_chart.tsx new file mode 100644 index 0000000000000..239afd93a7a1f --- /dev/null +++ b/x-pack/plugins/infra/public/components/alerting/shared/criterion_preview_chart/criterion_preview_chart.tsx @@ -0,0 +1,136 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React, { useMemo } from 'react'; +import { niceTimeFormatter, TooltipValue } from '@elastic/charts'; +import { Theme, LIGHT_THEME, DARK_THEME } from '@elastic/charts'; +import { sum, min as getMin, max as getMax } from 'lodash'; +import moment from 'moment'; +import { i18n } from '@kbn/i18n'; +import { EuiText } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { formatNumber } from '../../../../../common/formatters/number'; +import { GetLogAlertsChartPreviewDataSuccessResponsePayload } from '../../../../../common/http_api'; + +type Series = GetLogAlertsChartPreviewDataSuccessResponsePayload['data']['series']; + +export const tooltipProps = { + headerFormatter: (tooltipValue: TooltipValue) => + moment(tooltipValue.value).format('Y-MM-DD HH:mm:ss'), +}; + +export const NUM_BUCKETS = 20; + +export const TIME_LABELS = { + s: i18n.translate('xpack.infra.alerts.timeLabels.seconds', { defaultMessage: 'seconds' }), + m: i18n.translate('xpack.infra.alerts.timeLabels.minutes', { defaultMessage: 'minutes' }), + h: i18n.translate('xpack.infra.alerts.timeLabels.hours', { defaultMessage: 'hours' }), + d: i18n.translate('xpack.infra.alerts.timeLabels.days', { defaultMessage: 'days' }), +}; + +export const useDateFormatter = (xMin?: number, xMax?: number) => { + const dateFormatter = useMemo(() => { + if (typeof xMin === 'number' && typeof xMax === 'number') { + return niceTimeFormatter([xMin, xMax]); + } else { + return (value: number) => `${value}`; + } + }, [xMin, xMax]); + return dateFormatter; +}; + +export const yAxisFormatter = formatNumber; + +export const getDomain = (series: Series, stacked: boolean = false) => { + let min: number | null = null; + let max: number | null = null; + const valuesByTimestamp = series.reduce<{ [timestamp: number]: number[] }>((acc, serie) => { + serie.points.forEach((point) => { + const valuesForTimestamp = acc[point.timestamp] || []; + acc[point.timestamp] = [...valuesForTimestamp, point.value]; + }); + return acc; + }, {}); + const pointValues = Object.values(valuesByTimestamp); + pointValues.forEach((results) => { + const maxResult = stacked ? sum(results) : getMax(results); + const minResult = getMin(results); + if (maxResult && (!max || maxResult > max)) { + max = maxResult; + } + if (minResult && (!min || minResult < min)) { + min = minResult; + } + }); + const timestampValues = Object.keys(valuesByTimestamp).map(Number); + const minTimestamp = getMin(timestampValues) || 0; + const maxTimestamp = getMax(timestampValues) || 0; + return { yMin: min || 0, yMax: max || 0, xMin: minTimestamp, xMax: maxTimestamp }; +}; + +export const getChartTheme = (isDarkMode: boolean): Theme => { + return isDarkMode ? DARK_THEME : LIGHT_THEME; +}; + +export const EmptyContainer: React.FC = ({ children }) => ( +
+ {children} +
+); + +export const ChartContainer: React.FC = ({ children }) => ( +
+ {children} +
+); + +export const NoDataState = () => { + return ( + + + + + + ); +}; + +export const LoadingState = () => { + return ( + + + + + + ); +}; + +export const ErrorState = () => { + return ( + + + + + + ); +}; diff --git a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/helpers/calculate_domian.test.ts b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/helpers/calculate_domian.test.ts index f94c6b6156ae4..d706d598058bd 100644 --- a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/helpers/calculate_domian.test.ts +++ b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/helpers/calculate_domian.test.ts @@ -7,7 +7,7 @@ import { calculateDomain } from './calculate_domain'; import { MetricsExplorerSeries } from '../../../../../../common/http_api/metrics_explorer'; import { MetricsExplorerOptionsMetric } from '../../hooks/use_metrics_explorer_options'; -import { MetricsExplorerColor } from '../../../../../../common/color_palette'; +import { Color } from '../../../../../../common/color_palette'; describe('calculateDomain()', () => { const series: MetricsExplorerSeries = { id: 'test-01', @@ -29,12 +29,12 @@ describe('calculateDomain()', () => { { aggregation: 'avg', field: 'system.memory.free', - color: MetricsExplorerColor.color0, + color: Color.color0, }, { aggregation: 'avg', field: 'system.memory.used.bytes', - color: MetricsExplorerColor.color1, + color: Color.color1, }, ]; it('should return the min and max across 2 metrics', () => { diff --git a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/helpers/create_tsvb_link.ts b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/helpers/create_tsvb_link.ts index afddaf6621f10..15ed28c095199 100644 --- a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/helpers/create_tsvb_link.ts +++ b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/helpers/create_tsvb_link.ts @@ -7,7 +7,7 @@ import { encode } from 'rison-node'; import uuid from 'uuid'; import { set } from '@elastic/safer-lodash-set'; -import { colorTransformer, MetricsExplorerColor } from '../../../../../../common/color_palette'; +import { colorTransformer, Color } from '../../../../../../common/color_palette'; import { MetricsExplorerSeries } from '../../../../../../common/http_api/metrics_explorer'; import { MetricsExplorerOptions, @@ -91,9 +91,7 @@ const mapMetricToSeries = (chartOptions: MetricsExplorerChartOptions) => ( label: createMetricLabel(metric), axis_position: 'right', chart_type: 'line', - color: - (metric.color && colorTransformer(metric.color)) || - colorTransformer(MetricsExplorerColor.color0), + color: (metric.color && colorTransformer(metric.color)) || colorTransformer(Color.color0), fill: chartOptions.type === MetricsExplorerChartType.area ? 0.5 : 0, formatter: format === InfraFormatterType.bits ? InfraFormatterType.bytes : format, value_template: 'rate' === metric.aggregation ? '{{value}}/s' : '{{value}}', diff --git a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/metrics.tsx b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/metrics.tsx index 8be03a7096f08..b81a905b4aa87 100644 --- a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/metrics.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/metrics.tsx @@ -9,7 +9,7 @@ import { i18n } from '@kbn/i18n'; import React, { useCallback, useState } from 'react'; import { IFieldType } from 'src/plugins/data/public'; -import { colorTransformer, MetricsExplorerColor } from '../../../../../common/color_palette'; +import { colorTransformer, Color } from '../../../../../common/color_palette'; import { MetricsExplorerMetric } from '../../../../../common/http_api/metrics_explorer'; import { MetricsExplorerOptions } from '../hooks/use_metrics_explorer_options'; @@ -26,7 +26,7 @@ interface SelectedOption { } export const MetricsExplorerMetrics = ({ options, onChange, fields, autoFocus = false }: Props) => { - const colors = Object.keys(MetricsExplorerColor) as MetricsExplorerColor[]; + const colors = Object.keys(Color) as Array; const [shouldFocus, setShouldFocus] = useState(autoFocus); // the EuiCombobox forwards the ref to an input element @@ -59,7 +59,7 @@ export const MetricsExplorerMetrics = ({ options, onChange, fields, autoFocus = .map((metric) => ({ label: metric.field || '', value: metric.field || '', - color: colorTransformer(metric.color || MetricsExplorerColor.color0), + color: colorTransformer(metric.color || Color.color0), })); const placeholderText = i18n.translate('xpack.infra.metricsExplorer.metricComboBoxPlaceholder', { diff --git a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/series_chart.tsx b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/series_chart.tsx index 9b594ef5e630f..a621dca1e0c51 100644 --- a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/series_chart.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/series_chart.tsx @@ -14,7 +14,7 @@ import { BarSeriesStyle, } from '@elastic/charts'; import { MetricsExplorerSeries } from '../../../../../common/http_api/metrics_explorer'; -import { colorTransformer, MetricsExplorerColor } from '../../../../../common/color_palette'; +import { colorTransformer, Color } from '../../../../../common/color_palette'; import { createMetricLabel } from './helpers/create_metric_label'; import { MetricsExplorerOptionsMetric, @@ -41,9 +41,7 @@ export const MetricExplorerSeriesChart = (props: Props) => { }; export const MetricsExplorerAreaChart = ({ metric, id, series, type, stack, opacity }: Props) => { - const color = - (metric.color && colorTransformer(metric.color)) || - colorTransformer(MetricsExplorerColor.color0); + const color = (metric.color && colorTransformer(metric.color)) || colorTransformer(Color.color0); const yAccessors = Array.isArray(id) ? id.map((i) => getMetricId(metric, i)).slice(id.length - 1, id.length) @@ -84,9 +82,7 @@ export const MetricsExplorerAreaChart = ({ metric, id, series, type, stack, opac }; export const MetricsExplorerBarChart = ({ metric, id, series, stack }: Props) => { - const color = - (metric.color && colorTransformer(metric.color)) || - colorTransformer(MetricsExplorerColor.color0); + const color = (metric.color && colorTransformer(metric.color)) || colorTransformer(Color.color0); const yAccessors = Array.isArray(id) ? id.map((i) => getMetricId(metric, i)).slice(id.length - 1, id.length) diff --git a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/hooks/use_metrics_explorer_options.ts b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/hooks/use_metrics_explorer_options.ts index 299231f1821f0..d54cb758188c6 100644 --- a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/hooks/use_metrics_explorer_options.ts +++ b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/hooks/use_metrics_explorer_options.ts @@ -9,19 +9,14 @@ import { values } from 'lodash'; import createContainer from 'constate'; import { useState, useEffect, useMemo, Dispatch, SetStateAction } from 'react'; import { useAlertPrefillContext } from '../../../../alerting/use_alert_prefill'; -import { MetricsExplorerColor } from '../../../../../common/color_palette'; +import { Color } from '../../../../../common/color_palette'; import { metricsExplorerMetricRT } from '../../../../../common/http_api/metrics_explorer'; const metricsExplorerOptionsMetricRT = t.intersection([ metricsExplorerMetricRT, t.partial({ rate: t.boolean, - color: t.keyof( - Object.fromEntries(values(MetricsExplorerColor).map((c) => [c, null])) as Record< - MetricsExplorerColor, - null - > - ), + color: t.keyof(Object.fromEntries(values(Color).map((c) => [c, null])) as Record), label: t.string, }), ]); @@ -100,17 +95,17 @@ export const DEFAULT_METRICS: MetricsExplorerOptionsMetric[] = [ { aggregation: 'avg', field: 'system.cpu.user.pct', - color: MetricsExplorerColor.color0, + color: Color.color0, }, { aggregation: 'avg', field: 'kubernetes.pod.cpu.usage.node.pct', - color: MetricsExplorerColor.color1, + color: Color.color1, }, { aggregation: 'avg', field: 'docker.cpu.total.pct', - color: MetricsExplorerColor.color2, + color: Color.color2, }, ]; diff --git a/x-pack/plugins/infra/server/infra_server.ts b/x-pack/plugins/infra/server/infra_server.ts index c080618f2a563..a72e40e25b479 100644 --- a/x-pack/plugins/infra/server/infra_server.ts +++ b/x-pack/plugins/infra/server/infra_server.ts @@ -36,6 +36,7 @@ import { initInventoryMetaRoute } from './routes/inventory_metadata'; import { initLogSourceConfigurationRoutes, initLogSourceStatusRoutes } from './routes/log_sources'; import { initSourceRoute } from './routes/source'; import { initAlertPreviewRoute } from './routes/alerting'; +import { initGetLogAlertsChartPreviewDataRoute } from './routes/log_alerts'; export const initInfraServer = (libs: InfraBackendLibs) => { const schema = makeExecutableSchema({ @@ -72,4 +73,5 @@ export const initInfraServer = (libs: InfraBackendLibs) => { initLogSourceConfigurationRoutes(libs); initLogSourceStatusRoutes(libs); initAlertPreviewRoute(libs); + initGetLogAlertsChartPreviewDataRoute(libs); }; diff --git a/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_chart_preview.ts b/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_chart_preview.ts new file mode 100644 index 0000000000000..026f003463ef2 --- /dev/null +++ b/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_chart_preview.ts @@ -0,0 +1,186 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; +import { RequestHandlerContext } from 'src/core/server'; +import { InfraSource } from '../../sources'; +import { KibanaFramework } from '../../adapters/framework/kibana_framework_adapter'; +import { + GetLogAlertsChartPreviewDataAlertParamsSubset, + Series, + Point, +} from '../../../../common/http_api/log_alerts'; +import { + getGroupedESQuery, + getUngroupedESQuery, + buildFiltersFromCriteria, +} from './log_threshold_executor'; +import { + UngroupedSearchQueryResponseRT, + UngroupedSearchQueryResponse, + GroupedSearchQueryResponse, + GroupedSearchQueryResponseRT, +} from '../../../../common/alerting/logs/types'; +import { decodeOrThrow } from '../../../../common/runtime_types'; + +const COMPOSITE_GROUP_SIZE = 40; + +export async function getChartPreviewData( + requestContext: RequestHandlerContext, + sourceConfiguration: InfraSource, + callWithRequest: KibanaFramework['callWithRequest'], + alertParams: GetLogAlertsChartPreviewDataAlertParamsSubset, + buckets: number +) { + const indexPattern = sourceConfiguration.configuration.logAlias; + const timestampField = sourceConfiguration.configuration.fields.timestamp; + + const { groupBy, timeSize, timeUnit } = alertParams; + const isGrouped = groupBy && groupBy.length > 0 ? true : false; + + // Charts will use an expanded time range + const expandedAlertParams = { + ...alertParams, + timeSize: timeSize * buckets, + }; + + const { rangeFilter } = buildFiltersFromCriteria(expandedAlertParams, timestampField); + + const query = isGrouped + ? getGroupedESQuery(expandedAlertParams, sourceConfiguration.configuration, indexPattern) + : getUngroupedESQuery(expandedAlertParams, sourceConfiguration.configuration, indexPattern); + + if (!query) { + throw new Error('ES query could not be built from the provided alert params'); + } + + const expandedQuery = addHistogramAggregationToQuery( + query, + rangeFilter, + `${timeSize}${timeUnit}`, + timestampField, + isGrouped + ); + + const series = isGrouped + ? processGroupedResults(await getGroupedResults(expandedQuery, requestContext, callWithRequest)) + : processUngroupedResults( + await getUngroupedResults(expandedQuery, requestContext, callWithRequest) + ); + + return { series }; +} + +// Expand the same query that powers the executor with a date histogram aggregation +const addHistogramAggregationToQuery = ( + query: any, + rangeFilter: any, + interval: string, + timestampField: string, + isGrouped: boolean +) => { + const histogramAggregation = { + histogramBuckets: { + date_histogram: { + field: timestampField, + fixed_interval: interval, + // Utilise extended bounds to make sure we get a full set of buckets even if there are empty buckets + // at the start and / or end of the range. + extended_bounds: { + min: rangeFilter.range[timestampField].gte, + max: rangeFilter.range[timestampField].lte, + }, + }, + }, + }; + + if (isGrouped) { + query.body.aggregations.groups.aggregations.filtered_results = { + ...query.body.aggregations.groups.aggregations.filtered_results, + aggregations: histogramAggregation, + }; + } else { + query.body = { + ...query.body, + aggregations: histogramAggregation, + }; + } + + return query; +}; + +const getUngroupedResults = async ( + query: object, + requestContext: RequestHandlerContext, + callWithRequest: KibanaFramework['callWithRequest'] +) => { + return decodeOrThrow(UngroupedSearchQueryResponseRT)( + await callWithRequest(requestContext, 'search', query) + ); +}; + +const getGroupedResults = async ( + query: object, + requestContext: RequestHandlerContext, + callWithRequest: KibanaFramework['callWithRequest'] +) => { + let compositeGroupBuckets: GroupedSearchQueryResponse['aggregations']['groups']['buckets'] = []; + let lastAfterKey: GroupedSearchQueryResponse['aggregations']['groups']['after_key'] | undefined; + + while (true) { + const queryWithAfterKey: any = { ...query }; + queryWithAfterKey.body.aggregations.groups.composite.after = lastAfterKey; + const groupResponse: GroupedSearchQueryResponse = decodeOrThrow(GroupedSearchQueryResponseRT)( + await callWithRequest(requestContext, 'search', queryWithAfterKey) + ); + compositeGroupBuckets = [ + ...compositeGroupBuckets, + ...groupResponse.aggregations.groups.buckets, + ]; + lastAfterKey = groupResponse.aggregations.groups.after_key; + if (groupResponse.aggregations.groups.buckets.length < COMPOSITE_GROUP_SIZE) { + break; + } + } + + return compositeGroupBuckets; +}; + +const processGroupedResults = ( + results: GroupedSearchQueryResponse['aggregations']['groups']['buckets'] +): Series => { + return results.reduce((series, group) => { + if (!group.filtered_results.histogramBuckets) return series; + const groupName = Object.values(group.key).join(', '); + const points = group.filtered_results.histogramBuckets.buckets.reduce( + (pointsAcc, bucket) => { + const { key, doc_count: count } = bucket; + return [...pointsAcc, { timestamp: key, value: count }]; + }, + [] + ); + return [...series, { id: groupName, points }]; + }, []); +}; + +const processUngroupedResults = (results: UngroupedSearchQueryResponse): Series => { + if (!results.aggregations?.histogramBuckets) return []; + const points = results.aggregations.histogramBuckets.buckets.reduce( + (pointsAcc, bucket) => { + const { key, doc_count: count } = bucket; + return [...pointsAcc, { timestamp: key, value: count }]; + }, + [] + ); + return [{ id: everythingSeriesName, points }]; +}; + +const everythingSeriesName = i18n.translate( + 'xpack.infra.logs.alerting.threshold.everythingSeriesName', + { + defaultMessage: 'Log entries', + } +); diff --git a/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_executor.ts b/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_executor.ts index 85bb18e199192..db76e955f0073 100644 --- a/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_executor.ts +++ b/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_executor.ts @@ -145,7 +145,10 @@ const processGroupByResults = ( }); }; -const buildFiltersFromCriteria = (params: LogDocumentCountAlertParams, timestampField: string) => { +export const buildFiltersFromCriteria = ( + params: Omit, + timestampField: string +) => { const { timeSize, timeUnit, criteria } = params; const interval = `${timeSize}${timeUnit}`; const intervalAsSeconds = getIntervalInSeconds(interval); @@ -193,8 +196,8 @@ const buildFiltersFromCriteria = (params: LogDocumentCountAlertParams, timestamp return { rangeFilter, groupedRangeFilter, mustFilters, mustNotFilters }; }; -const getGroupedESQuery = ( - params: LogDocumentCountAlertParams, +export const getGroupedESQuery = ( + params: Omit, sourceConfiguration: InfraSource['configuration'], index: string ): object | undefined => { @@ -253,8 +256,8 @@ const getGroupedESQuery = ( }; }; -const getUngroupedESQuery = ( - params: LogDocumentCountAlertParams, +export const getUngroupedESQuery = ( + params: Omit, sourceConfiguration: InfraSource['configuration'], index: string ): object => { diff --git a/x-pack/plugins/infra/server/routes/log_alerts/chart_preview_data.ts b/x-pack/plugins/infra/server/routes/log_alerts/chart_preview_data.ts new file mode 100644 index 0000000000000..95389e14acdb8 --- /dev/null +++ b/x-pack/plugins/infra/server/routes/log_alerts/chart_preview_data.ts @@ -0,0 +1,64 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import Boom from 'boom'; +import { InfraBackendLibs } from '../../lib/infra_types'; +import { + LOG_ALERTS_CHART_PREVIEW_DATA_PATH, + getLogAlertsChartPreviewDataSuccessResponsePayloadRT, + getLogAlertsChartPreviewDataRequestPayloadRT, +} from '../../../common/http_api/log_alerts/chart_preview_data'; +import { createValidationFunction } from '../../../common/runtime_types'; +import { getChartPreviewData } from '../../lib/alerting/log_threshold/log_threshold_chart_preview'; + +export const initGetLogAlertsChartPreviewDataRoute = ({ framework, sources }: InfraBackendLibs) => { + framework.registerRoute( + { + method: 'post', + path: LOG_ALERTS_CHART_PREVIEW_DATA_PATH, + validate: { + body: createValidationFunction(getLogAlertsChartPreviewDataRequestPayloadRT), + }, + }, + framework.router.handleLegacyErrors(async (requestContext, request, response) => { + const { + data: { sourceId, buckets, alertParams }, + } = request.body; + + const sourceConfiguration = await sources.getSourceConfiguration( + requestContext.core.savedObjects.client, + sourceId + ); + + try { + const { series } = await getChartPreviewData( + requestContext, + sourceConfiguration, + framework.callWithRequest, + alertParams, + buckets + ); + + return response.ok({ + body: getLogAlertsChartPreviewDataSuccessResponsePayloadRT.encode({ + data: { series }, + }), + }); + } catch (error) { + if (Boom.isBoom(error)) { + throw error; + } + + return response.customError({ + statusCode: error.statusCode ?? 500, + body: { + message: error.message ?? 'An unexpected error occurred', + }, + }); + } + }) + ); +}; diff --git a/x-pack/plugins/infra/server/routes/log_alerts/index.ts b/x-pack/plugins/infra/server/routes/log_alerts/index.ts new file mode 100644 index 0000000000000..5634fda043a52 --- /dev/null +++ b/x-pack/plugins/infra/server/routes/log_alerts/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export * from './chart_preview_data'; From 1257aad5b20d8778da9a0bd18f4179c4d6d9d041 Mon Sep 17 00:00:00 2001 From: Shahzad Date: Tue, 25 Aug 2020 12:35:29 +0200 Subject: [PATCH 33/71] [Uptime]fix wrapping issue in certificate list column (#74749) Co-authored-by: Elastic Machine --- .../fingerprint_col.test.tsx.snap | 26 +++++++------------ .../certificates/fingerprint_col.tsx | 10 +++---- .../__snapshots__/monitor_list.test.tsx.snap | 8 +++--- .../__snapshots__/status_filter.test.tsx.snap | 8 +++--- .../monitor_list/filter_status_button.tsx | 3 --- .../overview/monitor_list/status_filter.tsx | 13 +++------- 6 files changed, 24 insertions(+), 44 deletions(-) diff --git a/x-pack/plugins/uptime/public/components/certificates/__tests__/__snapshots__/fingerprint_col.test.tsx.snap b/x-pack/plugins/uptime/public/components/certificates/__tests__/__snapshots__/fingerprint_col.test.tsx.snap index b1da4aa929207..78c21e515c21e 100644 --- a/x-pack/plugins/uptime/public/components/certificates/__tests__/__snapshots__/fingerprint_col.test.tsx.snap +++ b/x-pack/plugins/uptime/public/components/certificates/__tests__/__snapshots__/fingerprint_col.test.tsx.snap @@ -1,8 +1,7 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`FingerprintCol renders expected elements for valid props 1`] = ` -Array [ - .c1 .euiButtonEmpty__content { +.c1 .euiButtonEmpty__content { padding-right: 0px; } @@ -10,8 +9,9 @@ Array [ margin-right: 8px; } - + - , - .c1 .euiButtonEmpty__content { - padding-right: 0px; -} - -.c0 { - margin-right: 8px; -} - - + - , -] + + `; exports[`FingerprintCol shallow renders expected elements for valid props 1`] = ` diff --git a/x-pack/plugins/uptime/public/components/certificates/fingerprint_col.tsx b/x-pack/plugins/uptime/public/components/certificates/fingerprint_col.tsx index 3bab0a183a0b5..049e206a3fc3c 100644 --- a/x-pack/plugins/uptime/public/components/certificates/fingerprint_col.tsx +++ b/x-pack/plugins/uptime/public/components/certificates/fingerprint_col.tsx @@ -16,7 +16,7 @@ const EmptyButton = styled(EuiButtonEmpty)` } `; -const Span = styled.span` +const StyledSpan = styled.span` margin-right: 8px; `; @@ -27,7 +27,7 @@ interface Props { export const FingerprintCol: React.FC = ({ cert }) => { const ShaComponent = ({ text, val }: { text: string; val: string }) => { return ( - + {text} @@ -41,13 +41,13 @@ export const FingerprintCol: React.FC = ({ cert }) => { /> )} - + ); }; return ( - <> + - + ); }; diff --git a/x-pack/plugins/uptime/public/components/overview/monitor_list/__tests__/__snapshots__/monitor_list.test.tsx.snap b/x-pack/plugins/uptime/public/components/overview/monitor_list/__tests__/__snapshots__/monitor_list.test.tsx.snap index bfe32acf29e39..4898ec00b38e2 100644 --- a/x-pack/plugins/uptime/public/components/overview/monitor_list/__tests__/__snapshots__/monitor_list.test.tsx.snap +++ b/x-pack/plugins/uptime/public/components/overview/monitor_list/__tests__/__snapshots__/monitor_list.test.tsx.snap @@ -904,12 +904,10 @@ exports[`MonitorList component renders the monitor list 1`] = ` > - - Up - + Up diff --git a/x-pack/plugins/uptime/public/components/overview/monitor_list/__tests__/__snapshots__/status_filter.test.tsx.snap b/x-pack/plugins/uptime/public/components/overview/monitor_list/__tests__/__snapshots__/status_filter.test.tsx.snap index 2aa6e0ea8b312..ed344510c1cd7 100644 --- a/x-pack/plugins/uptime/public/components/overview/monitor_list/__tests__/__snapshots__/status_filter.test.tsx.snap +++ b/x-pack/plugins/uptime/public/components/overview/monitor_list/__tests__/__snapshots__/status_filter.test.tsx.snap @@ -38,12 +38,10 @@ exports[`StatusFilterComponent renders without errors for valid props 1`] = ` > - - Up - + Up diff --git a/x-pack/plugins/uptime/public/components/overview/monitor_list/filter_status_button.tsx b/x-pack/plugins/uptime/public/components/overview/monitor_list/filter_status_button.tsx index 6e63c21d08ca9..19b09d50ced15 100644 --- a/x-pack/plugins/uptime/public/components/overview/monitor_list/filter_status_button.tsx +++ b/x-pack/plugins/uptime/public/components/overview/monitor_list/filter_status_button.tsx @@ -15,7 +15,6 @@ export interface FilterStatusButtonProps { isActive: boolean; value: 'up' | 'down' | ''; withNext: boolean; - color?: string; } export const FilterStatusButton = ({ @@ -24,14 +23,12 @@ export const FilterStatusButton = ({ isDisabled, isActive, value, - color, withNext, }: FilterStatusButtonProps) => { const [getUrlParams, setUrlParams] = useUrlParams(); const { statusFilter: urlValue } = getUrlParams(); return ( { isActive={statusFilter === ''} /> - {i18n.translate('xpack.uptime.filterBar.filterUpLabel', { - defaultMessage: 'Up', - })} - - } + content={i18n.translate('xpack.uptime.filterBar.filterUpLabel', { + defaultMessage: 'Up', + })} dataTestSubj="xpack.uptime.filterBar.filterStatusUp" value="up" withNext={true} @@ -47,7 +43,6 @@ export const StatusFilter: React.FC = () => { dataTestSubj="xpack.uptime.filterBar.filterStatusDown" value="down" withNext={false} - color={'danger'} isActive={statusFilter === 'down'} /> From fec0d515b3fe9fcaa185ccb77191afd0baaaead6 Mon Sep 17 00:00:00 2001 From: Shahzad Date: Tue, 25 Aug 2020 12:39:52 +0200 Subject: [PATCH 34/71] [RUM Dashboard] Rum design improvement (#74946) * craete new path for client side monitoring * update * update app * fix i18n * remove space * added feature on server * use lazy load * update test * update * remove csm serve file * update test * added design improvements * imrpove design * fix types * rervet conflict screw up * revert Co-authored-by: Elastic Machine --- .../app/RumDashboard/Charts/PageViewsChart.tsx | 18 +++++++++++++++--- .../app/RumDashboard/ClientMetrics/index.tsx | 6 ++++-- .../PageLoadDistribution/index.tsx | 2 +- .../app/RumDashboard/RumDashboard.tsx | 1 + .../app/RumDashboard/translations.ts | 3 --- .../translations/translations/ja-JP.json | 1 - .../translations/translations/zh-CN.json | 1 - 7 files changed, 21 insertions(+), 11 deletions(-) diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/Charts/PageViewsChart.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/Charts/PageViewsChart.tsx index 934a985dd735a..9211504a2dffe 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/Charts/PageViewsChart.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/Charts/PageViewsChart.tsx @@ -30,6 +30,7 @@ import { history } from '../../../../utils/history'; import { fromQuery, toQuery } from '../../../shared/Links/url_helpers'; import { ChartWrapper } from '../ChartWrapper'; import { useUiSetting$ } from '../../../../../../../../src/plugins/kibana_react/public'; +import { useUrlParams } from '../../../../hooks/useUrlParams'; interface Props { data?: Array>; @@ -37,7 +38,15 @@ interface Props { } export function PageViewsChart({ data, loading }: Props) { - const formatter = timeFormatter(niceTimeFormatByDay(2)); + const { urlParams } = useUrlParams(); + + const { start, end } = urlParams; + const diffInDays = moment(new Date(end as string)).diff( + moment(new Date(start as string)), + 'day' + ); + + const formatter = timeFormatter(niceTimeFormatByDay(diffInDays > 1 ? 2 : 1)); const onBrushEnd: BrushEndListener = ({ x }) => { if (!x) { @@ -91,18 +100,21 @@ export function PageViewsChart({ data, loading }: Props) { } showLegend onBrushEnd={onBrushEnd} + xDomain={{ + min: new Date(start as string).valueOf(), + max: new Date(end as string).valueOf(), + }} /> numeral(d).format('0.0 a')} + tickFormat={(d) => numeral(d).format('0a')} /> @@ -54,7 +56,7 @@ export function ClientMetrics() { diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/index.tsx index c7545ff9a2764..53f2d5ae238c5 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/index.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/index.tsx @@ -102,7 +102,7 @@ export function PageLoadDistribution() { /> - + + diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/translations.ts b/x-pack/plugins/apm/public/components/app/RumDashboard/translations.ts index 96d1b529c52f9..66eeaf433d2a1 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/translations.ts +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/translations.ts @@ -16,9 +16,6 @@ export const I18LABELS = { pageViews: i18n.translate('xpack.apm.rum.dashboard.pageViews', { defaultMessage: 'Page views', }), - dateTime: i18n.translate('xpack.apm.rum.dashboard.dateTime.label', { - defaultMessage: 'Date / Time', - }), percPageLoaded: i18n.translate('xpack.apm.rum.dashboard.pagesLoaded.label', { defaultMessage: 'Pages loaded', }), diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 411aa3424c855..118362f494b47 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -4909,7 +4909,6 @@ "xpack.apm.registerTransactionDurationAlertType.variables.serviceName": "サービス名", "xpack.apm.registerTransactionDurationAlertType.variables.transactionType": "トランザクションタイプ", "xpack.apm.rum.dashboard.backend": "バックエンド", - "xpack.apm.rum.dashboard.dateTime.label": "日付/時刻", "xpack.apm.rum.dashboard.frontend": "フロントエンド", "xpack.apm.rum.dashboard.overall.label": "全体", "xpack.apm.rum.dashboard.pageLoadDistribution.label": "ページ読み込み分布", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index c46135633a3c8..de1f206118447 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -4911,7 +4911,6 @@ "xpack.apm.registerTransactionDurationAlertType.variables.serviceName": "服务名称", "xpack.apm.registerTransactionDurationAlertType.variables.transactionType": "事务类型", "xpack.apm.rum.dashboard.backend": "后端", - "xpack.apm.rum.dashboard.dateTime.label": "日期 / 时间", "xpack.apm.rum.dashboard.frontend": "前端", "xpack.apm.rum.dashboard.overall.label": "总体", "xpack.apm.rum.dashboard.pageLoadDistribution.label": "页面加载分布", From 446c5237d59b7e324b39b87412880205f3b43db9 Mon Sep 17 00:00:00 2001 From: Marta Bondyra Date: Tue, 25 Aug 2020 13:47:04 +0200 Subject: [PATCH 35/71] [Visualize] fix performance degradation after lodash@4 upgrade --- src/plugins/vis_type_vislib/public/vislib/lib/vis_config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins/vis_type_vislib/public/vislib/lib/vis_config.js b/src/plugins/vis_type_vislib/public/vislib/lib/vis_config.js index 6490dfe252b29..dda9d85ec43c5 100644 --- a/src/plugins/vis_type_vislib/public/vislib/lib/vis_config.js +++ b/src/plugins/vis_type_vislib/public/vislib/lib/vis_config.js @@ -41,7 +41,7 @@ export class VisConfig { const visType = visTypes[visConfigArgs.type]; const typeDefaults = visType(visConfigArgs, this.data); - this._values = _.defaultsDeep({}, typeDefaults, DEFAULT_VIS_CONFIG); + this._values = _.defaultsDeep({ ...typeDefaults }, DEFAULT_VIS_CONFIG); this._values.el = el; } From 7fa23a4ec1f917164fef3fc84f64a5d2f23aa287 Mon Sep 17 00:00:00 2001 From: Matthew Kime Date: Tue, 25 Aug 2020 08:20:17 -0500 Subject: [PATCH 36/71] IndexPattern class - no longer use `getConfig` or `uiSettingsValues` (#75717) * remove getConfig and uiSettingsValues from IndexPattern class --- ...-data-public.indexpattern._constructor_.md | 4 +- ...plugin-plugins-data-public.indexpattern.md | 2 +- .../data/common/field_formats/errors.ts | 27 ++++++++ .../field_formats/field_formats_registry.ts | 3 +- .../data/common/field_formats/index.ts | 2 + .../index_patterns/_fields_fetcher.ts | 2 +- .../index_patterns/index_pattern.test.ts | 18 ++--- .../index_patterns/index_pattern.ts | 67 +++++++------------ .../index_patterns/index_patterns.ts | 10 ++- .../data/common/index_patterns/types.ts | 2 +- src/plugins/data/public/plugin.ts | 2 +- src/plugins/data/public/public.api.md | 2 +- 12 files changed, 75 insertions(+), 66 deletions(-) create mode 100644 src/plugins/data/common/field_formats/errors.ts diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern._constructor_.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern._constructor_.md index 0268846772f2c..2e078e3404fe6 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern._constructor_.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern._constructor_.md @@ -9,7 +9,7 @@ Constructs a new instance of the `IndexPattern` class Signature: ```typescript -constructor(id: string | undefined, { getConfig, savedObjectsClient, apiClient, patternCache, fieldFormats, onNotification, onError, uiSettingsValues, }: IndexPatternDeps); +constructor(id: string | undefined, { savedObjectsClient, apiClient, patternCache, fieldFormats, onNotification, onError, shortDotsEnable, metaFields, }: IndexPatternDeps); ``` ## Parameters @@ -17,5 +17,5 @@ constructor(id: string | undefined, { getConfig, savedObjectsClient, apiClient, | Parameter | Type | Description | | --- | --- | --- | | id | string | undefined | | -| { getConfig, savedObjectsClient, apiClient, patternCache, fieldFormats, onNotification, onError, uiSettingsValues, } | IndexPatternDeps | | +| { savedObjectsClient, apiClient, patternCache, fieldFormats, onNotification, onError, shortDotsEnable, metaFields, } | IndexPatternDeps | | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.md index d340aaeeef25e..649f8ef077e3f 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.md @@ -14,7 +14,7 @@ export declare class IndexPattern implements IIndexPattern | Constructor | Modifiers | Description | | --- | --- | --- | -| [(constructor)(id, { getConfig, savedObjectsClient, apiClient, patternCache, fieldFormats, onNotification, onError, uiSettingsValues, })](./kibana-plugin-plugins-data-public.indexpattern._constructor_.md) | | Constructs a new instance of the IndexPattern class | +| [(constructor)(id, { savedObjectsClient, apiClient, patternCache, fieldFormats, onNotification, onError, shortDotsEnable, metaFields, })](./kibana-plugin-plugins-data-public.indexpattern._constructor_.md) | | Constructs a new instance of the IndexPattern class | ## Properties diff --git a/src/plugins/data/common/field_formats/errors.ts b/src/plugins/data/common/field_formats/errors.ts new file mode 100644 index 0000000000000..d72eef080923d --- /dev/null +++ b/src/plugins/data/common/field_formats/errors.ts @@ -0,0 +1,27 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export class FieldFormatNotFoundError extends Error { + public readonly formatId: string; + constructor(message: string, formatId: string) { + super(message); + this.name = 'FieldFormatNotFoundError'; + this.formatId = formatId; + } +} diff --git a/src/plugins/data/common/field_formats/field_formats_registry.ts b/src/plugins/data/common/field_formats/field_formats_registry.ts index 32f9f37b9ba53..4b46adf399363 100644 --- a/src/plugins/data/common/field_formats/field_formats_registry.ts +++ b/src/plugins/data/common/field_formats/field_formats_registry.ts @@ -34,6 +34,7 @@ import { FieldFormat } from './field_format'; import { SerializedFieldFormat } from '../../../expressions/common/types'; import { ES_FIELD_TYPES, KBN_FIELD_TYPES } from '../kbn_field_types/types'; import { UI_SETTINGS } from '../constants'; +import { FieldFormatNotFoundError } from '../field_formats'; export class FieldFormatsRegistry { protected fieldFormats: Map = new Map(); @@ -161,7 +162,7 @@ export class FieldFormatsRegistry { const ConcreteFieldFormat = this.getType(formatId); if (!ConcreteFieldFormat) { - throw new Error(`Field Format '${formatId}' not found!`); + throw new FieldFormatNotFoundError(`Field Format '${formatId}' not found!`, formatId); } return new ConcreteFieldFormat(params, this.getConfig); diff --git a/src/plugins/data/common/field_formats/index.ts b/src/plugins/data/common/field_formats/index.ts index d622af2f663a1..c1b1619abd247 100644 --- a/src/plugins/data/common/field_formats/index.ts +++ b/src/plugins/data/common/field_formats/index.ts @@ -55,3 +55,5 @@ export { IFieldFormat, FieldFormatsStartCommon, } from './types'; + +export * from './errors'; diff --git a/src/plugins/data/common/index_patterns/index_patterns/_fields_fetcher.ts b/src/plugins/data/common/index_patterns/index_patterns/_fields_fetcher.ts index baeb1587d57a9..4eba0576ff235 100644 --- a/src/plugins/data/common/index_patterns/index_patterns/_fields_fetcher.ts +++ b/src/plugins/data/common/index_patterns/index_patterns/_fields_fetcher.ts @@ -24,7 +24,7 @@ import { GetFieldsOptions, IIndexPatternsApiClient } from '../types'; export const createFieldsFetcher = ( indexPattern: IndexPattern, apiClient: IIndexPatternsApiClient, - metaFields: string + metaFields: string[] = [] ) => { const fieldFetcher = { fetch: (options: GetFieldsOptions) => { diff --git a/src/plugins/data/common/index_patterns/index_patterns/index_pattern.test.ts b/src/plugins/data/common/index_patterns/index_patterns/index_pattern.test.ts index e4f297b29c372..09b79cae4aac2 100644 --- a/src/plugins/data/common/index_patterns/index_patterns/index_pattern.test.ts +++ b/src/plugins/data/common/index_patterns/index_patterns/index_pattern.test.ts @@ -32,7 +32,7 @@ import { fieldFormatsMock } from '../../field_formats/mocks'; class MockFieldFormatter {} -fieldFormatsMock.getType = jest.fn().mockImplementation(() => MockFieldFormatter); +fieldFormatsMock.getInstance = jest.fn().mockImplementation(() => new MockFieldFormatter()) as any; jest.mock('../../field_mapping', () => { const originalModule = jest.requireActual('../../field_mapping'); @@ -89,10 +89,6 @@ const patternCache = { clearAll: jest.fn(), }; -const config = { - get: jest.fn(), -}; - const apiClient = { _getUrl: jest.fn(), getFieldsForTimePattern: jest.fn(), @@ -102,14 +98,14 @@ const apiClient = { // helper function to create index patterns function create(id: string, payload?: any): Promise { const indexPattern = new IndexPattern(id, { - getConfig: (cfg: any) => config.get(cfg), savedObjectsClient: savedObjectsClient as any, apiClient, patternCache, fieldFormats: fieldFormatsMock, onNotification: () => {}, onError: () => {}, - uiSettingsValues: { shortDotsEnable: false, metaFields: [] }, + shortDotsEnable: false, + metaFields: [], }); setDocsourcePayload(id, payload); @@ -391,14 +387,14 @@ describe('IndexPattern', () => { }); // Create a normal index pattern const pattern = new IndexPattern('foo', { - getConfig: (cfg: any) => config.get(cfg), savedObjectsClient: savedObjectsClient as any, apiClient, patternCache, fieldFormats: fieldFormatsMock, onNotification: () => {}, onError: () => {}, - uiSettingsValues: { shortDotsEnable: false, metaFields: [] }, + shortDotsEnable: false, + metaFields: [], }); await pattern.init(); @@ -406,14 +402,14 @@ describe('IndexPattern', () => { // Create the same one - we're going to handle concurrency const samePattern = new IndexPattern('foo', { - getConfig: (cfg: any) => config.get(cfg), savedObjectsClient: savedObjectsClient as any, apiClient, patternCache, fieldFormats: fieldFormatsMock, onNotification: () => {}, onError: () => {}, - uiSettingsValues: { shortDotsEnable: false, metaFields: [] }, + shortDotsEnable: false, + metaFields: [], }); await samePattern.init(); diff --git a/src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts b/src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts index 4e484dce7826f..e81ef1d6b2482 100644 --- a/src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts +++ b/src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts @@ -22,20 +22,19 @@ import { i18n } from '@kbn/i18n'; import { SavedObjectsClientCommon } from '../..'; import { DuplicateField, SavedObjectNotFound } from '../../../../kibana_utils/common'; -import { ES_FIELD_TYPES, KBN_FIELD_TYPES, IIndexPattern } from '../../../common'; +import { + ES_FIELD_TYPES, + KBN_FIELD_TYPES, + IIndexPattern, + FieldFormatNotFoundError, +} from '../../../common'; import { findByTitle } from '../utils'; import { IndexPatternMissingIndices } from '../lib'; import { IndexPatternField, IIndexPatternFieldList, FieldList } from '../fields'; import { createFieldsFetcher } from './_fields_fetcher'; import { formatHitProvider } from './format_hit'; import { flattenHitWrapper } from './flatten_hit'; -import { - OnNotification, - OnError, - UiSettingsCommon, - IIndexPatternsApiClient, - IndexPatternAttributes, -} from '../types'; +import { OnNotification, OnError, IIndexPatternsApiClient, IndexPatternAttributes } from '../types'; import { FieldFormatsStartCommon, FieldFormat } from '../../field_formats'; import { PatternCache } from './_pattern_cache'; import { expandShorthand, FieldMappingSpec, MappingObject } from '../../field_mapping'; @@ -44,21 +43,16 @@ import { SerializedFieldFormat } from '../../../../expressions/common'; const MAX_ATTEMPTS_TO_RESOLVE_CONFLICTS = 3; const savedObjectType = 'index-pattern'; -interface IUiSettingsValues { - [key: string]: any; - shortDotsEnable: any; - metaFields: any; -} interface IndexPatternDeps { - getConfig: UiSettingsCommon['get']; savedObjectsClient: SavedObjectsClientCommon; apiClient: IIndexPatternsApiClient; patternCache: PatternCache; fieldFormats: FieldFormatsStartCommon; onNotification: OnNotification; onError: OnError; - uiSettingsValues: IUiSettingsValues; + shortDotsEnable: boolean; + metaFields: string[]; } export class IndexPattern implements IIndexPattern { @@ -78,7 +72,6 @@ export class IndexPattern implements IIndexPattern { private version: string | undefined; private savedObjectsClient: SavedObjectsClientCommon; private patternCache: PatternCache; - private getConfig: UiSettingsCommon['get']; private sourceFilters?: SourceFilter[]; private originalBody: { [key: string]: any } = {}; public fieldsFetcher: any; // probably want to factor out any direct usage and change to private @@ -87,7 +80,6 @@ export class IndexPattern implements IIndexPattern { private onNotification: OnNotification; private onError: OnError; private apiClient: IIndexPatternsApiClient; - private uiSettingsValues: IUiSettingsValues; private mapping: MappingObject = expandShorthand({ title: ES_FIELD_TYPES.TEXT, @@ -114,35 +106,31 @@ export class IndexPattern implements IIndexPattern { constructor( id: string | undefined, { - getConfig, savedObjectsClient, apiClient, patternCache, fieldFormats, onNotification, onError, - uiSettingsValues, + shortDotsEnable = false, + metaFields = [], }: IndexPatternDeps ) { this.id = id; this.savedObjectsClient = savedObjectsClient; this.patternCache = patternCache; - // instead of storing config we rather store the getter only as np uiSettingsClient has circular references - // which cause problems when being consumed from angular - this.getConfig = getConfig; this.fieldFormats = fieldFormats; this.onNotification = onNotification; this.onError = onError; - this.uiSettingsValues = uiSettingsValues; - this.shortDotsEnable = uiSettingsValues.shortDotsEnable; - this.metaFields = uiSettingsValues.metaFields; + this.shortDotsEnable = shortDotsEnable; + this.metaFields = metaFields; this.fields = new FieldList(this, [], this.shortDotsEnable, this.onUnknownType); this.apiClient = apiClient; - this.fieldsFetcher = createFieldsFetcher(this, apiClient, uiSettingsValues.metaFields); - this.flattenHit = flattenHitWrapper(this, uiSettingsValues.metaFields); + this.fieldsFetcher = createFieldsFetcher(this, apiClient, metaFields); + this.flattenHit = flattenHitWrapper(this, metaFields); this.formatHit = formatHitProvider( this, fieldFormats.getDefaultInstance(KBN_FIELD_TYPES.STRING) @@ -157,15 +145,15 @@ export class IndexPattern implements IIndexPattern { } private deserializeFieldFormatMap(mapping: any) { - const FieldFormatter = this.fieldFormats.getType(mapping.id); - - return ( - FieldFormatter && - new FieldFormatter( - mapping.params, - (key: string) => this.uiSettingsValues[key]?.userValue || this.uiSettingsValues[key]?.value - ) - ); + try { + return this.fieldFormats.getInstance(mapping.id, mapping.params); + } catch (err) { + if (err instanceof FieldFormatNotFoundError) { + return undefined; + } else { + throw err; + } + } } private isFieldRefreshRequired(specs?: FieldSpec[]): boolean { @@ -513,17 +501,14 @@ export class IndexPattern implements IIndexPattern { saveAttempts++ < MAX_ATTEMPTS_TO_RESOLVE_CONFLICTS ) { const samePattern = new IndexPattern(this.id, { - getConfig: this.getConfig, savedObjectsClient: this.savedObjectsClient, apiClient: this.apiClient, patternCache: this.patternCache, fieldFormats: this.fieldFormats, onNotification: this.onNotification, onError: this.onError, - uiSettingsValues: { - shortDotsEnable: this.shortDotsEnable, - metaFields: this.metaFields, - }, + shortDotsEnable: this.shortDotsEnable, + metaFields: this.metaFields, }); return samePattern.init().then(() => { diff --git a/src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts b/src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts index 8874ce5f04b7c..0ad9ae8f2014f 100644 --- a/src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts +++ b/src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts @@ -185,17 +185,16 @@ export class IndexPatternsService { async specToIndexPattern(spec: IndexPatternSpec) { const shortDotsEnable = await this.config.get(UI_SETTINGS.SHORT_DOTS_ENABLE); const metaFields = await this.config.get(UI_SETTINGS.META_FIELDS); - const uiSettingsValues = await this.config.getAll(); const indexPattern = new IndexPattern(spec.id, { - getConfig: (cfg: any) => this.config.get(cfg), savedObjectsClient: this.savedObjectsClient, apiClient: this.apiClient, patternCache: indexPatternCache, fieldFormats: this.fieldFormats, onNotification: this.onNotification, onError: this.onError, - uiSettingsValues: { ...uiSettingsValues, shortDotsEnable, metaFields }, + shortDotsEnable, + metaFields, }); indexPattern.initFromSpec(spec); @@ -205,17 +204,16 @@ export class IndexPatternsService { async make(id?: string): Promise { const shortDotsEnable = await this.config.get(UI_SETTINGS.SHORT_DOTS_ENABLE); const metaFields = await this.config.get(UI_SETTINGS.META_FIELDS); - const uiSettingsValues = await this.config.getAll(); const indexPattern = new IndexPattern(id, { - getConfig: (cfg: any) => this.config.get(cfg), savedObjectsClient: this.savedObjectsClient, apiClient: this.apiClient, patternCache: indexPatternCache, fieldFormats: this.fieldFormats, onNotification: this.onNotification, onError: this.onError, - uiSettingsValues: { ...uiSettingsValues, shortDotsEnable, metaFields }, + shortDotsEnable, + metaFields, }); return indexPattern.init(); diff --git a/src/plugins/data/common/index_patterns/types.ts b/src/plugins/data/common/index_patterns/types.ts index a771113acd231..7a230c20f6cd0 100644 --- a/src/plugins/data/common/index_patterns/types.ts +++ b/src/plugins/data/common/index_patterns/types.ts @@ -96,7 +96,7 @@ export interface GetFieldsOptions { type?: string; params?: any; lookBack?: boolean; - metaFields?: string; + metaFields?: string[]; } export interface IIndexPatternsApiClient { diff --git a/src/plugins/data/public/plugin.ts b/src/plugins/data/public/plugin.ts index ee0b0714febc0..3bc19a578a417 100644 --- a/src/plugins/data/public/plugin.ts +++ b/src/plugins/data/public/plugin.ts @@ -177,7 +177,7 @@ export class DataPublicPlugin onNotification: (toastInputFields) => { notifications.toasts.add(toastInputFields); }, - onError: notifications.toasts.addError, + onError: notifications.toasts.addError.bind(notifications.toasts), onRedirectNoIndexPattern: onRedirectNoIndexPattern( application.capabilities, application.navigateToApp, diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md index f8a108a5a4c58..261f16229460a 100644 --- a/src/plugins/data/public/public.api.md +++ b/src/plugins/data/public/public.api.md @@ -944,7 +944,7 @@ export type IMetricAggType = MetricAggType; // @public (undocumented) export class IndexPattern implements IIndexPattern { // Warning: (ae-forgotten-export) The symbol "IndexPatternDeps" needs to be exported by the entry point index.d.ts - constructor(id: string | undefined, { getConfig, savedObjectsClient, apiClient, patternCache, fieldFormats, onNotification, onError, uiSettingsValues, }: IndexPatternDeps); + constructor(id: string | undefined, { savedObjectsClient, apiClient, patternCache, fieldFormats, onNotification, onError, shortDotsEnable, metaFields, }: IndexPatternDeps); // (undocumented) [key: string]: any; // (undocumented) From 90f0a294afa921baead26cb37d4d91ca338d2f23 Mon Sep 17 00:00:00 2001 From: Patrick Mueller Date: Tue, 25 Aug 2020 09:29:55 -0400 Subject: [PATCH 37/71] [Actions] change routing key refereence in Pager Duty action message to include integration key (#75516) resolves https://github.com/elastic/kibana/issues/68209 Since routing key figures fairly prominently throughout PagerDuty APIs, and ours, it seems like it make sense to include it in the single validation message we have for it, as well as using the term we use for it in the product: "integration key". See the referenced issue for more background. --- .../builtin_action_types/pagerduty/pagerduty.test.tsx | 2 +- .../components/builtin_action_types/pagerduty/pagerduty.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty.test.tsx index ba7eb598c120d..0674e5b35c61f 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty.test.tsx @@ -69,7 +69,7 @@ describe('pagerduty connector validation', () => { expect(actionTypeModel.validateConnector(actionConnector)).toEqual({ errors: { - routingKey: ['A routing key is required.'], + routingKey: ['An integration key / routing key is required.'], }, }); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty.tsx index 5e29fca397180..90d8da346c71d 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty.tsx @@ -38,7 +38,7 @@ export function getActionType(): ActionTypeModel { i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.error.requiredRoutingKeyText', { - defaultMessage: 'A routing key is required.', + defaultMessage: 'An integration key / routing key is required.', } ) ); From 59c4cd4a6983db6954f28e89b5a8d1267c1d632a Mon Sep 17 00:00:00 2001 From: Yuliia Naumenko Date: Tue, 25 Aug 2020 06:33:04 -0700 Subject: [PATCH 38/71] Reduced the number of targets for a proxy server, only actions executions should be affected (#75839) * Reduced the number of targets for a proxy server, only actions executions should be affected * fixed typecheck --- .../common/lib/get_proxy_server.ts | 30 +++++++------------ .../actions/builtin_action_types/jira.ts | 22 +++++++++++++- .../actions/builtin_action_types/pagerduty.ts | 22 +++++++++++++- .../actions/builtin_action_types/resilient.ts | 20 +++++++++++++ .../builtin_action_types/servicenow.ts | 21 ++++++++++++- .../actions/builtin_action_types/slack.ts | 16 ++++++++++ .../actions/builtin_action_types/webhook.ts | 17 +++++++++++ .../tests/actions/index.ts | 19 ------------ 8 files changed, 126 insertions(+), 41 deletions(-) diff --git a/x-pack/test/alerting_api_integration/common/lib/get_proxy_server.ts b/x-pack/test/alerting_api_integration/common/lib/get_proxy_server.ts index 7528b00f926d0..2b4908c156e51 100644 --- a/x-pack/test/alerting_api_integration/common/lib/get_proxy_server.ts +++ b/x-pack/test/alerting_api_integration/common/lib/get_proxy_server.ts @@ -4,32 +4,24 @@ * you may not use this file except in compliance with the Elastic License. */ -import http from 'http'; import httpProxy from 'http-proxy'; -import { ToolingLog } from '@kbn/dev-utils'; export const getHttpProxyServer = async ( - defaultKibanaTargetUrl: string, + targetUrl: string, kbnTestServerConfig: any, - log: ToolingLog -): Promise => { - const proxy = httpProxy.createProxyServer({ secure: false, selfHandleResponse: false }); - - const proxyPort = getProxyPort(kbnTestServerConfig); - const proxyServer = http.createServer((req: http.IncomingMessage, res: http.ServerResponse) => { - const targetUrl = new URL(req.url ?? defaultKibanaTargetUrl); + onProxyResHandler: (proxyRes?: unknown, req?: unknown, res?: unknown) => void +): Promise => { + const proxyServer = httpProxy.createProxyServer({ + target: targetUrl, + secure: false, + selfHandleResponse: false, + }); - if (targetUrl.hostname !== 'some.non.existent.com') { - proxy.web(req, res, { - target: `${targetUrl.protocol}//${targetUrl.hostname}:${targetUrl.port}`, - }); - } else { - res.writeHead(500, { 'Content-Type': 'text/plain' }); - res.write('error on call some.non.existent.com'); - res.end(); - } + proxyServer.on('proxyRes', (proxyRes: unknown, req: unknown, res: unknown) => { + onProxyResHandler(proxyRes, req, res); }); + const proxyPort = getProxyPort(kbnTestServerConfig); proxyServer.listen(proxyPort); return proxyServer; diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/jira.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/jira.ts index 78a1df0b9c1c7..3ffd58b945ddb 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/jira.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/jira.ts @@ -4,8 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ +import httpProxy from 'http-proxy'; import expect from '@kbn/expect'; +import { getHttpProxyServer } from '../../../../common/lib/get_proxy_server'; import { FtrProviderContext } from '../../../../common/ftr_provider_context'; import { @@ -35,6 +37,7 @@ const mapping = [ export default function jiraTest({ getService }: FtrProviderContext) { const supertest = getService('supertest'); const kibanaServer = getService('kibanaServer'); + const configService = getService('config'); const mockJira = { config: { @@ -80,7 +83,6 @@ export default function jiraTest({ getService }: FtrProviderContext) { getExternalServiceSimulatorPath(ExternalServiceSimulator.JIRA) ); }); - describe('Jira - Action Creation', () => { it('should return 200 when creating a jira action successfully', async () => { const { body: createdAction } = await supertest @@ -292,6 +294,9 @@ export default function jiraTest({ getService }: FtrProviderContext) { describe('Jira - Executor', () => { let simulatedActionId: string; + let proxyServer: httpProxy | undefined; + let proxyHaveBeenCalled = false; + before(async () => { const { body } = await supertest .post('/api/actions/action') @@ -307,6 +312,14 @@ export default function jiraTest({ getService }: FtrProviderContext) { secrets: mockJira.secrets, }); simulatedActionId = body.id; + + proxyServer = await getHttpProxyServer( + kibanaServer.resolveUrl('/'), + configService.get('kbnTestServer.serverArgs'), + () => { + proxyHaveBeenCalled = true; + } + ); }); describe('Validation', () => { @@ -529,6 +542,7 @@ export default function jiraTest({ getService }: FtrProviderContext) { }) .expect(200); + expect(proxyHaveBeenCalled).to.equal(true); expect(body).to.eql({ status: 'ok', actionId: simulatedActionId, @@ -541,6 +555,12 @@ export default function jiraTest({ getService }: FtrProviderContext) { }); }); }); + + after(() => { + if (proxyServer) { + proxyServer.close(); + } + }); }); }); } diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/pagerduty.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/pagerduty.ts index 76b3e8e39791a..0c4d9096aa31a 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/pagerduty.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/pagerduty.ts @@ -4,8 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ +import httpProxy from 'http-proxy'; import expect from '@kbn/expect'; +import { getHttpProxyServer } from '../../../../common/lib/get_proxy_server'; import { FtrProviderContext } from '../../../../common/ftr_provider_context'; import { @@ -17,16 +19,27 @@ import { export default function pagerdutyTest({ getService }: FtrProviderContext) { const supertest = getService('supertest'); const kibanaServer = getService('kibanaServer'); + const configService = getService('config'); describe('pagerduty action', () => { let simulatedActionId = ''; let pagerdutySimulatorURL: string = ''; + let proxyServer: httpProxy | undefined; + let proxyHaveBeenCalled = false; // need to wait for kibanaServer to settle ... - before(() => { + before(async () => { pagerdutySimulatorURL = kibanaServer.resolveUrl( getExternalServiceSimulatorPath(ExternalServiceSimulator.PAGERDUTY) ); + + proxyServer = await getHttpProxyServer( + kibanaServer.resolveUrl('/'), + configService.get('kbnTestServer.serverArgs'), + () => { + proxyHaveBeenCalled = true; + } + ); }); it('should return successfully when passed valid create parameters', async () => { @@ -145,6 +158,7 @@ export default function pagerdutyTest({ getService }: FtrProviderContext) { }) .expect(200); + expect(proxyHaveBeenCalled).to.equal(true); expect(result).to.eql({ status: 'ok', actionId: simulatedActionId, @@ -203,5 +217,11 @@ export default function pagerdutyTest({ getService }: FtrProviderContext) { expect(result.message).to.match(/error posting pagerduty event: http status 502/); expect(result.retry).to.equal(true); }); + + after(() => { + if (proxyServer) { + proxyServer.close(); + } + }); }); } diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/resilient.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/resilient.ts index 8adaf9f121931..9cbc2373ef943 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/resilient.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/resilient.ts @@ -4,8 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ +import httpProxy from 'http-proxy'; import expect from '@kbn/expect'; +import { getHttpProxyServer } from '../../../../common/lib/get_proxy_server'; import { FtrProviderContext } from '../../../../common/ftr_provider_context'; import { @@ -35,6 +37,7 @@ const mapping = [ export default function resilientTest({ getService }: FtrProviderContext) { const supertest = getService('supertest'); const kibanaServer = getService('kibanaServer'); + const configService = getService('config'); const mockResilient = { config: { @@ -292,6 +295,8 @@ export default function resilientTest({ getService }: FtrProviderContext) { describe('IBM Resilient - Executor', () => { let simulatedActionId: string; + let proxyServer: httpProxy | undefined; + let proxyHaveBeenCalled = false; before(async () => { const { body } = await supertest .post('/api/actions/action') @@ -307,6 +312,14 @@ export default function resilientTest({ getService }: FtrProviderContext) { secrets: mockResilient.secrets, }); simulatedActionId = body.id; + + proxyServer = await getHttpProxyServer( + kibanaServer.resolveUrl('/'), + configService.get('kbnTestServer.serverArgs'), + () => { + proxyHaveBeenCalled = true; + } + ); }); describe('Validation', () => { @@ -529,6 +542,7 @@ export default function resilientTest({ getService }: FtrProviderContext) { }) .expect(200); + expect(proxyHaveBeenCalled).to.equal(true); expect(body).to.eql({ status: 'ok', actionId: simulatedActionId, @@ -541,6 +555,12 @@ export default function resilientTest({ getService }: FtrProviderContext) { }); }); }); + + after(() => { + if (proxyServer) { + proxyServer.close(); + } + }); }); }); } diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/servicenow.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/servicenow.ts index 2dad6f2c425e5..3f8341df3d295 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/servicenow.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/servicenow.ts @@ -4,8 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ +import httpProxy from 'http-proxy'; import expect from '@kbn/expect'; +import { getHttpProxyServer } from '../../../../common/lib/get_proxy_server'; import { FtrProviderContext } from '../../../../common/ftr_provider_context'; import { @@ -35,6 +37,7 @@ const mapping = [ export default function servicenowTest({ getService }: FtrProviderContext) { const supertest = getService('supertest'); const kibanaServer = getService('kibanaServer'); + const configService = getService('config'); const mockServiceNow = { config: { @@ -264,6 +267,8 @@ export default function servicenowTest({ getService }: FtrProviderContext) { describe('ServiceNow - Executor', () => { let simulatedActionId: string; + let proxyServer: httpProxy | undefined; + let proxyHaveBeenCalled = false; before(async () => { const { body } = await supertest .post('/api/actions/action') @@ -279,6 +284,14 @@ export default function servicenowTest({ getService }: FtrProviderContext) { secrets: mockServiceNow.secrets, }); simulatedActionId = body.id; + + proxyServer = await getHttpProxyServer( + kibanaServer.resolveUrl('/'), + configService.get('kbnTestServer.serverArgs'), + () => { + proxyHaveBeenCalled = true; + } + ); }); describe('Validation', () => { @@ -448,7 +461,7 @@ export default function servicenowTest({ getService }: FtrProviderContext) { }, }) .expect(200); - + expect(proxyHaveBeenCalled).to.equal(true); expect(result).to.eql({ status: 'ok', actionId: simulatedActionId, @@ -461,6 +474,12 @@ export default function servicenowTest({ getService }: FtrProviderContext) { }); }); }); + + after(() => { + if (proxyServer) { + proxyServer.close(); + } + }); }); }); } diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/slack.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/slack.ts index 1712c31187b02..83ad17757f3a6 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/slack.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/slack.ts @@ -4,9 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ +import httpProxy from 'http-proxy'; import expect from '@kbn/expect'; import http from 'http'; import getPort from 'get-port'; +import { getHttpProxyServer } from '../../../../common/lib/get_proxy_server'; import { FtrProviderContext } from '../../../../common/ftr_provider_context'; import { getSlackServer } from '../../../../common/fixtures/plugins/actions_simulators/server/plugin'; @@ -14,11 +16,14 @@ import { getSlackServer } from '../../../../common/fixtures/plugins/actions_simu // eslint-disable-next-line import/no-default-export export default function slackTest({ getService }: FtrProviderContext) { const supertest = getService('supertest'); + const configService = getService('config'); describe('slack action', () => { let simulatedActionId = ''; let slackSimulatorURL: string = ''; let slackServer: http.Server; + let proxyServer: httpProxy | undefined; + let proxyHaveBeenCalled = false; // need to wait for kibanaServer to settle ... before(async () => { @@ -28,6 +33,13 @@ export default function slackTest({ getService }: FtrProviderContext) { slackServer.listen(availablePort); } slackSimulatorURL = `http://localhost:${availablePort}`; + proxyServer = await getHttpProxyServer( + slackSimulatorURL, + configService.get('kbnTestServer.serverArgs'), + () => { + proxyHaveBeenCalled = true; + } + ); }); it('should return 200 when creating a slack action successfully', async () => { @@ -157,6 +169,7 @@ export default function slackTest({ getService }: FtrProviderContext) { }) .expect(200); expect(result.status).to.eql('ok'); + expect(proxyHaveBeenCalled).to.equal(true); }); it('should handle an empty message error', async () => { @@ -224,6 +237,9 @@ export default function slackTest({ getService }: FtrProviderContext) { after(() => { slackServer.close(); + if (proxyServer) { + proxyServer.close(); + } }); }); } diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/webhook.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/webhook.ts index abebb2650ad08..d82d116396cd6 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/webhook.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/webhook.ts @@ -4,10 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ +import httpProxy from 'http-proxy'; import http from 'http'; import expect from '@kbn/expect'; import { URL, format as formatUrl } from 'url'; import getPort from 'get-port'; +import { getHttpProxyServer } from '../../../../common/lib/get_proxy_server'; import { FtrProviderContext } from '../../../../common/ftr_provider_context'; import { getExternalServiceSimulatorPath, @@ -31,6 +33,7 @@ function parsePort(url: Record): Record { @@ -80,6 +85,14 @@ export default function webhookTest({ getService }: FtrProviderContext) { kibanaURL = kibanaServer.resolveUrl( getExternalServiceSimulatorPath(ExternalServiceSimulator.WEBHOOK) ); + + proxyServer = await getHttpProxyServer( + webhookSimulatorURL, + configService.get('kbnTestServer.serverArgs'), + () => { + proxyHaveBeenCalled = true; + } + ); }); it('should return 200 when creating a webhook action successfully', async () => { @@ -178,6 +191,7 @@ export default function webhookTest({ getService }: FtrProviderContext) { }) .expect(200); + expect(proxyHaveBeenCalled).to.equal(true); expect(result.status).to.eql('ok'); }); @@ -241,6 +255,9 @@ export default function webhookTest({ getService }: FtrProviderContext) { after(() => { webhookServer.close(); + if (proxyServer) { + proxyServer.close(); + } }); }); } diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/index.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/index.ts index 54484ba34636f..7b2e5f14fc4b6 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/index.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/index.ts @@ -4,24 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import http from 'http'; -import { getHttpProxyServer } from '../../../common/lib/get_proxy_server'; import { FtrProviderContext } from '../../../common/ftr_provider_context'; // eslint-disable-next-line import/no-default-export export default function actionsTests({ loadTestFile, getService }: FtrProviderContext) { - const configService = getService('config'); - const kibanaServer = getService('kibanaServer'); - const log = getService('log'); describe('Actions', () => { - let proxyServer: http.Server | undefined; - before(async () => { - proxyServer = await getHttpProxyServer( - kibanaServer.resolveUrl('/'), - configService.get('kbnTestServer.serverArgs'), - log - ); - }); loadTestFile(require.resolve('./builtin_action_types/email')); loadTestFile(require.resolve('./builtin_action_types/es_index')); loadTestFile(require.resolve('./builtin_action_types/es_index_preconfigured')); @@ -39,11 +26,5 @@ export default function actionsTests({ loadTestFile, getService }: FtrProviderCo loadTestFile(require.resolve('./get')); loadTestFile(require.resolve('./list_action_types')); loadTestFile(require.resolve('./update')); - - after(() => { - if (proxyServer) { - proxyServer.close(); - } - }); }); } From 1e8c05f87ad5901abeeffe40a730153c3e1658c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cau=C3=AA=20Marcondes?= <55978943+cauemarcondes@users.noreply.github.com> Date: Tue, 25 Aug 2020 15:15:36 +0100 Subject: [PATCH 39/71] [APM] UI filters: Change transaction type selector from dropdown to radio buttons (#75625) * changing transaction type filter to radio group * fixing unit test * changing transaction type filter to radio group * adding onclick to the badge component * adding onclick to the badge component * adding i18n to aria Co-authored-by: Elastic Machine --- .../TransactionOverview.test.tsx | 46 ++++++++----------- .../app/TransactionOverview/index.tsx | 2 +- .../LocalUIFilters/Filter/FilterBadgeList.tsx | 26 +++++++---- .../shared/LocalUIFilters/Filter/index.tsx | 2 +- .../TransactionTypeFilter/index.tsx | 15 +++--- 5 files changed, 47 insertions(+), 44 deletions(-) rename x-pack/plugins/apm/public/components/app/TransactionOverview/{__jest__ => }/TransactionOverview.test.tsx (70%) diff --git a/x-pack/plugins/apm/public/components/app/TransactionOverview/__jest__/TransactionOverview.test.tsx b/x-pack/plugins/apm/public/components/app/TransactionOverview/TransactionOverview.test.tsx similarity index 70% rename from x-pack/plugins/apm/public/components/app/TransactionOverview/__jest__/TransactionOverview.test.tsx rename to x-pack/plugins/apm/public/components/app/TransactionOverview/TransactionOverview.test.tsx index 9c514e429c374..28030dd509835 100644 --- a/x-pack/plugins/apm/public/components/app/TransactionOverview/__jest__/TransactionOverview.test.tsx +++ b/x-pack/plugins/apm/public/components/app/TransactionOverview/TransactionOverview.test.tsx @@ -4,25 +4,23 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; import { + fireEvent, + getByText, queryByLabelText, render, - getByText, - getByDisplayValue, - queryByDisplayValue, - fireEvent, } from '@testing-library/react'; import { omit } from 'lodash'; -import { history } from '../../../../utils/history'; -import { TransactionOverview } from '..'; -import { IUrlParams } from '../../../../context/UrlParamsContext/types'; -import * as useServiceTransactionTypesHook from '../../../../hooks/useServiceTransactionTypes'; -import * as useFetcherHook from '../../../../hooks/useFetcher'; -import { fromQuery } from '../../../shared/Links/url_helpers'; +import React from 'react'; import { Router } from 'react-router-dom'; -import { UrlParamsProvider } from '../../../../context/UrlParamsContext'; -import { MockApmPluginContextWrapper } from '../../../../context/ApmPluginContext/MockApmPluginContext'; +import { TransactionOverview } from './'; +import { MockApmPluginContextWrapper } from '../../../context/ApmPluginContext/MockApmPluginContext'; +import { UrlParamsProvider } from '../../../context/UrlParamsContext'; +import { IUrlParams } from '../../../context/UrlParamsContext/types'; +import * as useFetcherHook from '../../../hooks/useFetcher'; +import * as useServiceTransactionTypesHook from '../../../hooks/useServiceTransactionTypes'; +import { history } from '../../../utils/history'; +import { fromQuery } from '../../shared/Links/url_helpers'; jest.spyOn(history, 'push'); jest.spyOn(history, 'replace'); @@ -85,7 +83,7 @@ describe('TransactionOverview', () => { const FILTER_BY_TYPE_LABEL = 'Transaction type'; describe('when transactionType is selected and multiple transaction types are given', () => { - it('should render dropdown with transaction types', () => { + it('renders a radio group with transaction types', () => { const { container } = setup({ serviceTransactionTypes: ['firstType', 'secondType'], urlParams: { @@ -94,9 +92,8 @@ describe('TransactionOverview', () => { }, }); - // secondType is selected in the dropdown - expect(queryByDisplayValue(container, 'secondType')).not.toBeNull(); - expect(queryByDisplayValue(container, 'firstType')).toBeNull(); + expect(getByText(container, 'firstType')).toBeInTheDocument(); + expect(getByText(container, 'secondType')).toBeInTheDocument(); expect(getByText(container, 'firstType')).not.toBeNull(); }); @@ -110,22 +107,19 @@ describe('TransactionOverview', () => { }, }); - expect(queryByDisplayValue(container, 'firstType')).toBeNull(); + expect(history.location.search).toEqual('?transactionType=secondType'); + expect(getByText(container, 'firstType')).toBeInTheDocument(); + expect(getByText(container, 'secondType')).toBeInTheDocument(); - fireEvent.change(getByDisplayValue(container, 'secondType'), { - target: { value: 'firstType' }, - }); + fireEvent.click(getByText(container, 'firstType')); expect(history.push).toHaveBeenCalled(); - - getByDisplayValue(container, 'firstType'); - - expect(queryByDisplayValue(container, 'firstType')).not.toBeNull(); + expect(history.location.search).toEqual('?transactionType=firstType'); }); }); describe('when a transaction type is selected, and there are no other transaction types', () => { - it('should not render a dropdown with transaction types', () => { + it('does not render a radio group with transaction types', () => { const { container } = setup({ serviceTransactionTypes: ['firstType'], urlParams: { diff --git a/x-pack/plugins/apm/public/components/app/TransactionOverview/index.tsx b/x-pack/plugins/apm/public/components/app/TransactionOverview/index.tsx index d9bd3e59d281f..f6eb131a8a733 100644 --- a/x-pack/plugins/apm/public/components/app/TransactionOverview/index.tsx +++ b/x-pack/plugins/apm/public/components/app/TransactionOverview/index.tsx @@ -121,7 +121,7 @@ export function TransactionOverview() { - + diff --git a/x-pack/plugins/apm/public/components/shared/LocalUIFilters/Filter/FilterBadgeList.tsx b/x-pack/plugins/apm/public/components/shared/LocalUIFilters/Filter/FilterBadgeList.tsx index 2090a92bf0de4..ed8d865d2d288 100644 --- a/x-pack/plugins/apm/public/components/shared/LocalUIFilters/Filter/FilterBadgeList.tsx +++ b/x-pack/plugins/apm/public/components/shared/LocalUIFilters/Filter/FilterBadgeList.tsx @@ -5,8 +5,9 @@ */ import React from 'react'; -import { EuiFlexGrid, EuiFlexItem, EuiBadge, EuiIcon } from '@elastic/eui'; +import { EuiFlexGrid, EuiFlexItem, EuiBadge } from '@elastic/eui'; import styled from 'styled-components'; +import { i18n } from '@kbn/i18n'; import { unit, px, truncate } from '../../../../style/variables'; const BadgeText = styled.div` @@ -20,22 +21,31 @@ interface Props { onRemove: (val: string) => void; } +const removeFilterLabel = i18n.translate( + 'xpack.apm.uifilter.badge.removeFilter', + { defaultMessage: 'Remove filter' } +); + function FilterBadgeList({ onRemove, value }: Props) { return ( {value.map((val) => ( - + {val} + ))} diff --git a/x-pack/plugins/apm/public/components/shared/LocalUIFilters/Filter/index.tsx b/x-pack/plugins/apm/public/components/shared/LocalUIFilters/Filter/index.tsx index c13439a3c5928..48ebc2add0053 100644 --- a/x-pack/plugins/apm/public/components/shared/LocalUIFilters/Filter/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/LocalUIFilters/Filter/index.tsx @@ -164,7 +164,7 @@ function Filter({ name, title, options, onChange, value, showCount }: Props) { }} value={value} /> - + ) : null} diff --git a/x-pack/plugins/apm/public/components/shared/LocalUIFilters/TransactionTypeFilter/index.tsx b/x-pack/plugins/apm/public/components/shared/LocalUIFilters/TransactionTypeFilter/index.tsx index afd2d023d16ba..54a08e9d45af5 100644 --- a/x-pack/plugins/apm/public/components/shared/LocalUIFilters/TransactionTypeFilter/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/LocalUIFilters/TransactionTypeFilter/index.tsx @@ -9,7 +9,7 @@ import { EuiTitle, EuiHorizontalRule, EuiSpacer, - EuiSelect, + EuiRadioGroup, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { useUrlParams } from '../../../../hooks/useUrlParams'; @@ -26,8 +26,8 @@ function TransactionTypeFilter({ transactionTypes }: Props) { } = useUrlParams(); const options = transactionTypes.map((type) => ({ - text: type, - value: type, + id: type, + label: type, })); return ( @@ -42,16 +42,15 @@ function TransactionTypeFilter({ transactionTypes }: Props) { - { + idSelected={transactionType} + onChange={(selectedTransactionType) => { const newLocation = { ...history.location, search: fromQuery({ ...toQuery(history.location.search), - transactionType: event.target.value, + transactionType: selectedTransactionType, }), }; history.push(newLocation); From c3b6745e3db705a9e14f4b951d967e9880bdd116 Mon Sep 17 00:00:00 2001 From: James Rodewig <40268737+jrodewig@users.noreply.github.com> Date: Tue, 25 Aug 2020 10:29:57 -0400 Subject: [PATCH 40/71] Correct punctuation for ingest processors help text (#75695) --- .../processors/common_fields/ignore_missing_field.tsx | 2 +- .../manage_processor_form/processors/date_index_name.tsx | 2 +- .../components/manage_processor_form/processors/enrich.tsx | 6 +++--- .../components/manage_processor_form/processors/fail.tsx | 2 +- .../components/manage_processor_form/processors/foreach.tsx | 4 ++-- .../components/manage_processor_form/processors/geoip.tsx | 4 ++-- .../components/manage_processor_form/processors/grok.tsx | 4 ++-- .../components/manage_processor_form/processors/gsub.tsx | 6 +++--- .../manage_processor_form/processors/html_strip.tsx | 2 +- .../manage_processor_form/processors/inference.tsx | 2 +- .../components/manage_processor_form/processors/join.tsx | 4 ++-- .../components/manage_processor_form/processors/json.tsx | 2 +- 12 files changed, 20 insertions(+), 20 deletions(-) diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/common_fields/ignore_missing_field.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/common_fields/ignore_missing_field.tsx index 35dd462d88425..63ebb47dfc573 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/common_fields/ignore_missing_field.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/common_fields/ignore_missing_field.tsx @@ -32,7 +32,7 @@ export const fieldsConfig: FieldsConfig = { helpText: ( {'field'}, }} diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/date_index_name.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/date_index_name.tsx index 2a278a251c30f..8cbc064c1c90c 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/date_index_name.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/date_index_name.tsx @@ -133,7 +133,7 @@ const fieldsConfig: FieldsConfig = { helpText: ( {'ENGLISH'} }} /> ), diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/enrich.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/enrich.tsx index 31eac38222afb..5986374b338cf 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/enrich.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/enrich.tsx @@ -157,7 +157,7 @@ export const Enrich: FunctionComponent = () => { helpText: ( @@ -182,7 +182,7 @@ export const Enrich: FunctionComponent = () => { helpText={i18n.translate( 'xpack.ingestPipelines.pipelineEditor.enrichForm.targetFieldHelpText', { - defaultMessage: 'Field used to contain enrich data', + defaultMessage: 'Field used to contain enrich data.', } )} validations={[targetFieldValidator]} @@ -202,7 +202,7 @@ export const Enrich: FunctionComponent = () => { helpText: ( { diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/geoip.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/geoip.tsx index ef2aa62c4a7de..9bb1d679938ed 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/geoip.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/geoip.tsx @@ -80,7 +80,7 @@ export const GeoIP: FunctionComponent = () => { @@ -88,7 +88,7 @@ export const GeoIP: FunctionComponent = () => { helpText={i18n.translate( 'xpack.ingestPipelines.pipelineEditor.geoIPForm.targetFieldHelpText', { - defaultMessage: 'Field used to contain geo data properties', + defaultMessage: 'Field used to contain geo data properties.', } )} /> diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/grok.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/grok.tsx index 1ed9898149a67..d021038fda94f 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/grok.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/grok.tsx @@ -87,7 +87,7 @@ const fieldsConfig: FieldsConfig = { helpText: i18n.translate( 'xpack.ingestPipelines.pipelineEditor.grokForm.traceMatchFieldHelpText', { - defaultMessage: 'Add metadata about the matching expression to the document', + defaultMessage: 'Add metadata about the matching expression to the document.', } ), }, @@ -99,7 +99,7 @@ export const Grok: FunctionComponent = () => { diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/gsub.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/gsub.tsx index 4d3445d469da2..a0bda245d667b 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/gsub.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/gsub.tsx @@ -29,7 +29,7 @@ const fieldsConfig: FieldsConfig = { }), deserializer: String, helpText: i18n.translate('xpack.ingestPipelines.pipelineEditor.gsubForm.patternFieldHelpText', { - defaultMessage: 'Regular expression used to match substrings in the field', + defaultMessage: 'Regular expression used to match substrings in the field.', }), validations: [ { @@ -49,7 +49,7 @@ const fieldsConfig: FieldsConfig = { }), helpText: i18n.translate( 'xpack.ingestPipelines.pipelineEditor.gsubForm.replacementFieldHelpText', - { defaultMessage: 'Replacement text for matches' } + { defaultMessage: 'Replacement text for matches.' } ), validations: [ { @@ -69,7 +69,7 @@ export const Gsub: FunctionComponent = () => { diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/html_strip.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/html_strip.tsx index c6ca7df4cc3e7..fb1a2d97672b0 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/html_strip.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/html_strip.tsx @@ -19,7 +19,7 @@ export const HtmlStrip: FunctionComponent = () => { diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/inference.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/inference.tsx index ee8d7cc55a9f1..68281fc11f340 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/inference.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/inference.tsx @@ -82,7 +82,7 @@ const fieldsConfig: FieldsConfig = { helpText: i18n.translate( 'xpack.ingestPipelines.pipelineEditor.inferenceForm.modelIDFieldHelpText', { - defaultMessage: 'ID of the model to infer against', + defaultMessage: 'ID of the model to infer against.', } ), validations: [ diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/join.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/join.tsx index 712d0106459b1..c35a5b463f573 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/join.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/join.tsx @@ -28,7 +28,7 @@ const fieldsConfig: FieldsConfig = { helpText: i18n.translate( 'xpack.ingestPipelines.pipelineEditor.joinForm.separatorFieldHelpText', { - defaultMessage: 'Separator character', + defaultMessage: 'Separator character.', } ), validations: [ @@ -49,7 +49,7 @@ export const Join: FunctionComponent = () => { diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/json.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/json.tsx index 9d62c67460136..5c4c53b65b6dc 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/json.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/json.tsx @@ -61,7 +61,7 @@ export const Json: FunctionComponent = () => { From 75232a74f3be93135ac9f1be959b2890301ad662 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cau=C3=AA=20Marcondes?= <55978943+cauemarcondes@users.noreply.github.com> Date: Tue, 25 Aug 2020 15:39:57 +0100 Subject: [PATCH 41/71] [APM] Implement nest level expand/collapse toggle for each span row (#75259) * returning an waterfallTransaction * fixing style * fixing unit test * fixing style * addressing PR comment * addressing PR comment Co-authored-by: Elastic Machine --- .../Waterfall/WaterfallItem.tsx | 6 +- .../Waterfall/accordion_waterfall.tsx | 170 ++ .../WaterfallContainer/Waterfall/index.tsx | 121 +- .../waterfall_helpers.test.ts.snap | 1949 +++++++++++++---- .../waterfall_helpers/waterfall_helpers.ts | 9 +- .../WaterfallContainer/index.tsx | 2 +- .../WaterfallWithSummmary/index.tsx | 6 +- 7 files changed, 1759 insertions(+), 504 deletions(-) create mode 100644 x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/accordion_waterfall.tsx diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/WaterfallItem.tsx b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/WaterfallItem.tsx index a4d42bcf51d01..e1b5ffcd0e0f5 100644 --- a/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/WaterfallItem.tsx +++ b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/WaterfallItem.tsx @@ -40,7 +40,6 @@ const Container = styled.div` padding-bottom: ${px(units.plus)}; margin-right: ${(props) => px(props.timelineMargins.right)}; margin-left: ${(props) => px(props.timelineMargins.left)}; - border-top: 1px solid ${({ theme }) => theme.eui.euiColorLightShade}; background-color: ${({ isSelected, theme }) => isSelected ? theme.eui.euiColorLightestShade : 'initial'}; cursor: pointer; @@ -191,7 +190,10 @@ export function WaterfallItem({ type={item.docType} timelineMargins={timelineMargins} isSelected={isSelected} - onClick={onClick} + onClick={(e: React.MouseEvent) => { + e.stopPropagation(); + onClick(); + }} > ; + onToggleEntryTransaction?: ( + nextState: EuiAccordionProps['forceState'] + ) => void; + timelineMargins: Margins; + onClickWaterfallItem: (item: IWaterfallItem) => void; +} + +const StyledAccordion = styled(EuiAccordion).withConfig({ + shouldForwardProp: (prop) => + !['childrenCount', 'marginLeftLevel', 'hasError'].includes(prop), +})< + EuiAccordionProps & { + childrenCount: number; + marginLeftLevel: number; + hasError: boolean; + } +>` + .euiAccordion { + border-top: 1px solid ${({ theme }) => theme.eui.euiColorLightShade}; + } + .euiIEFlexWrapFix { + width: 100%; + height: 48px; + } + .euiAccordion__childWrapper { + transition: none; + } + + .euiAccordion__padding--l { + padding-top: 0; + padding-bottom: 0; + } + + .euiAccordion__iconWrapper { + display: flex; + position: relative; + &:after { + content: ${(props) => `'${props.childrenCount}'`}; + position: absolute; + left: 20px; + top: -1px; + z-index: 1; + font-size: ${({ theme }) => theme.eui.euiFontSizeXS}; + } + } + + ${(props) => { + const borderLeft = props.hasError + ? `2px solid ${props.theme.eui.euiColorDanger};` + : `1px solid ${props.theme.eui.euiColorLightShade};`; + return `.button_${props.id} { + margin-left: ${props.marginLeftLevel}px; + border-left: ${borderLeft} + &:hover { + background-color: ${props.theme.eui.euiColorLightestShade}; + } + }`; + // + }} +`; + +const WaterfallItemContainer = styled.div` + position: absolute; + width: 100%; + left: 0; +`; + +export function AccordionWaterfall(props: AccordionWaterfallProps) { + const [isOpen, setIsOpen] = useState(props.isOpen); + + const { + item, + level, + serviceColors, + duration, + childrenByParentId, + waterfallItemId, + location, + errorsPerTransaction, + timelineMargins, + onClickWaterfallItem, + } = props; + + const nextLevel = level + 1; + + const errorCount = + item.docType === 'transaction' + ? errorsPerTransaction[item.doc.transaction.id] + : 0; + + const children = childrenByParentId[item.id] || []; + + // To indent the items creating the parent/child tree + const marginLeftLevel = 8 * level; + + return ( + 0} + marginLeftLevel={marginLeftLevel} + childrenCount={children.length} + buttonContent={ + + { + onClickWaterfallItem(item); + }} + /> + + } + arrowDisplay={isEmpty(children) ? 'none' : 'left'} + initialIsOpen={true} + forceState={isOpen ? 'open' : 'closed'} + onToggle={() => setIsOpen((isCurrentOpen) => !isCurrentOpen)} + > + {children.map((child) => ( + + ))} + + ); +} diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/index.tsx b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/index.tsx index 1fd0ec761b1ae..7daf1b798749b 100644 --- a/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/index.tsx +++ b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/index.tsx @@ -4,21 +4,22 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiCallOut } from '@elastic/eui'; +import { EuiButtonEmpty, EuiCallOut } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { Location } from 'history'; -import React from 'react'; +import React, { useState } from 'react'; // @ts-ignore import { StickyContainer } from 'react-sticky'; import styled from 'styled-components'; import { px } from '../../../../../../style/variables'; import { history } from '../../../../../../utils/history'; import { Timeline } from '../../../../../shared/charts/Timeline'; +import { HeightRetainer } from '../../../../../shared/HeightRetainer'; import { fromQuery, toQuery } from '../../../../../shared/Links/url_helpers'; import { getAgentMarks } from '../Marks/get_agent_marks'; import { getErrorMarks } from '../Marks/get_error_marks'; +import { AccordionWaterfall } from './accordion_waterfall'; import { WaterfallFlyout } from './WaterfallFlyout'; -import { WaterfallItem } from './WaterfallItem'; import { IWaterfall, IWaterfallItem, @@ -32,7 +33,7 @@ const Container = styled.div` const TIMELINE_MARGINS = { top: 40, - left: 50, + left: 100, right: 50, bottom: 0, }; @@ -58,6 +59,7 @@ const WaterfallItemsContainer = styled.div<{ paddingTop: number; }>` padding-top: ${(props) => px(props.paddingTop)}; + border-bottom: 1px solid ${({ theme }) => theme.eui.euiColorMediumShade}; `; interface Props { @@ -66,72 +68,91 @@ interface Props { location: Location; exceedsMax: boolean; } - export function Waterfall({ waterfall, exceedsMax, waterfallItemId, location, }: Props) { + const [isAccordionOpen, setIsAccordionOpen] = useState(true); const itemContainerHeight = 58; // TODO: This is a nasty way to calculate the height of the svg element. A better approach should be found const waterfallHeight = itemContainerHeight * waterfall.items.length; const { serviceColors, duration } = waterfall; - const agentMarks = getAgentMarks(waterfall.entryTransaction); + const agentMarks = getAgentMarks(waterfall.entryWaterfallTransaction?.doc); const errorMarks = getErrorMarks(waterfall.errorItems, serviceColors); - function renderWaterfallItem(item: IWaterfallItem) { - const errorCount = - item.docType === 'transaction' - ? waterfall.errorsPerTransaction[item.doc.transaction.id] - : 0; - + function renderItems( + childrenByParentId: Record + ) { + const { entryWaterfallTransaction } = waterfall; + if (!entryWaterfallTransaction) { + return null; + } return ( - toggleFlyout({ item, location })} + onClickWaterfallItem={(item: IWaterfallItem) => + toggleFlyout({ item, location }) + } /> ); } return ( - - {exceedsMax && ( - - )} - - - - {waterfall.items.map(renderWaterfallItem)} - - + + + {exceedsMax && ( + + )} + +
+ { + setIsAccordionOpen((isOpen) => !isOpen); + }} + /> + +
+ + {renderItems(waterfall.childrenByParentId)} + +
- -
+ +
+ ); } diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/__snapshots__/waterfall_helpers.test.ts.snap b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/__snapshots__/waterfall_helpers.test.ts.snap index c9b29e8692f44..204c5e9ae6da2 100644 --- a/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/__snapshots__/waterfall_helpers.test.ts.snap +++ b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/__snapshots__/waterfall_helpers.test.ts.snap @@ -2,27 +2,734 @@ exports[`waterfall_helpers getWaterfall should return full waterfall 1`] = ` Object { + "childrenByParentId": Object { + "mySpanIdA": Array [ + Object { + "doc": Object { + "parent": Object { + "id": "mySpanIdA", + }, + "processor": Object { + "event": "span", + }, + "service": Object { + "name": "opbeans-ruby", + }, + "span": Object { + "duration": Object { + "us": 481, + }, + "id": "mySpanIdB", + "name": "SELECT FROM products", + }, + "timestamp": Object { + "us": 1549324795825633, + }, + "trace": Object { + "id": "myTraceId", + }, + "transaction": Object { + "id": "myTransactionId2", + }, + }, + "docType": "span", + "duration": 481, + "id": "mySpanIdB", + "offset": 41627, + "parent": Object { + "doc": Object { + "parent": Object { + "id": "myTransactionId2", + }, + "processor": Object { + "event": "span", + }, + "service": Object { + "name": "opbeans-ruby", + }, + "span": Object { + "duration": Object { + "us": 6161, + }, + "id": "mySpanIdA", + "name": "Api::ProductsController#index", + }, + "timestamp": Object { + "us": 1549324795824504, + }, + "trace": Object { + "id": "myTraceId", + }, + "transaction": Object { + "id": "myTransactionId2", + }, + }, + "docType": "span", + "duration": 6161, + "id": "mySpanIdA", + "offset": 40498, + "parent": Object { + "doc": Object { + "parent": Object { + "id": "mySpanIdD", + }, + "processor": Object { + "event": "transaction", + }, + "service": Object { + "name": "opbeans-ruby", + }, + "timestamp": Object { + "us": 1549324795823304, + }, + "trace": Object { + "id": "myTraceId", + }, + "transaction": Object { + "duration": Object { + "us": 8634, + }, + "id": "myTransactionId2", + "marks": Object { + "agent": Object { + "domComplete": 383, + "domInteractive": 382, + "timeToFirstByte": 14, + }, + }, + "name": "Api::ProductsController#index", + }, + }, + "docType": "transaction", + "duration": 8634, + "id": "myTransactionId2", + "offset": 39298, + "parent": Object { + "doc": Object { + "parent": Object { + "id": "myTransactionId1", + }, + "processor": Object { + "event": "span", + }, + "service": Object { + "name": "opbeans-node", + }, + "span": Object { + "duration": Object { + "us": 47557, + }, + "id": "mySpanIdD", + "name": "GET opbeans-ruby:3000/api/products", + }, + "timestamp": Object { + "us": 1549324795785760, + }, + "trace": Object { + "id": "myTraceId", + }, + "transaction": Object { + "id": "myTransactionId1", + }, + }, + "docType": "span", + "duration": 47557, + "id": "mySpanIdD", + "offset": 1754, + "parent": Object { + "doc": Object { + "processor": Object { + "event": "transaction", + }, + "service": Object { + "name": "opbeans-node", + }, + "timestamp": Object { + "us": 1549324795784006, + }, + "trace": Object { + "id": "myTraceId", + }, + "transaction": Object { + "duration": Object { + "us": 49660, + }, + "id": "myTransactionId1", + "name": "GET /api", + }, + }, + "docType": "transaction", + "duration": 49660, + "id": "myTransactionId1", + "offset": 0, + "parent": undefined, + "parentId": undefined, + "skew": 0, + }, + "parentId": "myTransactionId1", + "skew": 0, + }, + "parentId": "mySpanIdD", + "skew": 0, + }, + "parentId": "myTransactionId2", + "skew": 0, + }, + "parentId": "mySpanIdA", + "skew": 0, + }, + Object { + "doc": Object { + "parent": Object { + "id": "mySpanIdA", + }, + "processor": Object { + "event": "span", + }, + "service": Object { + "name": "opbeans-ruby", + }, + "span": Object { + "duration": Object { + "us": 532, + }, + "id": "mySpanIdC", + "name": "SELECT FROM product", + }, + "timestamp": Object { + "us": 1549324795827905, + }, + "trace": Object { + "id": "myTraceId", + }, + "transaction": Object { + "id": "myTransactionId2", + }, + }, + "docType": "span", + "duration": 532, + "id": "mySpanIdC", + "offset": 43899, + "parent": Object { + "doc": Object { + "parent": Object { + "id": "myTransactionId2", + }, + "processor": Object { + "event": "span", + }, + "service": Object { + "name": "opbeans-ruby", + }, + "span": Object { + "duration": Object { + "us": 6161, + }, + "id": "mySpanIdA", + "name": "Api::ProductsController#index", + }, + "timestamp": Object { + "us": 1549324795824504, + }, + "trace": Object { + "id": "myTraceId", + }, + "transaction": Object { + "id": "myTransactionId2", + }, + }, + "docType": "span", + "duration": 6161, + "id": "mySpanIdA", + "offset": 40498, + "parent": Object { + "doc": Object { + "parent": Object { + "id": "mySpanIdD", + }, + "processor": Object { + "event": "transaction", + }, + "service": Object { + "name": "opbeans-ruby", + }, + "timestamp": Object { + "us": 1549324795823304, + }, + "trace": Object { + "id": "myTraceId", + }, + "transaction": Object { + "duration": Object { + "us": 8634, + }, + "id": "myTransactionId2", + "marks": Object { + "agent": Object { + "domComplete": 383, + "domInteractive": 382, + "timeToFirstByte": 14, + }, + }, + "name": "Api::ProductsController#index", + }, + }, + "docType": "transaction", + "duration": 8634, + "id": "myTransactionId2", + "offset": 39298, + "parent": Object { + "doc": Object { + "parent": Object { + "id": "myTransactionId1", + }, + "processor": Object { + "event": "span", + }, + "service": Object { + "name": "opbeans-node", + }, + "span": Object { + "duration": Object { + "us": 47557, + }, + "id": "mySpanIdD", + "name": "GET opbeans-ruby:3000/api/products", + }, + "timestamp": Object { + "us": 1549324795785760, + }, + "trace": Object { + "id": "myTraceId", + }, + "transaction": Object { + "id": "myTransactionId1", + }, + }, + "docType": "span", + "duration": 47557, + "id": "mySpanIdD", + "offset": 1754, + "parent": Object { + "doc": Object { + "processor": Object { + "event": "transaction", + }, + "service": Object { + "name": "opbeans-node", + }, + "timestamp": Object { + "us": 1549324795784006, + }, + "trace": Object { + "id": "myTraceId", + }, + "transaction": Object { + "duration": Object { + "us": 49660, + }, + "id": "myTransactionId1", + "name": "GET /api", + }, + }, + "docType": "transaction", + "duration": 49660, + "id": "myTransactionId1", + "offset": 0, + "parent": undefined, + "parentId": undefined, + "skew": 0, + }, + "parentId": "myTransactionId1", + "skew": 0, + }, + "parentId": "mySpanIdD", + "skew": 0, + }, + "parentId": "myTransactionId2", + "skew": 0, + }, + "parentId": "mySpanIdA", + "skew": 0, + }, + ], + "mySpanIdD": Array [ + Object { + "doc": Object { + "parent": Object { + "id": "mySpanIdD", + }, + "processor": Object { + "event": "transaction", + }, + "service": Object { + "name": "opbeans-ruby", + }, + "timestamp": Object { + "us": 1549324795823304, + }, + "trace": Object { + "id": "myTraceId", + }, + "transaction": Object { + "duration": Object { + "us": 8634, + }, + "id": "myTransactionId2", + "marks": Object { + "agent": Object { + "domComplete": 383, + "domInteractive": 382, + "timeToFirstByte": 14, + }, + }, + "name": "Api::ProductsController#index", + }, + }, + "docType": "transaction", + "duration": 8634, + "id": "myTransactionId2", + "offset": 39298, + "parent": Object { + "doc": Object { + "parent": Object { + "id": "myTransactionId1", + }, + "processor": Object { + "event": "span", + }, + "service": Object { + "name": "opbeans-node", + }, + "span": Object { + "duration": Object { + "us": 47557, + }, + "id": "mySpanIdD", + "name": "GET opbeans-ruby:3000/api/products", + }, + "timestamp": Object { + "us": 1549324795785760, + }, + "trace": Object { + "id": "myTraceId", + }, + "transaction": Object { + "id": "myTransactionId1", + }, + }, + "docType": "span", + "duration": 47557, + "id": "mySpanIdD", + "offset": 1754, + "parent": Object { + "doc": Object { + "processor": Object { + "event": "transaction", + }, + "service": Object { + "name": "opbeans-node", + }, + "timestamp": Object { + "us": 1549324795784006, + }, + "trace": Object { + "id": "myTraceId", + }, + "transaction": Object { + "duration": Object { + "us": 49660, + }, + "id": "myTransactionId1", + "name": "GET /api", + }, + }, + "docType": "transaction", + "duration": 49660, + "id": "myTransactionId1", + "offset": 0, + "parent": undefined, + "parentId": undefined, + "skew": 0, + }, + "parentId": "myTransactionId1", + "skew": 0, + }, + "parentId": "mySpanIdD", + "skew": 0, + }, + ], + "myTransactionId1": Array [ + Object { + "doc": Object { + "parent": Object { + "id": "myTransactionId1", + }, + "processor": Object { + "event": "span", + }, + "service": Object { + "name": "opbeans-node", + }, + "span": Object { + "duration": Object { + "us": 47557, + }, + "id": "mySpanIdD", + "name": "GET opbeans-ruby:3000/api/products", + }, + "timestamp": Object { + "us": 1549324795785760, + }, + "trace": Object { + "id": "myTraceId", + }, + "transaction": Object { + "id": "myTransactionId1", + }, + }, + "docType": "span", + "duration": 47557, + "id": "mySpanIdD", + "offset": 1754, + "parent": Object { + "doc": Object { + "processor": Object { + "event": "transaction", + }, + "service": Object { + "name": "opbeans-node", + }, + "timestamp": Object { + "us": 1549324795784006, + }, + "trace": Object { + "id": "myTraceId", + }, + "transaction": Object { + "duration": Object { + "us": 49660, + }, + "id": "myTransactionId1", + "name": "GET /api", + }, + }, + "docType": "transaction", + "duration": 49660, + "id": "myTransactionId1", + "offset": 0, + "parent": undefined, + "parentId": undefined, + "skew": 0, + }, + "parentId": "myTransactionId1", + "skew": 0, + }, + ], + "myTransactionId2": Array [ + Object { + "doc": Object { + "parent": Object { + "id": "myTransactionId2", + }, + "processor": Object { + "event": "span", + }, + "service": Object { + "name": "opbeans-ruby", + }, + "span": Object { + "duration": Object { + "us": 6161, + }, + "id": "mySpanIdA", + "name": "Api::ProductsController#index", + }, + "timestamp": Object { + "us": 1549324795824504, + }, + "trace": Object { + "id": "myTraceId", + }, + "transaction": Object { + "id": "myTransactionId2", + }, + }, + "docType": "span", + "duration": 6161, + "id": "mySpanIdA", + "offset": 40498, + "parent": Object { + "doc": Object { + "parent": Object { + "id": "mySpanIdD", + }, + "processor": Object { + "event": "transaction", + }, + "service": Object { + "name": "opbeans-ruby", + }, + "timestamp": Object { + "us": 1549324795823304, + }, + "trace": Object { + "id": "myTraceId", + }, + "transaction": Object { + "duration": Object { + "us": 8634, + }, + "id": "myTransactionId2", + "marks": Object { + "agent": Object { + "domComplete": 383, + "domInteractive": 382, + "timeToFirstByte": 14, + }, + }, + "name": "Api::ProductsController#index", + }, + }, + "docType": "transaction", + "duration": 8634, + "id": "myTransactionId2", + "offset": 39298, + "parent": Object { + "doc": Object { + "parent": Object { + "id": "myTransactionId1", + }, + "processor": Object { + "event": "span", + }, + "service": Object { + "name": "opbeans-node", + }, + "span": Object { + "duration": Object { + "us": 47557, + }, + "id": "mySpanIdD", + "name": "GET opbeans-ruby:3000/api/products", + }, + "timestamp": Object { + "us": 1549324795785760, + }, + "trace": Object { + "id": "myTraceId", + }, + "transaction": Object { + "id": "myTransactionId1", + }, + }, + "docType": "span", + "duration": 47557, + "id": "mySpanIdD", + "offset": 1754, + "parent": Object { + "doc": Object { + "processor": Object { + "event": "transaction", + }, + "service": Object { + "name": "opbeans-node", + }, + "timestamp": Object { + "us": 1549324795784006, + }, + "trace": Object { + "id": "myTraceId", + }, + "transaction": Object { + "duration": Object { + "us": 49660, + }, + "id": "myTransactionId1", + "name": "GET /api", + }, + }, + "docType": "transaction", + "duration": 49660, + "id": "myTransactionId1", + "offset": 0, + "parent": undefined, + "parentId": undefined, + "skew": 0, + }, + "parentId": "myTransactionId1", + "skew": 0, + }, + "parentId": "mySpanIdD", + "skew": 0, + }, + "parentId": "myTransactionId2", + "skew": 0, + }, + ], + "root": Array [ + Object { + "doc": Object { + "processor": Object { + "event": "transaction", + }, + "service": Object { + "name": "opbeans-node", + }, + "timestamp": Object { + "us": 1549324795784006, + }, + "trace": Object { + "id": "myTraceId", + }, + "transaction": Object { + "duration": Object { + "us": 49660, + }, + "id": "myTransactionId1", + "name": "GET /api", + }, + }, + "docType": "transaction", + "duration": 49660, + "id": "myTransactionId1", + "offset": 0, + "parent": undefined, + "parentId": undefined, + "skew": 0, + }, + ], + }, "duration": 49660, - "entryTransaction": Object { - "processor": Object { - "event": "transaction", - }, - "service": Object { - "name": "opbeans-node", - }, - "timestamp": Object { - "us": 1549324795784006, - }, - "trace": Object { - "id": "myTraceId", - }, - "transaction": Object { - "duration": Object { - "us": 49660, + "entryWaterfallTransaction": Object { + "doc": Object { + "processor": Object { + "event": "transaction", + }, + "service": Object { + "name": "opbeans-node", + }, + "timestamp": Object { + "us": 1549324795784006, + }, + "trace": Object { + "id": "myTraceId", + }, + "transaction": Object { + "duration": Object { + "us": 49660, + }, + "id": "myTransactionId1", + "name": "GET /api", }, - "id": "myTransactionId1", - "name": "GET /api", }, + "docType": "transaction", + "duration": 49660, + "id": "myTransactionId1", + "offset": 0, + "parent": undefined, + "parentId": undefined, + "skew": 0, }, "errorItems": Array [ Object { @@ -42,13 +749,115 @@ Object { "id": "myTransactionId1", }, "processor": Object { - "event": "error", + "event": "error", + }, + "service": Object { + "name": "opbeans-ruby", + }, + "timestamp": Object { + "us": 1549324795810000, + }, + "trace": Object { + "id": "myTraceId", + }, + "transaction": Object { + "id": "myTransactionId1", + }, + }, + "docType": "error", + "duration": 0, + "id": "error1", + "offset": 25994, + "parent": Object { + "doc": Object { + "processor": Object { + "event": "transaction", + }, + "service": Object { + "name": "opbeans-node", + }, + "timestamp": Object { + "us": 1549324795784006, + }, + "trace": Object { + "id": "myTraceId", + }, + "transaction": Object { + "duration": Object { + "us": 49660, + }, + "id": "myTransactionId1", + "name": "GET /api", + }, + }, + "docType": "transaction", + "duration": 49660, + "id": "myTransactionId1", + "offset": 0, + "parent": undefined, + "parentId": undefined, + "skew": 0, + }, + "parentId": "myTransactionId1", + "skew": 0, + }, + ], + "errorsCount": 1, + "errorsPerTransaction": Object { + "myTransactionId1": 2, + "myTransactionId2": 3, + }, + "items": Array [ + Object { + "doc": Object { + "processor": Object { + "event": "transaction", + }, + "service": Object { + "name": "opbeans-node", + }, + "timestamp": Object { + "us": 1549324795784006, + }, + "trace": Object { + "id": "myTraceId", + }, + "transaction": Object { + "duration": Object { + "us": 49660, + }, + "id": "myTransactionId1", + "name": "GET /api", + }, + }, + "docType": "transaction", + "duration": 49660, + "id": "myTransactionId1", + "offset": 0, + "parent": undefined, + "parentId": undefined, + "skew": 0, + }, + Object { + "doc": Object { + "parent": Object { + "id": "myTransactionId1", + }, + "processor": Object { + "event": "span", }, "service": Object { - "name": "opbeans-ruby", + "name": "opbeans-node", + }, + "span": Object { + "duration": Object { + "us": 47557, + }, + "id": "mySpanIdD", + "name": "GET opbeans-ruby:3000/api/products", }, "timestamp": Object { - "us": 1549324795810000, + "us": 1549324795785760, }, "trace": Object { "id": "myTraceId", @@ -57,10 +866,10 @@ Object { "id": "myTransactionId1", }, }, - "docType": "error", - "duration": 0, - "id": "error1", - "offset": 25994, + "docType": "span", + "duration": 47557, + "id": "mySpanIdD", + "offset": 1754, "parent": Object { "doc": Object { "processor": Object { @@ -94,387 +903,744 @@ Object { "parentId": "myTransactionId1", "skew": 0, }, - ], - "errorsCount": 1, - "errorsPerTransaction": Object { - "myTransactionId1": 2, - "myTransactionId2": 3, - }, - "items": Array [ Object { "doc": Object { + "parent": Object { + "id": "mySpanIdD", + }, "processor": Object { "event": "transaction", }, "service": Object { - "name": "opbeans-node", + "name": "opbeans-ruby", }, "timestamp": Object { - "us": 1549324795784006, + "us": 1549324795823304, }, "trace": Object { "id": "myTraceId", }, "transaction": Object { "duration": Object { - "us": 49660, + "us": 8634, + }, + "id": "myTransactionId2", + "marks": Object { + "agent": Object { + "domComplete": 383, + "domInteractive": 382, + "timeToFirstByte": 14, + }, + }, + "name": "Api::ProductsController#index", + }, + }, + "docType": "transaction", + "duration": 8634, + "id": "myTransactionId2", + "offset": 39298, + "parent": Object { + "doc": Object { + "parent": Object { + "id": "myTransactionId1", + }, + "processor": Object { + "event": "span", + }, + "service": Object { + "name": "opbeans-node", + }, + "span": Object { + "duration": Object { + "us": 47557, + }, + "id": "mySpanIdD", + "name": "GET opbeans-ruby:3000/api/products", + }, + "timestamp": Object { + "us": 1549324795785760, + }, + "trace": Object { + "id": "myTraceId", + }, + "transaction": Object { + "id": "myTransactionId1", + }, + }, + "docType": "span", + "duration": 47557, + "id": "mySpanIdD", + "offset": 1754, + "parent": Object { + "doc": Object { + "processor": Object { + "event": "transaction", + }, + "service": Object { + "name": "opbeans-node", + }, + "timestamp": Object { + "us": 1549324795784006, + }, + "trace": Object { + "id": "myTraceId", + }, + "transaction": Object { + "duration": Object { + "us": 49660, + }, + "id": "myTransactionId1", + "name": "GET /api", + }, }, + "docType": "transaction", + "duration": 49660, "id": "myTransactionId1", - "name": "GET /api", + "offset": 0, + "parent": undefined, + "parentId": undefined, + "skew": 0, + }, + "parentId": "myTransactionId1", + "skew": 0, + }, + "parentId": "mySpanIdD", + "skew": 0, + }, + Object { + "doc": Object { + "parent": Object { + "id": "myTransactionId2", + }, + "processor": Object { + "event": "span", + }, + "service": Object { + "name": "opbeans-ruby", + }, + "span": Object { + "duration": Object { + "us": 6161, + }, + "id": "mySpanIdA", + "name": "Api::ProductsController#index", + }, + "timestamp": Object { + "us": 1549324795824504, + }, + "trace": Object { + "id": "myTraceId", + }, + "transaction": Object { + "id": "myTransactionId2", + }, + }, + "docType": "span", + "duration": 6161, + "id": "mySpanIdA", + "offset": 40498, + "parent": Object { + "doc": Object { + "parent": Object { + "id": "mySpanIdD", + }, + "processor": Object { + "event": "transaction", + }, + "service": Object { + "name": "opbeans-ruby", + }, + "timestamp": Object { + "us": 1549324795823304, + }, + "trace": Object { + "id": "myTraceId", + }, + "transaction": Object { + "duration": Object { + "us": 8634, + }, + "id": "myTransactionId2", + "marks": Object { + "agent": Object { + "domComplete": 383, + "domInteractive": 382, + "timeToFirstByte": 14, + }, + }, + "name": "Api::ProductsController#index", + }, + }, + "docType": "transaction", + "duration": 8634, + "id": "myTransactionId2", + "offset": 39298, + "parent": Object { + "doc": Object { + "parent": Object { + "id": "myTransactionId1", + }, + "processor": Object { + "event": "span", + }, + "service": Object { + "name": "opbeans-node", + }, + "span": Object { + "duration": Object { + "us": 47557, + }, + "id": "mySpanIdD", + "name": "GET opbeans-ruby:3000/api/products", + }, + "timestamp": Object { + "us": 1549324795785760, + }, + "trace": Object { + "id": "myTraceId", + }, + "transaction": Object { + "id": "myTransactionId1", + }, + }, + "docType": "span", + "duration": 47557, + "id": "mySpanIdD", + "offset": 1754, + "parent": Object { + "doc": Object { + "processor": Object { + "event": "transaction", + }, + "service": Object { + "name": "opbeans-node", + }, + "timestamp": Object { + "us": 1549324795784006, + }, + "trace": Object { + "id": "myTraceId", + }, + "transaction": Object { + "duration": Object { + "us": 49660, + }, + "id": "myTransactionId1", + "name": "GET /api", + }, + }, + "docType": "transaction", + "duration": 49660, + "id": "myTransactionId1", + "offset": 0, + "parent": undefined, + "parentId": undefined, + "skew": 0, + }, + "parentId": "myTransactionId1", + "skew": 0, }, + "parentId": "mySpanIdD", + "skew": 0, }, - "docType": "transaction", - "duration": 49660, - "id": "myTransactionId1", - "offset": 0, - "parent": undefined, - "parentId": undefined, + "parentId": "myTransactionId2", "skew": 0, }, Object { "doc": Object { "parent": Object { - "id": "myTransactionId1", + "id": "mySpanIdA", }, "processor": Object { "event": "span", }, "service": Object { - "name": "opbeans-node", + "name": "opbeans-ruby", }, "span": Object { "duration": Object { - "us": 47557, + "us": 481, }, - "id": "mySpanIdD", - "name": "GET opbeans-ruby:3000/api/products", + "id": "mySpanIdB", + "name": "SELECT FROM products", }, "timestamp": Object { - "us": 1549324795785760, + "us": 1549324795825633, }, "trace": Object { "id": "myTraceId", }, "transaction": Object { - "id": "myTransactionId1", + "id": "myTransactionId2", }, }, "docType": "span", - "duration": 47557, - "id": "mySpanIdD", - "offset": 1754, + "duration": 481, + "id": "mySpanIdB", + "offset": 41627, "parent": Object { "doc": Object { + "parent": Object { + "id": "myTransactionId2", + }, "processor": Object { - "event": "transaction", + "event": "span", }, "service": Object { - "name": "opbeans-node", + "name": "opbeans-ruby", + }, + "span": Object { + "duration": Object { + "us": 6161, + }, + "id": "mySpanIdA", + "name": "Api::ProductsController#index", }, "timestamp": Object { - "us": 1549324795784006, + "us": 1549324795824504, }, "trace": Object { "id": "myTraceId", }, "transaction": Object { - "duration": Object { - "us": 49660, + "id": "myTransactionId2", + }, + }, + "docType": "span", + "duration": 6161, + "id": "mySpanIdA", + "offset": 40498, + "parent": Object { + "doc": Object { + "parent": Object { + "id": "mySpanIdD", }, - "id": "myTransactionId1", - "name": "GET /api", + "processor": Object { + "event": "transaction", + }, + "service": Object { + "name": "opbeans-ruby", + }, + "timestamp": Object { + "us": 1549324795823304, + }, + "trace": Object { + "id": "myTraceId", + }, + "transaction": Object { + "duration": Object { + "us": 8634, + }, + "id": "myTransactionId2", + "marks": Object { + "agent": Object { + "domComplete": 383, + "domInteractive": 382, + "timeToFirstByte": 14, + }, + }, + "name": "Api::ProductsController#index", + }, + }, + "docType": "transaction", + "duration": 8634, + "id": "myTransactionId2", + "offset": 39298, + "parent": Object { + "doc": Object { + "parent": Object { + "id": "myTransactionId1", + }, + "processor": Object { + "event": "span", + }, + "service": Object { + "name": "opbeans-node", + }, + "span": Object { + "duration": Object { + "us": 47557, + }, + "id": "mySpanIdD", + "name": "GET opbeans-ruby:3000/api/products", + }, + "timestamp": Object { + "us": 1549324795785760, + }, + "trace": Object { + "id": "myTraceId", + }, + "transaction": Object { + "id": "myTransactionId1", + }, + }, + "docType": "span", + "duration": 47557, + "id": "mySpanIdD", + "offset": 1754, + "parent": Object { + "doc": Object { + "processor": Object { + "event": "transaction", + }, + "service": Object { + "name": "opbeans-node", + }, + "timestamp": Object { + "us": 1549324795784006, + }, + "trace": Object { + "id": "myTraceId", + }, + "transaction": Object { + "duration": Object { + "us": 49660, + }, + "id": "myTransactionId1", + "name": "GET /api", + }, + }, + "docType": "transaction", + "duration": 49660, + "id": "myTransactionId1", + "offset": 0, + "parent": undefined, + "parentId": undefined, + "skew": 0, + }, + "parentId": "myTransactionId1", + "skew": 0, }, + "parentId": "mySpanIdD", + "skew": 0, }, - "docType": "transaction", - "duration": 49660, - "id": "myTransactionId1", - "offset": 0, - "parent": undefined, - "parentId": undefined, + "parentId": "myTransactionId2", "skew": 0, }, - "parentId": "myTransactionId1", + "parentId": "mySpanIdA", "skew": 0, }, Object { "doc": Object { "parent": Object { - "id": "mySpanIdD", + "id": "mySpanIdA", }, "processor": Object { - "event": "transaction", + "event": "span", }, "service": Object { "name": "opbeans-ruby", }, + "span": Object { + "duration": Object { + "us": 532, + }, + "id": "mySpanIdC", + "name": "SELECT FROM product", + }, "timestamp": Object { - "us": 1549324795823304, + "us": 1549324795827905, }, "trace": Object { "id": "myTraceId", }, "transaction": Object { - "duration": Object { - "us": 8634, - }, "id": "myTransactionId2", - "marks": Object { - "agent": Object { - "domComplete": 383, - "domInteractive": 382, - "timeToFirstByte": 14, - }, - }, - "name": "Api::ProductsController#index", }, }, - "docType": "transaction", - "duration": 8634, - "id": "myTransactionId2", - "offset": 39298, + "docType": "span", + "duration": 532, + "id": "mySpanIdC", + "offset": 43899, "parent": Object { "doc": Object { "parent": Object { - "id": "myTransactionId1", + "id": "myTransactionId2", }, "processor": Object { "event": "span", }, "service": Object { - "name": "opbeans-node", + "name": "opbeans-ruby", }, "span": Object { "duration": Object { - "us": 47557, + "us": 6161, }, - "id": "mySpanIdD", - "name": "GET opbeans-ruby:3000/api/products", + "id": "mySpanIdA", + "name": "Api::ProductsController#index", }, "timestamp": Object { - "us": 1549324795785760, + "us": 1549324795824504, }, "trace": Object { "id": "myTraceId", }, "transaction": Object { - "id": "myTransactionId1", + "id": "myTransactionId2", }, }, "docType": "span", - "duration": 47557, - "id": "mySpanIdD", - "offset": 1754, + "duration": 6161, + "id": "mySpanIdA", + "offset": 40498, "parent": Object { "doc": Object { + "parent": Object { + "id": "mySpanIdD", + }, "processor": Object { "event": "transaction", }, "service": Object { - "name": "opbeans-node", + "name": "opbeans-ruby", }, "timestamp": Object { - "us": 1549324795784006, + "us": 1549324795823304, }, "trace": Object { "id": "myTraceId", }, - "transaction": Object { - "duration": Object { - "us": 49660, + "transaction": Object { + "duration": Object { + "us": 8634, + }, + "id": "myTransactionId2", + "marks": Object { + "agent": Object { + "domComplete": 383, + "domInteractive": 382, + "timeToFirstByte": 14, + }, + }, + "name": "Api::ProductsController#index", + }, + }, + "docType": "transaction", + "duration": 8634, + "id": "myTransactionId2", + "offset": 39298, + "parent": Object { + "doc": Object { + "parent": Object { + "id": "myTransactionId1", + }, + "processor": Object { + "event": "span", + }, + "service": Object { + "name": "opbeans-node", + }, + "span": Object { + "duration": Object { + "us": 47557, + }, + "id": "mySpanIdD", + "name": "GET opbeans-ruby:3000/api/products", + }, + "timestamp": Object { + "us": 1549324795785760, + }, + "trace": Object { + "id": "myTraceId", + }, + "transaction": Object { + "id": "myTransactionId1", + }, + }, + "docType": "span", + "duration": 47557, + "id": "mySpanIdD", + "offset": 1754, + "parent": Object { + "doc": Object { + "processor": Object { + "event": "transaction", + }, + "service": Object { + "name": "opbeans-node", + }, + "timestamp": Object { + "us": 1549324795784006, + }, + "trace": Object { + "id": "myTraceId", + }, + "transaction": Object { + "duration": Object { + "us": 49660, + }, + "id": "myTransactionId1", + "name": "GET /api", + }, }, + "docType": "transaction", + "duration": 49660, "id": "myTransactionId1", - "name": "GET /api", + "offset": 0, + "parent": undefined, + "parentId": undefined, + "skew": 0, }, + "parentId": "myTransactionId1", + "skew": 0, }, - "docType": "transaction", - "duration": 49660, - "id": "myTransactionId1", - "offset": 0, - "parent": undefined, - "parentId": undefined, + "parentId": "mySpanIdD", "skew": 0, }, - "parentId": "myTransactionId1", + "parentId": "myTransactionId2", "skew": 0, }, - "parentId": "mySpanIdD", + "parentId": "mySpanIdA", "skew": 0, }, - Object { - "doc": Object { - "parent": Object { - "id": "myTransactionId2", - }, - "processor": Object { - "event": "span", - }, - "service": Object { - "name": "opbeans-ruby", - }, - "span": Object { - "duration": Object { - "us": 6161, - }, - "id": "mySpanIdA", - "name": "Api::ProductsController#index", - }, - "timestamp": Object { - "us": 1549324795824504, - }, - "trace": Object { - "id": "myTraceId", - }, - "transaction": Object { - "id": "myTransactionId2", - }, + ], + "rootTransaction": Object { + "processor": Object { + "event": "transaction", + }, + "service": Object { + "name": "opbeans-node", + }, + "timestamp": Object { + "us": 1549324795784006, + }, + "trace": Object { + "id": "myTraceId", + }, + "transaction": Object { + "duration": Object { + "us": 49660, }, - "docType": "span", - "duration": 6161, - "id": "mySpanIdA", - "offset": 40498, - "parent": Object { + "id": "myTransactionId1", + "name": "GET /api", + }, + }, + "serviceColors": Object { + "opbeans-node": "#6092c0", + "opbeans-ruby": "#54b399", + }, +} +`; + +exports[`waterfall_helpers getWaterfall should return partial waterfall 1`] = ` +Object { + "childrenByParentId": Object { + "mySpanIdA": Array [ + Object { "doc": Object { "parent": Object { - "id": "mySpanIdD", + "id": "mySpanIdA", }, "processor": Object { - "event": "transaction", + "event": "span", }, "service": Object { "name": "opbeans-ruby", }, + "span": Object { + "duration": Object { + "us": 481, + }, + "id": "mySpanIdB", + "name": "SELECT FROM products", + }, "timestamp": Object { - "us": 1549324795823304, + "us": 1549324795825633, }, "trace": Object { "id": "myTraceId", }, "transaction": Object { - "duration": Object { - "us": 8634, - }, "id": "myTransactionId2", - "marks": Object { - "agent": Object { - "domComplete": 383, - "domInteractive": 382, - "timeToFirstByte": 14, - }, - }, - "name": "Api::ProductsController#index", }, }, - "docType": "transaction", - "duration": 8634, - "id": "myTransactionId2", - "offset": 39298, + "docType": "span", + "duration": 481, + "id": "mySpanIdB", + "offset": 2329, "parent": Object { "doc": Object { "parent": Object { - "id": "myTransactionId1", + "id": "myTransactionId2", }, "processor": Object { "event": "span", }, "service": Object { - "name": "opbeans-node", + "name": "opbeans-ruby", }, "span": Object { "duration": Object { - "us": 47557, + "us": 6161, }, - "id": "mySpanIdD", - "name": "GET opbeans-ruby:3000/api/products", + "id": "mySpanIdA", + "name": "Api::ProductsController#index", }, "timestamp": Object { - "us": 1549324795785760, + "us": 1549324795824504, }, "trace": Object { "id": "myTraceId", }, "transaction": Object { - "id": "myTransactionId1", + "id": "myTransactionId2", }, }, "docType": "span", - "duration": 47557, - "id": "mySpanIdD", - "offset": 1754, + "duration": 6161, + "id": "mySpanIdA", + "offset": 1200, "parent": Object { "doc": Object { + "parent": Object { + "id": "mySpanIdD", + }, "processor": Object { "event": "transaction", }, "service": Object { - "name": "opbeans-node", + "name": "opbeans-ruby", }, "timestamp": Object { - "us": 1549324795784006, + "us": 1549324795823304, }, "trace": Object { "id": "myTraceId", }, "transaction": Object { "duration": Object { - "us": 49660, + "us": 8634, }, - "id": "myTransactionId1", - "name": "GET /api", + "id": "myTransactionId2", + "marks": Object { + "agent": Object { + "domComplete": 383, + "domInteractive": 382, + "timeToFirstByte": 14, + }, + }, + "name": "Api::ProductsController#index", }, }, "docType": "transaction", - "duration": 49660, - "id": "myTransactionId1", + "duration": 8634, + "id": "myTransactionId2", "offset": 0, "parent": undefined, - "parentId": undefined, + "parentId": "mySpanIdD", "skew": 0, }, - "parentId": "myTransactionId1", + "parentId": "myTransactionId2", "skew": 0, }, - "parentId": "mySpanIdD", + "parentId": "mySpanIdA", "skew": 0, }, - "parentId": "myTransactionId2", - "skew": 0, - }, - Object { - "doc": Object { - "parent": Object { - "id": "mySpanIdA", - }, - "processor": Object { - "event": "span", - }, - "service": Object { - "name": "opbeans-ruby", - }, - "span": Object { - "duration": Object { - "us": 481, - }, - "id": "mySpanIdB", - "name": "SELECT FROM products", - }, - "timestamp": Object { - "us": 1549324795825633, - }, - "trace": Object { - "id": "myTraceId", - }, - "transaction": Object { - "id": "myTransactionId2", - }, - }, - "docType": "span", - "duration": 481, - "id": "mySpanIdB", - "offset": 41627, - "parent": Object { + Object { "doc": Object { "parent": Object { - "id": "myTransactionId2", + "id": "mySpanIdA", }, "processor": Object { "event": "span", @@ -484,13 +1650,13 @@ Object { }, "span": Object { "duration": Object { - "us": 6161, + "us": 532, }, - "id": "mySpanIdA", - "name": "Api::ProductsController#index", + "id": "mySpanIdC", + "name": "SELECT FROM product", }, "timestamp": Object { - "us": 1549324795824504, + "us": 1549324795827905, }, "trace": Object { "id": "myTraceId", @@ -500,152 +1666,132 @@ Object { }, }, "docType": "span", - "duration": 6161, - "id": "mySpanIdA", - "offset": 40498, + "duration": 532, + "id": "mySpanIdC", + "offset": 4601, "parent": Object { "doc": Object { "parent": Object { - "id": "mySpanIdD", + "id": "myTransactionId2", }, "processor": Object { - "event": "transaction", + "event": "span", }, "service": Object { "name": "opbeans-ruby", }, + "span": Object { + "duration": Object { + "us": 6161, + }, + "id": "mySpanIdA", + "name": "Api::ProductsController#index", + }, "timestamp": Object { - "us": 1549324795823304, + "us": 1549324795824504, }, "trace": Object { "id": "myTraceId", }, "transaction": Object { - "duration": Object { - "us": 8634, - }, "id": "myTransactionId2", - "marks": Object { - "agent": Object { - "domComplete": 383, - "domInteractive": 382, - "timeToFirstByte": 14, - }, - }, - "name": "Api::ProductsController#index", }, }, - "docType": "transaction", - "duration": 8634, - "id": "myTransactionId2", - "offset": 39298, + "docType": "span", + "duration": 6161, + "id": "mySpanIdA", + "offset": 1200, "parent": Object { "doc": Object { "parent": Object { - "id": "myTransactionId1", + "id": "mySpanIdD", }, "processor": Object { - "event": "span", + "event": "transaction", }, "service": Object { - "name": "opbeans-node", - }, - "span": Object { - "duration": Object { - "us": 47557, - }, - "id": "mySpanIdD", - "name": "GET opbeans-ruby:3000/api/products", + "name": "opbeans-ruby", }, "timestamp": Object { - "us": 1549324795785760, + "us": 1549324795823304, }, "trace": Object { "id": "myTraceId", }, "transaction": Object { - "id": "myTransactionId1", - }, - }, - "docType": "span", - "duration": 47557, - "id": "mySpanIdD", - "offset": 1754, - "parent": Object { - "doc": Object { - "processor": Object { - "event": "transaction", - }, - "service": Object { - "name": "opbeans-node", - }, - "timestamp": Object { - "us": 1549324795784006, - }, - "trace": Object { - "id": "myTraceId", + "duration": Object { + "us": 8634, }, - "transaction": Object { - "duration": Object { - "us": 49660, + "id": "myTransactionId2", + "marks": Object { + "agent": Object { + "domComplete": 383, + "domInteractive": 382, + "timeToFirstByte": 14, }, - "id": "myTransactionId1", - "name": "GET /api", }, + "name": "Api::ProductsController#index", }, - "docType": "transaction", - "duration": 49660, - "id": "myTransactionId1", - "offset": 0, - "parent": undefined, - "parentId": undefined, - "skew": 0, }, - "parentId": "myTransactionId1", + "docType": "transaction", + "duration": 8634, + "id": "myTransactionId2", + "offset": 0, + "parent": undefined, + "parentId": "mySpanIdD", "skew": 0, }, - "parentId": "mySpanIdD", + "parentId": "myTransactionId2", "skew": 0, }, - "parentId": "myTransactionId2", + "parentId": "mySpanIdA", "skew": 0, }, - "parentId": "mySpanIdA", - "skew": 0, - }, - Object { - "doc": Object { - "parent": Object { - "id": "mySpanIdA", - }, - "processor": Object { - "event": "span", - }, - "service": Object { - "name": "opbeans-ruby", - }, - "span": Object { - "duration": Object { - "us": 532, + ], + "mySpanIdD": Array [ + Object { + "doc": Object { + "parent": Object { + "id": "mySpanIdD", + }, + "processor": Object { + "event": "transaction", + }, + "service": Object { + "name": "opbeans-ruby", + }, + "timestamp": Object { + "us": 1549324795823304, + }, + "trace": Object { + "id": "myTraceId", + }, + "transaction": Object { + "duration": Object { + "us": 8634, + }, + "id": "myTransactionId2", + "marks": Object { + "agent": Object { + "domComplete": 383, + "domInteractive": 382, + "timeToFirstByte": 14, + }, + }, + "name": "Api::ProductsController#index", }, - "id": "mySpanIdC", - "name": "SELECT FROM product", - }, - "timestamp": Object { - "us": 1549324795827905, - }, - "trace": Object { - "id": "myTraceId", - }, - "transaction": Object { - "id": "myTransactionId2", }, + "docType": "transaction", + "duration": 8634, + "id": "myTransactionId2", + "offset": 0, + "parent": undefined, + "parentId": "mySpanIdD", + "skew": 0, }, - "docType": "span", - "duration": 532, - "id": "mySpanIdC", - "offset": 43899, - "parent": Object { + ], + "myTransactionId2": Array [ + Object { "doc": Object { "parent": Object { "id": "myTransactionId2", @@ -676,7 +1822,7 @@ Object { "docType": "span", "duration": 6161, "id": "mySpanIdA", - "offset": 40498, + "offset": 1200, "parent": Object { "doc": Object { "parent": Object { @@ -712,143 +1858,56 @@ Object { "docType": "transaction", "duration": 8634, "id": "myTransactionId2", - "offset": 39298, - "parent": Object { - "doc": Object { - "parent": Object { - "id": "myTransactionId1", - }, - "processor": Object { - "event": "span", - }, - "service": Object { - "name": "opbeans-node", - }, - "span": Object { - "duration": Object { - "us": 47557, - }, - "id": "mySpanIdD", - "name": "GET opbeans-ruby:3000/api/products", - }, - "timestamp": Object { - "us": 1549324795785760, - }, - "trace": Object { - "id": "myTraceId", - }, - "transaction": Object { - "id": "myTransactionId1", - }, - }, - "docType": "span", - "duration": 47557, - "id": "mySpanIdD", - "offset": 1754, - "parent": Object { - "doc": Object { - "processor": Object { - "event": "transaction", - }, - "service": Object { - "name": "opbeans-node", - }, - "timestamp": Object { - "us": 1549324795784006, - }, - "trace": Object { - "id": "myTraceId", - }, - "transaction": Object { - "duration": Object { - "us": 49660, - }, - "id": "myTransactionId1", - "name": "GET /api", - }, - }, - "docType": "transaction", - "duration": 49660, - "id": "myTransactionId1", - "offset": 0, - "parent": undefined, - "parentId": undefined, - "skew": 0, - }, - "parentId": "myTransactionId1", - "skew": 0, - }, + "offset": 0, + "parent": undefined, "parentId": "mySpanIdD", "skew": 0, }, "parentId": "myTransactionId2", "skew": 0, }, - "parentId": "mySpanIdA", - "skew": 0, - }, - ], - "rootTransaction": Object { - "processor": Object { - "event": "transaction", - }, - "service": Object { - "name": "opbeans-node", - }, - "timestamp": Object { - "us": 1549324795784006, - }, - "trace": Object { - "id": "myTraceId", - }, - "transaction": Object { - "duration": Object { - "us": 49660, - }, - "id": "myTransactionId1", - "name": "GET /api", - }, - }, - "serviceColors": Object { - "opbeans-node": "#6092c0", - "opbeans-ruby": "#54b399", + ], }, -} -`; - -exports[`waterfall_helpers getWaterfall should return partial waterfall 1`] = ` -Object { "duration": 8634, - "entryTransaction": Object { - "parent": Object { - "id": "mySpanIdD", - }, - "processor": Object { - "event": "transaction", - }, - "service": Object { - "name": "opbeans-ruby", - }, - "timestamp": Object { - "us": 1549324795823304, - }, - "trace": Object { - "id": "myTraceId", - }, - "transaction": Object { - "duration": Object { - "us": 8634, + "entryWaterfallTransaction": Object { + "doc": Object { + "parent": Object { + "id": "mySpanIdD", }, - "id": "myTransactionId2", - "marks": Object { - "agent": Object { - "domComplete": 383, - "domInteractive": 382, - "timeToFirstByte": 14, + "processor": Object { + "event": "transaction", + }, + "service": Object { + "name": "opbeans-ruby", + }, + "timestamp": Object { + "us": 1549324795823304, + }, + "trace": Object { + "id": "myTraceId", + }, + "transaction": Object { + "duration": Object { + "us": 8634, + }, + "id": "myTransactionId2", + "marks": Object { + "agent": Object { + "domComplete": 383, + "domInteractive": 382, + "timeToFirstByte": 14, + }, }, + "name": "Api::ProductsController#index", }, - "name": "Api::ProductsController#index", }, + "docType": "transaction", + "duration": 8634, + "id": "myTransactionId2", + "offset": 0, + "parent": undefined, + "parentId": "mySpanIdD", + "skew": 0, }, "errorItems": Array [], "errorsCount": 0, diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/waterfall_helpers.ts b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/waterfall_helpers.ts index 441a51bcba646..44e5e09e506af 100644 --- a/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/waterfall_helpers.ts +++ b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/waterfall_helpers.ts @@ -28,7 +28,7 @@ interface IWaterfallGroup { const ROOT_ID = 'root'; export interface IWaterfall { - entryTransaction?: Transaction; + entryWaterfallTransaction?: IWaterfallTransaction; rootTransaction?: Transaction; /** @@ -36,6 +36,7 @@ export interface IWaterfall { */ duration: number; items: IWaterfallItem[]; + childrenByParentId: Record; errorsPerTransaction: TraceAPIResponse['errorsPerTransaction']; errorsCount: number; serviceColors: IServiceColors; @@ -329,6 +330,7 @@ export function getWaterfall( errorsCount: sum(Object.values(errorsPerTransaction)), serviceColors: {}, errorItems: [], + childrenByParentId: {}, }; } @@ -357,10 +359,8 @@ export function getWaterfall( const duration = getWaterfallDuration(items); const serviceColors = getServiceColors(items); - const entryTransaction = entryWaterfallTransaction?.doc; - return { - entryTransaction, + entryWaterfallTransaction, rootTransaction, duration, items, @@ -368,5 +368,6 @@ export function getWaterfall( errorsCount: errorItems.length, serviceColors, errorItems, + childrenByParentId: getChildrenGroupedByParentId(items), }; } diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/index.tsx b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/index.tsx index 6fd139b470ce1..501ca6d33d5af 100644 --- a/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/index.tsx +++ b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/index.tsx @@ -8,8 +8,8 @@ import { Location } from 'history'; import React from 'react'; import { IUrlParams } from '../../../../../context/UrlParamsContext/types'; import { ServiceLegends } from './ServiceLegends'; -import { Waterfall } from './Waterfall'; import { IWaterfall } from './Waterfall/waterfall_helpers/waterfall_helpers'; +import { Waterfall } from './Waterfall'; interface Props { urlParams: IUrlParams; diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/index.tsx b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/index.tsx index 12676b7c15f1c..392bd90ffbabc 100644 --- a/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/index.tsx +++ b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/index.tsx @@ -64,8 +64,8 @@ export function WaterfallWithSummmary({ }); }; - const { entryTransaction } = waterfall; - if (!entryTransaction) { + const { entryWaterfallTransaction } = waterfall; + if (!entryWaterfallTransaction) { const content = isLoading ? ( ) : ( @@ -84,6 +84,8 @@ export function WaterfallWithSummmary({ return {content}; } + const entryTransaction = entryWaterfallTransaction.doc; + return ( From 1dc48b3fdd0b32b1bd4aaf9169498aaa328e5a39 Mon Sep 17 00:00:00 2001 From: Spencer Date: Tue, 25 Aug 2020 08:07:10 -0700 Subject: [PATCH 42/71] [src/dev/build] stop including public source in distributable (#75841) Co-authored-by: spalger --- src/dev/build/tasks/copy_source_task.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/dev/build/tasks/copy_source_task.ts b/src/dev/build/tasks/copy_source_task.ts index 7a5d84da527db..948e2357effb0 100644 --- a/src/dev/build/tasks/copy_source_task.ts +++ b/src/dev/build/tasks/copy_source_task.ts @@ -33,11 +33,11 @@ export const CopySource: Task = { '!src/**/{target,__tests__,__snapshots__,__mocks__}/**', '!src/test_utils/**', '!src/fixtures/**', - '!src/legacy/core_plugins/console/public/tests/**', '!src/cli/cluster/**', '!src/cli/repl/**', '!src/functional_test_runner/**', '!src/dev/**', + '!**/public/**', 'typings/**', 'config/kibana.yml', 'config/node.options', From 8f85593910f49afc48e335618f0ef4aaede5efb5 Mon Sep 17 00:00:00 2001 From: Frank Hassanabad Date: Tue, 25 Aug 2020 09:22:13 -0600 Subject: [PATCH 43/71] [Security Solution] Fixes assert unreachable to be within the common section and the type to never (#75798) ## Summary Assert unreachable was created through advice given by both the Typescript community and through the techniques that TyepScript is trying to achieve type safety with switch statements. This fixes recent bugs by: * Re-adding the never type * Reduces the two different types by putting the helper within the common section so there's not duplication * Fixes on type that looks like it was a regular string rather than a one of the enum types The reasoning for exhaustive checks within switch statements and techniques can be seen in numerous areas such as here: https://stackoverflow.com/questions/39419170/how-do-i-check-that-a-switch-block-is-exhaustive-in-typescript You can do it either way with TypeScript as long as you ensure you have a explicit return type and you do early return statements you can actually avoid having to call into the assertUnreachable. If introduced and used correctly it is there to help out like this error it is telling us that this string type is not exhaustive: Screen Shot 2020-08-24 at 10 39 42 AM You can notice that for this pull request I actually remove the assertion like so if someone accidentally removes one of the switch statements: Screen Shot 2020-08-24 at 10 42 08 AM And since the function has an explicit return type it is not needed. You will see that TypeScript improved its never types behind the scenes where it actually will tell you that it will never reach the `assertUnreachable` and want to remove it as an auto-refactor. That is ok as long as we have explicit return types and what I did with one line of code here. Screen Shot 2020-08-24 at 11 21 05 AM Without this fix, and having the never type become an unknown it introduces less safety where any code that is utilizing the assertUnknown without explicit return types will be prone to having run time errors being thrown when something new is added to their switch enum types. --- .../search_strategy/security_solution/index.ts | 2 +- .../security_solution/common/utility_types.ts | 18 ++++++++++++++++++ .../public/common/lib/helpers/index.tsx | 13 ------------- .../rules/description_step/helpers.tsx | 2 +- .../hosts/components/hosts_table/index.tsx | 2 +- .../network/components/users_table/index.tsx | 2 +- .../body/column_headers/header/helpers.ts | 2 +- .../lib/detection_engine/signals/get_filter.ts | 2 +- .../signals/rule_status_service.ts | 2 +- .../lib/events/query.last_event_time.dsl.ts | 2 +- .../server/lib/hosts/query.hosts.dsl.ts | 3 ++- .../server/lib/ip_details/query_users.dsl.ts | 3 ++- .../server/lib/network/query_dns.dsl.ts | 3 ++- .../lib/network/query_top_countries.dsl.ts | 4 ++-- .../server/lib/network/query_top_n_flow.dsl.ts | 3 ++- .../server/lib/tls/query_tls.dsl.ts | 3 ++- .../factory/hosts/dsl/query.hosts.dsl.ts | 4 +--- .../server/utils/build_query/index.ts | 7 ------- 18 files changed, 39 insertions(+), 38 deletions(-) diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/index.ts index edb5dda2ca6da..a188eb7619e6b 100644 --- a/x-pack/plugins/security_solution/common/search_strategy/security_solution/index.ts +++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/index.ts @@ -40,7 +40,7 @@ export enum Direction { } export interface SortField { - field: string; + field: 'lastSeen' | 'hostName'; direction: Direction; } diff --git a/x-pack/plugins/security_solution/common/utility_types.ts b/x-pack/plugins/security_solution/common/utility_types.ts index 43271dc40ba12..4a7bd02d0442b 100644 --- a/x-pack/plugins/security_solution/common/utility_types.ts +++ b/x-pack/plugins/security_solution/common/utility_types.ts @@ -26,3 +26,21 @@ export const stringEnum = (enumObj: T, enumName = 'enum') => : runtimeTypes.failure(u, c), (a) => (a as unknown) as string ); + +/** + * Unreachable Assertion helper for scenarios like exhaustive switches. + * For references see: https://stackoverflow.com/questions/39419170/how-do-i-check-that-a-switch-block-is-exhaustive-in-typescript + * This "x" should _always_ be a type of "never" and not change to "unknown" or any other type. See above link or the generic + * concept of exhaustive checks in switch blocks. + * + * Optionally you can avoid the use of this by using early returns and TypeScript will clear your type checking without complaints + * but there are situations and times where this function might still be needed. + * @param x Unreachable field + * @param message Message of error thrown + */ +export const assertUnreachable = ( + x: never, // This should always be a type of "never" + message = 'Unknown Field in switch statement' +): never => { + throw new Error(`${message}: ${x}`); +}; diff --git a/x-pack/plugins/security_solution/public/common/lib/helpers/index.tsx b/x-pack/plugins/security_solution/public/common/lib/helpers/index.tsx index 96b0343efdf72..35f51b3c65f95 100644 --- a/x-pack/plugins/security_solution/public/common/lib/helpers/index.tsx +++ b/x-pack/plugins/security_solution/public/common/lib/helpers/index.tsx @@ -24,19 +24,6 @@ export const asArrayIfExists: WrapArrayIfExitts = (value) => */ export type ValueOf = T[keyof T]; -/** - * Unreachable Assertion helper for scenarios like exhaustive switches - * - * @param x Unreachable field - * @param message Message of error thrown - */ -export const assertUnreachable = ( - x: never, - message = 'Unknown Field in switch statement' -): never => { - throw new Error(`${message}: ${x}`); -}; - /** * Global variables */ diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/helpers.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/helpers.tsx index 600bc999849d1..3a0a5b04c5874 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/helpers.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/helpers.tsx @@ -21,6 +21,7 @@ import { isEmpty } from 'lodash/fp'; import React from 'react'; import styled from 'styled-components'; +import { assertUnreachable } from '../../../../../common/utility_types'; import * as i18nSeverity from '../severity_mapping/translations'; import * as i18nRiskScore from '../risk_score_mapping/translations'; import { Threshold } from '../../../../../common/detection_engine/schemas/common/schemas'; @@ -33,7 +34,6 @@ import * as i18n from './translations'; import { BuildQueryBarDescription, BuildThreatDescription, ListItems } from './types'; import { SeverityBadge } from '../severity_badge'; import ListTreeIcon from './assets/list_tree_icon.svg'; -import { assertUnreachable } from '../../../../common/lib/helpers'; import { AboutStepRiskScore, AboutStepSeverity } from '../../../pages/detection_engine/rules/types'; import { defaultToEmptyTag } from '../../../../common/components/empty_value'; diff --git a/x-pack/plugins/security_solution/public/hosts/components/hosts_table/index.tsx b/x-pack/plugins/security_solution/public/hosts/components/hosts_table/index.tsx index d72891fad8f53..8b795fca41512 100644 --- a/x-pack/plugins/security_solution/public/hosts/components/hosts_table/index.tsx +++ b/x-pack/plugins/security_solution/public/hosts/components/hosts_table/index.tsx @@ -8,6 +8,7 @@ import React, { useMemo, useCallback } from 'react'; import { connect, ConnectedProps } from 'react-redux'; import { IIndexPattern } from 'src/plugins/data/public'; +import { assertUnreachable } from '../../../../common/utility_types'; import { Direction, HostFields, @@ -17,7 +18,6 @@ import { HostsSortField, OsFields, } from '../../../graphql/types'; -import { assertUnreachable } from '../../../common/lib/helpers'; import { State } from '../../../common/store'; import { Columns, diff --git a/x-pack/plugins/security_solution/public/network/components/users_table/index.tsx b/x-pack/plugins/security_solution/public/network/components/users_table/index.tsx index af9d2b0ffefe3..9a971e0087d12 100644 --- a/x-pack/plugins/security_solution/public/network/components/users_table/index.tsx +++ b/x-pack/plugins/security_solution/public/network/components/users_table/index.tsx @@ -8,6 +8,7 @@ import React, { useCallback, useMemo } from 'react'; import { connect, ConnectedProps } from 'react-redux'; import deepEqual from 'fast-deep-equal'; +import { assertUnreachable } from '../../../../common/utility_types'; import { networkActions, networkModel, networkSelectors } from '../../store'; import { Direction, @@ -26,7 +27,6 @@ import { import { getUsersColumns } from './columns'; import * as i18n from './translations'; -import { assertUnreachable } from '../../../common/lib/helpers'; const tableType = networkModel.IpDetailsTableType.users; interface OwnProps { diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header/helpers.ts b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header/helpers.ts index 6d70795c422d9..609f690903bf2 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header/helpers.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header/helpers.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +import { assertUnreachable } from '../../../../../../../common/utility_types'; import { Direction } from '../../../../../../graphql/types'; -import { assertUnreachable } from '../../../../../../common/lib/helpers'; import { ColumnHeaderOptions } from '../../../../../../timelines/store/timeline/model'; import { Sort, SortDirection } from '../../sort'; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/get_filter.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/get_filter.ts index 67dc1d50eefcd..f77485f39a98d 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/get_filter.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/get_filter.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { assertUnreachable } from '../../../../common/utility_types'; import { getQueryFilter } from '../../../../common/detection_engine/get_query_filter'; import { LanguageOrUndefined, @@ -15,7 +16,6 @@ import { } from '../../../../common/detection_engine/schemas/common/schemas'; import { ExceptionListItemSchema } from '../../../../../lists/common/schemas'; import { AlertServices } from '../../../../../alerts/server'; -import { assertUnreachable } from '../../../utils/build_query'; import { PartialFilter } from '../types'; import { BadRequestError } from '../errors/bad_request_error'; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/rule_status_service.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/rule_status_service.ts index 0f4b8d1472b3f..8fdbe282eece5 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/rule_status_service.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/rule_status_service.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +import { assertUnreachable } from '../../../../common/utility_types'; import { JobStatus } from '../../../../common/detection_engine/schemas/common/schemas'; -import { assertUnreachable } from '../../../utils/build_query'; import { IRuleStatusAttributes } from '../rules/types'; import { getOrCreateRuleStatuses } from './get_or_create_rule_statuses'; import { RuleStatusSavedObjectsClient } from './rule_status_saved_objects_client'; diff --git a/x-pack/plugins/security_solution/server/lib/events/query.last_event_time.dsl.ts b/x-pack/plugins/security_solution/server/lib/events/query.last_event_time.dsl.ts index 6c443fed3c99d..02badd3ccee8f 100644 --- a/x-pack/plugins/security_solution/server/lib/events/query.last_event_time.dsl.ts +++ b/x-pack/plugins/security_solution/server/lib/events/query.last_event_time.dsl.ts @@ -6,9 +6,9 @@ import { isEmpty } from 'lodash/fp'; +import { assertUnreachable } from '../../../common/utility_types'; import { LastEventTimeRequestOptions } from './types'; import { LastEventIndexKey } from '../../graphql/types'; -import { assertUnreachable } from '../../utils/build_query'; interface EventIndices { [key: string]: string[]; diff --git a/x-pack/plugins/security_solution/server/lib/hosts/query.hosts.dsl.ts b/x-pack/plugins/security_solution/server/lib/hosts/query.hosts.dsl.ts index 013afd5cd58f5..dfe45a00e0513 100644 --- a/x-pack/plugins/security_solution/server/lib/hosts/query.hosts.dsl.ts +++ b/x-pack/plugins/security_solution/server/lib/hosts/query.hosts.dsl.ts @@ -6,8 +6,9 @@ import { isEmpty } from 'lodash/fp'; +import { assertUnreachable } from '../../../common/utility_types'; import { Direction, HostsFields, HostsSortField } from '../../graphql/types'; -import { assertUnreachable, createQueryFilterClauses } from '../../utils/build_query'; +import { createQueryFilterClauses } from '../../utils/build_query'; import { HostsRequestOptions } from '.'; diff --git a/x-pack/plugins/security_solution/server/lib/ip_details/query_users.dsl.ts b/x-pack/plugins/security_solution/server/lib/ip_details/query_users.dsl.ts index 10678dc033eb5..293a487777fd2 100644 --- a/x-pack/plugins/security_solution/server/lib/ip_details/query_users.dsl.ts +++ b/x-pack/plugins/security_solution/server/lib/ip_details/query_users.dsl.ts @@ -4,8 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ +import { assertUnreachable } from '../../../common/utility_types'; import { Direction, UsersFields, UsersSortField } from '../../graphql/types'; -import { assertUnreachable, createQueryFilterClauses } from '../../utils/build_query'; +import { createQueryFilterClauses } from '../../utils/build_query'; import { UsersRequestOptions } from './index'; diff --git a/x-pack/plugins/security_solution/server/lib/network/query_dns.dsl.ts b/x-pack/plugins/security_solution/server/lib/network/query_dns.dsl.ts index e7c86e1d3d66b..90781e7b48b4a 100644 --- a/x-pack/plugins/security_solution/server/lib/network/query_dns.dsl.ts +++ b/x-pack/plugins/security_solution/server/lib/network/query_dns.dsl.ts @@ -6,8 +6,9 @@ import { isEmpty } from 'lodash/fp'; +import { assertUnreachable } from '../../../common/utility_types'; import { Direction, NetworkDnsFields, NetworkDnsSortField } from '../../graphql/types'; -import { assertUnreachable, createQueryFilterClauses } from '../../utils/build_query'; +import { createQueryFilterClauses } from '../../utils/build_query'; import { NetworkDnsRequestOptions } from './index'; diff --git a/x-pack/plugins/security_solution/server/lib/network/query_top_countries.dsl.ts b/x-pack/plugins/security_solution/server/lib/network/query_top_countries.dsl.ts index 93ffc35161fa9..be0b8fb64c76a 100644 --- a/x-pack/plugins/security_solution/server/lib/network/query_top_countries.dsl.ts +++ b/x-pack/plugins/security_solution/server/lib/network/query_top_countries.dsl.ts @@ -10,8 +10,8 @@ import { NetworkTopTablesSortField, NetworkTopTablesFields, } from '../../graphql/types'; -import { assertUnreachable, createQueryFilterClauses } from '../../utils/build_query'; - +import { createQueryFilterClauses } from '../../utils/build_query'; +import { assertUnreachable } from '../../../common/utility_types'; import { NetworkTopCountriesRequestOptions } from './index'; const getCountAgg = (flowTarget: FlowTargetSourceDest) => ({ diff --git a/x-pack/plugins/security_solution/server/lib/network/query_top_n_flow.dsl.ts b/x-pack/plugins/security_solution/server/lib/network/query_top_n_flow.dsl.ts index 7cb8b76e7b524..14a9c5e33aca0 100644 --- a/x-pack/plugins/security_solution/server/lib/network/query_top_n_flow.dsl.ts +++ b/x-pack/plugins/security_solution/server/lib/network/query_top_n_flow.dsl.ts @@ -4,13 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ +import { assertUnreachable } from '../../../common/utility_types'; import { Direction, FlowTargetSourceDest, NetworkTopTablesSortField, NetworkTopTablesFields, } from '../../graphql/types'; -import { assertUnreachable, createQueryFilterClauses } from '../../utils/build_query'; +import { createQueryFilterClauses } from '../../utils/build_query'; import { NetworkTopNFlowRequestOptions } from './index'; diff --git a/x-pack/plugins/security_solution/server/lib/tls/query_tls.dsl.ts b/x-pack/plugins/security_solution/server/lib/tls/query_tls.dsl.ts index 82f16ff58d135..f6921ddcdf508 100644 --- a/x-pack/plugins/security_solution/server/lib/tls/query_tls.dsl.ts +++ b/x-pack/plugins/security_solution/server/lib/tls/query_tls.dsl.ts @@ -4,7 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { createQueryFilterClauses, assertUnreachable } from '../../utils/build_query'; +import { assertUnreachable } from '../../../common/utility_types'; +import { createQueryFilterClauses } from '../../utils/build_query'; import { TlsRequestOptions } from './index'; import { TlsSortField, Direction, TlsFields } from '../../graphql/types'; diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/dsl/query.hosts.dsl.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/dsl/query.hosts.dsl.ts index 3d72f98f35355..a9101f54ada55 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/dsl/query.hosts.dsl.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/dsl/query.hosts.dsl.ts @@ -11,7 +11,7 @@ import { HostsRequestOptions, SortField, } from '../../../../../../common/search_strategy/security_solution'; -import { assertUnreachable, createQueryFilterClauses } from '../../../../../utils/build_query'; +import { createQueryFilterClauses } from '../../../../../utils/build_query'; export const buildHostsQuery = ({ defaultIndex, @@ -83,7 +83,5 @@ const getQueryOrder = (sort: SortField): QueryOrder => { return { lastSeen: sort.direction }; case 'hostName': return { _key: sort.direction }; - default: - return assertUnreachable(sort.field); } }; diff --git a/x-pack/plugins/security_solution/server/utils/build_query/index.ts b/x-pack/plugins/security_solution/server/utils/build_query/index.ts index 233ba70968fa1..f0f4ba07ab2ae 100644 --- a/x-pack/plugins/security_solution/server/utils/build_query/index.ts +++ b/x-pack/plugins/security_solution/server/utils/build_query/index.ts @@ -9,13 +9,6 @@ export * from './filters'; export * from './merge_fields_with_hits'; export * from './calculate_timeseries_interval'; -export const assertUnreachable = ( - x: unknown, - message: string = 'Unknown Field in switch statement' -): never => { - throw new Error(`${message} ${x}`); -}; - export const inspectStringifyObject = (obj: unknown) => { try { return JSON.stringify(obj, null, 2); From 9cafade2b9d4a22a364c921e94a30c54e82e2ee0 Mon Sep 17 00:00:00 2001 From: Spencer Date: Tue, 25 Aug 2020 08:27:15 -0700 Subject: [PATCH 44/71] [remove] production deps which are only used in public code (#75838) Co-authored-by: spalger --- .eslintrc.js | 2 +- package.json | 98 ++++++------ packages/kbn-optimizer/package.json | 10 +- .../plugins/kbn_tp_run_pipeline/package.json | 8 +- .../kbn_sample_panel_action/package.json | 6 +- .../kbn_tp_custom_visualizations/package.json | 6 +- x-pack/package.json | 142 +++++++++--------- x-pack/plugins/apm/e2e/package.json | 6 +- x-pack/plugins/apm/scripts/package.json | 4 +- x-pack/plugins/security_solution/package.json | 4 +- x-pack/plugins/security_solution/yarn.lock | 1 - 11 files changed, 142 insertions(+), 145 deletions(-) delete mode 120000 x-pack/plugins/security_solution/yarn.lock diff --git a/.eslintrc.js b/.eslintrc.js index 8c2a46f80a3a8..a07d0830907b6 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -517,7 +517,7 @@ module.exports = { 'packages/kbn-interpreter/tasks/**/*.js', 'packages/kbn-interpreter/src/plugin/**/*.js', 'x-pack/{dev-tools,tasks,scripts,test,build_chromium}/**/*.js', - 'x-pack/**/{__tests__,__test__,__jest__,__fixtures__,__mocks__}/**/*.js', + 'x-pack/**/{__tests__,__test__,__jest__,__fixtures__,__mocks__,public}/**/*.js', 'x-pack/**/*.test.js', 'x-pack/test_utils/**/*', 'x-pack/gulpfile.js', diff --git a/package.json b/package.json index 46418e52d8548..84f6f30f064f9 100644 --- a/package.json +++ b/package.json @@ -123,18 +123,13 @@ "dependencies": { "@babel/core": "^7.11.1", "@babel/register": "^7.10.5", - "@elastic/apm-rum": "^5.5.0", - "@elastic/charts": "19.8.1", "@elastic/datemath": "5.0.3", "@elastic/elasticsearch": "7.9.0-rc.2", - "@elastic/ems-client": "7.9.3", "@elastic/eui": "27.4.1", - "@elastic/filesaver": "1.1.2", "@elastic/good": "8.1.1-kibana2", "@elastic/numeral": "^2.5.0", "@elastic/request-crypto": "1.1.4", "@elastic/safer-lodash-set": "0.0.0", - "@elastic/ui-ace": "0.2.3", "@hapi/good-squeeze": "5.2.1", "@hapi/wreck": "^15.0.2", "@kbn/analytics": "1.0.0", @@ -151,35 +146,24 @@ "abortcontroller-polyfill": "^1.4.0", "accept": "3.0.2", "angular": "^1.8.0", - "angular-aria": "^1.8.0", "angular-elastic": "^2.5.1", - "angular-recursion": "^1.0.5", - "angular-route": "^1.8.0", "angular-sanitize": "^1.8.0", - "angular-sortable-view": "^0.0.17", "bluebird": "3.5.5", "boom": "^7.2.0", - "brace": "0.11.1", "chalk": "^2.4.2", "check-disk-space": "^2.1.0", "chokidar": "3.2.1", "color": "1.0.3", "commander": "3.0.2", - "compare-versions": "3.5.1", "core-js": "^3.6.4", - "d3": "3.5.17", - "d3-cloud": "1.2.5", "deep-freeze-strict": "^1.1.1", - "deepmerge": "^4.2.2", "del": "^5.1.0", "elastic-apm-node": "^3.7.0", "elasticsearch": "^16.7.0", - "elasticsearch-browser": "^16.7.0", "execa": "^4.0.2", "expiry-js": "0.1.7", "fast-deep-equal": "^3.1.1", "font-awesome": "4.7.0", - "fp-ts": "^2.3.1", "getos": "^3.1.0", "glob": "^7.1.2", "glob-all": "^3.2.1", @@ -188,67 +172,40 @@ "handlebars": "4.7.6", "hapi": "^17.5.3", "hapi-auth-cookie": "^9.0.0", - "history": "^4.9.0", - "hjson": "3.2.1", "hoek": "^5.0.4", "http-proxy-agent": "^2.1.0", "https-proxy-agent": "^5.0.0", - "immer": "^1.5.0", "inert": "^5.1.0", "inline-style": "^2.0.0", "joi": "^13.5.2", - "jquery": "^3.5.0", - "js-levenshtein": "^1.1.6", "js-yaml": "3.13.1", "json-stable-stringify": "^1.0.1", - "json-stringify-pretty-compact": "1.2.0", "json-stringify-safe": "5.0.1", - "leaflet": "1.5.1", - "leaflet-draw": "0.4.14", - "leaflet-responsive-popup": "0.6.4", - "leaflet-vega": "^0.8.6", - "leaflet.heat": "0.2.0", - "less": "npm:@elastic/less@2.7.3-kibana", "lodash": "^4.17.20", "lru-cache": "4.1.5", - "markdown-it": "^10.0.0", "minimatch": "^3.0.4", "moment": "^2.24.0", "moment-timezone": "^0.5.27", - "monaco-editor": "~0.17.0", "mustache": "2.3.2", - "ngreact": "0.5.1", "node-fetch": "1.7.3", "node-forge": "^0.9.1", "opn": "^5.5.0", "oppsy": "^2.0.0", "pegjs": "0.10.0", - "prop-types": "15.6.0", "proxy-from-env": "1.0.0", "query-string": "5.1.1", "re2": "^1.15.4", "react": "^16.12.0", "react-color": "^2.13.8", "react-dom": "^16.12.0", - "react-grid-layout": "^0.16.2", "react-input-range": "^1.3.0", - "react-markdown": "^4.3.1", - "react-monaco-editor": "~0.27.0", - "react-redux": "^7.2.0", - "react-resize-detector": "^4.2.0", "react-router": "^5.2.0", - "react-router-dom": "^5.2.0", - "react-sizeme": "^2.3.6", "react-use": "^13.27.0", - "reactcss": "1.2.3", - "redux": "^4.0.5", "redux-actions": "^2.6.5", "redux-thunk": "^2.3.0", "regenerator-runtime": "^0.13.3", "request": "^2.88.0", "require-in-the-middle": "^5.0.2", - "reselect": "^4.0.0", - "resize-observer-polyfill": "^1.5.0", "rison-node": "1.0.2", "rxjs": "^6.5.5", "seedrandom": "^3.0.5", @@ -258,15 +215,9 @@ "tar": "4.4.13", "tinygradient": "0.4.3", "tinymath": "1.2.1", - "topojson-client": "3.0.0", "tslib": "^2.0.0", "type-detect": "^4.0.8", - "ui-select": "0.19.8", "uuid": "3.3.2", - "vega": "^5.13.0", - "vega-lite": "^4.13.1", - "vega-schema-url-parser": "^1.1.0", - "vega-tooltip": "^0.12.0", "vision": "^5.3.3", "whatwg-fetch": "^3.0.0", "yauzl": "2.10.0" @@ -274,10 +225,15 @@ "devDependencies": { "@babel/parser": "^7.11.2", "@babel/types": "^7.11.0", + "@elastic/apm-rum": "^5.5.0", + "@elastic/charts": "19.8.1", + "@elastic/ems-client": "7.9.3", "@elastic/eslint-config-kibana": "0.15.0", "@elastic/eslint-plugin-eui": "0.0.2", + "@elastic/filesaver": "1.1.2", "@elastic/github-checks-reporter": "0.0.20b3", "@elastic/makelogs": "^6.0.0", + "@elastic/ui-ace": "0.2.3", "@kbn/dev-utils": "1.0.0", "@kbn/es": "1.0.0", "@kbn/es-archiver": "1.0.0", @@ -383,20 +339,30 @@ "@types/zen-observable": "^0.8.0", "@typescript-eslint/eslint-plugin": "^3.7.1", "@typescript-eslint/parser": "^3.7.1", + "angular-aria": "^1.8.0", "angular-mocks": "^1.7.9", + "angular-recursion": "^1.0.5", + "angular-route": "^1.8.0", + "angular-sortable-view": "^0.0.17", "archiver": "^3.1.1", "axe-core": "^3.4.1", "babel-eslint": "^10.0.3", "babel-jest": "^25.5.1", "babel-plugin-istanbul": "^6.0.0", "backport": "5.5.1", + "brace": "0.11.1", "chai": "3.5.0", "chance": "1.0.18", "cheerio": "0.22.0", "chromedriver": "^84.0.0", "classnames": "2.2.6", + "compare-versions": "3.5.1", + "d3": "3.5.17", + "d3-cloud": "1.2.5", "dedent": "^0.7.0", + "deepmerge": "^4.2.2", "delete-empty": "^2.0.0", + "elasticsearch-browser": "^16.7.0", "enzyme": "^3.11.0", "enzyme-adapter-react-16": "^1.15.2", "enzyme-adapter-utils": "^1.13.0", @@ -421,6 +387,7 @@ "exit-hook": "^2.2.0", "faker": "1.1.0", "fetch-mock": "^7.3.9", + "fp-ts": "^2.3.1", "geckodriver": "^1.20.0", "getopts": "^2.2.4", "grunt": "1.0.4", @@ -432,7 +399,10 @@ "gulp-babel": "^8.0.0", "gulp-sourcemaps": "2.6.5", "has-ansi": "^3.0.0", + "history": "^4.9.0", + "hjson": "3.2.1", "iedriver": "^3.14.2", + "immer": "^1.5.0", "intl-messageformat-parser": "^1.4.0", "jest": "^25.5.4", "jest-canvas-mock": "^2.2.0", @@ -441,18 +411,30 @@ "jest-environment-jsdom-thirteen": "^1.0.1", "jest-raw-loader": "^1.0.1", "jimp": "^0.14.0", + "jquery": "^3.5.0", + "js-levenshtein": "^1.1.6", + "json-stringify-pretty-compact": "1.2.0", "json5": "^1.0.1", + "leaflet": "1.5.1", + "leaflet-draw": "0.4.14", + "leaflet-responsive-popup": "0.6.4", + "leaflet-vega": "^0.8.6", + "leaflet.heat": "0.2.0", + "less": "npm:@elastic/less@2.7.3-kibana", "license-checker": "^16.0.0", "listr": "^0.14.1", "load-grunt-config": "^3.0.1", "load-json-file": "^6.2.0", + "markdown-it": "^10.0.0", "mocha": "^7.1.1", "mock-fs": "^4.12.0", "mock-http-server": "1.3.0", + "monaco-editor": "~0.17.0", "ms-chromium-edge-driver": "^0.2.3", "multistream": "^2.1.1", "murmurhash3js": "3.0.1", "mutation-observer": "^1.0.3", + "ngreact": "0.5.1", "nock": "12.0.3", "normalize-path": "^3.0.0", "nyc": "^15.0.1", @@ -461,9 +443,21 @@ "pngjs": "^3.4.0", "postcss": "^7.0.32", "prettier": "^2.0.5", + "prop-types": "15.6.0", "proxyquire": "1.8.0", + "react-grid-layout": "^0.16.2", + "react-markdown": "^4.3.1", + "react-monaco-editor": "~0.27.0", "react-popper-tooltip": "^2.10.1", + "react-redux": "^7.2.0", + "react-resize-detector": "^4.2.0", + "react-router-dom": "^5.2.0", + "react-sizeme": "^2.3.6", + "reactcss": "1.2.3", + "redux": "^4.0.5", "regenerate": "^1.4.0", + "reselect": "^4.0.0", + "resize-observer-polyfill": "^1.5.0", "sass-lint": "^1.12.1", "selenium-webdriver": "^4.0.0-alpha.7", "simple-git": "1.116.0", @@ -472,9 +466,15 @@ "supertest": "^3.1.0", "supertest-as-promised": "^4.0.2", "tape": "^4.13.0", + "topojson-client": "3.0.0", "tree-kill": "^1.2.2", "typescript": "3.9.5", "typings-tester": "^0.3.2", + "ui-select": "0.19.8", + "vega": "^5.13.0", + "vega-lite": "^4.13.1", + "vega-schema-url-parser": "^1.1.0", + "vega-tooltip": "^0.12.0", "vinyl-fs": "^3.0.3", "xml2js": "^0.4.22", "xmlbuilder": "13.0.2", diff --git a/packages/kbn-optimizer/package.json b/packages/kbn-optimizer/package.json index 740555fd87897..b80d1365659dd 100644 --- a/packages/kbn-optimizer/package.json +++ b/packages/kbn-optimizer/package.json @@ -14,10 +14,6 @@ "@kbn/babel-preset": "1.0.0", "@kbn/dev-utils": "1.0.0", "@kbn/ui-shared-deps": "1.0.0", - "@types/compression-webpack-plugin": "^2.0.2", - "@types/loader-utils": "^1.1.3", - "@types/watchpack": "^1.1.5", - "@types/webpack": "^4.41.3", "autoprefixer": "^9.7.4", "babel-loader": "^8.0.6", "clean-webpack-plugin": "^3.0.0", @@ -46,5 +42,11 @@ "watchpack": "^1.6.0", "webpack": "^4.41.5", "webpack-merge": "^4.2.2" + }, + "devDependencies": { + "@types/compression-webpack-plugin": "^2.0.2", + "@types/loader-utils": "^1.1.3", + "@types/watchpack": "^1.1.5", + "@types/webpack": "^4.41.3" } } diff --git a/test/interpreter_functional/plugins/kbn_tp_run_pipeline/package.json b/test/interpreter_functional/plugins/kbn_tp_run_pipeline/package.json index c5e080e3c8175..e1ee1153a28ac 100644 --- a/test/interpreter_functional/plugins/kbn_tp_run_pipeline/package.json +++ b/test/interpreter_functional/plugins/kbn_tp_run_pipeline/package.json @@ -7,17 +7,15 @@ "templateVersion": "1.0.0" }, "license": "Apache-2.0", - "dependencies": { - "@elastic/eui": "27.4.1", - "react": "^16.12.0", - "react-dom": "^16.12.0" - }, "scripts": { "kbn": "node ../../../../scripts/kbn.js", "build": "rm -rf './target' && tsc" }, "devDependencies": { + "@elastic/eui": "27.4.1", "@kbn/plugin-helpers": "9.0.2", + "react": "^16.12.0", + "react-dom": "^16.12.0", "typescript": "3.9.5" } } diff --git a/test/plugin_functional/plugins/kbn_sample_panel_action/package.json b/test/plugin_functional/plugins/kbn_sample_panel_action/package.json index dac901f496304..b3cef400089b0 100644 --- a/test/plugin_functional/plugins/kbn_sample_panel_action/package.json +++ b/test/plugin_functional/plugins/kbn_sample_panel_action/package.json @@ -7,15 +7,13 @@ "templateVersion": "1.0.0" }, "license": "Apache-2.0", - "dependencies": { - "@elastic/eui": "27.4.1", - "react": "^16.12.0" - }, "scripts": { "kbn": "node ../../../../scripts/kbn.js", "build": "rm -rf './target' && tsc" }, "devDependencies": { + "@elastic/eui": "27.4.1", + "react": "^16.12.0", "typescript": "3.9.5" } } diff --git a/test/plugin_functional/plugins/kbn_tp_custom_visualizations/package.json b/test/plugin_functional/plugins/kbn_tp_custom_visualizations/package.json index b7c494807672e..9250a4499662f 100644 --- a/test/plugin_functional/plugins/kbn_tp_custom_visualizations/package.json +++ b/test/plugin_functional/plugins/kbn_tp_custom_visualizations/package.json @@ -7,16 +7,14 @@ "templateVersion": "1.0.0" }, "license": "Apache-2.0", - "dependencies": { - "@elastic/eui": "27.4.1", - "react": "^16.12.0" - }, "scripts": { "kbn": "node ../../../../scripts/kbn.js", "build": "rm -rf './target' && tsc" }, "devDependencies": { + "@elastic/eui": "27.4.1", "@kbn/plugin-helpers": "9.0.2", + "react": "^16.12.0", "typescript": "3.9.5" } } diff --git a/x-pack/package.json b/x-pack/package.json index 992a186d41d78..247130f4895bb 100644 --- a/x-pack/package.json +++ b/x-pack/package.json @@ -30,6 +30,8 @@ }, "devDependencies": { "@cypress/webpack-preprocessor": "^4.1.0", + "@elastic/apm-rum-react": "^1.2.3", + "@elastic/maki": "6.3.0", "@kbn/dev-utils": "1.0.0", "@kbn/es": "1.0.0", "@kbn/expect": "1.0.0", @@ -37,6 +39,10 @@ "@kbn/storybook": "1.0.0", "@kbn/test": "1.0.0", "@kbn/utility-types": "1.0.0", + "@mapbox/geojson-rewind": "^0.4.1", + "@mapbox/mapbox-gl-draw": "^1.2.0", + "@mapbox/mapbox-gl-rtl-text": "^0.2.3", + "@scant/router": "^0.1.0", "@storybook/addon-actions": "^5.3.19", "@storybook/addon-console": "^1.2.1", "@storybook/addon-info": "^5.3.19", @@ -47,6 +53,11 @@ "@testing-library/jest-dom": "^5.8.0", "@testing-library/react": "^9.3.2", "@testing-library/react-hooks": "^3.2.1", + "@turf/bbox": "6.0.1", + "@turf/bbox-polygon": "6.0.1", + "@turf/boolean-contains": "6.0.1", + "@turf/distance": "6.0.1", + "@turf/helpers": "6.0.1", "@types/angular": "^1.6.56", "@types/archiver": "^3.1.0", "@types/base64-js": "^1.2.5", @@ -75,6 +86,7 @@ "@types/history": "^4.7.3", "@types/hoist-non-react-statics": "^3.3.1", "@types/http-proxy": "^1.17.4", + "@types/http-proxy-agent": "^2.0.2", "@types/jest": "^25.2.3", "@types/jest-specific-snapshot": "^0.5.4", "@types/joi": "^13.4.2", @@ -124,6 +136,11 @@ "@types/xml2js": "^0.4.5", "@welldone-software/why-did-you-render": "^4.0.0", "abab": "^1.0.4", + "angular": "^1.8.0", + "angular-sanitize": "1.8.0", + "apollo-link": "^1.2.3", + "apollo-link-error": "^1.1.7", + "apollo-link-state": "^0.4.1", "autoprefixer": "^9.7.4", "axios": "^0.19.0", "babel-jest": "^25.5.1", @@ -131,14 +148,22 @@ "babel-plugin-require-context-hook": "npm:babel-plugin-require-context-hook-babel7@1.0.0", "base64-js": "^1.3.1", "base64url": "^3.0.1", + "brace": "0.11.1", + "broadcast-channel": "^3.0.3", "canvas": "^2.6.1", "chalk": "^4.1.0", "chance": "1.0.18", "cheerio": "0.22.0", "commander": "3.0.2", + "constate": "^1.3.2", + "copy-to-clipboard": "^3.0.8", "copy-webpack-plugin": "^6.0.2", + "cronstrue": "^1.51.0", "cypress": "4.11.0", "cypress-multi-reporters": "^1.2.3", + "d3": "3.5.17", + "d3-scale": "1.0.7", + "dragselect": "1.13.1", "enzyme": "^3.11.0", "enzyme-adapter-react-16": "^1.15.2", "enzyme-adapter-utils": "^1.13.0", @@ -146,6 +171,8 @@ "execa": "^4.0.2", "fancy-log": "^1.3.2", "fetch-mock": "^7.3.9", + "file-saver": "^1.3.8", + "formsy-react": "^1.1.5", "graphql-code-generator": "^0.18.2", "graphql-codegen-add": "^0.18.2", "graphql-codegen-introspection": "^0.18.2", @@ -155,16 +182,27 @@ "graphql-codegen-typescript-server": "^0.18.2", "gulp": "4.0.2", "hapi": "^17.5.3", + "he": "^1.2.0", + "history-extra": "^5.0.1", "hoist-non-react-statics": "^3.3.2", + "i18n-iso-countries": "^4.3.1", + "icalendar": "0.7.1", "jest": "^25.5.4", "jest-circus": "^25.5.4", "jest-cli": "^25.5.4", "jest-styled-components": "^7.0.2", + "js-search": "^1.4.3", "jsdom": "13.1.0", "jsondiffpatch": "0.4.1", + "jsts": "^1.6.2", + "kea": "^2.0.1", "loader-utils": "^1.2.3", + "lz-string": "^1.4.4", "madge": "3.4.4", + "mapbox-gl": "^1.10.0", + "mapbox-gl-draw-rectangle-mode": "^1.0.4", "marge": "^1.0.1", + "memoize-one": "^5.0.0", "mini-css-extract-plugin": "0.8.0", "mocha": "^7.1.1", "mocha-junit-reporter": "^1.23.1", @@ -174,14 +212,39 @@ "mutation-observer": "^1.0.3", "node-fetch": "^2.6.0", "null-loader": "^3.0.0", + "oboe": "^2.1.4", "pixelmatch": "^5.1.0", + "pluralize": "3.1.0", + "polished": "^1.9.2", "postcss": "^7.0.32", "postcss-loader": "^3.0.0", "postcss-prefix-selector": "^1.7.2", "proxyquire": "1.8.0", + "re-resizable": "^6.1.1", + "react-apollo": "^2.1.4", + "react-beautiful-dnd": "^12.2.0", "react-docgen-typescript-loader": "^3.1.1", + "react-dropzone": "^4.2.9", + "react-fast-compare": "^2.0.4", "react-is": "^16.8.0", + "react-markdown": "^4.3.1", + "react-reverse-portal": "^1.0.4", + "react-router": "^5.2.0", + "react-shortcuts": "^2.0.0", + "react-sticky": "^6.0.3", + "react-syntax-highlighter": "^5.7.0", "react-test-renderer": "^16.12.0", + "react-tiny-virtual-list": "^2.2.0", + "react-use": "^13.27.0", + "react-virtualized": "^9.21.2", + "react-vis": "^1.8.1", + "react-visibility-sensor": "^5.1.1", + "reduce-reducers": "^1.0.4", + "redux-actions": "^2.6.5", + "redux-saga": "^1.1.3", + "redux-thunks": "^1.0.0", + "reselect": "^4.0.0", + "resize-observer-polyfill": "^1.5.0", "rxjs-marbles": "^5.0.6", "sass-loader": "^8.0.2", "sass-resources-loader": "^2.0.1", @@ -190,10 +253,18 @@ "string-replace-loader": "^2.2.0", "supertest": "^3.1.0", "supertest-as-promised": "^4.0.2", + "suricata-sid-db": "^1.0.2", + "tinycolor2": "1.4.1", "tmp": "0.1.0", + "topojson-client": "3.0.0", "tree-kill": "^1.2.2", "ts-loader": "^6.0.4", "typescript": "3.9.5", + "typescript-fsa": "^3.0.0", + "typescript-fsa-reducers": "^1.2.1", + "unstated": "^2.1.1", + "use-resize-observer": "^6.0.0", + "venn.js": "0.2.20", "vinyl-fs": "^3.0.3", "whatwg-fetch": "^3.0.0", "xml-crypto": "^1.4.0", @@ -203,12 +274,10 @@ "@babel/core": "^7.11.1", "@babel/register": "^7.10.5", "@babel/runtime": "^7.11.2", - "@elastic/apm-rum-react": "^1.2.3", "@elastic/datemath": "5.0.3", "@elastic/ems-client": "7.9.3", "@elastic/eui": "27.4.1", "@elastic/filesaver": "1.1.2", - "@elastic/maki": "6.3.0", "@elastic/node-crypto": "1.2.1", "@elastic/numeral": "^2.5.0", "@elastic/safer-lodash-set": "0.0.0", @@ -217,57 +286,32 @@ "@kbn/i18n": "1.0.0", "@kbn/interpreter": "1.0.0", "@kbn/ui-framework": "1.0.0", - "@mapbox/geojson-rewind": "^0.4.1", - "@mapbox/mapbox-gl-draw": "^1.2.0", - "@mapbox/mapbox-gl-rtl-text": "^0.2.3", - "@scant/router": "^0.1.0", "@slack/webhook": "^5.0.0", - "@turf/bbox": "6.0.1", - "@turf/bbox-polygon": "6.0.1", - "@turf/boolean-contains": "6.0.1", "@turf/circle": "6.0.1", - "@turf/distance": "6.0.1", - "@turf/helpers": "6.0.1", - "@types/http-proxy-agent": "^2.0.2", - "angular": "^1.8.0", "angular-resource": "1.8.0", - "angular-sanitize": "1.8.0", "angular-ui-ace": "0.2.3", "apollo-cache-inmemory": "1.6.2", "apollo-client": "^2.3.8", - "apollo-link": "^1.2.3", - "apollo-link-error": "^1.1.7", "apollo-link-http": "^1.5.16", "apollo-link-schema": "^1.1.0", - "apollo-link-state": "^0.4.1", "apollo-server-errors": "^2.0.2", "apollo-server-hapi": "^1.3.6", "archiver": "3.1.1", "axios": "^0.19.0", "bluebird": "3.5.5", "boom": "^7.2.0", - "brace": "0.11.1", - "broadcast-channel": "^3.0.3", "chroma-js": "^1.4.1", "classnames": "2.2.6", "concat-stream": "1.6.2", - "constate": "^1.3.2", "content-disposition": "0.5.3", - "copy-to-clipboard": "^3.0.8", - "cronstrue": "^1.51.0", "cytoscape": "^3.10.0", - "d3": "3.5.17", "d3-array": "1.2.4", - "d3-scale": "1.0.7", "dedent": "^0.7.0", "del": "^5.1.0", - "dragselect": "1.13.1", "elasticsearch": "^16.7.0", "extract-zip": "^1.7.0", - "file-saver": "^1.3.8", "file-type": "^10.9.0", "font-awesome": "4.7.0", - "formsy-react": "^1.1.5", "fp-ts": "^2.3.1", "get-port": "^5.0.0", "getos": "^3.1.0", @@ -280,11 +324,7 @@ "graphql-tools": "^3.0.2", "h2o2": "^8.1.2", "handlebars": "4.7.6", - "he": "^1.2.0", "history": "4.9.0", - "history-extra": "^5.0.1", - "i18n-iso-countries": "^4.3.1", - "icalendar": "0.7.1", "idx": "^2.5.6", "immer": "^1.5.0", "inline-style": "^2.0.0", @@ -293,18 +333,11 @@ "isbinaryfile": "4.0.2", "joi": "^13.5.2", "jquery": "^3.5.0", - "js-search": "^1.4.3", "js-yaml": "3.13.1", "json-stable-stringify": "^1.0.1", "jsonwebtoken": "^8.5.1", - "jsts": "^1.6.2", - "kea": "^2.0.1", "lodash": "^4.17.15", - "lz-string": "^1.4.4", - "mapbox-gl": "^1.10.0", - "mapbox-gl-draw-rectangle-mode": "^1.0.4", "markdown-it": "^10.0.0", - "memoize-one": "^5.0.0", "mime": "^2.4.4", "moment": "^2.24.0", "moment-duration-format": "^2.3.2", @@ -315,54 +348,29 @@ "nodemailer": "^4.7.0", "object-hash": "^1.3.1", "object-path-immutable": "^3.1.1", - "oboe": "^2.1.4", "oppsy": "^2.0.0", "p-retry": "^4.2.0", "papaparse": "^5.2.0", "pdfmake": "^0.1.65", - "pluralize": "3.1.0", "pngjs": "3.4.0", - "polished": "^1.9.2", "prop-types": "^15.6.0", "proper-lockfile": "^3.2.0", "puid": "1.0.7", "puppeteer-core": "^1.19.0", "query-string": "5.1.1", "raw-loader": "3.1.0", - "re-resizable": "^6.1.1", "react": "^16.12.0", - "react-apollo": "^2.1.4", - "react-beautiful-dnd": "^12.2.0", "react-datetime": "^2.14.0", "react-dom": "^16.12.0", - "react-dropzone": "^4.2.9", - "react-fast-compare": "^2.0.4", - "react-markdown": "^4.3.1", "react-moment-proptypes": "^1.7.0", "react-portal": "^3.2.0", "react-redux": "^7.2.0", - "react-reverse-portal": "^1.0.4", - "react-router": "^5.2.0", "react-router-dom": "^5.2.0", - "react-shortcuts": "^2.0.0", - "react-sticky": "^6.0.3", - "react-syntax-highlighter": "^5.7.0", - "react-tiny-virtual-list": "^2.2.0", - "react-use": "^13.27.0", - "react-virtualized": "^9.21.2", - "react-vis": "^1.8.1", - "react-visibility-sensor": "^5.1.1", "recompose": "^0.26.0", - "reduce-reducers": "^1.0.4", "redux": "^4.0.5", - "redux-actions": "^2.6.5", "redux-observable": "^1.2.0", - "redux-saga": "^1.1.3", "redux-thunk": "^2.3.0", - "redux-thunks": "^1.0.0", "request": "^2.88.0", - "reselect": "^4.0.0", - "resize-observer-polyfill": "^1.5.0", "rison-node": "0.3.1", "rxjs": "^6.5.5", "semver": "5.7.0", @@ -371,18 +379,10 @@ "stats-lite": "^2.2.0", "style-it": "^2.1.3", "styled-components": "^5.1.0", - "suricata-sid-db": "^1.0.2", - "tinycolor2": "1.4.1", "tinymath": "1.2.1", - "topojson-client": "3.0.0", "tslib": "^2.0.0", - "typescript-fsa": "^3.0.0", - "typescript-fsa-reducers": "^1.2.1", "ui-select": "0.19.8", - "unstated": "^2.1.1", - "use-resize-observer": "^6.0.0", "uuid": "3.3.2", - "venn.js": "0.2.20", "vscode-languageserver": "^5.2.1", "webpack": "^4.41.5", "wellknown": "^0.5.0", diff --git a/x-pack/plugins/apm/e2e/package.json b/x-pack/plugins/apm/e2e/package.json index 5101e64235c62..d113b465fdf2f 100644 --- a/x-pack/plugins/apm/e2e/package.json +++ b/x-pack/plugins/apm/e2e/package.json @@ -10,8 +10,6 @@ "dependencies": { "@cypress/snapshot": "^2.1.3", "@cypress/webpack-preprocessor": "^5.4.1", - "@types/cypress-cucumber-preprocessor": "^1.14.1", - "@types/node": "^14.0.14", "axios": "^0.19.2", "cypress": "^4.9.0", "cypress-cucumber-preprocessor": "^2.5.2", @@ -23,5 +21,9 @@ "wait-on": "^5.0.1", "webpack": "^4.43.0", "yargs": "^15.4.0" + }, + "devDependencies": { + "@types/cypress-cucumber-preprocessor": "^1.14.1", + "@types/node": "^14.0.14" } } diff --git a/x-pack/plugins/apm/scripts/package.json b/x-pack/plugins/apm/scripts/package.json index 4d0906514b5e1..d3e2d42f972a9 100644 --- a/x-pack/plugins/apm/scripts/package.json +++ b/x-pack/plugins/apm/scripts/package.json @@ -6,8 +6,10 @@ "dependencies": { "@elastic/elasticsearch": "7.9.0-rc.1", "@octokit/rest": "^16.35.0", - "@types/console-stamp": "^0.2.32", "console-stamp": "^0.2.9", "hdr-histogram-js": "^1.2.0" + }, + "devDependencies": { + "@types/console-stamp": "^0.2.32" } } diff --git a/x-pack/plugins/security_solution/package.json b/x-pack/plugins/security_solution/package.json index 703ef6584f164..687099541b3d2 100644 --- a/x-pack/plugins/security_solution/package.json +++ b/x-pack/plugins/security_solution/package.json @@ -13,9 +13,7 @@ "test:generate": "ts-node --project scripts/endpoint/cli_tsconfig.json scripts/endpoint/resolver_generator.ts" }, "devDependencies": { - "@types/md5": "^2.2.0" - }, - "dependencies": { + "@types/md5": "^2.2.0", "@types/rbush": "^3.0.0", "@types/seedrandom": ">=2.0.0 <4.0.0", "querystring": "^0.2.0", diff --git a/x-pack/plugins/security_solution/yarn.lock b/x-pack/plugins/security_solution/yarn.lock deleted file mode 120000 index 6e09764ec763b..0000000000000 --- a/x-pack/plugins/security_solution/yarn.lock +++ /dev/null @@ -1 +0,0 @@ -../../../yarn.lock \ No newline at end of file From c634208e4f2426b9762b46d88073121529df23d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yulia=20=C4=8Cech?= <6585477+yuliacech@users.noreply.github.com> Date: Tue, 25 Aug 2020 18:59:47 +0200 Subject: [PATCH 45/71] [ILM] TS conversion of Edit policy page (#75148) * [ILM] TS conversion of Edit policy page * [ILM] Deleted some constants * [ILM] Fixed imports * [ILM] Fixed imports * [ILM] Clean up * [ILM] Clean up * [ILM] Fixed ui_metric jest test * [ILM] Fixed ui_metric jest test * [ILM] Fix review suggestions Co-authored-by: Elastic Machine --- .../edit_policy/constants.ts | 10 +- .../edit_policy/edit_policy.helpers.tsx | 2 - .../edit_policy/edit_policy.test.ts | 8 +- .../__jest__/components/edit_policy.test.js | 28 +- .../public/application/app.tsx | 2 +- .../public/application/constants/index.ts | 99 +---- .../public/application/constants/policy.ts | 60 +++ .../edit_policy/components/form_errors.tsx | 12 +- .../edit_policy/components/min_age_input.tsx | 58 ++- .../components/node_allocation.tsx | 41 +- .../components/policy_json_flyout.tsx | 15 +- .../components/set_priority_input.tsx | 31 +- .../edit_policy/edit_policy.container.js | 58 --- .../edit_policy/edit_policy.container.tsx | 83 ++++ .../sections/edit_policy/edit_policy.js | 390 ------------------ .../sections/edit_policy/edit_policy.tsx | 383 +++++++++++++++++ .../sections/edit_policy/index.d.ts | 7 - .../edit_policy/{index.js => index.ts} | 0 .../edit_policy/phases/cold_phase.tsx | 67 ++- .../edit_policy/phases/delete_phase.tsx | 34 +- .../sections/edit_policy/phases/hot_phase.tsx | 262 ++++++------ .../edit_policy/phases/warm_phase.tsx | 138 +++---- .../components/policy_table/policy_table.js | 2 +- .../public/application/services/api.ts | 13 +- .../application/services/find_errors.js | 24 -- .../services/policies/cold_phase.ts | 159 +++++++ .../services/policies/delete_phase.ts | 88 ++++ .../services/policies/hot_phase.ts | 155 +++++++ .../policies/policy_save.ts} | 30 +- .../services/policies/policy_serialization.ts | 104 +++++ .../services/policies/policy_validation.ts | 191 +++++++++ .../application/services/policies/types.ts | 140 +++++++ .../services/policies/warm_phase.ts | 219 ++++++++++ .../{ui_metric.test.js => ui_metric.test.ts} | 19 +- .../public/application/services/ui_metric.ts | 64 +-- .../application/store/actions/general.js | 11 - .../public/application/store/actions/index.js | 3 - .../public/application/store/actions/nodes.js | 8 - .../application/store/actions/policies.js | 7 - .../application/store/defaults/cold_phase.js | 30 -- .../store/defaults/delete_phase.js | 23 -- .../application/store/defaults/hot_phase.js | 35 -- .../application/store/defaults/index.d.ts | 10 - .../application/store/defaults/index.js | 10 - .../application/store/defaults/warm_phase.js | 39 -- .../application/store/reducers/general.js | 38 -- .../application/store/reducers/index.js | 4 - .../application/store/reducers/nodes.js | 50 --- .../application/store/reducers/policies.js | 97 ----- .../application/store/selectors/general.js | 9 - .../application/store/selectors/index.js | 3 - .../application/store/selectors/lifecycle.js | 287 ------------- .../application/store/selectors/nodes.js | 12 - .../application/store/selectors/policies.js | 291 +------------ 54 files changed, 1996 insertions(+), 1967 deletions(-) create mode 100644 x-pack/plugins/index_lifecycle_management/public/application/constants/policy.ts delete mode 100644 x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.container.js create mode 100644 x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.container.tsx delete mode 100644 x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.js create mode 100644 x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.tsx delete mode 100644 x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/index.d.ts rename x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/{index.js => index.ts} (100%) delete mode 100644 x-pack/plugins/index_lifecycle_management/public/application/services/find_errors.js create mode 100644 x-pack/plugins/index_lifecycle_management/public/application/services/policies/cold_phase.ts create mode 100644 x-pack/plugins/index_lifecycle_management/public/application/services/policies/delete_phase.ts create mode 100644 x-pack/plugins/index_lifecycle_management/public/application/services/policies/hot_phase.ts rename x-pack/plugins/index_lifecycle_management/public/application/{store/actions/lifecycle.js => services/policies/policy_save.ts} (58%) create mode 100644 x-pack/plugins/index_lifecycle_management/public/application/services/policies/policy_serialization.ts create mode 100644 x-pack/plugins/index_lifecycle_management/public/application/services/policies/policy_validation.ts create mode 100644 x-pack/plugins/index_lifecycle_management/public/application/services/policies/types.ts create mode 100644 x-pack/plugins/index_lifecycle_management/public/application/services/policies/warm_phase.ts rename x-pack/plugins/index_lifecycle_management/public/application/services/{ui_metric.test.js => ui_metric.test.ts} (75%) delete mode 100644 x-pack/plugins/index_lifecycle_management/public/application/store/actions/general.js delete mode 100644 x-pack/plugins/index_lifecycle_management/public/application/store/actions/nodes.js delete mode 100644 x-pack/plugins/index_lifecycle_management/public/application/store/defaults/cold_phase.js delete mode 100644 x-pack/plugins/index_lifecycle_management/public/application/store/defaults/delete_phase.js delete mode 100644 x-pack/plugins/index_lifecycle_management/public/application/store/defaults/hot_phase.js delete mode 100644 x-pack/plugins/index_lifecycle_management/public/application/store/defaults/index.d.ts delete mode 100644 x-pack/plugins/index_lifecycle_management/public/application/store/defaults/index.js delete mode 100644 x-pack/plugins/index_lifecycle_management/public/application/store/defaults/warm_phase.js delete mode 100644 x-pack/plugins/index_lifecycle_management/public/application/store/reducers/general.js delete mode 100644 x-pack/plugins/index_lifecycle_management/public/application/store/reducers/nodes.js delete mode 100644 x-pack/plugins/index_lifecycle_management/public/application/store/selectors/general.js delete mode 100644 x-pack/plugins/index_lifecycle_management/public/application/store/selectors/lifecycle.js delete mode 100644 x-pack/plugins/index_lifecycle_management/public/application/store/selectors/nodes.js diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/constants.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/constants.ts index e5037a6477aca..acf642f250a7b 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/constants.ts +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/constants.ts @@ -4,21 +4,20 @@ * you may not use this file except in compliance with the Elastic License. */ +import { PolicyFromES } from '../../../public/application/services/policies/types'; + export const POLICY_NAME = 'my_policy'; export const SNAPSHOT_POLICY_NAME = 'my_snapshot_policy'; export const NEW_SNAPSHOT_POLICY_NAME = 'my_new_snapshot_policy'; -export const DELETE_PHASE_POLICY = { +export const DELETE_PHASE_POLICY: PolicyFromES = { version: 1, - modified_date: Date.now(), + modified_date: Date.now().toString(), policy: { phases: { hot: { min_age: '0ms', actions: { - set_priority: { - priority: null, - }, rollover: { max_size: '50gb', }, @@ -36,6 +35,7 @@ export const DELETE_PHASE_POLICY = { }, }, }, + name: POLICY_NAME, }, name: POLICY_NAME, }; diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.helpers.tsx b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.helpers.tsx index ebe1c12e2a079..6365bb8caa963 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.helpers.tsx +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.helpers.tsx @@ -13,7 +13,6 @@ import { POLICY_NAME } from './constants'; import { TestSubjects } from '../helpers'; import { EditPolicy } from '../../../public/application/sections/edit_policy'; -import { indexLifecycleManagementStore } from '../../../public/application/store'; jest.mock('@elastic/eui', () => { const original = jest.requireActual('@elastic/eui'); @@ -35,7 +34,6 @@ jest.mock('@elastic/eui', () => { }); const testBedConfig: TestBedConfig = { - store: () => indexLifecycleManagementStore(), memoryRouter: { initialEntries: [`/policies/edit/${POLICY_NAME}`], componentRoutePath: `/policies/edit/:policyName`, diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.test.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.test.ts index 06829e6ef6f1e..36feb3f6203c8 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.test.ts +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.test.ts @@ -40,8 +40,8 @@ describe('', () => { test('wait for snapshot policy field should correctly display snapshot policy name', () => { expect(testBed.find('snapshotPolicyCombobox').prop('data-currentvalue')).toEqual([ { - label: DELETE_PHASE_POLICY.policy.phases.delete.actions.wait_for_snapshot.policy, - value: DELETE_PHASE_POLICY.policy.phases.delete.actions.wait_for_snapshot.policy, + label: DELETE_PHASE_POLICY.policy.phases.delete?.actions.wait_for_snapshot?.policy, + value: DELETE_PHASE_POLICY.policy.phases.delete?.actions.wait_for_snapshot?.policy, }, ]); }); @@ -59,7 +59,7 @@ describe('', () => { delete: { ...DELETE_PHASE_POLICY.policy.phases.delete, actions: { - ...DELETE_PHASE_POLICY.policy.phases.delete.actions, + ...DELETE_PHASE_POLICY.policy.phases.delete?.actions, wait_for_snapshot: { policy: NEW_SNAPSHOT_POLICY_NAME, }, @@ -96,7 +96,7 @@ describe('', () => { delete: { ...DELETE_PHASE_POLICY.policy.phases.delete, actions: { - ...DELETE_PHASE_POLICY.policy.phases.delete.actions, + ...DELETE_PHASE_POLICY.policy.phases.delete?.actions, }, }, }, diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/components/edit_policy.test.js b/x-pack/plugins/index_lifecycle_management/__jest__/components/edit_policy.test.js index 4fe3d5c66696e..81c30579cd4dd 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/components/edit_policy.test.js +++ b/x-pack/plugins/index_lifecycle_management/__jest__/components/edit_policy.test.js @@ -7,7 +7,6 @@ import React from 'react'; import { act } from 'react-dom/test-utils'; import moment from 'moment-timezone'; -import { Provider } from 'react-redux'; // axios has a $http like interface so using it to simulate $http import axios from 'axios'; import axiosXhrAdapter from 'axios/lib/adapters/xhr'; @@ -21,9 +20,7 @@ import { import { usageCollectionPluginMock } from '../../../../../src/plugins/usage_collection/public/mocks'; import { mountWithIntl } from '../../../../test_utils/enzyme_helpers'; -import { fetchedPolicies } from '../../public/application/store/actions'; -import { indexLifecycleManagementStore } from '../../public/application/store'; -import { EditPolicy } from '../../public/application/sections/edit_policy'; +import { EditPolicy } from '../../public/application/sections/edit_policy/edit_policy'; import { init as initHttp } from '../../public/application/services/http'; import { init as initUiMetric } from '../../public/application/services/ui_metric'; import { init as initNotification } from '../../public/application/services/notification'; @@ -40,7 +37,7 @@ import { policyNameMustBeDifferentErrorMessage, policyNameAlreadyUsedErrorMessage, maximumDocumentsRequiredMessage, -} from '../../public/application/store/selectors'; +} from '../../public/application/services/policies/policy_validation'; initHttp(axios.create({ adapter: axiosXhrAdapter })); initUiMetric(usageCollectionPluginMock.createSetupContract()); @@ -51,7 +48,6 @@ initNotification( let server; let httpRequestsMockHelpers; -let store; const policy = { phases: { hot: { @@ -128,13 +124,14 @@ const save = (rendered) => { }; describe('edit policy', () => { beforeEach(() => { - store = indexLifecycleManagementStore(); component = ( - - {} }} getUrlForApp={() => {}} /> - + {} }} + getUrlForApp={() => {}} + policies={policies} + policyName={''} + /> ); - store.dispatch(fetchedPolicies(policies)); ({ server, httpRequestsMockHelpers } = initHttpRequests()); httpRequestsMockHelpers.setPoliciesResponse(policies); @@ -162,9 +159,12 @@ describe('edit policy', () => { }); test('should show error when trying to save as new policy but using the same name', () => { component = ( - - {}} /> - + {}} + history={{ push: () => {} }} + /> ); const rendered = mountWithIntl(component); findTestSubject(rendered, 'saveAsNewSwitch').simulate('click'); diff --git a/x-pack/plugins/index_lifecycle_management/public/application/app.tsx b/x-pack/plugins/index_lifecycle_management/public/application/app.tsx index 14b0e72317c66..f7f8b30324bca 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/app.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/app.tsx @@ -9,7 +9,7 @@ import { Router, Switch, Route, Redirect } from 'react-router-dom'; import { ScopedHistory, ApplicationStart } from 'kibana/public'; import { METRIC_TYPE } from '@kbn/analytics'; -import { UIM_APP_LOAD } from './constants'; +import { UIM_APP_LOAD } from './constants/ui_metric'; import { EditPolicy } from './sections/edit_policy'; import { PolicyTable } from './sections/policy_table'; import { trackUiMetric } from './services/ui_metric'; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/constants/index.ts b/x-pack/plugins/index_lifecycle_management/public/application/constants/index.ts index 6319fc0d68543..61c197f2ba149 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/constants/index.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/constants/index.ts @@ -4,102 +4,5 @@ * you may not use this file except in compliance with the Elastic License. */ +export * from './policy'; export * from './ui_metric'; - -export const SET_PHASE_DATA: string = 'SET_PHASE_DATA'; -export const SET_SELECTED_NODE_ATTRS: string = 'SET_SELECTED_NODE_ATTRS'; -export const PHASE_HOT: string = 'hot'; -export const PHASE_WARM: string = 'warm'; -export const PHASE_COLD: string = 'cold'; -export const PHASE_DELETE: string = 'delete'; - -export const PHASE_ENABLED: string = 'phaseEnabled'; - -export const PHASE_ROLLOVER_ENABLED: string = 'rolloverEnabled'; -export const WARM_PHASE_ON_ROLLOVER: string = 'warmPhaseOnRollover'; -export const PHASE_ROLLOVER_ALIAS: string = 'selectedAlias'; -export const PHASE_ROLLOVER_MAX_AGE: string = 'selectedMaxAge'; -export const PHASE_ROLLOVER_MAX_AGE_UNITS: string = 'selectedMaxAgeUnits'; -export const PHASE_ROLLOVER_MAX_SIZE_STORED: string = 'selectedMaxSizeStored'; -export const PHASE_ROLLOVER_MAX_DOCUMENTS: string = 'selectedMaxDocuments'; -export const PHASE_ROLLOVER_MAX_SIZE_STORED_UNITS: string = 'selectedMaxSizeStoredUnits'; -export const PHASE_ROLLOVER_MINIMUM_AGE: string = 'selectedMinimumAge'; -export const PHASE_ROLLOVER_MINIMUM_AGE_UNITS: string = 'selectedMinimumAgeUnits'; - -export const PHASE_FORCE_MERGE_SEGMENTS: string = 'selectedForceMergeSegments'; -export const PHASE_FORCE_MERGE_ENABLED: string = 'forceMergeEnabled'; -export const PHASE_FREEZE_ENABLED: string = 'freezeEnabled'; - -export const PHASE_SHRINK_ENABLED: string = 'shrinkEnabled'; - -export const PHASE_NODE_ATTRS: string = 'selectedNodeAttrs'; -export const PHASE_PRIMARY_SHARD_COUNT: string = 'selectedPrimaryShardCount'; -export const PHASE_REPLICA_COUNT: string = 'selectedReplicaCount'; -export const PHASE_INDEX_PRIORITY: string = 'phaseIndexPriority'; - -export const PHASE_WAIT_FOR_SNAPSHOT_POLICY = 'waitForSnapshotPolicy'; - -export const PHASE_ATTRIBUTES_THAT_ARE_NUMBERS_VALIDATE: string[] = [ - PHASE_ROLLOVER_MINIMUM_AGE, - PHASE_FORCE_MERGE_SEGMENTS, - PHASE_PRIMARY_SHARD_COUNT, - PHASE_REPLICA_COUNT, - PHASE_INDEX_PRIORITY, -]; -export const PHASE_ATTRIBUTES_THAT_ARE_NUMBERS: string[] = [ - ...PHASE_ATTRIBUTES_THAT_ARE_NUMBERS_VALIDATE, - PHASE_ROLLOVER_MAX_AGE, - PHASE_ROLLOVER_MAX_SIZE_STORED, - PHASE_ROLLOVER_MAX_DOCUMENTS, -]; - -export const STRUCTURE_INDEX_TEMPLATE: string = 'indexTemplate'; -export const STRUCTURE_TEMPLATE_SELECTION: string = 'templateSelection'; -export const STRUCTURE_TEMPLATE_NAME: string = 'templateName'; -export const STRUCTURE_CONFIGURATION: string = 'configuration'; -export const STRUCTURE_NODE_ATTRS: string = 'node_attrs'; -export const STRUCTURE_PRIMARY_NODES: string = 'primary_nodes'; -export const STRUCTURE_REPLICAS: string = 'replicas'; - -export const STRUCTURE_POLICY_CONFIGURATION: string = 'policyConfiguration'; - -export const STRUCTURE_REVIEW: string = 'review'; -export const STRUCTURE_POLICY_NAME: string = 'policyName'; -export const STRUCTURE_INDEX_NAME: string = 'indexName'; -export const STRUCTURE_ALIAS_NAME: string = 'aliasName'; - -export const ERROR_STRUCTURE: any = { - [PHASE_HOT]: { - [PHASE_ROLLOVER_ALIAS]: [], - [PHASE_ROLLOVER_MAX_AGE]: [], - [PHASE_ROLLOVER_MAX_AGE_UNITS]: [], - [PHASE_ROLLOVER_MAX_SIZE_STORED]: [], - [PHASE_ROLLOVER_MAX_DOCUMENTS]: [], - [PHASE_ROLLOVER_MAX_SIZE_STORED_UNITS]: [], - [PHASE_INDEX_PRIORITY]: [], - }, - [PHASE_WARM]: { - [PHASE_ROLLOVER_ALIAS]: [], - [PHASE_ROLLOVER_MINIMUM_AGE]: [], - [PHASE_ROLLOVER_MINIMUM_AGE_UNITS]: [], - [PHASE_NODE_ATTRS]: [], - [PHASE_PRIMARY_SHARD_COUNT]: [], - [PHASE_REPLICA_COUNT]: [], - [PHASE_FORCE_MERGE_SEGMENTS]: [], - [PHASE_INDEX_PRIORITY]: [], - }, - [PHASE_COLD]: { - [PHASE_ROLLOVER_ALIAS]: [], - [PHASE_ROLLOVER_MINIMUM_AGE]: [], - [PHASE_ROLLOVER_MINIMUM_AGE_UNITS]: [], - [PHASE_NODE_ATTRS]: [], - [PHASE_REPLICA_COUNT]: [], - [PHASE_INDEX_PRIORITY]: [], - }, - [PHASE_DELETE]: { - [PHASE_ROLLOVER_ALIAS]: [], - [PHASE_ROLLOVER_MINIMUM_AGE]: [], - [PHASE_ROLLOVER_MINIMUM_AGE_UNITS]: [], - }, - [STRUCTURE_POLICY_NAME]: [], -}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/constants/policy.ts b/x-pack/plugins/index_lifecycle_management/public/application/constants/policy.ts new file mode 100644 index 0000000000000..3a19f03547b5b --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/constants/policy.ts @@ -0,0 +1,60 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + SerializedPhase, + ColdPhase, + DeletePhase, + HotPhase, + WarmPhase, +} from '../services/policies/types'; + +export const defaultNewHotPhase: HotPhase = { + phaseEnabled: true, + rolloverEnabled: true, + selectedMaxAge: '30', + selectedMaxAgeUnits: 'd', + selectedMaxSizeStored: '50', + selectedMaxSizeStoredUnits: 'gb', + phaseIndexPriority: '100', + selectedMaxDocuments: '', +}; + +export const defaultNewWarmPhase: WarmPhase = { + phaseEnabled: false, + forceMergeEnabled: false, + selectedForceMergeSegments: '', + selectedMinimumAge: '0', + selectedMinimumAgeUnits: 'd', + selectedNodeAttrs: '', + shrinkEnabled: false, + selectedPrimaryShardCount: '', + selectedReplicaCount: '', + warmPhaseOnRollover: true, + phaseIndexPriority: '50', +}; + +export const defaultNewColdPhase: ColdPhase = { + phaseEnabled: false, + selectedMinimumAge: '0', + selectedMinimumAgeUnits: 'd', + selectedNodeAttrs: '', + selectedReplicaCount: '', + freezeEnabled: false, + phaseIndexPriority: '0', +}; + +export const defaultNewDeletePhase: DeletePhase = { + phaseEnabled: false, + selectedMinimumAge: '0', + selectedMinimumAgeUnits: 'd', + waitForSnapshotPolicy: '', +}; + +export const serializedPhaseInitialization: SerializedPhase = { + min_age: '0ms', + actions: {}, +}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/form_errors.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/form_errors.tsx index a3278b6c231b9..9db40ebf5521f 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/form_errors.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/form_errors.tsx @@ -8,28 +8,22 @@ import React, { cloneElement, Children, Fragment, ReactElement } from 'react'; import { EuiFormRow, EuiFormRowProps } from '@elastic/eui'; type Props = EuiFormRowProps & { - errorKey: string; isShowingErrors: boolean; - errors: Record; + errors?: string[]; }; export const ErrableFormRow: React.FunctionComponent = ({ - errorKey, isShowingErrors, errors, children, ...rest }) => { return ( - 0} - error={errors[errorKey]} - {...rest} - > + 0} error={errors} {...rest}> {Children.map(children, (child) => cloneElement(child as ReactElement, { - isInvalid: isShowingErrors && errors[errorKey].length > 0, + isInvalid: errors && isShowingErrors && errors.length > 0, }) )} diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/min_age_input.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/min_age_input.tsx index c9732f2311758..11b743ecc4bb6 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/min_age_input.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/min_age_input.tsx @@ -9,40 +9,35 @@ import { i18n } from '@kbn/i18n'; import { EuiFieldNumber, EuiFlexGroup, EuiFlexItem, EuiFormRow, EuiSelect } from '@elastic/eui'; -import { - PHASE_ROLLOVER_MINIMUM_AGE, - PHASE_ROLLOVER_MINIMUM_AGE_UNITS, - PHASE_WARM, - PHASE_COLD, - PHASE_DELETE, -} from '../../../constants'; import { LearnMoreLink } from './learn_more_link'; import { ErrableFormRow } from './form_errors'; +import { PhaseValidationErrors, propertyof } from '../../../services/policies/policy_validation'; +import { ColdPhase, DeletePhase, Phase, Phases, WarmPhase } from '../../../services/policies/types'; -function getTimingLabelForPhase(phase: string) { +function getTimingLabelForPhase(phase: keyof Phases) { // NOTE: Hot phase isn't necessary, because indices begin in the hot phase. switch (phase) { - case PHASE_WARM: + case 'warm': return i18n.translate('xpack.indexLifecycleMgmt.editPolicy.phaseWarm.minimumAgeLabel', { defaultMessage: 'Timing for warm phase', }); - case PHASE_COLD: + case 'cold': return i18n.translate('xpack.indexLifecycleMgmt.editPolicy.phaseCold.minimumAgeLabel', { defaultMessage: 'Timing for cold phase', }); - case PHASE_DELETE: + case 'delete': return i18n.translate('xpack.indexLifecycleMgmt.editPolicy.phaseDelete.minimumAgeLabel', { defaultMessage: 'Timing for delete phase', }); } } -function getUnitsAriaLabelForPhase(phase: string) { +function getUnitsAriaLabelForPhase(phase: keyof Phases) { // NOTE: Hot phase isn't necessary, because indices begin in the hot phase. switch (phase) { - case PHASE_WARM: + case 'warm': return i18n.translate( 'xpack.indexLifecycleMgmt.editPolicy.phaseWarm.minimumAgeUnitsAriaLabel', { @@ -50,7 +45,7 @@ function getUnitsAriaLabelForPhase(phase: string) { } ); - case PHASE_COLD: + case 'cold': return i18n.translate( 'xpack.indexLifecycleMgmt.editPolicy.phaseCold.minimumAgeUnitsAriaLabel', { @@ -58,7 +53,7 @@ function getUnitsAriaLabelForPhase(phase: string) { } ); - case PHASE_DELETE: + case 'delete': return i18n.translate( 'xpack.indexLifecycleMgmt.editPolicy.phaseDelete.minimumAgeUnitsAriaLabel', { @@ -68,24 +63,23 @@ function getUnitsAriaLabelForPhase(phase: string) { } } -interface Props { +interface Props { rolloverEnabled: boolean; - errors: Record; - phase: string; - // TODO add types for phaseData and setPhaseData after policy is typed - phaseData: any; - setPhaseData: (dataKey: string, value: any) => void; + errors?: PhaseValidationErrors; + phase: keyof Phases & string; + phaseData: T; + setPhaseData: (dataKey: keyof T & string, value: string) => void; isShowingErrors: boolean; } -export const MinAgeInput: React.FunctionComponent = ({ +export const MinAgeInput = ({ rolloverEnabled, errors, phaseData, phase, setPhaseData, isShowingErrors, -}) => { +}: React.PropsWithChildren>): React.ReactElement => { let daysOptionLabel; let hoursOptionLabel; let minutesOptionLabel; @@ -192,15 +186,17 @@ export const MinAgeInput: React.FunctionComponent = ({ ); } + // check that these strings are valid properties + const selectedMinimumAgeProperty = propertyof('selectedMinimumAge'); + const selectedMinimumAgeUnitsProperty = propertyof('selectedMinimumAgeUnits'); return ( = ({ } > { - setPhaseData(PHASE_ROLLOVER_MINIMUM_AGE, e.target.value); + setPhaseData(selectedMinimumAgeProperty, e.target.value); }} min={0} /> @@ -227,8 +223,8 @@ export const MinAgeInput: React.FunctionComponent = ({ setPhaseData(PHASE_ROLLOVER_MINIMUM_AGE_UNITS, e.target.value)} + value={phaseData.selectedMinimumAgeUnits} + onChange={(e) => setPhaseData(selectedMinimumAgeUnitsProperty, e.target.value)} options={[ { value: 'd', diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/node_allocation.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/node_allocation.tsx index 576483a5ab9c2..0ce2c0d7ea566 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/node_allocation.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/node_allocation.tsx @@ -16,20 +16,12 @@ import { EuiButton, } from '@elastic/eui'; -import { PHASE_NODE_ATTRS } from '../../../constants'; import { LearnMoreLink } from './learn_more_link'; import { ErrableFormRow } from './form_errors'; import { useLoadNodes } from '../../../services/api'; import { NodeAttrsDetails } from './node_attrs_details'; - -interface Props { - phase: string; - errors: Record; - // TODO add types for phaseData and setPhaseData after policy is typed - phaseData: any; - setPhaseData: (dataKey: string, value: any) => void; - isShowingErrors: boolean; -} +import { ColdPhase, Phase, Phases, WarmPhase } from '../../../services/policies/types'; +import { PhaseValidationErrors, propertyof } from '../../../services/policies/policy_validation'; const learnMoreLink = ( @@ -46,13 +38,20 @@ const learnMoreLink = ( ); -export const NodeAllocation: React.FunctionComponent = ({ +interface Props { + phase: keyof Phases & string; + errors?: PhaseValidationErrors; + phaseData: T; + setPhaseData: (dataKey: keyof T & string, value: string) => void; + isShowingErrors: boolean; +} +export const NodeAllocation = ({ phase, setPhaseData, errors, phaseData, isShowingErrors, -}) => { +}: React.PropsWithChildren>) => { const { isLoading, data: nodes, error, sendRequest } = useLoadNodes(); const [selectedNodeAttrsForDetails, setSelectedNodeAttrsForDetails] = useState( @@ -140,33 +139,35 @@ export const NodeAllocation: React.FunctionComponent = ({ ); } + // check that this string is a valid property + const nodeAttrsProperty = propertyof('selectedNodeAttrs'); + return ( { - setPhaseData(PHASE_NODE_ATTRS, e.target.value); + setPhaseData(nodeAttrsProperty, e.target.value); }} /> - {!!phaseData[PHASE_NODE_ATTRS] ? ( + {!!phaseData.selectedNodeAttrs ? ( setSelectedNodeAttrsForDetails(phaseData[PHASE_NODE_ATTRS])} + onClick={() => setSelectedNodeAttrsForDetails(phaseData.selectedNodeAttrs)} > void; - // TODO add types for lifecycle after policy is typed - lifecycle: any; + policy: Policy; policyName: string; } -export const PolicyJsonFlyout: React.FunctionComponent = ({ - close, - lifecycle, - policyName, -}) => { - // @ts-ignore until store is typed - const getEsJson = ({ phases }) => { +export const PolicyJsonFlyout: React.FunctionComponent = ({ close, policy, policyName }) => { + const getEsJson = ({ phases }: Policy) => { return JSON.stringify( { policy: { @@ -45,7 +40,7 @@ export const PolicyJsonFlyout: React.FunctionComponent = ({ }; const endpoint = `PUT _ilm/policy/${policyName || ''}`; - const request = `${endpoint}\n${getEsJson(lifecycle)}`; + const request = `${endpoint}\n${getEsJson(policy)}`; return ( diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/set_priority_input.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/set_priority_input.tsx index 0034de85fce17..1da7508049f24 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/set_priority_input.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/set_priority_input.tsx @@ -7,27 +7,27 @@ import React, { Fragment } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiFieldNumber, EuiTextColor, EuiDescribedFormGroup } from '@elastic/eui'; -import { PHASE_INDEX_PRIORITY } from '../../../constants'; - import { LearnMoreLink } from './'; import { OptionalLabel } from './'; import { ErrableFormRow } from './'; +import { ColdPhase, HotPhase, Phase, Phases, WarmPhase } from '../../../services/policies/types'; +import { PhaseValidationErrors, propertyof } from '../../../services/policies/policy_validation'; -interface Props { - errors: Record; - // TODO add types for phaseData and setPhaseData after policy is typed - phase: string; - phaseData: any; - setPhaseData: (dataKey: string, value: any) => void; +interface Props { + errors?: PhaseValidationErrors; + phase: keyof Phases & string; + phaseData: T; + setPhaseData: (dataKey: keyof T & string, value: any) => void; isShowingErrors: boolean; } -export const SetPriorityInput: React.FunctionComponent = ({ +export const SetPriorityInput = ({ errors, phaseData, phase, setPhaseData, isShowingErrors, -}) => { +}: React.PropsWithChildren>) => { + const phaseIndexPriorityProperty = propertyof('phaseIndexPriority'); return ( = ({ fullWidth > = ({ } - errorKey={PHASE_INDEX_PRIORITY} isShowingErrors={isShowingErrors} - errors={errors} + errors={errors?.phaseIndexPriority} > { - setPhaseData(PHASE_INDEX_PRIORITY, e.target.value); + setPhaseData(phaseIndexPriorityProperty, e.target.value); }} min={0} /> diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.container.js b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.container.js deleted file mode 100644 index e7f20a66d09f0..0000000000000 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.container.js +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { connect } from 'react-redux'; - -import { - getSaveAsNewPolicy, - getSelectedPolicy, - validateLifecycle, - getLifecycle, - getPolicies, - isPolicyListLoaded, - getIsNewPolicy, - getSelectedOriginalPolicyName, - getPhases, -} from '../../store/selectors'; - -import { - setSelectedPolicy, - setSelectedPolicyName, - setSaveAsNewPolicy, - saveLifecyclePolicy, - fetchPolicies, - setPhaseData, -} from '../../store/actions'; - -import { findFirstError } from '../../services/find_errors'; -import { EditPolicy as PresentationComponent } from './edit_policy'; - -export const EditPolicy = connect( - (state) => { - const errors = validateLifecycle(state); - const firstError = findFirstError(errors); - return { - firstError, - errors, - selectedPolicy: getSelectedPolicy(state), - saveAsNewPolicy: getSaveAsNewPolicy(state), - lifecycle: getLifecycle(state), - policies: getPolicies(state), - isPolicyListLoaded: isPolicyListLoaded(state), - isNewPolicy: getIsNewPolicy(state), - originalPolicyName: getSelectedOriginalPolicyName(state), - phases: getPhases(state), - }; - }, - { - setSelectedPolicy, - setSelectedPolicyName, - setSaveAsNewPolicy, - saveLifecyclePolicy, - fetchPolicies, - setPhaseData, - } -)(PresentationComponent); diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.container.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.container.tsx new file mode 100644 index 0000000000000..359134e015f7f --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.container.tsx @@ -0,0 +1,83 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { RouteComponentProps } from 'react-router-dom'; +import { EuiButton, EuiCallOut, EuiEmptyPrompt, EuiLoadingSpinner } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { useLoadPoliciesList } from '../../services/api'; + +import { EditPolicy as PresentationComponent } from './edit_policy'; + +interface RouterProps { + policyName: string; +} + +interface Props { + getUrlForApp: ( + appId: string, + options?: { + path?: string; + absolute?: boolean; + } + ) => string; +} + +export const EditPolicy: React.FunctionComponent> = ({ + match: { + params: { policyName }, + }, + getUrlForApp, + history, +}) => { + const { error, isLoading, data: policies, sendRequest } = useLoadPoliciesList(false); + if (isLoading) { + return ( + } + body={ + + } + /> + ); + } + if (error || !policies) { + const { statusCode, message } = error ? error : { statusCode: '', message: '' }; + return ( + + } + color="danger" + > +

+ {message} ({statusCode}) +

+ + + +
+ ); + } + + return ( + + ); +}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.js b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.js deleted file mode 100644 index a29ecd07c5e45..0000000000000 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.js +++ /dev/null @@ -1,390 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React, { Component, Fragment } from 'react'; -import PropTypes from 'prop-types'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { i18n } from '@kbn/i18n'; - -import { - EuiPage, - EuiPageBody, - EuiFieldText, - EuiPageContent, - EuiFormRow, - EuiTitle, - EuiText, - EuiSpacer, - EuiSwitch, - EuiHorizontalRule, - EuiButton, - EuiButtonEmpty, - EuiFlexGroup, - EuiFlexItem, - EuiDescribedFormGroup, -} from '@elastic/eui'; - -import { - PHASE_HOT, - PHASE_COLD, - PHASE_DELETE, - PHASE_WARM, - STRUCTURE_POLICY_NAME, - WARM_PHASE_ON_ROLLOVER, - PHASE_ROLLOVER_ENABLED, -} from '../../constants'; - -import { toasts } from '../../services/notification'; -import { findFirstError } from '../../services/find_errors'; -import { LearnMoreLink, PolicyJsonFlyout, ErrableFormRow } from './components'; - -import { HotPhase, WarmPhase, ColdPhase, DeletePhase } from './phases'; - -export class EditPolicy extends Component { - static propTypes = { - selectedPolicy: PropTypes.object.isRequired, - errors: PropTypes.object.isRequired, - }; - - constructor(props) { - super(props); - this.state = { - isShowingErrors: false, - isShowingPolicyJsonFlyout: false, - }; - } - - selectPolicy = (policyName) => { - const { setSelectedPolicy, policies } = this.props; - - const selectedPolicy = policies.find((policy) => { - return policy.name === policyName; - }); - - if (selectedPolicy) { - setSelectedPolicy(selectedPolicy); - } - }; - - componentDidMount() { - window.scrollTo(0, 0); - - const { - isPolicyListLoaded, - fetchPolicies, - match: { params: { policyName } } = { params: {} }, - } = this.props; - - if (policyName) { - const decodedPolicyName = decodeURIComponent(policyName); - if (isPolicyListLoaded) { - this.selectPolicy(decodedPolicyName); - } else { - fetchPolicies(true, () => { - this.selectPolicy(decodedPolicyName); - }); - } - } else { - this.props.setSelectedPolicy(null); - } - } - - backToPolicyList = () => { - this.props.setSelectedPolicy(null); - this.props.history.push('/policies'); - }; - - submit = async () => { - this.setState({ isShowingErrors: true }); - const { saveLifecyclePolicy, lifecycle, saveAsNewPolicy, firstError } = this.props; - if (firstError) { - toasts.addDanger( - i18n.translate('xpack.indexLifecycleMgmt.editPolicy.formErrorsMessage', { - defaultMessage: 'Please fix the errors on this page.', - }) - ); - const errorRowId = `${firstError.replace('.', '-')}-row`; - const element = document.getElementById(errorRowId); - if (element) { - element.scrollIntoView({ block: 'center', inline: 'nearest' }); - } - } else { - const success = await saveLifecyclePolicy(lifecycle, saveAsNewPolicy); - if (success) { - this.backToPolicyList(); - } - } - }; - - togglePolicyJsonFlyout = () => { - this.setState(({ isShowingPolicyJsonFlyout }) => ({ - isShowingPolicyJsonFlyout: !isShowingPolicyJsonFlyout, - })); - }; - - render() { - const { - selectedPolicy, - errors, - setSaveAsNewPolicy, - saveAsNewPolicy, - setSelectedPolicyName, - isNewPolicy, - lifecycle, - originalPolicyName, - phases, - setPhaseData, - } = this.props; - const selectedPolicyName = selectedPolicy.name; - const { isShowingErrors, isShowingPolicyJsonFlyout } = this.state; - - return ( - - - - -

- {isNewPolicy - ? i18n.translate('xpack.indexLifecycleMgmt.editPolicy.createPolicyMessage', { - defaultMessage: 'Create an index lifecycle policy', - }) - : i18n.translate('xpack.indexLifecycleMgmt.editPolicy.editPolicyMessage', { - defaultMessage: 'Edit index lifecycle policy {originalPolicyName}', - values: { originalPolicyName }, - })} -

-
- -
- - -

- {' '} - - } - /> -

-
- - - - - {isNewPolicy ? null : ( - - - -

- - - - .{' '} - -

-
- -
- - - { - await setSaveAsNewPolicy(e.target.checked); - }} - label={ - - - - } - /> - -
- )} - - {saveAsNewPolicy || isNewPolicy ? ( - - - - -
- } - titleSize="s" - fullWidth - > - - } - > - { - await setSelectedPolicyName(e.target.value); - }} - /> - - - ) : null} - - - - - setPhaseData(PHASE_HOT, key, value)} - phaseData={phases[PHASE_HOT]} - setWarmPhaseOnRollover={(value) => - setPhaseData(PHASE_WARM, WARM_PHASE_ON_ROLLOVER, value) - } - /> - - - - setPhaseData(PHASE_WARM, key, value)} - phaseData={phases[PHASE_WARM]} - hotPhaseRolloverEnabled={phases[PHASE_HOT][PHASE_ROLLOVER_ENABLED]} - /> - - - - setPhaseData(PHASE_COLD, key, value)} - phaseData={phases[PHASE_COLD]} - hotPhaseRolloverEnabled={phases[PHASE_HOT][PHASE_ROLLOVER_ENABLED]} - /> - - - - setPhaseData(PHASE_DELETE, key, value)} - phaseData={phases[PHASE_DELETE]} - hotPhaseRolloverEnabled={phases[PHASE_HOT][PHASE_ROLLOVER_ENABLED]} - /> - - - - - - - - - {saveAsNewPolicy ? ( - - ) : ( - - )} - - - - - - - - - - - - - - {isShowingPolicyJsonFlyout ? ( - - ) : ( - - )} - - - - - {this.state.isShowingPolicyJsonFlyout ? ( - this.setState({ isShowingPolicyJsonFlyout: false })} - /> - ) : null} - -
-
-
- ); - } -} diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.tsx new file mode 100644 index 0000000000000..6cffde577b35e --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.tsx @@ -0,0 +1,383 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { Fragment, useEffect, useState } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; + +import { + EuiButton, + EuiButtonEmpty, + EuiDescribedFormGroup, + EuiFieldText, + EuiFlexGroup, + EuiFlexItem, + EuiFormRow, + EuiHorizontalRule, + EuiPage, + EuiPageBody, + EuiPageContent, + EuiSpacer, + EuiSwitch, + EuiText, + EuiTitle, +} from '@elastic/eui'; + +import { toasts } from '../../services/notification'; + +import { Policy, PolicyFromES } from '../../services/policies/types'; +import { + validatePolicy, + ValidationErrors, + findFirstError, +} from '../../services/policies/policy_validation'; +import { savePolicy } from '../../services/policies/policy_save'; +import { + deserializePolicy, + getPolicyByName, + initializeNewPolicy, +} from '../../services/policies/policy_serialization'; + +import { ErrableFormRow, LearnMoreLink, PolicyJsonFlyout } from './components'; +import { ColdPhase, DeletePhase, HotPhase, WarmPhase } from './phases'; + +interface Props { + policies: PolicyFromES[]; + policyName: string; + getUrlForApp: ( + appId: string, + options?: { + path?: string; + absolute?: boolean; + } + ) => string; + history: any; +} +export const EditPolicy: React.FunctionComponent = ({ + policies, + policyName, + history, + getUrlForApp, +}) => { + useEffect(() => { + window.scrollTo(0, 0); + }, []); + + const [isShowingErrors, setIsShowingErrors] = useState(false); + const [errors, setErrors] = useState(); + const [isShowingPolicyJsonFlyout, setIsShowingPolicyJsonFlyout] = useState(false); + + const existingPolicy = getPolicyByName(policies, policyName); + + const [policy, setPolicy] = useState( + existingPolicy ? deserializePolicy(existingPolicy) : initializeNewPolicy(policyName) + ); + + const isNewPolicy: boolean = !Boolean(existingPolicy); + const [saveAsNew, setSaveAsNew] = useState(isNewPolicy); + const originalPolicyName: string = existingPolicy ? existingPolicy.name : ''; + + const backToPolicyList = () => { + history.push('/policies'); + }; + + const submit = async () => { + setIsShowingErrors(true); + const [isValid, validationErrors] = validatePolicy( + saveAsNew, + policy, + policies, + originalPolicyName + ); + setErrors(validationErrors); + + if (!isValid) { + toasts.addDanger( + i18n.translate('xpack.indexLifecycleMgmt.editPolicy.formErrorsMessage', { + defaultMessage: 'Please fix the errors on this page.', + }) + ); + const firstError = findFirstError(validationErrors); + const errorRowId = `${firstError ? firstError.replace('.', '-') : ''}-row`; + const element = document.getElementById(errorRowId); + if (element) { + element.scrollIntoView({ block: 'center', inline: 'nearest' }); + } + } else { + const success = await savePolicy(policy, isNewPolicy || saveAsNew, existingPolicy); + if (success) { + backToPolicyList(); + } + } + }; + + const togglePolicyJsonFlyout = () => { + setIsShowingPolicyJsonFlyout(!isShowingPolicyJsonFlyout); + }; + + const setPhaseData = (phase: 'hot' | 'warm' | 'cold' | 'delete', key: string, value: any) => { + setPolicy({ + ...policy, + phases: { + ...policy.phases, + [phase]: { ...policy.phases[phase], [key]: value }, + }, + }); + }; + + const setWarmPhaseOnRollover = (value: boolean) => { + setPolicy({ + ...policy, + phases: { + ...policy.phases, + hot: { + ...policy.phases.hot, + rolloverEnabled: value, + }, + warm: { + ...policy.phases.warm, + warmPhaseOnRollover: value, + }, + }, + }); + }; + + return ( + + + + +

+ {isNewPolicy + ? i18n.translate('xpack.indexLifecycleMgmt.editPolicy.createPolicyMessage', { + defaultMessage: 'Create an index lifecycle policy', + }) + : i18n.translate('xpack.indexLifecycleMgmt.editPolicy.editPolicyMessage', { + defaultMessage: 'Edit index lifecycle policy {originalPolicyName}', + values: { originalPolicyName }, + })} +

+
+ +
+ + +

+ {' '} + + } + /> +

+
+ + + + {isNewPolicy ? null : ( + + +

+ + + + .{' '} + +

+
+ + + + { + setSaveAsNew(e.target.checked); + }} + label={ + + + + } + /> + +
+ )} + + {saveAsNew || isNewPolicy ? ( + + + + +
+ } + titleSize="s" + fullWidth + > + + } + > + { + setPolicy({ ...policy, name: e.target.value }); + }} + /> + + + ) : null} + + + + 0} + setPhaseData={(key, value) => setPhaseData('hot', key, value)} + phaseData={policy.phases.hot} + setWarmPhaseOnRollover={setWarmPhaseOnRollover} + /> + + + + 0} + setPhaseData={(key, value) => setPhaseData('warm', key, value)} + phaseData={policy.phases.warm} + hotPhaseRolloverEnabled={policy.phases.hot.rolloverEnabled} + /> + + + + 0} + setPhaseData={(key, value) => setPhaseData('cold', key, value)} + phaseData={policy.phases.cold} + hotPhaseRolloverEnabled={policy.phases.hot.rolloverEnabled} + /> + + + + 0} + getUrlForApp={getUrlForApp} + setPhaseData={(key, value) => setPhaseData('delete', key, value)} + phaseData={policy.phases.delete} + hotPhaseRolloverEnabled={policy.phases.hot.rolloverEnabled} + /> + + + + + + + + + {saveAsNew ? ( + + ) : ( + + )} + + + + + + + + + + + + + + {isShowingPolicyJsonFlyout ? ( + + ) : ( + + )} + + + + + {isShowingPolicyJsonFlyout ? ( + setIsShowingPolicyJsonFlyout(false)} + /> + ) : null} + +
+
+
+ ); +}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/index.d.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/index.d.ts deleted file mode 100644 index 5f15d929a4916..0000000000000 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/index.d.ts +++ /dev/null @@ -1,7 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -export declare const EditPolicy: any; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/index.js b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/index.ts similarity index 100% rename from x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/index.js rename to x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/index.ts diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/cold_phase.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/cold_phase.tsx index babbbf7638ebe..fb32752fe24ea 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/cold_phase.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/cold_phase.tsx @@ -18,12 +18,9 @@ import { EuiTextColor, } from '@elastic/eui'; -import { - PHASE_COLD, - PHASE_ENABLED, - PHASE_REPLICA_COUNT, - PHASE_FREEZE_ENABLED, -} from '../../../constants'; +import { ColdPhase as ColdPhaseInterface, Phases } from '../../../services/policies/types'; +import { PhaseValidationErrors, propertyof } from '../../../services/policies/policy_validation'; + import { LearnMoreLink, ActiveBadge, @@ -35,14 +32,21 @@ import { SetPriorityInput, } from '../components'; +const freezeLabel = i18n.translate('xpack.indexLifecycleMgmt.coldPhase.freezeIndexLabel', { + defaultMessage: 'Freeze index', +}); + +const coldProperty = propertyof('cold'); +const phaseProperty = (propertyName: keyof ColdPhaseInterface) => + propertyof(propertyName); + interface Props { - setPhaseData: (key: string, value: any) => void; - phaseData: any; + setPhaseData: (key: keyof ColdPhaseInterface & string, value: string | boolean) => void; + phaseData: ColdPhaseInterface; isShowingErrors: boolean; - errors: Record; + errors?: PhaseValidationErrors; hotPhaseRolloverEnabled: boolean; } - export class ColdPhase extends PureComponent { render() { const { @@ -53,10 +57,6 @@ export class ColdPhase extends PureComponent { hotPhaseRolloverEnabled, } = this.props; - const freezeLabel = i18n.translate('xpack.indexLifecycleMgmt.coldPhase.freezeIndexLabel', { - defaultMessage: 'Freeze index', - }); - return (
{ defaultMessage="Cold phase" /> {' '} - {phaseData[PHASE_ENABLED] && !isShowingErrors ? : null} + {phaseData.phaseEnabled && !isShowingErrors ? : null}
} @@ -91,10 +91,10 @@ export class ColdPhase extends PureComponent { defaultMessage="Activate cold phase" /> } - id={`${PHASE_COLD}-${PHASE_ENABLED}`} - checked={phaseData[PHASE_ENABLED]} + id={`${coldProperty}-${phaseProperty('phaseEnabled')}`} + checked={phaseData.phaseEnabled} onChange={(e) => { - setPhaseData(PHASE_ENABLED, e.target.checked); + setPhaseData(phaseProperty('phaseEnabled'), e.target.checked); }} aria-controls="coldPhaseContent" /> @@ -103,20 +103,20 @@ export class ColdPhase extends PureComponent { fullWidth > - {phaseData[PHASE_ENABLED] ? ( + {phaseData.phaseEnabled ? ( - errors={errors} phaseData={phaseData} - phase={PHASE_COLD} + phase={coldProperty} isShowingErrors={isShowingErrors} setPhaseData={setPhaseData} rolloverEnabled={hotPhaseRolloverEnabled} /> - + phase={coldProperty} setPhaseData={setPhaseData} errors={errors} phaseData={phaseData} @@ -126,7 +126,7 @@ export class ColdPhase extends PureComponent { { } - errorKey={PHASE_REPLICA_COUNT} isShowingErrors={isShowingErrors} - errors={errors} + errors={errors?.freezeEnabled} helpText={i18n.translate( 'xpack.indexLifecycleMgmt.coldPhase.replicaCountHelpText', { @@ -147,10 +146,10 @@ export class ColdPhase extends PureComponent { )} > { - setPhaseData(PHASE_REPLICA_COUNT, e.target.value); + setPhaseData(phaseProperty('selectedReplicaCount'), e.target.value); }} min={0} /> @@ -163,7 +162,7 @@ export class ColdPhase extends PureComponent { )} - {phaseData[PHASE_ENABLED] ? ( + {phaseData.phaseEnabled ? ( { > { - setPhaseData(PHASE_FREEZE_ENABLED, e.target.checked); + setPhaseData(phaseProperty('freezeEnabled'), e.target.checked); }} label={freezeLabel} aria-label={freezeLabel} /> - errors={errors} phaseData={phaseData} - phase={PHASE_COLD} + phase={coldProperty} isShowingErrors={isShowingErrors} setPhaseData={setPhaseData} /> diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/delete_phase.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/delete_phase.tsx index 0143cc4af24e3..d3c73090f25f2 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/delete_phase.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/delete_phase.tsx @@ -8,7 +8,9 @@ import React, { PureComponent, Fragment } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiDescribedFormGroup, EuiSwitch, EuiTextColor, EuiFormRow } from '@elastic/eui'; -import { PHASE_DELETE, PHASE_ENABLED, PHASE_WAIT_FOR_SNAPSHOT_POLICY } from '../../../constants'; +import { DeletePhase as DeletePhaseInterface, Phases } from '../../../services/policies/types'; +import { PhaseValidationErrors, propertyof } from '../../../services/policies/policy_validation'; + import { ActiveBadge, LearnMoreLink, @@ -18,11 +20,15 @@ import { SnapshotPolicies, } from '../components'; +const deleteProperty = propertyof('delete'); +const phaseProperty = (propertyName: keyof DeletePhaseInterface) => + propertyof(propertyName); + interface Props { - setPhaseData: (key: string, value: any) => void; - phaseData: any; + setPhaseData: (key: keyof DeletePhaseInterface & string, value: string | boolean) => void; + phaseData: DeletePhaseInterface; isShowingErrors: boolean; - errors: Record; + errors?: PhaseValidationErrors; hotPhaseRolloverEnabled: boolean; getUrlForApp: ( appId: string, @@ -55,7 +61,7 @@ export class DeletePhase extends PureComponent { defaultMessage="Delete phase" /> {' '} - {phaseData[PHASE_ENABLED] && !isShowingErrors ? : null} + {phaseData.phaseEnabled && !isShowingErrors ? : null} } @@ -76,10 +82,10 @@ export class DeletePhase extends PureComponent { defaultMessage="Activate delete phase" /> } - id={`${PHASE_DELETE}-${PHASE_ENABLED}`} - checked={phaseData[PHASE_ENABLED]} + id={`${deleteProperty}-${phaseProperty('phaseEnabled')}`} + checked={phaseData.phaseEnabled} onChange={(e) => { - setPhaseData(PHASE_ENABLED, e.target.checked); + setPhaseData(phaseProperty('phaseEnabled'), e.target.checked); }} aria-controls="deletePhaseContent" /> @@ -87,11 +93,11 @@ export class DeletePhase extends PureComponent { } fullWidth > - {phaseData[PHASE_ENABLED] ? ( - errors={errors} phaseData={phaseData} - phase={PHASE_DELETE} + phase={deleteProperty} isShowingErrors={isShowingErrors} setPhaseData={setPhaseData} rolloverEnabled={hotPhaseRolloverEnabled} @@ -100,7 +106,7 @@ export class DeletePhase extends PureComponent {
)} - {phaseData[PHASE_ENABLED] ? ( + {phaseData.phaseEnabled ? ( @@ -135,8 +141,8 @@ export class DeletePhase extends PureComponent { } > setPhaseData(PHASE_WAIT_FOR_SNAPSHOT_POLICY, value)} + value={phaseData.waitForSnapshotPolicy} + onChange={(value) => setPhaseData(phaseProperty('waitForSnapshotPolicy'), value)} getUrlForApp={getUrlForApp} /> diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/hot_phase.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/hot_phase.tsx index dbd48f3a85634..22f0114d16afe 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/hot_phase.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/hot_phase.tsx @@ -7,7 +7,6 @@ import React, { Fragment, PureComponent } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; - import { EuiFlexGroup, EuiFlexItem, @@ -19,15 +18,9 @@ import { EuiDescribedFormGroup, } from '@elastic/eui'; -import { - PHASE_HOT, - PHASE_ROLLOVER_MAX_AGE, - PHASE_ROLLOVER_MAX_AGE_UNITS, - PHASE_ROLLOVER_MAX_DOCUMENTS, - PHASE_ROLLOVER_MAX_SIZE_STORED, - PHASE_ROLLOVER_MAX_SIZE_STORED_UNITS, - PHASE_ROLLOVER_ENABLED, -} from '../../../constants'; +import { HotPhase as HotPhaseInterface, Phases } from '../../../services/policies/types'; +import { PhaseValidationErrors, propertyof } from '../../../services/policies/policy_validation'; + import { LearnMoreLink, ActiveBadge, @@ -36,11 +29,98 @@ import { SetPriorityInput, } from '../components'; +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', + }), + }, +]; + +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', + }), + }, +]; +const hotProperty = propertyof('hot'); +const phaseProperty = (propertyName: keyof HotPhaseInterface) => + propertyof(propertyName); + interface Props { - errors: Record; + errors?: PhaseValidationErrors; isShowingErrors: boolean; - phaseData: any; - setPhaseData: (key: string, value: any) => void; + phaseData: HotPhaseInterface; + setPhaseData: (key: keyof HotPhaseInterface & string, value: string | boolean) => void; setWarmPhaseOnRollover: (value: boolean) => void; } @@ -104,39 +184,36 @@ export class HotPhase extends PureComponent { > { - const { checked } = e.target; - setPhaseData(PHASE_ROLLOVER_ENABLED, checked); - setWarmPhaseOnRollover(checked); + setWarmPhaseOnRollover(e.target.checked); }} label={i18n.translate('xpack.indexLifecycleMgmt.hotPhase.enableRolloverLabel', { defaultMessage: 'Enable rollover', })} /> - {phaseData[PHASE_ROLLOVER_ENABLED] ? ( + {phaseData.rolloverEnabled ? ( { - setPhaseData(PHASE_ROLLOVER_MAX_SIZE_STORED, e.target.value); + setPhaseData(phaseProperty('selectedMaxSizeStored'), e.target.value); }} min={1} /> @@ -144,11 +221,10 @@ export class HotPhase extends PureComponent { { defaultMessage: 'Maximum index size units', } )} - value={phaseData[PHASE_ROLLOVER_MAX_SIZE_STORED_UNITS]} + value={phaseData.selectedMaxSizeStoredUnits} onChange={(e) => { - setPhaseData(PHASE_ROLLOVER_MAX_SIZE_STORED_UNITS, e.target.value); + setPhaseData(phaseProperty('selectedMaxSizeStoredUnits'), e.target.value); }} - options={[ - { - 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', - }), - }, - ]} + options={maxSizeStoredUnits} /> @@ -207,22 +246,21 @@ export class HotPhase extends PureComponent { { - setPhaseData(PHASE_ROLLOVER_MAX_DOCUMENTS, e.target.value); + setPhaseData(phaseProperty('selectedMaxDocuments'), e.target.value); }} min={1} /> @@ -233,19 +271,18 @@ export class HotPhase extends PureComponent { { - setPhaseData(PHASE_ROLLOVER_MAX_AGE, e.target.value); + setPhaseData(phaseProperty('selectedMaxAge'), e.target.value); }} min={1} /> @@ -253,11 +290,10 @@ export class HotPhase extends PureComponent { { defaultMessage: 'Maximum age units', } )} - value={phaseData[PHASE_ROLLOVER_MAX_AGE_UNITS]} + value={phaseData.selectedMaxAgeUnits} onChange={(e) => { - setPhaseData(PHASE_ROLLOVER_MAX_AGE_UNITS, e.target.value); + setPhaseData(phaseProperty('selectedMaxAgeUnits'), e.target.value); }} - options={[ - { - 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', - } - ), - }, - ]} + options={maxAgeUnits} /> @@ -330,10 +314,10 @@ export class HotPhase extends PureComponent { ) : null} - errors={errors} phaseData={phaseData} - phase={PHASE_HOT} + phase={hotProperty} isShowingErrors={isShowingErrors} setPhaseData={setPhaseData} /> diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/warm_phase.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/warm_phase.tsx index 6ed81bf8f45d5..f7b8c60a5c71f 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/warm_phase.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/warm_phase.tsx @@ -18,16 +18,6 @@ import { EuiDescribedFormGroup, } from '@elastic/eui'; -import { - PHASE_WARM, - PHASE_ENABLED, - WARM_PHASE_ON_ROLLOVER, - PHASE_FORCE_MERGE_ENABLED, - PHASE_FORCE_MERGE_SEGMENTS, - PHASE_PRIMARY_SHARD_COUNT, - PHASE_REPLICA_COUNT, - PHASE_SHRINK_ENABLED, -} from '../../../constants'; import { LearnMoreLink, ActiveBadge, @@ -39,11 +29,33 @@ import { MinAgeInput, } from '../components'; +import { Phases, WarmPhase as WarmPhaseInterface } from '../../../services/policies/types'; +import { PhaseValidationErrors, propertyof } from '../../../services/policies/policy_validation'; + +const shrinkLabel = i18n.translate('xpack.indexLifecycleMgmt.warmPhase.shrinkIndexLabel', { + defaultMessage: 'Shrink index', +}); + +const moveToWarmPhaseOnRolloverLabel = i18n.translate( + 'xpack.indexLifecycleMgmt.warmPhase.moveToWarmPhaseOnRolloverLabel', + { + defaultMessage: 'Move to warm phase on rollover', + } +); + +const forcemergeLabel = i18n.translate('xpack.indexLifecycleMgmt.warmPhase.forceMergeDataLabel', { + defaultMessage: 'Force merge data', +}); + +const warmProperty = propertyof('warm'); +const phaseProperty = (propertyName: keyof WarmPhaseInterface) => + propertyof(propertyName); + interface Props { - setPhaseData: (key: string, value: any) => void; - phaseData: any; + setPhaseData: (key: keyof WarmPhaseInterface & string, value: boolean | string) => void; + phaseData: WarmPhaseInterface; isShowingErrors: boolean; - errors: Record; + errors?: PhaseValidationErrors; hotPhaseRolloverEnabled: boolean; } export class WarmPhase extends PureComponent { @@ -56,24 +68,6 @@ export class WarmPhase extends PureComponent { hotPhaseRolloverEnabled, } = this.props; - const shrinkLabel = i18n.translate('xpack.indexLifecycleMgmt.warmPhase.shrinkIndexLabel', { - defaultMessage: 'Shrink index', - }); - - const moveToWarmPhaseOnRolloverLabel = i18n.translate( - 'xpack.indexLifecycleMgmt.warmPhase.moveToWarmPhaseOnRolloverLabel', - { - defaultMessage: 'Move to warm phase on rollover', - } - ); - - const forcemergeLabel = i18n.translate( - 'xpack.indexLifecycleMgmt.warmPhase.forceMergeDataLabel', - { - defaultMessage: 'Force merge data', - } - ); - return (
{ defaultMessage="Warm phase" /> {' '} - {phaseData[PHASE_ENABLED] && !isShowingErrors ? : null} + {phaseData.phaseEnabled && !isShowingErrors ? : null}
} @@ -108,10 +102,10 @@ export class WarmPhase extends PureComponent { defaultMessage="Activate warm phase" /> } - id={`${PHASE_WARM}-${PHASE_ENABLED}`} - checked={phaseData[PHASE_ENABLED]} + id={`${warmProperty}-${phaseProperty('phaseEnabled')}`} + checked={phaseData.phaseEnabled} onChange={(e) => { - setPhaseData(PHASE_ENABLED, e.target.checked); + setPhaseData(phaseProperty('phaseEnabled'), e.target.checked); }} aria-controls="warmPhaseContent" /> @@ -120,28 +114,28 @@ export class WarmPhase extends PureComponent { fullWidth > - {phaseData[PHASE_ENABLED] ? ( + {phaseData.phaseEnabled ? ( {hotPhaseRolloverEnabled ? ( - + { - setPhaseData(WARM_PHASE_ON_ROLLOVER, e.target.checked); + setPhaseData(phaseProperty('warmPhaseOnRollover'), e.target.checked); }} /> ) : null} - {!phaseData[WARM_PHASE_ON_ROLLOVER] ? ( + {!phaseData.warmPhaseOnRollover ? ( - errors={errors} phaseData={phaseData} - phase={PHASE_WARM} + phase={warmProperty} isShowingErrors={isShowingErrors} setPhaseData={setPhaseData} rolloverEnabled={hotPhaseRolloverEnabled} @@ -151,8 +145,8 @@ export class WarmPhase extends PureComponent { - + phase={warmProperty} setPhaseData={setPhaseData} errors={errors} phaseData={phaseData} @@ -162,7 +156,7 @@ export class WarmPhase extends PureComponent { { } - errorKey={PHASE_REPLICA_COUNT} isShowingErrors={isShowingErrors} - errors={errors} + errors={errors?.selectedReplicaCount} helpText={i18n.translate( 'xpack.indexLifecycleMgmt.warmPhase.replicaCountHelpText', { @@ -183,10 +176,10 @@ export class WarmPhase extends PureComponent { )} > { - setPhaseData(PHASE_REPLICA_COUNT, e.target.value); + setPhaseData('selectedReplicaCount', e.target.value); }} min={0} /> @@ -199,7 +192,7 @@ export class WarmPhase extends PureComponent { ) : null} - {phaseData[PHASE_ENABLED] ? ( + {phaseData.phaseEnabled ? ( { { - setPhaseData(PHASE_SHRINK_ENABLED, e.target.checked); + setPhaseData(phaseProperty('shrinkEnabled'), e.target.checked); }} label={shrinkLabel} aria-label={shrinkLabel} @@ -235,28 +228,30 @@ export class WarmPhase extends PureComponent { />
- {phaseData[PHASE_SHRINK_ENABLED] ? ( + {phaseData.shrinkEnabled ? ( { - setPhaseData(PHASE_PRIMARY_SHARD_COUNT, e.target.value); + setPhaseData( + phaseProperty('selectedPrimaryShardCount'), + e.target.value + ); }} min={1} /> @@ -294,33 +289,32 @@ export class WarmPhase extends PureComponent { data-test-subj="forceMergeSwitch" label={forcemergeLabel} aria-label={forcemergeLabel} - checked={phaseData[PHASE_FORCE_MERGE_ENABLED]} + checked={phaseData.forceMergeEnabled} onChange={(e) => { - setPhaseData(PHASE_FORCE_MERGE_ENABLED, e.target.checked); + setPhaseData(phaseProperty('forceMergeEnabled'), e.target.checked); }} aria-controls="forcemergeContent" />
- {phaseData[PHASE_FORCE_MERGE_ENABLED] ? ( + {phaseData.forceMergeEnabled ? ( { - setPhaseData(PHASE_FORCE_MERGE_SEGMENTS, e.target.value); + setPhaseData(phaseProperty('selectedForceMergeSegments'), e.target.value); }} min={1} /> @@ -328,10 +322,10 @@ export class WarmPhase extends PureComponent { ) : null}
- errors={errors} phaseData={phaseData} - phase={PHASE_WARM} + phase={warmProperty} isShowingErrors={isShowingErrors} setPhaseData={setPhaseData} /> diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/components/policy_table/policy_table.js b/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/components/policy_table/policy_table.js index 500ab44d96694..ec1cdb987f4b3 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/components/policy_table/policy_table.js +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/components/policy_table/policy_table.js @@ -38,7 +38,7 @@ import { import { RIGHT_ALIGNMENT } from '@elastic/eui/lib/services'; import { reactRouterNavigate } from '../../../../../../../../../src/plugins/kibana_react/public'; import { getIndexListUri } from '../../../../../../../index_management/public'; -import { UIM_EDIT_CLICK } from '../../../../constants'; +import { UIM_EDIT_CLICK } from '../../../../constants/ui_metric'; import { getPolicyPath } from '../../../../services/navigation'; import { flattenPanelTree } from '../../../../services/flatten_panel_tree'; import { trackUiMetric } from '../../../../services/ui_metric'; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/services/api.ts b/x-pack/plugins/index_lifecycle_management/public/application/services/api.ts index 61de37bbfad11..b80e9e70c54fa 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/services/api.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/services/api.ts @@ -12,10 +12,11 @@ import { UIM_POLICY_ATTACH_INDEX_TEMPLATE, UIM_POLICY_DETACH_INDEX, UIM_INDEX_RETRY_STEP, -} from '../constants'; +} from '../constants/ui_metric'; import { trackUiMetric } from './ui_metric'; import { sendGet, sendPost, sendDelete, useRequest } from './http'; +import { PolicyFromES, SerializedPolicy } from './policies/types'; interface GenericObject { [key: string]: any; @@ -44,7 +45,15 @@ export async function loadPolicies(withIndices: boolean) { return await sendGet('policies', { withIndices }); } -export async function savePolicy(policy: GenericObject) { +export const useLoadPoliciesList = (withIndices: boolean) => { + return useRequest({ + path: `policies`, + method: 'get', + query: { withIndices }, + }); +}; + +export async function savePolicy(policy: SerializedPolicy) { return await sendPost(`policies`, policy); } diff --git a/x-pack/plugins/index_lifecycle_management/public/application/services/find_errors.js b/x-pack/plugins/index_lifecycle_management/public/application/services/find_errors.js deleted file mode 100644 index 12b53ad1eaf52..0000000000000 --- a/x-pack/plugins/index_lifecycle_management/public/application/services/find_errors.js +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -export const findFirstError = (object, topLevel = true) => { - let firstError; - const keys = topLevel ? ['policyName', 'hot', 'warm', 'cold', 'delete'] : Object.keys(object); - for (const key of keys) { - const value = object[key]; - if (Array.isArray(value) && value.length > 0) { - firstError = key; - break; - } else if (value) { - firstError = findFirstError(value, false); - if (firstError) { - firstError = `${key}.${firstError}`; - break; - } - } - } - return firstError; -}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/services/policies/cold_phase.ts b/x-pack/plugins/index_lifecycle_management/public/application/services/policies/cold_phase.ts new file mode 100644 index 0000000000000..d9ed7a0bf51eb --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/services/policies/cold_phase.ts @@ -0,0 +1,159 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { isEmpty } from 'lodash'; +import { serializedPhaseInitialization } from '../../constants'; +import { AllocateAction, ColdPhase, SerializedColdPhase } from './types'; +import { isNumber, splitSizeAndUnits } from './policy_serialization'; +import { + numberRequiredMessage, + PhaseValidationErrors, + positiveNumberRequiredMessage, +} from './policy_validation'; + +const coldPhaseInitialization: ColdPhase = { + phaseEnabled: false, + selectedMinimumAge: '0', + selectedMinimumAgeUnits: 'd', + selectedNodeAttrs: '', + selectedReplicaCount: '', + freezeEnabled: false, + phaseIndexPriority: '', +}; + +export const coldPhaseFromES = (phaseSerialized?: SerializedColdPhase): ColdPhase => { + const phase = { ...coldPhaseInitialization }; + if (phaseSerialized === undefined || phaseSerialized === null) { + return phase; + } + + phase.phaseEnabled = true; + + if (phaseSerialized.min_age) { + const { size: minAge, units: minAgeUnits } = splitSizeAndUnits(phaseSerialized.min_age); + phase.selectedMinimumAge = minAge; + phase.selectedMinimumAgeUnits = minAgeUnits; + } + + if (phaseSerialized.actions) { + const actions = phaseSerialized.actions; + if (actions.allocate) { + const allocate = actions.allocate; + if (allocate.require) { + Object.entries(allocate.require).forEach((entry) => { + phase.selectedNodeAttrs = entry.join(':'); + }); + if (allocate.number_of_replicas) { + phase.selectedReplicaCount = allocate.number_of_replicas.toString(); + } + } + } + + if (actions.freeze) { + phase.freezeEnabled = true; + } + + if (actions.set_priority) { + phase.phaseIndexPriority = actions.set_priority.priority + ? actions.set_priority.priority.toString() + : ''; + } + } + + return phase; +}; + +export const coldPhaseToES = ( + phase: ColdPhase, + originalPhase: SerializedColdPhase | undefined +): SerializedColdPhase => { + if (!originalPhase) { + originalPhase = { ...serializedPhaseInitialization }; + } + + const esPhase = { ...originalPhase }; + + if (isNumber(phase.selectedMinimumAge)) { + esPhase.min_age = `${phase.selectedMinimumAge}${phase.selectedMinimumAgeUnits}`; + } + + esPhase.actions = esPhase.actions ? { ...esPhase.actions } : {}; + + if (phase.selectedNodeAttrs) { + const [name, value] = phase.selectedNodeAttrs.split(':'); + esPhase.actions.allocate = esPhase.actions.allocate || ({} as AllocateAction); + esPhase.actions.allocate.require = { + [name]: value, + }; + } else { + if (esPhase.actions.allocate) { + delete esPhase.actions.allocate.require; + } + } + + if (isNumber(phase.selectedReplicaCount)) { + esPhase.actions.allocate = esPhase.actions.allocate || ({} as AllocateAction); + esPhase.actions.allocate.number_of_replicas = parseInt(phase.selectedReplicaCount, 10); + } else { + if (esPhase.actions.allocate) { + delete esPhase.actions.allocate.number_of_replicas; + } + } + + if ( + esPhase.actions.allocate && + !esPhase.actions.allocate.require && + !isNumber(esPhase.actions.allocate.number_of_replicas) && + isEmpty(esPhase.actions.allocate.include) && + isEmpty(esPhase.actions.allocate.exclude) + ) { + // remove allocate action if it does not define require or number of nodes + // and both include and exclude are empty objects (ES will fail to parse if we don't) + delete esPhase.actions.allocate; + } + + if (phase.freezeEnabled) { + esPhase.actions.freeze = {}; + } else { + delete esPhase.actions.freeze; + } + + if (isNumber(phase.phaseIndexPriority)) { + esPhase.actions.set_priority = { + priority: parseInt(phase.phaseIndexPriority, 10), + }; + } else { + delete esPhase.actions.set_priority; + } + + return esPhase; +}; + +export const validateColdPhase = (phase: ColdPhase): PhaseValidationErrors => { + if (!phase.phaseEnabled) { + return {}; + } + + const phaseErrors = {} as PhaseValidationErrors; + + // index priority is optional, but if it's set, it needs to be a positive number + if (phase.phaseIndexPriority) { + if (!isNumber(phase.phaseIndexPriority)) { + phaseErrors.phaseIndexPriority = [numberRequiredMessage]; + } else if (parseInt(phase.phaseIndexPriority, 10) < 0) { + phaseErrors.phaseIndexPriority = [positiveNumberRequiredMessage]; + } + } + + // min age needs to be a positive number + if (!isNumber(phase.selectedMinimumAge)) { + phaseErrors.phaseIndexPriority = [numberRequiredMessage]; + } else if (parseInt(phase.selectedMinimumAge, 10) < 0) { + phaseErrors.phaseIndexPriority = [positiveNumberRequiredMessage]; + } + + return { ...phaseErrors }; +}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/services/policies/delete_phase.ts b/x-pack/plugins/index_lifecycle_management/public/application/services/policies/delete_phase.ts new file mode 100644 index 0000000000000..70e7c21da8cb6 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/services/policies/delete_phase.ts @@ -0,0 +1,88 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { serializedPhaseInitialization } from '../../constants'; +import { DeletePhase, SerializedDeletePhase } from './types'; +import { isNumber, splitSizeAndUnits } from './policy_serialization'; +import { + numberRequiredMessage, + PhaseValidationErrors, + positiveNumberRequiredMessage, +} from './policy_validation'; + +const deletePhaseInitialization: DeletePhase = { + phaseEnabled: false, + selectedMinimumAge: '0', + selectedMinimumAgeUnits: 'd', + waitForSnapshotPolicy: '', +}; + +export const deletePhaseFromES = (phaseSerialized?: SerializedDeletePhase): DeletePhase => { + const phase = { ...deletePhaseInitialization }; + if (phaseSerialized === undefined || phaseSerialized === null) { + return phase; + } + + phase.phaseEnabled = true; + if (phaseSerialized.min_age) { + const { size: minAge, units: minAgeUnits } = splitSizeAndUnits(phaseSerialized.min_age); + phase.selectedMinimumAge = minAge; + phase.selectedMinimumAgeUnits = minAgeUnits; + } + + if (phaseSerialized.actions) { + const actions = phaseSerialized.actions; + + if (actions.wait_for_snapshot) { + phase.waitForSnapshotPolicy = actions.wait_for_snapshot.policy; + } + } + + return phase; +}; + +export const deletePhaseToES = ( + phase: DeletePhase, + originalEsPhase?: SerializedDeletePhase +): SerializedDeletePhase => { + if (!originalEsPhase) { + originalEsPhase = { ...serializedPhaseInitialization }; + } + const esPhase = { ...originalEsPhase }; + + if (isNumber(phase.selectedMinimumAge)) { + esPhase.min_age = `${phase.selectedMinimumAge}${phase.selectedMinimumAgeUnits}`; + } + + esPhase.actions = esPhase.actions ? { ...esPhase.actions } : {}; + + if (phase.waitForSnapshotPolicy) { + esPhase.actions.wait_for_snapshot = { + policy: phase.waitForSnapshotPolicy, + }; + } else { + delete esPhase.actions.wait_for_snapshot; + } + + return esPhase; +}; + +export const validateDeletePhase = (phase: DeletePhase): PhaseValidationErrors => { + if (!phase.phaseEnabled) { + return {}; + } + + const phaseErrors = {} as PhaseValidationErrors; + + // min age needs to be a positive number + if (!isNumber(phase.selectedMinimumAge)) { + phaseErrors.selectedMinimumAge = [numberRequiredMessage]; + } else if (parseInt(phase.selectedMinimumAge, 10) < 0) { + phaseErrors.selectedMinimumAge = [positiveNumberRequiredMessage]; + } + + return { ...phaseErrors }; +}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/services/policies/hot_phase.ts b/x-pack/plugins/index_lifecycle_management/public/application/services/policies/hot_phase.ts new file mode 100644 index 0000000000000..34ac8f3e270e6 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/services/policies/hot_phase.ts @@ -0,0 +1,155 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { serializedPhaseInitialization } from '../../constants'; +import { isNumber, splitSizeAndUnits } from './policy_serialization'; +import { HotPhase, SerializedHotPhase } from './types'; +import { + maximumAgeRequiredMessage, + maximumDocumentsRequiredMessage, + maximumSizeRequiredMessage, + numberRequiredMessage, + PhaseValidationErrors, + positiveNumberRequiredMessage, + positiveNumbersAboveZeroErrorMessage, +} from './policy_validation'; + +const hotPhaseInitialization: HotPhase = { + phaseEnabled: false, + rolloverEnabled: false, + selectedMaxAge: '', + selectedMaxAgeUnits: 'd', + selectedMaxSizeStored: '', + selectedMaxSizeStoredUnits: 'gb', + phaseIndexPriority: '', + selectedMaxDocuments: '', +}; + +export const hotPhaseFromES = (phaseSerialized?: SerializedHotPhase): HotPhase => { + const phase: HotPhase = { ...hotPhaseInitialization }; + + if (phaseSerialized === undefined || phaseSerialized === null) { + return phase; + } + + phase.phaseEnabled = true; + + if (phaseSerialized.actions) { + const actions = phaseSerialized.actions; + + if (actions.rollover) { + const rollover = actions.rollover; + phase.rolloverEnabled = true; + if (rollover.max_age) { + const { size: maxAge, units: maxAgeUnits } = splitSizeAndUnits(rollover.max_age); + phase.selectedMaxAge = maxAge; + phase.selectedMaxAgeUnits = maxAgeUnits; + } + if (rollover.max_size) { + const { size: maxSize, units: maxSizeUnits } = splitSizeAndUnits(rollover.max_size); + phase.selectedMaxSizeStored = maxSize; + phase.selectedMaxSizeStoredUnits = maxSizeUnits; + } + if (rollover.max_docs) { + phase.selectedMaxDocuments = rollover.max_docs.toString(); + } + } + + if (actions.set_priority) { + phase.phaseIndexPriority = actions.set_priority.priority + ? actions.set_priority.priority.toString() + : ''; + } + } + + return phase; +}; + +export const hotPhaseToES = ( + phase: HotPhase, + originalPhase?: SerializedHotPhase +): SerializedHotPhase => { + if (!originalPhase) { + originalPhase = { ...serializedPhaseInitialization }; + } + + const esPhase = { ...originalPhase }; + + esPhase.actions = esPhase.actions ? { ...esPhase.actions } : {}; + + if (phase.rolloverEnabled) { + if (!esPhase.actions.rollover) { + esPhase.actions.rollover = {}; + } + if (isNumber(phase.selectedMaxAge)) { + esPhase.actions.rollover.max_age = `${phase.selectedMaxAge}${phase.selectedMaxAgeUnits}`; + } + if (isNumber(phase.selectedMaxSizeStored)) { + esPhase.actions.rollover.max_size = `${phase.selectedMaxSizeStored}${phase.selectedMaxSizeStoredUnits}`; + } + if (isNumber(phase.selectedMaxDocuments)) { + esPhase.actions.rollover.max_docs = parseInt(phase.selectedMaxDocuments, 10); + } + } else { + delete esPhase.actions.rollover; + } + + if (isNumber(phase.phaseIndexPriority)) { + esPhase.actions.set_priority = { + priority: parseInt(phase.phaseIndexPriority, 10), + }; + } else { + delete esPhase.actions.set_priority; + } + + return esPhase; +}; + +export const validateHotPhase = (phase: HotPhase): PhaseValidationErrors => { + if (!phase.phaseEnabled) { + return {}; + } + + const phaseErrors = {} as PhaseValidationErrors; + + // index priority is optional, but if it's set, it needs to be a positive number + if (phase.phaseIndexPriority) { + if (!isNumber(phase.phaseIndexPriority)) { + phaseErrors.phaseIndexPriority = [numberRequiredMessage]; + } else if (parseInt(phase.phaseIndexPriority, 10) < 0) { + phaseErrors.phaseIndexPriority = [positiveNumberRequiredMessage]; + } + } + + // if rollover is enabled + if (phase.rolloverEnabled) { + // either max_age, max_size or max_documents need to be set + if ( + !isNumber(phase.selectedMaxAge) && + !isNumber(phase.selectedMaxSizeStored) && + !isNumber(phase.selectedMaxDocuments) + ) { + phaseErrors.selectedMaxAge = [maximumAgeRequiredMessage]; + phaseErrors.selectedMaxSizeStored = [maximumSizeRequiredMessage]; + phaseErrors.selectedMaxDocuments = [maximumDocumentsRequiredMessage]; + } + + // max age, max size and max docs need to be above zero if set + if (isNumber(phase.selectedMaxAge) && parseInt(phase.selectedMaxAge, 10) < 1) { + phaseErrors.selectedMaxAge = [positiveNumbersAboveZeroErrorMessage]; + } + if (isNumber(phase.selectedMaxSizeStored) && parseInt(phase.selectedMaxSizeStored, 10) < 1) { + phaseErrors.selectedMaxSizeStored = [positiveNumbersAboveZeroErrorMessage]; + } + if (isNumber(phase.selectedMaxDocuments) && parseInt(phase.selectedMaxDocuments, 10) < 1) { + phaseErrors.selectedMaxDocuments = [positiveNumbersAboveZeroErrorMessage]; + } + } + + return { + ...phaseErrors, + }; +}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/store/actions/lifecycle.js b/x-pack/plugins/index_lifecycle_management/public/application/services/policies/policy_save.ts similarity index 58% rename from x-pack/plugins/index_lifecycle_management/public/application/store/actions/lifecycle.js rename to x-pack/plugins/index_lifecycle_management/public/application/services/policies/policy_save.ts index 0bb6543482bd6..12df071544952 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/store/actions/lifecycle.js +++ b/x-pack/plugins/index_lifecycle_management/public/application/services/policies/policy_save.ts @@ -5,28 +5,36 @@ */ import { i18n } from '@kbn/i18n'; +import { METRIC_TYPE } from '@kbn/analytics'; -import { UIM_POLICY_CREATE, UIM_POLICY_UPDATE } from '../../constants'; -import { showApiError } from '../../services/api_errors'; -import { toasts } from '../../services/notification'; -import { savePolicy as savePolicyApi } from '../../services/api'; -import { trackUiMetric, getUiMetricsForPhases } from '../../services/ui_metric'; +import { savePolicy as savePolicyApi } from '../api'; +import { showApiError } from '../api_errors'; +import { getUiMetricsForPhases, trackUiMetric } from '../ui_metric'; +import { UIM_POLICY_CREATE, UIM_POLICY_UPDATE } from '../../constants/ui_metric'; +import { toasts } from '../notification'; +import { Policy, PolicyFromES } from './types'; +import { serializePolicy } from './policy_serialization'; -export const saveLifecyclePolicy = (lifecycle, isNew) => async () => { +export const savePolicy = async ( + policy: Policy, + isNew: boolean, + originalEsPolicy?: PolicyFromES +): Promise => { + const serializedPolicy = serializePolicy(policy, originalEsPolicy?.policy); try { - await savePolicyApi(lifecycle); + await savePolicyApi(serializedPolicy); } catch (err) { const title = i18n.translate('xpack.indexLifecycleMgmt.editPolicy.saveErrorMessage', { defaultMessage: 'Error saving lifecycle policy {lifecycleName}', - values: { lifecycleName: lifecycle.name }, + values: { lifecycleName: policy.name }, }); showApiError(err, title); return false; } - const uiMetrics = getUiMetricsForPhases(lifecycle.phases); + const uiMetrics = getUiMetricsForPhases(serializedPolicy.phases); uiMetrics.push(isNew ? UIM_POLICY_CREATE : UIM_POLICY_UPDATE); - trackUiMetric('count', uiMetrics); + trackUiMetric(METRIC_TYPE.COUNT, uiMetrics); const message = i18n.translate('xpack.indexLifecycleMgmt.editPolicy.successfulSaveMessage', { defaultMessage: '{verb} lifecycle policy "{lifecycleName}"', @@ -38,7 +46,7 @@ export const saveLifecyclePolicy = (lifecycle, isNew) => async () => { : i18n.translate('xpack.indexLifecycleMgmt.editPolicy.updatedMessage', { defaultMessage: 'Updated', }), - lifecycleName: lifecycle.name, + lifecycleName: policy.name, }, }); toasts.addSuccess(message); diff --git a/x-pack/plugins/index_lifecycle_management/public/application/services/policies/policy_serialization.ts b/x-pack/plugins/index_lifecycle_management/public/application/services/policies/policy_serialization.ts new file mode 100644 index 0000000000000..3953521df1817 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/services/policies/policy_serialization.ts @@ -0,0 +1,104 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + defaultNewColdPhase, + defaultNewDeletePhase, + defaultNewHotPhase, + defaultNewWarmPhase, + serializedPhaseInitialization, +} from '../../constants'; + +import { Policy, PolicyFromES, SerializedPolicy } from './types'; + +import { hotPhaseFromES, hotPhaseToES } from './hot_phase'; +import { warmPhaseFromES, warmPhaseToES } from './warm_phase'; +import { coldPhaseFromES, coldPhaseToES } from './cold_phase'; +import { deletePhaseFromES, deletePhaseToES } from './delete_phase'; + +export const splitSizeAndUnits = (field: string): { size: string; units: string } => { + let size = ''; + let units = ''; + + const result = /(\d+)(\w+)/.exec(field); + if (result) { + size = result[1]; + units = result[2]; + } + + return { + size, + units, + }; +}; + +export const isNumber = (value: any): boolean => value !== '' && value !== null && isFinite(value); + +export const getPolicyByName = ( + policies: PolicyFromES[] | null | undefined, + policyName: string = '' +): PolicyFromES | undefined => { + if (policies && policies.length > 0) { + return policies.find((policy: PolicyFromES) => policy.name === policyName); + } +}; + +export const initializeNewPolicy = (newPolicyName: string = ''): Policy => { + return { + name: newPolicyName, + phases: { + hot: { ...defaultNewHotPhase }, + warm: { ...defaultNewWarmPhase }, + cold: { ...defaultNewColdPhase }, + delete: { ...defaultNewDeletePhase }, + }, + }; +}; + +export const deserializePolicy = (policy: PolicyFromES): Policy => { + const { + name, + policy: { phases }, + } = policy; + + return { + name, + phases: { + hot: hotPhaseFromES(phases.hot), + warm: warmPhaseFromES(phases.warm), + cold: coldPhaseFromES(phases.cold), + delete: deletePhaseFromES(phases.delete), + }, + }; +}; + +export const serializePolicy = ( + policy: Policy, + originalEsPolicy: SerializedPolicy = { + name: policy.name, + phases: { hot: { ...serializedPhaseInitialization } }, + } +): SerializedPolicy => { + const serializedPolicy = { + name: policy.name, + phases: { hot: hotPhaseToES(policy.phases.hot, originalEsPolicy.phases.hot) }, + } as SerializedPolicy; + if (policy.phases.warm.phaseEnabled) { + serializedPolicy.phases.warm = warmPhaseToES(policy.phases.warm, originalEsPolicy.phases.warm); + } + + if (policy.phases.cold.phaseEnabled) { + serializedPolicy.phases.cold = coldPhaseToES(policy.phases.cold, originalEsPolicy.phases.cold); + } + + if (policy.phases.delete.phaseEnabled) { + serializedPolicy.phases.delete = deletePhaseToES( + policy.phases.delete, + originalEsPolicy.phases.delete + ); + } + return serializedPolicy; +}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/services/policies/policy_validation.ts b/x-pack/plugins/index_lifecycle_management/public/application/services/policies/policy_validation.ts new file mode 100644 index 0000000000000..545488be2cd5e --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/services/policies/policy_validation.ts @@ -0,0 +1,191 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; +import { validateHotPhase } from './hot_phase'; +import { validateWarmPhase } from './warm_phase'; +import { validateColdPhase } from './cold_phase'; +import { validateDeletePhase } from './delete_phase'; +import { ColdPhase, DeletePhase, HotPhase, Phase, Policy, PolicyFromES, WarmPhase } from './types'; + +export const propertyof = (propertyName: keyof T & string) => propertyName; + +export const numberRequiredMessage = i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.numberRequiredError', + { + defaultMessage: 'A number is required.', + } +); + +// TODO validation includes 0 -> should be non-negative number? +export const positiveNumberRequiredMessage = i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.positiveNumberRequiredError', + { + defaultMessage: 'Only positive numbers are allowed.', + } +); + +export const maximumAgeRequiredMessage = i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.maximumAgeMissingError', + { + defaultMessage: 'A maximum age is required.', + } +); + +export const maximumSizeRequiredMessage = i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.maximumIndexSizeMissingError', + { + defaultMessage: 'A maximum index size is required.', + } +); + +export const maximumDocumentsRequiredMessage = i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.maximumDocumentsMissingError', + { + defaultMessage: 'Maximum documents is required.', + } +); + +export const positiveNumbersAboveZeroErrorMessage = i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.positiveNumberAboveZeroRequiredError', + { + defaultMessage: 'Only numbers above 0 are allowed.', + } +); + +export const policyNameRequiredMessage = i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.policyNameRequiredError', + { + defaultMessage: 'A policy name is required.', + } +); + +export const policyNameStartsWithUnderscoreErrorMessage = i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.policyNameStartsWithUnderscoreError', + { + defaultMessage: 'A policy name cannot start with an underscore.', + } +); +export const policyNameContainsCommaErrorMessage = i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.policyNameContainsCommaError', + { + defaultMessage: 'A policy name cannot include a comma.', + } +); +export const policyNameContainsSpaceErrorMessage = i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.policyNameContainsSpaceError', + { + defaultMessage: 'A policy name cannot include a space.', + } +); + +export const policyNameTooLongErrorMessage = i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.policyNameTooLongError', + { + defaultMessage: 'A policy name cannot be longer than 255 bytes.', + } +); +export const policyNameMustBeDifferentErrorMessage = i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.differentPolicyNameRequiredError', + { + defaultMessage: 'The policy name must be different.', + } +); +export const policyNameAlreadyUsedErrorMessage = i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.policyNameAlreadyUsedError', + { + defaultMessage: 'That policy name is already used.', + } +); +export type PhaseValidationErrors = { + [P in keyof Partial]: string[]; +}; + +export interface ValidationErrors { + hot: PhaseValidationErrors; + warm: PhaseValidationErrors; + cold: PhaseValidationErrors; + delete: PhaseValidationErrors; + policyName: string[]; +} + +export const validatePolicy = ( + saveAsNew: boolean, + policy: Policy, + policies: PolicyFromES[], + originalPolicyName: string +): [boolean, ValidationErrors] => { + const policyNameErrors: string[] = []; + if (!policy.name) { + policyNameErrors.push(policyNameRequiredMessage); + } else { + if (policy.name.startsWith('_')) { + policyNameErrors.push(policyNameStartsWithUnderscoreErrorMessage); + } + if (policy.name.includes(',')) { + policyNameErrors.push(policyNameContainsCommaErrorMessage); + } + if (policy.name.includes(' ')) { + policyNameErrors.push(policyNameContainsSpaceErrorMessage); + } + if (window.TextEncoder && new window.TextEncoder().encode(policy.name).length > 255) { + policyNameErrors.push(policyNameTooLongErrorMessage); + } + + if (saveAsNew && policy.name === originalPolicyName) { + policyNameErrors.push(policyNameMustBeDifferentErrorMessage); + } else if (policy.name !== originalPolicyName) { + const policyNames = policies.map((existingPolicy) => existingPolicy.name); + if (policyNames.includes(policy.name)) { + policyNameErrors.push(policyNameAlreadyUsedErrorMessage); + } + } + } + + const hotPhaseErrors = validateHotPhase(policy.phases.hot); + const warmPhaseErrors = validateWarmPhase(policy.phases.warm); + const coldPhaseErrors = validateColdPhase(policy.phases.cold); + const deletePhaseErrors = validateDeletePhase(policy.phases.delete); + const isValid = + policyNameErrors.length === 0 && + Object.keys(hotPhaseErrors).length === 0 && + Object.keys(warmPhaseErrors).length === 0 && + Object.keys(coldPhaseErrors).length === 0 && + Object.keys(deletePhaseErrors).length === 0; + return [ + isValid, + { + policyName: [...policyNameErrors], + hot: hotPhaseErrors, + warm: warmPhaseErrors, + cold: coldPhaseErrors, + delete: deletePhaseErrors, + }, + ]; +}; + +export const findFirstError = (errors?: ValidationErrors): string | undefined => { + if (!errors) { + return; + } + + if (errors.policyName.length > 0) { + return propertyof('policyName'); + } + + if (Object.keys(errors.hot).length > 0) { + return `${propertyof('hot')}.${Object.keys(errors.hot)[0]}`; + } + if (Object.keys(errors.warm).length > 0) { + return `${propertyof('warm')}.${Object.keys(errors.warm)[0]}`; + } + if (Object.keys(errors.cold).length > 0) { + return `${propertyof('cold')}.${Object.keys(errors.cold)[0]}`; + } + if (Object.keys(errors.delete).length > 0) { + return `${propertyof('delete')}.${Object.keys(errors.delete)[0]}`; + } +}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/services/policies/types.ts b/x-pack/plugins/index_lifecycle_management/public/application/services/policies/types.ts new file mode 100644 index 0000000000000..2e2ed5b38bb87 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/services/policies/types.ts @@ -0,0 +1,140 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export interface SerializedPolicy { + name: string; + phases: Phases; +} + +export interface Phases { + hot?: SerializedHotPhase; + warm?: SerializedWarmPhase; + cold?: SerializedColdPhase; + delete?: SerializedDeletePhase; +} + +export interface PolicyFromES { + modified_date: string; + name: string; + policy: SerializedPolicy; + version: number; +} + +export interface SerializedPhase { + min_age: string; + actions: { + [action: string]: any; + }; +} + +export interface SerializedHotPhase extends SerializedPhase { + actions: { + rollover?: { + max_size?: string; + max_age?: string; + max_docs?: number; + }; + set_priority?: { + priority: number | null; + }; + }; +} + +export interface SerializedWarmPhase extends SerializedPhase { + actions: { + allocate?: AllocateAction; + shrink?: { + number_of_shards: number; + }; + forcemerge?: { + max_num_segments: number; + }; + set_priority?: { + priority: number | null; + }; + }; +} + +export interface SerializedColdPhase extends SerializedPhase { + actions: { + freeze?: {}; + allocate?: AllocateAction; + set_priority?: { + priority: number | null; + }; + }; +} + +export interface SerializedDeletePhase extends SerializedPhase { + actions: { + wait_for_snapshot?: { + policy: string; + }; + delete?: { + delete_searchable_snapshot: boolean; + }; + }; +} + +export interface AllocateAction { + number_of_replicas: number; + include: {}; + exclude: {}; + require: { + [attribute: string]: string; + }; +} + +export interface Policy { + name: string; + phases: { + hot: HotPhase; + warm: WarmPhase; + cold: ColdPhase; + delete: DeletePhase; + }; +} + +export interface Phase { + phaseEnabled: boolean; +} +export interface HotPhase extends Phase { + rolloverEnabled: boolean; + selectedMaxSizeStored: string; + selectedMaxSizeStoredUnits: string; + selectedMaxDocuments: string; + selectedMaxAge: string; + selectedMaxAgeUnits: string; + phaseIndexPriority: string; +} + +export interface WarmPhase extends Phase { + warmPhaseOnRollover: boolean; + selectedMinimumAge: string; + selectedMinimumAgeUnits: string; + selectedNodeAttrs: string; + selectedReplicaCount: string; + shrinkEnabled: boolean; + selectedPrimaryShardCount: string; + forceMergeEnabled: boolean; + selectedForceMergeSegments: string; + phaseIndexPriority: string; +} + +export interface ColdPhase extends Phase { + selectedMinimumAge: string; + selectedMinimumAgeUnits: string; + selectedNodeAttrs: string; + selectedReplicaCount: string; + freezeEnabled: boolean; + phaseIndexPriority: string; +} + +export interface DeletePhase extends Phase { + selectedMinimumAge: string; + selectedMinimumAgeUnits: string; + waitForSnapshotPolicy: string; +} diff --git a/x-pack/plugins/index_lifecycle_management/public/application/services/policies/warm_phase.ts b/x-pack/plugins/index_lifecycle_management/public/application/services/policies/warm_phase.ts new file mode 100644 index 0000000000000..3ca1a1cc83371 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/services/policies/warm_phase.ts @@ -0,0 +1,219 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { isEmpty } from 'lodash'; +import { serializedPhaseInitialization } from '../../constants'; +import { AllocateAction, WarmPhase, SerializedWarmPhase } from './types'; +import { isNumber, splitSizeAndUnits } from './policy_serialization'; + +import { + numberRequiredMessage, + PhaseValidationErrors, + positiveNumberRequiredMessage, + positiveNumbersAboveZeroErrorMessage, +} from './policy_validation'; + +const warmPhaseInitialization: WarmPhase = { + phaseEnabled: false, + warmPhaseOnRollover: false, + selectedMinimumAge: '0', + selectedMinimumAgeUnits: 'd', + selectedNodeAttrs: '', + selectedReplicaCount: '', + shrinkEnabled: false, + selectedPrimaryShardCount: '', + forceMergeEnabled: false, + selectedForceMergeSegments: '', + phaseIndexPriority: '', +}; + +export const warmPhaseFromES = (phaseSerialized?: SerializedWarmPhase): WarmPhase => { + const phase: WarmPhase = { ...warmPhaseInitialization }; + + if (phaseSerialized === undefined || phaseSerialized === null) { + return phase; + } + + phase.phaseEnabled = true; + + if (phaseSerialized.min_age) { + if (phaseSerialized.min_age === '0ms') { + phase.warmPhaseOnRollover = true; + } else { + const { size: minAge, units: minAgeUnits } = splitSizeAndUnits(phaseSerialized.min_age); + phase.selectedMinimumAge = minAge; + phase.selectedMinimumAgeUnits = minAgeUnits; + } + } + if (phaseSerialized.actions) { + const actions = phaseSerialized.actions; + if (actions.allocate) { + const allocate = actions.allocate; + if (allocate.require) { + Object.entries(allocate.require).forEach((entry) => { + phase.selectedNodeAttrs = entry.join(':'); + }); + if (allocate.number_of_replicas) { + phase.selectedReplicaCount = allocate.number_of_replicas.toString(); + } + } + } + + if (actions.forcemerge) { + const forcemerge = actions.forcemerge; + phase.forceMergeEnabled = true; + phase.selectedForceMergeSegments = forcemerge.max_num_segments.toString(); + } + + if (actions.shrink) { + phase.shrinkEnabled = true; + phase.selectedPrimaryShardCount = actions.shrink.number_of_shards + ? actions.shrink.number_of_shards.toString() + : ''; + } + } + return phase; +}; + +export const warmPhaseToES = ( + phase: WarmPhase, + originalEsPhase?: SerializedWarmPhase +): SerializedWarmPhase => { + if (!originalEsPhase) { + originalEsPhase = { ...serializedPhaseInitialization }; + } + + const esPhase = { ...originalEsPhase }; + + if (isNumber(phase.selectedMinimumAge)) { + esPhase.min_age = `${phase.selectedMinimumAge}${phase.selectedMinimumAgeUnits}`; + } + + // If warm phase on rollover is enabled, delete min age field + // An index lifecycle switches to warm phase when rollover occurs, so you cannot specify a warm phase time + // They are mutually exclusive + if (phase.warmPhaseOnRollover) { + delete esPhase.min_age; + } + + esPhase.actions = esPhase.actions ? { ...esPhase.actions } : {}; + + if (phase.selectedNodeAttrs) { + const [name, value] = phase.selectedNodeAttrs.split(':'); + esPhase.actions.allocate = esPhase.actions.allocate || ({} as AllocateAction); + esPhase.actions.allocate.require = { + [name]: value, + }; + } else { + if (esPhase.actions.allocate) { + delete esPhase.actions.allocate.require; + } + } + + if (isNumber(phase.selectedReplicaCount)) { + esPhase.actions.allocate = esPhase.actions.allocate || ({} as AllocateAction); + esPhase.actions.allocate.number_of_replicas = parseInt(phase.selectedReplicaCount, 10); + } else { + if (esPhase.actions.allocate) { + delete esPhase.actions.allocate.number_of_replicas; + } + } + + if ( + esPhase.actions.allocate && + !esPhase.actions.allocate.require && + !isNumber(esPhase.actions.allocate.number_of_replicas) && + isEmpty(esPhase.actions.allocate.include) && + isEmpty(esPhase.actions.allocate.exclude) + ) { + // remove allocate action if it does not define require or number of nodes + // and both include and exclude are empty objects (ES will fail to parse if we don't) + delete esPhase.actions.allocate; + } + + if (phase.forceMergeEnabled) { + esPhase.actions.forcemerge = { + max_num_segments: parseInt(phase.selectedForceMergeSegments, 10), + }; + } else { + delete esPhase.actions.forcemerge; + } + + if (phase.shrinkEnabled && isNumber(phase.selectedPrimaryShardCount)) { + esPhase.actions.shrink = { + number_of_shards: parseInt(phase.selectedPrimaryShardCount, 10), + }; + } else { + delete esPhase.actions.shrink; + } + + if (isNumber(phase.phaseIndexPriority)) { + esPhase.actions.set_priority = { + priority: parseInt(phase.phaseIndexPriority, 10), + }; + } else { + delete esPhase.actions.set_priority; + } + + return esPhase; +}; + +export const validateWarmPhase = (phase: WarmPhase): PhaseValidationErrors => { + if (!phase.phaseEnabled) { + return {}; + } + + const phaseErrors = {} as PhaseValidationErrors; + + // index priority is optional, but if it's set, it needs to be a positive number + if (phase.phaseIndexPriority) { + if (!isNumber(phase.phaseIndexPriority)) { + phaseErrors.phaseIndexPriority = [numberRequiredMessage]; + } else if (parseInt(phase.phaseIndexPriority, 10) < 0) { + phaseErrors.phaseIndexPriority = [positiveNumberRequiredMessage]; + } + } + + // if warm phase on rollover is disabled, min age needs to be a positive number + if (!phase.warmPhaseOnRollover) { + if (!isNumber(phase.selectedMinimumAge)) { + phaseErrors.selectedMinimumAge = [numberRequiredMessage]; + } else if (parseInt(phase.selectedMinimumAge, 10) < 0) { + phaseErrors.selectedMinimumAge = [positiveNumberRequiredMessage]; + } + } + + // if forcemerge is enabled, force merge segments needs to be a number above zero + if (phase.forceMergeEnabled) { + if (!isNumber(phase.selectedForceMergeSegments)) { + phaseErrors.selectedForceMergeSegments = [numberRequiredMessage]; + } else if (parseInt(phase.selectedForceMergeSegments, 10) < 1) { + phaseErrors.selectedForceMergeSegments = [positiveNumbersAboveZeroErrorMessage]; + } + } + + // if shrink is enabled, primary shard count needs to be a number above zero + if (phase.shrinkEnabled) { + if (!isNumber(phase.selectedPrimaryShardCount)) { + phaseErrors.selectedPrimaryShardCount = [numberRequiredMessage]; + } else if (parseInt(phase.selectedPrimaryShardCount, 10) < 1) { + phaseErrors.selectedPrimaryShardCount = [positiveNumbersAboveZeroErrorMessage]; + } + } + + // replica count is optional, but if it's set, it needs to be a positive number + if (phase.selectedReplicaCount) { + if (!isNumber(phase.selectedReplicaCount)) { + phaseErrors.selectedReplicaCount = [numberRequiredMessage]; + } else if (parseInt(phase.selectedReplicaCount, 10) < 0) { + phaseErrors.selectedReplicaCount = [numberRequiredMessage]; + } + } + + return { + ...phaseErrors, + }; +}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/services/ui_metric.test.js b/x-pack/plugins/index_lifecycle_management/public/application/services/ui_metric.test.ts similarity index 75% rename from x-pack/plugins/index_lifecycle_management/public/application/services/ui_metric.test.js rename to x-pack/plugins/index_lifecycle_management/public/application/services/ui_metric.test.ts index 99e6bfb99472c..7c7c0b70c0eed 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/services/ui_metric.test.js +++ b/x-pack/plugins/index_lifecycle_management/public/application/services/ui_metric.test.ts @@ -5,14 +5,13 @@ */ import { - PHASE_INDEX_PRIORITY, UIM_CONFIG_COLD_PHASE, UIM_CONFIG_WARM_PHASE, UIM_CONFIG_SET_PRIORITY, UIM_CONFIG_FREEZE_INDEX, -} from '../constants'; - -import { defaultColdPhase, defaultWarmPhase } from '../store/defaults'; + defaultNewWarmPhase, + defaultNewColdPhase, +} from '../constants/'; import { getUiMetricsForPhases } from './ui_metric'; jest.mock('ui/new_platform'); @@ -22,9 +21,10 @@ describe('getUiMetricsForPhases', () => { expect( getUiMetricsForPhases({ cold: { + min_age: '0ms', actions: { set_priority: { - priority: defaultColdPhase[PHASE_INDEX_PRIORITY], + priority: parseInt(defaultNewColdPhase.phaseIndexPriority, 10), }, }, }, @@ -36,9 +36,10 @@ describe('getUiMetricsForPhases', () => { expect( getUiMetricsForPhases({ warm: { + min_age: '0ms', actions: { set_priority: { - priority: defaultWarmPhase[PHASE_INDEX_PRIORITY], + priority: parseInt(defaultNewWarmPhase.phaseIndexPriority, 10), }, }, }, @@ -50,9 +51,10 @@ describe('getUiMetricsForPhases', () => { expect( getUiMetricsForPhases({ warm: { + min_age: '0ms', actions: { set_priority: { - priority: defaultWarmPhase[PHASE_INDEX_PRIORITY] + 1, + priority: parseInt(defaultNewWarmPhase.phaseIndexPriority, 10) + 1, }, }, }, @@ -64,10 +66,11 @@ describe('getUiMetricsForPhases', () => { expect( getUiMetricsForPhases({ cold: { + min_age: '0ms', actions: { freeze: {}, set_priority: { - priority: defaultColdPhase[PHASE_INDEX_PRIORITY], + priority: parseInt(defaultNewColdPhase.phaseIndexPriority, 10), }, }, }, diff --git a/x-pack/plugins/index_lifecycle_management/public/application/services/ui_metric.ts b/x-pack/plugins/index_lifecycle_management/public/application/services/ui_metric.ts index d71e38d0b31de..b38a734770546 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/services/ui_metric.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/services/ui_metric.ts @@ -4,24 +4,21 @@ * you may not use this file except in compliance with the Elastic License. */ -import { get } from 'lodash'; - import { UsageCollectionSetup } from 'src/plugins/usage_collection/public'; import { UiStatsMetricType } from '@kbn/analytics'; import { UIM_APP_NAME, UIM_CONFIG_COLD_PHASE, - UIM_CONFIG_WARM_PHASE, - UIM_CONFIG_SET_PRIORITY, UIM_CONFIG_FREEZE_INDEX, - PHASE_HOT, - PHASE_WARM, - PHASE_COLD, - PHASE_INDEX_PRIORITY, + UIM_CONFIG_SET_PRIORITY, + UIM_CONFIG_WARM_PHASE, + defaultNewColdPhase, + defaultNewHotPhase, + defaultNewWarmPhase, } from '../constants'; -import { defaultColdPhase, defaultWarmPhase, defaultHotPhase } from '../store/defaults'; +import { Phases } from './policies/types'; export let trackUiMetric = (metricType: UiStatsMetricType, eventName: string) => {}; @@ -31,49 +28,54 @@ export function init(usageCollection?: UsageCollectionSetup): void { } } -export function getUiMetricsForPhases(phases: any): any { +export function getUiMetricsForPhases(phases: Phases): any { const phaseUiMetrics = [ { metric: UIM_CONFIG_COLD_PHASE, - isTracked: () => Boolean(phases[PHASE_COLD]), + isTracked: () => Boolean(phases.cold), }, { metric: UIM_CONFIG_WARM_PHASE, - isTracked: () => Boolean(phases[PHASE_WARM]), + isTracked: () => Boolean(phases.warm), }, { metric: UIM_CONFIG_SET_PRIORITY, isTracked: () => { - const phaseToDefaultIndexPriorityMap = { - [PHASE_HOT]: defaultHotPhase[PHASE_INDEX_PRIORITY], - [PHASE_WARM]: defaultWarmPhase[PHASE_INDEX_PRIORITY], - [PHASE_COLD]: defaultColdPhase[PHASE_INDEX_PRIORITY], - }; - // We only care about whether the user has interacted with the priority of *any* phase at all. - return [PHASE_HOT, PHASE_WARM, PHASE_COLD].some((phase) => { - // If the priority is different than the default, we'll consider it a user interaction, - // even if the user has set it to undefined. - return ( - phases[phase] && - get(phases[phase], 'actions.set_priority.priority') !== - phaseToDefaultIndexPriorityMap[phase] - ); - }); + const isHotPhasePriorityChanged = + phases.hot && + phases.hot.actions.set_priority && + phases.hot.actions.set_priority.priority !== + parseInt(defaultNewHotPhase.phaseIndexPriority, 10); + + const isWarmPhasePriorityChanged = + phases.warm && + phases.warm.actions.set_priority && + phases.warm.actions.set_priority.priority !== + parseInt(defaultNewWarmPhase.phaseIndexPriority, 10); + + const isColdPhasePriorityChanged = + phases.cold && + phases.cold.actions.set_priority && + phases.cold.actions.set_priority.priority !== + parseInt(defaultNewColdPhase.phaseIndexPriority, 10); + // If the priority is different than the default, we'll consider it a user interaction, + // even if the user has set it to undefined. + return ( + isHotPhasePriorityChanged || isWarmPhasePriorityChanged || isColdPhasePriorityChanged + ); }, }, { metric: UIM_CONFIG_FREEZE_INDEX, - isTracked: () => phases[PHASE_COLD] && get(phases[PHASE_COLD], 'actions.freeze'), + isTracked: () => phases.cold && phases.cold.actions.freeze, }, ]; - const trackedUiMetrics = phaseUiMetrics.reduce((tracked: any, { metric, isTracked }) => { + return phaseUiMetrics.reduce((tracked: any, { metric, isTracked }) => { if (isTracked()) { tracked.push(metric); } return tracked; }, []); - - return trackedUiMetrics; } diff --git a/x-pack/plugins/index_lifecycle_management/public/application/store/actions/general.js b/x-pack/plugins/index_lifecycle_management/public/application/store/actions/general.js deleted file mode 100644 index 28719fde87b0c..0000000000000 --- a/x-pack/plugins/index_lifecycle_management/public/application/store/actions/general.js +++ /dev/null @@ -1,11 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { createAction } from 'redux-actions'; - -export const setBootstrapEnabled = createAction('SET_BOOTSTRAP_ENABLED'); -export const setIndexName = createAction('SET_INDEX_NAME'); -export const setAliasName = createAction('SET_ALIAS_NAME'); diff --git a/x-pack/plugins/index_lifecycle_management/public/application/store/actions/index.js b/x-pack/plugins/index_lifecycle_management/public/application/store/actions/index.js index ea539578c885c..fef79c7782bb0 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/store/actions/index.js +++ b/x-pack/plugins/index_lifecycle_management/public/application/store/actions/index.js @@ -4,7 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export * from './nodes'; export * from './policies'; -export * from './lifecycle'; -export * from './general'; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/store/actions/nodes.js b/x-pack/plugins/index_lifecycle_management/public/application/store/actions/nodes.js deleted file mode 100644 index 45a8e63f70e83..0000000000000 --- a/x-pack/plugins/index_lifecycle_management/public/application/store/actions/nodes.js +++ /dev/null @@ -1,8 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ -import { createAction } from 'redux-actions'; -export const setSelectedPrimaryShardCount = createAction('SET_SELECTED_PRIMARY_SHARED_COUNT'); -export const setSelectedReplicaCount = createAction('SET_SELECTED_REPLICA_COUNT'); diff --git a/x-pack/plugins/index_lifecycle_management/public/application/store/actions/policies.js b/x-pack/plugins/index_lifecycle_management/public/application/store/actions/policies.js index aa20c0eb1d326..d47136679604f 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/store/actions/policies.js +++ b/x-pack/plugins/index_lifecycle_management/public/application/store/actions/policies.js @@ -9,7 +9,6 @@ import { createAction } from 'redux-actions'; import { showApiError } from '../../services/api_errors'; import { loadPolicies } from '../../services/api'; -import { SET_PHASE_DATA } from '../../constants'; export const fetchedPolicies = createAction('FETCHED_POLICIES'); export const setSelectedPolicy = createAction('SET_SELECTED_POLICY'); @@ -41,9 +40,3 @@ export const fetchPolicies = (withIndices, callback) => async (dispatch) => { callback && callback(); return policies; }; - -export const setPhaseData = createAction(SET_PHASE_DATA, (phase, key, value) => ({ - phase, - key, - value, -})); diff --git a/x-pack/plugins/index_lifecycle_management/public/application/store/defaults/cold_phase.js b/x-pack/plugins/index_lifecycle_management/public/application/store/defaults/cold_phase.js deleted file mode 100644 index a8f7fd3f4bdfa..0000000000000 --- a/x-pack/plugins/index_lifecycle_management/public/application/store/defaults/cold_phase.js +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ -import { - PHASE_ENABLED, - PHASE_ROLLOVER_MINIMUM_AGE, - PHASE_NODE_ATTRS, - PHASE_REPLICA_COUNT, - PHASE_ROLLOVER_MINIMUM_AGE_UNITS, - PHASE_ROLLOVER_ALIAS, - PHASE_FREEZE_ENABLED, - PHASE_INDEX_PRIORITY, -} from '../../constants'; - -export const defaultColdPhase = { - [PHASE_ENABLED]: false, - [PHASE_ROLLOVER_ALIAS]: '', - [PHASE_ROLLOVER_MINIMUM_AGE]: 0, - [PHASE_ROLLOVER_MINIMUM_AGE_UNITS]: 'd', - [PHASE_NODE_ATTRS]: '', - [PHASE_REPLICA_COUNT]: '', - [PHASE_FREEZE_ENABLED]: false, - [PHASE_INDEX_PRIORITY]: 0, -}; -export const defaultEmptyColdPhase = { - ...defaultColdPhase, - [PHASE_INDEX_PRIORITY]: '', -}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/store/defaults/delete_phase.js b/x-pack/plugins/index_lifecycle_management/public/application/store/defaults/delete_phase.js deleted file mode 100644 index 8534893e7e3b3..0000000000000 --- a/x-pack/plugins/index_lifecycle_management/public/application/store/defaults/delete_phase.js +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ -import { - PHASE_ENABLED, - PHASE_ROLLOVER_ENABLED, - PHASE_ROLLOVER_MINIMUM_AGE, - PHASE_ROLLOVER_MINIMUM_AGE_UNITS, - PHASE_ROLLOVER_ALIAS, - PHASE_WAIT_FOR_SNAPSHOT_POLICY, -} from '../../constants'; - -export const defaultDeletePhase = { - [PHASE_ENABLED]: false, - [PHASE_ROLLOVER_ENABLED]: false, - [PHASE_ROLLOVER_ALIAS]: '', - [PHASE_ROLLOVER_MINIMUM_AGE]: 0, - [PHASE_ROLLOVER_MINIMUM_AGE_UNITS]: 'd', - [PHASE_WAIT_FOR_SNAPSHOT_POLICY]: '', -}; -export const defaultEmptyDeletePhase = defaultDeletePhase; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/store/defaults/hot_phase.js b/x-pack/plugins/index_lifecycle_management/public/application/store/defaults/hot_phase.js deleted file mode 100644 index 1f5b5c399a642..0000000000000 --- a/x-pack/plugins/index_lifecycle_management/public/application/store/defaults/hot_phase.js +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ -import { - PHASE_ENABLED, - PHASE_ROLLOVER_ENABLED, - PHASE_ROLLOVER_MAX_AGE, - PHASE_ROLLOVER_MAX_AGE_UNITS, - PHASE_ROLLOVER_MAX_SIZE_STORED, - PHASE_ROLLOVER_MAX_DOCUMENTS, - PHASE_ROLLOVER_MAX_SIZE_STORED_UNITS, - PHASE_INDEX_PRIORITY, -} from '../../constants'; - -export const defaultHotPhase = { - [PHASE_ENABLED]: true, - [PHASE_ROLLOVER_ENABLED]: true, - [PHASE_ROLLOVER_MAX_AGE]: 30, - [PHASE_ROLLOVER_MAX_AGE_UNITS]: 'd', - [PHASE_ROLLOVER_MAX_SIZE_STORED]: 50, - [PHASE_ROLLOVER_MAX_SIZE_STORED_UNITS]: 'gb', - [PHASE_INDEX_PRIORITY]: 100, - [PHASE_ROLLOVER_MAX_DOCUMENTS]: '', -}; -export const defaultEmptyHotPhase = { - ...defaultHotPhase, - [PHASE_ENABLED]: false, - [PHASE_ROLLOVER_ENABLED]: false, - [PHASE_ROLLOVER_MAX_AGE]: '', - [PHASE_ROLLOVER_MAX_SIZE_STORED]: '', - [PHASE_INDEX_PRIORITY]: '', - [PHASE_ROLLOVER_MAX_DOCUMENTS]: '', -}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/store/defaults/index.d.ts b/x-pack/plugins/index_lifecycle_management/public/application/store/defaults/index.d.ts deleted file mode 100644 index abf6db416c7f4..0000000000000 --- a/x-pack/plugins/index_lifecycle_management/public/application/store/defaults/index.d.ts +++ /dev/null @@ -1,10 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -export declare const defaultDeletePhase: any; -export declare const defaultColdPhase: any; -export declare const defaultWarmPhase: any; -export declare const defaultHotPhase: any; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/store/defaults/index.js b/x-pack/plugins/index_lifecycle_management/public/application/store/defaults/index.js deleted file mode 100644 index f5661eae91a8c..0000000000000 --- a/x-pack/plugins/index_lifecycle_management/public/application/store/defaults/index.js +++ /dev/null @@ -1,10 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -export * from './delete_phase'; -export * from './cold_phase'; -export * from './hot_phase'; -export * from './warm_phase'; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/store/defaults/warm_phase.js b/x-pack/plugins/index_lifecycle_management/public/application/store/defaults/warm_phase.js deleted file mode 100644 index f02ac2096675f..0000000000000 --- a/x-pack/plugins/index_lifecycle_management/public/application/store/defaults/warm_phase.js +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ -import { - PHASE_ENABLED, - PHASE_FORCE_MERGE_SEGMENTS, - PHASE_FORCE_MERGE_ENABLED, - PHASE_ROLLOVER_MINIMUM_AGE, - PHASE_NODE_ATTRS, - PHASE_PRIMARY_SHARD_COUNT, - PHASE_REPLICA_COUNT, - PHASE_ROLLOVER_MINIMUM_AGE_UNITS, - PHASE_ROLLOVER_ALIAS, - PHASE_SHRINK_ENABLED, - WARM_PHASE_ON_ROLLOVER, - PHASE_INDEX_PRIORITY, -} from '../../constants'; - -export const defaultWarmPhase = { - [PHASE_ENABLED]: false, - [PHASE_ROLLOVER_ALIAS]: '', - [PHASE_FORCE_MERGE_SEGMENTS]: '', - [PHASE_FORCE_MERGE_ENABLED]: false, - [PHASE_ROLLOVER_MINIMUM_AGE]: 0, - [PHASE_ROLLOVER_MINIMUM_AGE_UNITS]: 'd', - [PHASE_NODE_ATTRS]: '', - [PHASE_SHRINK_ENABLED]: false, - [PHASE_PRIMARY_SHARD_COUNT]: '', - [PHASE_REPLICA_COUNT]: '', - [WARM_PHASE_ON_ROLLOVER]: true, - [PHASE_INDEX_PRIORITY]: 50, -}; -export const defaultEmptyWarmPhase = { - ...defaultWarmPhase, - [WARM_PHASE_ON_ROLLOVER]: false, - [PHASE_INDEX_PRIORITY]: '', -}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/store/reducers/general.js b/x-pack/plugins/index_lifecycle_management/public/application/store/reducers/general.js deleted file mode 100644 index fcba2fd1358b0..0000000000000 --- a/x-pack/plugins/index_lifecycle_management/public/application/store/reducers/general.js +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { handleActions } from 'redux-actions'; -import { setIndexName, setAliasName, setBootstrapEnabled } from '../actions/general'; - -const defaultState = { - bootstrapEnabled: false, - indexName: '', - aliasName: '', -}; - -export const general = handleActions( - { - [setIndexName](state, { payload: indexName }) { - return { - ...state, - indexName, - }; - }, - [setAliasName](state, { payload: aliasName }) { - return { - ...state, - aliasName, - }; - }, - [setBootstrapEnabled](state, { payload: bootstrapEnabled }) { - return { - ...state, - bootstrapEnabled, - }; - }, - }, - defaultState -); diff --git a/x-pack/plugins/index_lifecycle_management/public/application/store/reducers/index.js b/x-pack/plugins/index_lifecycle_management/public/application/store/reducers/index.js index 60126b85c313e..7fe7134f5f5db 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/store/reducers/index.js +++ b/x-pack/plugins/index_lifecycle_management/public/application/store/reducers/index.js @@ -5,12 +5,8 @@ */ import { combineReducers } from 'redux'; -import { nodes } from './nodes'; import { policies } from './policies'; -import { general } from './general'; export const indexLifecycleManagement = combineReducers({ - nodes, policies, - general, }); diff --git a/x-pack/plugins/index_lifecycle_management/public/application/store/reducers/nodes.js b/x-pack/plugins/index_lifecycle_management/public/application/store/reducers/nodes.js deleted file mode 100644 index 383e61b5aacde..0000000000000 --- a/x-pack/plugins/index_lifecycle_management/public/application/store/reducers/nodes.js +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { handleActions } from 'redux-actions'; -import { setSelectedPrimaryShardCount, setSelectedReplicaCount } from '../actions'; - -const defaultState = { - isLoading: false, - selectedNodeAttrs: '', - selectedPrimaryShardCount: 1, - selectedReplicaCount: 1, - nodes: undefined, - details: {}, -}; - -export const nodes = handleActions( - { - [setSelectedPrimaryShardCount](state, { payload }) { - let selectedPrimaryShardCount = parseInt(payload); - if (isNaN(selectedPrimaryShardCount)) { - selectedPrimaryShardCount = ''; - } - return { - ...state, - selectedPrimaryShardCount, - }; - }, - [setSelectedReplicaCount](state, { payload }) { - let selectedReplicaCount; - if (payload != null) { - selectedReplicaCount = parseInt(payload); - if (isNaN(selectedReplicaCount)) { - selectedReplicaCount = ''; - } - } else { - // default value for Elasticsearch - selectedReplicaCount = 1; - } - - return { - ...state, - selectedReplicaCount, - }; - }, - }, - defaultState -); diff --git a/x-pack/plugins/index_lifecycle_management/public/application/store/reducers/policies.js b/x-pack/plugins/index_lifecycle_management/public/application/store/reducers/policies.js index a94e875a71845..ca9d59e295a29 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/store/reducers/policies.js +++ b/x-pack/plugins/index_lifecycle_management/public/application/store/reducers/policies.js @@ -7,49 +7,17 @@ import { handleActions } from 'redux-actions'; import { fetchedPolicies, - setSelectedPolicy, - unsetSelectedPolicy, - setSelectedPolicyName, - setSaveAsNewPolicy, - setPhaseData, policyFilterChanged, policyPageChanged, policyPageSizeChanged, policySortChanged, } from '../actions'; -import { policyFromES } from '../selectors'; -import { - PHASE_HOT, - PHASE_WARM, - PHASE_COLD, - PHASE_DELETE, - PHASE_ATTRIBUTES_THAT_ARE_NUMBERS, -} from '../../constants'; - -import { - defaultColdPhase, - defaultDeletePhase, - defaultHotPhase, - defaultWarmPhase, -} from '../defaults'; -export const defaultPolicy = { - name: '', - saveAsNew: true, - isNew: true, - phases: { - [PHASE_HOT]: defaultHotPhase, - [PHASE_WARM]: defaultWarmPhase, - [PHASE_COLD]: defaultColdPhase, - [PHASE_DELETE]: defaultDeletePhase, - }, -}; const defaultState = { isLoading: false, isLoaded: false, originalPolicyName: undefined, selectedPolicySet: false, - selectedPolicy: defaultPolicy, policies: [], sort: { sortField: 'name', @@ -70,71 +38,6 @@ export const policies = handleActions( policies, }; }, - [setSelectedPolicy](state, { payload: selectedPolicy }) { - if (!selectedPolicy) { - return { - ...state, - selectedPolicy: defaultPolicy, - selectedPolicySet: true, - }; - } - - return { - ...state, - originalPolicyName: selectedPolicy.name, - selectedPolicySet: true, - selectedPolicy: { - ...defaultPolicy, - ...policyFromES(selectedPolicy), - }, - }; - }, - [unsetSelectedPolicy]() { - return defaultState; - }, - [setSelectedPolicyName](state, { payload: name }) { - return { - ...state, - selectedPolicy: { - ...state.selectedPolicy, - name, - }, - }; - }, - [setSaveAsNewPolicy](state, { payload: saveAsNew }) { - return { - ...state, - selectedPolicy: { - ...state.selectedPolicy, - saveAsNew, - }, - }; - }, - [setPhaseData](state, { payload }) { - const { phase, key } = payload; - - let value = payload.value; - if (PHASE_ATTRIBUTES_THAT_ARE_NUMBERS.includes(key)) { - value = parseInt(value); - if (isNaN(value)) { - value = ''; - } - } - - return { - ...state, - selectedPolicy: { - ...state.selectedPolicy, - phases: { - ...state.selectedPolicy.phases, - [phase]: { - ...state.selectedPolicy.phases[phase], - [key]: value, - }, - }, - }, - }; - }, [policyFilterChanged](state, action) { const { filter } = action.payload; return { diff --git a/x-pack/plugins/index_lifecycle_management/public/application/store/selectors/general.js b/x-pack/plugins/index_lifecycle_management/public/application/store/selectors/general.js deleted file mode 100644 index 2d01749be3087..0000000000000 --- a/x-pack/plugins/index_lifecycle_management/public/application/store/selectors/general.js +++ /dev/null @@ -1,9 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -export const getBootstrapEnabled = (state) => state.general.bootstrapEnabled; -export const getIndexName = (state) => state.general.indexName; -export const getAliasName = (state) => state.general.aliasName; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/store/selectors/index.js b/x-pack/plugins/index_lifecycle_management/public/application/store/selectors/index.js index ea539578c885c..fef79c7782bb0 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/store/selectors/index.js +++ b/x-pack/plugins/index_lifecycle_management/public/application/store/selectors/index.js @@ -4,7 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export * from './nodes'; export * from './policies'; -export * from './lifecycle'; -export * from './general'; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/store/selectors/lifecycle.js b/x-pack/plugins/index_lifecycle_management/public/application/store/selectors/lifecycle.js deleted file mode 100644 index 03538fad9aa83..0000000000000 --- a/x-pack/plugins/index_lifecycle_management/public/application/store/selectors/lifecycle.js +++ /dev/null @@ -1,287 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { i18n } from '@kbn/i18n'; - -import { - PHASE_HOT, - PHASE_WARM, - PHASE_COLD, - PHASE_DELETE, - PHASE_ENABLED, - PHASE_ROLLOVER_ENABLED, - PHASE_ROLLOVER_MAX_AGE, - PHASE_ROLLOVER_MINIMUM_AGE, - PHASE_ROLLOVER_MAX_SIZE_STORED, - STRUCTURE_POLICY_NAME, - ERROR_STRUCTURE, - PHASE_ATTRIBUTES_THAT_ARE_NUMBERS_VALIDATE, - PHASE_PRIMARY_SHARD_COUNT, - PHASE_SHRINK_ENABLED, - PHASE_FORCE_MERGE_ENABLED, - PHASE_FORCE_MERGE_SEGMENTS, - PHASE_REPLICA_COUNT, - WARM_PHASE_ON_ROLLOVER, - PHASE_INDEX_PRIORITY, - PHASE_ROLLOVER_MAX_DOCUMENTS, -} from '../../constants'; - -import { - getPhase, - getPhases, - phaseToES, - getSelectedPolicyName, - isNumber, - getSaveAsNewPolicy, - getSelectedOriginalPolicyName, - getPolicies, -} from '.'; - -import { getPolicyByName } from './policies'; - -export const numberRequiredMessage = i18n.translate( - 'xpack.indexLifecycleMgmt.editPolicy.numberRequiredError', - { - defaultMessage: 'A number is required.', - } -); - -export const positiveNumberRequiredMessage = i18n.translate( - 'xpack.indexLifecycleMgmt.editPolicy.positiveNumberRequiredError', - { - defaultMessage: 'Only positive numbers are allowed.', - } -); - -export const maximumAgeRequiredMessage = i18n.translate( - 'xpack.indexLifecycleMgmt.editPolicy.maximumAgeMissingError', - { - defaultMessage: 'A maximum age is required.', - } -); - -export const maximumSizeRequiredMessage = i18n.translate( - 'xpack.indexLifecycleMgmt.editPolicy.maximumIndexSizeMissingError', - { - defaultMessage: 'A maximum index size is required.', - } -); - -export const maximumDocumentsRequiredMessage = i18n.translate( - 'xpack.indexLifecycleMgmt.editPolicy.maximumDocumentsMissingError', - { - defaultMessage: 'Maximum documents is required.', - } -); - -export const positiveNumbersAboveZeroErrorMessage = i18n.translate( - 'xpack.indexLifecycleMgmt.editPolicy.positiveNumberAboveZeroRequiredError', - { - defaultMessage: 'Only numbers above 0 are allowed.', - } -); - -export const validatePhase = (type, phase, errors) => { - const phaseErrors = {}; - - if (!phase[PHASE_ENABLED]) { - return; - } - - for (const numberedAttribute of PHASE_ATTRIBUTES_THAT_ARE_NUMBERS_VALIDATE) { - if (phase.hasOwnProperty(numberedAttribute)) { - // If WARM_PHASE_ON_ROLLOVER or PHASE_HOT there is no need to validate this - if ( - numberedAttribute === PHASE_ROLLOVER_MINIMUM_AGE && - (phase[WARM_PHASE_ON_ROLLOVER] || type === PHASE_HOT) - ) { - continue; - } - // If shrink is disabled, there is no need to validate this - if (numberedAttribute === PHASE_PRIMARY_SHARD_COUNT && !phase[PHASE_SHRINK_ENABLED]) { - continue; - } - // If forcemerge is disabled, there is no need to validate this - if (numberedAttribute === PHASE_FORCE_MERGE_SEGMENTS && !phase[PHASE_FORCE_MERGE_ENABLED]) { - continue; - } - // PHASE_REPLICA_COUNT is optional and can be zero - if (numberedAttribute === PHASE_REPLICA_COUNT && !phase[numberedAttribute]) { - continue; - } - // PHASE_INDEX_PRIORITY is optional and can be zero - if (numberedAttribute === PHASE_INDEX_PRIORITY && !phase[numberedAttribute]) { - continue; - } - if (!isNumber(phase[numberedAttribute])) { - phaseErrors[numberedAttribute] = [numberRequiredMessage]; - } else if (phase[numberedAttribute] < 0) { - phaseErrors[numberedAttribute] = [positiveNumberRequiredMessage]; - } - } - } - if (phase[PHASE_ROLLOVER_ENABLED]) { - if ( - !isNumber(phase[PHASE_ROLLOVER_MAX_AGE]) && - !isNumber(phase[PHASE_ROLLOVER_MAX_SIZE_STORED]) && - !isNumber(phase[PHASE_ROLLOVER_MAX_DOCUMENTS]) - ) { - phaseErrors[PHASE_ROLLOVER_MAX_AGE] = [maximumAgeRequiredMessage]; - phaseErrors[PHASE_ROLLOVER_MAX_SIZE_STORED] = [maximumSizeRequiredMessage]; - phaseErrors[PHASE_ROLLOVER_MAX_DOCUMENTS] = [maximumDocumentsRequiredMessage]; - } - if (isNumber(phase[PHASE_ROLLOVER_MAX_AGE]) && phase[PHASE_ROLLOVER_MAX_AGE] < 1) { - phaseErrors[PHASE_ROLLOVER_MAX_AGE] = [positiveNumbersAboveZeroErrorMessage]; - } - if ( - isNumber(phase[PHASE_ROLLOVER_MAX_SIZE_STORED]) && - phase[PHASE_ROLLOVER_MAX_SIZE_STORED] < 1 - ) { - phaseErrors[PHASE_ROLLOVER_MAX_SIZE_STORED] = [positiveNumbersAboveZeroErrorMessage]; - } - if (isNumber(phase[PHASE_ROLLOVER_MAX_DOCUMENTS]) && phase[PHASE_ROLLOVER_MAX_DOCUMENTS] < 1) { - phaseErrors[PHASE_ROLLOVER_MAX_DOCUMENTS] = [positiveNumbersAboveZeroErrorMessage]; - } - } - if (phase[PHASE_SHRINK_ENABLED]) { - if (!isNumber(phase[PHASE_PRIMARY_SHARD_COUNT])) { - phaseErrors[PHASE_PRIMARY_SHARD_COUNT] = [numberRequiredMessage]; - } else if (phase[PHASE_PRIMARY_SHARD_COUNT] < 1) { - phaseErrors[PHASE_PRIMARY_SHARD_COUNT] = [positiveNumbersAboveZeroErrorMessage]; - } - } - - if (phase[PHASE_FORCE_MERGE_ENABLED]) { - if (!isNumber(phase[PHASE_FORCE_MERGE_SEGMENTS])) { - phaseErrors[PHASE_FORCE_MERGE_SEGMENTS] = [numberRequiredMessage]; - } else if (phase[PHASE_FORCE_MERGE_SEGMENTS] < 1) { - phaseErrors[PHASE_FORCE_MERGE_SEGMENTS] = [positiveNumbersAboveZeroErrorMessage]; - } - } - errors[type] = { - ...errors[type], - ...phaseErrors, - }; -}; - -export const policyNameRequiredMessage = i18n.translate( - 'xpack.indexLifecycleMgmt.editPolicy.policyNameRequiredError', - { - defaultMessage: 'A policy name is required.', - } -); -export const policyNameStartsWithUnderscoreErrorMessage = i18n.translate( - 'xpack.indexLifecycleMgmt.editPolicy.policyNameStartsWithUnderscoreError', - { - defaultMessage: 'A policy name cannot start with an underscore.', - } -); -export const policyNameContainsCommaErrorMessage = i18n.translate( - 'xpack.indexLifecycleMgmt.editPolicy.policyNameContainsCommaError', - { - defaultMessage: 'A policy name cannot include a comma.', - } -); -export const policyNameContainsSpaceErrorMessage = i18n.translate( - 'xpack.indexLifecycleMgmt.editPolicy.policyNameContainsSpaceError', - { - defaultMessage: 'A policy name cannot include a space.', - } -); -export const policyNameTooLongErrorMessage = i18n.translate( - 'xpack.indexLifecycleMgmt.editPolicy.policyNameTooLongError', - { - defaultMessage: 'A policy name cannot be longer than 255 bytes.', - } -); -export const policyNameMustBeDifferentErrorMessage = i18n.translate( - 'xpack.indexLifecycleMgmt.editPolicy.differentPolicyNameRequiredError', - { - defaultMessage: 'The policy name must be different.', - } -); -export const policyNameAlreadyUsedErrorMessage = i18n.translate( - 'xpack.indexLifecycleMgmt.editPolicy.policyNameAlreadyUsedError', - { - defaultMessage: 'That policy name is already used.', - } -); -export const validateLifecycle = (state) => { - // This method of deep copy does not always work but it should be fine here - const errors = JSON.parse(JSON.stringify(ERROR_STRUCTURE)); - const policyName = getSelectedPolicyName(state); - if (!policyName) { - errors[STRUCTURE_POLICY_NAME].push(policyNameRequiredMessage); - } else { - if (policyName.startsWith('_')) { - errors[STRUCTURE_POLICY_NAME].push(policyNameStartsWithUnderscoreErrorMessage); - } - if (policyName.includes(',')) { - errors[STRUCTURE_POLICY_NAME].push(policyNameContainsCommaErrorMessage); - } - if (policyName.includes(' ')) { - errors[STRUCTURE_POLICY_NAME].push(policyNameContainsSpaceErrorMessage); - } - if (window.TextEncoder && new window.TextEncoder('utf-8').encode(policyName).length > 255) { - errors[STRUCTURE_POLICY_NAME].push(policyNameTooLongErrorMessage); - } - } - - if ( - getSaveAsNewPolicy(state) && - getSelectedOriginalPolicyName(state) === getSelectedPolicyName(state) - ) { - errors[STRUCTURE_POLICY_NAME].push(policyNameMustBeDifferentErrorMessage); - } else if (getSelectedOriginalPolicyName(state) !== getSelectedPolicyName(state)) { - const policyNames = getPolicies(state).map((policy) => policy.name); - if (policyNames.includes(getSelectedPolicyName(state))) { - errors[STRUCTURE_POLICY_NAME].push(policyNameAlreadyUsedErrorMessage); - } - } - - const hotPhase = getPhase(state, PHASE_HOT); - const warmPhase = getPhase(state, PHASE_WARM); - const coldPhase = getPhase(state, PHASE_COLD); - const deletePhase = getPhase(state, PHASE_DELETE); - - validatePhase(PHASE_HOT, hotPhase, errors); - validatePhase(PHASE_WARM, warmPhase, errors); - validatePhase(PHASE_COLD, coldPhase, errors); - validatePhase(PHASE_DELETE, deletePhase, errors); - return errors; -}; - -export const getLifecycle = (state) => { - const policyName = getSelectedPolicyName(state); - const phases = Object.entries(getPhases(state)).reduce((accum, [phaseName, phase]) => { - // Hot is ALWAYS enabled - if (phaseName === PHASE_HOT) { - phase[PHASE_ENABLED] = true; - } - const esPolicy = getPolicyByName(state, policyName).policy || {}; - const esPhase = esPolicy.phases ? esPolicy.phases[phaseName] : {}; - if (phase[PHASE_ENABLED]) { - accum[phaseName] = phaseToES(phase, esPhase); - - // These seem to be constants - if (phaseName === PHASE_DELETE) { - accum[phaseName].actions = { - ...accum[phaseName].actions, - delete: { - ...accum[phaseName].actions.delete, - }, - }; - } - } - return accum; - }, {}); - - return { - name: getSelectedPolicyName(state), - //type, TODO: figure this out (jsut store it and not let the user change it?) - phases, - }; -}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/store/selectors/nodes.js b/x-pack/plugins/index_lifecycle_management/public/application/store/selectors/nodes.js deleted file mode 100644 index 72bfd4b15a78a..0000000000000 --- a/x-pack/plugins/index_lifecycle_management/public/application/store/selectors/nodes.js +++ /dev/null @@ -1,12 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -export const getNodes = (state) => state.nodes.nodes; - -export const getSelectedPrimaryShardCount = (state) => state.nodes.selectedPrimaryShardCount; - -export const getSelectedReplicaCount = (state) => - state.nodes.selectedReplicaCount !== undefined ? state.nodes.selectedReplicaCount : 1; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/store/selectors/policies.js b/x-pack/plugins/index_lifecycle_management/public/application/store/selectors/policies.js index 5bea22f0b3a76..e1c89314a2ec5 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/store/selectors/policies.js +++ b/x-pack/plugins/index_lifecycle_management/public/application/store/selectors/policies.js @@ -7,49 +7,9 @@ import { createSelector } from 'reselect'; import { Pager } from '@elastic/eui'; -import { - PHASE_HOT, - PHASE_WARM, - PHASE_COLD, - PHASE_DELETE, - PHASE_ROLLOVER_MINIMUM_AGE, - PHASE_ROLLOVER_MINIMUM_AGE_UNITS, - PHASE_ROLLOVER_ENABLED, - PHASE_ROLLOVER_MAX_AGE, - PHASE_ROLLOVER_MAX_AGE_UNITS, - PHASE_ROLLOVER_MAX_SIZE_STORED, - PHASE_ROLLOVER_MAX_SIZE_STORED_UNITS, - PHASE_NODE_ATTRS, - PHASE_FORCE_MERGE_ENABLED, - PHASE_FORCE_MERGE_SEGMENTS, - PHASE_PRIMARY_SHARD_COUNT, - PHASE_REPLICA_COUNT, - PHASE_ENABLED, - PHASE_ATTRIBUTES_THAT_ARE_NUMBERS, - WARM_PHASE_ON_ROLLOVER, - PHASE_SHRINK_ENABLED, - PHASE_FREEZE_ENABLED, - PHASE_INDEX_PRIORITY, - PHASE_ROLLOVER_MAX_DOCUMENTS, - PHASE_WAIT_FOR_SNAPSHOT_POLICY, -} from '../../constants'; - import { filterItems, sortTable } from '../../services'; -import { - defaultEmptyDeletePhase, - defaultEmptyColdPhase, - defaultEmptyWarmPhase, - defaultEmptyHotPhase, -} from '../defaults'; - export const getPolicies = (state) => state.policies.policies; -export const getPolicyByName = (state, name) => - getPolicies(state).find((policy) => policy.name === name) || {}; -export const getIsNewPolicy = (state) => state.policies.selectedPolicy.isNew; -export const getSelectedPolicy = (state) => state.policies.selectedPolicy; -export const getIsSelectedPolicySet = (state) => state.policies.selectedPolicySet; -export const getSelectedOriginalPolicyName = (state) => state.policies.originalPolicyName; export const getPolicyFilter = (state) => state.policies.filter; export const getPolicySort = (state) => state.policies.sort; export const getPolicyCurrentPage = (state) => state.policies.currentPage; @@ -77,255 +37,6 @@ export const getPageOfPolicies = createSelector( (filteredPolicies, sort, pager) => { const sortedPolicies = sortTable(filteredPolicies, sort.sortField, sort.isSortAscending); const { firstItemIndex, lastItemIndex } = pager; - const pagedPolicies = sortedPolicies.slice(firstItemIndex, lastItemIndex + 1); - return pagedPolicies; + return sortedPolicies.slice(firstItemIndex, lastItemIndex + 1); } ); -export const getSaveAsNewPolicy = (state) => state.policies.selectedPolicy.saveAsNew; - -export const getSelectedPolicyName = (state) => { - if (!getSaveAsNewPolicy(state)) { - return getSelectedOriginalPolicyName(state); - } - return state.policies.selectedPolicy.name; -}; - -export const getPhases = (state) => state.policies.selectedPolicy.phases; - -export const getPhase = (state, phase) => getPhases(state)[phase]; - -export const getPhaseData = (state, phase, key) => { - if (PHASE_ATTRIBUTES_THAT_ARE_NUMBERS.includes(key)) { - return parseInt(getPhase(state, phase)[key]); - } - return getPhase(state, phase)[key]; -}; - -export const splitSizeAndUnits = (field) => { - let size; - let units; - - const result = /(\d+)(\w+)/.exec(field); - if (result) { - size = parseInt(result[1]) || 0; - units = result[2]; - } - - return { - size, - units, - }; -}; - -export const isNumber = (value) => typeof value === 'number'; -export const isEmptyObject = (obj) => { - return !obj || (Object.entries(obj).length === 0 && obj.constructor === Object); -}; - -const phaseFromES = (phase, phaseName, defaultEmptyPolicy) => { - const policy = { ...defaultEmptyPolicy }; - if (!phase) { - return policy; - } - - policy[PHASE_ENABLED] = true; - - if (phase.min_age) { - if (phaseName === PHASE_WARM && phase.min_age === '0ms') { - policy[WARM_PHASE_ON_ROLLOVER] = true; - } else { - const { size: minAge, units: minAgeUnits } = splitSizeAndUnits(phase.min_age); - policy[PHASE_ROLLOVER_MINIMUM_AGE] = minAge; - policy[PHASE_ROLLOVER_MINIMUM_AGE_UNITS] = minAgeUnits; - } - } - if (phaseName === PHASE_WARM) { - policy[PHASE_SHRINK_ENABLED] = false; - policy[PHASE_FORCE_MERGE_ENABLED] = false; - } - if (phase.actions) { - const actions = phase.actions; - - if (actions.rollover) { - const rollover = actions.rollover; - policy[PHASE_ROLLOVER_ENABLED] = true; - if (rollover.max_age) { - const { size: maxAge, units: maxAgeUnits } = splitSizeAndUnits(rollover.max_age); - policy[PHASE_ROLLOVER_MAX_AGE] = maxAge; - policy[PHASE_ROLLOVER_MAX_AGE_UNITS] = maxAgeUnits; - } - if (rollover.max_size) { - const { size: maxSize, units: maxSizeUnits } = splitSizeAndUnits(rollover.max_size); - policy[PHASE_ROLLOVER_MAX_SIZE_STORED] = maxSize; - policy[PHASE_ROLLOVER_MAX_SIZE_STORED_UNITS] = maxSizeUnits; - } - if (rollover.max_docs) { - policy[PHASE_ROLLOVER_MAX_DOCUMENTS] = rollover.max_docs; - } - } - - if (actions.allocate) { - const allocate = actions.allocate; - if (allocate.require) { - Object.entries(allocate.require).forEach((entry) => { - policy[PHASE_NODE_ATTRS] = entry.join(':'); - }); - // checking for null or undefined here - if (allocate.number_of_replicas != null) { - policy[PHASE_REPLICA_COUNT] = allocate.number_of_replicas; - } - } - } - - if (actions.forcemerge) { - const forcemerge = actions.forcemerge; - policy[PHASE_FORCE_MERGE_ENABLED] = true; - policy[PHASE_FORCE_MERGE_SEGMENTS] = forcemerge.max_num_segments; - } - - if (actions.shrink) { - policy[PHASE_SHRINK_ENABLED] = true; - policy[PHASE_PRIMARY_SHARD_COUNT] = actions.shrink.number_of_shards; - } - - if (actions.freeze) { - policy[PHASE_FREEZE_ENABLED] = true; - } - - if (actions.set_priority) { - const { priority } = actions.set_priority; - - policy[PHASE_INDEX_PRIORITY] = priority ?? ''; - } - - if (actions.wait_for_snapshot) { - policy[PHASE_WAIT_FOR_SNAPSHOT_POLICY] = actions.wait_for_snapshot.policy; - } - } - return policy; -}; - -export const policyFromES = (policy) => { - const { - name, - policy: { phases }, - } = policy; - - return { - name, - phases: { - [PHASE_HOT]: phaseFromES(phases[PHASE_HOT], PHASE_HOT, defaultEmptyHotPhase), - [PHASE_WARM]: phaseFromES(phases[PHASE_WARM], PHASE_WARM, defaultEmptyWarmPhase), - [PHASE_COLD]: phaseFromES(phases[PHASE_COLD], PHASE_COLD, defaultEmptyColdPhase), - [PHASE_DELETE]: phaseFromES(phases[PHASE_DELETE], PHASE_DELETE, defaultEmptyDeletePhase), - }, - isNew: false, - saveAsNew: false, - }; -}; - -export const phaseToES = (phase, originalEsPhase) => { - const esPhase = { ...originalEsPhase }; - - if (!phase[PHASE_ENABLED]) { - return {}; - } - if (isNumber(phase[PHASE_ROLLOVER_MINIMUM_AGE])) { - esPhase.min_age = `${phase[PHASE_ROLLOVER_MINIMUM_AGE]}${phase[PHASE_ROLLOVER_MINIMUM_AGE_UNITS]}`; - } - - // If warm phase on rollover is enabled, delete min age field - // An index lifecycle switches to warm phase when rollover occurs, so you cannot specify a warm phase time - // They are mutually exclusive - if (phase[WARM_PHASE_ON_ROLLOVER]) { - delete esPhase.min_age; - } - - esPhase.actions = esPhase.actions || {}; - - if (phase[PHASE_ROLLOVER_ENABLED]) { - esPhase.actions.rollover = {}; - - if (isNumber(phase[PHASE_ROLLOVER_MAX_AGE])) { - esPhase.actions.rollover.max_age = `${phase[PHASE_ROLLOVER_MAX_AGE]}${phase[PHASE_ROLLOVER_MAX_AGE_UNITS]}`; - } - if (isNumber(phase[PHASE_ROLLOVER_MAX_SIZE_STORED])) { - esPhase.actions.rollover.max_size = `${phase[PHASE_ROLLOVER_MAX_SIZE_STORED]}${phase[PHASE_ROLLOVER_MAX_SIZE_STORED_UNITS]}`; - } - if (isNumber(phase[PHASE_ROLLOVER_MAX_DOCUMENTS])) { - esPhase.actions.rollover.max_docs = phase[PHASE_ROLLOVER_MAX_DOCUMENTS]; - } - } else { - delete esPhase.actions.rollover; - } - if (phase[PHASE_NODE_ATTRS]) { - const [name, value] = phase[PHASE_NODE_ATTRS].split(':'); - esPhase.actions.allocate = esPhase.actions.allocate || {}; - esPhase.actions.allocate.require = { - [name]: value, - }; - } else { - if (esPhase.actions.allocate) { - delete esPhase.actions.allocate.require; - } - } - if (isNumber(phase[PHASE_REPLICA_COUNT])) { - esPhase.actions.allocate = esPhase.actions.allocate || {}; - esPhase.actions.allocate.number_of_replicas = phase[PHASE_REPLICA_COUNT]; - } else { - if (esPhase.actions.allocate) { - delete esPhase.actions.allocate.number_of_replicas; - } - } - if ( - esPhase.actions.allocate && - !esPhase.actions.allocate.require && - !isNumber(esPhase.actions.allocate.number_of_replicas) && - isEmptyObject(esPhase.actions.allocate.include) && - isEmptyObject(esPhase.actions.allocate.exclude) - ) { - // remove allocate action if it does not define require or number of nodes - // and both include and exclude are empty objects (ES will fail to parse if we don't) - delete esPhase.actions.allocate; - } - - if (phase[PHASE_FORCE_MERGE_ENABLED]) { - esPhase.actions.forcemerge = { - max_num_segments: phase[PHASE_FORCE_MERGE_SEGMENTS], - }; - } else { - delete esPhase.actions.forcemerge; - } - - if (phase[PHASE_SHRINK_ENABLED] && isNumber(phase[PHASE_PRIMARY_SHARD_COUNT])) { - esPhase.actions.shrink = { - number_of_shards: phase[PHASE_PRIMARY_SHARD_COUNT], - }; - } else { - delete esPhase.actions.shrink; - } - - if (phase[PHASE_FREEZE_ENABLED]) { - esPhase.actions.freeze = {}; - } else { - delete esPhase.actions.freeze; - } - if (isNumber(phase[PHASE_INDEX_PRIORITY])) { - esPhase.actions.set_priority = { - priority: phase[PHASE_INDEX_PRIORITY], - }; - } else if (phase[PHASE_INDEX_PRIORITY] === '') { - esPhase.actions.set_priority = { - priority: null, - }; - } - - if (phase[PHASE_WAIT_FOR_SNAPSHOT_POLICY]) { - esPhase.actions.wait_for_snapshot = { - policy: phase[PHASE_WAIT_FOR_SNAPSHOT_POLICY], - }; - } else { - delete esPhase.actions.wait_for_snapshot; - } - return esPhase; -}; From e9446b2060efd1d25f3a7bda4ee9298cb7844e06 Mon Sep 17 00:00:00 2001 From: Robert Austin Date: Tue, 25 Aug 2020 13:34:29 -0400 Subject: [PATCH 46/71] [Resolver] restore function to the resolverTest plugin. (#75799) Restore the resolverTest plugin. This will allow us to run the test plugin and try out Resolver using our mock data access layers. Eventually this could be expanded to support multiple different data access layers. It could even be expanded to allow us to control the data access layer via the browser. Another option: we could export the APIs from the server and use those in this test plugin. We eventually expect other plugins to use Resolver. This test plugin could allow us to test Resolver via the FTR (separately of the Security Solution.) This would also be useful for writing tests than use the FTR but which are essentially unit tests. For example: taking screenshots, using the mouse to zoom/pan. Start using: `yarn start --plugin-path x-pack/test/plugin_functional/plugins/resolver_test/` --- .../public/common/store/epic.ts | 12 +- .../public/common/store/store.ts | 6 +- .../security_solution/public/plugin.tsx | 9 +- .../public/resolver/index.ts | 30 ++++ .../public/resolver/store/index.ts | 2 +- .../test_utilities/simulator/index.tsx | 2 +- .../public/resolver/types.ts | 42 ++++- .../public/resolver/view/index.tsx | 4 +- .../public/timelines/store/timeline/types.ts | 4 +- .../plugins/security_solution/public/types.ts | 6 +- .../plugins/resolver_test/kibana.json | 9 +- .../applications/resolver_test/index.tsx | 158 ++++++++---------- .../plugins/resolver_test/public/plugin.ts | 48 +++--- 13 files changed, 195 insertions(+), 137 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/resolver/index.ts diff --git a/x-pack/plugins/security_solution/public/common/store/epic.ts b/x-pack/plugins/security_solution/public/common/store/epic.ts index d9de7951a86f4..51a9377b9fd04 100644 --- a/x-pack/plugins/security_solution/public/common/store/epic.ts +++ b/x-pack/plugins/security_solution/public/common/store/epic.ts @@ -4,14 +4,22 @@ * you may not use this file except in compliance with the Elastic License. */ -import { combineEpics } from 'redux-observable'; +import { combineEpics, Epic } from 'redux-observable'; +import { Action } from 'redux'; + import { createTimelineEpic } from '../../timelines/store/timeline/epic'; import { createTimelineFavoriteEpic } from '../../timelines/store/timeline/epic_favorite'; import { createTimelineNoteEpic } from '../../timelines/store/timeline/epic_note'; import { createTimelinePinnedEventEpic } from '../../timelines/store/timeline/epic_pinned_event'; import { createTimelineLocalStorageEpic } from '../../timelines/store/timeline/epic_local_storage'; +import { TimelineEpicDependencies } from '../../timelines/store/timeline/types'; -export const createRootEpic = () => +export const createRootEpic = (): Epic< + Action, + Action, + State, + TimelineEpicDependencies +> => combineEpics( createTimelineEpic(), createTimelineFavoriteEpic(), diff --git a/x-pack/plugins/security_solution/public/common/store/store.ts b/x-pack/plugins/security_solution/public/common/store/store.ts index a39c9f18bcdb8..f041e1fd82a9f 100644 --- a/x-pack/plugins/security_solution/public/common/store/store.ts +++ b/x-pack/plugins/security_solution/public/common/store/store.ts @@ -13,6 +13,7 @@ import { Middleware, Dispatch, PreloadedState, + CombinedState, } from 'redux'; import { createEpicMiddleware } from 'redux-observable'; @@ -30,6 +31,7 @@ import { Immutable } from '../../../common/endpoint/types'; import { State } from './types'; import { Storage } from '../../../../../../src/plugins/kibana_utils/public'; import { CoreStart } from '../../../../../../src/core/public'; +import { TimelineEpicDependencies } from '../../timelines/store/timeline/types'; type ComposeType = typeof compose; declare global { @@ -56,7 +58,7 @@ export const createStore = ( ): Store => { const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose; - const middlewareDependencies = { + const middlewareDependencies: TimelineEpicDependencies = { apolloClient$: apolloClient, kibana$: kibana, selectAllTimelineQuery: inputsSelectors.globalQueryByIdSelector, @@ -80,7 +82,7 @@ export const createStore = ( ) ); - epicMiddleware.run(createRootEpic()); + epicMiddleware.run(createRootEpic>()); return store; }; diff --git a/x-pack/plugins/security_solution/public/plugin.tsx b/x-pack/plugins/security_solution/public/plugin.tsx index f1a933fb34d66..a691dd98e7081 100644 --- a/x-pack/plugins/security_solution/public/plugin.tsx +++ b/x-pack/plugins/security_solution/public/plugin.tsx @@ -66,7 +66,7 @@ export class Plugin implements IPlugin, plugins: SetupPlugins) { + public setup(core: CoreSetup, plugins: SetupPlugins): PluginSetup { initTelemetry(plugins.usageCollection, APP_ID); plugins.home.featureCatalogue.register({ @@ -319,7 +319,12 @@ export class Plugin implements IPlugin { + const { resolverPluginSetup } = await import('./resolver'); + return resolverPluginSetup(); + }, + }; } public start(core: CoreStart, plugins: StartPlugins) { diff --git a/x-pack/plugins/security_solution/public/resolver/index.ts b/x-pack/plugins/security_solution/public/resolver/index.ts new file mode 100644 index 0000000000000..409f82c9d1560 --- /dev/null +++ b/x-pack/plugins/security_solution/public/resolver/index.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; + * you may not use this file except in compliance with the Elastic License. + */ +import { Provider } from 'react-redux'; +import { ResolverPluginSetup } from './types'; +import { resolverStoreFactory } from './store/index'; +import { ResolverWithoutProviders } from './view/resolver_without_providers'; +import { noAncestorsTwoChildren } from './data_access_layer/mocks/no_ancestors_two_children'; + +/** + * These exports are used by the plugin 'resolverTest' defined in x-pack's plugin_functional suite. + */ + +/** + * Provide access to Resolver APIs. + */ +export function resolverPluginSetup(): ResolverPluginSetup { + return { + Provider, + storeFactory: resolverStoreFactory, + ResolverWithoutProviders, + mocks: { + dataAccessLayer: { + noAncestorsTwoChildren, + }, + }, + }; +} diff --git a/x-pack/plugins/security_solution/public/resolver/store/index.ts b/x-pack/plugins/security_solution/public/resolver/store/index.ts index 950a61db33f17..ed8a5129c7ff6 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/index.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/index.ts @@ -11,7 +11,7 @@ import { resolverReducer } from './reducer'; import { resolverMiddlewareFactory } from './middleware'; import { ResolverAction } from './actions'; -export const storeFactory = ( +export const resolverStoreFactory = ( dataAccessLayer: DataAccessLayer ): Store => { const actionsDenylist: Array = ['userMovedPointer']; diff --git a/x-pack/plugins/security_solution/public/resolver/test_utilities/simulator/index.tsx b/x-pack/plugins/security_solution/public/resolver/test_utilities/simulator/index.tsx index b79b7df48a6de..a6520c8f0e06f 100644 --- a/x-pack/plugins/security_solution/public/resolver/test_utilities/simulator/index.tsx +++ b/x-pack/plugins/security_solution/public/resolver/test_utilities/simulator/index.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { Store, createStore, applyMiddleware } from 'redux'; import { mount, ReactWrapper } from 'enzyme'; -import { createMemoryHistory, History as HistoryPackageHistoryInterface } from 'history'; +import { History as HistoryPackageHistoryInterface, createMemoryHistory } from 'history'; import { CoreStart } from '../../../../../../../src/core/public'; import { coreMock } from '../../../../../../../src/core/public/mocks'; import { spyMiddlewareFactory } from '../spy_middleware_factory'; diff --git a/x-pack/plugins/security_solution/public/resolver/types.ts b/x-pack/plugins/security_solution/public/resolver/types.ts index 97d97700b11ae..33f7a1d97db13 100644 --- a/x-pack/plugins/security_solution/public/resolver/types.ts +++ b/x-pack/plugins/security_solution/public/resolver/types.ts @@ -9,6 +9,7 @@ import { Store } from 'redux'; import { Middleware, Dispatch } from 'redux'; import { BBox } from 'rbush'; +import { Provider } from 'react-redux'; import { ResolverAction } from './store/actions'; import { ResolverRelatedEvents, @@ -410,7 +411,7 @@ export interface SideEffectSimulator { /** * Mocked `SideEffectors`. */ - mock: jest.Mocked> & Pick; + mock: SideEffectors; } /** @@ -532,3 +533,42 @@ export interface SpyMiddleware { */ debugActions: () => () => void; } + +/** + * values of this type are exposed by the Security Solution plugin's setup phase. + */ +export interface ResolverPluginSetup { + /** + * Provide access to the instance of the `react-redux` `Provider` that Resolver recognizes. + */ + Provider: typeof Provider; + /** + * Takes a `DataAccessLayer`, which could be a mock one, and returns an redux Store. + * All data acess (e.g. HTTP requests) are done through the store. + */ + storeFactory: (dataAccessLayer: DataAccessLayer) => Store; + + /** + * The Resolver component without the required Providers. + * You must wrap this component in: `I18nProvider`, `Router` (from react-router,) `KibanaContextProvider`, + * and the `Provider` component provided by this object. + */ + ResolverWithoutProviders: React.MemoExoticComponent< + React.ForwardRefExoticComponent> + >; + + /** + * A collection of mock objects that can be used in examples or in testing. + */ + mocks: { + /** + * Mock `DataAccessLayer`s. All of Resolver's HTTP access is provided by a `DataAccessLayer`. + */ + dataAccessLayer: { + /** + * A mock `DataAccessLayer` that returns a tree that has no ancestor nodes but which has 2 children nodes. + */ + noAncestorsTwoChildren: () => { dataAccessLayer: DataAccessLayer }; + }; + }; +} diff --git a/x-pack/plugins/security_solution/public/resolver/view/index.tsx b/x-pack/plugins/security_solution/public/resolver/view/index.tsx index d9a0bf291d0e4..bcc420435e5d9 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/index.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/index.tsx @@ -7,7 +7,7 @@ import React, { useMemo } from 'react'; import { Provider } from 'react-redux'; -import { storeFactory } from '../store'; +import { resolverStoreFactory } from '../store'; import { StartServices } from '../../types'; import { useKibana } from '../../../../../../src/plugins/kibana_react/public'; import { DataAccessLayer, ResolverProps } from '../types'; @@ -24,7 +24,7 @@ export const Resolver = React.memo((props: ResolverProps) => { ]); const store = useMemo(() => { - return storeFactory(dataAccessLayer); + return resolverStoreFactory(dataAccessLayer); }, [dataAccessLayer]); return ( diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/types.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/types.ts index c64ed608339b6..8a5344e0754db 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/timeline/types.ts +++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/types.ts @@ -10,9 +10,9 @@ import { Storage } from '../../../../../../../src/plugins/kibana_utils/public'; import { AppApolloClient } from '../../../common/lib/lib'; import { inputsModel } from '../../../common/store/inputs'; import { NotesById } from '../../../common/store/app/model'; -import { StartServices } from '../../../types'; import { TimelineModel } from './model'; +import { CoreStart } from '../../../../../../../src/core/public'; export interface AutoSavedWarningMsg { timelineId: string | null; @@ -55,6 +55,6 @@ export interface TimelineEpicDependencies { selectAllTimelineQuery: () => (state: State, id: string) => inputsModel.GlobalQuery; selectNotesByIdSelector: (state: State) => NotesById; apolloClient$: Observable; - kibana$: Observable; + kibana$: Observable; storage: Storage; } diff --git a/x-pack/plugins/security_solution/public/types.ts b/x-pack/plugins/security_solution/public/types.ts index 3913b96b3e11a..fd1ff566a7719 100644 --- a/x-pack/plugins/security_solution/public/types.ts +++ b/x-pack/plugins/security_solution/public/types.ts @@ -21,6 +21,7 @@ import { } from '../../triggers_actions_ui/public'; import { SecurityPluginSetup } from '../../security/public'; import { AppFrontendLibs } from './common/lib/lib'; +import { ResolverPluginSetup } from './resolver/types'; export interface SetupPlugins { home: HomePublicPluginSetup; @@ -46,8 +47,9 @@ export type StartServices = CoreStart & storage: Storage; }; -// eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface PluginSetup {} +export interface PluginSetup { + resolver: () => Promise; +} // eslint-disable-next-line @typescript-eslint/no-empty-interface export interface PluginStart {} diff --git a/x-pack/test/plugin_functional/plugins/resolver_test/kibana.json b/x-pack/test/plugin_functional/plugins/resolver_test/kibana.json index c715a0aaa3b20..499983561e89d 100644 --- a/x-pack/test/plugin_functional/plugins/resolver_test/kibana.json +++ b/x-pack/test/plugin_functional/plugins/resolver_test/kibana.json @@ -2,8 +2,13 @@ "id": "resolver_test", "version": "1.0.0", "kibanaVersion": "kibana", - "configPath": ["xpack", "resolver_test"], - "requiredPlugins": ["embeddable"], + "configPath": ["xpack", "resolverTest"], + "requiredPlugins": [ + "securitySolution" + ], + "requiredBundles": [ + "kibanaReact" + ], "server": false, "ui": true } diff --git a/x-pack/test/plugin_functional/plugins/resolver_test/public/applications/resolver_test/index.tsx b/x-pack/test/plugin_functional/plugins/resolver_test/public/applications/resolver_test/index.tsx index 79665b6a393df..4afd71fd67a69 100644 --- a/x-pack/test/plugin_functional/plugins/resolver_test/public/applications/resolver_test/index.tsx +++ b/x-pack/test/plugin_functional/plugins/resolver_test/public/applications/resolver_test/index.tsx @@ -4,119 +4,95 @@ * you may not use this file except in compliance with the Elastic License. */ -import * as React from 'react'; +import { Router } from 'react-router-dom'; + +import React from 'react'; import ReactDOM from 'react-dom'; -import { AppMountParameters } from 'kibana/public'; -import { I18nProvider } from '@kbn/i18n/react'; -import { IEmbeddable } from 'src/plugins/embeddable/public'; -import { useEffect } from 'react'; +import { AppMountParameters, CoreStart } from 'kibana/public'; +import { useMemo } from 'react'; import styled from 'styled-components'; +import { I18nProvider } from '@kbn/i18n/react'; +import { KibanaContextProvider } from '../../../../../../../../src/plugins/kibana_react/public'; +import { + DataAccessLayer, + ResolverPluginSetup, +} from '../../../../../../../plugins/security_solution/public/resolver/types'; /** * Render the Resolver Test app. Returns a cleanup function. */ export function renderApp( - { element }: AppMountParameters, - embeddable: Promise + coreStart: CoreStart, + parameters: AppMountParameters, + resolverPluginSetup: ResolverPluginSetup ) { /** * The application DOM node should take all available space. */ - element.style.display = 'flex'; - element.style.flexGrow = '1'; + parameters.element.style.display = 'flex'; + parameters.element.style.flexGrow = '1'; ReactDOM.render( - - - , - element + , + parameters.element ); return () => { - ReactDOM.unmountComponentAtNode(element); + ReactDOM.unmountComponentAtNode(parameters.element); }; } -const AppRoot = styled( - React.memo( - ({ - embeddable: embeddablePromise, - className, - }: { - /** - * A promise which resolves to the Resolver embeddable. - */ - embeddable: Promise; - /** - * A `className` string provided by `styled` - */ - className?: string; - }) => { - /** - * This state holds the reference to the embeddable, once resolved. - */ - const [embeddable, setEmbeddable] = React.useState(undefined); - /** - * This state holds the reference to the DOM node that will contain the embeddable. - */ - const [renderTarget, setRenderTarget] = React.useState(null); - - /** - * Keep component state with the Resolver embeddable. - * - * If the reference to the embeddablePromise changes, we ignore the stale promise. - */ - useEffect(() => { - /** - * A promise rejection function that will prevent a stale embeddable promise from being resolved - * as the current eembeddable. - * - * If the embeddablePromise itself changes before the old one is resolved, we cancel and restart this effect. - */ - let cleanUp; - - const cleanupPromise = new Promise((_resolve, reject) => { - cleanUp = reject; - }); - - /** - * Either set the embeddable in state, or cancel and restart this process. - */ - Promise.race([cleanupPromise, embeddablePromise]).then((value) => { - setEmbeddable(value); - }); +const AppRoot = React.memo( + ({ + coreStart, + parameters, + resolverPluginSetup, + }: { + coreStart: CoreStart; + parameters: AppMountParameters; + resolverPluginSetup: ResolverPluginSetup; + }) => { + const { + Provider, + storeFactory, + ResolverWithoutProviders, + mocks: { + dataAccessLayer: { noAncestorsTwoChildren }, + }, + } = resolverPluginSetup; + const dataAccessLayer: DataAccessLayer = useMemo( + () => noAncestorsTwoChildren().dataAccessLayer, + [noAncestorsTwoChildren] + ); - /** - * If `embeddablePromise` is changed, the cleanup function is run. - */ - return cleanUp; - }, [embeddablePromise]); + const store = useMemo(() => { + return storeFactory(dataAccessLayer); + }, [storeFactory, dataAccessLayer]); - /** - * Render the eembeddable into the DOM node. - */ - useEffect(() => { - if (embeddable && renderTarget) { - embeddable.render(renderTarget); - /** - * If the embeddable or DOM node changes then destroy the old embeddable. - */ - return () => { - embeddable.destroy(); - }; - } - }, [embeddable, renderTarget]); + return ( + + + + + + + + + + + + ); + } +); - return ( -
- ); - } - ) -)` +const Wrapper = styled.div` /** * Take all available space. */ diff --git a/x-pack/test/plugin_functional/plugins/resolver_test/public/plugin.ts b/x-pack/test/plugin_functional/plugins/resolver_test/public/plugin.ts index 853265ae6e5de..3da3044283556 100644 --- a/x-pack/test/plugin_functional/plugins/resolver_test/public/plugin.ts +++ b/x-pack/test/plugin_functional/plugins/resolver_test/public/plugin.ts @@ -4,16 +4,16 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Plugin, CoreSetup } from 'kibana/public'; +import { Plugin, CoreSetup, AppMountParameters } from 'kibana/public'; import { i18n } from '@kbn/i18n'; -import { IEmbeddable, EmbeddableStart } from '../../../../../../src/plugins/embeddable/public'; +import { PluginSetup as SecuritySolutionPluginSetup } from '../../../../../plugins/security_solution/public'; export type ResolverTestPluginSetup = void; export type ResolverTestPluginStart = void; -export interface ResolverTestPluginSetupDependencies {} // eslint-disable-line @typescript-eslint/no-empty-interface -export interface ResolverTestPluginStartDependencies { - embeddable: EmbeddableStart; +export interface ResolverTestPluginSetupDependencies { + securitySolution: SecuritySolutionPluginSetup; } +export interface ResolverTestPluginStartDependencies {} // eslint-disable-line @typescript-eslint/no-empty-interface export class ResolverTestPlugin implements @@ -23,34 +23,24 @@ export class ResolverTestPlugin ResolverTestPluginSetupDependencies, ResolverTestPluginStartDependencies > { - public setup(core: CoreSetup) { + public setup( + core: CoreSetup, + setupDependencies: ResolverTestPluginSetupDependencies + ) { core.application.register({ - id: 'resolver_test', - title: i18n.translate('xpack.resolver_test.pluginTitle', { + id: 'resolverTest', + title: i18n.translate('xpack.resolverTest.pluginTitle', { defaultMessage: 'Resolver Test', }), - mount: async (_context, params) => { - let resolveEmbeddable: ( - value: IEmbeddable | undefined | PromiseLike | undefined - ) => void; + mount: async (params: AppMountParameters) => { + const startServices = await core.getStartServices(); + const [coreStart] = startServices; - const promise = new Promise((resolve) => { - resolveEmbeddable = resolve; - }); - - (async () => { - const [, { embeddable }] = await core.getStartServices(); - const factory = embeddable.getEmbeddableFactory('resolver'); - if (factory) { - resolveEmbeddable!(factory.create({ id: 'test basic render' })); - } - })(); - - const { renderApp } = await import('./applications/resolver_test'); - /** - * Pass a promise which resolves to the Resolver embeddable. - */ - return renderApp(params, promise); + const [{ renderApp }, resolverPluginSetup] = await Promise.all([ + import('./applications/resolver_test'), + setupDependencies.securitySolution.resolver(), + ]); + return renderApp(coreStart, params, resolverPluginSetup); }, }); } From e236bdf4af95a8219e30a9e176d3f2169cb19ab8 Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Tue, 25 Aug 2020 12:13:57 -0600 Subject: [PATCH 47/71] [Maps] add message to empty add tooltip card (#75809) * [Maps] add message to empty add tooltip card * use suggested text --- .../tooltip_selector/tooltip_selector.tsx | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/maps/public/components/tooltip_selector/tooltip_selector.tsx b/x-pack/plugins/maps/public/components/tooltip_selector/tooltip_selector.tsx index 84316a1b9105d..9bab590d1f5ea 100644 --- a/x-pack/plugins/maps/public/components/tooltip_selector/tooltip_selector.tsx +++ b/x-pack/plugins/maps/public/components/tooltip_selector/tooltip_selector.tsx @@ -13,8 +13,10 @@ import { EuiDroppable, EuiText, EuiTextAlign, + EuiTextColor, EuiSpacer, } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; import { AddTooltipFieldPopover, FieldProps } from './add_tooltip_field_popover'; import { IField } from '../../classes/fields/field'; @@ -156,7 +158,18 @@ export class TooltipSelector extends Component { _renderProperties() { if (!this.state.selectedFieldProps.length) { - return null; + return ( + +

+ + + +

+
+ ); } return ( From f2fef70282d7d2e7ecbf4e38b6b9cc075b51f361 Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Tue, 25 Aug 2020 14:21:35 -0600 Subject: [PATCH 48/71] Migrate legacy map UI settings (#75887) * Migrate legacy map UI settings * i18n fixes --- .../kibana/server/ui_setting_defaults.js | 83 +------------ src/plugins/maps_legacy/server/index.ts | 7 +- src/plugins/maps_legacy/server/ui_settings.ts | 113 ++++++++++++++++++ src/plugins/region_map/server/index.ts | 7 +- src/plugins/region_map/server/ui_settings.ts | 42 +++++++ .../translations/translations/ja-JP.json | 8 -- .../translations/translations/zh-CN.json | 8 -- 7 files changed, 167 insertions(+), 101 deletions(-) create mode 100644 src/plugins/maps_legacy/server/ui_settings.ts create mode 100644 src/plugins/region_map/server/ui_settings.ts diff --git a/src/legacy/core_plugins/kibana/server/ui_setting_defaults.js b/src/legacy/core_plugins/kibana/server/ui_setting_defaults.js index 2562657a71624..7de5fb581643a 100644 --- a/src/legacy/core_plugins/kibana/server/ui_setting_defaults.js +++ b/src/legacy/core_plugins/kibana/server/ui_setting_defaults.js @@ -17,88 +17,7 @@ * under the License. */ -import { i18n } from '@kbn/i18n'; - export function getUiSettingDefaults() { // wrapped in provider so that a new instance is given to each app/test - return { - 'visualization:tileMap:maxPrecision': { - name: i18n.translate('kbn.advancedSettings.visualization.tileMap.maxPrecisionTitle', { - defaultMessage: 'Maximum tile map precision', - }), - value: 7, - description: i18n.translate('kbn.advancedSettings.visualization.tileMap.maxPrecisionText', { - defaultMessage: - 'The maximum geoHash precision displayed on tile maps: 7 is high, 10 is very high, 12 is the max. {cellDimensionsLink}', - description: - 'Part of composite text: kbn.advancedSettings.visualization.tileMap.maxPrecisionText + ' + - 'kbn.advancedSettings.visualization.tileMap.maxPrecision.cellDimensionsLinkText', - values: { - cellDimensionsLink: - `` + - i18n.translate( - 'kbn.advancedSettings.visualization.tileMap.maxPrecision.cellDimensionsLinkText', - { - defaultMessage: 'Explanation of cell dimensions', - } - ) + - '', - }, - }), - category: ['visualization'], - }, - 'visualization:tileMap:WMSdefaults': { - name: i18n.translate('kbn.advancedSettings.visualization.tileMap.wmsDefaultsTitle', { - defaultMessage: 'Default WMS properties', - }), - value: JSON.stringify( - { - enabled: false, - url: undefined, - options: { - version: undefined, - layers: undefined, - format: 'image/png', - transparent: true, - attribution: undefined, - styles: undefined, - }, - }, - null, - 2 - ), - type: 'json', - description: i18n.translate('kbn.advancedSettings.visualization.tileMap.wmsDefaultsText', { - defaultMessage: - 'Default {propertiesLink} for the WMS map server support in the coordinate map', - description: - 'Part of composite text: kbn.advancedSettings.visualization.tileMap.wmsDefaultsText + ' + - 'kbn.advancedSettings.visualization.tileMap.wmsDefaults.propertiesLinkText', - values: { - propertiesLink: - '' + - i18n.translate( - 'kbn.advancedSettings.visualization.tileMap.wmsDefaults.propertiesLinkText', - { - defaultMessage: 'properties', - } - ) + - '', - }, - }), - category: ['visualization'], - }, - 'visualization:regionmap:showWarnings': { - name: i18n.translate('kbn.advancedSettings.visualization.showRegionMapWarningsTitle', { - defaultMessage: 'Show region map warning', - }), - value: true, - description: i18n.translate('kbn.advancedSettings.visualization.showRegionMapWarningsText', { - defaultMessage: - 'Whether the region map shows a warning when terms cannot be joined to a shape on the map.', - }), - category: ['visualization'], - }, - }; + return {}; } diff --git a/src/plugins/maps_legacy/server/index.ts b/src/plugins/maps_legacy/server/index.ts index 5da3ce1a84408..79ecbb238314a 100644 --- a/src/plugins/maps_legacy/server/index.ts +++ b/src/plugins/maps_legacy/server/index.ts @@ -18,9 +18,10 @@ */ import { Plugin, PluginConfigDescriptor } from 'kibana/server'; -import { PluginInitializerContext } from 'src/core/server'; +import { CoreSetup, PluginInitializerContext } from 'src/core/server'; import { Observable } from 'rxjs'; import { configSchema, ConfigSchema } from '../config'; +import { getUiSettings } from './ui_settings'; export const config: PluginConfigDescriptor = { exposeToBrowser: { @@ -49,7 +50,9 @@ export class MapsLegacyPlugin implements Plugin { this._initializerContext = initializerContext; } - public setup() { + public setup(core: CoreSetup) { + core.uiSettings.register(getUiSettings()); + // @ts-ignore const config$ = this._initializerContext.config.create(); return { diff --git a/src/plugins/maps_legacy/server/ui_settings.ts b/src/plugins/maps_legacy/server/ui_settings.ts new file mode 100644 index 0000000000000..f92ccf848f409 --- /dev/null +++ b/src/plugins/maps_legacy/server/ui_settings.ts @@ -0,0 +1,113 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { i18n } from '@kbn/i18n'; +import { UiSettingsParams } from 'kibana/server'; +import { schema } from '@kbn/config-schema'; + +export function getUiSettings(): Record> { + return { + 'visualization:tileMap:maxPrecision': { + name: i18n.translate('maps_legacy.advancedSettings.visualization.tileMap.maxPrecisionTitle', { + defaultMessage: 'Maximum tile map precision', + }), + value: 7, + description: i18n.translate( + 'maps_legacy.advancedSettings.visualization.tileMap.maxPrecisionText', + { + defaultMessage: + 'The maximum geoHash precision displayed on tile maps: 7 is high, 10 is very high, 12 is the max. {cellDimensionsLink}', + description: + 'Part of composite text: maps_legacy.advancedSettings.visualization.tileMap.maxPrecisionText + ' + + 'maps_legacy.advancedSettings.visualization.tileMap.maxPrecision.cellDimensionsLinkText', + values: { + cellDimensionsLink: + `` + + i18n.translate( + 'maps_legacy.advancedSettings.visualization.tileMap.maxPrecision.cellDimensionsLinkText', + { + defaultMessage: 'Explanation of cell dimensions', + } + ) + + '', + }, + } + ), + schema: schema.number(), + category: ['visualization'], + }, + 'visualization:tileMap:WMSdefaults': { + name: i18n.translate('maps_legacy.advancedSettings.visualization.tileMap.wmsDefaultsTitle', { + defaultMessage: 'Default WMS properties', + }), + value: JSON.stringify( + { + enabled: false, + url: '', + options: { + version: '', + layers: '', + format: 'image/png', + transparent: true, + attribution: '', + styles: '', + }, + }, + null, + 2 + ), + type: 'json', + description: i18n.translate( + 'maps_legacy.advancedSettings.visualization.tileMap.wmsDefaultsText', + { + defaultMessage: + 'Default {propertiesLink} for the WMS map server support in the coordinate map', + description: + 'Part of composite text: maps_legacy.advancedSettings.visualization.tileMap.wmsDefaultsText + ' + + 'maps_legacy.advancedSettings.visualization.tileMap.wmsDefaults.propertiesLinkText', + values: { + propertiesLink: + '' + + i18n.translate( + 'maps_legacy.advancedSettings.visualization.tileMap.wmsDefaults.propertiesLinkText', + { + defaultMessage: 'properties', + } + ) + + '', + }, + } + ), + schema: schema.object({ + enabled: schema.boolean(), + url: schema.string(), + options: schema.object({ + version: schema.string(), + layers: schema.string(), + format: schema.string(), + transparent: schema.boolean(), + attribution: schema.string(), + styles: schema.string(), + }), + }), + category: ['visualization'], + }, + }; +} diff --git a/src/plugins/region_map/server/index.ts b/src/plugins/region_map/server/index.ts index e2c544d2d0ba6..f4684e1c60349 100644 --- a/src/plugins/region_map/server/index.ts +++ b/src/plugins/region_map/server/index.ts @@ -18,7 +18,9 @@ */ import { PluginConfigDescriptor } from 'kibana/server'; +import { CoreSetup } from 'src/core/server'; import { configSchema, ConfigSchema } from '../config'; +import { getUiSettings } from './ui_settings'; export const config: PluginConfigDescriptor = { exposeToBrowser: { @@ -29,6 +31,9 @@ export const config: PluginConfigDescriptor = { }; export const plugin = () => ({ - setup() {}, + setup(core: CoreSetup) { + core.uiSettings.register(getUiSettings()); + }, + start() {}, }); diff --git a/src/plugins/region_map/server/ui_settings.ts b/src/plugins/region_map/server/ui_settings.ts new file mode 100644 index 0000000000000..9c404676b9ffd --- /dev/null +++ b/src/plugins/region_map/server/ui_settings.ts @@ -0,0 +1,42 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { i18n } from '@kbn/i18n'; +import { UiSettingsParams } from 'kibana/server'; +import { schema } from '@kbn/config-schema'; + +export function getUiSettings(): Record> { + return { + 'visualization:regionmap:showWarnings': { + name: i18n.translate('regionMap.advancedSettings.visualization.showRegionMapWarningsTitle', { + defaultMessage: 'Show region map warning', + }), + value: true, + description: i18n.translate( + 'regionMap.advancedSettings.visualization.showRegionMapWarningsText', + { + defaultMessage: + 'Whether the region map shows a warning when terms cannot be joined to a shape on the map.', + } + ), + schema: schema.boolean(), + category: ['visualization'], + }, + }; +} diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 118362f494b47..0b51c00475d7e 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -2775,14 +2775,6 @@ "inspector.requests.statisticsTabLabel": "統計", "inspector.title": "インスペクター", "inspector.view": "{viewName} を表示", - "kbn.advancedSettings.visualization.showRegionMapWarningsText": "用語がマップの形に合わない場合に地域マップに警告を表示するかどうかです。", - "kbn.advancedSettings.visualization.showRegionMapWarningsTitle": "地域マップに警告を表示", - "kbn.advancedSettings.visualization.tileMap.maxPrecision.cellDimensionsLinkText": "ディメンションの説明", - "kbn.advancedSettings.visualization.tileMap.maxPrecisionText": "マップに表示されるジオハッシュの最高精度です。7 が高い、10 が非常に高い、12 が最高を意味します。{cellDimensionsLink}", - "kbn.advancedSettings.visualization.tileMap.maxPrecisionTitle": "タイルマップの最高精度", - "kbn.advancedSettings.visualization.tileMap.wmsDefaults.propertiesLinkText": "プロパティ", - "kbn.advancedSettings.visualization.tileMap.wmsDefaultsText": "座標マップの WMS マップサーバーサポートのデフォルトの {propertiesLink} です。", - "kbn.advancedSettings.visualization.tileMap.wmsDefaultsTitle": "デフォルトの WMS プロパティ", "kibana_legacy.notify.fatalError.errorStatusMessage": "エラー {errStatus} {errStatusText}: {errMessage}", "kibana_legacy.notify.fatalError.unavailableServerErrorMessage": "HTTP リクエストで接続に失敗しました。Kibana サーバーが実行されていて、ご使用のブラウザの接続が正常に動作していることを確認するか、システム管理者にお問い合わせください。", "kibana_legacy.notify.toaster.errorMessage": "エラー: {errorMessage}\n {errorStack}", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index de1f206118447..d520f63fe7484 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -2776,14 +2776,6 @@ "inspector.requests.statisticsTabLabel": "统计信息", "inspector.title": "检查器", "inspector.view": "视图:{viewName}", - "kbn.advancedSettings.visualization.showRegionMapWarningsText": "词无法联接到地图上的形状时,区域地图是否显示警告。", - "kbn.advancedSettings.visualization.showRegionMapWarningsTitle": "显示区域地图警告", - "kbn.advancedSettings.visualization.tileMap.maxPrecision.cellDimensionsLinkText": "单元格维度的解释", - "kbn.advancedSettings.visualization.tileMap.maxPrecisionText": "在磁贴地图上显示的最大 geoHash 精确度:7 为高,10 为很高,12 为最大值。{cellDimensionsLink}", - "kbn.advancedSettings.visualization.tileMap.maxPrecisionTitle": "最大磁贴地图精确度", - "kbn.advancedSettings.visualization.tileMap.wmsDefaults.propertiesLinkText": "属性", - "kbn.advancedSettings.visualization.tileMap.wmsDefaultsText": "坐标地图中 WMS 地图服务器支持的默认{propertiesLink}", - "kbn.advancedSettings.visualization.tileMap.wmsDefaultsTitle": "默认 WMS 属性", "kibana_legacy.notify.fatalError.errorStatusMessage": "错误 {errStatus} {errStatusText}:{errMessage}", "kibana_legacy.notify.fatalError.unavailableServerErrorMessage": "HTTP 请求无法连接。请检查 Kibana 服务器是否正在运行以及您的浏览器是否具有有效的连接,或请联系您的系统管理员。", "kibana_legacy.notify.toaster.errorMessage": "错误:{errorMessage}\n {errorStack}", From c3e226cf31899203c69d8d0616861c7dadecfc3e Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Tue, 25 Aug 2020 14:24:14 -0600 Subject: [PATCH 49/71] [Maps] Originating App Breadcrumb (#75692) * [Maps] Originating App Breadcrumb * pass getHasUnsavedChanges instead of passing boolean Co-authored-by: Elastic Machine --- .../routes/maps_app/get_breadcrumbs.test.tsx | 36 +++++++++++ .../routes/maps_app/get_breadcrumbs.tsx | 59 +++++++++++++++++++ .../routing/routes/maps_app/maps_app_view.js | 36 ++++------- 3 files changed, 105 insertions(+), 26 deletions(-) create mode 100644 x-pack/plugins/maps/public/routing/routes/maps_app/get_breadcrumbs.test.tsx create mode 100644 x-pack/plugins/maps/public/routing/routes/maps_app/get_breadcrumbs.tsx diff --git a/x-pack/plugins/maps/public/routing/routes/maps_app/get_breadcrumbs.test.tsx b/x-pack/plugins/maps/public/routing/routes/maps_app/get_breadcrumbs.test.tsx new file mode 100644 index 0000000000000..e8e0e583a7c6d --- /dev/null +++ b/x-pack/plugins/maps/public/routing/routes/maps_app/get_breadcrumbs.test.tsx @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { getBreadcrumbs } from './get_breadcrumbs'; + +jest.mock('../../../kibana_services', () => {}); +jest.mock('../../maps_router', () => {}); + +const getHasUnsavedChanges = () => { + return false; +}; + +test('should get breadcrumbs "Maps / mymap"', () => { + const breadcrumbs = getBreadcrumbs({ title: 'mymap', getHasUnsavedChanges }); + expect(breadcrumbs.length).toBe(2); + expect(breadcrumbs[0].text).toBe('Maps'); + expect(breadcrumbs[1].text).toBe('mymap'); +}); + +test('should get breadcrumbs "Dashboard / Maps / mymap" with originatingApp', () => { + const breadcrumbs = getBreadcrumbs({ + title: 'mymap', + getHasUnsavedChanges, + originatingApp: 'dashboardId', + getAppNameFromId: (appId) => { + return 'Dashboard'; + }, + }); + expect(breadcrumbs.length).toBe(3); + expect(breadcrumbs[0].text).toBe('Dashboard'); + expect(breadcrumbs[1].text).toBe('Maps'); + expect(breadcrumbs[2].text).toBe('mymap'); +}); diff --git a/x-pack/plugins/maps/public/routing/routes/maps_app/get_breadcrumbs.tsx b/x-pack/plugins/maps/public/routing/routes/maps_app/get_breadcrumbs.tsx new file mode 100644 index 0000000000000..1ccf890597edc --- /dev/null +++ b/x-pack/plugins/maps/public/routing/routes/maps_app/get_breadcrumbs.tsx @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; +import { getNavigateToApp } from '../../../kibana_services'; +// @ts-expect-error +import { goToSpecifiedPath } from '../../maps_router'; + +export const unsavedChangesWarning = i18n.translate( + 'xpack.maps.breadCrumbs.unsavedChangesWarning', + { + defaultMessage: 'Your map has unsaved changes. Are you sure you want to leave?', + } +); + +export function getBreadcrumbs({ + title, + getHasUnsavedChanges, + originatingApp, + getAppNameFromId, +}: { + title: string; + getHasUnsavedChanges: () => boolean; + originatingApp?: string; + getAppNameFromId?: (id: string) => string; +}) { + const breadcrumbs = []; + if (originatingApp && getAppNameFromId) { + breadcrumbs.push({ + onClick: () => { + getNavigateToApp()(originatingApp); + }, + text: getAppNameFromId(originatingApp), + }); + } + + breadcrumbs.push({ + text: i18n.translate('xpack.maps.mapController.mapsBreadcrumbLabel', { + defaultMessage: 'Maps', + }), + onClick: () => { + if (getHasUnsavedChanges()) { + const navigateAway = window.confirm(unsavedChangesWarning); + if (navigateAway) { + goToSpecifiedPath('/'); + } + } else { + goToSpecifiedPath('/'); + } + }, + }); + + breadcrumbs.push({ text: title }); + + return breadcrumbs; +} diff --git a/x-pack/plugins/maps/public/routing/routes/maps_app/maps_app_view.js b/x-pack/plugins/maps/public/routing/routes/maps_app/maps_app_view.js index 58f0bf16e93f2..485b0ed7682fa 100644 --- a/x-pack/plugins/maps/public/routing/routes/maps_app/maps_app_view.js +++ b/x-pack/plugins/maps/public/routing/routes/maps_app/maps_app_view.js @@ -5,7 +5,6 @@ */ import React from 'react'; -import { i18n } from '@kbn/i18n'; import 'mapbox-gl/dist/mapbox-gl.css'; import _ from 'lodash'; import { DEFAULT_IS_LAYER_TOC_OPEN } from '../../../reducers/ui'; @@ -29,13 +28,9 @@ import { AppStateManager } from '../../state_syncing/app_state_manager'; import { startAppStateSyncing } from '../../state_syncing/app_sync'; import { esFilters } from '../../../../../../../src/plugins/data/public'; import { MapContainer } from '../../../connected_components/map_container'; -import { goToSpecifiedPath } from '../../maps_router'; import { getIndexPatternsFromIds } from '../../../index_pattern_util'; import { getTopNavConfig } from './top_nav_config'; - -const unsavedChangesWarning = i18n.translate('xpack.maps.breadCrumbs.unsavedChangesWarning', { - defaultMessage: 'Your map has unsaved changes. Are you sure you want to leave?', -}); +import { getBreadcrumbs, unsavedChangesWarning } from './get_breadcrumbs'; export class MapsAppView extends React.Component { _globalSyncUnsubscribe = null; @@ -104,7 +99,7 @@ export class MapsAppView extends React.Component { getCoreChrome().setBreadcrumbs([]); } - _hasUnsavedChanges() { + _hasUnsavedChanges = () => { const savedLayerList = this.props.savedMap.getLayerList(); return !savedLayerList ? !_.isEqual(this.props.layerListConfigOnly, this.state.initialLayerListConfig) @@ -114,27 +109,16 @@ export class MapsAppView extends React.Component { // Need to perform the same process for layerListConfigOnly to compare apples to apples // and avoid undefined properties in layerListConfigOnly triggering unsaved changes. !_.isEqual(JSON.parse(JSON.stringify(this.props.layerListConfigOnly)), savedLayerList); - } + }; _setBreadcrumbs = () => { - getCoreChrome().setBreadcrumbs([ - { - text: i18n.translate('xpack.maps.mapController.mapsBreadcrumbLabel', { - defaultMessage: 'Maps', - }), - onClick: () => { - if (this._hasUnsavedChanges()) { - const navigateAway = window.confirm(unsavedChangesWarning); - if (navigateAway) { - goToSpecifiedPath('/'); - } - } else { - goToSpecifiedPath('/'); - } - }, - }, - { text: this.props.savedMap.title }, - ]); + const breadcrumbs = getBreadcrumbs({ + title: this.props.savedMap.title, + getHasUnsavedChanges: this._hasUnsavedChanges, + originatingApp: this.state.originatingApp, + getAppNameFromId: this.props.stateTransfer.getAppNameFromId, + }); + getCoreChrome().setBreadcrumbs(breadcrumbs); }; _updateFromGlobalState = ({ changes, state: globalState }) => { From 9511285bbd95f9f074dc28077c22505208272cf9 Mon Sep 17 00:00:00 2001 From: Spencer Date: Tue, 25 Aug 2020 13:27:27 -0700 Subject: [PATCH 50/71] [src/dev/build] report file count of archives when building (#75900) Co-authored-by: spalger Co-authored-by: Elastic Machine --- src/dev/build/lib/fs.ts | 22 ++++++- src/dev/build/tasks/create_archives_task.ts | 71 +++++++++++---------- 2 files changed, 56 insertions(+), 37 deletions(-) diff --git a/src/dev/build/lib/fs.ts b/src/dev/build/lib/fs.ts index d86901c41e436..a91113ab2d1c4 100644 --- a/src/dev/build/lib/fs.ts +++ b/src/dev/build/lib/fs.ts @@ -273,7 +273,16 @@ export async function compressTar({ archive.pipe(output); - return archive.directory(source, name).finalize(); + let fileCount = 0; + archive.on('entry', (entry) => { + if (entry.stats?.isFile()) { + fileCount += 1; + } + }); + + await archive.directory(source, name).finalize(); + + return fileCount; } interface CompressZipOptions { @@ -294,5 +303,14 @@ export async function compressZip({ archive.pipe(output); - return archive.directory(source, name).finalize(); + let fileCount = 0; + archive.on('entry', (entry) => { + if (entry.stats?.isFile()) { + fileCount += 1; + } + }); + + await archive.directory(source, name).finalize(); + + return fileCount; } diff --git a/src/dev/build/tasks/create_archives_task.ts b/src/dev/build/tasks/create_archives_task.ts index 3ffb1afef7469..0083881e9f748 100644 --- a/src/dev/build/tasks/create_archives_task.ts +++ b/src/dev/build/tasks/create_archives_task.ts @@ -21,7 +21,7 @@ import Path from 'path'; import Fs from 'fs'; import { promisify } from 'util'; -import { CiStatsReporter } from '@kbn/dev-utils'; +import { CiStatsReporter, CiStatsMetrics } from '@kbn/dev-utils'; import { mkdirp, compressTar, compressZip, Task } from '../lib'; @@ -47,17 +47,16 @@ export const CreateArchives: Task = { archives.push({ format: 'zip', path: destination, - }); - - await compressZip({ - source, - destination, - archiverOptions: { - zlib: { - level: 9, + fileCount: await compressZip({ + source, + destination, + archiverOptions: { + zlib: { + level: 9, + }, }, - }, - createRootDirectory: true, + createRootDirectory: true, + }), }); break; @@ -65,18 +64,17 @@ export const CreateArchives: Task = { archives.push({ format: 'tar', path: destination, - }); - - await compressTar({ - source, - destination, - archiverOptions: { - gzip: true, - gzipOptions: { - level: 9, + fileCount: await compressTar({ + source, + destination, + archiverOptions: { + gzip: true, + gzipOptions: { + level: 9, + }, }, - }, - createRootDirectory: true, + createRootDirectory: true, + }), }); break; @@ -85,19 +83,22 @@ export const CreateArchives: Task = { } } - const reporter = CiStatsReporter.fromEnv(log); - if (reporter.isEnabled()) { - await reporter.metrics( - await Promise.all( - archives.map(async ({ format, path }) => { - return { - group: `${build.isOss() ? 'oss ' : ''}distributable size`, - id: format, - value: (await asyncStat(path)).size, - }; - }) - ) - ); + const metrics: CiStatsMetrics = []; + for (const { format, path, fileCount } of archives) { + metrics.push({ + group: `${build.isOss() ? 'oss ' : ''}distributable size`, + id: format, + value: (await asyncStat(path)).size, + }); + + metrics.push({ + group: `${build.isOss() ? 'oss ' : ''}distributable file count`, + id: 'total', + value: fileCount, + }); } + log.debug('archive metrics:', metrics); + + await CiStatsReporter.fromEnv(log).metrics(metrics); }, }; From 947a93900d05e8837ac26eae706c96254f48c86d Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Tue, 25 Aug 2020 15:02:38 -0600 Subject: [PATCH 51/71] [Maps] fix IVectorLayer.getStyle typing (#75829) * [Maps] fix IVectorLayer.getStyle typing * update typing in VectorLayer type definition * fix unit tests * review feedback --- .../public/classes/layers/vector_layer/vector_layer.d.ts | 2 -- .../classes/styles/vector/properties/__tests__/test_util.ts | 4 ++-- .../styles/vector/properties/dynamic_style_property.tsx | 5 +++-- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.d.ts b/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.d.ts index ad4479d3a324b..fa614ae87b290 100644 --- a/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.d.ts +++ b/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.d.ts @@ -32,7 +32,6 @@ export interface IVectorLayer extends ILayer { getJoins(): IJoin[]; getValidJoins(): IJoin[]; getSource(): IVectorSource; - getStyle(): IVectorStyle; getFeatureById(id: string | number): Feature | null; getPropertiesForTooltip(properties: GeoJsonProperties): Promise; hasJoins(): boolean; @@ -79,7 +78,6 @@ export class VectorLayer extends AbstractLayer implements IVectorLayer { _setMbPointsProperties(mbMap: unknown, mvtSourceLayer?: string): void; _setMbLinePolygonProperties(mbMap: unknown, mvtSourceLayer?: string): void; getSource(): IVectorSource; - getStyle(): IVectorStyle; getFeatureById(id: string | number): Feature | null; getPropertiesForTooltip(properties: GeoJsonProperties): Promise; hasJoins(): boolean; diff --git a/x-pack/plugins/maps/public/classes/styles/vector/properties/__tests__/test_util.ts b/x-pack/plugins/maps/public/classes/styles/vector/properties/__tests__/test_util.ts index 3f6edc81e30ef..a2dfdc94d8058 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/properties/__tests__/test_util.ts +++ b/x-pack/plugins/maps/public/classes/styles/vector/properties/__tests__/test_util.ts @@ -5,7 +5,7 @@ */ // eslint-disable-next-line max-classes-per-file -import { FIELD_ORIGIN } from '../../../../../../common/constants'; +import { FIELD_ORIGIN, LAYER_STYLE_TYPE } from '../../../../../../common/constants'; import { StyleMeta } from '../../style_meta'; import { CategoryFieldMeta, @@ -44,7 +44,7 @@ export class MockStyle implements IStyle { } getType() { - return 'mockStyle'; + return LAYER_STYLE_TYPE.VECTOR; } getStyleMeta(): StyleMeta { diff --git a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_style_property.tsx b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_style_property.tsx index 47659e055936e..826acd41e27a9 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_style_property.tsx +++ b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_style_property.tsx @@ -27,6 +27,7 @@ import { import { IField } from '../../../fields/field'; import { IVectorLayer } from '../../../layers/vector_layer/vector_layer'; import { IJoin } from '../../../joins/join'; +import { IVectorStyle } from '../vector_style'; export interface IDynamicStyleProperty extends IStyleProperty { getFieldMetaOptions(): FieldMetaOptions; @@ -88,7 +89,7 @@ export class DynamicStyleProperty extends AbstractStyleProperty } getRangeFieldMeta() { - const style = this._layer.getStyle(); + const style = this._layer.getStyle() as IVectorStyle; const styleMeta = style.getStyleMeta(); const fieldName = this.getFieldName(); const rangeFieldMetaFromLocalFeatures = styleMeta.getRangeFieldMetaDescriptor(fieldName); @@ -113,7 +114,7 @@ export class DynamicStyleProperty extends AbstractStyleProperty } getCategoryFieldMeta() { - const style = this._layer.getStyle(); + const style = this._layer.getStyle() as IVectorStyle; const styleMeta = style.getStyleMeta(); const fieldName = this.getFieldName(); const categoryFieldMetaFromLocalFeatures = styleMeta.getCategoryFieldMetaDescriptor(fieldName); From fef89334b573b5a4fca89969ff6e2dca9de95a43 Mon Sep 17 00:00:00 2001 From: Scotty Bollinger Date: Tue, 25 Aug 2020 16:43:28 -0500 Subject: [PATCH 52/71] [Enterprise Search] Move views into separate folder from components (#75906) * Move views into separate folder from components * Fix paths in tests * More error_state to views --- .../public/applications/workplace_search/index.test.tsx | 4 ++-- .../public/applications/workplace_search/index.tsx | 8 ++++---- .../error_state/error_state.test.tsx | 0 .../{components => views}/error_state/error_state.tsx | 2 +- .../{components => views}/error_state/index.ts | 0 .../{components => views}/overview/__mocks__/index.ts | 0 .../overview/__mocks__/overview_logic.mock.ts | 0 .../{components => views}/overview/index.ts | 0 .../overview/onboarding_card.test.tsx | 0 .../{components => views}/overview/onboarding_card.tsx | 0 .../overview/onboarding_steps.test.tsx | 0 .../{components => views}/overview/onboarding_steps.tsx | 4 ++-- .../overview/organization_stats.test.tsx | 0 .../{components => views}/overview/organization_stats.tsx | 2 +- .../{components => views}/overview/overview.test.tsx | 4 ++-- .../{components => views}/overview/overview.tsx | 6 +++--- .../{components => views}/overview/overview_logic.test.ts | 0 .../{components => views}/overview/overview_logic.ts | 0 .../{components => views}/overview/recent_activity.scss | 0 .../overview/recent_activity.test.tsx | 0 .../{components => views}/overview/recent_activity.tsx | 2 +- .../overview/statistic_card.test.tsx | 0 .../{components => views}/overview/statistic_card.tsx | 0 .../{components => views}/setup_guide/index.ts | 0 .../setup_guide/setup_guide.test.tsx | 0 .../{components => views}/setup_guide/setup_guide.tsx | 0 26 files changed, 16 insertions(+), 16 deletions(-) rename x-pack/plugins/enterprise_search/public/applications/workplace_search/{components => views}/error_state/error_state.test.tsx (100%) rename x-pack/plugins/enterprise_search/public/applications/workplace_search/{components => views}/error_state/error_state.tsx (93%) rename x-pack/plugins/enterprise_search/public/applications/workplace_search/{components => views}/error_state/index.ts (100%) rename x-pack/plugins/enterprise_search/public/applications/workplace_search/{components => views}/overview/__mocks__/index.ts (100%) rename x-pack/plugins/enterprise_search/public/applications/workplace_search/{components => views}/overview/__mocks__/overview_logic.mock.ts (100%) rename x-pack/plugins/enterprise_search/public/applications/workplace_search/{components => views}/overview/index.ts (100%) rename x-pack/plugins/enterprise_search/public/applications/workplace_search/{components => views}/overview/onboarding_card.test.tsx (100%) rename x-pack/plugins/enterprise_search/public/applications/workplace_search/{components => views}/overview/onboarding_card.tsx (100%) rename x-pack/plugins/enterprise_search/public/applications/workplace_search/{components => views}/overview/onboarding_steps.test.tsx (100%) rename x-pack/plugins/enterprise_search/public/applications/workplace_search/{components => views}/overview/onboarding_steps.tsx (97%) rename x-pack/plugins/enterprise_search/public/applications/workplace_search/{components => views}/overview/organization_stats.test.tsx (100%) rename x-pack/plugins/enterprise_search/public/applications/workplace_search/{components => views}/overview/organization_stats.tsx (97%) rename x-pack/plugins/enterprise_search/public/applications/workplace_search/{components => views}/overview/overview.test.tsx (93%) rename x-pack/plugins/enterprise_search/public/applications/workplace_search/{components => views}/overview/overview.tsx (93%) rename x-pack/plugins/enterprise_search/public/applications/workplace_search/{components => views}/overview/overview_logic.test.ts (100%) rename x-pack/plugins/enterprise_search/public/applications/workplace_search/{components => views}/overview/overview_logic.ts (100%) rename x-pack/plugins/enterprise_search/public/applications/workplace_search/{components => views}/overview/recent_activity.scss (100%) rename x-pack/plugins/enterprise_search/public/applications/workplace_search/{components => views}/overview/recent_activity.test.tsx (100%) rename x-pack/plugins/enterprise_search/public/applications/workplace_search/{components => views}/overview/recent_activity.tsx (98%) rename x-pack/plugins/enterprise_search/public/applications/workplace_search/{components => views}/overview/statistic_card.test.tsx (100%) rename x-pack/plugins/enterprise_search/public/applications/workplace_search/{components => views}/overview/statistic_card.tsx (100%) rename x-pack/plugins/enterprise_search/public/applications/workplace_search/{components => views}/setup_guide/index.ts (100%) rename x-pack/plugins/enterprise_search/public/applications/workplace_search/{components => views}/setup_guide/setup_guide.test.tsx (100%) rename x-pack/plugins/enterprise_search/public/applications/workplace_search/{components => views}/setup_guide/setup_guide.tsx (100%) diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.test.tsx index 654f4dce0ebf3..a0d9352ee9f82 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.test.tsx @@ -12,8 +12,8 @@ import { Redirect } from 'react-router-dom'; import { shallow } from 'enzyme'; import { useValues } from 'kea'; -import { Overview } from './components/overview'; -import { ErrorState } from './components/error_state'; +import { Overview } from './views/overview'; +import { ErrorState } from './views/error_state'; import { WorkplaceSearch } from './'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx index b261c83e30dde..8582a003c6fa8 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx @@ -16,11 +16,11 @@ import { WorkplaceSearchNav } from './components/layout/nav'; import { SETUP_GUIDE_PATH } from './routes'; -import { SetupGuide } from './components/setup_guide'; -import { ErrorState } from './components/error_state'; -import { Overview } from './components/overview'; +import { SetupGuide } from './views/setup_guide'; +import { ErrorState } from './views/error_state'; +import { Overview } from './views/overview'; -export const WorkplaceSearch: React.FC = (props) => { +export const WorkplaceSearch: React.FC = () => { const { config } = useContext(KibanaContext) as IKibanaContext; const { errorConnecting } = useValues(HttpLogic) as IHttpLogicValues; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/error_state/error_state.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/error_state/error_state.test.tsx similarity index 100% rename from x-pack/plugins/enterprise_search/public/applications/workplace_search/components/error_state/error_state.test.tsx rename to x-pack/plugins/enterprise_search/public/applications/workplace_search/views/error_state/error_state.test.tsx diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/error_state/error_state.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/error_state/error_state.tsx similarity index 93% rename from x-pack/plugins/enterprise_search/public/applications/workplace_search/components/error_state/error_state.tsx rename to x-pack/plugins/enterprise_search/public/applications/workplace_search/views/error_state/error_state.tsx index 53f3a7a274429..9ad649c292fb7 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/error_state/error_state.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/error_state/error_state.tsx @@ -13,7 +13,7 @@ import { WORKPLACE_SEARCH_PLUGIN } from '../../../../../common/constants'; import { ErrorStatePrompt } from '../../../shared/error_state'; import { SetWorkplaceSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome'; import { SendWorkplaceSearchTelemetry as SendTelemetry } from '../../../shared/telemetry'; -import { ViewContentHeader } from '../shared/view_content_header'; +import { ViewContentHeader } from '../../components/shared/view_content_header'; export const ErrorState: React.FC = () => { return ( diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/error_state/index.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/error_state/index.ts similarity index 100% rename from x-pack/plugins/enterprise_search/public/applications/workplace_search/components/error_state/index.ts rename to x-pack/plugins/enterprise_search/public/applications/workplace_search/views/error_state/index.ts diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/__mocks__/index.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/__mocks__/index.ts similarity index 100% rename from x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/__mocks__/index.ts rename to x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/__mocks__/index.ts diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/__mocks__/overview_logic.mock.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/__mocks__/overview_logic.mock.ts similarity index 100% rename from x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/__mocks__/overview_logic.mock.ts rename to x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/__mocks__/overview_logic.mock.ts diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/index.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/index.ts similarity index 100% rename from x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/index.ts rename to x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/index.ts diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/onboarding_card.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/onboarding_card.test.tsx similarity index 100% rename from x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/onboarding_card.test.tsx rename to x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/onboarding_card.test.tsx diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/onboarding_card.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/onboarding_card.tsx similarity index 100% rename from x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/onboarding_card.tsx rename to x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/onboarding_card.tsx diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/onboarding_steps.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/onboarding_steps.test.tsx similarity index 100% rename from x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/onboarding_steps.test.tsx rename to x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/onboarding_steps.test.tsx diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/onboarding_steps.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/onboarding_steps.tsx similarity index 97% rename from x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/onboarding_steps.tsx rename to x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/onboarding_steps.tsx index d0f5893bdb88a..fa4decccb34b1 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/onboarding_steps.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/onboarding_steps.tsx @@ -21,12 +21,12 @@ import { EuiButtonEmptyProps, EuiLinkProps, } from '@elastic/eui'; -import sharedSourcesIcon from '../shared/assets/share_circle.svg'; +import sharedSourcesIcon from '../../components/shared/assets/share_circle.svg'; import { sendTelemetry } from '../../../shared/telemetry'; import { KibanaContext, IKibanaContext } from '../../../index'; import { ORG_SOURCES_PATH, USERS_PATH, ORG_SETTINGS_PATH } from '../../routes'; -import { ContentSection } from '../shared/content_section'; +import { ContentSection } from '../../components/shared/content_section'; import { OverviewLogic, IOverviewValues } from './overview_logic'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/organization_stats.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/organization_stats.test.tsx similarity index 100% rename from x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/organization_stats.test.tsx rename to x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/organization_stats.test.tsx diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/organization_stats.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/organization_stats.tsx similarity index 97% rename from x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/organization_stats.tsx rename to x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/organization_stats.tsx index 4c5efce9baf12..53549cfcdbce7 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/organization_stats.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/organization_stats.tsx @@ -11,7 +11,7 @@ import { useValues } from 'kea'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; -import { ContentSection } from '../shared/content_section'; +import { ContentSection } from '../../components/shared/content_section'; import { ORG_SOURCES_PATH, USERS_PATH } from '../../routes'; import { OverviewLogic, IOverviewValues } from './overview_logic'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/overview.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/overview.test.tsx similarity index 93% rename from x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/overview.test.tsx rename to x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/overview.test.tsx index fee966a56923d..e4531ff03587b 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/overview.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/overview.test.tsx @@ -11,8 +11,8 @@ import { mockLogicActions, setMockValues } from './__mocks__'; import React from 'react'; import { shallow, mount } from 'enzyme'; -import { Loading } from '../shared/loading'; -import { ViewContentHeader } from '../shared/view_content_header'; +import { Loading } from '../../components/shared/loading'; +import { ViewContentHeader } from '../../components/shared/view_content_header'; import { OnboardingSteps } from './onboarding_steps'; import { OrganizationStats } from './organization_stats'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/overview.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/overview.tsx similarity index 93% rename from x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/overview.tsx rename to x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/overview.tsx index 6aa3e1e608bfe..134fc9389694d 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/overview.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/overview.tsx @@ -16,9 +16,9 @@ import { SendWorkplaceSearchTelemetry as SendTelemetry } from '../../../shared/t import { OverviewLogic, IOverviewActions, IOverviewValues } from './overview_logic'; -import { Loading } from '../shared/loading'; -import { ProductButton } from '../shared/product_button'; -import { ViewContentHeader } from '../shared/view_content_header'; +import { Loading } from '../../components/shared/loading'; +import { ProductButton } from '../../components/shared/product_button'; +import { ViewContentHeader } from '../../components/shared/view_content_header'; import { OnboardingSteps } from './onboarding_steps'; import { OrganizationStats } from './organization_stats'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/overview_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/overview_logic.test.ts similarity index 100% rename from x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/overview_logic.test.ts rename to x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/overview_logic.test.ts diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/overview_logic.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/overview_logic.ts similarity index 100% rename from x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/overview_logic.ts rename to x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/overview_logic.ts diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/recent_activity.scss b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/recent_activity.scss similarity index 100% rename from x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/recent_activity.scss rename to x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/recent_activity.scss diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/recent_activity.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/recent_activity.test.tsx similarity index 100% rename from x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/recent_activity.test.tsx rename to x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/recent_activity.test.tsx diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/recent_activity.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/recent_activity.tsx similarity index 98% rename from x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/recent_activity.tsx rename to x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/recent_activity.tsx index 0f4f6c65d083c..ada89c33be7e2 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/recent_activity.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/recent_activity.tsx @@ -12,7 +12,7 @@ import { useValues } from 'kea'; import { EuiEmptyPrompt, EuiLink, EuiPanel, EuiSpacer, EuiLinkProps } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { ContentSection } from '../shared/content_section'; +import { ContentSection } from '../../components/shared/content_section'; import { sendTelemetry } from '../../../shared/telemetry'; import { KibanaContext, IKibanaContext } from '../../../index'; import { getSourcePath } from '../../routes'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/statistic_card.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/statistic_card.test.tsx similarity index 100% rename from x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/statistic_card.test.tsx rename to x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/statistic_card.test.tsx diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/statistic_card.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/statistic_card.tsx similarity index 100% rename from x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/statistic_card.tsx rename to x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/statistic_card.tsx diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/setup_guide/index.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/setup_guide/index.ts similarity index 100% rename from x-pack/plugins/enterprise_search/public/applications/workplace_search/components/setup_guide/index.ts rename to x-pack/plugins/enterprise_search/public/applications/workplace_search/views/setup_guide/index.ts diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/setup_guide/setup_guide.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/setup_guide/setup_guide.test.tsx similarity index 100% rename from x-pack/plugins/enterprise_search/public/applications/workplace_search/components/setup_guide/setup_guide.test.tsx rename to x-pack/plugins/enterprise_search/public/applications/workplace_search/views/setup_guide/setup_guide.test.tsx diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/setup_guide/setup_guide.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/setup_guide/setup_guide.tsx similarity index 100% rename from x-pack/plugins/enterprise_search/public/applications/workplace_search/components/setup_guide/setup_guide.tsx rename to x-pack/plugins/enterprise_search/public/applications/workplace_search/views/setup_guide/setup_guide.tsx From 1fee8f16ef8c6399bfa9d00a7c59cdb92be12361 Mon Sep 17 00:00:00 2001 From: Marta Bondyra Date: Wed, 26 Aug 2020 00:00:24 +0200 Subject: [PATCH 53/71] [Lens] fix dimension popover design on mobile (#75866) --- .../indexpattern_datasource/dimension_panel/field_select.tsx | 2 +- .../indexpattern_datasource/dimension_panel/popover_editor.scss | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/field_select.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/field_select.tsx index b2a59788b50f9..e4dfa69813743 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/field_select.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/field_select.tsx @@ -181,7 +181,7 @@ export function FieldSelect({ }} renderOption={(option, searchValue) => { return ( - + Date: Tue, 25 Aug 2020 18:13:41 -0400 Subject: [PATCH 54/71] [Security Solution][Detections] Disables add exception for ML and threshold rules (#75802) --- .../components/alerts_table/default_config.tsx | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/default_config.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/default_config.tsx index 5bab2e3c78970..ca17d331c67e5 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/default_config.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/default_config.tsx @@ -9,6 +9,8 @@ import ApolloClient from 'apollo-client'; import { Dispatch } from 'redux'; import { EuiText } from '@elastic/eui'; +import { RuleType } from '../../../../common/detection_engine/types'; +import { isMlRule } from '../../../../common/machine_learning/helpers'; import { RowRendererId } from '../../../../common/types/timeline'; import { DEFAULT_INDEX_PATTERN } from '../../../../common/constants'; import { Status } from '../../../../common/detection_engine/schemas/common/schemas'; @@ -39,6 +41,7 @@ import { import { Ecs, TimelineNonEcsData } from '../../../graphql/types'; import { AddExceptionModalBaseProps } from '../../../common/components/exceptions/add_exception_modal'; import { getMappedNonEcsValue } from '../../../common/components/exceptions/helpers'; +import { isThresholdRule } from '../../../../common/detection_engine/utils'; export const buildAlertStatusFilter = (status: Status): Filter[] => [ { @@ -193,6 +196,7 @@ export const requiredFieldsForActions = [ 'signal.rule.query', 'signal.rule.to', 'signal.rule.id', + 'signal.rule.type', 'signal.original_event.kind', 'signal.original_event.module', @@ -317,6 +321,15 @@ export const getAlertActions = ({ return module === 'endpoint' && kind === 'alert'; }; + const exceptionsAreAllowed = () => { + const ruleTypes = getMappedNonEcsValue({ + data: nonEcsRowData, + fieldName: 'signal.rule.type', + }); + const [ruleType] = ruleTypes as RuleType[]; + return !isMlRule(ruleType) && !isThresholdRule(ruleType); + }; + return [ { ...getInvestigateInResolverAction({ dispatch, timelineId }), @@ -386,7 +399,7 @@ export const getAlertActions = ({ } }, id: 'addException', - isActionDisabled: () => !canUserCRUD || !hasIndexWrite, + isActionDisabled: () => !canUserCRUD || !hasIndexWrite || !exceptionsAreAllowed(), dataTestSubj: 'add-exception-menu-item', ariaLabel: 'Add Exception', content: {i18n.ACTION_ADD_EXCEPTION}, From ba9a60738425a2948f6e074408a0e5d22d07c721 Mon Sep 17 00:00:00 2001 From: Frank Hassanabad Date: Tue, 25 Aug 2020 19:48:18 -0600 Subject: [PATCH 55/71] Optimizes the index queries to not block the NodeJS event loop (#75716) ## Summary Before this PR you can see event loop block times of: ```ts formatIndexFields: 7986.884ms ``` After this PR you will see event loop block times of: ```ts formatIndexFields: 85.012ms ``` within the file: ```ts x-pack/plugins/security_solution/server/lib/index_fields/elasticsearch_adapter.ts ``` For the GraphQL query of `SourceQuery`/`IndexFields` This also fixes the issue of `unknown` being returned to the front end by removing code that is no longer functioning as it was intended. Ensure during testing of this PR that blank/default and non exist indexes within `securitySolution:defaultIndex` still work as expected. Before, notice the `unknown` instead of the `filebeat-*`: Screen Shot 2020-08-20 at 4 55 52 PM After: Screen Shot 2020-08-20 at 4 56 03 PM An explanation of how to see the block times for before and after --- For perf testing you first add timed testing to the file: ```ts x-pack/plugins/security_solution/server/lib/index_fields/elasticsearch_adapter.ts ``` Before this PR, around lines 42: ```ts console.time('formatIndexFields'); // <--- start timer const fields = formatIndexFields( responsesIndexFields, Object.keys(indexesAliasIndices) as IndexAlias[] ); console.timeEnd('formatIndexFields'); // <--- outputs the end timer return fields; ``` After this PR, around lines 42: ```ts console.time('formatIndexFields'); // <--- start timer const fields = await formatIndexFields(responsesIndexFields, indices); console.timeEnd('formatIndexFields'); // <--- outputs the end timer return fields; ``` And then reload the security solutions application web page here: ``` http://localhost:5601/app/security/timelines/default ``` Be sure to load it _twice_ for testing as NodeJS will sometimes report better numbers the second time as it does optimizations after the first time it encounters some code paths. You will begin to see numbers similar to this before this PR: ```ts formatIndexFields: 2553.279ms ``` This indicates that it is blocking the event loop for ~2.5 seconds befofe this fix. If you add additional indexes to your `securitySolution:defaultIndex` indexes that have additional fields then this amount will increase exponentially. For developers using our test servers I created two other indexes called delme-1 and delme-2 with additional mappings you can add like below ```ts apm-*-transaction*, auditbeat-*, endgame-*, filebeat-*, logs-*, packetbeat-*, winlogbeat-*, delme-1, delme-2 ``` Screen Shot 2020-08-21 at 8 21 50 PM Then you are going to see times approaching 8 seconds of blocking the event loop like so: ```ts formatIndexFields: 7986.884ms ``` After this fix on the first pass unoptimized it will report ```ts formatIndexFields: 373.082ms ``` Then after it optimizes the code paths on a second page load it will report ```ts formatIndexFields: 84.304ms ``` ### Checklist Delete any items that are not applicable to this PR. - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios --- .../elasticsearch_adapter.test.ts | 564 +++++++++++++++++- .../lib/index_fields/elasticsearch_adapter.ts | 203 ++++--- .../server/utils/beat_schema/index.test.ts | 22 +- .../server/utils/beat_schema/index.ts | 15 - .../server/utils/beat_schema/type.ts | 2 - 5 files changed, 692 insertions(+), 114 deletions(-) diff --git a/x-pack/plugins/security_solution/server/lib/index_fields/elasticsearch_adapter.test.ts b/x-pack/plugins/security_solution/server/lib/index_fields/elasticsearch_adapter.test.ts index 20bc1387a3c4e..e8883111c95f6 100644 --- a/x-pack/plugins/security_solution/server/lib/index_fields/elasticsearch_adapter.test.ts +++ b/x-pack/plugins/security_solution/server/lib/index_fields/elasticsearch_adapter.test.ts @@ -6,16 +6,21 @@ import { sortBy } from 'lodash/fp'; -import { formatIndexFields } from './elasticsearch_adapter'; +import { + formatIndexFields, + formatFirstFields, + formatSecondFields, + createFieldItem, +} from './elasticsearch_adapter'; import { mockAuditbeatIndexField, mockFilebeatIndexField, mockPacketbeatIndexField } from './mock'; describe('Index Fields', () => { describe('formatIndexFields', () => { - test('Test Basic functionality', async () => { + test('Basic functionality', async () => { expect( sortBy( 'name', - formatIndexFields( + await formatIndexFields( [mockAuditbeatIndexField, mockFilebeatIndexField, mockPacketbeatIndexField], ['auditbeat', 'filebeat', 'packetbeat'] ) @@ -130,4 +135,557 @@ describe('Index Fields', () => { ); }); }); + + describe('formatFirstFields', () => { + test('Basic functionality', async () => { + const fields = await formatFirstFields( + [mockAuditbeatIndexField, mockFilebeatIndexField, mockPacketbeatIndexField], + ['auditbeat', 'filebeat', 'packetbeat'] + ); + expect(fields).toEqual([ + { + description: 'Each document has an _id that uniquely identifies it', + example: 'Y-6TfmcB0WOhS6qyMv3s', + footnote: '', + group: 1, + level: 'core', + name: '_id', + required: true, + type: 'string', + searchable: true, + aggregatable: false, + readFromDocValues: true, + category: '_id', + indexes: ['auditbeat'], + }, + { + description: + 'An index is like a ‘database’ in a relational database. It has a mapping which defines multiple types. An index is a logical namespace which maps to one or more primary shards and can have zero or more replica shards.', + example: 'auditbeat-8.0.0-2019.02.19-000001', + footnote: '', + group: 1, + level: 'core', + name: '_index', + required: true, + type: 'string', + searchable: true, + aggregatable: true, + readFromDocValues: true, + category: '_index', + indexes: ['auditbeat'], + }, + { + description: + 'Date/time when the event originated.\n\nThis is the date/time extracted from the event, typically representing when\nthe event was generated by the source.\n\nIf the event source has no original timestamp, this value is typically populated\nby the first time the event was received by the pipeline.\n\nRequired field for all events.', + example: '2016-05-23T08:05:34.853Z', + name: '@timestamp', + type: 'date', + searchable: true, + aggregatable: true, + category: 'base', + indexes: ['auditbeat'], + }, + { + description: + 'Ephemeral identifier of this agent (if one exists).\n\nThis id normally changes across restarts, but `agent.id` does not.', + example: '8a4f500f', + name: 'agent.ephemeral_id', + type: 'string', + searchable: true, + aggregatable: true, + category: 'agent', + indexes: ['auditbeat'], + }, + { + description: + 'Custom name of the agent.\n\nThis is a name that can be given to an agent. This can be helpful if for example\ntwo Filebeat instances are running on the same host but a human readable separation\nis needed on which Filebeat instance data is coming from.\n\nIf no name is given, the name is often left empty.', + example: 'foo', + name: 'agent.name', + type: 'string', + searchable: true, + aggregatable: true, + category: 'agent', + indexes: ['auditbeat'], + }, + { + description: + 'Type of the agent.\n\nThe agent type stays always the same and should be given by the agent used.\nIn case of Filebeat the agent would always be Filebeat also if two Filebeat\ninstances are run on the same machine.', + example: 'filebeat', + name: 'agent.type', + type: 'string', + searchable: true, + aggregatable: true, + category: 'agent', + indexes: ['auditbeat'], + }, + { + description: 'Version of the agent.', + example: '6.0.0-rc2', + name: 'agent.version', + type: 'string', + searchable: true, + aggregatable: true, + category: 'agent', + indexes: ['auditbeat'], + }, + { + description: 'Each document has an _id that uniquely identifies it', + example: 'Y-6TfmcB0WOhS6qyMv3s', + footnote: '', + group: 1, + level: 'core', + name: '_id', + required: true, + type: 'string', + searchable: true, + aggregatable: false, + readFromDocValues: true, + category: '_id', + indexes: ['filebeat'], + }, + { + description: + 'An index is like a ‘database’ in a relational database. It has a mapping which defines multiple types. An index is a logical namespace which maps to one or more primary shards and can have zero or more replica shards.', + example: 'auditbeat-8.0.0-2019.02.19-000001', + footnote: '', + group: 1, + level: 'core', + name: '_index', + required: true, + type: 'string', + searchable: true, + aggregatable: true, + readFromDocValues: true, + category: '_index', + indexes: ['filebeat'], + }, + { + description: + 'Date/time when the event originated.\n\nThis is the date/time extracted from the event, typically representing when\nthe event was generated by the source.\n\nIf the event source has no original timestamp, this value is typically populated\nby the first time the event was received by the pipeline.\n\nRequired field for all events.', + example: '2016-05-23T08:05:34.853Z', + name: '@timestamp', + type: 'date', + searchable: true, + aggregatable: true, + category: 'base', + indexes: ['filebeat'], + }, + { + name: 'agent.hostname', + searchable: true, + type: 'string', + aggregatable: true, + category: 'agent', + indexes: ['filebeat'], + }, + { + description: + 'Custom name of the agent.\n\nThis is a name that can be given to an agent. This can be helpful if for example\ntwo Filebeat instances are running on the same host but a human readable separation\nis needed on which Filebeat instance data is coming from.\n\nIf no name is given, the name is often left empty.', + example: 'foo', + name: 'agent.name', + type: 'string', + searchable: true, + aggregatable: true, + category: 'agent', + indexes: ['filebeat'], + }, + { + description: 'Version of the agent.', + example: '6.0.0-rc2', + name: 'agent.version', + type: 'string', + searchable: true, + aggregatable: true, + category: 'agent', + indexes: ['filebeat'], + }, + { + description: 'Each document has an _id that uniquely identifies it', + example: 'Y-6TfmcB0WOhS6qyMv3s', + footnote: '', + group: 1, + level: 'core', + name: '_id', + required: true, + type: 'string', + searchable: true, + aggregatable: false, + readFromDocValues: true, + category: '_id', + indexes: ['packetbeat'], + }, + { + description: + 'An index is like a ‘database’ in a relational database. It has a mapping which defines multiple types. An index is a logical namespace which maps to one or more primary shards and can have zero or more replica shards.', + example: 'auditbeat-8.0.0-2019.02.19-000001', + footnote: '', + group: 1, + level: 'core', + name: '_index', + required: true, + type: 'string', + searchable: true, + aggregatable: true, + readFromDocValues: true, + category: '_index', + indexes: ['packetbeat'], + }, + { + description: + 'Date/time when the event originated.\n\nThis is the date/time extracted from the event, typically representing when\nthe event was generated by the source.\n\nIf the event source has no original timestamp, this value is typically populated\nby the first time the event was received by the pipeline.\n\nRequired field for all events.', + example: '2016-05-23T08:05:34.853Z', + name: '@timestamp', + type: 'date', + searchable: true, + aggregatable: true, + category: 'base', + indexes: ['packetbeat'], + }, + { + description: + 'Unique identifier of this agent (if one exists).\n\nExample: For Beats this would be beat.id.', + example: '8a4f500d', + name: 'agent.id', + type: 'string', + searchable: true, + aggregatable: true, + category: 'agent', + indexes: ['packetbeat'], + }, + { + description: + 'Type of the agent.\n\nThe agent type stays always the same and should be given by the agent used.\nIn case of Filebeat the agent would always be Filebeat also if two Filebeat\ninstances are run on the same machine.', + example: 'filebeat', + name: 'agent.type', + type: 'string', + searchable: true, + aggregatable: true, + category: 'agent', + indexes: ['packetbeat'], + }, + ]); + }); + }); + + describe('formatSecondFields', () => { + test('Basic functionality', async () => { + const fields = await formatSecondFields([ + { + description: 'Each document has an _id that uniquely identifies it', + example: 'Y-6TfmcB0WOhS6qyMv3s', + name: '_id', + type: 'string', + searchable: true, + aggregatable: false, + category: '_id', + indexes: ['auditbeat'], + }, + { + description: + 'An index is like a ‘database’ in a relational database. It has a mapping which defines multiple types. An index is a logical namespace which maps to one or more primary shards and can have zero or more replica shards.', + example: 'auditbeat-8.0.0-2019.02.19-000001', + name: '_index', + type: 'string', + searchable: true, + aggregatable: true, + category: '_index', + indexes: ['auditbeat'], + }, + { + description: + 'Date/time when the event originated.\n\nThis is the date/time extracted from the event, typically representing when\nthe event was generated by the source.\n\nIf the event source has no original timestamp, this value is typically populated\nby the first time the event was received by the pipeline.\n\nRequired field for all events.', + example: '2016-05-23T08:05:34.853Z', + name: '@timestamp', + type: 'date', + searchable: true, + aggregatable: true, + category: 'base', + indexes: ['auditbeat'], + }, + { + description: + 'Ephemeral identifier of this agent (if one exists).\n\nThis id normally changes across restarts, but `agent.id` does not.', + example: '8a4f500f', + name: 'agent.ephemeral_id', + type: 'string', + searchable: true, + aggregatable: true, + category: 'agent', + indexes: ['auditbeat'], + }, + { + description: + 'Custom name of the agent.\n\nThis is a name that can be given to an agent. This can be helpful if for example\ntwo Filebeat instances are running on the same host but a human readable separation\nis needed on which Filebeat instance data is coming from.\n\nIf no name is given, the name is often left empty.', + example: 'foo', + name: 'agent.name', + type: 'string', + searchable: true, + aggregatable: true, + category: 'agent', + indexes: ['auditbeat'], + }, + { + description: + 'Type of the agent.\n\nThe agent type stays always the same and should be given by the agent used.\nIn case of Filebeat the agent would always be Filebeat also if two Filebeat\ninstances are run on the same machine.', + example: 'filebeat', + name: 'agent.type', + type: 'string', + searchable: true, + aggregatable: true, + category: 'agent', + indexes: ['auditbeat'], + }, + { + description: 'Version of the agent.', + example: '6.0.0-rc2', + name: 'agent.version', + type: 'string', + searchable: true, + aggregatable: true, + category: 'agent', + indexes: ['auditbeat'], + }, + { + description: 'Each document has an _id that uniquely identifies it', + example: 'Y-6TfmcB0WOhS6qyMv3s', + name: '_id', + type: 'string', + searchable: true, + aggregatable: false, + category: '_id', + indexes: ['filebeat'], + }, + { + description: + 'An index is like a ‘database’ in a relational database. It has a mapping which defines multiple types. An index is a logical namespace which maps to one or more primary shards and can have zero or more replica shards.', + example: 'auditbeat-8.0.0-2019.02.19-000001', + name: '_index', + type: 'string', + searchable: true, + aggregatable: true, + category: '_index', + indexes: ['filebeat'], + }, + { + description: + 'Date/time when the event originated.\n\nThis is the date/time extracted from the event, typically representing when\nthe event was generated by the source.\n\nIf the event source has no original timestamp, this value is typically populated\nby the first time the event was received by the pipeline.\n\nRequired field for all events.', + example: '2016-05-23T08:05:34.853Z', + name: '@timestamp', + type: 'date', + searchable: true, + aggregatable: true, + category: 'base', + indexes: ['filebeat'], + }, + { + name: 'agent.hostname', + searchable: true, + type: 'string', + aggregatable: true, + category: 'agent', + indexes: ['filebeat'], + }, + { + description: + 'Custom name of the agent.\n\nThis is a name that can be given to an agent. This can be helpful if for example\ntwo Filebeat instances are running on the same host but a human readable separation\nis needed on which Filebeat instance data is coming from.\n\nIf no name is given, the name is often left empty.', + example: 'foo', + name: 'agent.name', + type: 'string', + searchable: true, + aggregatable: true, + category: 'agent', + indexes: ['filebeat'], + }, + { + description: 'Version of the agent.', + example: '6.0.0-rc2', + name: 'agent.version', + type: 'string', + searchable: true, + aggregatable: true, + category: 'agent', + indexes: ['filebeat'], + }, + { + description: 'Each document has an _id that uniquely identifies it', + example: 'Y-6TfmcB0WOhS6qyMv3s', + name: '_id', + type: 'string', + searchable: true, + aggregatable: false, + category: '_id', + indexes: ['packetbeat'], + }, + { + description: + 'An index is like a ‘database’ in a relational database. It has a mapping which defines multiple types. An index is a logical namespace which maps to one or more primary shards and can have zero or more replica shards.', + example: 'auditbeat-8.0.0-2019.02.19-000001', + name: '_index', + type: 'string', + searchable: true, + aggregatable: true, + category: '_index', + indexes: ['packetbeat'], + }, + { + description: + 'Date/time when the event originated.\n\nThis is the date/time extracted from the event, typically representing when\nthe event was generated by the source.\n\nIf the event source has no original timestamp, this value is typically populated\nby the first time the event was received by the pipeline.\n\nRequired field for all events.', + example: '2016-05-23T08:05:34.853Z', + name: '@timestamp', + type: 'date', + searchable: true, + aggregatable: true, + category: 'base', + indexes: ['packetbeat'], + }, + { + description: + 'Unique identifier of this agent (if one exists).\n\nExample: For Beats this would be beat.id.', + example: '8a4f500d', + name: 'agent.id', + type: 'string', + searchable: true, + aggregatable: true, + category: 'agent', + indexes: ['packetbeat'], + }, + { + description: + 'Type of the agent.\n\nThe agent type stays always the same and should be given by the agent used.\nIn case of Filebeat the agent would always be Filebeat also if two Filebeat\ninstances are run on the same machine.', + example: 'filebeat', + name: 'agent.type', + type: 'string', + searchable: true, + aggregatable: true, + category: 'agent', + indexes: ['packetbeat'], + }, + ]); + expect(fields).toEqual([ + { + description: 'Each document has an _id that uniquely identifies it', + example: 'Y-6TfmcB0WOhS6qyMv3s', + name: '_id', + type: 'string', + searchable: true, + aggregatable: false, + category: '_id', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + }, + { + description: + 'An index is like a ‘database’ in a relational database. It has a mapping which defines multiple types. An index is a logical namespace which maps to one or more primary shards and can have zero or more replica shards.', + example: 'auditbeat-8.0.0-2019.02.19-000001', + name: '_index', + type: 'string', + searchable: true, + aggregatable: true, + category: '_index', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + }, + { + description: + 'Date/time when the event originated.\n\nThis is the date/time extracted from the event, typically representing when\nthe event was generated by the source.\n\nIf the event source has no original timestamp, this value is typically populated\nby the first time the event was received by the pipeline.\n\nRequired field for all events.', + example: '2016-05-23T08:05:34.853Z', + name: '@timestamp', + type: 'date', + searchable: true, + aggregatable: true, + category: 'base', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + }, + { + description: + 'Ephemeral identifier of this agent (if one exists).\n\nThis id normally changes across restarts, but `agent.id` does not.', + example: '8a4f500f', + name: 'agent.ephemeral_id', + type: 'string', + searchable: true, + aggregatable: true, + category: 'agent', + indexes: ['auditbeat'], + }, + { + description: + 'Custom name of the agent.\n\nThis is a name that can be given to an agent. This can be helpful if for example\ntwo Filebeat instances are running on the same host but a human readable separation\nis needed on which Filebeat instance data is coming from.\n\nIf no name is given, the name is often left empty.', + example: 'foo', + name: 'agent.name', + type: 'string', + searchable: true, + aggregatable: true, + category: 'agent', + indexes: ['auditbeat', 'filebeat'], + }, + { + description: + 'Type of the agent.\n\nThe agent type stays always the same and should be given by the agent used.\nIn case of Filebeat the agent would always be Filebeat also if two Filebeat\ninstances are run on the same machine.', + example: 'filebeat', + name: 'agent.type', + type: 'string', + searchable: true, + aggregatable: true, + category: 'agent', + indexes: ['auditbeat', 'packetbeat'], + }, + { + description: 'Version of the agent.', + example: '6.0.0-rc2', + name: 'agent.version', + type: 'string', + searchable: true, + aggregatable: true, + category: 'agent', + indexes: ['auditbeat', 'filebeat'], + }, + { + name: 'agent.hostname', + searchable: true, + type: 'string', + aggregatable: true, + category: 'agent', + indexes: ['filebeat'], + }, + { + description: + 'Unique identifier of this agent (if one exists).\n\nExample: For Beats this would be beat.id.', + example: '8a4f500d', + name: 'agent.id', + type: 'string', + searchable: true, + aggregatable: true, + category: 'agent', + indexes: ['packetbeat'], + }, + ]); + }); + }); + + describe('createFieldItem', () => { + test('Basic functionality', () => { + const item = createFieldItem( + ['auditbeat'], + { + name: '_id', + type: 'string', + searchable: true, + aggregatable: false, + }, + 0 + ); + expect(item).toEqual({ + description: 'Each document has an _id that uniquely identifies it', + example: 'Y-6TfmcB0WOhS6qyMv3s', + footnote: '', + group: 1, + level: 'core', + name: '_id', + required: true, + type: 'string', + searchable: true, + aggregatable: false, + category: '_id', + indexes: ['auditbeat'], + }); + }); + }); }); diff --git a/x-pack/plugins/security_solution/server/lib/index_fields/elasticsearch_adapter.ts b/x-pack/plugins/security_solution/server/lib/index_fields/elasticsearch_adapter.ts index bb0a4b9e2ba9b..777b1cf3bb80d 100644 --- a/x-pack/plugins/security_solution/server/lib/index_fields/elasticsearch_adapter.ts +++ b/x-pack/plugins/security_solution/server/lib/index_fields/elasticsearch_adapter.ts @@ -4,45 +4,25 @@ * you may not use this file except in compliance with the Elastic License. */ -import { isEmpty, get } from 'lodash/fp'; +import { isEmpty } from 'lodash/fp'; import { IndexField } from '../../graphql/types'; -import { - baseCategoryFields, - getDocumentation, - getIndexAlias, - hasDocumentation, - IndexAlias, -} from '../../utils/beat_schema'; +import { baseCategoryFields, getDocumentation, hasDocumentation } from '../../utils/beat_schema'; import { FrameworkAdapter, FrameworkRequest } from '../framework'; import { FieldsAdapter, IndexFieldDescriptor } from './types'; export class ElasticsearchIndexFieldAdapter implements FieldsAdapter { constructor(private readonly framework: FrameworkAdapter) {} - public async getIndexFields(request: FrameworkRequest, indices: string[]): Promise { const indexPatternsService = this.framework.getIndexPatternsService(request); - const indexesAliasIndices = indices.reduce>((accumulator, indice) => { - const key = getIndexAlias(indices, indice); - - if (get(key, accumulator)) { - accumulator[key] = [...accumulator[key], indice]; - } else { - accumulator[key] = [indice]; - } - return accumulator; - }, {}); - const responsesIndexFields: IndexFieldDescriptor[][] = await Promise.all( - Object.values(indexesAliasIndices).map((indicesByGroup) => - indexPatternsService.getFieldsForWildcard({ - pattern: indicesByGroup, - }) - ) - ); - return formatIndexFields( - responsesIndexFields, - Object.keys(indexesAliasIndices) as IndexAlias[] + const responsesIndexFields = await Promise.all( + indices.map((index) => { + return indexPatternsService.getFieldsForWildcard({ + pattern: index, + }); + }) ); + return formatIndexFields(responsesIndexFields, indices); } } @@ -63,51 +43,128 @@ const missingFields = [ }, ]; -export const formatIndexFields = ( +/** + * Creates a single field item. + * + * This is a mutatious HOT CODE PATH function that will have array sizes up to 4.7 megs + * in size at a time calling this function repeatedly. This function should be as optimized as possible + * and should avoid any and all creation of new arrays, iterating over the arrays or performing + * any n^2 operations. + * @param indexesAlias The index alias + * @param index The index its self + * @param indexesAliasIdx The index within the alias + */ +export const createFieldItem = ( + indexesAlias: string[], + index: IndexFieldDescriptor, + indexesAliasIdx: number +): IndexField => { + const alias = indexesAlias[indexesAliasIdx]; + const splitName = index.name.split('.'); + const category = baseCategoryFields.includes(splitName[0]) ? 'base' : splitName[0]; + return { + ...(hasDocumentation(alias, index.name) ? getDocumentation(alias, index.name) : {}), + ...index, + category, + indexes: [alias], + }; +}; + +/** + * This is a mutatious HOT CODE PATH function that will have array sizes up to 4.7 megs + * in size at a time when being called. This function should be as optimized as possible + * and should avoid any and all creation of new arrays, iterating over the arrays or performing + * any n^2 operations. The `.push`, and `forEach` operations are expected within this function + * to speed up performance. + * + * This intentionally waits for the next tick on the event loop to process as the large 4.7 megs + * has already consumed a lot of the event loop processing up to this function and we want to give + * I/O opportunity to occur by scheduling this on the next loop. + * @param responsesIndexFields The response index fields to loop over + * @param indexesAlias The index aliases such as filebeat-* + */ +export const formatFirstFields = async ( responsesIndexFields: IndexFieldDescriptor[][], - indexesAlias: IndexAlias[] -): IndexField[] => - responsesIndexFields - .reduce( - (accumulator: IndexField[], indexFields: IndexFieldDescriptor[], indexesAliasIdx: number) => [ - ...accumulator, - ...[...missingFields, ...indexFields].reduce( - (itemAccumulator: IndexField[], index: IndexFieldDescriptor) => { - const alias: IndexAlias = indexesAlias[indexesAliasIdx]; - const splitName = index.name.split('.'); - const category = baseCategoryFields.includes(splitName[0]) ? 'base' : splitName[0]; - return [ - ...itemAccumulator, - { - ...(hasDocumentation(alias, index.name) ? getDocumentation(alias, index.name) : {}), - ...index, - category, - indexes: [alias], - } as IndexField, - ]; + indexesAlias: string[] +): Promise => { + return new Promise((resolve) => { + setTimeout(() => { + resolve( + responsesIndexFields.reduce( + ( + accumulator: IndexField[], + indexFields: IndexFieldDescriptor[], + indexesAliasIdx: number + ) => { + missingFields.forEach((index) => { + const item = createFieldItem(indexesAlias, index, indexesAliasIdx); + accumulator.push(item); + }); + indexFields.forEach((index) => { + const item = createFieldItem(indexesAlias, index, indexesAliasIdx); + accumulator.push(item); + }); + return accumulator; }, [] - ), - ], - [] - ) - .reduce((accumulator: IndexField[], indexfield: IndexField) => { - const alreadyExistingIndexField = accumulator.findIndex( - (acc) => acc.name === indexfield.name + ) ); - if (alreadyExistingIndexField > -1) { - const existingIndexField = accumulator[alreadyExistingIndexField]; - return [ - ...accumulator.slice(0, alreadyExistingIndexField), - { - ...existingIndexField, - description: isEmpty(existingIndexField.description) - ? indexfield.description - : existingIndexField.description, - indexes: Array.from(new Set([...existingIndexField.indexes, ...indexfield.indexes])), - }, - ...accumulator.slice(alreadyExistingIndexField + 1), - ]; - } - return [...accumulator, indexfield]; - }, []); + }); + }); +}; + +/** + * This is a mutatious HOT CODE PATH function that will have array sizes up to 4.7 megs + * in size at a time when being called. This function should be as optimized as possible + * and should avoid any and all creation of new arrays, iterating over the arrays or performing + * any n^2 operations. The `.push`, and `forEach` operations are expected within this function + * to speed up performance. The "indexFieldNameHash" side effect hash avoids additional expensive n^2 + * look ups. + * + * This intentionally waits for the next tick on the event loop to process as the large 4.7 megs + * has already consumed a lot of the event loop processing up to this function and we want to give + * I/O opportunity to occur by scheduling this on the next loop. + * @param fields The index fields to create the secondary fields for + */ +export const formatSecondFields = async (fields: IndexField[]): Promise => { + return new Promise((resolve) => { + setTimeout(() => { + const indexFieldNameHash: Record = {}; + const reduced = fields.reduce((accumulator: IndexField[], indexfield: IndexField) => { + const alreadyExistingIndexField = indexFieldNameHash[indexfield.name]; + if (alreadyExistingIndexField != null) { + const existingIndexField = accumulator[alreadyExistingIndexField]; + if (isEmpty(accumulator[alreadyExistingIndexField].description)) { + accumulator[alreadyExistingIndexField].description = indexfield.description; + } + accumulator[alreadyExistingIndexField].indexes = Array.from( + new Set([...existingIndexField.indexes, ...indexfield.indexes]) + ); + return accumulator; + } + accumulator.push(indexfield); + indexFieldNameHash[indexfield.name] = accumulator.length - 1; + return accumulator; + }, []); + resolve(reduced); + }); + }); +}; + +/** + * Formats the index fields into a format the UI wants. + * + * NOTE: This will have array sizes up to 4.7 megs in size at a time when being called. + * This function should be as optimized as possible and should avoid any and all creation + * of new arrays, iterating over the arrays or performing any n^2 operations. + * @param responsesIndexFields The response index fields to format + * @param indexesAlias The index alias + */ +export const formatIndexFields = async ( + responsesIndexFields: IndexFieldDescriptor[][], + indexesAlias: string[] +): Promise => { + const fields = await formatFirstFields(responsesIndexFields, indexesAlias); + const secondFields = await formatSecondFields(fields); + return secondFields; +}; diff --git a/x-pack/plugins/security_solution/server/utils/beat_schema/index.test.ts b/x-pack/plugins/security_solution/server/utils/beat_schema/index.test.ts index 5f002aa7fad7b..29944edf382f4 100644 --- a/x-pack/plugins/security_solution/server/utils/beat_schema/index.test.ts +++ b/x-pack/plugins/security_solution/server/utils/beat_schema/index.test.ts @@ -6,7 +6,7 @@ import { cloneDeep, isArray } from 'lodash/fp'; -import { convertSchemaToAssociativeArray, getIndexSchemaDoc, getIndexAlias } from '.'; +import { convertSchemaToAssociativeArray, getIndexSchemaDoc } from '.'; import { auditbeatSchema, filebeatSchema, packetbeatSchema } from './8.0.0'; import { Schema } from './type'; @@ -394,24 +394,4 @@ describe('Schema Beat', () => { ]); }); }); - - describe('getIndexAlias', () => { - test('getIndexAlias handles values with leading wildcard', () => { - const leadingWildcardIndex = '*-auditbeat-*'; - const result = getIndexAlias([leadingWildcardIndex], leadingWildcardIndex); - expect(result).toBe(leadingWildcardIndex); - }); - - test('getIndexAlias no match returns "unknown" string', () => { - const index = 'auditbeat-*'; - const result = getIndexAlias([index], 'hello'); - expect(result).toBe('unknown'); - }); - - test('empty index should not cause an error to return although it will cause an invalid regular expression to occur', () => { - const index = ''; - const result = getIndexAlias([index], 'hello'); - expect(result).toBe('unknown'); - }); - }); }); diff --git a/x-pack/plugins/security_solution/server/utils/beat_schema/index.ts b/x-pack/plugins/security_solution/server/utils/beat_schema/index.ts index 6ec15d328714d..58627a199a181 100644 --- a/x-pack/plugins/security_solution/server/utils/beat_schema/index.ts +++ b/x-pack/plugins/security_solution/server/utils/beat_schema/index.ts @@ -76,21 +76,6 @@ const convertFieldsToAssociativeArray = ( }, {}) : {}; -export const getIndexAlias = (defaultIndex: string[], indexName: string): string => { - try { - const found = defaultIndex.find((index) => `\\${indexName}`.match(`\\${index}`) != null); - if (found != null) { - return found; - } else { - return 'unknown'; - } - } catch (error) { - // if we encounter an error because the index contains invalid regular expressions then we should return an unknown - // rather than blow up with a toaster error upstream - return 'unknown'; - } -}; - export const getIndexSchemaDoc = memoize((index: string) => { if (index.match('auditbeat') != null) { return { diff --git a/x-pack/plugins/security_solution/server/utils/beat_schema/type.ts b/x-pack/plugins/security_solution/server/utils/beat_schema/type.ts index 2b7be8f4b7539..722589ce7e2bb 100644 --- a/x-pack/plugins/security_solution/server/utils/beat_schema/type.ts +++ b/x-pack/plugins/security_solution/server/utils/beat_schema/type.ts @@ -4,8 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -export type IndexAlias = 'auditbeat' | 'filebeat' | 'packetbeat' | 'ecs' | 'winlogbeat' | 'unknown'; - /* * BEAT Interface * From eecf4aa71f27b0fc22113f73ddf745a4fcd59cb5 Mon Sep 17 00:00:00 2001 From: Justin Ibarra Date: Tue, 25 Aug 2020 23:25:07 -0500 Subject: [PATCH 56/71] [Detection Rules] Add 7.9.1 rules (#75939) * increase lookback (`from`) and bump versions --- .../command_and_control_certutil_network_connection.json | 3 ++- .../credential_access_credential_dumping_msbuild.json | 3 ++- .../prepackaged_rules/credential_access_tcpdump_activity.json | 3 ++- ...on_adding_the_hidden_file_attribute_with_via_attribexe.json | 3 ++- ...efense_evasion_attempt_to_disable_iptables_or_firewall.json | 3 ++- .../defense_evasion_attempt_to_disable_syslog_service.json | 3 ++- ...evasion_base16_or_base32_encoding_or_decoding_activity.json | 3 ++- .../defense_evasion_base64_encoding_or_decoding_activity.json | 3 ++- .../defense_evasion_clearing_windows_event_logs.json | 3 ++- .../defense_evasion_delete_volume_usn_journal_with_fsutil.json | 3 ++- .../defense_evasion_deleting_backup_catalogs_with_wbadmin.json | 3 ++- .../defense_evasion_deletion_of_bash_command_line_history.json | 3 ++- .../defense_evasion_disable_selinux_attempt.json | 3 ++- ...ense_evasion_disable_windows_firewall_rules_with_netsh.json | 3 ++- ...efense_evasion_encoding_or_decoding_files_via_certutil.json | 3 ++- ...efense_evasion_execution_msbuild_started_by_office_app.json | 3 ++- .../defense_evasion_execution_msbuild_started_by_script.json | 3 ++- ...se_evasion_execution_msbuild_started_by_system_process.json | 3 ++- .../defense_evasion_execution_msbuild_started_renamed.json | 3 ++- ...fense_evasion_execution_msbuild_started_unusal_process.json | 3 ++- .../defense_evasion_file_deletion_via_shred.json | 3 ++- .../defense_evasion_file_mod_writable_dir.json | 3 ++- .../defense_evasion_hex_encoding_or_decoding_activity.json | 3 ++- .../prepackaged_rules/defense_evasion_hidden_file_dir_tmp.json | 3 ++- .../defense_evasion_kernel_module_removal.json | 3 ++- ...defense_evasion_misc_lolbin_connecting_to_the_internet.json | 3 ++- .../defense_evasion_modification_of_boot_config.json | 3 ++- ...fense_evasion_volume_shadow_copy_deletion_via_vssadmin.json | 3 ++- .../defense_evasion_volume_shadow_copy_deletion_via_wmic.json | 3 ++- .../prepackaged_rules/discovery_kernel_module_enumeration.json | 3 ++- .../discovery_net_command_system_account.json | 3 ++- .../discovery_virtual_machine_fingerprinting.json | 3 ++- .../rules/prepackaged_rules/discovery_whoami_commmand.json | 3 ++- .../execution_command_prompt_connecting_to_the_internet.json | 3 ++- .../execution_command_shell_started_by_powershell.json | 3 ++- .../execution_command_shell_started_by_svchost.json | 3 ++- ...tml_help_executable_program_connecting_to_the_internet.json | 3 ++- .../prepackaged_rules/execution_local_service_commands.json | 3 ++- .../execution_msbuild_making_network_connections.json | 3 ++- .../execution_mshta_making_network_connections.json | 3 ++- .../rules/prepackaged_rules/execution_msxsl_network.json | 3 ++- .../rules/prepackaged_rules/execution_perl_tty_shell.json | 3 ++- .../execution_psexec_lateral_movement_command.json | 3 ++- .../rules/prepackaged_rules/execution_python_tty_shell.json | 3 ++- ...ion_register_server_program_connecting_to_the_internet.json | 3 ++- .../execution_script_executing_powershell.json | 3 ++- .../execution_suspicious_ms_office_child_process.json | 3 ++- .../execution_suspicious_ms_outlook_child_process.json | 3 ++- .../prepackaged_rules/execution_suspicious_pdf_reader.json | 3 ++- .../execution_unusual_network_connection_via_rundll32.json | 3 ++- .../execution_unusual_process_network_connection.json | 3 ++- .../prepackaged_rules/execution_via_net_com_assemblies.json | 3 ++- .../lateral_movement_direct_outbound_smb_connection.json | 3 ++- .../lateral_movement_telnet_network_activity_external.json | 3 ++- .../lateral_movement_telnet_network_activity_internal.json | 3 ++- .../rules/prepackaged_rules/linux_hping_activity.json | 3 ++- .../rules/prepackaged_rules/linux_iodine_activity.json | 3 ++- .../rules/prepackaged_rules/linux_mknod_activity.json | 3 ++- .../prepackaged_rules/linux_netcat_network_connection.json | 3 ++- .../rules/prepackaged_rules/linux_nmap_activity.json | 3 ++- .../rules/prepackaged_rules/linux_nping_activity.json | 3 ++- .../linux_process_started_in_temp_directory.json | 3 ++- .../rules/prepackaged_rules/linux_socat_activity.json | 3 ++- .../rules/prepackaged_rules/linux_strace_activity.json | 3 ++- .../persistence_adobe_hijack_persistence.json | 3 ++- .../prepackaged_rules/persistence_kernel_module_activity.json | 3 ++- .../persistence_local_scheduled_task_commands.json | 3 ++- .../persistence_shell_activity_by_web_server.json | 3 ++- .../persistence_system_shells_via_services.json | 3 ++- .../prepackaged_rules/persistence_user_account_creation.json | 3 ++- .../privilege_escalation_setgid_bit_set_via_chmod.json | 3 ++- .../privilege_escalation_setuid_bit_set_via_chmod.json | 3 ++- .../privilege_escalation_sudoers_file_mod.json | 3 ++- .../privilege_escalation_uac_bypass_event_viewer.json | 3 ++- .../privilege_escalation_unusual_parentchild_relationship.json | 3 ++- 75 files changed, 150 insertions(+), 75 deletions(-) diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_certutil_network_connection.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_certutil_network_connection.json index 25274928aa2b7..a8be0fe97524e 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_certutil_network_connection.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_certutil_network_connection.json @@ -3,6 +3,7 @@ "Elastic" ], "description": "Identifies certutil.exe making a network connection. Adversaries could abuse certutil.exe to download a certificate, or malware, from a remote URL.", + "from": "now-9m", "index": [ "winlogbeat-*", "logs-endpoint.events.*" @@ -36,5 +37,5 @@ } ], "type": "query", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_credential_dumping_msbuild.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_credential_dumping_msbuild.json index 6be1f037f967e..f2032b5bef218 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_credential_dumping_msbuild.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_credential_dumping_msbuild.json @@ -6,6 +6,7 @@ "false_positives": [ "The Build Engine is commonly used by Windows developers but use by non-engineers is unusual." ], + "from": "now-9m", "index": [ "winlogbeat-*", "logs-endpoint.events.*" @@ -39,5 +40,5 @@ } ], "type": "query", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_tcpdump_activity.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_tcpdump_activity.json index d5b069f7b81e7..306a38f5d2a28 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_tcpdump_activity.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_tcpdump_activity.json @@ -6,6 +6,7 @@ "false_positives": [ "Some normal use of this command may originate from server or network administrators engaged in network troubleshooting." ], + "from": "now-9m", "index": [ "auditbeat-*", "logs-endpoint.events.*" @@ -54,5 +55,5 @@ } ], "type": "query", - "version": 3 + "version": 4 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_adding_the_hidden_file_attribute_with_via_attribexe.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_adding_the_hidden_file_attribute_with_via_attribexe.json index b22b74ebc53bc..c80f24a21d958 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_adding_the_hidden_file_attribute_with_via_attribexe.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_adding_the_hidden_file_attribute_with_via_attribexe.json @@ -3,6 +3,7 @@ "Elastic" ], "description": "Adversaries can add the 'hidden' attribute to files to hide them from the user in an attempt to evade detection.", + "from": "now-9m", "index": [ "winlogbeat-*", "logs-endpoint.events.*" @@ -51,5 +52,5 @@ } ], "type": "query", - "version": 3 + "version": 4 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_attempt_to_disable_iptables_or_firewall.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_attempt_to_disable_iptables_or_firewall.json index e2ba81da917b3..4d4f10bbaa599 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_attempt_to_disable_iptables_or_firewall.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_attempt_to_disable_iptables_or_firewall.json @@ -3,6 +3,7 @@ "Elastic" ], "description": "Adversaries may attempt to disable the iptables or firewall service in an attempt to affect how a host is allowed to receive or send network traffic.", + "from": "now-9m", "index": [ "auditbeat-*", "logs-endpoint.events.*" @@ -36,5 +37,5 @@ } ], "type": "query", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_attempt_to_disable_syslog_service.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_attempt_to_disable_syslog_service.json index 4f4a9aacd79aa..3c34b04a77a50 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_attempt_to_disable_syslog_service.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_attempt_to_disable_syslog_service.json @@ -3,6 +3,7 @@ "Elastic" ], "description": "Adversaries may attempt to disable the syslog service in an attempt to an attempt to disrupt event logging and evade detection by security controls.", + "from": "now-9m", "index": [ "auditbeat-*", "logs-endpoint.events.*" @@ -36,5 +37,5 @@ } ], "type": "query", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_base16_or_base32_encoding_or_decoding_activity.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_base16_or_base32_encoding_or_decoding_activity.json index 5bcc4a00ccd82..3cdfac92572b1 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_base16_or_base32_encoding_or_decoding_activity.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_base16_or_base32_encoding_or_decoding_activity.json @@ -6,6 +6,7 @@ "false_positives": [ "Automated tools such as Jenkins may encode or decode files as part of their normal behavior. These events can be filtered by the process executable or username values." ], + "from": "now-9m", "index": [ "auditbeat-*", "logs-endpoint.events.*" @@ -54,5 +55,5 @@ } ], "type": "query", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_base64_encoding_or_decoding_activity.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_base64_encoding_or_decoding_activity.json index a17fd6d2702dd..2d26d867b8718 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_base64_encoding_or_decoding_activity.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_base64_encoding_or_decoding_activity.json @@ -6,6 +6,7 @@ "false_positives": [ "Automated tools such as Jenkins may encode or decode files as part of their normal behavior. These events can be filtered by the process executable or username values." ], + "from": "now-9m", "index": [ "auditbeat-*", "logs-endpoint.events.*" @@ -54,5 +55,5 @@ } ], "type": "query", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_clearing_windows_event_logs.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_clearing_windows_event_logs.json index cf09bc512916f..60ce575148f4c 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_clearing_windows_event_logs.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_clearing_windows_event_logs.json @@ -3,6 +3,7 @@ "Elastic" ], "description": "Identifies attempts to clear Windows event log stores. This is often done by attackers in an attempt to evade detection or destroy forensic evidence on a system.", + "from": "now-9m", "index": [ "winlogbeat-*", "logs-endpoint.events.*" @@ -36,5 +37,5 @@ } ], "type": "query", - "version": 3 + "version": 4 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_delete_volume_usn_journal_with_fsutil.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_delete_volume_usn_journal_with_fsutil.json index 0c82444dd9397..50213b9f1a42c 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_delete_volume_usn_journal_with_fsutil.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_delete_volume_usn_journal_with_fsutil.json @@ -3,6 +3,7 @@ "Elastic" ], "description": "Identifies use of the fsutil.exe to delete the volume USNJRNL. This technique is used by attackers to eliminate evidence of files created during post-exploitation activities.", + "from": "now-9m", "index": [ "winlogbeat-*", "logs-endpoint.events.*" @@ -36,5 +37,5 @@ } ], "type": "query", - "version": 3 + "version": 4 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_deleting_backup_catalogs_with_wbadmin.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_deleting_backup_catalogs_with_wbadmin.json index c76c5f20fa88b..026735f413eab 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_deleting_backup_catalogs_with_wbadmin.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_deleting_backup_catalogs_with_wbadmin.json @@ -3,6 +3,7 @@ "Elastic" ], "description": "Identifies use of the wbadmin.exe to delete the backup catalog. Ransomware and other malware may do this to prevent system recovery.", + "from": "now-9m", "index": [ "winlogbeat-*", "logs-endpoint.events.*" @@ -36,5 +37,5 @@ } ], "type": "query", - "version": 3 + "version": 4 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_deletion_of_bash_command_line_history.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_deletion_of_bash_command_line_history.json index b38ed94e132e1..85d8bdcb2582f 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_deletion_of_bash_command_line_history.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_deletion_of_bash_command_line_history.json @@ -3,6 +3,7 @@ "Elastic" ], "description": "Adversaries may attempt to clear the bash command line history in an attempt to evade detection or forensic investigations.", + "from": "now-9m", "index": [ "auditbeat-*", "logs-endpoint.events.*" @@ -36,5 +37,5 @@ } ], "type": "query", - "version": 1 + "version": 2 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_disable_selinux_attempt.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_disable_selinux_attempt.json index 229a03de39600..d107c0b262091 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_disable_selinux_attempt.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_disable_selinux_attempt.json @@ -3,6 +3,7 @@ "Elastic" ], "description": "Identifies potential attempts to disable Security-Enhanced Linux (SELinux), which is a Linux kernel security feature to support access control policies. Adversaries may disable security tools to avoid possible detection of their tools and activities.", + "from": "now-9m", "index": [ "auditbeat-*", "logs-endpoint.events.*" @@ -36,5 +37,5 @@ } ], "type": "query", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_disable_windows_firewall_rules_with_netsh.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_disable_windows_firewall_rules_with_netsh.json index 4800e87c180e2..6fbf9ca800f79 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_disable_windows_firewall_rules_with_netsh.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_disable_windows_firewall_rules_with_netsh.json @@ -3,6 +3,7 @@ "Elastic" ], "description": "Identifies use of the netsh.exe to disable or weaken the local firewall. Attackers will use this command line tool to disable the firewall during troubleshooting or to enable network mobility.", + "from": "now-9m", "index": [ "winlogbeat-*", "logs-endpoint.events.*" @@ -36,5 +37,5 @@ } ], "type": "query", - "version": 3 + "version": 4 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_encoding_or_decoding_files_via_certutil.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_encoding_or_decoding_files_via_certutil.json index 075dd13d9819b..0d47aab2c64bd 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_encoding_or_decoding_files_via_certutil.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_encoding_or_decoding_files_via_certutil.json @@ -3,6 +3,7 @@ "Elastic" ], "description": "Identifies the use of certutil.exe to encode or decode data. CertUtil is a native Windows component which is part of Certificate Services. CertUtil is often abused by attackers to encode or decode base64 data for stealthier command and control or exfiltration.", + "from": "now-9m", "index": [ "winlogbeat-*", "logs-endpoint.events.*" @@ -36,5 +37,5 @@ } ], "type": "query", - "version": 3 + "version": 4 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_execution_msbuild_started_by_office_app.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_execution_msbuild_started_by_office_app.json index 133863f8e2148..df7fc85b63d4a 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_execution_msbuild_started_by_office_app.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_execution_msbuild_started_by_office_app.json @@ -6,6 +6,7 @@ "false_positives": [ "The Build Engine is commonly used by Windows developers but use by non-engineers is unusual. It is quite unusual for this program to be started by an Office application like Word or Excel." ], + "from": "now-9m", "index": [ "winlogbeat-*", "logs-endpoint.events.*" @@ -57,5 +58,5 @@ } ], "type": "query", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_execution_msbuild_started_by_script.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_execution_msbuild_started_by_script.json index 85d348bb14be0..aa4674f75bcd0 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_execution_msbuild_started_by_script.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_execution_msbuild_started_by_script.json @@ -6,6 +6,7 @@ "false_positives": [ "The Build Engine is commonly used by Windows developers but use by non-engineers is unusual." ], + "from": "now-9m", "index": [ "winlogbeat-*", "logs-endpoint.events.*" @@ -54,5 +55,5 @@ } ], "type": "query", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_execution_msbuild_started_by_system_process.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_execution_msbuild_started_by_system_process.json index 38482c0a70fc9..da7d91933bd2a 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_execution_msbuild_started_by_system_process.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_execution_msbuild_started_by_system_process.json @@ -6,6 +6,7 @@ "false_positives": [ "The Build Engine is commonly used by Windows developers but use by non-engineers is unusual." ], + "from": "now-9m", "index": [ "winlogbeat-*", "logs-endpoint.events.*" @@ -54,5 +55,5 @@ } ], "type": "query", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_execution_msbuild_started_renamed.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_execution_msbuild_started_renamed.json index 7db683caf2bb2..8e4f7366a7657 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_execution_msbuild_started_renamed.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_execution_msbuild_started_renamed.json @@ -6,6 +6,7 @@ "false_positives": [ "The Build Engine is commonly used by Windows developers but use by non-engineers is unusual." ], + "from": "now-9m", "index": [ "winlogbeat-*", "logs-endpoint.events.*" @@ -39,5 +40,5 @@ } ], "type": "query", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_execution_msbuild_started_unusal_process.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_execution_msbuild_started_unusal_process.json index 1c4666955dde0..4f353a6ff9e6f 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_execution_msbuild_started_unusal_process.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_execution_msbuild_started_unusal_process.json @@ -6,6 +6,7 @@ "false_positives": [ "The Build Engine is commonly used by Windows developers but use by non-engineers is unusual. If a build system triggers this rule it can be exempted by process, user or host name." ], + "from": "now-9m", "index": [ "winlogbeat-*", "logs-endpoint.events.*" @@ -42,5 +43,5 @@ } ], "type": "query", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_file_deletion_via_shred.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_file_deletion_via_shred.json index c375ea7b19b37..5b02f63a1c7f7 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_file_deletion_via_shred.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_file_deletion_via_shred.json @@ -3,6 +3,7 @@ "Elastic" ], "description": "Malware or other files dropped or created on a system by an adversary may leave traces behind as to what was done within a network and how. Adversaries may remove these files over the course of an intrusion to keep their footprint low or remove them at the end as part of the post-intrusion cleanup process.", + "from": "now-9m", "index": [ "auditbeat-*", "logs-endpoint.events.*" @@ -36,5 +37,5 @@ } ], "type": "query", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_file_mod_writable_dir.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_file_mod_writable_dir.json index 22090e1a241e7..8ee2d4fda7bf8 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_file_mod_writable_dir.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_file_mod_writable_dir.json @@ -6,6 +6,7 @@ "false_positives": [ "Certain programs or applications may modify files or change ownership in writable directories. These can be exempted by username." ], + "from": "now-9m", "index": [ "auditbeat-*", "logs-endpoint.events.*" @@ -39,5 +40,5 @@ } ], "type": "query", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_hex_encoding_or_decoding_activity.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_hex_encoding_or_decoding_activity.json index 00491937e9aae..f5345b2276e8a 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_hex_encoding_or_decoding_activity.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_hex_encoding_or_decoding_activity.json @@ -6,6 +6,7 @@ "false_positives": [ "Automated tools such as Jenkins may encode or decode files as part of their normal behavior. These events can be filtered by the process executable or username values." ], + "from": "now-9m", "index": [ "auditbeat-*", "logs-endpoint.events.*" @@ -54,5 +55,5 @@ } ], "type": "query", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_hidden_file_dir_tmp.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_hidden_file_dir_tmp.json index 16a398011fc53..e66968a50709e 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_hidden_file_dir_tmp.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_hidden_file_dir_tmp.json @@ -6,6 +6,7 @@ "false_positives": [ "Certain tools may create hidden temporary files or directories upon installation or as part of their normal behavior. These events can be filtered by the process arguments, username, or process name values." ], + "from": "now-9m", "index": [ "auditbeat-*", "logs-endpoint.events.*" @@ -55,5 +56,5 @@ } ], "type": "query", - "version": 1 + "version": 2 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_kernel_module_removal.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_kernel_module_removal.json index 11781cb719599..ad751a1031437 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_kernel_module_removal.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_kernel_module_removal.json @@ -6,6 +6,7 @@ "false_positives": [ "There is usually no reason to remove modules, but some buggy modules require it. These can be exempted by username. Note that some Linux distributions are not built to support the removal of modules at all." ], + "from": "now-9m", "index": [ "auditbeat-*", "logs-endpoint.events.*" @@ -57,5 +58,5 @@ } ], "type": "query", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_misc_lolbin_connecting_to_the_internet.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_misc_lolbin_connecting_to_the_internet.json index 7d931725fa6eb..5b5f69a0aef74 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_misc_lolbin_connecting_to_the_internet.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_misc_lolbin_connecting_to_the_internet.json @@ -3,6 +3,7 @@ "Elastic" ], "description": "Binaries signed with trusted digital certificates can execute on Windows systems protected by digital signature validation. Adversaries may use these binaries to 'live off the land' and execute malicious files that could bypass application allowlists and signature validation.", + "from": "now-9m", "index": [ "winlogbeat-*", "logs-endpoint.events.*" @@ -51,5 +52,5 @@ } ], "type": "query", - "version": 3 + "version": 4 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_modification_of_boot_config.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_modification_of_boot_config.json index 1bffe7a1cfc24..6025fc5ca6452 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_modification_of_boot_config.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_modification_of_boot_config.json @@ -3,6 +3,7 @@ "Elastic" ], "description": "Identifies use of bcdedit.exe to delete boot configuration data. This tactic is sometimes used as by malware or an attacker as a destructive technique.", + "from": "now-9m", "index": [ "winlogbeat-*", "logs-endpoint.events.*" @@ -36,5 +37,5 @@ } ], "type": "query", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_volume_shadow_copy_deletion_via_vssadmin.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_volume_shadow_copy_deletion_via_vssadmin.json index f3cc5c2eec8a3..8a504281b03f7 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_volume_shadow_copy_deletion_via_vssadmin.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_volume_shadow_copy_deletion_via_vssadmin.json @@ -3,6 +3,7 @@ "Elastic" ], "description": "Identifies use of vssadmin.exe for shadow copy deletion on endpoints. This commonly occurs in tandem with ransomware or other destructive attacks.", + "from": "now-9m", "index": [ "winlogbeat-*", "logs-endpoint.events.*" @@ -36,5 +37,5 @@ } ], "type": "query", - "version": 3 + "version": 4 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_volume_shadow_copy_deletion_via_wmic.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_volume_shadow_copy_deletion_via_wmic.json index 334276142ca42..2ae938bb34104 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_volume_shadow_copy_deletion_via_wmic.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_volume_shadow_copy_deletion_via_wmic.json @@ -3,6 +3,7 @@ "Elastic" ], "description": "Identifies use of wmic.exe for shadow copy deletion on endpoints. This commonly occurs in tandem with ransomware or other destructive attacks.", + "from": "now-9m", "index": [ "winlogbeat-*", "logs-endpoint.events.*" @@ -36,5 +37,5 @@ } ], "type": "query", - "version": 3 + "version": 4 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_kernel_module_enumeration.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_kernel_module_enumeration.json index 0e4bea426c591..af9c4b5409964 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_kernel_module_enumeration.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_kernel_module_enumeration.json @@ -6,6 +6,7 @@ "false_positives": [ "Security tools and device drivers may run these programs in order to enumerate kernel modules. Use of these programs by ordinary users is uncommon. These can be exempted by process name or username." ], + "from": "now-9m", "index": [ "auditbeat-*", "logs-endpoint.events.*" @@ -39,5 +40,5 @@ } ], "type": "query", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_net_command_system_account.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_net_command_system_account.json index 6ac2bbf355961..f1a214b7cd436 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_net_command_system_account.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_net_command_system_account.json @@ -3,6 +3,7 @@ "Elastic" ], "description": "Identifies the SYSTEM account using the Net utility. The Net utility is a component of the Windows operating system. It is used in command line operations for control of users, groups, services, and network connections.", + "from": "now-9m", "index": [ "winlogbeat-*", "logs-endpoint.events.*" @@ -36,5 +37,5 @@ } ], "type": "query", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_virtual_machine_fingerprinting.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_virtual_machine_fingerprinting.json index e73aa5f4566a7..d913a92e2ee0e 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_virtual_machine_fingerprinting.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_virtual_machine_fingerprinting.json @@ -6,6 +6,7 @@ "false_positives": [ "Certain tools or automated software may enumerate hardware information. These tools can be exempted via user name or process arguments to eliminate potential noise." ], + "from": "now-9m", "index": [ "auditbeat-*", "logs-endpoint.events.*" @@ -39,5 +40,5 @@ } ], "type": "query", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_whoami_commmand.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_whoami_commmand.json index 0017186787139..a8b34362d9579 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_whoami_commmand.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_whoami_commmand.json @@ -6,6 +6,7 @@ "false_positives": [ "Security testing tools and frameworks may run this command. Some normal use of this command may originate from automation tools and frameworks." ], + "from": "now-9m", "index": [ "auditbeat-*", "logs-endpoint.events.*" @@ -39,5 +40,5 @@ } ], "type": "query", - "version": 3 + "version": 4 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_command_prompt_connecting_to_the_internet.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_command_prompt_connecting_to_the_internet.json index 0ba6480fe42a1..46208f3753fa1 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_command_prompt_connecting_to_the_internet.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_command_prompt_connecting_to_the_internet.json @@ -6,6 +6,7 @@ "false_positives": [ "Administrators may use the command prompt for regular administrative tasks. It's important to baseline your environment for network connections being made from the command prompt to determine any abnormal use of this tool." ], + "from": "now-9m", "index": [ "winlogbeat-*", "logs-endpoint.events.*" @@ -54,5 +55,5 @@ } ], "type": "query", - "version": 3 + "version": 4 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_command_shell_started_by_powershell.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_command_shell_started_by_powershell.json index 2d3edb0f5f6cc..c619d8f764bc4 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_command_shell_started_by_powershell.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_command_shell_started_by_powershell.json @@ -3,6 +3,7 @@ "Elastic" ], "description": "Identifies a suspicious parent child process relationship with cmd.exe descending from PowerShell.exe.", + "from": "now-9m", "index": [ "winlogbeat-*", "logs-endpoint.events.*" @@ -51,5 +52,5 @@ } ], "type": "query", - "version": 3 + "version": 4 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_command_shell_started_by_svchost.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_command_shell_started_by_svchost.json index 3a4b4915f3c8b..140212e4148eb 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_command_shell_started_by_svchost.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_command_shell_started_by_svchost.json @@ -3,6 +3,7 @@ "Elastic" ], "description": "Identifies a suspicious parent child process relationship with cmd.exe descending from svchost.exe", + "from": "now-9m", "index": [ "winlogbeat-*", "logs-endpoint.events.*" @@ -36,5 +37,5 @@ } ], "type": "query", - "version": 3 + "version": 4 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_html_help_executable_program_connecting_to_the_internet.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_html_help_executable_program_connecting_to_the_internet.json index a2eb76b9831f0..963c6b2e53ed6 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_html_help_executable_program_connecting_to_the_internet.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_html_help_executable_program_connecting_to_the_internet.json @@ -3,6 +3,7 @@ "Elastic" ], "description": "Compiled HTML files (.chm) are commonly distributed as part of the Microsoft HTML Help system. Adversaries may conceal malicious code in a CHM file and deliver it to a victim for execution. CHM content is loaded by the HTML Help executable program (hh.exe).", + "from": "now-9m", "index": [ "winlogbeat-*", "logs-endpoint.events.*" @@ -51,5 +52,5 @@ } ], "type": "query", - "version": 3 + "version": 4 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_local_service_commands.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_local_service_commands.json index e43ab9de86ef7..7b20cefdc67f0 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_local_service_commands.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_local_service_commands.json @@ -3,6 +3,7 @@ "Elastic" ], "description": "Identifies use of sc.exe to create, modify, or start services on remote hosts. This could be indicative of adversary lateral movement but will be noisy if commonly done by admins.", + "from": "now-9m", "index": [ "winlogbeat-*", "logs-endpoint.events.*" @@ -36,5 +37,5 @@ } ], "type": "query", - "version": 3 + "version": 4 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_msbuild_making_network_connections.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_msbuild_making_network_connections.json index 9d480259d49de..629efa90a71ea 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_msbuild_making_network_connections.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_msbuild_making_network_connections.json @@ -3,6 +3,7 @@ "Elastic" ], "description": "Identifies MsBuild.exe making outbound network connections. This may indicate adversarial activity as MsBuild is often leveraged by adversaries to execute code and evade detection.", + "from": "now-9m", "index": [ "winlogbeat-*", "logs-endpoint.events.*" @@ -36,5 +37,5 @@ } ], "type": "query", - "version": 3 + "version": 4 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_mshta_making_network_connections.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_mshta_making_network_connections.json index cdef5f16e5cd7..7af823070889f 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_mshta_making_network_connections.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_mshta_making_network_connections.json @@ -3,6 +3,7 @@ "Elastic" ], "description": "Identifies mshta.exe making a network connection. This may indicate adversarial activity as mshta.exe is often leveraged by adversaries to execute malicious scripts and evade detection.", + "from": "now-9m", "index": [ "winlogbeat-*", "logs-endpoint.events.*" @@ -39,5 +40,5 @@ } ], "type": "query", - "version": 3 + "version": 4 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_msxsl_network.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_msxsl_network.json index d501bda08c3a5..1dc75575636fb 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_msxsl_network.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_msxsl_network.json @@ -3,6 +3,7 @@ "Elastic" ], "description": "Identifies msxsl.exe making a network connection. This may indicate adversarial activity as msxsl.exe is often leveraged by adversaries to execute malicious scripts and evade detection.", + "from": "now-9m", "index": [ "winlogbeat-*", "logs-endpoint.events.*" @@ -36,5 +37,5 @@ } ], "type": "query", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_perl_tty_shell.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_perl_tty_shell.json index e82b42869e44d..9b6ee099116f3 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_perl_tty_shell.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_perl_tty_shell.json @@ -3,6 +3,7 @@ "Elastic" ], "description": "Identifies when a terminal (tty) is spawned via Perl. Attackers may upgrade a simple reverse shell to a fully interactive tty after obtaining initial access to a host.", + "from": "now-9m", "index": [ "auditbeat-*", "logs-endpoint.events.*" @@ -36,5 +37,5 @@ } ], "type": "query", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_psexec_lateral_movement_command.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_psexec_lateral_movement_command.json index e4c84fd3c3b83..f647d8d00e084 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_psexec_lateral_movement_command.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_psexec_lateral_movement_command.json @@ -6,6 +6,7 @@ "false_positives": [ "PsExec is a dual-use tool that can be used for benign or malicious activity. It's important to baseline your environment to determine the amount of noise to expect from this tool." ], + "from": "now-9m", "index": [ "winlogbeat-*", "logs-endpoint.events.*" @@ -54,5 +55,5 @@ } ], "type": "query", - "version": 3 + "version": 4 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_python_tty_shell.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_python_tty_shell.json index 3aa9ac20bba9e..d9c26a9c26cc9 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_python_tty_shell.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_python_tty_shell.json @@ -3,6 +3,7 @@ "Elastic" ], "description": "Identifies when a terminal (tty) is spawned via Python. Attackers may upgrade a simple reverse shell to a fully interactive tty after obtaining initial access to a host.", + "from": "now-9m", "index": [ "auditbeat-*", "logs-endpoint.events.*" @@ -36,5 +37,5 @@ } ], "type": "query", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_register_server_program_connecting_to_the_internet.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_register_server_program_connecting_to_the_internet.json index 0a1ba97bd01ea..b3b6a2b0c7fab 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_register_server_program_connecting_to_the_internet.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_register_server_program_connecting_to_the_internet.json @@ -6,6 +6,7 @@ "false_positives": [ "Security testing may produce events like this. Activity of this kind performed by non-engineers and ordinary users is unusual." ], + "from": "now-9m", "index": [ "winlogbeat-*", "logs-endpoint.events.*" @@ -54,5 +55,5 @@ } ], "type": "query", - "version": 3 + "version": 4 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_script_executing_powershell.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_script_executing_powershell.json index 7305247192f57..6d7f11f01fae0 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_script_executing_powershell.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_script_executing_powershell.json @@ -3,6 +3,7 @@ "Elastic" ], "description": "Identifies a PowerShell process launched by either cscript.exe or wscript.exe. Observing Windows scripting processes executing a PowerShell script, may be indicative of malicious activity.", + "from": "now-9m", "index": [ "winlogbeat-*", "logs-endpoint.events.*" @@ -36,5 +37,5 @@ } ], "type": "query", - "version": 3 + "version": 4 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_suspicious_ms_office_child_process.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_suspicious_ms_office_child_process.json index 7ff8eb9424d5f..005a0c38c8a8b 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_suspicious_ms_office_child_process.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_suspicious_ms_office_child_process.json @@ -3,6 +3,7 @@ "Elastic" ], "description": "Identifies suspicious child processes of frequently targeted Microsoft Office applications (Word, PowerPoint, Excel). These child processes are often launched during exploitation of Office applications or from documents with malicious macros.", + "from": "now-9m", "index": [ "winlogbeat-*", "logs-endpoint.events.*" @@ -36,5 +37,5 @@ } ], "type": "query", - "version": 3 + "version": 4 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_suspicious_ms_outlook_child_process.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_suspicious_ms_outlook_child_process.json index e923407765f8f..74e21c7d17479 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_suspicious_ms_outlook_child_process.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_suspicious_ms_outlook_child_process.json @@ -3,6 +3,7 @@ "Elastic" ], "description": "Identifies suspicious child processes of Microsoft Outlook. These child processes are often associated with spear phishing activity.", + "from": "now-9m", "index": [ "winlogbeat-*", "logs-endpoint.events.*" @@ -36,5 +37,5 @@ } ], "type": "query", - "version": 3 + "version": 4 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_suspicious_pdf_reader.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_suspicious_pdf_reader.json index 24a744ce30832..adf1a76bfb901 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_suspicious_pdf_reader.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_suspicious_pdf_reader.json @@ -3,6 +3,7 @@ "Elastic" ], "description": "Identifies suspicious child processes of PDF reader applications. These child processes are often launched via exploitation of PDF applications or social engineering.", + "from": "now-9m", "index": [ "winlogbeat-*", "logs-endpoint.events.*" @@ -36,5 +37,5 @@ } ], "type": "query", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_unusual_network_connection_via_rundll32.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_unusual_network_connection_via_rundll32.json index 529f2199e46dc..1104159350655 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_unusual_network_connection_via_rundll32.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_unusual_network_connection_via_rundll32.json @@ -3,6 +3,7 @@ "Elastic" ], "description": "Identifies unusual instances of rundll32.exe making outbound network connections. This may indicate adversarial activity and may identify malicious DLLs.", + "from": "now-9m", "index": [ "winlogbeat-*", "logs-endpoint.events.*" @@ -36,5 +37,5 @@ } ], "type": "query", - "version": 4 + "version": 5 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_unusual_process_network_connection.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_unusual_process_network_connection.json index 69a25b3b24bac..854ecc40d76ab 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_unusual_process_network_connection.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_unusual_process_network_connection.json @@ -3,6 +3,7 @@ "Elastic" ], "description": "Identifies network activity from unexpected system applications. This may indicate adversarial activity as these applications are often leveraged by adversaries to execute code and evade detection.", + "from": "now-9m", "index": [ "winlogbeat-*", "logs-endpoint.events.*" @@ -36,5 +37,5 @@ } ], "type": "query", - "version": 3 + "version": 4 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_via_net_com_assemblies.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_via_net_com_assemblies.json index cae5d1b7e0f1f..d9dcbfe25a4c2 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_via_net_com_assemblies.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_via_net_com_assemblies.json @@ -3,6 +3,7 @@ "Elastic" ], "description": "RegSvcs.exe and RegAsm.exe are Windows command line utilities that are used to register .NET Component Object Model (COM) assemblies. Adversaries can use RegSvcs.exe and RegAsm.exe to proxy execution of code through a trusted Windows utility.", + "from": "now-9m", "index": [ "winlogbeat-*", "logs-endpoint.events.*" @@ -51,5 +52,5 @@ } ], "type": "query", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_direct_outbound_smb_connection.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_direct_outbound_smb_connection.json index 8a68b26abad20..e4014b22a6c09 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_direct_outbound_smb_connection.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_direct_outbound_smb_connection.json @@ -3,6 +3,7 @@ "Elastic" ], "description": "Identifies unexpected processes making network connections over port 445. Windows File Sharing is typically implemented over Server Message Block (SMB), which communicates between hosts using port 445. When legitimate, these network connections are established by the kernel. Processes making 445/tcp connections may be port scanners, exploits, or suspicious user-level processes moving laterally.", + "from": "now-9m", "index": [ "winlogbeat-*", "logs-endpoint.events.*" @@ -36,5 +37,5 @@ } ], "type": "query", - "version": 3 + "version": 4 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_telnet_network_activity_external.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_telnet_network_activity_external.json index 2ea75dbd758cb..e4804329c0f30 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_telnet_network_activity_external.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_telnet_network_activity_external.json @@ -6,6 +6,7 @@ "false_positives": [ "Telnet can be used for both benign or malicious purposes. Telnet is included by default in some Linux distributions, so its presence is not inherently suspicious. The use of Telnet to manage devices remotely has declined in recent years in favor of more secure protocols such as SSH. Telnet usage by non-automated tools or frameworks may be suspicious." ], + "from": "now-9m", "index": [ "auditbeat-*", "logs-endpoint.events.*" @@ -39,5 +40,5 @@ } ], "type": "query", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_telnet_network_activity_internal.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_telnet_network_activity_internal.json index 4379759608aba..30312987d166c 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_telnet_network_activity_internal.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_telnet_network_activity_internal.json @@ -6,6 +6,7 @@ "false_positives": [ "Telnet can be used for both benign or malicious purposes. Telnet is included by default in some Linux distributions, so its presence is not inherently suspicious. The use of Telnet to manage devices remotely has declined in recent years in favor of more secure protocols such as SSH. Telnet usage by non-automated tools or frameworks may be suspicious." ], + "from": "now-9m", "index": [ "auditbeat-*", "logs-endpoint.events.*" @@ -39,5 +40,5 @@ } ], "type": "query", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_hping_activity.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_hping_activity.json index 24104439cd0ec..3a5c4d9e69d49 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_hping_activity.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_hping_activity.json @@ -6,6 +6,7 @@ "false_positives": [ "Normal use of hping is uncommon apart from security testing and research. Use by non-security engineers is very uncommon." ], + "from": "now-9m", "index": [ "auditbeat-*", "logs-endpoint.events.*" @@ -25,5 +26,5 @@ "Linux" ], "type": "query", - "version": 3 + "version": 4 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_iodine_activity.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_iodine_activity.json index 73bf20a5a175e..63c82c5662df6 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_iodine_activity.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_iodine_activity.json @@ -6,6 +6,7 @@ "false_positives": [ "Normal use of Iodine is uncommon apart from security testing and research. Use by non-security engineers is very uncommon." ], + "from": "now-9m", "index": [ "auditbeat-*", "logs-endpoint.events.*" @@ -25,5 +26,5 @@ "Linux" ], "type": "query", - "version": 3 + "version": 4 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_mknod_activity.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_mknod_activity.json index 1895caf4dea81..37d5468c773bf 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_mknod_activity.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_mknod_activity.json @@ -6,6 +6,7 @@ "false_positives": [ "Mknod is a Linux system program. Some normal use of this program, at varying levels of frequency, may originate from scripts, automation tools, and frameworks. Usage by web servers is more likely to be suspicious." ], + "from": "now-9m", "index": [ "auditbeat-*", "logs-endpoint.events.*" @@ -25,5 +26,5 @@ "Linux" ], "type": "query", - "version": 3 + "version": 4 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_netcat_network_connection.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_netcat_network_connection.json index ac46bcbdbc083..bce10f640691b 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_netcat_network_connection.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_netcat_network_connection.json @@ -6,6 +6,7 @@ "false_positives": [ "Netcat is a dual-use tool that can be used for benign or malicious activity. Netcat is included in some Linux distributions so its presence is not necessarily suspicious. Some normal use of this program, while uncommon, may originate from scripts, automation tools, and frameworks." ], + "from": "now-9m", "index": [ "auditbeat-*", "logs-endpoint.events.*" @@ -27,5 +28,5 @@ "Linux" ], "type": "query", - "version": 3 + "version": 4 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_nmap_activity.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_nmap_activity.json index 2825dc28ad18f..5d9e338425bda 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_nmap_activity.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_nmap_activity.json @@ -6,6 +6,7 @@ "false_positives": [ "Security testing tools and frameworks may run `Nmap` in the course of security auditing. Some normal use of this command may originate from security engineers and network or server administrators. Use of nmap by ordinary users is uncommon." ], + "from": "now-9m", "index": [ "auditbeat-*", "logs-endpoint.events.*" @@ -25,5 +26,5 @@ "Linux" ], "type": "query", - "version": 3 + "version": 4 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_nping_activity.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_nping_activity.json index 234a09e9607b9..bd019c9a80c4c 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_nping_activity.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_nping_activity.json @@ -6,6 +6,7 @@ "false_positives": [ "Some normal use of this command may originate from security engineers and network or server administrators, but this is usually not routine or unannounced. Use of `Nping` by non-engineers or ordinary users is uncommon." ], + "from": "now-9m", "index": [ "auditbeat-*", "logs-endpoint.events.*" @@ -25,5 +26,5 @@ "Linux" ], "type": "query", - "version": 3 + "version": 4 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_process_started_in_temp_directory.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_process_started_in_temp_directory.json index 759622804444e..f0bbc892d7d9c 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_process_started_in_temp_directory.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_process_started_in_temp_directory.json @@ -6,6 +6,7 @@ "false_positives": [ "Build systems, like Jenkins, may start processes in the `/tmp` directory. These can be exempted by name or by username." ], + "from": "now-9m", "index": [ "auditbeat-*", "logs-endpoint.events.*" @@ -22,5 +23,5 @@ "Linux" ], "type": "query", - "version": 3 + "version": 4 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_socat_activity.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_socat_activity.json index cd38aff3f2164..fac03d31b57bf 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_socat_activity.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_socat_activity.json @@ -6,6 +6,7 @@ "false_positives": [ "Socat is a dual-use tool that can be used for benign or malicious activity. Some normal use of this program, at varying levels of frequency, may originate from scripts, automation tools, and frameworks. Usage by web servers is more likely to be suspicious." ], + "from": "now-9m", "index": [ "auditbeat-*", "logs-endpoint.events.*" @@ -25,5 +26,5 @@ "Linux" ], "type": "query", - "version": 3 + "version": 4 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_strace_activity.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_strace_activity.json index 7fcb9f915c560..c1b782d612ccb 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_strace_activity.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_strace_activity.json @@ -6,6 +6,7 @@ "false_positives": [ "Strace is a dual-use tool that can be used for benign or malicious activity. Some normal use of this command may originate from developers or SREs engaged in debugging or system call tracing." ], + "from": "now-9m", "index": [ "auditbeat-*", "logs-endpoint.events.*" @@ -25,5 +26,5 @@ "Linux" ], "type": "query", - "version": 3 + "version": 4 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_adobe_hijack_persistence.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_adobe_hijack_persistence.json index 3392a1bff23b8..a4c62b98fb060 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_adobe_hijack_persistence.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_adobe_hijack_persistence.json @@ -3,6 +3,7 @@ "Elastic" ], "description": "Detects writing executable files that will be automatically launched by Adobe on launch.", + "from": "now-9m", "index": [ "winlogbeat-*", "logs-endpoint.events.*" @@ -36,5 +37,5 @@ } ], "type": "query", - "version": 3 + "version": 4 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_kernel_module_activity.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_kernel_module_activity.json index e76379d171bf7..e3dedeef07eb5 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_kernel_module_activity.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_kernel_module_activity.json @@ -6,6 +6,7 @@ "false_positives": [ "Security tools and device drivers may run these programs in order to load legitimate kernel modules. Use of these programs by ordinary users is uncommon." ], + "from": "now-9m", "index": [ "auditbeat-*", "logs-endpoint.events.*" @@ -42,5 +43,5 @@ } ], "type": "query", - "version": 3 + "version": 4 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_local_scheduled_task_commands.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_local_scheduled_task_commands.json index b9e7f941ee5df..8b81789f6aa8f 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_local_scheduled_task_commands.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_local_scheduled_task_commands.json @@ -6,6 +6,7 @@ "false_positives": [ "Legitimate scheduled tasks may be created during installation of new software." ], + "from": "now-9m", "index": [ "winlogbeat-*", "logs-endpoint.events.*" @@ -39,5 +40,5 @@ } ], "type": "query", - "version": 3 + "version": 4 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_shell_activity_by_web_server.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_shell_activity_by_web_server.json index 0cf6fcdb3875a..2aaf0012acabf 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_shell_activity_by_web_server.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_shell_activity_by_web_server.json @@ -6,6 +6,7 @@ "false_positives": [ "Network monitoring or management products may have a web server component that runs shell commands as part of normal behavior." ], + "from": "now-9m", "index": [ "auditbeat-*", "logs-endpoint.events.*" @@ -42,5 +43,5 @@ } ], "type": "query", - "version": 4 + "version": 5 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_system_shells_via_services.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_system_shells_via_services.json index 59715dae441f4..32d78480325e6 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_system_shells_via_services.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_system_shells_via_services.json @@ -3,6 +3,7 @@ "Elastic" ], "description": "Windows services typically run as SYSTEM and can be used as a privilege escalation opportunity. Malware or penetration testers may run a shell as a service to gain SYSTEM permissions.", + "from": "now-9m", "index": [ "winlogbeat-*", "logs-endpoint.events.*" @@ -36,5 +37,5 @@ } ], "type": "query", - "version": 3 + "version": 4 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_user_account_creation.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_user_account_creation.json index 7465751d5cd49..3f2e00f0976de 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_user_account_creation.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_user_account_creation.json @@ -3,6 +3,7 @@ "Elastic" ], "description": "Identifies attempts to create new local users. This is sometimes done by attackers to increase access to a system or domain.", + "from": "now-9m", "index": [ "winlogbeat-*", "logs-endpoint.events.*" @@ -36,5 +37,5 @@ } ], "type": "query", - "version": 3 + "version": 4 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_setgid_bit_set_via_chmod.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_setgid_bit_set_via_chmod.json index 9550eea6ca6aa..bb0856c0452d5 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_setgid_bit_set_via_chmod.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_setgid_bit_set_via_chmod.json @@ -3,6 +3,7 @@ "Elastic" ], "description": "An adversary may add the setgid bit to a file or directory in order to run a file with the privileges of the owning group. An adversary can take advantage of this to either do a shell escape or exploit a vulnerability in an application with the setgid bit to get code running in a different user\u2019s context. Additionally, adversaries can use this mechanism on their own malware to make sure they're able to execute in elevated contexts in the future.", + "from": "now-9m", "index": [ "auditbeat-*", "logs-endpoint.events.*" @@ -52,5 +53,5 @@ } ], "type": "query", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_setuid_bit_set_via_chmod.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_setuid_bit_set_via_chmod.json index 343426953add6..4cf60d2c9d0de 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_setuid_bit_set_via_chmod.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_setuid_bit_set_via_chmod.json @@ -3,6 +3,7 @@ "Elastic" ], "description": "An adversary may add the setuid bit to a file or directory in order to run a file with the privileges of the owning user. An adversary can take advantage of this to either do a shell escape or exploit a vulnerability in an application with the setuid bit to get code running in a different user\u2019s context. Additionally, adversaries can use this mechanism on their own malware to make sure they're able to execute in elevated contexts in the future.", + "from": "now-9m", "index": [ "auditbeat-*", "logs-endpoint.events.*" @@ -52,5 +53,5 @@ } ], "type": "query", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_sudoers_file_mod.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_sudoers_file_mod.json index 44b50c74bafe6..73a804fcbda8f 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_sudoers_file_mod.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_sudoers_file_mod.json @@ -3,6 +3,7 @@ "Elastic" ], "description": "A sudoers file specifies the commands that users or groups can run and from which terminals. Adversaries can take advantage of these configurations to execute commands as other users or spawn processes with higher privileges.", + "from": "now-9m", "index": [ "auditbeat-*", "logs-endpoint.events.*" @@ -36,5 +37,5 @@ } ], "type": "query", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_uac_bypass_event_viewer.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_uac_bypass_event_viewer.json index 50692dae3856f..740ff47e5abe5 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_uac_bypass_event_viewer.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_uac_bypass_event_viewer.json @@ -3,6 +3,7 @@ "Elastic" ], "description": "Identifies User Account Control (UAC) bypass via eventvwr.exe. Attackers bypass UAC to stealthily execute code with elevated permissions.", + "from": "now-9m", "index": [ "winlogbeat-*", "logs-endpoint.events.*" @@ -36,5 +37,5 @@ } ], "type": "query", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_unusual_parentchild_relationship.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_unusual_parentchild_relationship.json index 8f938c0ceee6d..c6c5cbce2c095 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_unusual_parentchild_relationship.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_unusual_parentchild_relationship.json @@ -3,6 +3,7 @@ "Elastic" ], "description": "Identifies Windows programs run from unexpected parent processes. This could indicate masquerading or other strange activity on a system.", + "from": "now-9m", "index": [ "winlogbeat-*", "logs-endpoint.events.*" @@ -36,5 +37,5 @@ } ], "type": "query", - "version": 3 + "version": 4 } From ddf99b64db371f22f6752adc50648fcf2ff413fb Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Wed, 26 Aug 2020 09:09:40 +0200 Subject: [PATCH 57/71] [Lens] Fix rollup related bugs (#75314) Co-authored-by: Marta Bondyra --- .../datapanel.test.tsx | 19 +- .../indexpattern_datasource/datapanel.tsx | 8 +- .../dimension_panel/dimension_panel.test.tsx | 2 + .../fields_accordion.tsx | 12 +- .../indexpattern.test.ts | 2 + .../indexpattern_suggestions.test.tsx | 7 + .../layerpanel.test.tsx | 3 + .../indexpattern_datasource/loader.test.ts | 15 +- .../public/indexpattern_datasource/loader.ts | 8 + .../public/indexpattern_datasource/mocks.ts | 2 + .../definitions/date_histogram.test.tsx | 43 +- .../operations/definitions/date_histogram.tsx | 33 +- .../operations/definitions/index.ts | 2 +- .../operations/definitions/metrics.tsx | 2 +- .../operations/definitions/terms.test.tsx | 5 +- .../operations/definitions/terms.tsx | 2 +- .../operations/operations.test.ts | 1 + .../state_helpers.test.ts | 1 + .../indexpattern_datasource/to_expression.ts | 6 +- .../public/indexpattern_datasource/types.ts | 1 + x-pack/test/functional/apps/lens/index.ts | 3 + x-pack/test/functional/apps/lens/rollup.ts | 75 + .../es_archives/lens/rollup/config/data.json | 65 + .../lens/rollup/config/mappings.json | 1294 +++++++++++++++++ .../es_archives/lens/rollup/data/data.json | 59 + .../lens/rollup/data/mappings.json | 129 ++ 26 files changed, 1765 insertions(+), 34 deletions(-) create mode 100644 x-pack/test/functional/apps/lens/rollup.ts create mode 100644 x-pack/test/functional/es_archives/lens/rollup/config/data.json create mode 100644 x-pack/test/functional/es_archives/lens/rollup/config/mappings.json create mode 100644 x-pack/test/functional/es_archives/lens/rollup/data/data.json create mode 100644 x-pack/test/functional/es_archives/lens/rollup/data/mappings.json diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.test.tsx index 8291b673cd17a..f17bf172b0fb1 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.test.tsx @@ -84,6 +84,7 @@ const initialState: IndexPatternPrivateState = { id: '1', title: 'idx1', timeFieldName: 'timestamp', + hasRestrictions: false, fields: [ { name: 'timestamp', @@ -134,6 +135,7 @@ const initialState: IndexPatternPrivateState = { id: '2', title: 'idx2', timeFieldName: 'timestamp', + hasRestrictions: true, fields: [ { name: 'timestamp', @@ -191,6 +193,7 @@ const initialState: IndexPatternPrivateState = { id: '3', title: 'idx3', timeFieldName: 'timestamp', + hasRestrictions: false, fields: [ { name: 'timestamp', @@ -322,8 +325,20 @@ describe('IndexPattern Data Panel', () => { isFirstExistenceFetch: false, currentIndexPatternId: 'a', indexPatterns: { - a: { id: 'a', title: 'aaa', timeFieldName: 'atime', fields: [] }, - b: { id: 'b', title: 'bbb', timeFieldName: 'btime', fields: [] }, + a: { + id: 'a', + title: 'aaa', + timeFieldName: 'atime', + fields: [], + hasRestrictions: false, + }, + b: { + id: 'b', + title: 'bbb', + timeFieldName: 'btime', + fields: [], + hasRestrictions: false, + }, }, layers: { 1: { diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx index 0777b9b9d8e57..f7adf91e307da 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx @@ -126,6 +126,7 @@ export function IndexPatternDataPanel({ title: indexPatterns[id].title, timeFieldName: indexPatterns[id].timeFieldName, fields: indexPatterns[id].fields, + hasRestrictions: indexPatterns[id].hasRestrictions, })); const dslQuery = buildSafeEsQuery( @@ -422,6 +423,8 @@ export const InnerIndexPatternDataPanel = function InnerIndexPatternDataPanel({ ] ); + const fieldInfoUnavailable = existenceFetchFailed || currentIndexPattern.hasRestrictions; + return ( - {!existenceFetchFailed && ( + {!fieldInfoUnavailable && ( { foo: { id: 'foo', title: 'Foo pattern', + hasRestrictions: false, fields: [ { aggregatable: true, diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/fields_accordion.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/fields_accordion.tsx index af2ed97ad8125..30a92c21ff661 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/fields_accordion.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/fields_accordion.tsx @@ -47,6 +47,7 @@ export interface FieldsAccordionProps { renderCallout: JSX.Element; exists: boolean; showExistenceFetchError?: boolean; + hideDetails?: boolean; } export const InnerFieldsAccordion = function InnerFieldsAccordion({ @@ -61,13 +62,20 @@ export const InnerFieldsAccordion = function InnerFieldsAccordion({ fieldProps, renderCallout, exists, + hideDetails, showExistenceFetchError, }: FieldsAccordionProps) { const renderField = useCallback( (field: IndexPatternField) => ( - + ), - [fieldProps, exists] + [fieldProps, exists, hideDetails] ); return ( diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts index 0ba7b7df97853..900cd02622aaf 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts @@ -21,6 +21,7 @@ const expectedIndexPatterns = { id: '1', title: 'my-fake-index-pattern', timeFieldName: 'timestamp', + hasRestrictions: false, fields: [ { name: 'timestamp', @@ -70,6 +71,7 @@ const expectedIndexPatterns = { id: '2', title: 'my-fake-restricted-pattern', timeFieldName: 'timestamp', + hasRestrictions: true, fields: [ { name: 'timestamp', diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.test.tsx index 5489dcffc52c4..663d7c18bb370 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.test.tsx @@ -20,6 +20,7 @@ const expectedIndexPatterns = { id: '1', title: 'my-fake-index-pattern', timeFieldName: 'timestamp', + hasRestrictions: false, fields: [ { name: 'timestamp', @@ -68,6 +69,7 @@ const expectedIndexPatterns = { 2: { id: '2', title: 'my-fake-restricted-pattern', + hasRestrictions: true, timeFieldName: 'timestamp', fields: [ { @@ -322,6 +324,7 @@ describe('IndexPattern Data Source suggestions', () => { 1: { id: '1', title: 'no timefield', + hasRestrictions: false, fields: [ { name: 'bytes', @@ -532,6 +535,7 @@ describe('IndexPattern Data Source suggestions', () => { 1: { id: '1', title: 'no timefield', + hasRestrictions: false, fields: [ { name: 'bytes', @@ -1350,6 +1354,7 @@ describe('IndexPattern Data Source suggestions', () => { 1: { id: '1', title: 'my-fake-index-pattern', + hasRestrictions: false, fields: [ { name: 'field1', @@ -1493,6 +1498,7 @@ describe('IndexPattern Data Source suggestions', () => { 1: { id: '1', title: 'my-fake-index-pattern', + hasRestrictions: false, fields: [ { name: 'field1', @@ -1555,6 +1561,7 @@ describe('IndexPattern Data Source suggestions', () => { 1: { id: '1', title: 'my-fake-index-pattern', + hasRestrictions: false, fields: [ { name: 'field1', diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/layerpanel.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/layerpanel.test.tsx index 738cdd611a7ba..92e35b257f24a 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/layerpanel.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/layerpanel.test.tsx @@ -62,6 +62,7 @@ const initialState: IndexPatternPrivateState = { id: '1', title: 'my-fake-index-pattern', timeFieldName: 'timestamp', + hasRestrictions: false, fields: [ { name: 'timestamp', @@ -103,6 +104,7 @@ const initialState: IndexPatternPrivateState = { '2': { id: '2', title: 'my-fake-restricted-pattern', + hasRestrictions: true, timeFieldName: 'timestamp', fields: [ { @@ -160,6 +162,7 @@ const initialState: IndexPatternPrivateState = { id: '3', title: 'my-compatible-pattern', timeFieldName: 'timestamp', + hasRestrictions: false, fields: [ { name: 'timestamp', diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/loader.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/loader.test.ts index d80bf779a5d17..660be9514a92f 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/loader.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/loader.test.ts @@ -40,6 +40,7 @@ const indexPattern1 = ({ id: '1', title: 'my-fake-index-pattern', timeFieldName: 'timestamp', + hasRestrictions: false, fields: [ { name: 'timestamp', @@ -105,6 +106,7 @@ const indexPattern2 = ({ id: '2', title: 'my-fake-restricted-pattern', timeFieldName: 'timestamp', + hasRestrictions: true, fields: [ { name: 'timestamp', @@ -733,9 +735,9 @@ describe('loader', () => { dateRange: { fromDate: '1900-01-01', toDate: '2000-01-01' }, fetchJson, indexPatterns: [ - { id: '1', title: '1', fields: [] }, - { id: '2', title: '1', fields: [] }, - { id: '3', title: '1', fields: [] }, + { id: '1', title: '1', fields: [], hasRestrictions: false }, + { id: '2', title: '1', fields: [], hasRestrictions: false }, + { id: '3', title: '1', fields: [], hasRestrictions: false }, ], setState, dslQuery, @@ -783,9 +785,9 @@ describe('loader', () => { dateRange: { fromDate: '1900-01-01', toDate: '2000-01-01' }, fetchJson, indexPatterns: [ - { id: '1', title: '1', fields: [] }, - { id: '2', title: '1', fields: [] }, - { id: 'c', title: '1', fields: [] }, + { id: '1', title: '1', fields: [], hasRestrictions: false }, + { id: '2', title: '1', fields: [], hasRestrictions: false }, + { id: 'c', title: '1', fields: [], hasRestrictions: false }, ], setState, dslQuery, @@ -817,6 +819,7 @@ describe('loader', () => { { id: '1', title: '1', + hasRestrictions: false, fields: [{ name: 'field1' }, { name: 'field2' }] as IndexPatternField[], }, ], diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/loader.ts b/x-pack/plugins/lens/public/indexpattern_datasource/loader.ts index 24906790a9fc9..585a1281cbf51 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/loader.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/loader.ts @@ -91,6 +91,7 @@ export async function loadIndexPatterns({ timeFieldName, fieldFormatMap, fields: newFields, + hasRestrictions: !!typeMeta?.aggs, }; return { @@ -334,6 +335,7 @@ export async function syncExistingFields({ title: string; fields: IndexPatternField[]; timeFieldName?: string | null; + hasRestrictions: boolean; }>; fetchJson: HttpSetup['post']; setState: SetState; @@ -343,6 +345,12 @@ export async function syncExistingFields({ showNoDataPopover: () => void; }) { const existenceRequests = indexPatterns.map((pattern) => { + if (pattern.hasRestrictions) { + return { + indexPatternTitle: pattern.title, + existingFieldNames: pattern.fields.map((field) => field.name), + }; + } const body: Record = { dslQuery, fromDate: dateRange.fromDate, diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/mocks.ts b/x-pack/plugins/lens/public/indexpattern_datasource/mocks.ts index 869eee67d381d..31e6240993d36 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/mocks.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/mocks.ts @@ -11,6 +11,7 @@ export const createMockedIndexPattern = (): IndexPattern => ({ id: '1', title: 'my-fake-index-pattern', timeFieldName: 'timestamp', + hasRestrictions: false, fields: [ { name: 'timestamp', @@ -70,6 +71,7 @@ export const createMockedRestrictedIndexPattern = () => ({ id: '2', title: 'my-fake-restricted-pattern', timeFieldName: 'timestamp', + hasRestrictions: true, fields: [ { name: 'timestamp', diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/date_histogram.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/date_histogram.test.tsx index 48a6079c58ac0..ac6bf63c37110 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/date_histogram.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/date_histogram.test.tsx @@ -55,6 +55,7 @@ describe('date_histogram', () => { id: '1', title: 'Mock Indexpattern', timeFieldName: 'timestamp', + hasRestrictions: false, fields: [ { name: 'timestamp', @@ -69,6 +70,7 @@ describe('date_histogram', () => { 2: { id: '2', title: 'Mock Indexpattern 2', + hasRestrictions: false, fields: [ { name: 'other_timestamp', @@ -229,13 +231,50 @@ describe('date_histogram', () => { it('should reflect params correctly', () => { const esAggsConfig = dateHistogramOperation.toEsAggsConfig( state.layers.first.columns.col1 as DateHistogramIndexPatternColumn, - 'col1' + 'col1', + state.indexPatterns['1'] ); expect(esAggsConfig).toEqual( expect.objectContaining({ params: expect.objectContaining({ interval: '42w', field: 'timestamp', + useNormalizedEsInterval: true, + }), + }) + ); + }); + + it('should not use normalized es interval for rollups', () => { + const esAggsConfig = dateHistogramOperation.toEsAggsConfig( + state.layers.first.columns.col1 as DateHistogramIndexPatternColumn, + 'col1', + { + ...state.indexPatterns['1'], + fields: [ + { + name: 'timestamp', + displayName: 'timestamp', + aggregatable: true, + searchable: true, + type: 'date', + aggregationRestrictions: { + date_histogram: { + agg: 'date_histogram', + time_zone: 'UTC', + calendar_interval: '42w', + }, + }, + }, + ], + } + ); + expect(esAggsConfig).toEqual( + expect.objectContaining({ + params: expect.objectContaining({ + interval: '42w', + field: 'timestamp', + useNormalizedEsInterval: false, }), }) ); @@ -300,6 +339,7 @@ describe('date_histogram', () => { { title: '', id: '', + hasRestrictions: true, fields: [ { name: 'dateField', @@ -343,6 +383,7 @@ describe('date_histogram', () => { { title: '', id: '', + hasRestrictions: false, fields: [ { name: 'dateField', diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/date_histogram.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/date_histogram.tsx index 2236bc576e2b6..57454291d43c5 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/date_histogram.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/date_histogram.tsx @@ -119,21 +119,24 @@ export const dateHistogramOperation: OperationDefinition ({ - id: columnId, - enabled: true, - type: 'date_histogram', - schema: 'segment', - params: { - field: column.sourceField, - time_zone: column.params.timeZone, - useNormalizedEsInterval: true, - interval: column.params.interval, - drop_partials: false, - min_doc_count: 0, - extended_bounds: {}, - }, - }), + toEsAggsConfig: (column, columnId, indexPattern) => { + const usedField = indexPattern.fields.find((field) => field.name === column.sourceField); + return { + id: columnId, + enabled: true, + type: 'date_histogram', + schema: 'segment', + params: { + field: column.sourceField, + time_zone: column.params.timeZone, + useNormalizedEsInterval: !usedField || !usedField.aggregationRestrictions?.date_histogram, + interval: column.params.interval, + drop_partials: false, + min_doc_count: 0, + extended_bounds: {}, + }, + }; + }, paramEditor: ({ state, setState, currentColumn: currentColumn, layerId, dateRange, data }) => { const field = currentColumn && diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/index.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/index.ts index ef12fca690f0c..8ef53c1e0b425 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/index.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/index.ts @@ -84,7 +84,7 @@ interface BaseOperationDefinitionProps { * Function turning a column into an agg config passed to the `esaggs` function * together with the agg configs returned from other columns. */ - toEsAggsConfig: (column: C, columnId: string) => unknown; + toEsAggsConfig: (column: C, columnId: string, indexPattern: IndexPattern) => unknown; /** * Returns true if the `column` can also be used on `newIndexPattern`. * If this function returns false, the column is removed when switching index pattern diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/metrics.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/metrics.tsx index e6c8a5f6ac852..4c37d95f6b050 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/metrics.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/metrics.tsx @@ -68,7 +68,7 @@ function buildMetricOperation>({ sourceField: field.name, }; }, - toEsAggsConfig: (column, columnId) => ({ + toEsAggsConfig: (column, columnId, _indexPattern) => ({ id: columnId, enabled: true, type: column.operationType, diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms.test.tsx index 05bb2ef673888..2972ed2d0231b 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms.test.tsx @@ -13,7 +13,7 @@ import { dataPluginMock } from '../../../../../../../src/plugins/data/public/moc import { createMockedIndexPattern } from '../../mocks'; import { TermsIndexPatternColumn } from './terms'; import { termsOperation } from './index'; -import { IndexPatternPrivateState } from '../../types'; +import { IndexPatternPrivateState, IndexPattern } from '../../types'; const defaultProps = { storage: {} as IStorageWrapper, @@ -69,7 +69,8 @@ describe('terms', () => { it('should reflect params correctly', () => { const esAggsConfig = termsOperation.toEsAggsConfig( state.layers.first.columns.col1 as TermsIndexPatternColumn, - 'col1' + 'col1', + {} as IndexPattern ); expect(esAggsConfig).toEqual( expect.objectContaining({ diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms.tsx index ac1ff9da2fea0..c1b19fd5549e7 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms.tsx @@ -95,7 +95,7 @@ export const termsOperation: OperationDefinition = { }, }; }, - toEsAggsConfig: (column, columnId) => ({ + toEsAggsConfig: (column, columnId, _indexPattern) => ({ id: columnId, enabled: true, type: 'terms', diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/operations.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/operations.test.ts index 3fce2562f528e..4ac3fc89500f9 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/operations.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/operations.test.ts @@ -16,6 +16,7 @@ const expectedIndexPatterns = { id: '1', title: 'my-fake-index-pattern', timeFieldName: 'timestamp', + hasRestrictions: false, fields: [ { name: 'timestamp', diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/state_helpers.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/state_helpers.test.ts index d7fd0d3661c86..7b6eb11efc494 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/state_helpers.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/state_helpers.test.ts @@ -570,6 +570,7 @@ describe('state_helpers', () => { const indexPattern: IndexPattern = { id: 'test', title: '', + hasRestrictions: true, fields: [ { name: 'fieldA', diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/to_expression.ts b/x-pack/plugins/lens/public/indexpattern_datasource/to_expression.ts index 9473a1523b8ca..1b87c48dc7193 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/to_expression.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/to_expression.ts @@ -21,7 +21,11 @@ function getExpressionForLayer( } function getEsAggsConfig(column: C, columnId: string) { - return operationDefinitionMap[column.operationType].toEsAggsConfig(column, columnId); + return operationDefinitionMap[column.operationType].toEsAggsConfig( + column, + columnId, + indexPattern + ); } const columnEntries = columnOrder.map((colId) => [colId, columns[colId]] as const); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/types.ts b/x-pack/plugins/lens/public/indexpattern_datasource/types.ts index 95cc47e68f8a1..c101f1354b703 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/types.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/types.ts @@ -19,6 +19,7 @@ export interface IndexPattern { params: unknown; } >; + hasRestrictions: boolean; } export interface IndexPatternField { diff --git a/x-pack/test/functional/apps/lens/index.ts b/x-pack/test/functional/apps/lens/index.ts index f2dcf28c01743..d1ecf8fa0973a 100644 --- a/x-pack/test/functional/apps/lens/index.ts +++ b/x-pack/test/functional/apps/lens/index.ts @@ -31,6 +31,9 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./dashboard')); loadTestFile(require.resolve('./persistent_context')); loadTestFile(require.resolve('./lens_reporting')); + + // has to be last one in the suite because it overrides saved objects + loadTestFile(require.resolve('./rollup')); }); }); } diff --git a/x-pack/test/functional/apps/lens/rollup.ts b/x-pack/test/functional/apps/lens/rollup.ts new file mode 100644 index 0000000000000..f6882c8aed214 --- /dev/null +++ b/x-pack/test/functional/apps/lens/rollup.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const PageObjects = getPageObjects(['visualize', 'lens']); + const find = getService('find'); + const listingTable = getService('listingTable'); + const esArchiver = getService('esArchiver'); + + describe('lens rollup tests', () => { + before(async () => { + await esArchiver.loadIfNeeded('lens/rollup/data'); + await esArchiver.loadIfNeeded('lens/rollup/config'); + }); + + after(async () => { + await esArchiver.unload('lens/rollup/data'); + await esArchiver.unload('lens/rollup/config'); + }); + + it('should allow creation of lens xy chart', async () => { + await PageObjects.visualize.navigateToNewVisualization(); + await PageObjects.visualize.clickVisType('lens'); + await PageObjects.lens.goToTimeRange(); + + await PageObjects.lens.configureDimension({ + dimension: 'lnsXY_xDimensionPanel > lns-empty-dimension', + operation: 'date_histogram', + field: '@timestamp', + }); + + await PageObjects.lens.configureDimension({ + dimension: 'lnsXY_yDimensionPanel > lns-empty-dimension', + operation: 'sum', + field: 'bytes', + }); + + await PageObjects.lens.configureDimension({ + dimension: 'lnsXY_splitDimensionPanel > lns-empty-dimension', + operation: 'terms', + field: 'geo.src', + }); + expect(await find.allByCssSelector('.echLegendItem')).to.have.length(2); + + await PageObjects.lens.save('Afancilenstest'); + + // Ensure the visualization shows up in the visualize list, and takes + // us back to the visualization as we configured it. + await PageObjects.visualize.gotoVisualizationLandingPage(); + await listingTable.searchForItemWithName('Afancilenstest'); + await PageObjects.lens.clickVisualizeListItemTitle('Afancilenstest'); + await PageObjects.lens.goToTimeRange(); + + expect(await PageObjects.lens.getTitle()).to.eql('Afancilenstest'); + + // .echLegendItem__title is the only viable way of getting the xy chart's + // legend item(s), so we're using a class selector here. + expect(await find.allByCssSelector('.echLegendItem')).to.have.length(2); + }); + + it('should allow seamless transition to and from table view', async () => { + await PageObjects.lens.switchToVisualization('lnsMetric'); + await PageObjects.lens.assertMetric('Sum of bytes', '16,788'); + await PageObjects.lens.switchToVisualization('lnsDatatable'); + expect(await PageObjects.lens.getDatatableHeaderText()).to.eql('Sum of bytes'); + expect(await PageObjects.lens.getDatatableCellText(0, 0)).to.eql('16,788'); + }); + }); +} diff --git a/x-pack/test/functional/es_archives/lens/rollup/config/data.json b/x-pack/test/functional/es_archives/lens/rollup/config/data.json new file mode 100644 index 0000000000000..268b98542fe11 --- /dev/null +++ b/x-pack/test/functional/es_archives/lens/rollup/config/data.json @@ -0,0 +1,65 @@ +{ + "type": "doc", + "value": { + "id": "space:default", + "index": ".kibana_1", + "source": { + "migrationVersion": { + "space": "6.6.0" + }, + "references": [], + "space": { + "_reserved": true, + "description": "This is the default space!", + "disabledFeatures": [], + "name": "Default" + }, + "type": "space" + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "index-pattern:lens-rolled-up-data", + "index": ".kibana_1", + "source": { + "index-pattern" : { + "title" : "lens_rolled_up_data", + "timeFieldName" : "@timestamp", + "fields" : "[{\"count\":0,\"name\":\"_source\",\"type\":\"_source\",\"esTypes\":[\"_source\"],\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"count\":0,\"name\":\"_id\",\"type\":\"string\",\"esTypes\":[\"_id\"],\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"count\":0,\"name\":\"_type\",\"type\":\"string\",\"esTypes\":[\"_type\"],\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"count\":0,\"name\":\"_index\",\"type\":\"string\",\"esTypes\":[\"_index\"],\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"count\":0,\"name\":\"_score\",\"type\":\"number\",\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"count\":0,\"name\":\"@timestamp\",\"type\":\"date\",\"esTypes\":[\"date\"],\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"count\":0,\"name\":\"bytes\",\"type\":\"number\",\"esTypes\":[\"float\"],\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"count\":0,\"name\":\"geo.src\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true}]", + "type" : "rollup", + "typeMeta" : "{\"params\":{\"rollup_index\":\"lens_rolled_up_data\"},\"aggs\":{\"date_histogram\":{\"@timestamp\":{\"agg\":\"date_histogram\",\"fixed_interval\":\"60m\",\"time_zone\":\"UTC\"}},\"sum\":{\"bytes\":{\"agg\":\"sum\"}},\"max\":{\"bytes\":{\"agg\":\"max\"}},\"terms\":{\"geo.src\":{\"agg\":\"terms\"}}}}" + }, + "type" : "index-pattern", + "references" : [ ], + "migrationVersion" : { + "index-pattern" : "7.6.0" + }, + "updated_at" : "2020-08-19T08:39:09.998Z" + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "config:8.0.0", + "index": ".kibana_1", + "source": { + "config": { + "accessibility:disableAnimations": true, + "buildNum": 9007199254740991, + "dateFormat:tz": "UTC", + "defaultIndex": "logstash-*" + }, + "references": [], + "type": "config", + "updated_at": "2019-09-04T18:47:24.761Z" + }, + "type": "_doc" + } +} diff --git a/x-pack/test/functional/es_archives/lens/rollup/config/mappings.json b/x-pack/test/functional/es_archives/lens/rollup/config/mappings.json new file mode 100644 index 0000000000000..f2a29f022ff5e --- /dev/null +++ b/x-pack/test/functional/es_archives/lens/rollup/config/mappings.json @@ -0,0 +1,1294 @@ +{ + "type": "index", + "value": { + "aliases": { + ".kibana": { + } + }, + "index": ".kibana_1", + "mappings": { + "_meta": { + "migrationMappingPropertyHashes": { + "apm-telemetry": "07ee1939fa4302c62ddc052ec03fed90", + "canvas-element": "7390014e1091044523666d97247392fc", + "canvas-workpad": "b0a1706d356228dbdcb4a17e6b9eb231", + "config": "87aca8fdb053154f11383fce3dbf3edf", + "dashboard": "d00f614b29a80360e1190193fd333bab", + "file-upload-telemetry": "0ed4d3e1983d1217a30982630897092e", + "graph-workspace": "cd7ba1330e6682e9cc00b78850874be1", + "index-pattern": "66eccb05066c5a89924f48a9e9736499", + "infrastructure-ui-source": "ddc0ecb18383f6b26101a2fadb2dab0c", + "inventory-view": "84b320fd67209906333ffce261128462", + "kql-telemetry": "d12a98a6f19a2d273696597547e064ee", + "lens": "21c3ea0763beb1ecb0162529706b88c5", + "map": "23d7aa4a720d4938ccde3983f87bd58d", + "maps-telemetry": "a4229f8b16a6820c6d724b7e0c1f729d", + "metrics-explorer-view": "53c5365793677328df0ccb6138bf3cdd", + "migrationVersion": "4a1746014a75ade3a714e1db5763276f", + "ml-telemetry": "257fd1d4b4fdbb9cb4b8a3b27da201e9", + "namespace": "2f4316de49999235636386fe51dc06c1", + "query": "11aaeb7f5f7fa5bb43f25e18ce26e7d9", + "references": "7997cf5a56cc02bdc9c93361bde732b0", + "sample-data-telemetry": "7d3cfeb915303c9641c59681967ffeb4", + "search": "181661168bbadd1eff5902361e2a0d5c", + "server": "ec97f1c5da1a19609a60874e5af1100c", + "siem-ui-timeline": "1f6f0860ad7bc0dba3e42467ca40470d", + "siem-ui-timeline-note": "8874706eedc49059d4cf0f5094559084", + "siem-ui-timeline-pinned-event": "20638091112f0e14f0e443d512301c29", + "space": "c5ca8acafa0beaa4d08d014a97b6bc6b", + "telemetry": "e1c8bc94e443aefd9458932cc0697a4d", + "timelion-sheet": "9a2a2748877c7a7b582fef201ab1d4cf", + "type": "2f4316de49999235636386fe51dc06c1", + "ui-metric": "0d409297dc5ebe1e3a1da691c6ee32e3", + "updated_at": "00da57df13e94e9d98437d13ace4bfe0", + "upgrade-assistant-reindex-operation": "a53a20fe086b72c9a86da3cc12dad8a6", + "upgrade-assistant-telemetry": "56702cec857e0a9dacfb696655b4ff7b", + "url": "c7f66a0df8b1b52f17c28c4adb111105", + "visualization": "52d7a13ad68a150c4525b292d23e12cc" + } + }, + "dynamic": "strict", + "properties": { + "action": { + "properties": { + "actionTypeId": { + "type": "keyword" + }, + "config": { + "enabled": false, + "type": "object" + }, + "description": { + "type": "text" + }, + "secrets": { + "type": "binary" + } + } + }, + "action_task_params": { + "properties": { + "actionId": { + "type": "keyword" + }, + "apiKey": { + "type": "binary" + }, + "params": { + "enabled": false, + "type": "object" + } + } + }, + "alert": { + "properties": { + "actions": { + "properties": { + "actionRef": { + "type": "keyword" + }, + "group": { + "type": "keyword" + }, + "params": { + "enabled": false, + "type": "object" + } + }, + "type": "nested" + }, + "alertTypeId": { + "type": "keyword" + }, + "params": { + "enabled": false, + "type": "object" + }, + "apiKey": { + "type": "binary" + }, + "apiKeyOwner": { + "type": "keyword" + }, + "createdBy": { + "type": "keyword" + }, + "enabled": { + "type": "boolean" + }, + "interval": { + "type": "keyword" + }, + "scheduledTaskId": { + "type": "keyword" + }, + "updatedBy": { + "type": "keyword" + } + } + }, + "apm-telemetry": { + "properties": { + "has_any_services": { + "type": "boolean" + }, + "services_per_agent": { + "properties": { + "dotnet": { + "null_value": 0, + "type": "long" + }, + "go": { + "null_value": 0, + "type": "long" + }, + "java": { + "null_value": 0, + "type": "long" + }, + "js-base": { + "null_value": 0, + "type": "long" + }, + "nodejs": { + "null_value": 0, + "type": "long" + }, + "python": { + "null_value": 0, + "type": "long" + }, + "ruby": { + "null_value": 0, + "type": "long" + }, + "rum-js": { + "null_value": 0, + "type": "long" + } + } + } + } + }, + "canvas-element": { + "dynamic": "false", + "properties": { + "@created": { + "type": "date" + }, + "@timestamp": { + "type": "date" + }, + "content": { + "type": "text" + }, + "help": { + "type": "text" + }, + "image": { + "type": "text" + }, + "name": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "canvas-workpad": { + "dynamic": "false", + "properties": { + "@created": { + "type": "date" + }, + "@timestamp": { + "type": "date" + }, + "name": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "config": { + "dynamic": "true", + "properties": { + "accessibility:disableAnimations": { + "type": "boolean" + }, + "buildNum": { + "type": "keyword" + }, + "dateFormat:tz": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "defaultIndex": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "dashboard": { + "properties": { + "description": { + "type": "text" + }, + "hits": { + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "optionsJSON": { + "type": "text" + }, + "panelsJSON": { + "type": "text" + }, + "refreshInterval": { + "properties": { + "display": { + "type": "keyword" + }, + "pause": { + "type": "boolean" + }, + "section": { + "type": "integer" + }, + "value": { + "type": "integer" + } + } + }, + "timeFrom": { + "type": "keyword" + }, + "timeRestore": { + "type": "boolean" + }, + "timeTo": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "file-upload-telemetry": { + "properties": { + "filesUploadedTotalCount": { + "type": "long" + } + } + }, + "gis-map": { + "properties": { + "bounds": { + "strategy": "recursive", + "type": "geo_shape" + }, + "description": { + "type": "text" + }, + "layerListJSON": { + "type": "text" + }, + "mapStateJSON": { + "type": "text" + }, + "title": { + "type": "text" + }, + "uiStateJSON": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "graph-workspace": { + "properties": { + "description": { + "type": "text" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "numLinks": { + "type": "integer" + }, + "numVertices": { + "type": "integer" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + }, + "wsState": { + "type": "text" + } + } + }, + "index-pattern": { + "properties": { + "fieldFormatMap": { + "type": "text" + }, + "fields": { + "type": "text" + }, + "intervalName": { + "type": "keyword" + }, + "notExpandable": { + "type": "boolean" + }, + "sourceFilters": { + "type": "text" + }, + "timeFieldName": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "type": { + "type": "keyword" + }, + "typeMeta": { + "type": "keyword" + } + } + }, + "infrastructure-ui-source": { + "properties": { + "description": { + "type": "text" + }, + "fields": { + "properties": { + "container": { + "type": "keyword" + }, + "host": { + "type": "keyword" + }, + "pod": { + "type": "keyword" + }, + "tiebreaker": { + "type": "keyword" + }, + "timestamp": { + "type": "keyword" + } + } + }, + "logAlias": { + "type": "keyword" + }, + "logColumns": { + "properties": { + "fieldColumn": { + "properties": { + "field": { + "type": "keyword" + }, + "id": { + "type": "keyword" + } + } + }, + "messageColumn": { + "properties": { + "id": { + "type": "keyword" + } + } + }, + "timestampColumn": { + "properties": { + "id": { + "type": "keyword" + } + } + } + }, + "type": "nested" + }, + "metricAlias": { + "type": "keyword" + }, + "name": { + "type": "text" + } + } + }, + "inventory-view": { + "properties": { + "autoBounds": { + "type": "boolean" + }, + "autoReload": { + "type": "boolean" + }, + "boundsOverride": { + "properties": { + "max": { + "type": "integer" + }, + "min": { + "type": "integer" + } + } + }, + "customOptions": { + "properties": { + "field": { + "type": "keyword" + }, + "text": { + "type": "keyword" + } + }, + "type": "nested" + }, + "filterQuery": { + "properties": { + "expression": { + "type": "keyword" + }, + "kind": { + "type": "keyword" + } + } + }, + "groupBy": { + "properties": { + "field": { + "type": "keyword" + }, + "label": { + "type": "keyword" + } + }, + "type": "nested" + }, + "metric": { + "properties": { + "type": { + "type": "keyword" + } + } + }, + "name": { + "type": "keyword" + }, + "nodeType": { + "type": "keyword" + }, + "time": { + "type": "integer" + }, + "view": { + "type": "keyword" + } + } + }, + "kql-telemetry": { + "properties": { + "optInCount": { + "type": "long" + }, + "optOutCount": { + "type": "long" + } + } + }, + "lens": { + "properties": { + "expression": { + "index": false, + "type": "keyword" + }, + "state": { + "type": "flattened" + }, + "title": { + "type": "text" + }, + "visualizationType": { + "type": "keyword" + } + } + }, + "map": { + "properties": { + "bounds": { + "type": "geo_shape" + }, + "description": { + "type": "text" + }, + "layerListJSON": { + "type": "text" + }, + "mapStateJSON": { + "type": "text" + }, + "title": { + "type": "text" + }, + "uiStateJSON": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "maps-telemetry": { + "properties": { + "attributesPerMap": { + "properties": { + "dataSourcesCount": { + "properties": { + "avg": { + "type": "long" + }, + "max": { + "type": "long" + }, + "min": { + "type": "long" + } + } + }, + "emsVectorLayersCount": { + "dynamic": "true", + "type": "object" + }, + "layerTypesCount": { + "dynamic": "true", + "type": "object" + }, + "layersCount": { + "properties": { + "avg": { + "type": "long" + }, + "max": { + "type": "long" + }, + "min": { + "type": "long" + } + } + } + } + }, + "mapsTotalCount": { + "type": "long" + }, + "timeCaptured": { + "type": "date" + } + } + }, + "metrics-explorer-view": { + "properties": { + "chartOptions": { + "properties": { + "stack": { + "type": "boolean" + }, + "type": { + "type": "keyword" + }, + "yAxisMode": { + "type": "keyword" + } + } + }, + "currentTimerange": { + "properties": { + "from": { + "type": "keyword" + }, + "interval": { + "type": "keyword" + }, + "to": { + "type": "keyword" + } + } + }, + "name": { + "type": "keyword" + }, + "options": { + "properties": { + "aggregation": { + "type": "keyword" + }, + "filterQuery": { + "type": "keyword" + }, + "groupBy": { + "type": "keyword" + }, + "limit": { + "type": "integer" + }, + "metrics": { + "properties": { + "aggregation": { + "type": "keyword" + }, + "color": { + "type": "keyword" + }, + "field": { + "type": "keyword" + }, + "label": { + "type": "keyword" + } + }, + "type": "nested" + } + } + } + } + }, + "migrationVersion": { + "dynamic": "true", + "properties": { + "index-pattern": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "space": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "visualization": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "ml-telemetry": { + "properties": { + "file_data_visualizer": { + "properties": { + "index_creation_count": { + "type": "long" + } + } + } + } + }, + "namespace": { + "type": "keyword" + }, + "query": { + "properties": { + "description": { + "type": "text" + }, + "filters": { + "enabled": false, + "type": "object" + }, + "query": { + "properties": { + "language": { + "type": "keyword" + }, + "query": { + "index": false, + "type": "keyword" + } + } + }, + "timefilter": { + "enabled": false, + "type": "object" + }, + "title": { + "type": "text" + } + } + }, + "references": { + "properties": { + "id": { + "type": "keyword" + }, + "name": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + }, + "type": "nested" + }, + "sample-data-telemetry": { + "properties": { + "installCount": { + "type": "long" + }, + "unInstallCount": { + "type": "long" + } + } + }, + "search": { + "properties": { + "columns": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "hits": { + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "sort": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "server": { + "properties": { + "uuid": { + "type": "keyword" + } + } + }, + "siem-ui-timeline": { + "properties": { + "columns": { + "properties": { + "aggregatable": { + "type": "boolean" + }, + "category": { + "type": "keyword" + }, + "columnHeaderType": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "example": { + "type": "text" + }, + "id": { + "type": "keyword" + }, + "indexes": { + "type": "keyword" + }, + "name": { + "type": "text" + }, + "placeholder": { + "type": "text" + }, + "searchable": { + "type": "boolean" + }, + "type": { + "type": "keyword" + } + } + }, + "created": { + "type": "date" + }, + "createdBy": { + "type": "text" + }, + "dataProviders": { + "properties": { + "and": { + "properties": { + "enabled": { + "type": "boolean" + }, + "excluded": { + "type": "boolean" + }, + "id": { + "type": "keyword" + }, + "kqlQuery": { + "type": "text" + }, + "name": { + "type": "text" + }, + "queryMatch": { + "properties": { + "displayField": { + "type": "text" + }, + "displayValue": { + "type": "text" + }, + "field": { + "type": "text" + }, + "operator": { + "type": "text" + }, + "value": { + "type": "text" + } + } + } + } + }, + "enabled": { + "type": "boolean" + }, + "excluded": { + "type": "boolean" + }, + "id": { + "type": "keyword" + }, + "kqlQuery": { + "type": "text" + }, + "name": { + "type": "text" + }, + "queryMatch": { + "properties": { + "displayField": { + "type": "text" + }, + "displayValue": { + "type": "text" + }, + "field": { + "type": "text" + }, + "operator": { + "type": "text" + }, + "value": { + "type": "text" + } + } + } + } + }, + "dateRange": { + "properties": { + "end": { + "type": "date" + }, + "start": { + "type": "date" + } + } + }, + "description": { + "type": "text" + }, + "favorite": { + "properties": { + "favoriteDate": { + "type": "date" + }, + "fullName": { + "type": "text" + }, + "keySearch": { + "type": "text" + }, + "userName": { + "type": "text" + } + } + }, + "kqlMode": { + "type": "keyword" + }, + "kqlQuery": { + "properties": { + "filterQuery": { + "properties": { + "kuery": { + "properties": { + "expression": { + "type": "text" + }, + "kind": { + "type": "keyword" + } + } + }, + "serializedQuery": { + "type": "text" + } + } + } + } + }, + "sort": { + "properties": { + "columnId": { + "type": "keyword" + }, + "sortDirection": { + "type": "keyword" + } + } + }, + "title": { + "type": "text" + }, + "updated": { + "type": "date" + }, + "updatedBy": { + "type": "text" + } + } + }, + "siem-ui-timeline-note": { + "properties": { + "created": { + "type": "date" + }, + "createdBy": { + "type": "text" + }, + "eventId": { + "type": "keyword" + }, + "note": { + "type": "text" + }, + "timelineId": { + "type": "keyword" + }, + "updated": { + "type": "date" + }, + "updatedBy": { + "type": "text" + } + } + }, + "siem-ui-timeline-pinned-event": { + "properties": { + "created": { + "type": "date" + }, + "createdBy": { + "type": "text" + }, + "eventId": { + "type": "keyword" + }, + "timelineId": { + "type": "keyword" + }, + "updated": { + "type": "date" + }, + "updatedBy": { + "type": "text" + } + } + }, + "space": { + "properties": { + "_reserved": { + "type": "boolean" + }, + "color": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "disabledFeatures": { + "type": "keyword" + }, + "imageUrl": { + "index": false, + "type": "text" + }, + "initials": { + "type": "keyword" + }, + "name": { + "fields": { + "keyword": { + "ignore_above": 2048, + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "spaceId": { + "type": "keyword" + }, + "telemetry": { + "properties": { + "enabled": { + "type": "boolean" + } + } + }, + "timelion-sheet": { + "properties": { + "description": { + "type": "text" + }, + "hits": { + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "timelion_chart_height": { + "type": "integer" + }, + "timelion_columns": { + "type": "integer" + }, + "timelion_interval": { + "type": "keyword" + }, + "timelion_other_interval": { + "type": "keyword" + }, + "timelion_rows": { + "type": "integer" + }, + "timelion_sheet": { + "type": "text" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "type": { + "type": "keyword" + }, + "ui-metric": { + "properties": { + "count": { + "type": "integer" + } + } + }, + "updated_at": { + "type": "date" + }, + "upgrade-assistant-reindex-operation": { + "dynamic": "true", + "properties": { + "indexName": { + "type": "keyword" + }, + "status": { + "type": "integer" + } + } + }, + "upgrade-assistant-telemetry": { + "properties": { + "features": { + "properties": { + "deprecation_logging": { + "properties": { + "enabled": { + "null_value": true, + "type": "boolean" + } + } + } + } + }, + "ui_open": { + "properties": { + "cluster": { + "null_value": 0, + "type": "long" + }, + "indices": { + "null_value": 0, + "type": "long" + }, + "overview": { + "null_value": 0, + "type": "long" + } + } + }, + "ui_reindex": { + "properties": { + "close": { + "null_value": 0, + "type": "long" + }, + "open": { + "null_value": 0, + "type": "long" + }, + "start": { + "null_value": 0, + "type": "long" + }, + "stop": { + "null_value": 0, + "type": "long" + } + } + } + } + }, + "url": { + "properties": { + "accessCount": { + "type": "long" + }, + "accessDate": { + "type": "date" + }, + "createDate": { + "type": "date" + }, + "url": { + "fields": { + "keyword": { + "ignore_above": 2048, + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "visualization": { + "properties": { + "description": { + "type": "text" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "savedSearchRefName": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "uiStateJSON": { + "type": "text" + }, + "version": { + "type": "integer" + }, + "visState": { + "type": "text" + } + } + } + } + }, + "settings": { + "index": { + "auto_expand_replicas": "0-1", + "number_of_replicas": "0", + "number_of_shards": "1" + } + } + } +} diff --git a/x-pack/test/functional/es_archives/lens/rollup/data/data.json b/x-pack/test/functional/es_archives/lens/rollup/data/data.json new file mode 100644 index 0000000000000..36dc10c05f0b9 --- /dev/null +++ b/x-pack/test/functional/es_archives/lens/rollup/data/data.json @@ -0,0 +1,59 @@ +{ + "type": "doc", + "value": { + "index": "lens_rolled_up_data", + "id": "lens_rolled_up_data$vuSq1a9Ph2Nq-2yfGpE34g", + "source": { + "@timestamp.date_histogram.time_zone": "UTC", + "@timestamp.date_histogram.timestamp": 1442710800000, + "geo.src.terms.value": "CN", + "bytes.max.value": 5678.0, + "_rollup.version": 2, + "bytes.sum.value": 5678.0, + "@timestamp.date_histogram.interval": "60m", + "geo.src.terms._count": 1, + "@timestamp.date_histogram._count": 1, + "_rollup.id": "lens_rolled_up_data" + } + } +} + +{ + "type": "doc", + "value": { + "index": "lens_rolled_up_data", + "id": "lens_rolled_up_data$QFyUWoecErSYPMrIb6CgZA", + "source": { + "@timestamp.date_histogram.time_zone": "UTC", + "@timestamp.date_histogram.timestamp": 1442710800000, + "geo.src.terms.value": "US", + "bytes.max.value": 1234.0, + "_rollup.version": 2, + "bytes.sum.value": 1234.0, + "@timestamp.date_histogram.interval": "60m", + "geo.src.terms._count": 1, + "@timestamp.date_histogram._count": 1, + "_rollup.id": "lens_rolled_up_data" + } + } +} + +{ + "type": "doc", + "value": { + "index": "lens_rolled_up_data", + "id": "lens_rolled_up_data$cKCjv1OPjYiyv5WPPblohw", + "source": { + "@timestamp.date_histogram.time_zone": "UTC", + "@timestamp.date_histogram.timestamp": 1442714400000, + "geo.src.terms.value": "CN", + "bytes.max.value": 9876.0, + "_rollup.version": 2, + "bytes.sum.value": 9876.0, + "@timestamp.date_histogram.interval": "60m", + "geo.src.terms._count": 1, + "@timestamp.date_histogram._count": 1, + "_rollup.id": "lens_rolled_up_data" + } + } +} \ No newline at end of file diff --git a/x-pack/test/functional/es_archives/lens/rollup/data/mappings.json b/x-pack/test/functional/es_archives/lens/rollup/data/mappings.json new file mode 100644 index 0000000000000..0e47a632bbf3f --- /dev/null +++ b/x-pack/test/functional/es_archives/lens/rollup/data/mappings.json @@ -0,0 +1,129 @@ +{ + "type": "index", + "value": { + "index": "lens_rolled_up_data", + "mappings": { + "_meta": { + "_rollup": { + "lens_rolled_up_data": { + "cron": "0 * * * * ?", + "rollup_index": "lens_rolled_up_data", + "groups": { + "date_histogram": { + "fixed_interval": "60m", + "field": "@timestamp", + "time_zone": "UTC" + }, + "terms": { + "fields": ["geo.src", "ip"] + } + }, + "id": "lens_rolled_up_data", + "metrics": [ + { + "field": "bytes", + "metrics": ["sum", "max"] + } + ], + "index_pattern": "lens_raw", + "timeout": "20s", + "page_size": 1000 + } + }, + "rollup-version": "8.0.0" + }, + "dynamic_templates": [ + { + "strings": { + "match_mapping_type": "string", + "mapping": { + "type": "keyword" + } + } + }, + { + "date_histograms": { + "path_match": "*.date_histogram.timestamp", + "mapping": { + "type": "date" + } + } + } + ], + "properties": { + "@timestamp": { + "properties": { + "date_histogram": { + "properties": { + "_count": { + "type": "long" + }, + "interval": { + "type": "keyword" + }, + "time_zone": { + "type": "keyword" + }, + "timestamp": { + "type": "date" + } + } + } + } + }, + "_rollup": { + "properties": { + "id": { + "type": "keyword" + }, + "version": { + "type": "long" + } + } + }, + "bytes": { + "properties": { + "max": { + "properties": { + "value": { + "type": "float" + } + } + }, + "sum": { + "properties": { + "value": { + "type": "float" + } + } + } + } + }, + "geo": { + "properties": { + "src": { + "properties": { + "terms": { + "properties": { + "_count": { + "type": "long" + }, + "value": { + "type": "keyword" + } + } + } + } + } + } + } + } + }, + "settings": { + "index": { + "number_of_shards": "1", + "number_of_replicas": "0" + } + } + } +} From 686cde88afa4c303ff92906ae6c100d130967812 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Fern=C3=A1ndez=20G=C3=B3mez?= Date: Wed, 26 Aug 2020 10:38:54 +0200 Subject: [PATCH 58/71] [Logs UI] View log details for anomaly log examples (#75425) Co-authored-by: Elastic Machine --- .../logging/log_entry_flyout/index.tsx | 2 +- .../log_entry_flyout/log_entry_flyout.tsx | 36 ++-- .../logs/log_entry_rate/page_providers.tsx | 23 ++- .../log_entry_rate/page_results_content.tsx | 183 +++++++++++------- .../sections/anomalies/log_entry_example.tsx | 27 ++- .../pages/logs/stream/page_logs_content.tsx | 21 +- 6 files changed, 184 insertions(+), 108 deletions(-) diff --git a/x-pack/plugins/infra/public/components/logging/log_entry_flyout/index.tsx b/x-pack/plugins/infra/public/components/logging/log_entry_flyout/index.tsx index 521fbf209870c..f11d6cdb8d26d 100644 --- a/x-pack/plugins/infra/public/components/logging/log_entry_flyout/index.tsx +++ b/x-pack/plugins/infra/public/components/logging/log_entry_flyout/index.tsx @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export { LogEntryFlyout } from './log_entry_flyout'; +export * from './log_entry_flyout'; diff --git a/x-pack/plugins/infra/public/components/logging/log_entry_flyout/log_entry_flyout.tsx b/x-pack/plugins/infra/public/components/logging/log_entry_flyout/log_entry_flyout.tsx index 57f27ee76184b..76ffada510e51 100644 --- a/x-pack/plugins/infra/public/components/logging/log_entry_flyout/log_entry_flyout.tsx +++ b/x-pack/plugins/infra/public/components/logging/log_entry_flyout/log_entry_flyout.tsx @@ -26,12 +26,10 @@ import { InfraLoadingPanel } from '../../loading'; import { LogEntryActionsMenu } from './log_entry_actions_menu'; import { LogEntriesItem, LogEntriesItemField } from '../../../../common/http_api'; -interface Props { +export interface LogEntryFlyoutProps { flyoutItem: LogEntriesItem | null; setFlyoutVisibility: (visible: boolean) => void; - setFilter: (filter: string) => void; - setTarget: (timeKey: TimeKey, flyoutItemId: string) => void; - + setFilter: (filter: string, flyoutItemId: string, timeKey?: TimeKey) => void; loading: boolean; } @@ -40,27 +38,27 @@ export const LogEntryFlyout = ({ loading, setFlyoutVisibility, setFilter, - setTarget, -}: Props) => { +}: LogEntryFlyoutProps) => { const createFilterHandler = useCallback( (field: LogEntriesItemField) => () => { + if (!flyoutItem) { + return; + } + const filter = `${field.field}:"${field.value}"`; - setFilter(filter); + const timestampMoment = moment(flyoutItem.key.time); + let target; - if (flyoutItem && flyoutItem.key) { - const timestampMoment = moment(flyoutItem.key.time); - if (timestampMoment.isValid()) { - setTarget( - { - time: timestampMoment.valueOf(), - tiebreaker: flyoutItem.key.tiebreaker, - }, - flyoutItem.id - ); - } + if (timestampMoment.isValid()) { + target = { + time: timestampMoment.valueOf(), + tiebreaker: flyoutItem.key.tiebreaker, + }; } + + setFilter(filter, flyoutItem.id, target); }, - [flyoutItem, setFilter, setTarget] + [flyoutItem, setFilter] ); const closeFlyout = useCallback(() => setFlyoutVisibility(false), [setFlyoutVisibility]); diff --git a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/page_providers.tsx b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/page_providers.tsx index e986fa37c2b2c..4ad654614237d 100644 --- a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/page_providers.tsx +++ b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/page_providers.tsx @@ -10,6 +10,7 @@ import { LogEntryCategoriesModuleProvider } from '../../../containers/logs/log_a import { LogEntryRateModuleProvider } from '../../../containers/logs/log_analysis/modules/log_entry_rate'; import { useLogSourceContext } from '../../../containers/logs/log_source'; import { useActiveKibanaSpace } from '../../../hooks/use_kibana_space'; +import { LogFlyout } from '../../../containers/logs/log_flyout'; export const LogEntryRatePageProviders: React.FunctionComponent = ({ children }) => { const { sourceId, sourceConfiguration } = useLogSourceContext(); @@ -23,20 +24,22 @@ export const LogEntryRatePageProviders: React.FunctionComponent = ({ children }) } return ( - - + - {children} - - + + {children} + + + ); }; diff --git a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/page_results_content.tsx b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/page_results_content.tsx index 65cc4a6c4a704..de72ac5c5a574 100644 --- a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/page_results_content.tsx +++ b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/page_results_content.tsx @@ -7,7 +7,9 @@ import datemath from '@elastic/datemath'; import { EuiFlexGroup, EuiFlexItem, EuiPage, EuiPanel, EuiSuperDatePicker } from '@elastic/eui'; import moment from 'moment'; -import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import { encode, RisonValue } from 'rison-node'; +import { stringify } from 'query-string'; +import React, { useCallback, useEffect, useMemo, useState, useContext } from 'react'; import { euiStyled, useTrackPageview } from '../../../../../observability/public'; import { TimeRange } from '../../../../common/http_api/shared/time_range'; import { bucketSpan } from '../../../../common/log_analysis'; @@ -29,6 +31,9 @@ import { StringTimeRange, useLogAnalysisResultsUrlState, } from './use_log_entry_rate_results_url_state'; +import { LogEntryFlyout, LogEntryFlyoutProps } from '../../../components/logging/log_entry_flyout'; +import { LogFlyout } from '../../../containers/logs/log_flyout'; +import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; export const SORT_DEFAULTS = { direction: 'desc' as const, @@ -42,6 +47,7 @@ export const PAGINATION_DEFAULTS = { export const LogEntryRateResultsContent: React.FunctionComponent = () => { useTrackPageview({ app: 'infra_logs', path: 'log_entry_rate_results' }); useTrackPageview({ app: 'infra_logs', path: 'log_entry_rate_results', delay: 15000 }); + const navigateToApp = useKibana().services.application?.navigateToApp; const { sourceId } = useLogSourceContext(); @@ -79,6 +85,30 @@ export const LogEntryRateResultsContent: React.FunctionComponent = () => { lastChangedTime: Date.now(), })); + const linkToLogStream = useCallback( + (filter, id, timeKey) => { + const params = { + logPosition: encode({ + end: moment(queryTimeRange.value.endTime).format('YYYY-MM-DDTHH:mm:ss.SSSZ'), + position: timeKey as RisonValue, + start: moment(queryTimeRange.value.startTime).format('YYYY-MM-DDTHH:mm:ss.SSSZ'), + streamLive: false, + }), + flyoutOptions: encode({ + surroundingLogsId: id, + }), + logFilter: encode({ + expression: filter, + kind: 'kuery', + }), + }; + + // eslint-disable-next-line no-unused-expressions + navigateToApp?.('logs', { path: `/stream?${stringify(params)}` }); + }, + [queryTimeRange, navigateToApp] + ); + const bucketDuration = useMemo( () => getBucketDuration(queryTimeRange.value.startTime, queryTimeRange.value.endTime), [queryTimeRange.value.endTime, queryTimeRange.value.startTime] @@ -115,6 +145,10 @@ export const LogEntryRateResultsContent: React.FunctionComponent = () => { filteredDatasets: selectedDatasets, }); + const { flyoutVisible, setFlyoutVisibility, flyoutItem, isLoading: isFlyoutLoading } = useContext( + LogFlyout.Context + ); + const handleQueryTimeRangeChange = useCallback( ({ start: startTime, end: endTime }: { start: string; end: string }) => { setQueryTimeRange({ @@ -198,75 +232,86 @@ export const LogEntryRateResultsContent: React.FunctionComponent = () => { ); return ( - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + - - - - + + + + + + + + + + {flyoutVisible ? ( + + ) : null} + ); }; diff --git a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/log_entry_example.tsx b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/log_entry_example.tsx index fece2522de574..a543f95bf4ffb 100644 --- a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/log_entry_example.tsx +++ b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/log_entry_example.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useMemo, useCallback, useState } from 'react'; +import React, { useMemo, useCallback, useState, useContext } from 'react'; import moment from 'moment'; import { encode } from 'rison-node'; import { i18n } from '@kbn/i18n'; @@ -37,6 +37,7 @@ import { } from '../../../../../utils/source_configuration'; import { localizedDate } from '../../../../../../common/formatters/datetime'; import { LogEntryAnomaly } from '../../../../../../common/http_api'; +import { LogFlyout } from '../../../../../containers/logs/log_flyout'; export const exampleMessageScale = 'medium' as const; export const exampleTimestampFormat = 'time' as const; @@ -45,6 +46,13 @@ const MENU_LABEL = i18n.translate('xpack.infra.logAnomalies.logEntryExamplesMenu defaultMessage: 'View actions for log entry', }); +const VIEW_DETAILS_LABEL = i18n.translate( + 'xpack.infra.logs.analysis.logEntryExamplesViewDetailsLabel', + { + defaultMessage: 'View details', + } +); + const VIEW_IN_STREAM_LABEL = i18n.translate( 'xpack.infra.logs.analysis.logEntryExamplesViewInStreamLabel', { @@ -80,6 +88,8 @@ export const LogEntryExampleMessage: React.FunctionComponent = ({ const setItemIsHovered = useCallback(() => setIsHovered(true), []); const setItemIsNotHovered = useCallback(() => setIsHovered(false), []); + const { setFlyoutVisibility, setFlyoutId } = useContext(LogFlyout.Context); + // handle special cases for the dataset value const humanFriendlyDataset = getFriendlyNameForPartitionId(dataset); @@ -116,6 +126,13 @@ export const LogEntryExampleMessage: React.FunctionComponent = ({ } return [ + { + label: VIEW_DETAILS_LABEL, + onClick: () => { + setFlyoutId(id); + setFlyoutVisibility(true); + }, + }, { label: VIEW_IN_STREAM_LABEL, onClick: viewInStreamLinkProps.onClick, @@ -127,7 +144,13 @@ export const LogEntryExampleMessage: React.FunctionComponent = ({ href: viewAnomalyInMachineLearningLinkProps.href, }, ]; - }, [viewInStreamLinkProps, viewAnomalyInMachineLearningLinkProps]); + }, [ + id, + setFlyoutId, + setFlyoutVisibility, + viewInStreamLinkProps, + viewAnomalyInMachineLearningLinkProps, + ]); return ( { const [, { setContextEntry }] = useContext(ViewLogInContext.Context); + const setFilter = useCallback( + (filter, flyoutItemId, timeKey) => { + applyLogFilterQuery(filter); + if (timeKey) { + jumpToTargetPosition(timeKey); + } + setSurroundingLogsId(flyoutItemId); + stopLiveStreaming(); + }, + [applyLogFilterQuery, jumpToTargetPosition, setSurroundingLogsId, stopLiveStreaming] + ); + return ( <> @@ -65,12 +77,7 @@ export const LogsPageLogsContent: React.FunctionComponent = () => { {flyoutVisible ? ( { - jumpToTargetPosition(timeKey); - setSurroundingLogsId(flyoutItemId); - stopLiveStreaming(); - }} + setFilter={setFilter} setFlyoutVisibility={setFlyoutVisibility} flyoutItem={flyoutItem} loading={isLoading} From 4efaba3298f79ceda4586fcb891d84987fc95980 Mon Sep 17 00:00:00 2001 From: Daniil Suleiman <31325372+sulemanof@users.noreply.github.com> Date: Wed, 26 Aug 2020 11:48:27 +0300 Subject: [PATCH 59/71] Reset chrome fields while switching an app (#73064) * Reset chrome help extension while switching an app * Reset other chrome fields * Set docTitle in saved objects app * Add unit tests Co-authored-by: Elastic Machine --- src/core/public/chrome/chrome_service.test.ts | 53 +++++++++++++++++++ src/core/public/chrome/chrome_service.tsx | 8 +++ .../management_section/mount_section.tsx | 7 +++ 3 files changed, 68 insertions(+) diff --git a/src/core/public/chrome/chrome_service.test.ts b/src/core/public/chrome/chrome_service.test.ts index 8dc81dceaccd6..0150554a60906 100644 --- a/src/core/public/chrome/chrome_service.test.ts +++ b/src/core/public/chrome/chrome_service.test.ts @@ -405,6 +405,59 @@ describe('start', () => { `); }); }); + + describe('erase chrome fields', () => { + it('while switching an app', async () => { + const startDeps = defaultStartDeps([new FakeApp('alpha')]); + const { navigateToApp } = startDeps.application; + const { chrome, service } = await start({ startDeps }); + + const helpExtensionPromise = chrome.getHelpExtension$().pipe(toArray()).toPromise(); + const breadcrumbsPromise = chrome.getBreadcrumbs$().pipe(toArray()).toPromise(); + const badgePromise = chrome.getBadge$().pipe(toArray()).toPromise(); + const docTitleResetSpy = jest.spyOn(chrome.docTitle, 'reset'); + + const promises = Promise.all([helpExtensionPromise, breadcrumbsPromise, badgePromise]); + + chrome.setHelpExtension({ appName: 'App name' }); + chrome.setBreadcrumbs([{ text: 'App breadcrumb' }]); + chrome.setBadge({ text: 'App badge', tooltip: 'App tooltip' }); + + navigateToApp('alpha'); + + service.stop(); + + expect(docTitleResetSpy).toBeCalledTimes(1); + await expect(promises).resolves.toMatchInlineSnapshot(` + Array [ + Array [ + undefined, + Object { + "appName": "App name", + }, + undefined, + ], + Array [ + Array [], + Array [ + Object { + "text": "App breadcrumb", + }, + ], + Array [], + ], + Array [ + undefined, + Object { + "text": "App badge", + "tooltip": "App tooltip", + }, + undefined, + ], + ] + `); + }); + }); }); describe('stop', () => { diff --git a/src/core/public/chrome/chrome_service.tsx b/src/core/public/chrome/chrome_service.tsx index d29120e6ee9ac..ef9a682d609ec 100644 --- a/src/core/public/chrome/chrome_service.tsx +++ b/src/core/public/chrome/chrome_service.tsx @@ -157,6 +157,14 @@ export class ChromeService { const recentlyAccessed = await this.recentlyAccessed.start({ http }); const docTitle = this.docTitle.start({ document: window.document }); + // erase chrome fields from a previous app while switching to a next app + application.currentAppId$.subscribe(() => { + helpExtension$.next(undefined); + breadcrumbs$.next([]); + badge$.next(undefined); + docTitle.reset(); + }); + const setIsNavDrawerLocked = (isLocked: boolean) => { isNavDrawerLocked$.next(isLocked); localStorage.setItem(IS_LOCKED_KEY, `${isLocked}`); diff --git a/src/plugins/saved_objects_management/public/management_section/mount_section.tsx b/src/plugins/saved_objects_management/public/management_section/mount_section.tsx index 9cfe99fd3bbf8..4339c2fa13c0f 100644 --- a/src/plugins/saved_objects_management/public/management_section/mount_section.tsx +++ b/src/plugins/saved_objects_management/public/management_section/mount_section.tsx @@ -21,6 +21,7 @@ import React, { lazy, Suspense } from 'react'; import ReactDOM from 'react-dom'; import { Router, Switch, Route } from 'react-router-dom'; import { I18nProvider } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; import { EuiLoadingSpinner } from '@elastic/eui'; import { CoreSetup } from 'src/core/public'; import { ManagementAppMountParams } from '../../../management/public'; @@ -36,6 +37,10 @@ interface MountParams { let allowedObjectTypes: string[] | undefined; +const title = i18n.translate('savedObjectsManagement.objects.savedObjectsTitle', { + defaultMessage: 'Saved Objects', +}); + const SavedObjectsEditionPage = lazy(() => import('./saved_objects_edition_page')); const SavedObjectsTablePage = lazy(() => import('./saved_objects_table_page')); export const mountManagementSection = async ({ @@ -49,6 +54,8 @@ export const mountManagementSection = async ({ allowedObjectTypes = await getAllowedTypes(coreStart.http); } + coreStart.chrome.docTitle.change(title); + const capabilities = coreStart.application.capabilities; const RedirectToHomeIfUnauthorized: React.FunctionComponent = ({ children }) => { From 789b67fb5f6e6293dba812bfef3f08333f4a798e Mon Sep 17 00:00:00 2001 From: Dario Gieselaar Date: Wed, 26 Aug 2020 10:59:44 +0200 Subject: [PATCH 60/71] [APM] Improvements for breakdown data gaps (#75534) Closes #69704, #73387, #43780. --- .../shared/charts/CustomPlot/StaticPlot.js | 43 +- .../__snapshots__/CustomPlot.test.js.snap | 372 +++--- .../ErroneousTransactionsRateChart/index.tsx | 2 +- .../plugins/apm/public/utils/testHelpers.tsx | 15 +- x-pack/plugins/apm/server/index.ts | 2 + .../plugins/apm/server/lib/helpers/metrics.ts | 10 +- .../java/gc/fetch_and_transform_gc_metrics.ts | 8 +- .../metrics/fetch_and_transform_metrics.ts | 8 +- .../lib/transaction_groups/get_error_rate.ts | 9 +- .../lib/transactions/breakdown/index.ts | 8 +- .../expectation/error_rate.json | 1010 ++++++++++++++++- 11 files changed, 1242 insertions(+), 245 deletions(-) diff --git a/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/StaticPlot.js b/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/StaticPlot.js index d489970b55f29..e49899da85e0d 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/StaticPlot.js +++ b/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/StaticPlot.js @@ -71,11 +71,29 @@ class StaticPlot extends PureComponent { const data = serie.data.map((value) => { return 'y' in value && isValidCoordinateValue(value.y) ? value - : { - ...value, - y: undefined, - }; + : { ...value, y: undefined }; }); + + // make sure individual markers are displayed in cases + // where there are gaps + + const markersForGaps = serie.data.map((value, index) => { + const prevHasData = getNull(serie.data[index - 1] ?? {}); + const nextHasData = getNull(serie.data[index + 1] ?? {}); + const thisHasData = getNull(value); + + const isGap = !prevHasData && !nextHasData && thisHasData; + + if (!isGap) { + return { + ...value, + y: undefined, + }; + } + + return value; + }); + return [ , + , ]; } @@ -132,7 +165,7 @@ class StaticPlot extends PureComponent { curve={'curveMonotoneX'} data={serie.data} color={serie.color} - size={0.5} + size={1} /> ); default: diff --git a/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/test/__snapshots__/CustomPlot.test.js.snap b/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/test/__snapshots__/CustomPlot.test.js.snap index 8101b01a83b08..f413610ebd984 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/test/__snapshots__/CustomPlot.test.js.snap +++ b/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/test/__snapshots__/CustomPlot.test.js.snap @@ -460,7 +460,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#da8b45", @@ -477,7 +477,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#da8b45", @@ -494,7 +494,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#da8b45", @@ -511,7 +511,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#da8b45", @@ -528,7 +528,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#da8b45", @@ -545,7 +545,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#da8b45", @@ -562,7 +562,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#da8b45", @@ -579,7 +579,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#da8b45", @@ -596,7 +596,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#da8b45", @@ -613,7 +613,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#da8b45", @@ -630,7 +630,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#da8b45", @@ -647,7 +647,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#da8b45", @@ -664,7 +664,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#da8b45", @@ -681,7 +681,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#da8b45", @@ -698,7 +698,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#da8b45", @@ -715,7 +715,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#da8b45", @@ -732,7 +732,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#da8b45", @@ -749,7 +749,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#da8b45", @@ -766,7 +766,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#da8b45", @@ -783,7 +783,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#da8b45", @@ -800,7 +800,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#da8b45", @@ -817,7 +817,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#da8b45", @@ -834,7 +834,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#da8b45", @@ -851,7 +851,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#da8b45", @@ -868,7 +868,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#da8b45", @@ -885,7 +885,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#da8b45", @@ -902,7 +902,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#da8b45", @@ -919,7 +919,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#da8b45", @@ -936,7 +936,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#da8b45", @@ -953,7 +953,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#da8b45", @@ -970,7 +970,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#da8b45", @@ -1013,7 +1013,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#d6bf57", @@ -1030,7 +1030,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#d6bf57", @@ -1047,7 +1047,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#d6bf57", @@ -1064,7 +1064,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#d6bf57", @@ -1081,7 +1081,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#d6bf57", @@ -1098,7 +1098,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#d6bf57", @@ -1115,7 +1115,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#d6bf57", @@ -1132,7 +1132,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#d6bf57", @@ -1149,7 +1149,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#d6bf57", @@ -1166,7 +1166,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#d6bf57", @@ -1183,7 +1183,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#d6bf57", @@ -1200,7 +1200,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#d6bf57", @@ -1217,7 +1217,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#d6bf57", @@ -1234,7 +1234,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#d6bf57", @@ -1251,7 +1251,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#d6bf57", @@ -1268,7 +1268,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#d6bf57", @@ -1285,7 +1285,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#d6bf57", @@ -1302,7 +1302,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#d6bf57", @@ -1319,7 +1319,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#d6bf57", @@ -1336,7 +1336,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#d6bf57", @@ -1353,7 +1353,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#d6bf57", @@ -1370,7 +1370,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#d6bf57", @@ -1387,7 +1387,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#d6bf57", @@ -1404,7 +1404,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#d6bf57", @@ -1421,7 +1421,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#d6bf57", @@ -1438,7 +1438,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#d6bf57", @@ -1455,7 +1455,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#d6bf57", @@ -1472,7 +1472,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#d6bf57", @@ -1489,7 +1489,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#d6bf57", @@ -1506,7 +1506,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#d6bf57", @@ -1523,7 +1523,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#d6bf57", @@ -1566,7 +1566,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#6092c0", @@ -1583,7 +1583,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#6092c0", @@ -1600,7 +1600,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#6092c0", @@ -1617,7 +1617,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#6092c0", @@ -1634,7 +1634,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#6092c0", @@ -1651,7 +1651,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#6092c0", @@ -1668,7 +1668,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#6092c0", @@ -1685,7 +1685,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#6092c0", @@ -1702,7 +1702,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#6092c0", @@ -1719,7 +1719,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#6092c0", @@ -1736,7 +1736,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#6092c0", @@ -1753,7 +1753,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#6092c0", @@ -1770,7 +1770,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#6092c0", @@ -1787,7 +1787,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#6092c0", @@ -1804,7 +1804,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#6092c0", @@ -1821,7 +1821,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#6092c0", @@ -1838,7 +1838,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#6092c0", @@ -1855,7 +1855,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#6092c0", @@ -1872,7 +1872,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#6092c0", @@ -1889,7 +1889,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#6092c0", @@ -1906,7 +1906,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#6092c0", @@ -1923,7 +1923,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#6092c0", @@ -1940,7 +1940,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#6092c0", @@ -1957,7 +1957,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#6092c0", @@ -1974,7 +1974,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#6092c0", @@ -1991,7 +1991,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#6092c0", @@ -2008,7 +2008,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#6092c0", @@ -2025,7 +2025,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#6092c0", @@ -2042,7 +2042,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#6092c0", @@ -2059,7 +2059,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#6092c0", @@ -2076,7 +2076,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#6092c0", @@ -3396,7 +3396,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#da8b45", @@ -3413,7 +3413,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#da8b45", @@ -3430,7 +3430,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#da8b45", @@ -3447,7 +3447,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#da8b45", @@ -3464,7 +3464,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#da8b45", @@ -3481,7 +3481,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#da8b45", @@ -3498,7 +3498,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#da8b45", @@ -3515,7 +3515,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#da8b45", @@ -3532,7 +3532,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#da8b45", @@ -3549,7 +3549,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#da8b45", @@ -3566,7 +3566,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#da8b45", @@ -3583,7 +3583,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#da8b45", @@ -3600,7 +3600,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#da8b45", @@ -3617,7 +3617,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#da8b45", @@ -3634,7 +3634,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#da8b45", @@ -3651,7 +3651,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#da8b45", @@ -3668,7 +3668,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#da8b45", @@ -3685,7 +3685,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#da8b45", @@ -3702,7 +3702,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#da8b45", @@ -3719,7 +3719,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#da8b45", @@ -3736,7 +3736,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#da8b45", @@ -3753,7 +3753,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#da8b45", @@ -3770,7 +3770,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#da8b45", @@ -3787,7 +3787,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#da8b45", @@ -3804,7 +3804,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#da8b45", @@ -3821,7 +3821,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#da8b45", @@ -3838,7 +3838,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#da8b45", @@ -3855,7 +3855,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#da8b45", @@ -3872,7 +3872,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#da8b45", @@ -3889,7 +3889,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#da8b45", @@ -3906,7 +3906,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#da8b45", @@ -3949,7 +3949,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#d6bf57", @@ -3966,7 +3966,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#d6bf57", @@ -3983,7 +3983,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#d6bf57", @@ -4000,7 +4000,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#d6bf57", @@ -4017,7 +4017,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#d6bf57", @@ -4034,7 +4034,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#d6bf57", @@ -4051,7 +4051,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#d6bf57", @@ -4068,7 +4068,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#d6bf57", @@ -4085,7 +4085,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#d6bf57", @@ -4102,7 +4102,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#d6bf57", @@ -4119,7 +4119,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#d6bf57", @@ -4136,7 +4136,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#d6bf57", @@ -4153,7 +4153,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#d6bf57", @@ -4170,7 +4170,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#d6bf57", @@ -4187,7 +4187,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#d6bf57", @@ -4204,7 +4204,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#d6bf57", @@ -4221,7 +4221,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#d6bf57", @@ -4238,7 +4238,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#d6bf57", @@ -4255,7 +4255,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#d6bf57", @@ -4272,7 +4272,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#d6bf57", @@ -4289,7 +4289,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#d6bf57", @@ -4306,7 +4306,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#d6bf57", @@ -4323,7 +4323,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#d6bf57", @@ -4340,7 +4340,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#d6bf57", @@ -4357,7 +4357,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#d6bf57", @@ -4374,7 +4374,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#d6bf57", @@ -4391,7 +4391,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#d6bf57", @@ -4408,7 +4408,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#d6bf57", @@ -4425,7 +4425,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#d6bf57", @@ -4442,7 +4442,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#d6bf57", @@ -4459,7 +4459,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#d6bf57", @@ -4502,7 +4502,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#6092c0", @@ -4519,7 +4519,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#6092c0", @@ -4536,7 +4536,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#6092c0", @@ -4553,7 +4553,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#6092c0", @@ -4570,7 +4570,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#6092c0", @@ -4587,7 +4587,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#6092c0", @@ -4604,7 +4604,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#6092c0", @@ -4621,7 +4621,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#6092c0", @@ -4638,7 +4638,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#6092c0", @@ -4655,7 +4655,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#6092c0", @@ -4672,7 +4672,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#6092c0", @@ -4689,7 +4689,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#6092c0", @@ -4706,7 +4706,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#6092c0", @@ -4723,7 +4723,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#6092c0", @@ -4740,7 +4740,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#6092c0", @@ -4757,7 +4757,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#6092c0", @@ -4774,7 +4774,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#6092c0", @@ -4791,7 +4791,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#6092c0", @@ -4808,7 +4808,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#6092c0", @@ -4825,7 +4825,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#6092c0", @@ -4842,7 +4842,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#6092c0", @@ -4859,7 +4859,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#6092c0", @@ -4876,7 +4876,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#6092c0", @@ -4893,7 +4893,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#6092c0", @@ -4910,7 +4910,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#6092c0", @@ -4927,7 +4927,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#6092c0", @@ -4944,7 +4944,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#6092c0", @@ -4961,7 +4961,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#6092c0", @@ -4978,7 +4978,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#6092c0", @@ -4995,7 +4995,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#6092c0", @@ -5012,7 +5012,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#6092c0", diff --git a/x-pack/plugins/apm/public/components/shared/charts/ErroneousTransactionsRateChart/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/ErroneousTransactionsRateChart/index.tsx index 8214c081e6ce1..3b6d1684e08e1 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/ErroneousTransactionsRateChart/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/ErroneousTransactionsRateChart/index.tsx @@ -88,7 +88,7 @@ export function ErroneousTransactionsRateChart() { }, { data: errorRates, - type: 'line', + type: 'linemark', color: theme.euiColorVis7, hideLegend: true, title: i18n.translate('xpack.apm.errorRateChart.rateLabel', { diff --git a/x-pack/plugins/apm/public/utils/testHelpers.tsx b/x-pack/plugins/apm/public/utils/testHelpers.tsx index 217e6a30a33b4..a750a9ea7af67 100644 --- a/x-pack/plugins/apm/public/utils/testHelpers.tsx +++ b/x-pack/plugins/apm/public/utils/testHelpers.tsx @@ -151,7 +151,20 @@ export async function inspectSearchParams( end: 1528977600000, apmEventClient: { search: spy } as any, internalClient: { search: spy } as any, - config: new Proxy({}, { get: () => 'myIndex' }) as APMConfig, + config: new Proxy( + {}, + { + get: (_, key) => { + switch (key) { + default: + return 'myIndex'; + + case 'xpack.apm.metricsInterval': + return 30; + } + }, + } + ) as APMConfig, uiFiltersES: [{ term: { 'my.custom.ui.filter': 'foo-bar' } }], indices: { /* eslint-disable @typescript-eslint/naming-convention */ diff --git a/x-pack/plugins/apm/server/index.ts b/x-pack/plugins/apm/server/index.ts index fa4b8b821f9f8..29b2a77df348e 100644 --- a/x-pack/plugins/apm/server/index.ts +++ b/x-pack/plugins/apm/server/index.ts @@ -31,6 +31,7 @@ export const config = { maxTraceItems: schema.number({ defaultValue: 1000 }), }), telemetryCollectionEnabled: schema.boolean({ defaultValue: true }), + metricsInterval: schema.number({ defaultValue: 30 }), }), }; @@ -68,6 +69,7 @@ export function mergeConfigs( 'xpack.apm.autocreateApmIndexPattern': apmConfig.autocreateApmIndexPattern, 'xpack.apm.telemetryCollectionEnabled': apmConfig.telemetryCollectionEnabled, + 'xpack.apm.metricsInterval': apmConfig.metricsInterval, }; } diff --git a/x-pack/plugins/apm/server/lib/helpers/metrics.ts b/x-pack/plugins/apm/server/lib/helpers/metrics.ts index c57769e9e15da..9f5b5cdf47552 100644 --- a/x-pack/plugins/apm/server/lib/helpers/metrics.ts +++ b/x-pack/plugins/apm/server/lib/helpers/metrics.ts @@ -6,13 +6,17 @@ import { getBucketSize } from './get_bucket_size'; -export function getMetricsDateHistogramParams(start: number, end: number) { +export function getMetricsDateHistogramParams( + start: number, + end: number, + metricsInterval: number +) { const { bucketSize } = getBucketSize(start, end, 'auto'); return { field: '@timestamp', - // ensure minimum bucket size of 30s since this is the default resolution for metric data - fixed_interval: `${Math.max(bucketSize, 30)}s`, + // ensure minimum bucket size of configured interval since this is the default resolution for metric data + fixed_interval: `${Math.max(bucketSize, metricsInterval)}s`, min_doc_count: 0, extended_bounds: { min: start, max: end }, diff --git a/x-pack/plugins/apm/server/lib/metrics/by_agent/java/gc/fetch_and_transform_gc_metrics.ts b/x-pack/plugins/apm/server/lib/metrics/by_agent/java/gc/fetch_and_transform_gc_metrics.ts index e5c573ba1ec02..551384da2cca7 100644 --- a/x-pack/plugins/apm/server/lib/metrics/by_agent/java/gc/fetch_and_transform_gc_metrics.ts +++ b/x-pack/plugins/apm/server/lib/metrics/by_agent/java/gc/fetch_and_transform_gc_metrics.ts @@ -42,7 +42,7 @@ export async function fetchAndTransformGcMetrics({ chartBase: ChartBase; fieldName: typeof METRIC_JAVA_GC_COUNT | typeof METRIC_JAVA_GC_TIME; }) { - const { start, end, apmEventClient } = setup; + const { start, end, apmEventClient, config } = setup; const { bucketSize } = getBucketSize(start, end, 'auto'); @@ -75,7 +75,11 @@ export async function fetchAndTransformGcMetrics({ }, aggs: { over_time: { - date_histogram: getMetricsDateHistogramParams(start, end), + date_histogram: getMetricsDateHistogramParams( + start, + end, + config['xpack.apm.metricsInterval'] + ), aggs: { // get the max value max: { diff --git a/x-pack/plugins/apm/server/lib/metrics/fetch_and_transform_metrics.ts b/x-pack/plugins/apm/server/lib/metrics/fetch_and_transform_metrics.ts index f6e201b395c37..a42a10d6518a0 100644 --- a/x-pack/plugins/apm/server/lib/metrics/fetch_and_transform_metrics.ts +++ b/x-pack/plugins/apm/server/lib/metrics/fetch_and_transform_metrics.ts @@ -65,7 +65,7 @@ export async function fetchAndTransformMetrics({ aggs: T; additionalFilters?: Filter[]; }) { - const { start, end, apmEventClient } = setup; + const { start, end, apmEventClient, config } = setup; const projection = getMetricsProjection({ setup, @@ -83,7 +83,11 @@ export async function fetchAndTransformMetrics({ }, aggs: { timeseriesData: { - date_histogram: getMetricsDateHistogramParams(start, end), + date_histogram: getMetricsDateHistogramParams( + start, + end, + config['xpack.apm.metricsInterval'] + ), aggs, }, ...aggs, diff --git a/x-pack/plugins/apm/server/lib/transaction_groups/get_error_rate.ts b/x-pack/plugins/apm/server/lib/transaction_groups/get_error_rate.ts index d4e0bd1d54da1..ec2d8144cf3ff 100644 --- a/x-pack/plugins/apm/server/lib/transaction_groups/get_error_rate.ts +++ b/x-pack/plugins/apm/server/lib/transaction_groups/get_error_rate.ts @@ -12,12 +12,12 @@ import { } from '../../../common/elasticsearch_fieldnames'; import { ProcessorEvent } from '../../../common/processor_event'; import { rangeFilter } from '../../../common/utils/range_filter'; -import { getMetricsDateHistogramParams } from '../helpers/metrics'; import { Setup, SetupTimeRange, SetupUIFilters, } from '../helpers/setup_request'; +import { getBucketSize } from '../helpers/get_bucket_size'; export async function getErrorRate({ serviceName, @@ -57,7 +57,12 @@ export async function getErrorRate({ query: { bool: { filter } }, aggs: { total_transactions: { - date_histogram: getMetricsDateHistogramParams(start, end), + date_histogram: { + field: '@timestamp', + fixed_interval: getBucketSize(start, end, 'auto').intervalString, + min_doc_count: 0, + extended_bounds: { min: start, max: end }, + }, aggs: { erroneous_transactions: { filter: { range: { [HTTP_RESPONSE_STATUS_CODE]: { gte: 400 } } }, diff --git a/x-pack/plugins/apm/server/lib/transactions/breakdown/index.ts b/x-pack/plugins/apm/server/lib/transactions/breakdown/index.ts index 7248399d1f93f..fbdddea32deb4 100644 --- a/x-pack/plugins/apm/server/lib/transactions/breakdown/index.ts +++ b/x-pack/plugins/apm/server/lib/transactions/breakdown/index.ts @@ -36,7 +36,7 @@ export async function getTransactionBreakdown({ transactionName?: string; transactionType: string; }) { - const { uiFiltersES, apmEventClient, start, end } = setup; + const { uiFiltersES, apmEventClient, start, end, config } = setup; const subAggs = { sum_all_self_times: { @@ -104,7 +104,11 @@ export async function getTransactionBreakdown({ aggs: { ...subAggs, by_date: { - date_histogram: getMetricsDateHistogramParams(start, end), + date_histogram: getMetricsDateHistogramParams( + start, + end, + config['xpack.apm.metricsInterval'] + ), aggs: subAggs, }, }, diff --git a/x-pack/test/apm_api_integration/basic/tests/transaction_groups/expectation/error_rate.json b/x-pack/test/apm_api_integration/basic/tests/transaction_groups/expectation/error_rate.json index 9ff45ebdbb21b..e448729f44a98 100644 --- a/x-pack/test/apm_api_integration/basic/tests/transaction_groups/expectation/error_rate.json +++ b/x-pack/test/apm_api_integration/basic/tests/transaction_groups/expectation/error_rate.json @@ -1,42 +1,970 @@ { - "noHits":false, - "erroneousTransactionsRate":[ - { - "x":1593413100000, - "y":null - }, - { - "x":1593413130000, - "y":null - }, - { - "x":1593413160000, - "y":null - }, - { - "x":1593413190000, - "y":null - }, - { - "x":1593413220000, - "y":null - }, - { - "x":1593413250000, - "y":0 - }, - { - "x":1593413280000, - "y":0.14102564102564102 - }, - { - "x":1593413310000, - "y":0.14634146341463414 - }, - { - "x":1593413340000, - "y":null - } - ], - "average":0.09578903481342504 -} \ No newline at end of file + "noHits": false, + "erroneousTransactionsRate": [ + { + "x": 1593413100000, + "y": null + }, + { + "x": 1593413101000, + "y": null + }, + { + "x": 1593413102000, + "y": null + }, + { + "x": 1593413103000, + "y": null + }, + { + "x": 1593413104000, + "y": null + }, + { + "x": 1593413105000, + "y": null + }, + { + "x": 1593413106000, + "y": null + }, + { + "x": 1593413107000, + "y": null + }, + { + "x": 1593413108000, + "y": null + }, + { + "x": 1593413109000, + "y": null + }, + { + "x": 1593413110000, + "y": null + }, + { + "x": 1593413111000, + "y": null + }, + { + "x": 1593413112000, + "y": null + }, + { + "x": 1593413113000, + "y": null + }, + { + "x": 1593413114000, + "y": null + }, + { + "x": 1593413115000, + "y": null + }, + { + "x": 1593413116000, + "y": null + }, + { + "x": 1593413117000, + "y": null + }, + { + "x": 1593413118000, + "y": null + }, + { + "x": 1593413119000, + "y": null + }, + { + "x": 1593413120000, + "y": null + }, + { + "x": 1593413121000, + "y": null + }, + { + "x": 1593413122000, + "y": null + }, + { + "x": 1593413123000, + "y": null + }, + { + "x": 1593413124000, + "y": null + }, + { + "x": 1593413125000, + "y": null + }, + { + "x": 1593413126000, + "y": null + }, + { + "x": 1593413127000, + "y": null + }, + { + "x": 1593413128000, + "y": null + }, + { + "x": 1593413129000, + "y": null + }, + { + "x": 1593413130000, + "y": null + }, + { + "x": 1593413131000, + "y": null + }, + { + "x": 1593413132000, + "y": null + }, + { + "x": 1593413133000, + "y": null + }, + { + "x": 1593413134000, + "y": null + }, + { + "x": 1593413135000, + "y": null + }, + { + "x": 1593413136000, + "y": null + }, + { + "x": 1593413137000, + "y": null + }, + { + "x": 1593413138000, + "y": null + }, + { + "x": 1593413139000, + "y": null + }, + { + "x": 1593413140000, + "y": null + }, + { + "x": 1593413141000, + "y": null + }, + { + "x": 1593413142000, + "y": null + }, + { + "x": 1593413143000, + "y": null + }, + { + "x": 1593413144000, + "y": null + }, + { + "x": 1593413145000, + "y": null + }, + { + "x": 1593413146000, + "y": null + }, + { + "x": 1593413147000, + "y": null + }, + { + "x": 1593413148000, + "y": null + }, + { + "x": 1593413149000, + "y": null + }, + { + "x": 1593413150000, + "y": null + }, + { + "x": 1593413151000, + "y": null + }, + { + "x": 1593413152000, + "y": null + }, + { + "x": 1593413153000, + "y": null + }, + { + "x": 1593413154000, + "y": null + }, + { + "x": 1593413155000, + "y": null + }, + { + "x": 1593413156000, + "y": null + }, + { + "x": 1593413157000, + "y": null + }, + { + "x": 1593413158000, + "y": null + }, + { + "x": 1593413159000, + "y": null + }, + { + "x": 1593413160000, + "y": null + }, + { + "x": 1593413161000, + "y": null + }, + { + "x": 1593413162000, + "y": null + }, + { + "x": 1593413163000, + "y": null + }, + { + "x": 1593413164000, + "y": null + }, + { + "x": 1593413165000, + "y": null + }, + { + "x": 1593413166000, + "y": null + }, + { + "x": 1593413167000, + "y": null + }, + { + "x": 1593413168000, + "y": null + }, + { + "x": 1593413169000, + "y": null + }, + { + "x": 1593413170000, + "y": null + }, + { + "x": 1593413171000, + "y": null + }, + { + "x": 1593413172000, + "y": null + }, + { + "x": 1593413173000, + "y": null + }, + { + "x": 1593413174000, + "y": null + }, + { + "x": 1593413175000, + "y": null + }, + { + "x": 1593413176000, + "y": null + }, + { + "x": 1593413177000, + "y": null + }, + { + "x": 1593413178000, + "y": null + }, + { + "x": 1593413179000, + "y": null + }, + { + "x": 1593413180000, + "y": null + }, + { + "x": 1593413181000, + "y": null + }, + { + "x": 1593413182000, + "y": null + }, + { + "x": 1593413183000, + "y": null + }, + { + "x": 1593413184000, + "y": null + }, + { + "x": 1593413185000, + "y": null + }, + { + "x": 1593413186000, + "y": null + }, + { + "x": 1593413187000, + "y": null + }, + { + "x": 1593413188000, + "y": null + }, + { + "x": 1593413189000, + "y": null + }, + { + "x": 1593413190000, + "y": null + }, + { + "x": 1593413191000, + "y": null + }, + { + "x": 1593413192000, + "y": null + }, + { + "x": 1593413193000, + "y": null + }, + { + "x": 1593413194000, + "y": null + }, + { + "x": 1593413195000, + "y": null + }, + { + "x": 1593413196000, + "y": null + }, + { + "x": 1593413197000, + "y": null + }, + { + "x": 1593413198000, + "y": null + }, + { + "x": 1593413199000, + "y": null + }, + { + "x": 1593413200000, + "y": null + }, + { + "x": 1593413201000, + "y": null + }, + { + "x": 1593413202000, + "y": null + }, + { + "x": 1593413203000, + "y": null + }, + { + "x": 1593413204000, + "y": null + }, + { + "x": 1593413205000, + "y": null + }, + { + "x": 1593413206000, + "y": null + }, + { + "x": 1593413207000, + "y": null + }, + { + "x": 1593413208000, + "y": null + }, + { + "x": 1593413209000, + "y": null + }, + { + "x": 1593413210000, + "y": null + }, + { + "x": 1593413211000, + "y": null + }, + { + "x": 1593413212000, + "y": null + }, + { + "x": 1593413213000, + "y": null + }, + { + "x": 1593413214000, + "y": null + }, + { + "x": 1593413215000, + "y": null + }, + { + "x": 1593413216000, + "y": null + }, + { + "x": 1593413217000, + "y": null + }, + { + "x": 1593413218000, + "y": null + }, + { + "x": 1593413219000, + "y": null + }, + { + "x": 1593413220000, + "y": null + }, + { + "x": 1593413221000, + "y": null + }, + { + "x": 1593413222000, + "y": null + }, + { + "x": 1593413223000, + "y": null + }, + { + "x": 1593413224000, + "y": null + }, + { + "x": 1593413225000, + "y": null + }, + { + "x": 1593413226000, + "y": null + }, + { + "x": 1593413227000, + "y": null + }, + { + "x": 1593413228000, + "y": null + }, + { + "x": 1593413229000, + "y": null + }, + { + "x": 1593413230000, + "y": null + }, + { + "x": 1593413231000, + "y": null + }, + { + "x": 1593413232000, + "y": null + }, + { + "x": 1593413233000, + "y": null + }, + { + "x": 1593413234000, + "y": null + }, + { + "x": 1593413235000, + "y": null + }, + { + "x": 1593413236000, + "y": null + }, + { + "x": 1593413237000, + "y": null + }, + { + "x": 1593413238000, + "y": null + }, + { + "x": 1593413239000, + "y": null + }, + { + "x": 1593413240000, + "y": null + }, + { + "x": 1593413241000, + "y": null + }, + { + "x": 1593413242000, + "y": null + }, + { + "x": 1593413243000, + "y": null + }, + { + "x": 1593413244000, + "y": null + }, + { + "x": 1593413245000, + "y": null + }, + { + "x": 1593413246000, + "y": null + }, + { + "x": 1593413247000, + "y": null + }, + { + "x": 1593413248000, + "y": null + }, + { + "x": 1593413249000, + "y": null + }, + { + "x": 1593413250000, + "y": null + }, + { + "x": 1593413251000, + "y": null + }, + { + "x": 1593413252000, + "y": null + }, + { + "x": 1593413253000, + "y": null + }, + { + "x": 1593413254000, + "y": null + }, + { + "x": 1593413255000, + "y": null + }, + { + "x": 1593413256000, + "y": null + }, + { + "x": 1593413257000, + "y": null + }, + { + "x": 1593413258000, + "y": null + }, + { + "x": 1593413259000, + "y": null + }, + { + "x": 1593413260000, + "y": null + }, + { + "x": 1593413261000, + "y": null + }, + { + "x": 1593413262000, + "y": null + }, + { + "x": 1593413263000, + "y": null + }, + { + "x": 1593413264000, + "y": null + }, + { + "x": 1593413265000, + "y": null + }, + { + "x": 1593413266000, + "y": null + }, + { + "x": 1593413267000, + "y": null + }, + { + "x": 1593413268000, + "y": null + }, + { + "x": 1593413269000, + "y": null + }, + { + "x": 1593413270000, + "y": null + }, + { + "x": 1593413271000, + "y": null + }, + { + "x": 1593413272000, + "y": 0 + }, + { + "x": 1593413273000, + "y": 0 + }, + { + "x": 1593413274000, + "y": null + }, + { + "x": 1593413275000, + "y": null + }, + { + "x": 1593413276000, + "y": null + }, + { + "x": 1593413277000, + "y": 0 + }, + { + "x": 1593413278000, + "y": null + }, + { + "x": 1593413279000, + "y": null + }, + { + "x": 1593413280000, + "y": null + }, + { + "x": 1593413281000, + "y": 0 + }, + { + "x": 1593413282000, + "y": null + }, + { + "x": 1593413283000, + "y": null + }, + { + "x": 1593413284000, + "y": 0 + }, + { + "x": 1593413285000, + "y": 0 + }, + { + "x": 1593413286000, + "y": 0.125 + }, + { + "x": 1593413287000, + "y": 0.5 + }, + { + "x": 1593413288000, + "y": 0 + }, + { + "x": 1593413289000, + "y": 0.5 + }, + { + "x": 1593413290000, + "y": 0 + }, + { + "x": 1593413291000, + "y": 0 + }, + { + "x": 1593413292000, + "y": 0.5 + }, + { + "x": 1593413293000, + "y": 0 + }, + { + "x": 1593413294000, + "y": 0 + }, + { + "x": 1593413295000, + "y": 0 + }, + { + "x": 1593413296000, + "y": 0 + }, + { + "x": 1593413297000, + "y": 0 + }, + { + "x": 1593413298000, + "y": 0 + }, + { + "x": 1593413299000, + "y": 0.5 + }, + { + "x": 1593413300000, + "y": 0.3333333333333333 + }, + { + "x": 1593413301000, + "y": 0.14285714285714285 + }, + { + "x": 1593413302000, + "y": 0 + }, + { + "x": 1593413303000, + "y": 0 + }, + { + "x": 1593413304000, + "y": 0 + }, + { + "x": 1593413305000, + "y": 0.6666666666666666 + }, + { + "x": 1593413306000, + "y": 0 + }, + { + "x": 1593413307000, + "y": 0 + }, + { + "x": 1593413308000, + "y": 0.3333333333333333 + }, + { + "x": 1593413309000, + "y": 0.3333333333333333 + }, + { + "x": 1593413310000, + "y": 0.3333333333333333 + }, + { + "x": 1593413311000, + "y": 0.5 + }, + { + "x": 1593413312000, + "y": 0 + }, + { + "x": 1593413313000, + "y": 0 + }, + { + "x": 1593413314000, + "y": 0 + }, + { + "x": 1593413315000, + "y": 0.5 + }, + { + "x": 1593413316000, + "y": 0 + }, + { + "x": 1593413317000, + "y": 0 + }, + { + "x": 1593413318000, + "y": 0 + }, + { + "x": 1593413319000, + "y": 0 + }, + { + "x": 1593413320000, + "y": 0.3333333333333333 + }, + { + "x": 1593413321000, + "y": 0 + }, + { + "x": 1593413322000, + "y": 0.5 + }, + { + "x": 1593413323000, + "y": null + }, + { + "x": 1593413324000, + "y": null + }, + { + "x": 1593413325000, + "y": null + }, + { + "x": 1593413326000, + "y": null + }, + { + "x": 1593413327000, + "y": null + }, + { + "x": 1593413328000, + "y": null + }, + { + "x": 1593413329000, + "y": null + }, + { + "x": 1593413330000, + "y": null + }, + { + "x": 1593413331000, + "y": null + }, + { + "x": 1593413332000, + "y": null + }, + { + "x": 1593413333000, + "y": null + }, + { + "x": 1593413334000, + "y": null + }, + { + "x": 1593413335000, + "y": null + }, + { + "x": 1593413336000, + "y": null + }, + { + "x": 1593413337000, + "y": null + }, + { + "x": 1593413338000, + "y": null + }, + { + "x": 1593413339000, + "y": null + }, + { + "x": 1593413340000, + "y": null + } + ], + "average": 0.14188815060908083 +} From 86d7050822865eb96b2fe8faf9997a71b71aaedd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Fern=C3=A1ndez=20Haro?= Date: Wed, 26 Aug 2020 12:51:22 +0100 Subject: [PATCH 61/71] [Telemetry] Add Application Usage Schema (#75283) Co-authored-by: Elastic Machine --- .telemetryrc.json | 1 - .../src/tools/__fixture__/mock_schema.json | 16 + ...exed_interface_with_not_matching_schema.ts | 54 + .../__fixture__/parsed_working_collector.ts | 22 + .../extract_collectors.test.ts.snap | 54 + .../tools/check_collector__integrity.test.ts | 15 + .../src/tools/extract_collectors.test.ts | 2 +- .../src/tools/serializer.ts | 5 + .../kbn-telemetry-tools/src/tools/utils.ts | 37 +- ...exed_interface_with_not_matching_schema.ts | 48 + .../telemetry_collectors/working_collector.ts | 9 + .../collectors/application_usage/schema.ts | 99 ++ .../telemetry_application_usage_collector.ts | 168 +-- src/plugins/telemetry/schema/oss_plugins.json | 1208 +++++++++++++++++ 14 files changed, 1650 insertions(+), 88 deletions(-) create mode 100644 packages/kbn-telemetry-tools/src/tools/__fixture__/parsed_indexed_interface_with_not_matching_schema.ts create mode 100644 src/fixtures/telemetry_collectors/indexed_interface_with_not_matching_schema.ts create mode 100644 src/plugins/kibana_usage_collection/server/collectors/application_usage/schema.ts diff --git a/.telemetryrc.json b/.telemetryrc.json index 2f57566159a70..818f9805628e1 100644 --- a/.telemetryrc.json +++ b/.telemetryrc.json @@ -7,7 +7,6 @@ "src/plugins/testbed/", "src/plugins/kibana_utils/", "src/plugins/kibana_usage_collection/server/collectors/kibana/kibana_usage_collector.ts", - "src/plugins/kibana_usage_collection/server/collectors/application_usage/telemetry_application_usage_collector.ts", "src/plugins/kibana_usage_collection/server/collectors/management/telemetry_management_collector.ts", "src/plugins/kibana_usage_collection/server/collectors/ui_metric/telemetry_ui_metric_collector.ts", "src/plugins/telemetry/server/collectors/usage/telemetry_usage_collector.ts" diff --git a/packages/kbn-telemetry-tools/src/tools/__fixture__/mock_schema.json b/packages/kbn-telemetry-tools/src/tools/__fixture__/mock_schema.json index e87699825b4e1..2e69d3625d7ff 100644 --- a/packages/kbn-telemetry-tools/src/tools/__fixture__/mock_schema.json +++ b/packages/kbn-telemetry-tools/src/tools/__fixture__/mock_schema.json @@ -5,6 +5,22 @@ "flat": { "type": "keyword" }, + "my_index_signature_prop": { + "properties": { + "avg": { + "type": "number" + }, + "count": { + "type": "number" + }, + "max": { + "type": "number" + }, + "min": { + "type": "number" + } + } + }, "my_str": { "type": "text" }, diff --git a/packages/kbn-telemetry-tools/src/tools/__fixture__/parsed_indexed_interface_with_not_matching_schema.ts b/packages/kbn-telemetry-tools/src/tools/__fixture__/parsed_indexed_interface_with_not_matching_schema.ts new file mode 100644 index 0000000000000..83866a2b6afec --- /dev/null +++ b/packages/kbn-telemetry-tools/src/tools/__fixture__/parsed_indexed_interface_with_not_matching_schema.ts @@ -0,0 +1,54 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { SyntaxKind } from 'typescript'; +import { ParsedUsageCollection } from '../ts_parser'; + +export const parsedIndexedInterfaceWithNoMatchingSchema: ParsedUsageCollection = [ + 'src/fixtures/telemetry_collectors/indexed_interface_with_not_matching_schema.ts', + { + collectorName: 'indexed_interface_with_not_matching_schema', + schema: { + value: { + something: { + count_1: { + type: 'number', + }, + }, + }, + }, + fetch: { + typeName: 'Usage', + typeDescriptor: { + '': { + '@@INDEX@@': { + count_1: { + kind: SyntaxKind.NumberKeyword, + type: 'NumberKeyword', + }, + count_2: { + kind: SyntaxKind.NumberKeyword, + type: 'NumberKeyword', + }, + }, + }, + }, + }, + }, +]; diff --git a/packages/kbn-telemetry-tools/src/tools/__fixture__/parsed_working_collector.ts b/packages/kbn-telemetry-tools/src/tools/__fixture__/parsed_working_collector.ts index 803bc7f13f59e..b238c5aa346ad 100644 --- a/packages/kbn-telemetry-tools/src/tools/__fixture__/parsed_working_collector.ts +++ b/packages/kbn-telemetry-tools/src/tools/__fixture__/parsed_working_collector.ts @@ -32,6 +32,20 @@ export const parsedWorkingCollector: ParsedUsageCollection = [ my_str: { type: 'text', }, + my_index_signature_prop: { + avg: { + type: 'number', + }, + count: { + type: 'number', + }, + max: { + type: 'number', + }, + min: { + type: 'number', + }, + }, my_objects: { total: { type: 'number', @@ -60,6 +74,14 @@ export const parsedWorkingCollector: ParsedUsageCollection = [ kind: SyntaxKind.StringKeyword, type: 'StringKeyword', }, + my_index_signature_prop: { + '': { + '@@INDEX@@': { + kind: SyntaxKind.NumberKeyword, + type: 'NumberKeyword', + }, + }, + }, my_objects: { total: { kind: SyntaxKind.NumberKeyword, diff --git a/packages/kbn-telemetry-tools/src/tools/__snapshots__/extract_collectors.test.ts.snap b/packages/kbn-telemetry-tools/src/tools/__snapshots__/extract_collectors.test.ts.snap index fc933b6c7fd35..bf1d5ffc1101e 100644 --- a/packages/kbn-telemetry-tools/src/tools/__snapshots__/extract_collectors.test.ts.snap +++ b/packages/kbn-telemetry-tools/src/tools/__snapshots__/extract_collectors.test.ts.snap @@ -90,6 +90,38 @@ Array [ }, }, ], + Array [ + "src/fixtures/telemetry_collectors/indexed_interface_with_not_matching_schema.ts", + Object { + "collectorName": "indexed_interface_with_not_matching_schema", + "fetch": Object { + "typeDescriptor": Object { + "": Object { + "@@INDEX@@": Object { + "count_1": Object { + "kind": 140, + "type": "NumberKeyword", + }, + "count_2": Object { + "kind": 140, + "type": "NumberKeyword", + }, + }, + }, + }, + "typeName": "Usage", + }, + "schema": Object { + "value": Object { + "something": Object { + "count_1": Object { + "type": "long", + }, + }, + }, + }, + }, + ], Array [ "src/fixtures/telemetry_collectors/nested_collector.ts", Object { @@ -132,6 +164,14 @@ Array [ "type": "BooleanKeyword", }, }, + "my_index_signature_prop": Object { + "": Object { + "@@INDEX@@": Object { + "kind": 140, + "type": "NumberKeyword", + }, + }, + }, "my_objects": Object { "total": Object { "kind": 140, @@ -166,6 +206,20 @@ Array [ "type": "boolean", }, }, + "my_index_signature_prop": Object { + "avg": Object { + "type": "number", + }, + "count": Object { + "type": "number", + }, + "max": Object { + "type": "number", + }, + "min": Object { + "type": "number", + }, + }, "my_objects": Object { "total": Object { "type": "number", diff --git a/packages/kbn-telemetry-tools/src/tools/check_collector__integrity.test.ts b/packages/kbn-telemetry-tools/src/tools/check_collector__integrity.test.ts index dbdda3f38afd5..a101210185a63 100644 --- a/packages/kbn-telemetry-tools/src/tools/check_collector__integrity.test.ts +++ b/packages/kbn-telemetry-tools/src/tools/check_collector__integrity.test.ts @@ -20,6 +20,7 @@ import { cloneDeep } from 'lodash'; import * as ts from 'typescript'; import { parsedWorkingCollector } from './__fixture__/parsed_working_collector'; +import { parsedIndexedInterfaceWithNoMatchingSchema } from './__fixture__/parsed_indexed_interface_with_not_matching_schema'; import { checkCompatibleTypeDescriptor, checkMatchingMapping } from './check_collector_integrity'; import * as path from 'path'; import { readFile } from 'fs'; @@ -82,6 +83,20 @@ describe('checkCompatibleTypeDescriptor', () => { expect(incompatibles).toHaveLength(0); }); + it('returns diff on indexed interface with no matching schema', () => { + const incompatibles = checkCompatibleTypeDescriptor([ + parsedIndexedInterfaceWithNoMatchingSchema, + ]); + expect(incompatibles).toHaveLength(1); + const { diff, message } = incompatibles[0]; + // eslint-disable-next-line @typescript-eslint/naming-convention + expect(diff).toEqual({ '.@@INDEX@@.count_2.kind': 'number' }); + expect(message).toHaveLength(1); + expect(message).toEqual([ + 'incompatible Type key (Usage..@@INDEX@@.count_2): expected (undefined) got ("number").', + ]); + }); + describe('Interface Change', () => { it('returns diff on incompatible type descriptor with mapping', () => { const malformedParsedCollector = cloneDeep(parsedWorkingCollector); diff --git a/packages/kbn-telemetry-tools/src/tools/extract_collectors.test.ts b/packages/kbn-telemetry-tools/src/tools/extract_collectors.test.ts index 1b4ed21a1635c..0517cb9034d0a 100644 --- a/packages/kbn-telemetry-tools/src/tools/extract_collectors.test.ts +++ b/packages/kbn-telemetry-tools/src/tools/extract_collectors.test.ts @@ -34,7 +34,7 @@ describe('extractCollectors', () => { const programPaths = await getProgramPaths(configs[0]); const results = [...extractCollectors(programPaths, tsConfig)]; - expect(results).toHaveLength(6); + expect(results).toHaveLength(7); expect(results).toMatchSnapshot(); }); }); diff --git a/packages/kbn-telemetry-tools/src/tools/serializer.ts b/packages/kbn-telemetry-tools/src/tools/serializer.ts index bce5dd7f58643..f945402ec5fc2 100644 --- a/packages/kbn-telemetry-tools/src/tools/serializer.ts +++ b/packages/kbn-telemetry-tools/src/tools/serializer.ts @@ -84,6 +84,11 @@ export function getDescriptor(node: ts.Node, program: ts.Program): Descriptor | }, {} as any); } + // If it's defined as signature { [key: string]: OtherInterface } + if (ts.isIndexSignatureDeclaration(node) && node.type) { + return { '@@INDEX@@': getDescriptor(node.type, program) }; + } + if (ts.SyntaxKind.FirstNode === node.kind) { return getDescriptor((node as any).right, program); } diff --git a/packages/kbn-telemetry-tools/src/tools/utils.ts b/packages/kbn-telemetry-tools/src/tools/utils.ts index 212b06a4c9895..c1424785b22a5 100644 --- a/packages/kbn-telemetry-tools/src/tools/utils.ts +++ b/packages/kbn-telemetry-tools/src/tools/utils.ts @@ -98,6 +98,14 @@ export function getVariableValue(node: ts.Node): string | Record { return serializeObject(node); } + if (ts.isIdentifier(node)) { + const declaration = getIdentifierDeclaration(node); + if (ts.isVariableDeclaration(declaration) && declaration.initializer) { + return getVariableValue(declaration.initializer); + } + // TODO: If this is another imported value from another file, we'll need to go fetch it like in getPropertyValue + } + throw Error(`Unsuppored Node: cannot get value of node (${node.getText()}) of kind ${node.kind}`); } @@ -112,10 +120,11 @@ export function serializeObject(node: ts.Node) { if (typeof propertyName === 'undefined') { throw new Error(`Unable to get property name ${property.getText()}`); } + const cleanPropertyName = propertyName.replace(/["']/g, ''); if (ts.isPropertyAssignment(property)) { - value[propertyName] = getVariableValue(property.initializer); + value[cleanPropertyName] = getVariableValue(property.initializer); } else { - value[propertyName] = getVariableValue(property); + value[cleanPropertyName] = getVariableValue(property); } } @@ -222,9 +231,29 @@ export const flattenKeys = (obj: any, keyPath: any[] = []): any => { }; export function difference(actual: any, expected: any) { - function changes(obj: any, base: any) { + function changes(obj: { [key: string]: any }, base: { [key: string]: any }) { return transform(obj, function (result, value, key) { - if (key && !isEqual(value, base[key])) { + if (key && /@@INDEX@@/.test(`${key}`)) { + // The type definition is an Index Signature, fuzzy searching for similar keys + const regexp = new RegExp(`${key}`.replace(/@@INDEX@@/g, '(.+)?')); + const keysInBase = Object.keys(base) + .map((k) => { + const match = k.match(regexp); + return match && match[0]; + }) + .filter((s): s is string => !!s); + + if (keysInBase.length === 0) { + // Mark this key as wrong because we couldn't find any matching keys + result[key] = value; + } + + keysInBase.forEach((k) => { + if (!isEqual(value, base[k])) { + result[k] = isObject(value) && isObject(base[k]) ? changes(value, base[k]) : value; + } + }); + } else if (key && !isEqual(value, base[key])) { result[key] = isObject(value) && isObject(base[key]) ? changes(value, base[key]) : value; } }); diff --git a/src/fixtures/telemetry_collectors/indexed_interface_with_not_matching_schema.ts b/src/fixtures/telemetry_collectors/indexed_interface_with_not_matching_schema.ts new file mode 100644 index 0000000000000..0ec8d2e15c34a --- /dev/null +++ b/src/fixtures/telemetry_collectors/indexed_interface_with_not_matching_schema.ts @@ -0,0 +1,48 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { CollectorSet } from '../../plugins/usage_collection/server/collector'; +import { loggerMock } from '../../core/server/logging/logger.mock'; + +const { makeUsageCollector } = new CollectorSet({ + logger: loggerMock.create(), + maximumWaitTimeForAllCollectorsInS: 0, +}); + +interface Usage { + [key: string]: { + count_1?: number; + count_2?: number; + }; +} + +export const myCollector = makeUsageCollector({ + type: 'indexed_interface_with_not_matching_schema', + isReady: () => true, + fetch() { + if (Math.random()) { + return { something: { count_1: 1 } }; + } + return { something: { count_2: 2 } }; + }, + schema: { + something: { + count_1: { type: 'long' }, // Intentionally missing count_2 + }, + }, +}); diff --git a/src/fixtures/telemetry_collectors/working_collector.ts b/src/fixtures/telemetry_collectors/working_collector.ts index d58a89db97d74..bdf10b5e54919 100644 --- a/src/fixtures/telemetry_collectors/working_collector.ts +++ b/src/fixtures/telemetry_collectors/working_collector.ts @@ -35,6 +35,9 @@ interface Usage { my_objects: MyObject; my_array?: MyObject[]; my_str_array?: string[]; + my_index_signature_prop?: { + [key: string]: number; + }; } const SOME_NUMBER: number = 123; @@ -93,5 +96,11 @@ export const myCollector = makeUsageCollector({ type: { type: 'boolean' }, }, my_str_array: { type: 'keyword' }, + my_index_signature_prop: { + count: { type: 'number' }, + avg: { type: 'number' }, + max: { type: 'number' }, + min: { type: 'number' }, + }, }, }); diff --git a/src/plugins/kibana_usage_collection/server/collectors/application_usage/schema.ts b/src/plugins/kibana_usage_collection/server/collectors/application_usage/schema.ts new file mode 100644 index 0000000000000..6efe872553583 --- /dev/null +++ b/src/plugins/kibana_usage_collection/server/collectors/application_usage/schema.ts @@ -0,0 +1,99 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { MakeSchemaFrom } from 'src/plugins/usage_collection/server'; +import { ApplicationUsageTelemetryReport } from './telemetry_application_usage_collector'; + +const commonSchema: MakeSchemaFrom = { + clicks_total: { + type: 'long', + }, + clicks_7_days: { + type: 'long', + }, + clicks_30_days: { + type: 'long', + }, + clicks_90_days: { + type: 'long', + }, + minutes_on_screen_total: { + type: 'float', + }, + minutes_on_screen_7_days: { + type: 'float', + }, + minutes_on_screen_30_days: { + type: 'float', + }, + minutes_on_screen_90_days: { + type: 'float', + }, +}; + +// These keys obtained by searching for `/application\w*\.register\(/` and checking the value of the attr `id`. +// TODO: Find a way to update these keys automatically. +export const applicationUsageSchema = { + // OSS + dashboards: commonSchema, + dev_tools: commonSchema, + discover: commonSchema, + home: commonSchema, + kibana: commonSchema, // It's a forward app so we'll likely never report it + management: commonSchema, + short_url_redirect: commonSchema, // It's a forward app so we'll likely never report it + timelion: commonSchema, + visualize: commonSchema, + + // X-Pack + apm: commonSchema, + csm: commonSchema, + canvas: commonSchema, + dashboard_mode: commonSchema, // It's a forward app so we'll likely never report it + appSearch: commonSchema, + workplaceSearch: commonSchema, + graph: commonSchema, + logs: commonSchema, + metrics: commonSchema, + infra: commonSchema, // It's a forward app so we'll likely never report it + ingestManager: commonSchema, + lens: commonSchema, + maps: commonSchema, + ml: commonSchema, + monitoring: commonSchema, + 'observability-overview': commonSchema, + security_account: commonSchema, + security_access_agreement: commonSchema, + security_capture_url: commonSchema, // It's a forward app so we'll likely never report it + security_logged_out: commonSchema, + security_login: commonSchema, + security_logout: commonSchema, + security_overwritten_session: commonSchema, + securitySolution: commonSchema, // It's a forward app so we'll likely never report it + 'securitySolution:overview': commonSchema, + 'securitySolution:detections': commonSchema, + 'securitySolution:hosts': commonSchema, + 'securitySolution:network': commonSchema, + 'securitySolution:timelines': commonSchema, + 'securitySolution:case': commonSchema, + 'securitySolution:administration': commonSchema, + siem: commonSchema, + space_selector: commonSchema, + uptime: commonSchema, +}; diff --git a/src/plugins/kibana_usage_collection/server/collectors/application_usage/telemetry_application_usage_collector.ts b/src/plugins/kibana_usage_collection/server/collectors/application_usage/telemetry_application_usage_collector.ts index 1f22ab0100101..69137681e0597 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/application_usage/telemetry_application_usage_collector.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/application_usage/telemetry_application_usage_collector.ts @@ -26,6 +26,7 @@ import { ApplicationUsageTransactional, registerMappings, } from './saved_objects_types'; +import { applicationUsageSchema } from './schema'; /** * Roll indices every 24h @@ -40,7 +41,7 @@ export const ROLL_INDICES_START = 5 * 60 * 1000; export const SAVED_OBJECTS_TOTAL_TYPE = 'application_usage_totals'; export const SAVED_OBJECTS_TRANSACTIONAL_TYPE = 'application_usage_transactional'; -interface ApplicationUsageTelemetryReport { +export interface ApplicationUsageTelemetryReport { [appId: string]: { clicks_total: number; clicks_7_days: number; @@ -60,93 +61,96 @@ export function registerApplicationUsageCollector( ) { registerMappings(registerType); - const collector = usageCollection.makeUsageCollector({ - type: 'application_usage', - isReady: () => typeof getSavedObjectsClient() !== 'undefined', - fetch: async () => { - const savedObjectsClient = getSavedObjectsClient(); - if (typeof savedObjectsClient === 'undefined') { - return; - } - const [rawApplicationUsageTotals, rawApplicationUsageTransactional] = await Promise.all([ - findAll(savedObjectsClient, { type: SAVED_OBJECTS_TOTAL_TYPE }), - findAll(savedObjectsClient, { - type: SAVED_OBJECTS_TRANSACTIONAL_TYPE, - }), - ]); - - const applicationUsageFromTotals = rawApplicationUsageTotals.reduce( - (acc, { attributes: { appId, minutesOnScreen, numberOfClicks } }) => { - const existing = acc[appId] || { clicks_total: 0, minutes_on_screen_total: 0 }; - return { - ...acc, - [appId]: { - clicks_total: numberOfClicks + existing.clicks_total, + const collector = usageCollection.makeUsageCollector( + { + type: 'application_usage', + isReady: () => typeof getSavedObjectsClient() !== 'undefined', + schema: applicationUsageSchema, + fetch: async () => { + const savedObjectsClient = getSavedObjectsClient(); + if (typeof savedObjectsClient === 'undefined') { + return; + } + const [rawApplicationUsageTotals, rawApplicationUsageTransactional] = await Promise.all([ + findAll(savedObjectsClient, { type: SAVED_OBJECTS_TOTAL_TYPE }), + findAll(savedObjectsClient, { + type: SAVED_OBJECTS_TRANSACTIONAL_TYPE, + }), + ]); + + const applicationUsageFromTotals = rawApplicationUsageTotals.reduce( + (acc, { attributes: { appId, minutesOnScreen, numberOfClicks } }) => { + const existing = acc[appId] || { clicks_total: 0, minutes_on_screen_total: 0 }; + return { + ...acc, + [appId]: { + clicks_total: numberOfClicks + existing.clicks_total, + clicks_7_days: 0, + clicks_30_days: 0, + clicks_90_days: 0, + minutes_on_screen_total: minutesOnScreen + existing.minutes_on_screen_total, + minutes_on_screen_7_days: 0, + minutes_on_screen_30_days: 0, + minutes_on_screen_90_days: 0, + }, + }; + }, + {} as ApplicationUsageTelemetryReport + ); + const nowMinus7 = moment().subtract(7, 'days'); + const nowMinus30 = moment().subtract(30, 'days'); + const nowMinus90 = moment().subtract(90, 'days'); + + const applicationUsage = rawApplicationUsageTransactional.reduce( + (acc, { attributes: { appId, minutesOnScreen, numberOfClicks, timestamp } }) => { + const existing = acc[appId] || { + clicks_total: 0, clicks_7_days: 0, clicks_30_days: 0, clicks_90_days: 0, - minutes_on_screen_total: minutesOnScreen + existing.minutes_on_screen_total, + minutes_on_screen_total: 0, minutes_on_screen_7_days: 0, minutes_on_screen_30_days: 0, minutes_on_screen_90_days: 0, - }, - }; - }, - {} as ApplicationUsageTelemetryReport - ); - const nowMinus7 = moment().subtract(7, 'days'); - const nowMinus30 = moment().subtract(30, 'days'); - const nowMinus90 = moment().subtract(90, 'days'); - - const applicationUsage = rawApplicationUsageTransactional.reduce( - (acc, { attributes: { appId, minutesOnScreen, numberOfClicks, timestamp } }) => { - const existing = acc[appId] || { - clicks_total: 0, - clicks_7_days: 0, - clicks_30_days: 0, - clicks_90_days: 0, - minutes_on_screen_total: 0, - minutes_on_screen_7_days: 0, - minutes_on_screen_30_days: 0, - minutes_on_screen_90_days: 0, - }; - - const timeOfEntry = moment(timestamp as string); - const isInLast7Days = timeOfEntry.isSameOrAfter(nowMinus7); - const isInLast30Days = timeOfEntry.isSameOrAfter(nowMinus30); - const isInLast90Days = timeOfEntry.isSameOrAfter(nowMinus90); - - const last7Days = { - clicks_7_days: existing.clicks_7_days + numberOfClicks, - minutes_on_screen_7_days: existing.minutes_on_screen_7_days + minutesOnScreen, - }; - const last30Days = { - clicks_30_days: existing.clicks_30_days + numberOfClicks, - minutes_on_screen_30_days: existing.minutes_on_screen_30_days + minutesOnScreen, - }; - const last90Days = { - clicks_90_days: existing.clicks_90_days + numberOfClicks, - minutes_on_screen_90_days: existing.minutes_on_screen_90_days + minutesOnScreen, - }; - - return { - ...acc, - [appId]: { - ...existing, - clicks_total: existing.clicks_total + numberOfClicks, - minutes_on_screen_total: existing.minutes_on_screen_total + minutesOnScreen, - ...(isInLast7Days ? last7Days : {}), - ...(isInLast30Days ? last30Days : {}), - ...(isInLast90Days ? last90Days : {}), - }, - }; - }, - applicationUsageFromTotals - ); - - return applicationUsage; - }, - }); + }; + + const timeOfEntry = moment(timestamp as string); + const isInLast7Days = timeOfEntry.isSameOrAfter(nowMinus7); + const isInLast30Days = timeOfEntry.isSameOrAfter(nowMinus30); + const isInLast90Days = timeOfEntry.isSameOrAfter(nowMinus90); + + const last7Days = { + clicks_7_days: existing.clicks_7_days + numberOfClicks, + minutes_on_screen_7_days: existing.minutes_on_screen_7_days + minutesOnScreen, + }; + const last30Days = { + clicks_30_days: existing.clicks_30_days + numberOfClicks, + minutes_on_screen_30_days: existing.minutes_on_screen_30_days + minutesOnScreen, + }; + const last90Days = { + clicks_90_days: existing.clicks_90_days + numberOfClicks, + minutes_on_screen_90_days: existing.minutes_on_screen_90_days + minutesOnScreen, + }; + + return { + ...acc, + [appId]: { + ...existing, + clicks_total: existing.clicks_total + numberOfClicks, + minutes_on_screen_total: existing.minutes_on_screen_total + minutesOnScreen, + ...(isInLast7Days ? last7Days : {}), + ...(isInLast30Days ? last30Days : {}), + ...(isInLast90Days ? last90Days : {}), + }, + }; + }, + applicationUsageFromTotals + ); + + return applicationUsage; + }, + } + ); usageCollection.registerCollector(collector); diff --git a/src/plugins/telemetry/schema/oss_plugins.json b/src/plugins/telemetry/schema/oss_plugins.json index c306446b9780d..acd575badbe5b 100644 --- a/src/plugins/telemetry/schema/oss_plugins.json +++ b/src/plugins/telemetry/schema/oss_plugins.json @@ -48,6 +48,1214 @@ } } }, + "application_usage": { + "properties": { + "dashboards": { + "properties": { + "clicks_total": { + "type": "long" + }, + "clicks_7_days": { + "type": "long" + }, + "clicks_30_days": { + "type": "long" + }, + "clicks_90_days": { + "type": "long" + }, + "minutes_on_screen_total": { + "type": "float" + }, + "minutes_on_screen_7_days": { + "type": "float" + }, + "minutes_on_screen_30_days": { + "type": "float" + }, + "minutes_on_screen_90_days": { + "type": "float" + } + } + }, + "dev_tools": { + "properties": { + "clicks_total": { + "type": "long" + }, + "clicks_7_days": { + "type": "long" + }, + "clicks_30_days": { + "type": "long" + }, + "clicks_90_days": { + "type": "long" + }, + "minutes_on_screen_total": { + "type": "float" + }, + "minutes_on_screen_7_days": { + "type": "float" + }, + "minutes_on_screen_30_days": { + "type": "float" + }, + "minutes_on_screen_90_days": { + "type": "float" + } + } + }, + "discover": { + "properties": { + "clicks_total": { + "type": "long" + }, + "clicks_7_days": { + "type": "long" + }, + "clicks_30_days": { + "type": "long" + }, + "clicks_90_days": { + "type": "long" + }, + "minutes_on_screen_total": { + "type": "float" + }, + "minutes_on_screen_7_days": { + "type": "float" + }, + "minutes_on_screen_30_days": { + "type": "float" + }, + "minutes_on_screen_90_days": { + "type": "float" + } + } + }, + "home": { + "properties": { + "clicks_total": { + "type": "long" + }, + "clicks_7_days": { + "type": "long" + }, + "clicks_30_days": { + "type": "long" + }, + "clicks_90_days": { + "type": "long" + }, + "minutes_on_screen_total": { + "type": "float" + }, + "minutes_on_screen_7_days": { + "type": "float" + }, + "minutes_on_screen_30_days": { + "type": "float" + }, + "minutes_on_screen_90_days": { + "type": "float" + } + } + }, + "kibana": { + "properties": { + "clicks_total": { + "type": "long" + }, + "clicks_7_days": { + "type": "long" + }, + "clicks_30_days": { + "type": "long" + }, + "clicks_90_days": { + "type": "long" + }, + "minutes_on_screen_total": { + "type": "float" + }, + "minutes_on_screen_7_days": { + "type": "float" + }, + "minutes_on_screen_30_days": { + "type": "float" + }, + "minutes_on_screen_90_days": { + "type": "float" + } + } + }, + "management": { + "properties": { + "clicks_total": { + "type": "long" + }, + "clicks_7_days": { + "type": "long" + }, + "clicks_30_days": { + "type": "long" + }, + "clicks_90_days": { + "type": "long" + }, + "minutes_on_screen_total": { + "type": "float" + }, + "minutes_on_screen_7_days": { + "type": "float" + }, + "minutes_on_screen_30_days": { + "type": "float" + }, + "minutes_on_screen_90_days": { + "type": "float" + } + } + }, + "short_url_redirect": { + "properties": { + "clicks_total": { + "type": "long" + }, + "clicks_7_days": { + "type": "long" + }, + "clicks_30_days": { + "type": "long" + }, + "clicks_90_days": { + "type": "long" + }, + "minutes_on_screen_total": { + "type": "float" + }, + "minutes_on_screen_7_days": { + "type": "float" + }, + "minutes_on_screen_30_days": { + "type": "float" + }, + "minutes_on_screen_90_days": { + "type": "float" + } + } + }, + "timelion": { + "properties": { + "clicks_total": { + "type": "long" + }, + "clicks_7_days": { + "type": "long" + }, + "clicks_30_days": { + "type": "long" + }, + "clicks_90_days": { + "type": "long" + }, + "minutes_on_screen_total": { + "type": "float" + }, + "minutes_on_screen_7_days": { + "type": "float" + }, + "minutes_on_screen_30_days": { + "type": "float" + }, + "minutes_on_screen_90_days": { + "type": "float" + } + } + }, + "visualize": { + "properties": { + "clicks_total": { + "type": "long" + }, + "clicks_7_days": { + "type": "long" + }, + "clicks_30_days": { + "type": "long" + }, + "clicks_90_days": { + "type": "long" + }, + "minutes_on_screen_total": { + "type": "float" + }, + "minutes_on_screen_7_days": { + "type": "float" + }, + "minutes_on_screen_30_days": { + "type": "float" + }, + "minutes_on_screen_90_days": { + "type": "float" + } + } + }, + "apm": { + "properties": { + "clicks_total": { + "type": "long" + }, + "clicks_7_days": { + "type": "long" + }, + "clicks_30_days": { + "type": "long" + }, + "clicks_90_days": { + "type": "long" + }, + "minutes_on_screen_total": { + "type": "float" + }, + "minutes_on_screen_7_days": { + "type": "float" + }, + "minutes_on_screen_30_days": { + "type": "float" + }, + "minutes_on_screen_90_days": { + "type": "float" + } + } + }, + "csm": { + "properties": { + "clicks_total": { + "type": "long" + }, + "clicks_7_days": { + "type": "long" + }, + "clicks_30_days": { + "type": "long" + }, + "clicks_90_days": { + "type": "long" + }, + "minutes_on_screen_total": { + "type": "float" + }, + "minutes_on_screen_7_days": { + "type": "float" + }, + "minutes_on_screen_30_days": { + "type": "float" + }, + "minutes_on_screen_90_days": { + "type": "float" + } + } + }, + "canvas": { + "properties": { + "clicks_total": { + "type": "long" + }, + "clicks_7_days": { + "type": "long" + }, + "clicks_30_days": { + "type": "long" + }, + "clicks_90_days": { + "type": "long" + }, + "minutes_on_screen_total": { + "type": "float" + }, + "minutes_on_screen_7_days": { + "type": "float" + }, + "minutes_on_screen_30_days": { + "type": "float" + }, + "minutes_on_screen_90_days": { + "type": "float" + } + } + }, + "dashboard_mode": { + "properties": { + "clicks_total": { + "type": "long" + }, + "clicks_7_days": { + "type": "long" + }, + "clicks_30_days": { + "type": "long" + }, + "clicks_90_days": { + "type": "long" + }, + "minutes_on_screen_total": { + "type": "float" + }, + "minutes_on_screen_7_days": { + "type": "float" + }, + "minutes_on_screen_30_days": { + "type": "float" + }, + "minutes_on_screen_90_days": { + "type": "float" + } + } + }, + "appSearch": { + "properties": { + "clicks_total": { + "type": "long" + }, + "clicks_7_days": { + "type": "long" + }, + "clicks_30_days": { + "type": "long" + }, + "clicks_90_days": { + "type": "long" + }, + "minutes_on_screen_total": { + "type": "float" + }, + "minutes_on_screen_7_days": { + "type": "float" + }, + "minutes_on_screen_30_days": { + "type": "float" + }, + "minutes_on_screen_90_days": { + "type": "float" + } + } + }, + "workplaceSearch": { + "properties": { + "clicks_total": { + "type": "long" + }, + "clicks_7_days": { + "type": "long" + }, + "clicks_30_days": { + "type": "long" + }, + "clicks_90_days": { + "type": "long" + }, + "minutes_on_screen_total": { + "type": "float" + }, + "minutes_on_screen_7_days": { + "type": "float" + }, + "minutes_on_screen_30_days": { + "type": "float" + }, + "minutes_on_screen_90_days": { + "type": "float" + } + } + }, + "graph": { + "properties": { + "clicks_total": { + "type": "long" + }, + "clicks_7_days": { + "type": "long" + }, + "clicks_30_days": { + "type": "long" + }, + "clicks_90_days": { + "type": "long" + }, + "minutes_on_screen_total": { + "type": "float" + }, + "minutes_on_screen_7_days": { + "type": "float" + }, + "minutes_on_screen_30_days": { + "type": "float" + }, + "minutes_on_screen_90_days": { + "type": "float" + } + } + }, + "logs": { + "properties": { + "clicks_total": { + "type": "long" + }, + "clicks_7_days": { + "type": "long" + }, + "clicks_30_days": { + "type": "long" + }, + "clicks_90_days": { + "type": "long" + }, + "minutes_on_screen_total": { + "type": "float" + }, + "minutes_on_screen_7_days": { + "type": "float" + }, + "minutes_on_screen_30_days": { + "type": "float" + }, + "minutes_on_screen_90_days": { + "type": "float" + } + } + }, + "metrics": { + "properties": { + "clicks_total": { + "type": "long" + }, + "clicks_7_days": { + "type": "long" + }, + "clicks_30_days": { + "type": "long" + }, + "clicks_90_days": { + "type": "long" + }, + "minutes_on_screen_total": { + "type": "float" + }, + "minutes_on_screen_7_days": { + "type": "float" + }, + "minutes_on_screen_30_days": { + "type": "float" + }, + "minutes_on_screen_90_days": { + "type": "float" + } + } + }, + "infra": { + "properties": { + "clicks_total": { + "type": "long" + }, + "clicks_7_days": { + "type": "long" + }, + "clicks_30_days": { + "type": "long" + }, + "clicks_90_days": { + "type": "long" + }, + "minutes_on_screen_total": { + "type": "float" + }, + "minutes_on_screen_7_days": { + "type": "float" + }, + "minutes_on_screen_30_days": { + "type": "float" + }, + "minutes_on_screen_90_days": { + "type": "float" + } + } + }, + "ingestManager": { + "properties": { + "clicks_total": { + "type": "long" + }, + "clicks_7_days": { + "type": "long" + }, + "clicks_30_days": { + "type": "long" + }, + "clicks_90_days": { + "type": "long" + }, + "minutes_on_screen_total": { + "type": "float" + }, + "minutes_on_screen_7_days": { + "type": "float" + }, + "minutes_on_screen_30_days": { + "type": "float" + }, + "minutes_on_screen_90_days": { + "type": "float" + } + } + }, + "lens": { + "properties": { + "clicks_total": { + "type": "long" + }, + "clicks_7_days": { + "type": "long" + }, + "clicks_30_days": { + "type": "long" + }, + "clicks_90_days": { + "type": "long" + }, + "minutes_on_screen_total": { + "type": "float" + }, + "minutes_on_screen_7_days": { + "type": "float" + }, + "minutes_on_screen_30_days": { + "type": "float" + }, + "minutes_on_screen_90_days": { + "type": "float" + } + } + }, + "maps": { + "properties": { + "clicks_total": { + "type": "long" + }, + "clicks_7_days": { + "type": "long" + }, + "clicks_30_days": { + "type": "long" + }, + "clicks_90_days": { + "type": "long" + }, + "minutes_on_screen_total": { + "type": "float" + }, + "minutes_on_screen_7_days": { + "type": "float" + }, + "minutes_on_screen_30_days": { + "type": "float" + }, + "minutes_on_screen_90_days": { + "type": "float" + } + } + }, + "ml": { + "properties": { + "clicks_total": { + "type": "long" + }, + "clicks_7_days": { + "type": "long" + }, + "clicks_30_days": { + "type": "long" + }, + "clicks_90_days": { + "type": "long" + }, + "minutes_on_screen_total": { + "type": "float" + }, + "minutes_on_screen_7_days": { + "type": "float" + }, + "minutes_on_screen_30_days": { + "type": "float" + }, + "minutes_on_screen_90_days": { + "type": "float" + } + } + }, + "monitoring": { + "properties": { + "clicks_total": { + "type": "long" + }, + "clicks_7_days": { + "type": "long" + }, + "clicks_30_days": { + "type": "long" + }, + "clicks_90_days": { + "type": "long" + }, + "minutes_on_screen_total": { + "type": "float" + }, + "minutes_on_screen_7_days": { + "type": "float" + }, + "minutes_on_screen_30_days": { + "type": "float" + }, + "minutes_on_screen_90_days": { + "type": "float" + } + } + }, + "observability-overview": { + "properties": { + "clicks_total": { + "type": "long" + }, + "clicks_7_days": { + "type": "long" + }, + "clicks_30_days": { + "type": "long" + }, + "clicks_90_days": { + "type": "long" + }, + "minutes_on_screen_total": { + "type": "float" + }, + "minutes_on_screen_7_days": { + "type": "float" + }, + "minutes_on_screen_30_days": { + "type": "float" + }, + "minutes_on_screen_90_days": { + "type": "float" + } + } + }, + "security_account": { + "properties": { + "clicks_total": { + "type": "long" + }, + "clicks_7_days": { + "type": "long" + }, + "clicks_30_days": { + "type": "long" + }, + "clicks_90_days": { + "type": "long" + }, + "minutes_on_screen_total": { + "type": "float" + }, + "minutes_on_screen_7_days": { + "type": "float" + }, + "minutes_on_screen_30_days": { + "type": "float" + }, + "minutes_on_screen_90_days": { + "type": "float" + } + } + }, + "security_access_agreement": { + "properties": { + "clicks_total": { + "type": "long" + }, + "clicks_7_days": { + "type": "long" + }, + "clicks_30_days": { + "type": "long" + }, + "clicks_90_days": { + "type": "long" + }, + "minutes_on_screen_total": { + "type": "float" + }, + "minutes_on_screen_7_days": { + "type": "float" + }, + "minutes_on_screen_30_days": { + "type": "float" + }, + "minutes_on_screen_90_days": { + "type": "float" + } + } + }, + "security_capture_url": { + "properties": { + "clicks_total": { + "type": "long" + }, + "clicks_7_days": { + "type": "long" + }, + "clicks_30_days": { + "type": "long" + }, + "clicks_90_days": { + "type": "long" + }, + "minutes_on_screen_total": { + "type": "float" + }, + "minutes_on_screen_7_days": { + "type": "float" + }, + "minutes_on_screen_30_days": { + "type": "float" + }, + "minutes_on_screen_90_days": { + "type": "float" + } + } + }, + "security_logged_out": { + "properties": { + "clicks_total": { + "type": "long" + }, + "clicks_7_days": { + "type": "long" + }, + "clicks_30_days": { + "type": "long" + }, + "clicks_90_days": { + "type": "long" + }, + "minutes_on_screen_total": { + "type": "float" + }, + "minutes_on_screen_7_days": { + "type": "float" + }, + "minutes_on_screen_30_days": { + "type": "float" + }, + "minutes_on_screen_90_days": { + "type": "float" + } + } + }, + "security_login": { + "properties": { + "clicks_total": { + "type": "long" + }, + "clicks_7_days": { + "type": "long" + }, + "clicks_30_days": { + "type": "long" + }, + "clicks_90_days": { + "type": "long" + }, + "minutes_on_screen_total": { + "type": "float" + }, + "minutes_on_screen_7_days": { + "type": "float" + }, + "minutes_on_screen_30_days": { + "type": "float" + }, + "minutes_on_screen_90_days": { + "type": "float" + } + } + }, + "security_logout": { + "properties": { + "clicks_total": { + "type": "long" + }, + "clicks_7_days": { + "type": "long" + }, + "clicks_30_days": { + "type": "long" + }, + "clicks_90_days": { + "type": "long" + }, + "minutes_on_screen_total": { + "type": "float" + }, + "minutes_on_screen_7_days": { + "type": "float" + }, + "minutes_on_screen_30_days": { + "type": "float" + }, + "minutes_on_screen_90_days": { + "type": "float" + } + } + }, + "security_overwritten_session": { + "properties": { + "clicks_total": { + "type": "long" + }, + "clicks_7_days": { + "type": "long" + }, + "clicks_30_days": { + "type": "long" + }, + "clicks_90_days": { + "type": "long" + }, + "minutes_on_screen_total": { + "type": "float" + }, + "minutes_on_screen_7_days": { + "type": "float" + }, + "minutes_on_screen_30_days": { + "type": "float" + }, + "minutes_on_screen_90_days": { + "type": "float" + } + } + }, + "securitySolution": { + "properties": { + "clicks_total": { + "type": "long" + }, + "clicks_7_days": { + "type": "long" + }, + "clicks_30_days": { + "type": "long" + }, + "clicks_90_days": { + "type": "long" + }, + "minutes_on_screen_total": { + "type": "float" + }, + "minutes_on_screen_7_days": { + "type": "float" + }, + "minutes_on_screen_30_days": { + "type": "float" + }, + "minutes_on_screen_90_days": { + "type": "float" + } + } + }, + "securitySolution:overview": { + "properties": { + "clicks_total": { + "type": "long" + }, + "clicks_7_days": { + "type": "long" + }, + "clicks_30_days": { + "type": "long" + }, + "clicks_90_days": { + "type": "long" + }, + "minutes_on_screen_total": { + "type": "float" + }, + "minutes_on_screen_7_days": { + "type": "float" + }, + "minutes_on_screen_30_days": { + "type": "float" + }, + "minutes_on_screen_90_days": { + "type": "float" + } + } + }, + "securitySolution:detections": { + "properties": { + "clicks_total": { + "type": "long" + }, + "clicks_7_days": { + "type": "long" + }, + "clicks_30_days": { + "type": "long" + }, + "clicks_90_days": { + "type": "long" + }, + "minutes_on_screen_total": { + "type": "float" + }, + "minutes_on_screen_7_days": { + "type": "float" + }, + "minutes_on_screen_30_days": { + "type": "float" + }, + "minutes_on_screen_90_days": { + "type": "float" + } + } + }, + "securitySolution:hosts": { + "properties": { + "clicks_total": { + "type": "long" + }, + "clicks_7_days": { + "type": "long" + }, + "clicks_30_days": { + "type": "long" + }, + "clicks_90_days": { + "type": "long" + }, + "minutes_on_screen_total": { + "type": "float" + }, + "minutes_on_screen_7_days": { + "type": "float" + }, + "minutes_on_screen_30_days": { + "type": "float" + }, + "minutes_on_screen_90_days": { + "type": "float" + } + } + }, + "securitySolution:network": { + "properties": { + "clicks_total": { + "type": "long" + }, + "clicks_7_days": { + "type": "long" + }, + "clicks_30_days": { + "type": "long" + }, + "clicks_90_days": { + "type": "long" + }, + "minutes_on_screen_total": { + "type": "float" + }, + "minutes_on_screen_7_days": { + "type": "float" + }, + "minutes_on_screen_30_days": { + "type": "float" + }, + "minutes_on_screen_90_days": { + "type": "float" + } + } + }, + "securitySolution:timelines": { + "properties": { + "clicks_total": { + "type": "long" + }, + "clicks_7_days": { + "type": "long" + }, + "clicks_30_days": { + "type": "long" + }, + "clicks_90_days": { + "type": "long" + }, + "minutes_on_screen_total": { + "type": "float" + }, + "minutes_on_screen_7_days": { + "type": "float" + }, + "minutes_on_screen_30_days": { + "type": "float" + }, + "minutes_on_screen_90_days": { + "type": "float" + } + } + }, + "securitySolution:case": { + "properties": { + "clicks_total": { + "type": "long" + }, + "clicks_7_days": { + "type": "long" + }, + "clicks_30_days": { + "type": "long" + }, + "clicks_90_days": { + "type": "long" + }, + "minutes_on_screen_total": { + "type": "float" + }, + "minutes_on_screen_7_days": { + "type": "float" + }, + "minutes_on_screen_30_days": { + "type": "float" + }, + "minutes_on_screen_90_days": { + "type": "float" + } + } + }, + "securitySolution:administration": { + "properties": { + "clicks_total": { + "type": "long" + }, + "clicks_7_days": { + "type": "long" + }, + "clicks_30_days": { + "type": "long" + }, + "clicks_90_days": { + "type": "long" + }, + "minutes_on_screen_total": { + "type": "float" + }, + "minutes_on_screen_7_days": { + "type": "float" + }, + "minutes_on_screen_30_days": { + "type": "float" + }, + "minutes_on_screen_90_days": { + "type": "float" + } + } + }, + "siem": { + "properties": { + "clicks_total": { + "type": "long" + }, + "clicks_7_days": { + "type": "long" + }, + "clicks_30_days": { + "type": "long" + }, + "clicks_90_days": { + "type": "long" + }, + "minutes_on_screen_total": { + "type": "float" + }, + "minutes_on_screen_7_days": { + "type": "float" + }, + "minutes_on_screen_30_days": { + "type": "float" + }, + "minutes_on_screen_90_days": { + "type": "float" + } + } + }, + "space_selector": { + "properties": { + "clicks_total": { + "type": "long" + }, + "clicks_7_days": { + "type": "long" + }, + "clicks_30_days": { + "type": "long" + }, + "clicks_90_days": { + "type": "long" + }, + "minutes_on_screen_total": { + "type": "float" + }, + "minutes_on_screen_7_days": { + "type": "float" + }, + "minutes_on_screen_30_days": { + "type": "float" + }, + "minutes_on_screen_90_days": { + "type": "float" + } + } + }, + "uptime": { + "properties": { + "clicks_total": { + "type": "long" + }, + "clicks_7_days": { + "type": "long" + }, + "clicks_30_days": { + "type": "long" + }, + "clicks_90_days": { + "type": "long" + }, + "minutes_on_screen_total": { + "type": "float" + }, + "minutes_on_screen_7_days": { + "type": "float" + }, + "minutes_on_screen_30_days": { + "type": "float" + }, + "minutes_on_screen_90_days": { + "type": "float" + } + } + } + } + }, "csp": { "properties": { "strict": { From 63265b6f57e421c335945aa4e948bd3334876222 Mon Sep 17 00:00:00 2001 From: Nicolas Chaulet Date: Wed, 26 Aug 2020 08:50:52 -0400 Subject: [PATCH 62/71] Compute AAD to encrypty/decrypt SO only if needed (#75818) --- .../server/crypto/encrypted_saved_objects_service.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/x-pack/plugins/encrypted_saved_objects/server/crypto/encrypted_saved_objects_service.ts b/x-pack/plugins/encrypted_saved_objects/server/crypto/encrypted_saved_objects_service.ts index 99361107047c2..82d6bb9be15f6 100644 --- a/x-pack/plugins/encrypted_saved_objects/server/crypto/encrypted_saved_objects_service.ts +++ b/x-pack/plugins/encrypted_saved_objects/server/crypto/encrypted_saved_objects_service.ts @@ -198,12 +198,15 @@ export class EncryptedSavedObjectsService { if (typeDefinition === undefined) { return attributes; } + let encryptionAAD: string | undefined; - const encryptionAAD = this.getAAD(typeDefinition, descriptor, attributes); const encryptedAttributes: Record = {}; for (const attributeName of typeDefinition.attributesToEncrypt) { const attributeValue = attributes[attributeName]; if (attributeValue != null) { + if (!encryptionAAD) { + encryptionAAD = this.getAAD(typeDefinition, descriptor, attributes); + } try { encryptedAttributes[attributeName] = (yield [attributeValue, encryptionAAD])!; } catch (err) { @@ -376,8 +379,7 @@ export class EncryptedSavedObjectsService { if (typeDefinition === undefined) { return attributes; } - - const encryptionAAD = this.getAAD(typeDefinition, descriptor, attributes); + let encryptionAAD: string | undefined; const decryptedAttributes: Record = {}; for (const attributeName of typeDefinition.attributesToEncrypt) { const attributeValue = attributes[attributeName]; @@ -393,7 +395,9 @@ export class EncryptedSavedObjectsService { )}` ); } - + if (!encryptionAAD) { + encryptionAAD = this.getAAD(typeDefinition, descriptor, attributes); + } try { decryptedAttributes[attributeName] = (yield [attributeValue, encryptionAAD])!; } catch (err) { From 4042f82035f7dd776f54c3452be51c1fc7365786 Mon Sep 17 00:00:00 2001 From: Jonathan Buttner <56361221+jonathan-buttner@users.noreply.github.com> Date: Wed, 26 Aug 2020 09:25:45 -0400 Subject: [PATCH 63/71] [Security Solution][Resolver] Support kuery filter (#74695) * Adding kql filter * Adding filter support for the backend and tests * Moving the filter to the body * switching events and alerts api to post * Removing unused import * Adding tests for events api results being in descending order * Switching frontend to use post for related events --- .../common/endpoint/generate_data.test.ts | 10 + .../common/endpoint/generate_data.ts | 32 +- .../common/endpoint/schema/resolver.ts | 10 + .../resolver/data_access_layer/factory.ts | 2 +- .../server/endpoint/routes/resolver.ts | 4 +- .../server/endpoint/routes/resolver/alerts.ts | 9 +- .../server/endpoint/routes/resolver/events.ts | 9 +- .../routes/resolver/queries/alerts.ts | 14 +- .../routes/resolver/queries/events.ts | 15 +- .../resolver/utils/alerts_query_handler.ts | 32 +- .../resolver/utils/events_query_handler.ts | 33 +- .../endpoint/routes/resolver/utils/fetch.ts | 70 ++-- .../routes/resolver/utils/pagination.test.ts | 14 + .../routes/resolver/utils/pagination.ts | 18 +- .../apis/resolver/alerts.ts | 159 ++++++++ .../apis/resolver/common.ts | 222 ++++++++++ .../apis/resolver/events.ts | 213 ++++++++++ .../apis/resolver/index.ts | 2 + .../apis/resolver/tree.ts | 386 +----------------- 19 files changed, 808 insertions(+), 446 deletions(-) create mode 100644 x-pack/test/security_solution_endpoint_api_int/apis/resolver/alerts.ts create mode 100644 x-pack/test/security_solution_endpoint_api_int/apis/resolver/common.ts create mode 100644 x-pack/test/security_solution_endpoint_api_int/apis/resolver/events.ts diff --git a/x-pack/plugins/security_solution/common/endpoint/generate_data.test.ts b/x-pack/plugins/security_solution/common/endpoint/generate_data.test.ts index 46fc002e76e7f..be3a1e82356c8 100644 --- a/x-pack/plugins/security_solution/common/endpoint/generate_data.test.ts +++ b/x-pack/plugins/security_solution/common/endpoint/generate_data.test.ts @@ -169,6 +169,7 @@ describe('data generator', () => { const childrenPerNode = 3; const generations = 3; const relatedAlerts = 4; + beforeEach(() => { tree = generator.generateTree({ alwaysGenMaxChildrenPerNode: true, @@ -182,6 +183,7 @@ describe('data generator', () => { { category: RelatedEventCategory.File, count: 2 }, { category: RelatedEventCategory.Network, count: 1 }, ], + relatedEventsOrdered: true, relatedAlerts, ancestryArraySize: ANCESTRY_LIMIT, }); @@ -212,6 +214,14 @@ describe('data generator', () => { } }; + it('creates related events in ascending order', () => { + // the order should not change since it should already be in ascending order + const relatedEventsAsc = _.cloneDeep(tree.origin.relatedEvents).sort( + (event1, event2) => event1['@timestamp'] - event2['@timestamp'] + ); + expect(tree.origin.relatedEvents).toStrictEqual(relatedEventsAsc); + }); + it('has ancestry array defined', () => { expect(tree.origin.lifecycle[0].process.Ext!.ancestry!.length).toBe(ANCESTRY_LIMIT); for (const event of tree.allEvents) { diff --git a/x-pack/plugins/security_solution/common/endpoint/generate_data.ts b/x-pack/plugins/security_solution/common/endpoint/generate_data.ts index 7340b1c021eba..0955f196df176 100644 --- a/x-pack/plugins/security_solution/common/endpoint/generate_data.ts +++ b/x-pack/plugins/security_solution/common/endpoint/generate_data.ts @@ -302,6 +302,12 @@ export interface TreeOptions { generations?: number; children?: number; relatedEvents?: RelatedEventInfo[] | number; + /** + * If true then the related events will be created with timestamps that preserve the + * generation order, meaning the first event will always have a timestamp number less + * than the next related event + */ + relatedEventsOrdered?: boolean; relatedAlerts?: number; percentWithRelated?: number; percentTerminated?: number; @@ -322,6 +328,7 @@ export function getTreeOptionsWithDef(options?: TreeOptions): TreeOptionDefaults generations: options?.generations ?? 2, children: options?.children ?? 2, relatedEvents: options?.relatedEvents ?? 5, + relatedEventsOrdered: options?.relatedEventsOrdered ?? false, relatedAlerts: options?.relatedAlerts ?? 3, percentWithRelated: options?.percentWithRelated ?? 30, percentTerminated: options?.percentTerminated ?? 100, @@ -809,7 +816,8 @@ export class EndpointDocGenerator { for (const relatedEvent of this.relatedEventsGenerator( node, opts.relatedEvents, - secBeforeEvent + secBeforeEvent, + opts.relatedEventsOrdered )) { eventList.push(relatedEvent); } @@ -877,6 +885,8 @@ export class EndpointDocGenerator { addRelatedAlerts(ancestor, numAlertsPerNode, processDuration, events); } } + timestamp = timestamp + 1000; + events.push( this.generateAlert( timestamp, @@ -961,7 +971,12 @@ export class EndpointDocGenerator { }); } if (this.randomN(100) < opts.percentWithRelated) { - yield* this.relatedEventsGenerator(child, opts.relatedEvents, processDuration); + yield* this.relatedEventsGenerator( + child, + opts.relatedEvents, + processDuration, + opts.relatedEventsOrdered + ); yield* this.relatedAlertsGenerator(child, opts.relatedAlerts, processDuration); } } @@ -973,13 +988,17 @@ export class EndpointDocGenerator { * @param relatedEvents - can be an array of RelatedEventInfo objects describing the related events that should be generated for each process node * or a number which defines the number of related events and will default to random categories * @param processDuration - maximum number of seconds after process event that related event timestamp can be + * @param ordered - if true the events will have an increasing timestamp, otherwise their timestamp will be random but + * guaranteed to be greater than or equal to the originating event */ public *relatedEventsGenerator( node: Event, relatedEvents: RelatedEventInfo[] | number = 10, - processDuration: number = 6 * 3600 + processDuration: number = 6 * 3600, + ordered: boolean = false ) { let relatedEventsInfo: RelatedEventInfo[]; + let ts = node['@timestamp'] + 1; if (typeof relatedEvents === 'number') { relatedEventsInfo = [{ category: RelatedEventCategory.Random, count: relatedEvents }]; } else { @@ -995,7 +1014,12 @@ export class EndpointDocGenerator { eventInfo = OTHER_EVENT_CATEGORIES[event.category]; } - const ts = node['@timestamp'] + this.randomN(processDuration) * 1000; + if (ordered) { + ts += this.randomN(processDuration) * 1000; + } else { + ts = node['@timestamp'] + this.randomN(processDuration) * 1000; + } + yield this.generateEvent({ timestamp: ts, entityID: node.process.entity_id, diff --git a/x-pack/plugins/security_solution/common/endpoint/schema/resolver.ts b/x-pack/plugins/security_solution/common/endpoint/schema/resolver.ts index f3e67f84b2880..311aa0c04c9ab 100644 --- a/x-pack/plugins/security_solution/common/endpoint/schema/resolver.ts +++ b/x-pack/plugins/security_solution/common/endpoint/schema/resolver.ts @@ -33,6 +33,11 @@ export const validateEvents = { afterEvent: schema.maybe(schema.string()), legacyEndpointID: schema.maybe(schema.string({ minLength: 1 })), }), + body: schema.nullable( + schema.object({ + filter: schema.maybe(schema.string()), + }) + ), }; /** @@ -45,6 +50,11 @@ export const validateAlerts = { afterAlert: schema.maybe(schema.string()), legacyEndpointID: schema.maybe(schema.string({ minLength: 1 })), }), + body: schema.nullable( + schema.object({ + filter: schema.maybe(schema.string()), + }) + ), }; /** diff --git a/x-pack/plugins/security_solution/public/resolver/data_access_layer/factory.ts b/x-pack/plugins/security_solution/public/resolver/data_access_layer/factory.ts index 016ebfa0faee4..dee53a624baff 100644 --- a/x-pack/plugins/security_solution/public/resolver/data_access_layer/factory.ts +++ b/x-pack/plugins/security_solution/public/resolver/data_access_layer/factory.ts @@ -25,7 +25,7 @@ export function dataAccessLayerFactory( * Used to get non-process related events for a node. */ async relatedEvents(entityID: string): Promise { - return context.services.http.get(`/api/endpoint/resolver/${entityID}/events`, { + return context.services.http.post(`/api/endpoint/resolver/${entityID}/events`, { query: { events: 100 }, }); }, diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver.ts index 5c92b23d594de..3ec968e4a0e1a 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver.ts @@ -24,7 +24,7 @@ import { handleEntities } from './resolver/entity'; export function registerResolverRoutes(router: IRouter, endpointAppContext: EndpointAppContext) { const log = endpointAppContext.logFactory.get('resolver'); - router.get( + router.post( { path: '/api/endpoint/resolver/{id}/events', validate: validateEvents, @@ -33,7 +33,7 @@ export function registerResolverRoutes(router: IRouter, endpointAppContext: Endp handleEvents(log, endpointAppContext) ); - router.get( + router.post( { path: '/api/endpoint/resolver/{id}/alerts', validate: validateAlerts, diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/alerts.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/alerts.ts index 830d92ef2efc0..8e641194ab899 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/alerts.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/alerts.ts @@ -14,11 +14,16 @@ import { EndpointAppContext } from '../../types'; export function handleAlerts( log: Logger, endpointAppContext: EndpointAppContext -): RequestHandler, TypeOf> { +): RequestHandler< + TypeOf, + TypeOf, + TypeOf +> { return async (context, req, res) => { const { params: { id }, query: { alerts, afterAlert, legacyEndpointID: endpointID }, + body, } = req; try { const client = context.core.elasticsearch.legacy.client; @@ -26,7 +31,7 @@ export function handleAlerts( const fetcher = new Fetcher(client, id, eventsIndexPattern, alertsIndexPattern, endpointID); return res.ok({ - body: await fetcher.alerts(alerts, afterAlert), + body: await fetcher.alerts(alerts, afterAlert, body?.filter), }); } catch (err) { log.warn(err); diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/events.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/events.ts index 9e5c6be43f728..80d21ae118284 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/events.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/events.ts @@ -14,11 +14,16 @@ import { EndpointAppContext } from '../../types'; export function handleEvents( log: Logger, endpointAppContext: EndpointAppContext -): RequestHandler, TypeOf> { +): RequestHandler< + TypeOf, + TypeOf, + TypeOf +> { return async (context, req, res) => { const { params: { id }, query: { events, afterEvent, legacyEndpointID: endpointID }, + body, } = req; try { const client = context.core.elasticsearch.legacy.client; @@ -26,7 +31,7 @@ export function handleEvents( const fetcher = new Fetcher(client, id, eventsIndexPattern, alertsIndexPattern, endpointID); return res.ok({ - body: await fetcher.events(events, afterEvent), + body: await fetcher.events(events, afterEvent, body?.filter), }); } catch (err) { log.warn(err); diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/alerts.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/alerts.ts index feb4a404b2359..54c6cf432aa89 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/alerts.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/alerts.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import { SearchResponse } from 'elasticsearch'; +import { esKuery } from '../../../../../../../../src/plugins/data/server'; import { ResolverEvent } from '../../../../../common/endpoint/types'; import { ResolverQuery } from './base'; import { PaginationBuilder } from '../utils/pagination'; @@ -13,12 +14,17 @@ import { JsonObject } from '../../../../../../../../src/plugins/kibana_utils/com * Builds a query for retrieving alerts for a node. */ export class AlertsQuery extends ResolverQuery { + private readonly kqlQuery: JsonObject[] = []; constructor( private readonly pagination: PaginationBuilder, indexPattern: string | string[], - endpointID?: string + endpointID?: string, + kql?: string ) { super(indexPattern, endpointID); + if (kql) { + this.kqlQuery.push(esKuery.toElasticsearchQuery(esKuery.fromKueryExpression(kql))); + } } protected legacyQuery(endpointID: string, uniquePIDs: string[]): JsonObject { @@ -26,6 +32,7 @@ export class AlertsQuery extends ResolverQuery { query: { bool: { filter: [ + ...this.kqlQuery, { terms: { 'endgame.unique_pid': uniquePIDs }, }, @@ -38,7 +45,7 @@ export class AlertsQuery extends ResolverQuery { ], }, }, - ...this.pagination.buildQueryFields('endgame.serial_event_id'), + ...this.pagination.buildQueryFields('endgame.serial_event_id', 'asc'), }; } @@ -47,6 +54,7 @@ export class AlertsQuery extends ResolverQuery { query: { bool: { filter: [ + ...this.kqlQuery, { terms: { 'process.entity_id': entityIDs }, }, @@ -56,7 +64,7 @@ export class AlertsQuery extends ResolverQuery { ], }, }, - ...this.pagination.buildQueryFields('event.id'), + ...this.pagination.buildQueryFields('event.id', 'asc'), }; } diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/events.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/events.ts index abc86826e77dd..0969a3c360e4a 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/events.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/events.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import { SearchResponse } from 'elasticsearch'; +import { esKuery } from '../../../../../../../../src/plugins/data/server'; import { ResolverEvent } from '../../../../../common/endpoint/types'; import { ResolverQuery } from './base'; import { PaginationBuilder } from '../utils/pagination'; @@ -13,12 +14,18 @@ import { JsonObject } from '../../../../../../../../src/plugins/kibana_utils/com * Builds a query for retrieving related events for a node. */ export class EventsQuery extends ResolverQuery { + private readonly kqlQuery: JsonObject[] = []; + constructor( private readonly pagination: PaginationBuilder, indexPattern: string | string[], - endpointID?: string + endpointID?: string, + kql?: string ) { super(indexPattern, endpointID); + if (kql) { + this.kqlQuery.push(esKuery.toElasticsearchQuery(esKuery.fromKueryExpression(kql))); + } } protected legacyQuery(endpointID: string, uniquePIDs: string[]): JsonObject { @@ -26,6 +33,7 @@ export class EventsQuery extends ResolverQuery { query: { bool: { filter: [ + ...this.kqlQuery, { terms: { 'endgame.unique_pid': uniquePIDs }, }, @@ -45,7 +53,7 @@ export class EventsQuery extends ResolverQuery { ], }, }, - ...this.pagination.buildQueryFields('endgame.serial_event_id'), + ...this.pagination.buildQueryFields('endgame.serial_event_id', 'desc'), }; } @@ -54,6 +62,7 @@ export class EventsQuery extends ResolverQuery { query: { bool: { filter: [ + ...this.kqlQuery, { terms: { 'process.entity_id': entityIDs }, }, @@ -70,7 +79,7 @@ export class EventsQuery extends ResolverQuery { ], }, }, - ...this.pagination.buildQueryFields('event.id'), + ...this.pagination.buildQueryFields('event.id', 'desc'), }; } diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/alerts_query_handler.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/alerts_query_handler.ts index ae17cf4c3a562..efffbc10473d4 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/alerts_query_handler.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/alerts_query_handler.ts @@ -13,23 +13,35 @@ import { PaginationBuilder } from './pagination'; import { QueryInfo } from '../queries/multi_searcher'; import { SingleQueryHandler } from './fetch'; +/** + * Parameters for RelatedAlertsQueryHandler + */ +export interface RelatedAlertsParams { + limit: number; + entityID: string; + indexPattern: string; + after?: string; + legacyEndpointID?: string; + filter?: string; +} + /** * Requests related alerts for the given node. */ export class RelatedAlertsQueryHandler implements SingleQueryHandler { private relatedAlerts: ResolverRelatedAlerts | undefined; private readonly query: AlertsQuery; - constructor( - private readonly limit: number, - private readonly entityID: string, - after: string | undefined, - indexPattern: string, - legacyEndpointID: string | undefined - ) { + private readonly limit: number; + private readonly entityID: string; + + constructor(options: RelatedAlertsParams) { + this.limit = options.limit; + this.entityID = options.entityID; this.query = new AlertsQuery( - PaginationBuilder.createBuilder(limit, after), - indexPattern, - legacyEndpointID + PaginationBuilder.createBuilder(this.limit, options.after), + options.indexPattern, + options.legacyEndpointID, + options.filter ); } diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/events_query_handler.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/events_query_handler.ts index 849dbc25fe4db..8792f917fb4d6 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/events_query_handler.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/events_query_handler.ts @@ -13,23 +13,36 @@ import { PaginationBuilder } from './pagination'; import { QueryInfo } from '../queries/multi_searcher'; import { SingleQueryHandler } from './fetch'; +/** + * Parameters for the RelatedEventsQueryHandler + */ +export interface RelatedEventsParams { + limit: number; + entityID: string; + indexPattern: string; + after?: string; + legacyEndpointID?: string; + filter?: string; +} + /** * This retrieves the related events for the origin node of a resolver tree. */ export class RelatedEventsQueryHandler implements SingleQueryHandler { private relatedEvents: ResolverRelatedEvents | undefined; private readonly query: EventsQuery; - constructor( - private readonly limit: number, - private readonly entityID: string, - after: string | undefined, - indexPattern: string, - legacyEndpointID: string | undefined - ) { + private readonly limit: number; + private readonly entityID: string; + + constructor(options: RelatedEventsParams) { + this.limit = options.limit; + this.entityID = options.entityID; + this.query = new EventsQuery( - PaginationBuilder.createBuilder(limit, after), - indexPattern, - legacyEndpointID + PaginationBuilder.createBuilder(this.limit, options.after), + options.indexPattern, + options.legacyEndpointID, + options.filter ); } diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/fetch.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/fetch.ts index 43c10d552ab4e..1b88f965909eb 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/fetch.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/fetch.ts @@ -110,21 +110,21 @@ export class Fetcher { this.endpointID ); - const eventsHandler = new RelatedEventsQueryHandler( - options.events, - this.id, - options.afterEvent, - this.eventsIndexPattern, - this.endpointID - ); + const eventsHandler = new RelatedEventsQueryHandler({ + limit: options.events, + entityID: this.id, + after: options.afterEvent, + indexPattern: this.eventsIndexPattern, + legacyEndpointID: this.endpointID, + }); - const alertsHandler = new RelatedAlertsQueryHandler( - options.alerts, - this.id, - options.afterAlert, - this.alertsIndexPattern, - this.endpointID - ); + const alertsHandler = new RelatedAlertsQueryHandler({ + limit: options.alerts, + entityID: this.id, + after: options.afterAlert, + indexPattern: this.alertsIndexPattern, + legacyEndpointID: this.endpointID, + }); // we need to get the start events first because the API request defines how many nodes to return and we don't want // to count or limit ourselves based on the other lifecycle events (end, etc) @@ -228,17 +228,24 @@ export class Fetcher { /** * Retrieves the related events for the origin node. * - * @param limit the upper bound number of related events to return + * @param limit the upper bound number of related events to return. The limit is applied after the cursor is used to + * skip the previous results. * @param after a cursor to use as the starting point for retrieving related events + * @param filter a kql query for filtering the results */ - public async events(limit: number, after?: string): Promise { - const eventsHandler = new RelatedEventsQueryHandler( + public async events( + limit: number, + after?: string, + filter?: string + ): Promise { + const eventsHandler = new RelatedEventsQueryHandler({ limit, - this.id, + entityID: this.id, after, - this.eventsIndexPattern, - this.endpointID - ); + indexPattern: this.eventsIndexPattern, + legacyEndpointID: this.endpointID, + filter, + }); return eventsHandler.search(this.client); } @@ -246,17 +253,24 @@ export class Fetcher { /** * Retrieves the alerts for the origin node. * - * @param limit the upper bound number of alerts to return + * @param limit the upper bound number of alerts to return. The limit is applied after the cursor is used to + * skip the previous results. * @param after a cursor to use as the starting point for retrieving alerts + * @param filter a kql query string for filtering the results */ - public async alerts(limit: number, after?: string): Promise { - const alertsHandler = new RelatedAlertsQueryHandler( + public async alerts( + limit: number, + after?: string, + filter?: string + ): Promise { + const alertsHandler = new RelatedAlertsQueryHandler({ limit, - this.id, + entityID: this.id, after, - this.alertsIndexPattern, - this.endpointID - ); + indexPattern: this.alertsIndexPattern, + legacyEndpointID: this.endpointID, + filter, + }); return alertsHandler.search(this.client); } diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/pagination.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/pagination.test.ts index 4daa45aec2a74..8e567bfb59c65 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/pagination.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/pagination.test.ts @@ -42,5 +42,19 @@ describe('Pagination', () => { const fields = builder.buildQueryFields(''); expect(fields).not.toHaveProperty('search_after'); }); + + it('creates the sort field in ascending order', () => { + const builder = PaginationBuilder.createBuilder(100); + expect(builder.buildQueryFields('a').sort).toContainEqual({ '@timestamp': 'asc' }); + expect(builder.buildQueryFields('', 'asc').sort).toContainEqual({ '@timestamp': 'asc' }); + }); + + it('creates the sort field in descending order', () => { + const builder = PaginationBuilder.createBuilder(100); + expect(builder.buildQueryFields('a', 'desc').sort).toStrictEqual([ + { '@timestamp': 'desc' }, + { a: 'asc' }, + ]); + }); }); }); diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/pagination.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/pagination.ts index f6ff4451b5d8e..4a6c65e55a6b6 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/pagination.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/pagination.ts @@ -16,6 +16,11 @@ interface PaginationCursor { eventID: string; } +/** + * The sort direction for the timestamp field + */ +export type TimeSortDirection = 'asc' | 'desc'; + /** * Defines the sorting fields for queries that leverage pagination */ @@ -158,10 +163,14 @@ export class PaginationBuilder { * Helper for creates an object for adding the pagination fields to a query * * @param tiebreaker a unique field to use as the tiebreaker for the search_after + * @param timeSort is the timestamp sort direction * @returns an object containing the pagination information */ - buildQueryFieldsAsInterface(tiebreaker: string): PaginationFields { - const sort: SortFields = [{ '@timestamp': 'asc' }, { [tiebreaker]: 'asc' }]; + buildQueryFieldsAsInterface( + tiebreaker: string, + timeSort: TimeSortDirection = 'asc' + ): PaginationFields { + const sort: SortFields = [{ '@timestamp': timeSort }, { [tiebreaker]: 'asc' }]; let searchAfter: SearchAfterFields | undefined; if (this.timestamp && this.eventID) { searchAfter = [this.timestamp, this.eventID]; @@ -174,11 +183,12 @@ export class PaginationBuilder { * Creates an object for adding the pagination fields to a query * * @param tiebreaker a unique field to use as the tiebreaker for the search_after + * @param timeSort is the timestamp sort direction * @returns an object containing the pagination information */ - buildQueryFields(tiebreaker: string): JsonObject { + buildQueryFields(tiebreaker: string, timeSort: TimeSortDirection = 'asc'): JsonObject { const fields: JsonObject = {}; - const pagination = this.buildQueryFieldsAsInterface(tiebreaker); + const pagination = this.buildQueryFieldsAsInterface(tiebreaker, timeSort); fields.sort = pagination.sort; fields.size = pagination.size; if (pagination.searchAfter) { diff --git a/x-pack/test/security_solution_endpoint_api_int/apis/resolver/alerts.ts b/x-pack/test/security_solution_endpoint_api_int/apis/resolver/alerts.ts new file mode 100644 index 0000000000000..82d844aae8016 --- /dev/null +++ b/x-pack/test/security_solution_endpoint_api_int/apis/resolver/alerts.ts @@ -0,0 +1,159 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import expect from '@kbn/expect'; +import { eventId } from '../../../../plugins/security_solution/common/endpoint/models/event'; +import { ResolverRelatedAlerts } from '../../../../plugins/security_solution/common/endpoint/types'; +import { FtrProviderContext } from '../../ftr_provider_context'; +import { + Tree, + RelatedEventCategory, +} from '../../../../plugins/security_solution/common/endpoint/generate_data'; +import { Options, GeneratedTrees } from '../../services/resolver'; +import { compareArrays } from './common'; + +export default function ({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const resolver = getService('resolverGenerator'); + + const relatedEventsToGen = [ + { category: RelatedEventCategory.Driver, count: 2 }, + { category: RelatedEventCategory.File, count: 1 }, + { category: RelatedEventCategory.Registry, count: 1 }, + ]; + const relatedAlerts = 4; + let resolverTrees: GeneratedTrees; + let tree: Tree; + const treeOptions: Options = { + ancestors: 5, + relatedEvents: relatedEventsToGen, + relatedAlerts, + children: 3, + generations: 2, + percentTerminated: 100, + percentWithRelated: 100, + numTrees: 1, + alwaysGenMaxChildrenPerNode: true, + ancestryArraySize: 2, + }; + + describe('related alerts route', () => { + before(async () => { + resolverTrees = await resolver.createTrees(treeOptions); + // we only requested a single alert so there's only 1 tree + tree = resolverTrees.trees[0]; + }); + after(async () => { + await resolver.deleteData(resolverTrees); + }); + + it('should not find any alerts', async () => { + const { body }: { body: ResolverRelatedAlerts } = await supertest + .post(`/api/endpoint/resolver/5555/alerts`) + .set('kbn-xsrf', 'xxx') + .expect(200); + expect(body.nextAlert).to.eql(null); + expect(body.alerts).to.be.empty(); + }); + + it('should return details for the root node', async () => { + const { body }: { body: ResolverRelatedAlerts } = await supertest + .post(`/api/endpoint/resolver/${tree.origin.id}/alerts`) + .set('kbn-xsrf', 'xxx') + .expect(200); + expect(body.alerts.length).to.eql(4); + compareArrays(tree.origin.relatedAlerts, body.alerts, true); + expect(body.nextAlert).to.eql(null); + }); + + it('should allow alerts to be filtered', async () => { + const filter = `not event.id:"${tree.origin.relatedAlerts[0].event.id}"`; + const { body }: { body: ResolverRelatedAlerts } = await supertest + .post(`/api/endpoint/resolver/${tree.origin.id}/alerts`) + .set('kbn-xsrf', 'xxx') + .send({ + filter, + }) + .expect(200); + expect(body.alerts.length).to.eql(3); + compareArrays(tree.origin.relatedAlerts, body.alerts); + expect(body.nextAlert).to.eql(null); + + // should not find the alert that we excluded in the filter + expect( + body.alerts.find((bodyAlert) => { + return eventId(bodyAlert) === tree.origin.relatedAlerts[0].event.id; + }) + ).to.not.be.ok(); + }); + + it('should return paginated results for the root node', async () => { + let { body }: { body: ResolverRelatedAlerts } = await supertest + .post(`/api/endpoint/resolver/${tree.origin.id}/alerts?alerts=2`) + .set('kbn-xsrf', 'xxx') + .expect(200); + expect(body.alerts.length).to.eql(2); + compareArrays(tree.origin.relatedAlerts, body.alerts); + expect(body.nextAlert).not.to.eql(null); + + ({ body } = await supertest + .post( + `/api/endpoint/resolver/${tree.origin.id}/alerts?alerts=2&afterAlert=${body.nextAlert}` + ) + .set('kbn-xsrf', 'xxx') + .expect(200)); + expect(body.alerts.length).to.eql(2); + compareArrays(tree.origin.relatedAlerts, body.alerts); + expect(body.nextAlert).to.not.eql(null); + + ({ body } = await supertest + .post( + `/api/endpoint/resolver/${tree.origin.id}/alerts?alerts=2&afterAlert=${body.nextAlert}` + ) + .set('kbn-xsrf', 'xxx') + .expect(200)); + expect(body.alerts).to.be.empty(); + expect(body.nextAlert).to.eql(null); + }); + + it('should return the first page of information when the cursor is invalid', async () => { + const { body }: { body: ResolverRelatedAlerts } = await supertest + .post(`/api/endpoint/resolver/${tree.origin.id}/alerts?afterAlert=blah`) + .set('kbn-xsrf', 'xxx') + .expect(200); + expect(body.alerts.length).to.eql(4); + compareArrays(tree.origin.relatedAlerts, body.alerts, true); + expect(body.nextAlert).to.eql(null); + }); + + it('should sort the alerts in ascending order', async () => { + const { body }: { body: ResolverRelatedAlerts } = await supertest + .post(`/api/endpoint/resolver/${tree.origin.id}/alerts`) + .set('kbn-xsrf', 'xxx') + .expect(200); + const sortedAsc = [...tree.origin.relatedAlerts].sort((event1, event2) => { + // this sorts the events by timestamp in ascending order + const diff = event1['@timestamp'] - event2['@timestamp']; + // if the timestamps are the same, fallback to the event.id sorted in + // ascending order + if (diff === 0) { + if (event1.event.id < event2.event.id) { + return -1; + } + if (event1.event.id > event2.event.id) { + return 1; + } + return 0; + } + return diff; + }); + + expect(body.alerts.length).to.eql(4); + for (let i = 0; i < body.alerts.length; i++) { + expect(eventId(body.alerts[i])).to.equal(sortedAsc[i].event.id); + } + }); + }); +} diff --git a/x-pack/test/security_solution_endpoint_api_int/apis/resolver/common.ts b/x-pack/test/security_solution_endpoint_api_int/apis/resolver/common.ts new file mode 100644 index 0000000000000..92d14fb94a2d8 --- /dev/null +++ b/x-pack/test/security_solution_endpoint_api_int/apis/resolver/common.ts @@ -0,0 +1,222 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import _ from 'lodash'; +import expect from '@kbn/expect'; +import { + ResolverChildNode, + ResolverLifecycleNode, + ResolverEvent, + ResolverNodeStats, +} from '../../../../plugins/security_solution/common/endpoint/types'; +import { + parentEntityId, + eventId, +} from '../../../../plugins/security_solution/common/endpoint/models/event'; +import { + Event, + Tree, + TreeNode, + RelatedEventInfo, + categoryMapping, +} from '../../../../plugins/security_solution/common/endpoint/generate_data'; + +/** + * Check that the given lifecycle is in the resolver tree's corresponding map + * + * @param node a lifecycle node containing the start and end events for a node + * @param nodeMap a map of entity_ids to nodes to look for the passed in `node` + */ +const expectLifecycleNodeInMap = (node: ResolverLifecycleNode, nodeMap: Map) => { + const genNode = nodeMap.get(node.entityID); + expect(genNode).to.be.ok(); + compareArrays(genNode!.lifecycle, node.lifecycle, true); +}; + +/** + * Verify that all the ancestor nodes are valid and optionally have parents. + * + * @param ancestors an array of ancestors + * @param tree the generated resolver tree as the source of truth + * @param verifyLastParent a boolean indicating whether to check the last ancestor. If the ancestors array intentionally + * does not contain all the ancestors, the last one will not have the parent + */ +export const verifyAncestry = ( + ancestors: ResolverLifecycleNode[], + tree: Tree, + verifyLastParent: boolean +) => { + // group the ancestors by their entity_id mapped to a lifecycle node + const groupedAncestors = _.groupBy(ancestors, (ancestor) => ancestor.entityID); + // group by parent entity_id + const groupedAncestorsParent = _.groupBy(ancestors, (ancestor) => + parentEntityId(ancestor.lifecycle[0]) + ); + // make sure there aren't any nodes with the same entity_id + expect(Object.keys(groupedAncestors).length).to.eql(ancestors.length); + // make sure there aren't any nodes with the same parent entity_id + expect(Object.keys(groupedAncestorsParent).length).to.eql(ancestors.length); + + // make sure each of the ancestors' lifecycle events are in the generated tree + for (const node of ancestors) { + expectLifecycleNodeInMap(node, tree.ancestry); + } + + // start at the origin which is always the first element of the array and make sure we have a connection + // using parent id between each of the nodes + let foundParents = 0; + let node = ancestors[0]; + for (let i = 0; i < ancestors.length; i++) { + const parentID = parentEntityId(node.lifecycle[0]); + if (parentID !== undefined) { + const nextNode = groupedAncestors[parentID]; + if (!nextNode) { + break; + } + // the grouped nodes should only have a single entry since each entity is unique + node = nextNode[0]; + } + foundParents++; + } + + if (verifyLastParent) { + expect(foundParents).to.eql(ancestors.length); + } else { + // if we only retrieved a portion of all the ancestors then the most distant grandparent's parent will not necessarily + // be in the results + expect(foundParents).to.eql(ancestors.length - 1); + } +}; + +/** + * Retrieves the most distant ancestor in the given array. + * + * @param ancestors an array of ancestor nodes + */ +export const retrieveDistantAncestor = (ancestors: ResolverLifecycleNode[]) => { + // group the ancestors by their entity_id mapped to a lifecycle node + const groupedAncestors = _.groupBy(ancestors, (ancestor) => ancestor.entityID); + let node = ancestors[0]; + for (let i = 0; i < ancestors.length; i++) { + const parentID = parentEntityId(node.lifecycle[0]); + if (parentID !== undefined) { + const nextNode = groupedAncestors[parentID]; + if (nextNode) { + node = nextNode[0]; + } else { + return node; + } + } + } + return node; +}; + +/** + * Verify that the children nodes are correct + * + * @param children the children nodes + * @param tree the generated resolver tree as the source of truth + * @param numberOfParents an optional number to compare that are a certain number of parents in the children array + * @param childrenPerParent an optional number to compare that there are a certain number of children for each parent + */ +export const verifyChildren = ( + children: ResolverChildNode[], + tree: Tree, + numberOfParents?: number, + childrenPerParent?: number +) => { + // group the children by their entity_id mapped to a child node + const groupedChildren = _.groupBy(children, (child) => child.entityID); + // make sure each child is unique + expect(Object.keys(groupedChildren).length).to.eql(children.length); + if (numberOfParents !== undefined) { + const groupParent = _.groupBy(children, (child) => parentEntityId(child.lifecycle[0])); + expect(Object.keys(groupParent).length).to.eql(numberOfParents); + if (childrenPerParent !== undefined) { + Object.values(groupParent).forEach((childNodes) => + expect(childNodes.length).to.be(childrenPerParent) + ); + } + } + + children.forEach((child) => { + expectLifecycleNodeInMap(child, tree.children); + }); +}; + +/** + * Compare an array of events returned from an API with an array of events generated + * + * @param expected an array to use as the source of truth + * @param toTest the array to test against the source of truth + * @param lengthCheck an optional flag to check that the arrays are the same length + */ +export const compareArrays = ( + expected: Event[], + toTest: ResolverEvent[], + lengthCheck: boolean = false +) => { + if (lengthCheck) { + expect(expected.length).to.eql(toTest.length); + } + + toTest.forEach((toTestEvent) => { + expect( + expected.find((arrEvent) => { + // we're only checking that the event ids are the same here. The reason we can't check the entire document + // is because ingest pipelines are used to add fields to the document when it is received by elasticsearch, + // therefore it will not be the same as the document created by the generator + return eventId(toTestEvent) === eventId(arrEvent); + }) + ).to.be.ok(); + }); +}; + +/** + * Verifies that the stats received from ES for a node reflect the categories of events that the generator created. + * + * @param relatedEvents the related events received for a particular node + * @param categories the related event info used when generating the resolver tree + */ +export const verifyStats = ( + stats: ResolverNodeStats | undefined, + categories: RelatedEventInfo[], + relatedAlerts: number +) => { + expect(stats).to.not.be(undefined); + let totalExpEvents = 0; + for (const cat of categories) { + const ecsCategories = categoryMapping[cat.category]; + if (Array.isArray(ecsCategories)) { + // if there are multiple ecs categories used to define a related event, the count for all of them should be the same + // and they should equal what is defined in the categories used to generate the related events + for (const ecsCat of ecsCategories) { + expect(stats?.events.byCategory[ecsCat]).to.be(cat.count); + } + } else { + expect(stats?.events.byCategory[ecsCategories]).to.be(cat.count); + } + + totalExpEvents += cat.count; + } + expect(stats?.events.total).to.be(totalExpEvents); + expect(stats?.totalAlerts); +}; + +/** + * A helper function for verifying the stats information an array of nodes. + * + * @param nodes an array of lifecycle nodes that should have a stats field defined + * @param categories the related event info used when generating the resolver tree + */ +export const verifyLifecycleStats = ( + nodes: ResolverLifecycleNode[], + categories: RelatedEventInfo[], + relatedAlerts: number +) => { + for (const node of nodes) { + verifyStats(node.stats, categories, relatedAlerts); + } +}; diff --git a/x-pack/test/security_solution_endpoint_api_int/apis/resolver/events.ts b/x-pack/test/security_solution_endpoint_api_int/apis/resolver/events.ts new file mode 100644 index 0000000000000..c0e4e466c7b62 --- /dev/null +++ b/x-pack/test/security_solution_endpoint_api_int/apis/resolver/events.ts @@ -0,0 +1,213 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import expect from '@kbn/expect'; +import { eventId } from '../../../../plugins/security_solution/common/endpoint/models/event'; +import { ResolverRelatedEvents } from '../../../../plugins/security_solution/common/endpoint/types'; +import { FtrProviderContext } from '../../ftr_provider_context'; +import { + Tree, + RelatedEventCategory, +} from '../../../../plugins/security_solution/common/endpoint/generate_data'; +import { Options, GeneratedTrees } from '../../services/resolver'; +import { compareArrays } from './common'; + +export default function ({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const resolver = getService('resolverGenerator'); + const esArchiver = getService('esArchiver'); + + const relatedEventsToGen = [ + { category: RelatedEventCategory.Driver, count: 2 }, + { category: RelatedEventCategory.File, count: 1 }, + { category: RelatedEventCategory.Registry, count: 1 }, + ]; + const relatedAlerts = 4; + let resolverTrees: GeneratedTrees; + let tree: Tree; + const treeOptions: Options = { + ancestors: 5, + relatedEvents: relatedEventsToGen, + relatedEventsOrdered: true, + relatedAlerts, + children: 3, + generations: 2, + percentTerminated: 100, + percentWithRelated: 100, + numTrees: 1, + alwaysGenMaxChildrenPerNode: true, + ancestryArraySize: 2, + }; + + describe('related events route', () => { + before(async () => { + await esArchiver.load('endpoint/resolver/api_feature'); + resolverTrees = await resolver.createTrees(treeOptions); + // we only requested a single alert so there's only 1 tree + tree = resolverTrees.trees[0]; + }); + after(async () => { + await resolver.deleteData(resolverTrees); + await esArchiver.unload('endpoint/resolver/api_feature'); + }); + + describe('legacy events', () => { + const endpointID = '5a0c957f-b8e7-4538-965e-57e8bb86ad3a'; + const entityID = '94042'; + const cursor = 'eyJ0aW1lc3RhbXAiOjE1ODE0NTYyNTUwMDAsImV2ZW50SUQiOiI5NDA0MyJ9'; + + it('should return details for the root node', async () => { + const { body }: { body: ResolverRelatedEvents } = await supertest + .post(`/api/endpoint/resolver/${entityID}/events?legacyEndpointID=${endpointID}`) + .set('kbn-xsrf', 'xxx') + .expect(200); + expect(body.events.length).to.eql(1); + expect(body.entityID).to.eql(entityID); + expect(body.nextEvent).to.eql(null); + }); + + it('returns no values when there is no more data', async () => { + const { body }: { body: ResolverRelatedEvents } = await supertest + // after is set to the document id of the last event so there shouldn't be any more after it + .post( + `/api/endpoint/resolver/${entityID}/events?legacyEndpointID=${endpointID}&afterEvent=${cursor}` + ) + .set('kbn-xsrf', 'xxx') + .expect(200); + expect(body.events).be.empty(); + expect(body.entityID).to.eql(entityID); + expect(body.nextEvent).to.eql(null); + }); + + it('should return the first page of information when the cursor is invalid', async () => { + const { body }: { body: ResolverRelatedEvents } = await supertest + .post( + `/api/endpoint/resolver/${entityID}/events?legacyEndpointID=${endpointID}&afterEvent=blah` + ) + .set('kbn-xsrf', 'xxx') + .expect(200); + expect(body.entityID).to.eql(entityID); + expect(body.nextEvent).to.eql(null); + }); + + it('should return no results for an invalid endpoint ID', async () => { + const { body }: { body: ResolverRelatedEvents } = await supertest + .post(`/api/endpoint/resolver/${entityID}/events?legacyEndpointID=foo`) + .set('kbn-xsrf', 'xxx') + .expect(200); + expect(body.nextEvent).to.eql(null); + expect(body.entityID).to.eql(entityID); + expect(body.events).to.be.empty(); + }); + + it('should error on invalid pagination values', async () => { + await supertest + .post(`/api/endpoint/resolver/${entityID}/events?events=0`) + .set('kbn-xsrf', 'xxx') + .expect(400); + await supertest + .post(`/api/endpoint/resolver/${entityID}/events?events=20000`) + .set('kbn-xsrf', 'xxx') + .expect(400); + await supertest + .post(`/api/endpoint/resolver/${entityID}/events?events=-1`) + .set('kbn-xsrf', 'xxx') + .expect(400); + }); + }); + + describe('endpoint events', () => { + it('should not find any events', async () => { + const { body }: { body: ResolverRelatedEvents } = await supertest + .post(`/api/endpoint/resolver/5555/events`) + .set('kbn-xsrf', 'xxx') + .expect(200); + expect(body.nextEvent).to.eql(null); + expect(body.events).to.be.empty(); + }); + + it('should return details for the root node', async () => { + const { body }: { body: ResolverRelatedEvents } = await supertest + .post(`/api/endpoint/resolver/${tree.origin.id}/events`) + .set('kbn-xsrf', 'xxx') + .expect(200); + expect(body.events.length).to.eql(4); + compareArrays(tree.origin.relatedEvents, body.events, true); + expect(body.nextEvent).to.eql(null); + }); + + it('should allow for the events to be filtered', async () => { + const filter = `event.category:"${RelatedEventCategory.Driver}"`; + const { body }: { body: ResolverRelatedEvents } = await supertest + .post(`/api/endpoint/resolver/${tree.origin.id}/events`) + .set('kbn-xsrf', 'xxx') + .send({ + filter, + }) + .expect(200); + expect(body.events.length).to.eql(2); + compareArrays(tree.origin.relatedEvents, body.events); + expect(body.nextEvent).to.eql(null); + for (const event of body.events) { + expect(event.event?.category).to.be(RelatedEventCategory.Driver); + } + }); + + it('should return paginated results for the root node', async () => { + let { body }: { body: ResolverRelatedEvents } = await supertest + .post(`/api/endpoint/resolver/${tree.origin.id}/events?events=2`) + .set('kbn-xsrf', 'xxx') + .expect(200); + expect(body.events.length).to.eql(2); + compareArrays(tree.origin.relatedEvents, body.events); + expect(body.nextEvent).not.to.eql(null); + + ({ body } = await supertest + .post( + `/api/endpoint/resolver/${tree.origin.id}/events?events=2&afterEvent=${body.nextEvent}` + ) + .set('kbn-xsrf', 'xxx') + .expect(200)); + expect(body.events.length).to.eql(2); + compareArrays(tree.origin.relatedEvents, body.events); + expect(body.nextEvent).to.not.eql(null); + + ({ body } = await supertest + .post( + `/api/endpoint/resolver/${tree.origin.id}/events?events=2&afterEvent=${body.nextEvent}` + ) + .set('kbn-xsrf', 'xxx') + .expect(200)); + expect(body.events).to.be.empty(); + expect(body.nextEvent).to.eql(null); + }); + + it('should return the first page of information when the cursor is invalid', async () => { + const { body }: { body: ResolverRelatedEvents } = await supertest + .post(`/api/endpoint/resolver/${tree.origin.id}/events?afterEvent=blah`) + .set('kbn-xsrf', 'xxx') + .expect(200); + expect(body.events.length).to.eql(4); + compareArrays(tree.origin.relatedEvents, body.events, true); + expect(body.nextEvent).to.eql(null); + }); + + it('should sort the events in descending order', async () => { + const { body }: { body: ResolverRelatedEvents } = await supertest + .post(`/api/endpoint/resolver/${tree.origin.id}/events`) + .set('kbn-xsrf', 'xxx') + .expect(200); + expect(body.events.length).to.eql(4); + // these events are created in the order they are defined in the array so the newest one is + // the last element in the array so let's reverse it + const relatedEvents = tree.origin.relatedEvents.reverse(); + for (let i = 0; i < body.events.length; i++) { + expect(body.events[i].event?.category).to.equal(relatedEvents[i].event.category); + expect(eventId(body.events[i])).to.equal(relatedEvents[i].event.id); + } + }); + }); + }); +} diff --git a/x-pack/test/security_solution_endpoint_api_int/apis/resolver/index.ts b/x-pack/test/security_solution_endpoint_api_int/apis/resolver/index.ts index dc9a1fab9ec02..fc603af3619a4 100644 --- a/x-pack/test/security_solution_endpoint_api_int/apis/resolver/index.ts +++ b/x-pack/test/security_solution_endpoint_api_int/apis/resolver/index.ts @@ -12,5 +12,7 @@ export default function (providerContext: FtrProviderContext) { loadTestFile(require.resolve('./entity_id')); loadTestFile(require.resolve('./children')); loadTestFile(require.resolve('./tree')); + loadTestFile(require.resolve('./alerts')); + loadTestFile(require.resolve('./events')); }); } diff --git a/x-pack/test/security_solution_endpoint_api_int/apis/resolver/tree.ts b/x-pack/test/security_solution_endpoint_api_int/apis/resolver/tree.ts index f4836379ca273..957d559087f5e 100644 --- a/x-pack/test/security_solution_endpoint_api_int/apis/resolver/tree.ts +++ b/x-pack/test/security_solution_endpoint_api_int/apis/resolver/tree.ts @@ -3,232 +3,28 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import _ from 'lodash'; import expect from '@kbn/expect'; import { - ResolverChildNode, - ResolverLifecycleNode, ResolverAncestry, - ResolverEvent, - ResolverRelatedEvents, ResolverChildren, ResolverTree, LegacyEndpointEvent, - ResolverNodeStats, - ResolverRelatedAlerts, } from '../../../../plugins/security_solution/common/endpoint/types'; -import { - parentEntityId, - eventId, -} from '../../../../plugins/security_solution/common/endpoint/models/event'; +import { parentEntityId } from '../../../../plugins/security_solution/common/endpoint/models/event'; import { FtrProviderContext } from '../../ftr_provider_context'; import { - Event, Tree, - TreeNode, RelatedEventCategory, - RelatedEventInfo, - categoryMapping, } from '../../../../plugins/security_solution/common/endpoint/generate_data'; import { Options, GeneratedTrees } from '../../services/resolver'; - -/** - * Check that the given lifecycle is in the resolver tree's corresponding map - * - * @param node a lifecycle node containing the start and end events for a node - * @param nodeMap a map of entity_ids to nodes to look for the passed in `node` - */ -const expectLifecycleNodeInMap = (node: ResolverLifecycleNode, nodeMap: Map) => { - const genNode = nodeMap.get(node.entityID); - expect(genNode).to.be.ok(); - compareArrays(genNode!.lifecycle, node.lifecycle, true); -}; - -/** - * Verify that all the ancestor nodes are valid and optionally have parents. - * - * @param ancestors an array of ancestors - * @param tree the generated resolver tree as the source of truth - * @param verifyLastParent a boolean indicating whether to check the last ancestor. If the ancestors array intentionally - * does not contain all the ancestors, the last one will not have the parent - */ -const verifyAncestry = ( - ancestors: ResolverLifecycleNode[], - tree: Tree, - verifyLastParent: boolean -) => { - // group the ancestors by their entity_id mapped to a lifecycle node - const groupedAncestors = _.groupBy(ancestors, (ancestor) => ancestor.entityID); - // group by parent entity_id - const groupedAncestorsParent = _.groupBy(ancestors, (ancestor) => - parentEntityId(ancestor.lifecycle[0]) - ); - // make sure there aren't any nodes with the same entity_id - expect(Object.keys(groupedAncestors).length).to.eql(ancestors.length); - // make sure there aren't any nodes with the same parent entity_id - expect(Object.keys(groupedAncestorsParent).length).to.eql(ancestors.length); - - // make sure each of the ancestors' lifecycle events are in the generated tree - for (const node of ancestors) { - expectLifecycleNodeInMap(node, tree.ancestry); - } - - // start at the origin which is always the first element of the array and make sure we have a connection - // using parent id between each of the nodes - let foundParents = 0; - let node = ancestors[0]; - for (let i = 0; i < ancestors.length; i++) { - const parentID = parentEntityId(node.lifecycle[0]); - if (parentID !== undefined) { - const nextNode = groupedAncestors[parentID]; - if (!nextNode) { - break; - } - // the grouped nodes should only have a single entry since each entity is unique - node = nextNode[0]; - } - foundParents++; - } - - if (verifyLastParent) { - expect(foundParents).to.eql(ancestors.length); - } else { - // if we only retrieved a portion of all the ancestors then the most distant grandparent's parent will not necessarily - // be in the results - expect(foundParents).to.eql(ancestors.length - 1); - } -}; - -/** - * Retrieves the most distant ancestor in the given array. - * - * @param ancestors an array of ancestor nodes - */ -const retrieveDistantAncestor = (ancestors: ResolverLifecycleNode[]) => { - // group the ancestors by their entity_id mapped to a lifecycle node - const groupedAncestors = _.groupBy(ancestors, (ancestor) => ancestor.entityID); - let node = ancestors[0]; - for (let i = 0; i < ancestors.length; i++) { - const parentID = parentEntityId(node.lifecycle[0]); - if (parentID !== undefined) { - const nextNode = groupedAncestors[parentID]; - if (nextNode) { - node = nextNode[0]; - } else { - return node; - } - } - } - return node; -}; - -/** - * Verify that the children nodes are correct - * - * @param children the children nodes - * @param tree the generated resolver tree as the source of truth - * @param numberOfParents an optional number to compare that are a certain number of parents in the children array - * @param childrenPerParent an optional number to compare that there are a certain number of children for each parent - */ -const verifyChildren = ( - children: ResolverChildNode[], - tree: Tree, - numberOfParents?: number, - childrenPerParent?: number -) => { - // group the children by their entity_id mapped to a child node - const groupedChildren = _.groupBy(children, (child) => child.entityID); - // make sure each child is unique - expect(Object.keys(groupedChildren).length).to.eql(children.length); - if (numberOfParents !== undefined) { - const groupParent = _.groupBy(children, (child) => parentEntityId(child.lifecycle[0])); - expect(Object.keys(groupParent).length).to.eql(numberOfParents); - if (childrenPerParent !== undefined) { - Object.values(groupParent).forEach((childNodes) => - expect(childNodes.length).to.be(childrenPerParent) - ); - } - } - - children.forEach((child) => { - expectLifecycleNodeInMap(child, tree.children); - }); -}; - -/** - * Compare an array of events returned from an API with an array of events generated - * - * @param expected an array to use as the source of truth - * @param toTest the array to test against the source of truth - * @param lengthCheck an optional flag to check that the arrays are the same length - */ -const compareArrays = ( - expected: Event[], - toTest: ResolverEvent[], - lengthCheck: boolean = false -) => { - if (lengthCheck) { - expect(expected.length).to.eql(toTest.length); - } - - toTest.forEach((toTestEvent) => { - expect( - expected.find((arrEvent) => { - // we're only checking that the event ids are the same here. The reason we can't check the entire document - // is because ingest pipelines are used to add fields to the document when it is received by elasticsearch, - // therefore it will not be the same as the document created by the generator - return eventId(toTestEvent) === eventId(arrEvent); - }) - ).to.be.ok(); - }); -}; - -/** - * Verifies that the stats received from ES for a node reflect the categories of events that the generator created. - * - * @param relatedEvents the related events received for a particular node - * @param categories the related event info used when generating the resolver tree - */ -const verifyStats = ( - stats: ResolverNodeStats | undefined, - categories: RelatedEventInfo[], - relatedAlerts: number -) => { - expect(stats).to.not.be(undefined); - let totalExpEvents = 0; - for (const cat of categories) { - const ecsCategories = categoryMapping[cat.category]; - if (Array.isArray(ecsCategories)) { - // if there are multiple ecs categories used to define a related event, the count for all of them should be the same - // and they should equal what is defined in the categories used to generate the related events - for (const ecsCat of ecsCategories) { - expect(stats?.events.byCategory[ecsCat]).to.be(cat.count); - } - } else { - expect(stats?.events.byCategory[ecsCategories]).to.be(cat.count); - } - - totalExpEvents += cat.count; - } - expect(stats?.events.total).to.be(totalExpEvents); - expect(stats?.totalAlerts); -}; - -/** - * A helper function for verifying the stats information an array of nodes. - * - * @param nodes an array of lifecycle nodes that should have a stats field defined - * @param categories the related event info used when generating the resolver tree - */ -const verifyLifecycleStats = ( - nodes: ResolverLifecycleNode[], - categories: RelatedEventInfo[], - relatedAlerts: number -) => { - for (const node of nodes) { - verifyStats(node.stats, categories, relatedAlerts); - } -}; +import { + compareArrays, + verifyAncestry, + retrieveDistantAncestor, + verifyChildren, + verifyLifecycleStats, + verifyStats, +} from './common'; export default function ({ getService }: FtrProviderContext) { const supertest = getService('supertest'); @@ -269,170 +65,6 @@ export default function ({ getService }: FtrProviderContext) { await esArchiver.unload('endpoint/resolver/api_feature'); }); - describe('related alerts route', () => { - describe('endpoint events', () => { - it('should not find any alerts', async () => { - const { body }: { body: ResolverRelatedAlerts } = await supertest - .get(`/api/endpoint/resolver/5555/alerts`) - .expect(200); - expect(body.nextAlert).to.eql(null); - expect(body.alerts).to.be.empty(); - }); - - it('should return details for the root node', async () => { - const { body }: { body: ResolverRelatedAlerts } = await supertest - .get(`/api/endpoint/resolver/${tree.origin.id}/alerts`) - .expect(200); - expect(body.alerts.length).to.eql(4); - compareArrays(tree.origin.relatedAlerts, body.alerts, true); - expect(body.nextAlert).to.eql(null); - }); - - it('should return paginated results for the root node', async () => { - let { body }: { body: ResolverRelatedAlerts } = await supertest - .get(`/api/endpoint/resolver/${tree.origin.id}/alerts?alerts=2`) - .expect(200); - expect(body.alerts.length).to.eql(2); - compareArrays(tree.origin.relatedAlerts, body.alerts); - expect(body.nextAlert).not.to.eql(null); - - ({ body } = await supertest - .get( - `/api/endpoint/resolver/${tree.origin.id}/alerts?alerts=2&afterAlert=${body.nextAlert}` - ) - .expect(200)); - expect(body.alerts.length).to.eql(2); - compareArrays(tree.origin.relatedAlerts, body.alerts); - expect(body.nextAlert).to.not.eql(null); - - ({ body } = await supertest - .get( - `/api/endpoint/resolver/${tree.origin.id}/alerts?alerts=2&afterAlert=${body.nextAlert}` - ) - .expect(200)); - expect(body.alerts).to.be.empty(); - expect(body.nextAlert).to.eql(null); - }); - - it('should return the first page of information when the cursor is invalid', async () => { - const { body }: { body: ResolverRelatedAlerts } = await supertest - .get(`/api/endpoint/resolver/${tree.origin.id}/alerts?afterAlert=blah`) - .expect(200); - expect(body.alerts.length).to.eql(4); - compareArrays(tree.origin.relatedAlerts, body.alerts, true); - expect(body.nextAlert).to.eql(null); - }); - }); - }); - - describe('related events route', () => { - describe('legacy events', () => { - const endpointID = '5a0c957f-b8e7-4538-965e-57e8bb86ad3a'; - const entityID = '94042'; - const cursor = 'eyJ0aW1lc3RhbXAiOjE1ODE0NTYyNTUwMDAsImV2ZW50SUQiOiI5NDA0MyJ9'; - - it('should return details for the root node', async () => { - const { body }: { body: ResolverRelatedEvents } = await supertest - .get(`/api/endpoint/resolver/${entityID}/events?legacyEndpointID=${endpointID}`) - .expect(200); - expect(body.events.length).to.eql(1); - expect(body.entityID).to.eql(entityID); - expect(body.nextEvent).to.eql(null); - }); - - it('returns no values when there is no more data', async () => { - const { body }: { body: ResolverRelatedEvents } = await supertest - // after is set to the document id of the last event so there shouldn't be any more after it - .get( - `/api/endpoint/resolver/${entityID}/events?legacyEndpointID=${endpointID}&afterEvent=${cursor}` - ) - .expect(200); - expect(body.events).be.empty(); - expect(body.entityID).to.eql(entityID); - expect(body.nextEvent).to.eql(null); - }); - - it('should return the first page of information when the cursor is invalid', async () => { - const { body }: { body: ResolverRelatedEvents } = await supertest - .get( - `/api/endpoint/resolver/${entityID}/events?legacyEndpointID=${endpointID}&afterEvent=blah` - ) - .expect(200); - expect(body.entityID).to.eql(entityID); - expect(body.nextEvent).to.eql(null); - }); - - it('should return no results for an invalid endpoint ID', async () => { - const { body }: { body: ResolverRelatedEvents } = await supertest - .get(`/api/endpoint/resolver/${entityID}/events?legacyEndpointID=foo`) - .expect(200); - expect(body.nextEvent).to.eql(null); - expect(body.entityID).to.eql(entityID); - expect(body.events).to.be.empty(); - }); - - it('should error on invalid pagination values', async () => { - await supertest.get(`/api/endpoint/resolver/${entityID}/events?events=0`).expect(400); - await supertest.get(`/api/endpoint/resolver/${entityID}/events?events=20000`).expect(400); - await supertest.get(`/api/endpoint/resolver/${entityID}/events?events=-1`).expect(400); - }); - }); - - describe('endpoint events', () => { - it('should not find any events', async () => { - const { body }: { body: ResolverRelatedEvents } = await supertest - .get(`/api/endpoint/resolver/5555/events`) - .expect(200); - expect(body.nextEvent).to.eql(null); - expect(body.events).to.be.empty(); - }); - - it('should return details for the root node', async () => { - const { body }: { body: ResolverRelatedEvents } = await supertest - .get(`/api/endpoint/resolver/${tree.origin.id}/events`) - .expect(200); - expect(body.events.length).to.eql(4); - compareArrays(tree.origin.relatedEvents, body.events, true); - expect(body.nextEvent).to.eql(null); - }); - - it('should return paginated results for the root node', async () => { - let { body }: { body: ResolverRelatedEvents } = await supertest - .get(`/api/endpoint/resolver/${tree.origin.id}/events?events=2`) - .expect(200); - expect(body.events.length).to.eql(2); - compareArrays(tree.origin.relatedEvents, body.events); - expect(body.nextEvent).not.to.eql(null); - - ({ body } = await supertest - .get( - `/api/endpoint/resolver/${tree.origin.id}/events?events=2&afterEvent=${body.nextEvent}` - ) - .expect(200)); - expect(body.events.length).to.eql(2); - compareArrays(tree.origin.relatedEvents, body.events); - expect(body.nextEvent).to.not.eql(null); - - ({ body } = await supertest - .get( - `/api/endpoint/resolver/${tree.origin.id}/events?events=2&afterEvent=${body.nextEvent}` - ) - .expect(200)); - expect(body.events).to.be.empty(); - expect(body.nextEvent).to.eql(null); - }); - - it('should return the first page of information when the cursor is invalid', async () => { - const { body }: { body: ResolverRelatedEvents } = await supertest - .get(`/api/endpoint/resolver/${tree.origin.id}/events?afterEvent=blah`) - .expect(200); - expect(body.events.length).to.eql(4); - compareArrays(tree.origin.relatedEvents, body.events, true); - expect(body.nextEvent).to.eql(null); - }); - }); - }); - describe('ancestry events route', () => { describe('legacy events', () => { const endpointID = '5a0c957f-b8e7-4538-965e-57e8bb86ad3a'; From 3541edbb5d86dc58824e7abe82a5b326b1b745a9 Mon Sep 17 00:00:00 2001 From: Nathan L Smith Date: Wed, 26 Aug 2020 08:30:47 -0500 Subject: [PATCH 64/71] Minor developer guide doc changes (#75763) --- docs/developer/best-practices/index.asciidoc | 2 +- docs/developer/best-practices/stability.asciidoc | 10 +++++----- .../developer/getting-started/building-kibana.asciidoc | 4 ++-- docs/developer/getting-started/index.asciidoc | 4 ++-- .../getting-started/running-kibana-advanced.asciidoc | 6 +++--- 5 files changed, 13 insertions(+), 13 deletions(-) diff --git a/docs/developer/best-practices/index.asciidoc b/docs/developer/best-practices/index.asciidoc index 63a44b54d454f..42cee6ef0e58a 100644 --- a/docs/developer/best-practices/index.asciidoc +++ b/docs/developer/best-practices/index.asciidoc @@ -48,7 +48,7 @@ guidelines] * Write all new code on {kib-repo}blob/{branch}/src/core/README.md[the platform], and following -{kib-repo}blob/{branch}/src/core/CONVENTIONS.md[conventions] +{kib-repo}blob/{branch}/src/core/CONVENTIONS.md[conventions]. * _Always_ use the `SavedObjectClient` for reading and writing Saved Objects. * Add `README`s to all your plugins and services. diff --git a/docs/developer/best-practices/stability.asciidoc b/docs/developer/best-practices/stability.asciidoc index f4b7ae1229909..348412e593d9e 100644 --- a/docs/developer/best-practices/stability.asciidoc +++ b/docs/developer/best-practices/stability.asciidoc @@ -52,15 +52,15 @@ storeinSessions?) [discrete] === Browser coverage -Refer to the list of browsers and OS {kib} supports +Refer to the list of browsers and OS {kib} supports: https://www.elastic.co/support/matrix Does the feature work efficiently on the list of supported browsers? [discrete] -=== Upgrade Scenarios - Migration scenarios- +=== Upgrade and Migration scenarios -Does the feature affect old -indices, saved objects ? - Has the feature been tested with {kib} -aliases - Read/Write privileges of the indices before and after the +* Does the feature affect old indices or saved objects? +* Has the feature been tested with {kib} aliases? +* Read/Write privileges of the indices before and after the upgrade? diff --git a/docs/developer/getting-started/building-kibana.asciidoc b/docs/developer/getting-started/building-kibana.asciidoc index 72054b1628fc2..04771b34bf69f 100644 --- a/docs/developer/getting-started/building-kibana.asciidoc +++ b/docs/developer/getting-started/building-kibana.asciidoc @@ -1,7 +1,7 @@ [[building-kibana]] == Building a {kib} distributable -The following commands will build a {kib} production distributable. +The following command will build a {kib} production distributable: [source,bash] ---- @@ -36,4 +36,4 @@ To specify a package to build you can add `rpm` or `deb` as an argument. yarn build --rpm ---- -Distributable packages can be found in `target/` after the build completes. \ No newline at end of file +Distributable packages can be found in `target/` after the build completes. diff --git a/docs/developer/getting-started/index.asciidoc b/docs/developer/getting-started/index.asciidoc index eaa35eece5a2c..10e603a8da8bb 100644 --- a/docs/developer/getting-started/index.asciidoc +++ b/docs/developer/getting-started/index.asciidoc @@ -49,7 +49,7 @@ ____ (You can also run `yarn kbn` to see the other available commands. For more info about this tool, see -{kib-repo}tree/{branch}/packages/kbn-pm[{kib-repo}tree/{branch}packages/kbn-pm].) +{kib-repo}tree/{branch}/packages/kbn-pm[{kib-repo}tree/{branch}/packages/kbn-pm].) When switching branches which use different versions of npm packages you may need to run: @@ -137,4 +137,4 @@ include::debugging.asciidoc[leveloffset=+1] include::building-kibana.asciidoc[leveloffset=+1] -include::development-plugin-resources.asciidoc[leveloffset=+1] \ No newline at end of file +include::development-plugin-resources.asciidoc[leveloffset=+1] diff --git a/docs/developer/getting-started/running-kibana-advanced.asciidoc b/docs/developer/getting-started/running-kibana-advanced.asciidoc index c3b7847b0f8ba..44897184f88f2 100644 --- a/docs/developer/getting-started/running-kibana-advanced.asciidoc +++ b/docs/developer/getting-started/running-kibana-advanced.asciidoc @@ -73,8 +73,8 @@ settings]. [discrete] === Potential Optimization Pitfalls -* Webpack is trying to include a file in the bundle that I deleted and -is now complaining about it is missing +* Webpack is trying to include a file in the bundle that was deleted and +is now complaining about it being missing * A module id that used to resolve to a single file now resolves to a directory, but webpack isn’t adapting * (if you discover other scenarios, please send a PR!) @@ -84,4 +84,4 @@ directory, but webpack isn’t adapting {kib} includes self-signed certificates that can be used for development purposes in the browser and for communicating with -{es}: `yarn start --ssl` & `yarn es snapshot --ssl`. \ No newline at end of file +{es}: `yarn start --ssl` & `yarn es snapshot --ssl`. From 4f2d4f8b018950481f1841792a3b7243152ae39b Mon Sep 17 00:00:00 2001 From: Bhavya RM Date: Wed, 26 Aug 2020 09:59:41 -0400 Subject: [PATCH 65/71] adding test user to pew pew maps test + adding a role for connections index pattern (#75920) --- x-pack/test/functional/apps/maps/es_pew_pew_source.js | 6 ++++++ x-pack/test/functional/config.js | 11 +++++++++++ 2 files changed, 17 insertions(+) diff --git a/x-pack/test/functional/apps/maps/es_pew_pew_source.js b/x-pack/test/functional/apps/maps/es_pew_pew_source.js index 382bde510170f..b0f98f807fd44 100644 --- a/x-pack/test/functional/apps/maps/es_pew_pew_source.js +++ b/x-pack/test/functional/apps/maps/es_pew_pew_source.js @@ -9,14 +9,20 @@ import expect from '@kbn/expect'; export default function ({ getPageObjects, getService }) { const PageObjects = getPageObjects(['maps']); const inspector = getService('inspector'); + const security = getService('security'); const VECTOR_SOURCE_ID = '67c1de2c-2fc5-4425-8983-094b589afe61'; describe('point to point source', () => { before(async () => { + await security.testUser.setRoles(['global_maps_all', 'geoconnections_data_reader']); await PageObjects.maps.loadSavedMap('pew pew demo'); }); + after(async () => { + await security.testUser.restoreDefaults(); + }); + it('should request source clusters for destination locations', async () => { await inspector.open(); await inspector.openInspectorRequestsView(); diff --git a/x-pack/test/functional/config.js b/x-pack/test/functional/config.js index fdd694e73394e..003d842cc3d6f 100644 --- a/x-pack/test/functional/config.js +++ b/x-pack/test/functional/config.js @@ -273,6 +273,17 @@ export default async function ({ readConfigFile }) { }, }, + geoconnections_data_reader: { + elasticsearch: { + indices: [ + { + names: ['connections*'], + privileges: ['read', 'view_index_metadata'], + }, + ], + }, + }, + global_devtools_read: { kibana: [ { From 4e1b1b5d9e3ab98e177c5b8c763d08918784aad7 Mon Sep 17 00:00:00 2001 From: Bhavya RM Date: Wed, 26 Aug 2020 10:02:10 -0400 Subject: [PATCH 66/71] adding test user to auto fit to bounds test (#75914) --- x-pack/test/functional/apps/maps/auto_fit_to_bounds.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/x-pack/test/functional/apps/maps/auto_fit_to_bounds.js b/x-pack/test/functional/apps/maps/auto_fit_to_bounds.js index d3d4fe054ec34..0e8775fa611b5 100644 --- a/x-pack/test/functional/apps/maps/auto_fit_to_bounds.js +++ b/x-pack/test/functional/apps/maps/auto_fit_to_bounds.js @@ -6,17 +6,23 @@ import expect from '@kbn/expect'; -export default function ({ getPageObjects }) { +export default function ({ getPageObjects, getService }) { const PageObjects = getPageObjects(['maps']); + const security = getService('security'); describe('auto fit map to bounds', () => { describe('initial location', () => { before(async () => { + await security.testUser.setRoles(['global_maps_all', 'test_logstash_reader']); await PageObjects.maps.loadSavedMap( 'document example - auto fit to bounds for initial location' ); }); + after(async () => { + await security.testUser.restoreDefaults(); + }); + it('should automatically fit to bounds on initial map load', async () => { const hits = await PageObjects.maps.getHits(); expect(hits).to.equal('6'); From b9c820120202dc44296e080550e87c93bd37dd55 Mon Sep 17 00:00:00 2001 From: Yara Tercero Date: Wed, 26 Aug 2020 10:16:17 -0400 Subject: [PATCH 67/71] [Security Solution][Exceptions] - Improve UX for missing exception list associated with rule (#75898) ## Summary **Current behavior:** - **Scenario 1:** User is in the exceptions viewer flow, they select to edit an exception item, but the list the item is associated with has since been deleted (let's say by another user) - a user is able to open modal to edit exception item and on save, an error toaster shows but no information is given to the user to indicate the issue. - **Scenario 2:** User exports rules from space 'X' and imports into space 'Y'. The exception lists associated with their newly imported rules do not exist in space 'Y' - a user goes to add an exception item and gets a modal with an error, unable to add any exceptions. - **Workaround:** current workaround exists only via API - user would need to remove the exception list from their rule via API **New behavior:** - **Scenario 1:** User is still able to oped edit modal, but on save they see an error explaining that the associated exception list does not exist and prompts them to remove the exception list --> now they're able to add exceptions to their rule - **Scenario 2:** User navigates to exceptions after importing their rule, tries to add exception, modal pops up with error informing them that they need to remove association to missing exception list, button prompts them to do so --> now can continue adding exceptions to rule --- .../exceptions/add_exception_modal/index.tsx | 90 +++++++--- .../edit_exception_modal/index.test.tsx | 5 + .../exceptions/edit_exception_modal/index.tsx | 91 +++++++--- .../exceptions/error_callout.test.tsx | 160 +++++++++++++++++ .../components/exceptions/error_callout.tsx | 169 ++++++++++++++++++ .../components/exceptions/translations.ts | 49 +++++ .../exceptions/use_add_exception.test.tsx | 44 +++++ .../exceptions/use_add_exception.tsx | 8 +- ...tch_or_create_rule_exception_list.test.tsx | 2 +- ...se_fetch_or_create_rule_exception_list.tsx | 8 +- .../components/exceptions/viewer/index.tsx | 2 + .../use_dissasociate_exception_list.test.tsx | 52 ++++++ .../rules/use_dissasociate_exception_list.tsx | 80 +++++++++ 13 files changed, 706 insertions(+), 54 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/common/components/exceptions/error_callout.test.tsx create mode 100644 x-pack/plugins/security_solution/public/common/components/exceptions/error_callout.tsx create mode 100644 x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_dissasociate_exception_list.test.tsx create mode 100644 x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_dissasociate_exception_list.tsx diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx index 03051ead357c9..21f82c6ab4c98 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx @@ -18,7 +18,6 @@ import { EuiCheckbox, EuiSpacer, EuiFormRow, - EuiCallOut, EuiText, } from '@elastic/eui'; import { Status } from '../../../../../common/detection_engine/schemas/common/schemas'; @@ -28,6 +27,7 @@ import { ExceptionListType, } from '../../../../../public/lists_plugin_deps'; import * as i18n from './translations'; +import * as sharedI18n from '../translations'; import { TimelineNonEcsData, Ecs } from '../../../../graphql/types'; import { useAppToasts } from '../../../hooks/use_app_toasts'; import { useKibana } from '../../../lib/kibana'; @@ -35,6 +35,7 @@ import { ExceptionBuilderComponent } from '../builder'; import { Loader } from '../../loader'; import { useAddOrUpdateException } from '../use_add_exception'; import { useSignalIndex } from '../../../../detections/containers/detection_engine/alerts/use_signal_index'; +import { useRuleAsync } from '../../../../detections/containers/detection_engine/rules/use_rule_async'; import { useFetchOrCreateRuleExceptionList } from '../use_fetch_or_create_rule_exception_list'; import { AddExceptionComments } from '../add_exception_comments'; import { @@ -46,6 +47,7 @@ import { entryHasNonEcsType, getMappedNonEcsValue, } from '../helpers'; +import { ErrorInfo, ErrorCallout } from '../error_callout'; import { useFetchIndexPatterns } from '../../../../detections/containers/detection_engine/rules'; export interface AddExceptionModalBaseProps { @@ -107,13 +109,14 @@ export const AddExceptionModal = memo(function AddExceptionModal({ }: AddExceptionModalProps) { const { http } = useKibana().services; const [comment, setComment] = useState(''); + const { rule: maybeRule } = useRuleAsync(ruleId); const [shouldCloseAlert, setShouldCloseAlert] = useState(false); const [shouldBulkCloseAlert, setShouldBulkCloseAlert] = useState(false); const [shouldDisableBulkClose, setShouldDisableBulkClose] = useState(false); const [exceptionItemsToAdd, setExceptionItemsToAdd] = useState< Array >([]); - const [fetchOrCreateListError, setFetchOrCreateListError] = useState(false); + const [fetchOrCreateListError, setFetchOrCreateListError] = useState(null); const { addError, addSuccess } = useAppToasts(); const { loading: isSignalIndexLoading, signalIndexName } = useSignalIndex(); const [ @@ -164,17 +167,41 @@ export const AddExceptionModal = memo(function AddExceptionModal({ }, [onRuleChange] ); - const onFetchOrCreateExceptionListError = useCallback( - (error: Error) => { - setFetchOrCreateListError(true); + + const handleDissasociationSuccess = useCallback( + (id: string): void => { + handleRuleChange(true); + addSuccess(sharedI18n.DISSASOCIATE_LIST_SUCCESS(id)); + onCancel(); + }, + [handleRuleChange, addSuccess, onCancel] + ); + + const handleDissasociationError = useCallback( + (error: Error): void => { + addError(error, { title: sharedI18n.DISSASOCIATE_EXCEPTION_LIST_ERROR }); + onCancel(); + }, + [addError, onCancel] + ); + + const handleFetchOrCreateExceptionListError = useCallback( + (error: Error, statusCode: number | null, message: string | null) => { + setFetchOrCreateListError({ + reason: error.message, + code: statusCode, + details: message, + listListId: null, + }); }, [setFetchOrCreateListError] ); + const [isLoadingExceptionList, ruleExceptionList] = useFetchOrCreateRuleExceptionList({ http, ruleId, exceptionListType, - onError: onFetchOrCreateExceptionListError, + onError: handleFetchOrCreateExceptionListError, onSuccess: handleRuleChange, }); @@ -279,7 +306,9 @@ export const AddExceptionModal = memo(function AddExceptionModal({ ]); const isSubmitButtonDisabled = useMemo( - () => fetchOrCreateListError || exceptionItemsToAdd.every((item) => item.entries.length === 0), + () => + fetchOrCreateListError != null || + exceptionItemsToAdd.every((item) => item.entries.length === 0), [fetchOrCreateListError, exceptionItemsToAdd] ); @@ -295,19 +324,27 @@ export const AddExceptionModal = memo(function AddExceptionModal({ - {fetchOrCreateListError === true && ( - -

{i18n.ADD_EXCEPTION_FETCH_ERROR}

-
+ {fetchOrCreateListError != null && ( + + + )} - {fetchOrCreateListError === false && + {fetchOrCreateListError == null && (isLoadingExceptionList || isIndexPatternLoading || isSignalIndexLoading || isSignalIndexPatternLoading) && ( )} - {fetchOrCreateListError === false && + {fetchOrCreateListError == null && !isSignalIndexLoading && !isSignalIndexPatternLoading && !isLoadingExceptionList && @@ -377,20 +414,21 @@ export const AddExceptionModal = memo(function AddExceptionModal({ )} + {fetchOrCreateListError == null && ( + + {i18n.CANCEL} - - {i18n.CANCEL} - - - {i18n.ADD_EXCEPTION} - - + + {i18n.ADD_EXCEPTION} + + + )} ); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.test.tsx index 6ff218ca06059..c724e6a2c711f 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.test.tsx @@ -77,6 +77,7 @@ describe('When the edit exception modal is opened', () => { ({ eui: euiLightVars, darkMode: false })}> { ({ eui: euiLightVars, darkMode: false })}> { ({ eui: euiLightVars, darkMode: false })}> { ({ eui: euiLightVars, darkMode: false })}> { ({ eui: euiLightVars, darkMode: false })}> void; onConfirm: () => void; + onRuleChange?: () => void; } const Modal = styled(EuiModal)` @@ -83,14 +88,18 @@ const ModalBodySection = styled.section` export const EditExceptionModal = memo(function EditExceptionModal({ ruleName, + ruleId, ruleIndices, exceptionItem, exceptionListType, onCancel, onConfirm, + onRuleChange, }: EditExceptionModalProps) { const { http } = useKibana().services; const [comment, setComment] = useState(''); + const { rule: maybeRule } = useRuleAsync(ruleId); + const [updateError, setUpdateError] = useState(null); const [hasVersionConflict, setHasVersionConflict] = useState(false); const [shouldBulkCloseAlert, setShouldBulkCloseAlert] = useState(false); const [shouldDisableBulkClose, setShouldDisableBulkClose] = useState(false); @@ -108,18 +117,44 @@ export const EditExceptionModal = memo(function EditExceptionModal({ 'rules' ); - const onError = useCallback( - (error) => { + const handleExceptionUpdateError = useCallback( + (error: Error, statusCode: number | null, message: string | null) => { if (error.message.includes('Conflict')) { setHasVersionConflict(true); } else { - addError(error, { title: i18n.EDIT_EXCEPTION_ERROR }); - onCancel(); + setUpdateError({ + reason: error.message, + code: statusCode, + details: message, + listListId: exceptionItem.list_id, + }); } }, + [setUpdateError, setHasVersionConflict, exceptionItem.list_id] + ); + + const handleDissasociationSuccess = useCallback( + (id: string): void => { + addSuccess(sharedI18n.DISSASOCIATE_LIST_SUCCESS(id)); + + if (onRuleChange) { + onRuleChange(); + } + + onCancel(); + }, + [addSuccess, onCancel, onRuleChange] + ); + + const handleDissasociationError = useCallback( + (error: Error): void => { + addError(error, { title: sharedI18n.DISSASOCIATE_EXCEPTION_LIST_ERROR }); + onCancel(); + }, [addError, onCancel] ); - const onSuccess = useCallback(() => { + + const handleExceptionUpdateSuccess = useCallback((): void => { addSuccess(i18n.EDIT_EXCEPTION_SUCCESS); onConfirm(); }, [addSuccess, onConfirm]); @@ -127,8 +162,8 @@ export const EditExceptionModal = memo(function EditExceptionModal({ const [{ isLoading: addExceptionIsLoading }, addOrUpdateExceptionItems] = useAddOrUpdateException( { http, - onSuccess, - onError, + onSuccess: handleExceptionUpdateSuccess, + onError: handleExceptionUpdateError, } ); @@ -222,11 +257,9 @@ export const EditExceptionModal = memo(function EditExceptionModal({ {ruleName} - {(addExceptionIsLoading || isIndexPatternLoading || isSignalIndexLoading) && ( )} - {!isSignalIndexLoading && !addExceptionIsLoading && !isIndexPatternLoading && ( <> @@ -280,7 +313,18 @@ export const EditExceptionModal = memo(function EditExceptionModal({ )} - + {updateError != null && ( + + + + )} {hasVersionConflict && ( @@ -288,20 +332,21 @@ export const EditExceptionModal = memo(function EditExceptionModal({ )} + {updateError == null && ( + + {i18n.CANCEL} - - {i18n.CANCEL} - - - {i18n.EDIT_EXCEPTION_SAVE_BUTTON} - - + + {i18n.EDIT_EXCEPTION_SAVE_BUTTON} + + + )} ); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/error_callout.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/error_callout.test.tsx new file mode 100644 index 0000000000000..c9efa5e54dccf --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/error_callout.test.tsx @@ -0,0 +1,160 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { ThemeProvider } from 'styled-components'; +import { mountWithIntl } from 'test_utils/enzyme_helpers'; +import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; + +import { getListMock } from '../../../../common/detection_engine/schemas/types/lists.mock'; +import { useDissasociateExceptionList } from '../../../detections/containers/detection_engine/rules/use_dissasociate_exception_list'; +import { createKibanaCoreStartMock } from '../../mock/kibana_core'; +import { ErrorCallout } from './error_callout'; +import { savedRuleMock } from '../../../detections/containers/detection_engine/rules/mock'; + +jest.mock('../../../detections/containers/detection_engine/rules/use_dissasociate_exception_list'); + +const mockKibanaHttpService = createKibanaCoreStartMock().http; + +describe('ErrorCallout', () => { + const mockDissasociate = jest.fn(); + + beforeEach(() => { + (useDissasociateExceptionList as jest.Mock).mockReturnValue([false, mockDissasociate]); + }); + + it('it renders error details', () => { + const wrapper = mountWithIntl( + ({ eui: euiLightVars, darkMode: false })}> + + + ); + + expect( + wrapper.find('[data-test-subj="errorCalloutContainer"] .euiCallOutHeader__title').text() + ).toEqual('Error: error reason (500)'); + expect(wrapper.find('[data-test-subj="errorCalloutMessage"]').at(0).text()).toEqual( + 'Error fetching exception list' + ); + }); + + it('it invokes "onCancel" when cancel button clicked', () => { + const mockOnCancel = jest.fn(); + const wrapper = mountWithIntl( + ({ eui: euiLightVars, darkMode: false })}> + + + ); + + wrapper.find('[data-test-subj="errorCalloutCancelButton"]').at(0).simulate('click'); + + expect(mockOnCancel).toHaveBeenCalled(); + }); + + it('it does not render status code if not available', () => { + const wrapper = mountWithIntl( + ({ eui: euiLightVars, darkMode: false })}> + + + ); + + expect( + wrapper.find('[data-test-subj="errorCalloutContainer"] .euiCallOutHeader__title').text() + ).toEqual('Error: not found'); + expect(wrapper.find('[data-test-subj="errorCalloutMessage"]').at(0).text()).toEqual( + 'Error fetching exception list' + ); + expect(wrapper.find('[data-test-subj="errorCalloutDissasociateButton"]').exists()).toBeFalsy(); + }); + + it('it renders specific missing exceptions list error', () => { + const wrapper = mountWithIntl( + ({ eui: euiLightVars, darkMode: false })}> + + + ); + + expect( + wrapper.find('[data-test-subj="errorCalloutContainer"] .euiCallOutHeader__title').text() + ).toEqual('Error: not found (404)'); + expect(wrapper.find('[data-test-subj="errorCalloutMessage"]').at(0).text()).toEqual( + 'The associated exception list (some_uuid) no longer exists. Please remove the missing exception list to add additional exceptions to the detection rule.' + ); + expect(wrapper.find('[data-test-subj="errorCalloutDissasociateButton"]').exists()).toBeTruthy(); + }); + + it('it dissasociates list from rule when remove exception list clicked ', () => { + const wrapper = mountWithIntl( + ({ eui: euiLightVars, darkMode: false })}> + + + ); + + wrapper.find('[data-test-subj="errorCalloutDissasociateButton"]').at(0).simulate('click'); + + expect(mockDissasociate).toHaveBeenCalledWith([]); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/error_callout.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/error_callout.tsx new file mode 100644 index 0000000000000..a2419ef16df3a --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/error_callout.tsx @@ -0,0 +1,169 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useMemo, useEffect, useState, useCallback } from 'react'; +import { + EuiButtonEmpty, + EuiAccordion, + EuiCodeBlock, + EuiButton, + EuiCallOut, + EuiText, + EuiSpacer, +} from '@elastic/eui'; + +import { HttpSetup } from '../../../../../../../src/core/public'; +import { List } from '../../../../common/detection_engine/schemas/types/lists'; +import { Rule } from '../../../detections/containers/detection_engine/rules/types'; +import * as i18n from './translations'; +import { useDissasociateExceptionList } from '../../../detections/containers/detection_engine/rules/use_dissasociate_exception_list'; + +export interface ErrorInfo { + reason: string | null; + code: number | null; + details: string | null; + listListId: string | null; +} + +export interface ErrorCalloutProps { + http: HttpSetup; + rule: Rule | null; + errorInfo: ErrorInfo; + onCancel: () => void; + onSuccess: (listId: string) => void; + onError: (arg: Error) => void; +} + +const ErrorCalloutComponent = ({ + http, + rule, + errorInfo, + onCancel, + onError, + onSuccess, +}: ErrorCalloutProps): JSX.Element => { + const [listToDelete, setListToDelete] = useState(null); + const [errorTitle, setErrorTitle] = useState(''); + const [errorMessage, setErrorMessage] = useState(i18n.ADD_EXCEPTION_FETCH_ERROR); + + const handleOnSuccess = useCallback((): void => { + onSuccess(listToDelete != null ? listToDelete.id : ''); + }, [onSuccess, listToDelete]); + + const [isDissasociatingList, handleDissasociateExceptionList] = useDissasociateExceptionList({ + http, + ruleRuleId: rule != null ? rule.rule_id : '', + onSuccess: handleOnSuccess, + onError, + }); + + const canDisplay404Actions = useMemo( + (): boolean => + errorInfo.code === 404 && + rule != null && + listToDelete != null && + handleDissasociateExceptionList != null, + [errorInfo.code, listToDelete, handleDissasociateExceptionList, rule] + ); + + useEffect((): void => { + // Yes, it's redundant, unfortunately typescript wasn't picking up + // that `listToDelete` is checked in canDisplay404Actions + if (canDisplay404Actions && listToDelete != null) { + setErrorMessage(i18n.ADD_EXCEPTION_FETCH_404_ERROR(listToDelete.id)); + } + + setErrorTitle(`${errorInfo.reason}${errorInfo.code != null ? ` (${errorInfo.code})` : ''}`); + }, [errorInfo.reason, errorInfo.code, listToDelete, canDisplay404Actions]); + + const handleDissasociateList = useCallback((): void => { + // Yes, it's redundant, unfortunately typescript wasn't picking up + // that `handleDissasociateExceptionList` and `list` are checked in + // canDisplay404Actions + if ( + canDisplay404Actions && + rule != null && + listToDelete != null && + handleDissasociateExceptionList != null + ) { + const exceptionLists = (rule.exceptions_list ?? []).filter( + ({ id }) => id !== listToDelete.id + ); + + handleDissasociateExceptionList(exceptionLists); + } + }, [handleDissasociateExceptionList, listToDelete, canDisplay404Actions, rule]); + + useEffect((): void => { + if (errorInfo.code === 404 && rule != null && rule.exceptions_list != null) { + const [listFound] = rule.exceptions_list.filter( + ({ id, list_id: listId }) => + (errorInfo.details != null && errorInfo.details.includes(id)) || + errorInfo.listListId === listId + ); + setListToDelete(listFound); + } + }, [rule, errorInfo.details, errorInfo.code, errorInfo.listListId]); + + return ( + + +

{errorMessage}

+
+ + {listToDelete != null && ( + +

{i18n.MODAL_ERROR_ACCORDION_TEXT}

+ + } + > + + {JSON.stringify(listToDelete)} + +
+ )} + + + {i18n.CANCEL} + + {canDisplay404Actions && ( + + {i18n.CLEAR_EXCEPTIONS_LABEL} + + )} +
+ ); +}; + +ErrorCalloutComponent.displayName = 'ErrorCalloutComponent'; + +export const ErrorCallout = React.memo(ErrorCalloutComponent); + +ErrorCallout.displayName = 'ErrorCallout'; diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/translations.ts b/x-pack/plugins/security_solution/public/common/components/exceptions/translations.ts index 13e9d0df549f8..484a3d593026e 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/translations.ts +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/translations.ts @@ -190,3 +190,52 @@ export const TOTAL_ITEMS_FETCH_ERROR = i18n.translate( defaultMessage: 'Error getting exception item totals', } ); + +export const CLEAR_EXCEPTIONS_LABEL = i18n.translate( + 'xpack.securitySolution.exceptions.clearExceptionsLabel', + { + defaultMessage: 'Remove Exception List', + } +); + +export const ADD_EXCEPTION_FETCH_404_ERROR = (listId: string) => + i18n.translate('xpack.securitySolution.exceptions.fetch404Error', { + values: { listId }, + defaultMessage: + 'The associated exception list ({listId}) no longer exists. Please remove the missing exception list to add additional exceptions to the detection rule.', + }); + +export const ADD_EXCEPTION_FETCH_ERROR = i18n.translate( + 'xpack.securitySolution.exceptions.fetchError', + { + defaultMessage: 'Error fetching exception list', + } +); + +export const ERROR = i18n.translate('xpack.securitySolution.exceptions.errorLabel', { + defaultMessage: 'Error', +}); + +export const CANCEL = i18n.translate('xpack.securitySolution.exceptions.cancelLabel', { + defaultMessage: 'Cancel', +}); + +export const MODAL_ERROR_ACCORDION_TEXT = i18n.translate( + 'xpack.securitySolution.exceptions.modalErrorAccordionText', + { + defaultMessage: 'Show rule reference information:', + } +); + +export const DISSASOCIATE_LIST_SUCCESS = (id: string) => + i18n.translate('xpack.securitySolution.exceptions.dissasociateListSuccessText', { + values: { id }, + defaultMessage: 'Exception list ({id}) has successfully been removed', + }); + +export const DISSASOCIATE_EXCEPTION_LIST_ERROR = i18n.translate( + 'xpack.securitySolution.exceptions.dissasociateExceptionListError', + { + defaultMessage: 'Failed to remove exception list', + } +); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/use_add_exception.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/use_add_exception.test.tsx index 6611ee2385d10..46923e07d225a 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/use_add_exception.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/use_add_exception.test.tsx @@ -148,6 +148,50 @@ describe('useAddOrUpdateException', () => { }); }); + it('invokes "onError" if call to add exception item fails', async () => { + const mockError = new Error('error adding item'); + + addExceptionListItem = jest + .spyOn(listsApi, 'addExceptionListItem') + .mockRejectedValue(mockError); + + await act(async () => { + const { rerender, result, waitForNextUpdate } = render(); + const addOrUpdateItems = await waitForAddOrUpdateFunc({ + rerender, + result, + waitForNextUpdate, + }); + if (addOrUpdateItems) { + addOrUpdateItems(...addOrUpdateItemsArgs); + } + await waitForNextUpdate(); + expect(onError).toHaveBeenCalledWith(mockError, null, null); + }); + }); + + it('invokes "onError" if call to update exception item fails', async () => { + const mockError = new Error('error updating item'); + + updateExceptionListItem = jest + .spyOn(listsApi, 'updateExceptionListItem') + .mockRejectedValue(mockError); + + await act(async () => { + const { rerender, result, waitForNextUpdate } = render(); + const addOrUpdateItems = await waitForAddOrUpdateFunc({ + rerender, + result, + waitForNextUpdate, + }); + if (addOrUpdateItems) { + addOrUpdateItems(...addOrUpdateItemsArgs); + } + await waitForNextUpdate(); + expect(onError).toHaveBeenCalledWith(mockError, null, null); + }); + }); + describe('when alertIdToClose is not passed in', () => { it('should not update the alert status', async () => { await act(async () => { diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/use_add_exception.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/use_add_exception.tsx index 9d45a411b5130..be289b0e85e66 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/use_add_exception.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/use_add_exception.tsx @@ -42,7 +42,7 @@ export type ReturnUseAddOrUpdateException = [ export interface UseAddOrUpdateExceptionProps { http: HttpStart; - onError: (arg: Error) => void; + onError: (arg: Error, code: number | null, message: string | null) => void; onSuccess: () => void; } @@ -157,7 +157,11 @@ export const useAddOrUpdateException = ({ } catch (error) { if (isSubscribed) { setIsLoading(false); - onError(error); + if (error.body != null) { + onError(error, error.body.status_code, error.body.message); + } else { + onError(error, null, null); + } } } }; diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/use_fetch_or_create_rule_exception_list.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/use_fetch_or_create_rule_exception_list.test.tsx index 39d88bd8e4724..f20a58b9ffa36 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/use_fetch_or_create_rule_exception_list.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/use_fetch_or_create_rule_exception_list.test.tsx @@ -379,7 +379,7 @@ describe('useFetchOrCreateRuleExceptionList', () => { await waitForNextUpdate(); await waitForNextUpdate(); expect(onError).toHaveBeenCalledTimes(1); - expect(onError).toHaveBeenCalledWith(error); + expect(onError).toHaveBeenCalledWith(error, null, null); }); }); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/use_fetch_or_create_rule_exception_list.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/use_fetch_or_create_rule_exception_list.tsx index 0d367e03a799f..944631d4e9fb5 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/use_fetch_or_create_rule_exception_list.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/use_fetch_or_create_rule_exception_list.tsx @@ -30,7 +30,7 @@ export interface UseFetchOrCreateRuleExceptionListProps { http: HttpStart; ruleId: Rule['id']; exceptionListType: ExceptionListSchema['type']; - onError: (arg: Error) => void; + onError: (arg: Error, code: number | null, message: string | null) => void; onSuccess?: (ruleWasChanged: boolean) => void; } @@ -179,7 +179,11 @@ export const useFetchOrCreateRuleExceptionList = ({ if (isSubscribed) { setIsLoading(false); setExceptionList(null); - onError(error); + if (error.body != null) { + onError(error, error.body.status_code, error.body.message); + } else { + onError(error, null, null); + } } } } diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/index.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/index.tsx index 7482068454a97..c97895cdfe236 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/index.tsx @@ -322,11 +322,13 @@ const ExceptionsViewerComponent = ({ exceptionListTypeToEdit != null && ( )} diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_dissasociate_exception_list.test.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_dissasociate_exception_list.test.tsx new file mode 100644 index 0000000000000..6b1938655dc33 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_dissasociate_exception_list.test.tsx @@ -0,0 +1,52 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { act, renderHook } from '@testing-library/react-hooks'; + +import { createKibanaCoreStartMock } from '../../../../common/mock/kibana_core'; + +import * as api from './api'; +import { ruleMock } from './mock'; +import { + ReturnUseDissasociateExceptionList, + UseDissasociateExceptionListProps, + useDissasociateExceptionList, +} from './use_dissasociate_exception_list'; + +const mockKibanaHttpService = createKibanaCoreStartMock().http; + +describe('useDissasociateExceptionList', () => { + const onError = jest.fn(); + const onSuccess = jest.fn(); + + beforeEach(() => { + jest.spyOn(api, 'patchRule').mockResolvedValue(ruleMock); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + test('initializes hook', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook< + UseDissasociateExceptionListProps, + ReturnUseDissasociateExceptionList + >(() => + useDissasociateExceptionList({ + http: mockKibanaHttpService, + ruleRuleId: 'rule_id', + onError, + onSuccess, + }) + ); + + await waitForNextUpdate(); + + expect(result.current).toEqual([false, null]); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_dissasociate_exception_list.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_dissasociate_exception_list.tsx new file mode 100644 index 0000000000000..dffba3e6e0436 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_dissasociate_exception_list.tsx @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { useEffect, useState, useRef } from 'react'; + +import { HttpStart } from '../../../../../../../../src/core/public'; +import { List } from '../../../../../common/detection_engine/schemas/types/lists'; +import { patchRule } from './api'; + +type Func = (lists: List[]) => void; +export type ReturnUseDissasociateExceptionList = [boolean, Func | null]; + +export interface UseDissasociateExceptionListProps { + http: HttpStart; + ruleRuleId: string; + onError: (arg: Error) => void; + onSuccess: () => void; +} + +/** + * Hook for removing an exception list reference from a rule + * + * @param http Kibana http service + * @param ruleRuleId a rule_id (NOT id) + * @param onError error callback + * @param onSuccess success callback + * + */ +export const useDissasociateExceptionList = ({ + http, + ruleRuleId, + onError, + onSuccess, +}: UseDissasociateExceptionListProps): ReturnUseDissasociateExceptionList => { + const [isLoading, setLoading] = useState(false); + const dissasociateList = useRef(null); + + useEffect(() => { + let isSubscribed = true; + const abortCtrl = new AbortController(); + + const dissasociateListFromRule = (id: string) => async ( + exceptionLists: List[] + ): Promise => { + try { + if (isSubscribed) { + setLoading(true); + + await patchRule({ + ruleProperties: { + rule_id: id, + exceptions_list: exceptionLists, + }, + signal: abortCtrl.signal, + }); + + onSuccess(); + setLoading(false); + } + } catch (err) { + if (isSubscribed) { + setLoading(false); + onError(err); + } + } + }; + + dissasociateList.current = dissasociateListFromRule(ruleRuleId); + + return (): void => { + isSubscribed = false; + abortCtrl.abort(); + }; + }, [http, ruleRuleId, onError, onSuccess]); + + return [isLoading, dissasociateList.current]; +}; From d6c45a2e70a20d552c91f3df89da1cd081077209 Mon Sep 17 00:00:00 2001 From: Frank Hassanabad Date: Wed, 26 Aug 2020 09:01:32 -0600 Subject: [PATCH 68/71] Fixes runtime error with meta when it is missing (#75844) ## Summary Found in 7.9.0, if you post a rule with an action that has a missing "meta" then you are going to get errors in your UI that look something like: ```ts An error occurred during rule execution: message: "Cannot read property 'kibana_siem_app_url' of null" name: "Unusual Windows Remote User" id: "1cc27e7e-d7c7-4f6a-b918-8c272fc6b1a3" rule id: "1781d055-5c66-4adf-9e93-fc0fa69550c9" signals index: ".siem-signals-default" ``` This fixes the accidental referencing of the null/undefined property and adds both integration and unit tests in that area of code. If you have an action id handy you can manually test this by editing the json file of: ```ts test_cases/queries/action_without_meta.json ``` to have your action id and then posting it like so: ```ts ./post_rule.sh ./rules/test_cases/queries/action_without_meta.json ``` ### Checklist Delete any items that are not applicable to this PR. - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios --- .../rules_notification_alert_type.test.ts | 78 ++++++++++ .../rules_notification_alert_type.ts | 4 +- .../queries/action_without_meta.json | 42 ++++++ .../signals/signal_rule_alert_type.test.ts | 100 +++++++++++++ .../signals/signal_rule_alert_type.ts | 3 +- .../security_and_spaces/tests/add_actions.ts | 134 ++++++++++++++++++ .../security_and_spaces/tests/index.ts | 1 + .../detection_engine_api_integration/utils.ts | 43 ++++++ 8 files changed, 402 insertions(+), 3 deletions(-) create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/scripts/rules/test_cases/queries/action_without_meta.json create mode 100644 x-pack/test/detection_engine_api_integration/security_and_spaces/tests/add_actions.ts diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/rules_notification_alert_type.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/rules_notification_alert_type.test.ts index 3eefd3e665cd6..593ada470b118 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/rules_notification_alert_type.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/rules_notification_alert_type.test.ts @@ -79,6 +79,84 @@ describe('rules_notification_alert_type', () => { ); }); + it('should resolve results_link when meta is undefined to use "/app/security"', async () => { + const ruleAlert = getResult(); + delete ruleAlert.params.meta; + alertServices.savedObjectsClient.get.mockResolvedValue({ + id: 'rule-id', + type: 'type', + references: [], + attributes: ruleAlert, + }); + alertServices.callCluster.mockResolvedValue({ + count: 10, + }); + + await alert.executor(payload); + expect(alertServices.alertInstanceFactory).toHaveBeenCalled(); + + const [{ value: alertInstanceMock }] = alertServices.alertInstanceFactory.mock.results; + expect(alertInstanceMock.scheduleActions).toHaveBeenCalledWith( + 'default', + expect.objectContaining({ + results_link: + '/app/security/detections/rules/id/rule-id?timerange=(global:(linkTo:!(timeline),timerange:(from:1576255233400,kind:absolute,to:1576341633400)),timeline:(linkTo:!(global),timerange:(from:1576255233400,kind:absolute,to:1576341633400)))', + }) + ); + }); + + it('should resolve results_link when meta is an empty object to use "/app/security"', async () => { + const ruleAlert = getResult(); + ruleAlert.params.meta = {}; + alertServices.savedObjectsClient.get.mockResolvedValue({ + id: 'rule-id', + type: 'type', + references: [], + attributes: ruleAlert, + }); + alertServices.callCluster.mockResolvedValue({ + count: 10, + }); + await alert.executor(payload); + expect(alertServices.alertInstanceFactory).toHaveBeenCalled(); + + const [{ value: alertInstanceMock }] = alertServices.alertInstanceFactory.mock.results; + expect(alertInstanceMock.scheduleActions).toHaveBeenCalledWith( + 'default', + expect.objectContaining({ + results_link: + '/app/security/detections/rules/id/rule-id?timerange=(global:(linkTo:!(timeline),timerange:(from:1576255233400,kind:absolute,to:1576341633400)),timeline:(linkTo:!(global),timerange:(from:1576255233400,kind:absolute,to:1576341633400)))', + }) + ); + }); + + it('should resolve results_link to custom kibana link when given one', async () => { + const ruleAlert = getResult(); + ruleAlert.params.meta = { + kibana_siem_app_url: 'http://localhost', + }; + alertServices.savedObjectsClient.get.mockResolvedValue({ + id: 'rule-id', + type: 'type', + references: [], + attributes: ruleAlert, + }); + alertServices.callCluster.mockResolvedValue({ + count: 10, + }); + await alert.executor(payload); + expect(alertServices.alertInstanceFactory).toHaveBeenCalled(); + + const [{ value: alertInstanceMock }] = alertServices.alertInstanceFactory.mock.results; + expect(alertInstanceMock.scheduleActions).toHaveBeenCalledWith( + 'default', + expect.objectContaining({ + results_link: + 'http://localhost/detections/rules/id/rule-id?timerange=(global:(linkTo:!(timeline),timerange:(from:1576255233400,kind:absolute,to:1576341633400)),timeline:(linkTo:!(global),timerange:(from:1576255233400,kind:absolute,to:1576341633400)))', + }) + ); + }); + it('should not call alertInstanceFactory if signalsCount was 0', async () => { const ruleAlert = getResult(); alertServices.savedObjectsClient.get.mockResolvedValue({ diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/rules_notification_alert_type.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/rules_notification_alert_type.ts index ab824957087fc..2eb34529d044c 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/rules_notification_alert_type.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/rules_notification_alert_type.ts @@ -64,8 +64,8 @@ export const rulesNotificationAlertType = ({ from: fromInMs, to: toInMs, id: ruleAlertSavedObject.id, - kibanaSiemAppUrl: (ruleAlertParams.meta as { kibana_siem_app_url?: string }) - .kibana_siem_app_url, + kibanaSiemAppUrl: (ruleAlertParams.meta as { kibana_siem_app_url?: string } | undefined) + ?.kibana_siem_app_url, }); logger.info( diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/rules/test_cases/queries/action_without_meta.json b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/rules/test_cases/queries/action_without_meta.json new file mode 100644 index 0000000000000..6569a641de3a2 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/rules/test_cases/queries/action_without_meta.json @@ -0,0 +1,42 @@ +{ + "type": "query", + "index": [ + "apm-*-transaction*", + "auditbeat-*", + "endgame-*", + "filebeat-*", + "logs-*", + "packetbeat-*", + "winlogbeat-*" + ], + "filters": [], + "language": "kuery", + "query": "host.name: *", + "author": [], + "false_positives": [], + "references": [], + "risk_score": 50, + "risk_score_mapping": [], + "severity": "low", + "severity_mapping": [], + "threat": [], + "name": "Host Name Test", + "description": "Host Name Test", + "tags": [], + "license": "", + "interval": "5m", + "from": "now-30s", + "to": "now", + "actions": [ + { + "group": "default", + "id": "4c42ecf2-5e9b-4ce6-8a7a-ab620fd8b169", + "params": { + "body": "{}" + }, + "action_type_id": ".webhook" + } + ], + "enabled": true, + "throttle": "rule" +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts index b0c855afa8be9..b29d15f5e5c72 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts @@ -16,6 +16,7 @@ import { getListsClient, getExceptions, sortExceptionItems, + parseScheduleDates, } from './utils'; import { RuleExecutorOptions } from './types'; import { searchAfterAndBulkCreate } from './search_after_bulk_create'; @@ -227,6 +228,105 @@ describe('rules_notification_alert_type', () => { ); }); + it('should resolve results_link when meta is an empty object to use "/app/security"', async () => { + const ruleAlert = getResult(); + ruleAlert.params.meta = {}; + ruleAlert.actions = [ + { + actionTypeId: '.slack', + params: { + message: + 'Rule generated {{state.signals_count}} signals\n\n{{context.rule.name}}\n{{{context.results_link}}}', + }, + group: 'default', + id: '99403909-ca9b-49ba-9d7a-7e5320e68d05', + }, + ]; + + alertServices.savedObjectsClient.get.mockResolvedValue({ + id: 'rule-id', + type: 'type', + references: [], + attributes: ruleAlert, + }); + (parseScheduleDates as jest.Mock).mockReturnValue(moment(100)); + payload.params.meta = {}; + await alert.executor(payload); + + expect(scheduleNotificationActions).toHaveBeenCalledWith( + expect.objectContaining({ + resultsLink: + '/app/security/detections/rules/id/rule-id?timerange=(global:(linkTo:!(timeline),timerange:(from:100,kind:absolute,to:100)),timeline:(linkTo:!(global),timerange:(from:100,kind:absolute,to:100)))', + }) + ); + }); + + it('should resolve results_link when meta is undefined use "/app/security"', async () => { + const ruleAlert = getResult(); + delete ruleAlert.params.meta; + ruleAlert.actions = [ + { + actionTypeId: '.slack', + params: { + message: + 'Rule generated {{state.signals_count}} signals\n\n{{context.rule.name}}\n{{{context.results_link}}}', + }, + group: 'default', + id: '99403909-ca9b-49ba-9d7a-7e5320e68d05', + }, + ]; + + alertServices.savedObjectsClient.get.mockResolvedValue({ + id: 'rule-id', + type: 'type', + references: [], + attributes: ruleAlert, + }); + (parseScheduleDates as jest.Mock).mockReturnValue(moment(100)); + delete payload.params.meta; + await alert.executor(payload); + + expect(scheduleNotificationActions).toHaveBeenCalledWith( + expect.objectContaining({ + resultsLink: + '/app/security/detections/rules/id/rule-id?timerange=(global:(linkTo:!(timeline),timerange:(from:100,kind:absolute,to:100)),timeline:(linkTo:!(global),timerange:(from:100,kind:absolute,to:100)))', + }) + ); + }); + + it('should resolve results_link with a custom link', async () => { + const ruleAlert = getResult(); + ruleAlert.params.meta = { kibana_siem_app_url: 'http://localhost' }; + ruleAlert.actions = [ + { + actionTypeId: '.slack', + params: { + message: + 'Rule generated {{state.signals_count}} signals\n\n{{context.rule.name}}\n{{{context.results_link}}}', + }, + group: 'default', + id: '99403909-ca9b-49ba-9d7a-7e5320e68d05', + }, + ]; + + alertServices.savedObjectsClient.get.mockResolvedValue({ + id: 'rule-id', + type: 'type', + references: [], + attributes: ruleAlert, + }); + (parseScheduleDates as jest.Mock).mockReturnValue(moment(100)); + payload.params.meta = { kibana_siem_app_url: 'http://localhost' }; + await alert.executor(payload); + + expect(scheduleNotificationActions).toHaveBeenCalledWith( + expect.objectContaining({ + resultsLink: + 'http://localhost/detections/rules/id/rule-id?timerange=(global:(linkTo:!(timeline),timerange:(from:100,kind:absolute,to:100)),timeline:(linkTo:!(global),timerange:(from:100,kind:absolute,to:100)))', + }) + ); + }); + describe('ML rule', () => { it('should throw an error if ML plugin was not available', async () => { const ruleAlert = getMlResult(); 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 0e859ecef31c6..b5cbf80b084f7 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 @@ -356,7 +356,8 @@ export const signalRulesAlertType = ({ from: fromInMs, to: toInMs, id: savedObject.id, - kibanaSiemAppUrl: (meta as { kibana_siem_app_url?: string }).kibana_siem_app_url, + kibanaSiemAppUrl: (meta as { kibana_siem_app_url?: string } | undefined) + ?.kibana_siem_app_url, }); logger.info( diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/add_actions.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/add_actions.ts new file mode 100644 index 0000000000000..2468851237047 --- /dev/null +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/add_actions.ts @@ -0,0 +1,134 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; + +import { DETECTION_ENGINE_RULES_URL } from '../../../../plugins/security_solution/common/constants'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; +import { + createSignalsIndex, + deleteAllAlerts, + deleteSignalsIndex, + removeServerGeneratedProperties, + waitFor, + getWebHookAction, + getRuleWithWebHookAction, + getSimpleRuleOutputWithWebHookAction, +} from '../../utils'; +import { CreateRulesSchema } from '../../../../plugins/security_solution/common/detection_engine/schemas/request/create_rules_schema'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext) => { + const supertest = getService('supertest'); + const es = getService('es'); + + describe('add_actions', () => { + describe('adding actions', () => { + beforeEach(async () => { + await createSignalsIndex(supertest); + }); + + afterEach(async () => { + await deleteSignalsIndex(supertest); + await deleteAllAlerts(es); + }); + + it('should be able to create a new webhook action and attach it to a rule', async () => { + // create a new action + const { body: hookAction } = await supertest + .post('/api/actions/action') + .set('kbn-xsrf', 'true') + .send(getWebHookAction()) + .expect(200); + + // create a rule with the action attached + const { body } = await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(getRuleWithWebHookAction(hookAction.id)) + .expect(200); + + const bodyToCompare = removeServerGeneratedProperties(body); + expect(bodyToCompare).to.eql( + getSimpleRuleOutputWithWebHookAction(`${bodyToCompare?.actions?.[0].id}`) + ); + }); + + it('should be able to create a new webhook action and attach it to a rule without a meta field and run it correctly', async () => { + // create a new action + const { body: hookAction } = await supertest + .post('/api/actions/action') + .set('kbn-xsrf', 'true') + .send(getWebHookAction()) + .expect(200); + + // create a rule with the action attached + const { body: rule } = await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(getRuleWithWebHookAction(hookAction.id)) + .expect(200); + + // wait for Task Manager to execute the rule and its update status + await waitFor(async () => { + const { body } = await supertest + .post(`${DETECTION_ENGINE_RULES_URL}/_find_statuses`) + .set('kbn-xsrf', 'true') + .send({ ids: [rule.id] }) + .expect(200); + return body[rule.id].current_status?.status === 'succeeded'; + }); + + // expected result for status should be 'succeeded' + const { body } = await supertest + .post(`${DETECTION_ENGINE_RULES_URL}/_find_statuses`) + .set('kbn-xsrf', 'true') + .send({ ids: [rule.id] }) + .expect(200); + expect(body[rule.id].current_status.status).to.eql('succeeded'); + }); + + it('should be able to create a new webhook action and attach it to a rule with a meta field and run it correctly', async () => { + // create a new action + const { body: hookAction } = await supertest + .post('/api/actions/action') + .set('kbn-xsrf', 'true') + .send(getWebHookAction()) + .expect(200); + + // create a rule with the action attached and a meta field + const ruleWithAction: CreateRulesSchema = { + ...getRuleWithWebHookAction(hookAction.id), + meta: {}, + }; + + const { body: rule } = await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(ruleWithAction) + .expect(200); + + // wait for Task Manager to execute the rule and update status + await waitFor(async () => { + const { body } = await supertest + .post(`${DETECTION_ENGINE_RULES_URL}/_find_statuses`) + .set('kbn-xsrf', 'true') + .send({ ids: [rule.id] }) + .expect(200); + return body[rule.id].current_status?.status === 'succeeded'; + }); + + // expected result for status should be 'succeeded' + const { body } = await supertest + .post(`${DETECTION_ENGINE_RULES_URL}/_find_statuses`) + .set('kbn-xsrf', 'true') + .send({ ids: [rule.id] }) + .expect(200); + expect(body[rule.id].current_status.status).to.eql('succeeded'); + }); + }); + }); +}; diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/index.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/index.ts index a480e63ff4a92..779205377621d 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/index.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/index.ts @@ -11,6 +11,7 @@ export default ({ loadTestFile }: FtrProviderContext): void => { describe('detection engine api security and spaces enabled', function () { this.tags('ciGroup1'); + loadTestFile(require.resolve('./add_actions')); loadTestFile(require.resolve('./add_prepackaged_rules')); loadTestFile(require.resolve('./create_rules')); loadTestFile(require.resolve('./create_rules_bulk')); diff --git a/x-pack/test/detection_engine_api_integration/utils.ts b/x-pack/test/detection_engine_api_integration/utils.ts index 604133a1c2dc7..4cbbc142edd40 100644 --- a/x-pack/test/detection_engine_api_integration/utils.ts +++ b/x-pack/test/detection_engine_api_integration/utils.ts @@ -557,6 +557,49 @@ export const getComplexRuleOutput = (ruleId = 'rule-1'): Partial => exceptions_list: [], }); +export const getWebHookAction = () => ({ + actionTypeId: '.webhook', + config: { + method: 'post', + url: 'http://localhost', + }, + secrets: { + user: 'example', + password: 'example', + }, + name: 'Some connector', +}); + +export const getRuleWithWebHookAction = (id: string): CreateRulesSchema => ({ + ...getSimpleRule(), + throttle: 'rule', + actions: [ + { + group: 'default', + id, + params: { + body: '{}', + }, + action_type_id: '.webhook', + }, + ], +}); + +export const getSimpleRuleOutputWithWebHookAction = (actionId: string): Partial => ({ + ...getSimpleRuleOutput(), + throttle: 'rule', + actions: [ + { + action_type_id: '.webhook', + group: 'default', + id: actionId, + params: { + body: '{}', + }, + }, + ], +}); + // Similar to ReactJs's waitFor from here: https://testing-library.com/docs/dom-testing-library/api-async#waitfor export const waitFor = async ( functionToTest: () => Promise, From e773f221a3814700d55284bc34bd4637cc7312bd Mon Sep 17 00:00:00 2001 From: Tyler Smalley Date: Wed, 26 Aug 2020 08:41:09 -0700 Subject: [PATCH 69/71] Revert "[Security Solution][Exceptions] - Improve UX for missing exception list associated with rule (#75898)" This reverts commit b9c820120202dc44296e080550e87c93bd37dd55. --- .../exceptions/add_exception_modal/index.tsx | 90 +++------- .../edit_exception_modal/index.test.tsx | 5 - .../exceptions/edit_exception_modal/index.tsx | 91 +++------- .../exceptions/error_callout.test.tsx | 160 ----------------- .../components/exceptions/error_callout.tsx | 169 ------------------ .../components/exceptions/translations.ts | 49 ----- .../exceptions/use_add_exception.test.tsx | 44 ----- .../exceptions/use_add_exception.tsx | 8 +- ...tch_or_create_rule_exception_list.test.tsx | 2 +- ...se_fetch_or_create_rule_exception_list.tsx | 8 +- .../components/exceptions/viewer/index.tsx | 2 - .../use_dissasociate_exception_list.test.tsx | 52 ------ .../rules/use_dissasociate_exception_list.tsx | 80 --------- 13 files changed, 54 insertions(+), 706 deletions(-) delete mode 100644 x-pack/plugins/security_solution/public/common/components/exceptions/error_callout.test.tsx delete mode 100644 x-pack/plugins/security_solution/public/common/components/exceptions/error_callout.tsx delete mode 100644 x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_dissasociate_exception_list.test.tsx delete mode 100644 x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_dissasociate_exception_list.tsx diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx index 21f82c6ab4c98..03051ead357c9 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx @@ -18,6 +18,7 @@ import { EuiCheckbox, EuiSpacer, EuiFormRow, + EuiCallOut, EuiText, } from '@elastic/eui'; import { Status } from '../../../../../common/detection_engine/schemas/common/schemas'; @@ -27,7 +28,6 @@ import { ExceptionListType, } from '../../../../../public/lists_plugin_deps'; import * as i18n from './translations'; -import * as sharedI18n from '../translations'; import { TimelineNonEcsData, Ecs } from '../../../../graphql/types'; import { useAppToasts } from '../../../hooks/use_app_toasts'; import { useKibana } from '../../../lib/kibana'; @@ -35,7 +35,6 @@ import { ExceptionBuilderComponent } from '../builder'; import { Loader } from '../../loader'; import { useAddOrUpdateException } from '../use_add_exception'; import { useSignalIndex } from '../../../../detections/containers/detection_engine/alerts/use_signal_index'; -import { useRuleAsync } from '../../../../detections/containers/detection_engine/rules/use_rule_async'; import { useFetchOrCreateRuleExceptionList } from '../use_fetch_or_create_rule_exception_list'; import { AddExceptionComments } from '../add_exception_comments'; import { @@ -47,7 +46,6 @@ import { entryHasNonEcsType, getMappedNonEcsValue, } from '../helpers'; -import { ErrorInfo, ErrorCallout } from '../error_callout'; import { useFetchIndexPatterns } from '../../../../detections/containers/detection_engine/rules'; export interface AddExceptionModalBaseProps { @@ -109,14 +107,13 @@ export const AddExceptionModal = memo(function AddExceptionModal({ }: AddExceptionModalProps) { const { http } = useKibana().services; const [comment, setComment] = useState(''); - const { rule: maybeRule } = useRuleAsync(ruleId); const [shouldCloseAlert, setShouldCloseAlert] = useState(false); const [shouldBulkCloseAlert, setShouldBulkCloseAlert] = useState(false); const [shouldDisableBulkClose, setShouldDisableBulkClose] = useState(false); const [exceptionItemsToAdd, setExceptionItemsToAdd] = useState< Array >([]); - const [fetchOrCreateListError, setFetchOrCreateListError] = useState(null); + const [fetchOrCreateListError, setFetchOrCreateListError] = useState(false); const { addError, addSuccess } = useAppToasts(); const { loading: isSignalIndexLoading, signalIndexName } = useSignalIndex(); const [ @@ -167,41 +164,17 @@ export const AddExceptionModal = memo(function AddExceptionModal({ }, [onRuleChange] ); - - const handleDissasociationSuccess = useCallback( - (id: string): void => { - handleRuleChange(true); - addSuccess(sharedI18n.DISSASOCIATE_LIST_SUCCESS(id)); - onCancel(); - }, - [handleRuleChange, addSuccess, onCancel] - ); - - const handleDissasociationError = useCallback( - (error: Error): void => { - addError(error, { title: sharedI18n.DISSASOCIATE_EXCEPTION_LIST_ERROR }); - onCancel(); - }, - [addError, onCancel] - ); - - const handleFetchOrCreateExceptionListError = useCallback( - (error: Error, statusCode: number | null, message: string | null) => { - setFetchOrCreateListError({ - reason: error.message, - code: statusCode, - details: message, - listListId: null, - }); + const onFetchOrCreateExceptionListError = useCallback( + (error: Error) => { + setFetchOrCreateListError(true); }, [setFetchOrCreateListError] ); - const [isLoadingExceptionList, ruleExceptionList] = useFetchOrCreateRuleExceptionList({ http, ruleId, exceptionListType, - onError: handleFetchOrCreateExceptionListError, + onError: onFetchOrCreateExceptionListError, onSuccess: handleRuleChange, }); @@ -306,9 +279,7 @@ export const AddExceptionModal = memo(function AddExceptionModal({ ]); const isSubmitButtonDisabled = useMemo( - () => - fetchOrCreateListError != null || - exceptionItemsToAdd.every((item) => item.entries.length === 0), + () => fetchOrCreateListError || exceptionItemsToAdd.every((item) => item.entries.length === 0), [fetchOrCreateListError, exceptionItemsToAdd] ); @@ -324,27 +295,19 @@ export const AddExceptionModal = memo(function AddExceptionModal({ - {fetchOrCreateListError != null && ( - - - + {fetchOrCreateListError === true && ( + +

{i18n.ADD_EXCEPTION_FETCH_ERROR}

+
)} - {fetchOrCreateListError == null && + {fetchOrCreateListError === false && (isLoadingExceptionList || isIndexPatternLoading || isSignalIndexLoading || isSignalIndexPatternLoading) && ( )} - {fetchOrCreateListError == null && + {fetchOrCreateListError === false && !isSignalIndexLoading && !isSignalIndexPatternLoading && !isLoadingExceptionList && @@ -414,21 +377,20 @@ export const AddExceptionModal = memo(function AddExceptionModal({ )} - {fetchOrCreateListError == null && ( - - {i18n.CANCEL} - - {i18n.ADD_EXCEPTION} - - - )} + + {i18n.CANCEL} + + + {i18n.ADD_EXCEPTION} + + ); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.test.tsx index c724e6a2c711f..6ff218ca06059 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.test.tsx @@ -77,7 +77,6 @@ describe('When the edit exception modal is opened', () => { ({ eui: euiLightVars, darkMode: false })}> { ({ eui: euiLightVars, darkMode: false })}> { ({ eui: euiLightVars, darkMode: false })}> { ({ eui: euiLightVars, darkMode: false })}> { ({ eui: euiLightVars, darkMode: false })}> void; onConfirm: () => void; - onRuleChange?: () => void; } const Modal = styled(EuiModal)` @@ -88,18 +83,14 @@ const ModalBodySection = styled.section` export const EditExceptionModal = memo(function EditExceptionModal({ ruleName, - ruleId, ruleIndices, exceptionItem, exceptionListType, onCancel, onConfirm, - onRuleChange, }: EditExceptionModalProps) { const { http } = useKibana().services; const [comment, setComment] = useState(''); - const { rule: maybeRule } = useRuleAsync(ruleId); - const [updateError, setUpdateError] = useState(null); const [hasVersionConflict, setHasVersionConflict] = useState(false); const [shouldBulkCloseAlert, setShouldBulkCloseAlert] = useState(false); const [shouldDisableBulkClose, setShouldDisableBulkClose] = useState(false); @@ -117,44 +108,18 @@ export const EditExceptionModal = memo(function EditExceptionModal({ 'rules' ); - const handleExceptionUpdateError = useCallback( - (error: Error, statusCode: number | null, message: string | null) => { + const onError = useCallback( + (error) => { if (error.message.includes('Conflict')) { setHasVersionConflict(true); } else { - setUpdateError({ - reason: error.message, - code: statusCode, - details: message, - listListId: exceptionItem.list_id, - }); + addError(error, { title: i18n.EDIT_EXCEPTION_ERROR }); + onCancel(); } }, - [setUpdateError, setHasVersionConflict, exceptionItem.list_id] - ); - - const handleDissasociationSuccess = useCallback( - (id: string): void => { - addSuccess(sharedI18n.DISSASOCIATE_LIST_SUCCESS(id)); - - if (onRuleChange) { - onRuleChange(); - } - - onCancel(); - }, - [addSuccess, onCancel, onRuleChange] - ); - - const handleDissasociationError = useCallback( - (error: Error): void => { - addError(error, { title: sharedI18n.DISSASOCIATE_EXCEPTION_LIST_ERROR }); - onCancel(); - }, [addError, onCancel] ); - - const handleExceptionUpdateSuccess = useCallback((): void => { + const onSuccess = useCallback(() => { addSuccess(i18n.EDIT_EXCEPTION_SUCCESS); onConfirm(); }, [addSuccess, onConfirm]); @@ -162,8 +127,8 @@ export const EditExceptionModal = memo(function EditExceptionModal({ const [{ isLoading: addExceptionIsLoading }, addOrUpdateExceptionItems] = useAddOrUpdateException( { http, - onSuccess: handleExceptionUpdateSuccess, - onError: handleExceptionUpdateError, + onSuccess, + onError, } ); @@ -257,9 +222,11 @@ export const EditExceptionModal = memo(function EditExceptionModal({ {ruleName} + {(addExceptionIsLoading || isIndexPatternLoading || isSignalIndexLoading) && ( )} + {!isSignalIndexLoading && !addExceptionIsLoading && !isIndexPatternLoading && ( <> @@ -313,18 +280,7 @@ export const EditExceptionModal = memo(function EditExceptionModal({ )} - {updateError != null && ( - - - - )} + {hasVersionConflict && ( @@ -332,21 +288,20 @@ export const EditExceptionModal = memo(function EditExceptionModal({ )} - {updateError == null && ( - - {i18n.CANCEL} - - {i18n.EDIT_EXCEPTION_SAVE_BUTTON} - - - )} + + {i18n.CANCEL} + + + {i18n.EDIT_EXCEPTION_SAVE_BUTTON} + + ); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/error_callout.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/error_callout.test.tsx deleted file mode 100644 index c9efa5e54dccf..0000000000000 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/error_callout.test.tsx +++ /dev/null @@ -1,160 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React from 'react'; -import { ThemeProvider } from 'styled-components'; -import { mountWithIntl } from 'test_utils/enzyme_helpers'; -import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; - -import { getListMock } from '../../../../common/detection_engine/schemas/types/lists.mock'; -import { useDissasociateExceptionList } from '../../../detections/containers/detection_engine/rules/use_dissasociate_exception_list'; -import { createKibanaCoreStartMock } from '../../mock/kibana_core'; -import { ErrorCallout } from './error_callout'; -import { savedRuleMock } from '../../../detections/containers/detection_engine/rules/mock'; - -jest.mock('../../../detections/containers/detection_engine/rules/use_dissasociate_exception_list'); - -const mockKibanaHttpService = createKibanaCoreStartMock().http; - -describe('ErrorCallout', () => { - const mockDissasociate = jest.fn(); - - beforeEach(() => { - (useDissasociateExceptionList as jest.Mock).mockReturnValue([false, mockDissasociate]); - }); - - it('it renders error details', () => { - const wrapper = mountWithIntl( - ({ eui: euiLightVars, darkMode: false })}> - - - ); - - expect( - wrapper.find('[data-test-subj="errorCalloutContainer"] .euiCallOutHeader__title').text() - ).toEqual('Error: error reason (500)'); - expect(wrapper.find('[data-test-subj="errorCalloutMessage"]').at(0).text()).toEqual( - 'Error fetching exception list' - ); - }); - - it('it invokes "onCancel" when cancel button clicked', () => { - const mockOnCancel = jest.fn(); - const wrapper = mountWithIntl( - ({ eui: euiLightVars, darkMode: false })}> - - - ); - - wrapper.find('[data-test-subj="errorCalloutCancelButton"]').at(0).simulate('click'); - - expect(mockOnCancel).toHaveBeenCalled(); - }); - - it('it does not render status code if not available', () => { - const wrapper = mountWithIntl( - ({ eui: euiLightVars, darkMode: false })}> - - - ); - - expect( - wrapper.find('[data-test-subj="errorCalloutContainer"] .euiCallOutHeader__title').text() - ).toEqual('Error: not found'); - expect(wrapper.find('[data-test-subj="errorCalloutMessage"]').at(0).text()).toEqual( - 'Error fetching exception list' - ); - expect(wrapper.find('[data-test-subj="errorCalloutDissasociateButton"]').exists()).toBeFalsy(); - }); - - it('it renders specific missing exceptions list error', () => { - const wrapper = mountWithIntl( - ({ eui: euiLightVars, darkMode: false })}> - - - ); - - expect( - wrapper.find('[data-test-subj="errorCalloutContainer"] .euiCallOutHeader__title').text() - ).toEqual('Error: not found (404)'); - expect(wrapper.find('[data-test-subj="errorCalloutMessage"]').at(0).text()).toEqual( - 'The associated exception list (some_uuid) no longer exists. Please remove the missing exception list to add additional exceptions to the detection rule.' - ); - expect(wrapper.find('[data-test-subj="errorCalloutDissasociateButton"]').exists()).toBeTruthy(); - }); - - it('it dissasociates list from rule when remove exception list clicked ', () => { - const wrapper = mountWithIntl( - ({ eui: euiLightVars, darkMode: false })}> - - - ); - - wrapper.find('[data-test-subj="errorCalloutDissasociateButton"]').at(0).simulate('click'); - - expect(mockDissasociate).toHaveBeenCalledWith([]); - }); -}); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/error_callout.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/error_callout.tsx deleted file mode 100644 index a2419ef16df3a..0000000000000 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/error_callout.tsx +++ /dev/null @@ -1,169 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React, { useMemo, useEffect, useState, useCallback } from 'react'; -import { - EuiButtonEmpty, - EuiAccordion, - EuiCodeBlock, - EuiButton, - EuiCallOut, - EuiText, - EuiSpacer, -} from '@elastic/eui'; - -import { HttpSetup } from '../../../../../../../src/core/public'; -import { List } from '../../../../common/detection_engine/schemas/types/lists'; -import { Rule } from '../../../detections/containers/detection_engine/rules/types'; -import * as i18n from './translations'; -import { useDissasociateExceptionList } from '../../../detections/containers/detection_engine/rules/use_dissasociate_exception_list'; - -export interface ErrorInfo { - reason: string | null; - code: number | null; - details: string | null; - listListId: string | null; -} - -export interface ErrorCalloutProps { - http: HttpSetup; - rule: Rule | null; - errorInfo: ErrorInfo; - onCancel: () => void; - onSuccess: (listId: string) => void; - onError: (arg: Error) => void; -} - -const ErrorCalloutComponent = ({ - http, - rule, - errorInfo, - onCancel, - onError, - onSuccess, -}: ErrorCalloutProps): JSX.Element => { - const [listToDelete, setListToDelete] = useState(null); - const [errorTitle, setErrorTitle] = useState(''); - const [errorMessage, setErrorMessage] = useState(i18n.ADD_EXCEPTION_FETCH_ERROR); - - const handleOnSuccess = useCallback((): void => { - onSuccess(listToDelete != null ? listToDelete.id : ''); - }, [onSuccess, listToDelete]); - - const [isDissasociatingList, handleDissasociateExceptionList] = useDissasociateExceptionList({ - http, - ruleRuleId: rule != null ? rule.rule_id : '', - onSuccess: handleOnSuccess, - onError, - }); - - const canDisplay404Actions = useMemo( - (): boolean => - errorInfo.code === 404 && - rule != null && - listToDelete != null && - handleDissasociateExceptionList != null, - [errorInfo.code, listToDelete, handleDissasociateExceptionList, rule] - ); - - useEffect((): void => { - // Yes, it's redundant, unfortunately typescript wasn't picking up - // that `listToDelete` is checked in canDisplay404Actions - if (canDisplay404Actions && listToDelete != null) { - setErrorMessage(i18n.ADD_EXCEPTION_FETCH_404_ERROR(listToDelete.id)); - } - - setErrorTitle(`${errorInfo.reason}${errorInfo.code != null ? ` (${errorInfo.code})` : ''}`); - }, [errorInfo.reason, errorInfo.code, listToDelete, canDisplay404Actions]); - - const handleDissasociateList = useCallback((): void => { - // Yes, it's redundant, unfortunately typescript wasn't picking up - // that `handleDissasociateExceptionList` and `list` are checked in - // canDisplay404Actions - if ( - canDisplay404Actions && - rule != null && - listToDelete != null && - handleDissasociateExceptionList != null - ) { - const exceptionLists = (rule.exceptions_list ?? []).filter( - ({ id }) => id !== listToDelete.id - ); - - handleDissasociateExceptionList(exceptionLists); - } - }, [handleDissasociateExceptionList, listToDelete, canDisplay404Actions, rule]); - - useEffect((): void => { - if (errorInfo.code === 404 && rule != null && rule.exceptions_list != null) { - const [listFound] = rule.exceptions_list.filter( - ({ id, list_id: listId }) => - (errorInfo.details != null && errorInfo.details.includes(id)) || - errorInfo.listListId === listId - ); - setListToDelete(listFound); - } - }, [rule, errorInfo.details, errorInfo.code, errorInfo.listListId]); - - return ( - - -

{errorMessage}

-
- - {listToDelete != null && ( - -

{i18n.MODAL_ERROR_ACCORDION_TEXT}

- - } - > - - {JSON.stringify(listToDelete)} - -
- )} - - - {i18n.CANCEL} - - {canDisplay404Actions && ( - - {i18n.CLEAR_EXCEPTIONS_LABEL} - - )} -
- ); -}; - -ErrorCalloutComponent.displayName = 'ErrorCalloutComponent'; - -export const ErrorCallout = React.memo(ErrorCalloutComponent); - -ErrorCallout.displayName = 'ErrorCallout'; diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/translations.ts b/x-pack/plugins/security_solution/public/common/components/exceptions/translations.ts index 484a3d593026e..13e9d0df549f8 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/translations.ts +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/translations.ts @@ -190,52 +190,3 @@ export const TOTAL_ITEMS_FETCH_ERROR = i18n.translate( defaultMessage: 'Error getting exception item totals', } ); - -export const CLEAR_EXCEPTIONS_LABEL = i18n.translate( - 'xpack.securitySolution.exceptions.clearExceptionsLabel', - { - defaultMessage: 'Remove Exception List', - } -); - -export const ADD_EXCEPTION_FETCH_404_ERROR = (listId: string) => - i18n.translate('xpack.securitySolution.exceptions.fetch404Error', { - values: { listId }, - defaultMessage: - 'The associated exception list ({listId}) no longer exists. Please remove the missing exception list to add additional exceptions to the detection rule.', - }); - -export const ADD_EXCEPTION_FETCH_ERROR = i18n.translate( - 'xpack.securitySolution.exceptions.fetchError', - { - defaultMessage: 'Error fetching exception list', - } -); - -export const ERROR = i18n.translate('xpack.securitySolution.exceptions.errorLabel', { - defaultMessage: 'Error', -}); - -export const CANCEL = i18n.translate('xpack.securitySolution.exceptions.cancelLabel', { - defaultMessage: 'Cancel', -}); - -export const MODAL_ERROR_ACCORDION_TEXT = i18n.translate( - 'xpack.securitySolution.exceptions.modalErrorAccordionText', - { - defaultMessage: 'Show rule reference information:', - } -); - -export const DISSASOCIATE_LIST_SUCCESS = (id: string) => - i18n.translate('xpack.securitySolution.exceptions.dissasociateListSuccessText', { - values: { id }, - defaultMessage: 'Exception list ({id}) has successfully been removed', - }); - -export const DISSASOCIATE_EXCEPTION_LIST_ERROR = i18n.translate( - 'xpack.securitySolution.exceptions.dissasociateExceptionListError', - { - defaultMessage: 'Failed to remove exception list', - } -); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/use_add_exception.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/use_add_exception.test.tsx index 46923e07d225a..6611ee2385d10 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/use_add_exception.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/use_add_exception.test.tsx @@ -148,50 +148,6 @@ describe('useAddOrUpdateException', () => { }); }); - it('invokes "onError" if call to add exception item fails', async () => { - const mockError = new Error('error adding item'); - - addExceptionListItem = jest - .spyOn(listsApi, 'addExceptionListItem') - .mockRejectedValue(mockError); - - await act(async () => { - const { rerender, result, waitForNextUpdate } = render(); - const addOrUpdateItems = await waitForAddOrUpdateFunc({ - rerender, - result, - waitForNextUpdate, - }); - if (addOrUpdateItems) { - addOrUpdateItems(...addOrUpdateItemsArgs); - } - await waitForNextUpdate(); - expect(onError).toHaveBeenCalledWith(mockError, null, null); - }); - }); - - it('invokes "onError" if call to update exception item fails', async () => { - const mockError = new Error('error updating item'); - - updateExceptionListItem = jest - .spyOn(listsApi, 'updateExceptionListItem') - .mockRejectedValue(mockError); - - await act(async () => { - const { rerender, result, waitForNextUpdate } = render(); - const addOrUpdateItems = await waitForAddOrUpdateFunc({ - rerender, - result, - waitForNextUpdate, - }); - if (addOrUpdateItems) { - addOrUpdateItems(...addOrUpdateItemsArgs); - } - await waitForNextUpdate(); - expect(onError).toHaveBeenCalledWith(mockError, null, null); - }); - }); - describe('when alertIdToClose is not passed in', () => { it('should not update the alert status', async () => { await act(async () => { diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/use_add_exception.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/use_add_exception.tsx index be289b0e85e66..9d45a411b5130 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/use_add_exception.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/use_add_exception.tsx @@ -42,7 +42,7 @@ export type ReturnUseAddOrUpdateException = [ export interface UseAddOrUpdateExceptionProps { http: HttpStart; - onError: (arg: Error, code: number | null, message: string | null) => void; + onError: (arg: Error) => void; onSuccess: () => void; } @@ -157,11 +157,7 @@ export const useAddOrUpdateException = ({ } catch (error) { if (isSubscribed) { setIsLoading(false); - if (error.body != null) { - onError(error, error.body.status_code, error.body.message); - } else { - onError(error, null, null); - } + onError(error); } } }; diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/use_fetch_or_create_rule_exception_list.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/use_fetch_or_create_rule_exception_list.test.tsx index f20a58b9ffa36..39d88bd8e4724 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/use_fetch_or_create_rule_exception_list.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/use_fetch_or_create_rule_exception_list.test.tsx @@ -379,7 +379,7 @@ describe('useFetchOrCreateRuleExceptionList', () => { await waitForNextUpdate(); await waitForNextUpdate(); expect(onError).toHaveBeenCalledTimes(1); - expect(onError).toHaveBeenCalledWith(error, null, null); + expect(onError).toHaveBeenCalledWith(error); }); }); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/use_fetch_or_create_rule_exception_list.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/use_fetch_or_create_rule_exception_list.tsx index 944631d4e9fb5..0d367e03a799f 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/use_fetch_or_create_rule_exception_list.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/use_fetch_or_create_rule_exception_list.tsx @@ -30,7 +30,7 @@ export interface UseFetchOrCreateRuleExceptionListProps { http: HttpStart; ruleId: Rule['id']; exceptionListType: ExceptionListSchema['type']; - onError: (arg: Error, code: number | null, message: string | null) => void; + onError: (arg: Error) => void; onSuccess?: (ruleWasChanged: boolean) => void; } @@ -179,11 +179,7 @@ export const useFetchOrCreateRuleExceptionList = ({ if (isSubscribed) { setIsLoading(false); setExceptionList(null); - if (error.body != null) { - onError(error, error.body.status_code, error.body.message); - } else { - onError(error, null, null); - } + onError(error); } } } diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/index.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/index.tsx index c97895cdfe236..7482068454a97 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/index.tsx @@ -322,13 +322,11 @@ const ExceptionsViewerComponent = ({ exceptionListTypeToEdit != null && ( )} diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_dissasociate_exception_list.test.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_dissasociate_exception_list.test.tsx deleted file mode 100644 index 6b1938655dc33..0000000000000 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_dissasociate_exception_list.test.tsx +++ /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; - * you may not use this file except in compliance with the Elastic License. - */ - -import { act, renderHook } from '@testing-library/react-hooks'; - -import { createKibanaCoreStartMock } from '../../../../common/mock/kibana_core'; - -import * as api from './api'; -import { ruleMock } from './mock'; -import { - ReturnUseDissasociateExceptionList, - UseDissasociateExceptionListProps, - useDissasociateExceptionList, -} from './use_dissasociate_exception_list'; - -const mockKibanaHttpService = createKibanaCoreStartMock().http; - -describe('useDissasociateExceptionList', () => { - const onError = jest.fn(); - const onSuccess = jest.fn(); - - beforeEach(() => { - jest.spyOn(api, 'patchRule').mockResolvedValue(ruleMock); - }); - - afterEach(() => { - jest.clearAllMocks(); - }); - - test('initializes hook', async () => { - await act(async () => { - const { result, waitForNextUpdate } = renderHook< - UseDissasociateExceptionListProps, - ReturnUseDissasociateExceptionList - >(() => - useDissasociateExceptionList({ - http: mockKibanaHttpService, - ruleRuleId: 'rule_id', - onError, - onSuccess, - }) - ); - - await waitForNextUpdate(); - - expect(result.current).toEqual([false, null]); - }); - }); -}); diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_dissasociate_exception_list.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_dissasociate_exception_list.tsx deleted file mode 100644 index dffba3e6e0436..0000000000000 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_dissasociate_exception_list.tsx +++ /dev/null @@ -1,80 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { useEffect, useState, useRef } from 'react'; - -import { HttpStart } from '../../../../../../../../src/core/public'; -import { List } from '../../../../../common/detection_engine/schemas/types/lists'; -import { patchRule } from './api'; - -type Func = (lists: List[]) => void; -export type ReturnUseDissasociateExceptionList = [boolean, Func | null]; - -export interface UseDissasociateExceptionListProps { - http: HttpStart; - ruleRuleId: string; - onError: (arg: Error) => void; - onSuccess: () => void; -} - -/** - * Hook for removing an exception list reference from a rule - * - * @param http Kibana http service - * @param ruleRuleId a rule_id (NOT id) - * @param onError error callback - * @param onSuccess success callback - * - */ -export const useDissasociateExceptionList = ({ - http, - ruleRuleId, - onError, - onSuccess, -}: UseDissasociateExceptionListProps): ReturnUseDissasociateExceptionList => { - const [isLoading, setLoading] = useState(false); - const dissasociateList = useRef(null); - - useEffect(() => { - let isSubscribed = true; - const abortCtrl = new AbortController(); - - const dissasociateListFromRule = (id: string) => async ( - exceptionLists: List[] - ): Promise => { - try { - if (isSubscribed) { - setLoading(true); - - await patchRule({ - ruleProperties: { - rule_id: id, - exceptions_list: exceptionLists, - }, - signal: abortCtrl.signal, - }); - - onSuccess(); - setLoading(false); - } - } catch (err) { - if (isSubscribed) { - setLoading(false); - onError(err); - } - } - }; - - dissasociateList.current = dissasociateListFromRule(ruleRuleId); - - return (): void => { - isSubscribed = false; - abortCtrl.abort(); - }; - }, [http, ruleRuleId, onError, onSuccess]); - - return [isLoading, dissasociateList.current]; -}; From 5a9d227eee1b53673c6445c00746a0846bb69e48 Mon Sep 17 00:00:00 2001 From: Tyler Smalley Date: Wed, 26 Aug 2020 08:03:12 -0700 Subject: [PATCH 70/71] Downloads Chrome 84 and adds to PATH Signed-off-by: Tyler Smalley --- src/dev/ci_setup/setup_env.sh | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/src/dev/ci_setup/setup_env.sh b/src/dev/ci_setup/setup_env.sh index 72ec73ad810e6..a82ca011b8a5d 100644 --- a/src/dev/ci_setup/setup_env.sh +++ b/src/dev/ci_setup/setup_env.sh @@ -10,6 +10,7 @@ installNode=$1 dir="$(pwd)" cacheDir="$HOME/.kibana" +downloads="$cacheDir/downloads" RED='\033[0;31m' C_RESET='\033[0m' # Reset color @@ -133,6 +134,26 @@ export CYPRESS_DOWNLOAD_MIRROR="https://us-central1-elastic-kibana-184716.cloudf export CHECKS_REPORTER_ACTIVE=false +### +### Download Chrome and install to this shell +### + +# Available using the version information search at https://omahaproxy.appspot.com/ +chromeVersion=84 + +mkdir -p "$downloads" + +if [ -d $cacheDir/chrome-$chromeVersion/chrome-linux ]; then + echo " -- Chrome already downloaded and extracted" +else + mkdir -p "$cacheDir/chrome-$chromeVersion" + + echo " -- Downloading and extracting Chrome" + curl -o "$downloads/chrome.zip" -L "https://us-central1-elastic-kibana-184716.cloudfunctions.net/kibana-ci-proxy-cache/chrome_$chromeVersion.zip" + unzip -o "$downloads/chrome.zip" -d "$cacheDir/chrome-$chromeVersion" + export PATH="$cacheDir/chrome-$chromeVersion/chrome-linux:$PATH" +fi + # This is mainly for release-manager builds, which run in an environment that doesn't have Chrome installed if [[ "$(which google-chrome-stable)" || "$(which google-chrome)" ]]; then echo "Chrome detected, setting DETECT_CHROMEDRIVER_VERSION=true" From 1ca76514933463220e32f4b246c5ba14a553d9a9 Mon Sep 17 00:00:00 2001 From: spalger Date: Wed, 26 Aug 2020 09:28:22 -0700 Subject: [PATCH 71/71] Revert "Downloads Chrome 84 and adds to PATH" This reverts commit 5a9d227eee1b53673c6445c00746a0846bb69e48. --- src/dev/ci_setup/setup_env.sh | 21 --------------------- 1 file changed, 21 deletions(-) diff --git a/src/dev/ci_setup/setup_env.sh b/src/dev/ci_setup/setup_env.sh index a82ca011b8a5d..72ec73ad810e6 100644 --- a/src/dev/ci_setup/setup_env.sh +++ b/src/dev/ci_setup/setup_env.sh @@ -10,7 +10,6 @@ installNode=$1 dir="$(pwd)" cacheDir="$HOME/.kibana" -downloads="$cacheDir/downloads" RED='\033[0;31m' C_RESET='\033[0m' # Reset color @@ -134,26 +133,6 @@ export CYPRESS_DOWNLOAD_MIRROR="https://us-central1-elastic-kibana-184716.cloudf export CHECKS_REPORTER_ACTIVE=false -### -### Download Chrome and install to this shell -### - -# Available using the version information search at https://omahaproxy.appspot.com/ -chromeVersion=84 - -mkdir -p "$downloads" - -if [ -d $cacheDir/chrome-$chromeVersion/chrome-linux ]; then - echo " -- Chrome already downloaded and extracted" -else - mkdir -p "$cacheDir/chrome-$chromeVersion" - - echo " -- Downloading and extracting Chrome" - curl -o "$downloads/chrome.zip" -L "https://us-central1-elastic-kibana-184716.cloudfunctions.net/kibana-ci-proxy-cache/chrome_$chromeVersion.zip" - unzip -o "$downloads/chrome.zip" -d "$cacheDir/chrome-$chromeVersion" - export PATH="$cacheDir/chrome-$chromeVersion/chrome-linux:$PATH" -fi - # This is mainly for release-manager builds, which run in an environment that doesn't have Chrome installed if [[ "$(which google-chrome-stable)" || "$(which google-chrome)" ]]; then echo "Chrome detected, setting DETECT_CHROMEDRIVER_VERSION=true"